Progressive delivery
Real configs, community demos, verification steps, and observable effects for multi-gray configuration and governance releases.
Progressive delivery separates edit state from runtime state. Rules and configuration are edited and validated first; immutable releases become the client-visible active view.
Applies to
| Object | Release unit | Client reads |
|---|---|---|
| Configuration | Config File Release | GetConfigFile / long-poll published versions |
| Governance | Rule Release | Discover active-release cache views |
Services, MCP servers, and A2A agents use registration/discovery and revisions—not the same draft → active release lifecycle.
Standard flow
- Edit draft — change config/rule body in Console/API; runtime unchanged.
- Pre-publish checks — auth, parameters, resource existence.
- Choose scope — full or gray (define client labels / environments first).
- Observe — client adoption, business errors, History.
- Finish or roll back — full publish, or stop gray / roll back per release.
A. Configuration multi-gray (run this first)
Actual configuration
One file can keep 1 active normal + multiple active grays (keyed by releaseName). Gray publish body:
{
"namespace": "default",
"group": "docs-multi-gray",
"file_name": "app.yaml",
"name": "gray-env-a",
"release_description": "gray for env=gray-a",
"release_type": "gray",
"beta_labels": [{
"key": "env",
"value": { "type": 0, "value": "gray-a", "value_type": 0 }
}]
}Full publish uses release_type=normal without beta_labels. Sample bodies:
| Release | content |
|---|---|
| Formal | mode: baseline / version: 1 |
| Gray A | mode: gray-a / cohort: env=gray-a |
| Gray B | mode: gray-b / cohort: env=gray-b |
See Configuration guide for semantics.
Community demo
| Entry | What it does |
|---|---|
| Website script | website/scripts/demo-config-multi-gray.sh — publish 1 normal + 2 grays, then assert client pulls |
| Control-plane smoke | pole-control-plane/console/web/scripts/smoke-configuration-flow.mjs |
| Docs GIF | Client curl demo on the configuration guide |
# from the website repo root
./scripts/demo-config-multi-gray.shEnv: POLE_BASE_URL (Console, default http://pole.localhost), POLE_CLIENT_URL (client HTTP API, default http://127.0.0.1:8090).
How to verify
CLIENT="${POLE_CLIENT_URL:-http://127.0.0.1:8090}"
FILE_Q='namespace=default&group=docs-multi-gray&fileName=app.yaml&version=0'
curl -sS "$CLIENT/v1/GetConfigFile?$FILE_Q" \
| jq '{name:.file.name, type:.file.release_type, content:.file.content}'
curl -sS "$CLIENT/v1/GetConfigFile?$FILE_Q&tags=env%3Dgray-a" | jq '{name:.file.name, type:.file.release_type, content:.file.content}'
curl -sS "$CLIENT/v1/GetConfigFile?$FILE_Q&tags=env%3Dgray-b" | jq '{name:.file.name, type:.file.release_type, content:.file.content}'Console: Configuration → group docs-multi-gray → app.yaml → Release history → Gray releases.
Expected effects
| Client tags | release_type | Content |
|---|---|---|
(none) or env=other | normal | mode: baseline |
env=gray-a | gray | mode: gray-a |
env=gray-b | gray | mode: gray-b |
- Full publish does not end active grays.
- Stop-gray is per
releaseName. - Multi-match → newest by version → mtime.
B. Governance gray (route / rate limit / lane)
Actual configuration
Rule body and release are separate. Shapes from community e2e fixtures (pole-control-plane/test/e2e/internal/e2e/fixtures.go):
Route — header x-tenant=vip → destination label version=v1:
{
"name": "vip-route-demo",
"enable": true,
"priority": 10,
"routing_config": {
"@type": "type.googleapis.com/v1.RuleRoutingConfig",
"caller": { "namespace": "default", "service": "order" },
"callee": { "namespace": "default", "service": "payment" },
"rules": [{
"name": "vip-route",
"arguments": {
"matchMode": "AND",
"arguments": [{
"type": "HEADER", "key": "x-tenant",
"value": { "type": "EXACT", "value": "vip", "value_type": "TEXT" }
}]
},
"destinations": [{
"namespace": "default", "service": "payment",
"name": "primary", "weight": 100,
"labels": {
"version": { "type": "EXACT", "value": "v1", "value_type": "TEXT" }
}
}]
}]
}
}Rate limit — POST /api/v1/payments + x-tenant=vip, local QPS 120, REJECT:
{
"name": "vip-rl-demo",
"namespace": "default",
"service": "payment",
"enable": true,
"priority": 10,
"rules": [{
"method": { "type": "EXACT", "value": "POST /api/v1/payments" },
"arguments": [{
"type": "HEADER", "key": "x-tenant",
"value": { "type": "EXACT", "value": "vip", "value_type": "TEXT" }
}],
"amounts": [{ "validDuration": "1s", "maxAmount": 120 }],
"resource": "QPS", "type": "LOCAL", "action": "REJECT",
"failover": "FAILOVER_LOCAL"
}]
}Lane — after entry match, destinations carry lane=blue (see fixture LaneGroup; traffic match uses header x-lane=blue).
Gray release body (only labeled clients see the new rule):
{
"rule_id": "<id>",
"rule_name": "vip-route-demo",
"resource": "RouteRules",
"version": 2,
"release_name": "e2e-gray",
"release_type": "ReleaseTypeGray",
"client_labels": [{
"key": "e2e-gray",
"value": { "type": "EXACT", "value": "true", "value_type": "TEXT" }
}],
"active": true
}Full release: ReleaseTypeNormal without client_labels.
Community demo
| Entry | What it does |
|---|---|
| e2e fixtures | fixtures.go — RouteRule / RateLimitRule / LaneGroup / GrayRuleRelease |
| e2e tests | test/e2e/console_api, test/e2e/client |
| Console guides | Route, Rate limit, Lane |
# in pole-control-plane (needs a reachable control plane for e2e)
CGO_ENABLED=0 go test -tags=e2e ./test/e2e/console_api/... -count=1
CGO_ENABLED=0 go test -tags=e2e ./test/e2e/client/... -count=1How to verify
- Console: Governance → create route/rate-limit/lane → publish gray with
client_labels. - Or HTTP:
POST /naming/v1/routings(orratelimits/lane/groups) →POST .../releases. - Client Discover:
curl -sS -X POST "$CLIENT/naming/v1/Discover" \
-H 'Content-Type: application/json' \
-d '{
"type": "CUSTOM_ROUTE_RULE",
"service": { "namespace": "default", "name": "payment" },
"filter": {}
}' | jq .Use RATE_LIMIT for rate-limit rules. Labeled clients should see the gray release; others keep the formal one.
Expected effects
| Rule | Trigger | Effect |
|---|---|---|
| Route | Header x-tenant: vip | Traffic to version=v1 instances; weights sum to 100% |
| Rate limit | VIP path QPS > 120/s | Matched requests REJECT; other tenants unaffected |
| Lane | Entry matches lane rule | Downstream only lane=blue destinations |
| Rule gray | Client label e2e-gray=true | Discover returns gray rule; unlabeled clients stay on formal |
After stop-gray / rollback, Discover no longer shows that release name (or reverts to previous active).
Checklist
- Correct Namespace (not accidental production)
- Targets exist; revisions are traceable
- Publisher has the matching Publish permission
- Progressive scope is reversible (config by
releaseName, governance byclient_labels) - Verified with client APIs (
GetConfigFile/Discover), not Console lists alone - Rollback path ready; History shows the change
Anti-patterns
- Writing “effective” data directly into the database.
- Treating unpublished drafts as runtime truth.
- Screenshotting Console badges without client pull / Discover checks.
- Full rollout with no observation window.
- Letting Pole Agent auto-publish or delete.