Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,37 @@ With `--yes`, table mode still writes the plan before the request. A successful
one valid JSON document and preserves the existing top-level `status` and `data` fields while adding
the top-level `operation` plan.

### Config synchronization contract

`config diff` and `config apply` use a conservative merge contract. Source-only keys are created,
keys whose source value or comment differs are updated, matching keys are unchanged, and target-only
keys are preserved. An empty source is therefore a successful no-op, not a request to empty the
target. The CLI does not currently provide `--prune`; deletion requires a separate explicit config
deletion workflow. If a Portal version reports delete operations from the synchronize diff endpoint,
the CLI rejects the plan and does not call `items/synchronize`.

Both table and JSON output report the source and target scopes plus `create`, `update`, `delete`, and
`unchanged` counts. The JSON contract also reports `strategy: "merge"` and
`targetOnlyBehavior: "preserve"`. It never includes config values in the diff result, apply plan, or
apply result.

A standalone `config diff` is advisory; it does not create a plan artifact for a later invocation.
`config apply` captures its own fully paginated source snapshot, assesses that exact snapshot through
`items/diff`, captures the fully paginated target state, and builds the detailed mutation plan from
the returned change set. After approval, it reads the target again and repeats the assessment with
the same captured source snapshot. If either the target item state or assessment changed, the
command returns `stale_plan` and sends no synchronize request. The initial approval intentionally
occurs before these preflight reads; the detailed approval covers the resulting change counts. If
all counts are zero, the command returns the deterministic `data.result: "no-op"` success response
without calling `items/synchronize`.

This stale-plan check is optimistic and best-effort. The current Apollo `items/synchronize` OpenAPI
contract has no target revision, ETag, or conditional-write precondition, so a target write after
the final check can still race with synchronization. Eliminating that window requires a
contract-first Apollo OpenAPI and server change that validates the target revision atomically with
the item update. Callers that require exclusive writes must coordinate them outside the current CLI
workflow.

## OpenAPI behavior

The first v0 implementation uses a small generic HTTP client instead of a generated SDK. This keeps
Expand Down
10 changes: 10 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,16 @@ namespace 创建只会在首次批准后发送只读预检请求。如果 Apollo

传入 `--yes` 时,table 模式仍会在请求前输出计划。成功的 JSON 输出仍是一个完整 JSON 文档,并保留现有顶层 `status` 和 `data` 字段,同时新增顶层 `operation` 计划。

### 配置同步约定

`config diff` 和 `config apply` 采用保守合并约定:只存在于源端的 key 会被创建,源端 value 或 comment 不同的 key 会被更新,相同 key 保持不变,只存在于目标端的 key 会被保留。因此,空源端会得到成功的 no-op,而不会清空目标端。CLI 当前不提供 `--prune`;删除必须通过独立、显式的配置删除流程完成。如果某个 Portal 版本通过同步 diff 接口返回删除操作,CLI 会拒绝该计划,并且不会调用 `items/synchronize`。

table 和 JSON 输出都会给出源端与目标端 scope,以及 `create`、`update`、`delete`、`unchanged` 计数。JSON 约定还会返回 `strategy: "merge"` 和 `targetOnlyBehavior: "preserve"`。diff 结果、apply 计划和 apply 结果都不会包含配置 value。

单独执行的 `config diff` 仅供参考,不会生成可供后续调用消费的计划制品。`config apply` 会自行捕获完整分页的源端快照,用这份快照调用 `items/diff`,同时捕获完整分页的目标端状态,并根据返回的变更集生成详细变更计划。批准后,CLI 会重新读取目标端,并使用同一份已捕获的源端快照再次评估;如果目标端配置状态或评估结果发生变化,命令会返回 `stale_plan`,且不会发送同步请求。首次批准有意发生在这些预检读取之前,第二次详细批准则确认实际变更计数。如果所有变更计数均为零,命令会返回确定性的 `data.result: "no-op"` 成功结果,并且不会调用 `items/synchronize`。

该 stale-plan 检查属于乐观、best-effort 防护。当前 Apollo `items/synchronize` OpenAPI 约定没有目标 revision、ETag 或条件写入前置条件,因此最终检查之后发生的目标端写入仍可能与同步竞争。要彻底消除这个窗口,需要先修改 Apollo OpenAPI contract,再由服务端在更新配置的同一原子操作中校验目标 revision。对写入互斥有严格要求的调用方,在当前 CLI 工作流之外仍需自行协调。

## OpenAPI 行为

第一版 v0 实现使用一个小型通用 HTTP client,而不是生成式 SDK。这样可以让 CLI 与 Apollo 服务端仓库解耦,同时仍然保证所有内置资源命令都限定在 `/openapi/v1/*`。
Expand Down
10 changes: 6 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,10 @@ pub enum ConfigCommand {
#[arg(long, help = "Operator for consumer-token mode")]
operator: Option<String>,
},
/// Compare a namespace against another target.
/// Preview a conservative namespace merge into another target.
#[command(
override_usage = "apollo config diff [OPTIONS] --env <ENV> --app <APP> [--cluster <CLUSTER>] [--namespace <NAMESPACE>] --target-env <TARGET_ENV> [--target-cluster <TARGET_CLUSTER>] [--target-namespace <TARGET_NAMESPACE>]"
override_usage = "apollo config diff [OPTIONS] --env <ENV> --app <APP> [--cluster <CLUSTER>] [--namespace <NAMESPACE>] --target-env <TARGET_ENV> [--target-cluster <TARGET_CLUSTER>] [--target-namespace <TARGET_NAMESPACE>]",
after_help = "Conservative merge preserves target-only keys. This preview is advisory; config apply captures and verifies its own snapshot."
)]
Diff {
#[command(flatten)]
Expand All @@ -342,9 +343,10 @@ pub enum ConfigCommand {
#[arg(long, help = "Target namespace, defaults to source namespace")]
target_namespace: Option<String>,
},
/// Sync namespace configuration items to another target.
/// Conservatively merge namespace items into another target.
#[command(
override_usage = "apollo config apply [OPTIONS] --env <ENV> --app <APP> [--cluster <CLUSTER>] [--namespace <NAMESPACE>] --target-env <TARGET_ENV> [--target-cluster <TARGET_CLUSTER>] [--target-namespace <TARGET_NAMESPACE>]"
override_usage = "apollo config apply [OPTIONS] --env <ENV> --app <APP> [--cluster <CLUSTER>] [--namespace <NAMESPACE>] --target-env <TARGET_ENV> [--target-cluster <TARGET_CLUSTER>] [--target-namespace <TARGET_NAMESPACE>]",
after_help = "Creates and updates source keys, preserves target-only keys, and sends no mutation when the assessed change set is empty. Deletion requires a separate explicit config delete."
)]
Apply {
#[command(flatten)]
Expand Down
Loading
Loading