Skip to content

Commit b8975a7

Browse files
committed
feat(cd): add reconciliation subject and events
A reconciliation is a convergence between a declared desired state and the observed state of a target. It is what a controller performs when it brings a target into line with a revision it has been told to converge on. This came from a DataOps request in which a schema change reached production through a GitOps controller, and nothing in the event stream said the controller had done anything. environment.modified reports that a mutation occurred but carries none of the relation that defines a control loop: which desired state was being converged to, whether the target had drifted from it, and whether the convergence closed the gap. Without that a consumer cannot distinguish a target that converged cleanly from one that was changed by hand, nor tell which revision the running state corresponds to. The subject is defined over the declared-versus-observed relation generally rather than over cluster convergence specifically. A controller reconciling a cluster to a commit and a process reconciling a data store to a declared contract are the same relation. Two predicates: started and finished. There is no queued predicate, since controllers converge continuously rather than accepting discrete jobs into a queue. A cycle that finds nothing to do emits nothing, which keeps the subject an occurrence model and avoids turning a continuously polling controller into a heartbeat. Event versions start at 0.1.0-draft. Signed-off-by: Dadisi Sanyika <dadisi@solduara.com>
1 parent 00b986b commit b8975a7

5 files changed

Lines changed: 493 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"context": {
3+
"specversion": "0.6.0-draft",
4+
"id": "271069a8-fc18-44f1-b38f-9d70a1695819",
5+
"chainId": "4c8cb7dd-3448-41de-8768-eec704e2829b",
6+
"source": "/gitops/argocd",
7+
"type": "dev.cdevents.reconciliation.finished.0.1.0-draft",
8+
"timestamp": "2026-07-27T17:32:10Z",
9+
"schemaUri": "https://myorg.com/schema/custom",
10+
"links": [
11+
{
12+
"linkType": "RELATION",
13+
"linkKind": "TRIGGER",
14+
"target": {
15+
"contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce"
16+
},
17+
"tags": {
18+
"foo1": "bar",
19+
"foo2": "bar"
20+
}
21+
},
22+
{
23+
"linkType": "PATH",
24+
"from": {
25+
"contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce"
26+
},
27+
"tags": {
28+
"foo1": "bar",
29+
"foo2": "bar"
30+
}
31+
},
32+
{
33+
"linkType": "END",
34+
"from": {
35+
"contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce"
36+
},
37+
"tags": {
38+
"foo1": "bar",
39+
"foo2": "bar"
40+
}
41+
}
42+
]
43+
},
44+
"subject": {
45+
"id": "argocd-sync-00481",
46+
"source": "/gitops/argocd",
47+
"content": {
48+
"desiredState": {
49+
"id": "a8f3c21",
50+
"source": "/scm/github/orders-svc"
51+
},
52+
"target": {
53+
"id": "prod",
54+
"source": "/clusters/prod",
55+
"type": "cluster"
56+
},
57+
"driftDetected": true,
58+
"changed": true,
59+
"outcome": "success"
60+
}
61+
}
62+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"context": {
3+
"specversion": "0.6.0-draft",
4+
"id": "271069a8-fc18-44f1-b38f-9d70a1695819",
5+
"chainId": "4c8cb7dd-3448-41de-8768-eec704e2829b",
6+
"source": "/gitops/argocd",
7+
"type": "dev.cdevents.reconciliation.started.0.1.0-draft",
8+
"timestamp": "2026-07-27T17:32:10Z",
9+
"schemaUri": "https://myorg.com/schema/custom",
10+
"links": [
11+
{
12+
"linkType": "RELATION",
13+
"linkKind": "TRIGGER",
14+
"target": {
15+
"contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce"
16+
},
17+
"tags": {
18+
"foo1": "bar",
19+
"foo2": "bar"
20+
}
21+
},
22+
{
23+
"linkType": "PATH",
24+
"from": {
25+
"contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce"
26+
},
27+
"tags": {
28+
"foo1": "bar",
29+
"foo2": "bar"
30+
}
31+
},
32+
{
33+
"linkType": "END",
34+
"from": {
35+
"contextId": "5328c37f-bb7e-4bb7-84ea-9f5f85e4a7ce"
36+
},
37+
"tags": {
38+
"foo1": "bar",
39+
"foo2": "bar"
40+
}
41+
}
42+
]
43+
},
44+
"subject": {
45+
"id": "argocd-sync-00481",
46+
"source": "/gitops/argocd",
47+
"content": {
48+
"desiredState": {
49+
"id": "a8f3c21",
50+
"source": "/scm/github/orders-svc"
51+
},
52+
"target": {
53+
"id": "prod",
54+
"source": "/clusters/prod",
55+
"type": "cluster"
56+
},
57+
"driftDetected": true
58+
}
59+
}
60+
}

continuous-deployment.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ Continuous Deployment (CD) events are related to continuous deployment pipelines
1616

1717
This specification defines two subjects in this stage: `environment` and `service`. The term `service` is used to represent a running Artifact. A `service` can represent a binary that is running, a daemon, an application, a docker container. The term `environment` represent any platform which has all the means to run a `service`.
1818

19+
Some systems reach a target state by converging toward it rather than by applying a discrete deployment. A [`reconciliation`](#reconciliation) is one such convergence between a declared desired state and the observed state of a target.
20+
1921
| Subject | Description | Predicates |
2022
|---------|-------------|------------|
2123
| [`environment`](#environment) | An environment where to run services | [`created`](#environment-created), [`modified`](#environment-modified), [`deleted`](#environment-deleted)|
2224
| [`service`](#service) | A service | [`deployed`](#service-deployed), [`upgraded`](#service-upgraded), [`rolledback`](#service-rolledback), [`removed`](#service-removed), [`published`](#service-published)|
25+
| [`reconciliation`](#reconciliation) | A convergence between a declared desired state and the observed state of a target | [`started`](#reconciliation-started), [`finished`](#reconciliation-finished)|
2326

2427
### `environment`
2528

@@ -43,6 +46,27 @@ A `service` can represent for example a binary that is running, a daemon, an app
4346
| environment | `Object` ([`environment`](#environment)) | Reference for the environment where the service runs | `{"id": "1234"}`, `{"id": "maven123, "source": "tekton-dev-123"}` |
4447
| artifactId | `Purl` | Identifier of the artifact deployed with this service | `pkg:oci/myapp@sha256%3A0b31b1c02ff458ad9b7b81cbdf8f028bd54699fa151f221d1e8de6817db93427`, `pkg:golang/mygit.com/myorg/myapp@234fd47e07d1004f0aed9c` |
4548

49+
### `reconciliation`
50+
51+
A [`reconciliation`](#reconciliation) is a convergence between a declared desired state and the observed state of a target. It is what a controller performs when it brings a target into line with a revision it has been told to converge on.
52+
53+
The relation is what defines it: which desired state was being converged to, whether the target had drifted from it, and whether the convergence closed the gap. `environment.modified` reports that a mutation occurred but carries none of that, so a consumer cannot distinguish a target that converged cleanly from one that was changed by hand, nor tell which revision the running state corresponds to.
54+
55+
The subject is defined over the declared-versus-observed relation generally, not over cluster convergence specifically. A controller reconciling a cluster to a commit and a process reconciling a data store to a declared contract are the same relation, and both are in scope.
56+
57+
A `reconciliation` is emitted when a convergence occurs — when observed state differs from desired and the controller acts. A cycle that finds nothing to do emits nothing. This keeps the subject an occurrence model, and means a large estate does not produce continuous events reporting that nothing happened. A convergence that was expected and did not occur is found by querying for the event that is missing.
58+
59+
| Field | Type | Description | Examples |
60+
|-------|------|-------------|----------|
61+
| id | `String` | See [id](spec.md#id-subject)| `argocd-sync-00481` |
62+
| source | `URI-Reference` | See [source](spec.md#source-subject) | `/gitops/argocd` |
63+
| desiredState | `Object` | The declared desired state being converged to, typically a commit or revision | `{"id": "a8f3c21", "source": "/scm/github/orders-svc"}` |
64+
| target | `Object` | The target being converged. `type` names its kind. | `{"id": "prod", "source": "/clusters/prod", "type": "cluster"}` |
65+
| driftDetected | `Boolean` | Whether observed state differed from desired at the start of the convergence | `true` |
66+
| changed | `Boolean` | Whether the convergence applied any change | `true` |
67+
| outcome | `String (enum)` | outcome of a finished `reconciliation` | `success`, `failure`, `cancel`, or `error` |
68+
| reason | `String` | Detail related to the outcome | `Target rejected the applied revision` |
69+
4670
## Events
4771

4872
### [`environment created`](conformance/environment_created.json)
@@ -161,3 +185,38 @@ This event represents an existing instance of a service that has an accessible U
161185
| id | `String` | See [id](spec.md#id-subject)| `service/myapp`, `daemonset/myapp` ||
162186
| source | `URI-Reference` | See [source](spec.md#source-subject) | | |
163187
| environment | `Object` ([`environment`](#environment)) | Reference for the environment where the service runs | `{"id": "1234"}`, `{"id": "maven123, "source": "tekton-dev-123"}` ||
188+
189+
### [`reconciliation started`](conformance/reconciliation_started.json)
190+
191+
A controller has begun converging a target toward a declared desired state.
192+
193+
- Event Type: __`dev.cdevents.reconciliation.started.0.1.0-draft`__
194+
- Predicate: started
195+
- Subject: [`reconciliation`](#reconciliation)
196+
197+
| Field | Type | Description | Examples | Required |
198+
|-------|------|-------------|----------|----------------------------|
199+
| id | `String` | See [id](spec.md#id-subject)| `argocd-sync-00481` ||
200+
| source | `URI-Reference` | [source](spec.md#source) from the context | | |
201+
| desiredState | `Object` | The declared desired state being converged to | `{"id": "a8f3c21", "source": "/scm/github/orders-svc"}` ||
202+
| target | `Object` | The target being converged | `{"id": "prod", "source": "/clusters/prod", "type": "cluster"}` ||
203+
| driftDetected | `Boolean` | Whether observed state differed from desired at the start of the convergence | `true` | |
204+
205+
### [`reconciliation finished`](conformance/reconciliation_finished.json)
206+
207+
A convergence has terminated, successfully or not.
208+
209+
- Event Type: __`dev.cdevents.reconciliation.finished.0.1.0-draft`__
210+
- Predicate: finished
211+
- Subject: [`reconciliation`](#reconciliation)
212+
213+
| Field | Type | Description | Examples | Required |
214+
|-------|------|-------------|----------|----------------------------|
215+
| id | `String` | See [id](spec.md#id-subject)| `argocd-sync-00481` ||
216+
| source | `URI-Reference` | [source](spec.md#source) from the context | | |
217+
| desiredState | `Object` | The declared desired state converged to | `{"id": "a8f3c21", "source": "/scm/github/orders-svc"}` ||
218+
| target | `Object` | The target that was converged | `{"id": "prod", "source": "/clusters/prod", "type": "cluster"}` ||
219+
| driftDetected | `Boolean` | Whether observed state differed from desired at the start of the convergence | `true` | |
220+
| changed | `Boolean` | Whether the convergence applied any change | `true` | |
221+
| outcome | `String (enum)` | outcome of a finished `reconciliation` | `success`, `failure`, `cancel`, or `error` | `success`, `failure`, `cancel`, `error` |
222+
| reason | `String` | Detail related to the outcome | `Target rejected the applied revision` | |
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://cdevents.dev/0.6.0-draft/schema/reconciliation-finished-event",
4+
"properties": {
5+
"context": {
6+
"properties": {
7+
"specversion": {
8+
"type": "string",
9+
"minLength": 1
10+
},
11+
"id": {
12+
"type": "string",
13+
"minLength": 1
14+
},
15+
"source": {
16+
"type": "string",
17+
"minLength": 1,
18+
"format": "uri-reference"
19+
},
20+
"type": {
21+
"type": "string",
22+
"enum": [
23+
"dev.cdevents.reconciliation.finished.0.1.0-draft"
24+
],
25+
"default": "dev.cdevents.reconciliation.finished.0.1.0-draft"
26+
},
27+
"timestamp": {
28+
"type": "string",
29+
"format": "date-time"
30+
},
31+
"schemaUri": {
32+
"type": "string",
33+
"minLength": 1,
34+
"format": "uri"
35+
},
36+
"chainId": {
37+
"type": "string",
38+
"minLength": 1
39+
},
40+
"links": {
41+
"$ref": "links/embeddedlinksarray"
42+
}
43+
},
44+
"additionalProperties": false,
45+
"type": "object",
46+
"required": [
47+
"specversion",
48+
"id",
49+
"source",
50+
"type",
51+
"timestamp"
52+
]
53+
},
54+
"subject": {
55+
"properties": {
56+
"id": {
57+
"type": "string",
58+
"minLength": 1
59+
},
60+
"source": {
61+
"type": "string",
62+
"minLength": 1,
63+
"format": "uri-reference"
64+
},
65+
"content": {
66+
"properties": {
67+
"desiredState": {
68+
"properties": {
69+
"id": {
70+
"type": "string",
71+
"minLength": 1
72+
},
73+
"source": {
74+
"type": "string",
75+
"minLength": 1,
76+
"format": "uri-reference"
77+
}
78+
},
79+
"additionalProperties": false,
80+
"type": "object",
81+
"required": [
82+
"id"
83+
]
84+
},
85+
"target": {
86+
"properties": {
87+
"id": {
88+
"type": "string",
89+
"minLength": 1
90+
},
91+
"source": {
92+
"type": "string",
93+
"minLength": 1,
94+
"format": "uri-reference"
95+
},
96+
"type": {
97+
"type": "string",
98+
"minLength": 1
99+
}
100+
},
101+
"additionalProperties": false,
102+
"type": "object",
103+
"required": [
104+
"id",
105+
"type"
106+
]
107+
},
108+
"driftDetected": {
109+
"type": "boolean"
110+
},
111+
"changed": {
112+
"type": "boolean"
113+
},
114+
"outcome": {
115+
"type": "string",
116+
"enum": [
117+
"success",
118+
"failure",
119+
"cancel",
120+
"error"
121+
]
122+
},
123+
"reason": {
124+
"type": "string"
125+
}
126+
},
127+
"additionalProperties": false,
128+
"type": "object",
129+
"required": [
130+
"desiredState",
131+
"target",
132+
"outcome"
133+
]
134+
}
135+
},
136+
"additionalProperties": false,
137+
"type": "object",
138+
"required": [
139+
"id",
140+
"content"
141+
]
142+
},
143+
"customData": {
144+
"oneOf": [
145+
{
146+
"type": "object"
147+
},
148+
{
149+
"type": "string",
150+
"contentEncoding": "base64"
151+
}
152+
]
153+
},
154+
"customDataContentType": {
155+
"type": "string"
156+
}
157+
},
158+
"additionalProperties": false,
159+
"type": "object",
160+
"required": [
161+
"context",
162+
"subject"
163+
]
164+
}

0 commit comments

Comments
 (0)