Skip to content

Commit 201015d

Browse files
authored
feat: add send-message step plugin base and Go implementation (#5)
* feat: implement send-message step plugin Signed-off-by: Seth Nickell <snickell@gmail.com> * refactor: polish send-message go plugin Signed-off-by: Seth Nickell <snickell@gmail.com> * test: clarify go send-message smoke output Signed-off-by: Seth Nickell <snickell@gmail.com> * fix: satisfy go lint for send-message plugin Signed-off-by: Seth Nickell <snickell@gmail.com> --------- Signed-off-by: Seth Nickell <snickell@gmail.com>
1 parent 6722637 commit 201015d

18 files changed

Lines changed: 2653 additions & 2 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# 0004 Implementation Checklist
2+
3+
Update this as implementation teaches us things.
4+
5+
## Baseline
6+
7+
- [x] Plugin work lives under `extended/plugins/send-message`.
8+
- [x] The plugin subtree does not rely on repo resources outside itself.
9+
- [x] Slack credentials stay local-only and out of git.
10+
- [x] The host does not own channel lookup, Secret reads, or Slack API calls.
11+
12+
## Phase 0: read and pin the seams
13+
14+
- [x] Read proposal 0004 again before starting code.
15+
- [x] Re-read the `send-message` section in proposal 0002 spec.
16+
- [x] Read the current StepPlugin host runtime and e2e harness seams.
17+
- [x] Record any host-side gap found during that read.
18+
19+
## Phase 1: create the standalone subtree
20+
21+
- [x] Create `extended/plugins/send-message/`.
22+
- [x] Add plugin-owned runtime source there.
23+
- [x] Add plugin-owned image build there.
24+
- [x] Add plugin-owned tests there.
25+
- [x] Add plugin-owned CRD manifests there.
26+
- [x] Add plugin-owned RBAC manifests there.
27+
- [x] Add plugin-owned smoke assets or smoke helper there.
28+
29+
## Phase 2: implement the runtime
30+
31+
- [x] Implement `POST /api/v1/step.execute`.
32+
- [x] Enforce bearer-token auth from `/var/run/kargo/token`.
33+
- [x] Return `403` on bad auth.
34+
- [x] Reject unsupported methods cleanly.
35+
- [x] Parse the v1 step-config subset.
36+
- [x] Resolve `MessageChannel` from the Project namespace.
37+
- [x] Resolve `ClusterMessageChannel` from the system-resources namespace.
38+
- [x] Resolve referenced Secrets from the correct namespace.
39+
- [x] Read Slack token from Secret key `apiKey`.
40+
- [x] Send the Slack message from the plugin.
41+
- [x] Return `slack.threadTS` output.
42+
43+
## Phase 3: own the OSS CRDs and RBAC
44+
45+
- [x] Add Slack-only `MessageChannel` CRD manifest.
46+
- [x] Add Slack-only `ClusterMessageChannel` CRD manifest.
47+
- [x] Keep the API group `ee.kargo.akuity.io/v1alpha1`.
48+
- [x] Add plugin-owned RBAC manifests for channel and Secret reads.
49+
- [x] Keep RBAC setup out of host code unless a real host gap is proven.
50+
51+
## Phase 4: tests inside the subtree
52+
53+
- [x] Add runtime tests for auth.
54+
- [x] Add runtime tests for namespaced channel lookup.
55+
- [x] Add runtime tests for cluster-scoped channel lookup.
56+
- [x] Add runtime tests for referenced Secret lookup.
57+
- [x] Add runtime tests for Slack request shaping.
58+
- [x] Add runtime tests for `slack.channelID` override.
59+
- [x] Add runtime tests for `slack.threadTS` override and output.
60+
- [x] Add runtime tests for missing channel or Secret failures.
61+
- [x] Add runtime tests for Slack API failure handling.
62+
63+
## Phase 5: local smoke path
64+
65+
- [x] Build the plugin image from `extended/plugins/send-message`.
66+
- [x] Load the image into the local kind cluster.
67+
- [x] Keep kube access on an isolated `KUBECONFIG`.
68+
- [x] Keep the user's existing kube context untouched.
69+
- [x] Install plugin CRDs and RBAC.
70+
- [x] Generate and install the StepPlugin `ConfigMap`.
71+
- [x] Inject a local-only Slack token into a cluster `Secret`.
72+
- [x] Create a test `MessageChannel` or `ClusterMessageChannel`.
73+
- [x] Run a `Stage` with `uses: send-message`.
74+
- [x] Prove promotion success in-cluster.
75+
- [x] Prove `slack.threadTS` output is populated.
76+
- [x] Verify the message appeared in the target Slack channel.
77+
78+
## Phase 6: repo harness integration
79+
80+
- [x] Extend `extended/tests/e2e_stepplugins.sh` to support the real
81+
`send-message` smoke path.
82+
- [x] Keep the committed harness credential-free.
83+
- [x] Gate Slack smoke on a local env var for the token.
84+
- [x] Keep any non-`extended/` edit to a tiny hook only, if one is needed.
85+
86+
## Phase Post-Green: Minimize Diff Of Files Outside ./extended Against Kargo Upstream
87+
88+
- [x] Fetch `upstream`.
89+
- [x] Review every edited file outside `extended/`, if any, against
90+
`upstream/main`.
91+
- [x] Move more logic behind `extended/` helpers if that shrinks the outside
92+
diff safely.
93+
- [x] Re-run matching tests after each cleanup pass.
94+
- [x] Stop only when no obvious outside-`extended/` shrink remains.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# 0004 Implementation Notes
2+
3+
- Plugin subtree:
4+
- `extended/plugins/send-message/`
5+
- standalone `go.mod`
6+
- standalone `Dockerfile`
7+
- standalone `plugin.yaml`
8+
- standalone CRDs and RBAC under `manifests/`
9+
- standalone smoke helper under `smoke/`
10+
11+
- Runtime contract implemented in the plugin:
12+
- `POST /api/v1/step.execute`
13+
- bearer auth from `/var/run/kargo/token`
14+
- direct Kubernetes API reads from projected service-account credentials
15+
- direct Slack API call from the plugin
16+
- encoded `json`, `yaml`, and `xml` payload support
17+
18+
- V1 scope shipped:
19+
- `send-message`
20+
- `MessageChannel`
21+
- `ClusterMessageChannel`
22+
- Slack only
23+
- no SMTP
24+
25+
- Secret and channel rules implemented:
26+
- `secretRef.name` resolves in the Project namespace for `MessageChannel`
27+
- `secretRef.name` resolves in the system-resources namespace for
28+
`ClusterMessageChannel`
29+
- Slack token key is `apiKey`
30+
- `spec.slack.channelID` is required in CRD schema
31+
32+
- Smoke harness knobs:
33+
- `STEPPLUGIN_SEND_MESSAGE_SMOKE=true`
34+
- `STEPPLUGIN_SEND_MESSAGE_CHANNEL_ID=<channel-id>`
35+
- `STEPPLUGIN_SEND_MESSAGE_SLACK_API_KEY=<token>`
36+
- Plugin-local smoke entrypoint:
37+
- `extended/plugins/send-message/smoke/smoke-test.sh`
38+
- repo harness calls that script rather than owning the full orchestration
39+
40+
- Host gap found during smoke:
41+
- the StepPlugin auth-token mount was not readable by non-root sidecars
42+
- fix was in `extended/pkg/stepplugin/agent/command_bridge.go`
43+
- auth directory mode is now `0755`
44+
- auth file mode is now `0444`
45+
46+
- Smoke-result note:
47+
- plugin response includes `output.slack.threadTS`
48+
- Promotion state observed in-cluster stored the value under
49+
`status.state.step-1.slack.threadTS`
50+
- the smoke harness accepts either that path or
51+
`status.stepExecutionMetadata[0].output.slack.threadTS`
52+
53+
- Encoded-message note:
54+
- when `encodingType` is set, `config.slack.channelID` and
55+
`config.slack.threadTS` are ignored
56+
- encoded body uses Slack-native field names such as `channel` and
57+
`thread_ts`
58+
- if encoded body omits `channel`, plugin fills it from channel resource
59+
`spec.slack.channelID`
60+
61+
- XML note:
62+
- no public exact upstream XML example was found
63+
- this implementation treats XML as a Kargo-owned alternate serialization of
64+
the same Slack payload object used for `json` and `yaml`
65+
- root name is ignored
66+
- repeated sibling names become arrays
67+
68+
- Non-plugin repo fix found during smoke:
69+
- `pkg/cli/cmd/promote/promote.go` assumed wrapped REST payloads
70+
- local smoke showed direct-object and direct-list payloads
71+
- decoder helpers now accept both shapes
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# 0004 Implementation Plan
2+
3+
## Goal
4+
5+
- Deliver a real Slack-only `send-message` StepPlugin under
6+
`extended/plugins/send-message`.
7+
- Keep that subtree self-contained enough to behave like its own Git repo.
8+
- Do not rely on code, files, build helpers, tests, or other resources outside
9+
`extended/plugins/send-message` for the plugin implementation itself.
10+
- Use the StepPlugin host slice from proposal
11+
[0002-kargo-executor-plugins-from-argo-workflows](../0002-kargo-executor-plugins-from-argo-workflows/proposal.md)
12+
without widening host responsibilities.
13+
- Keep channel lookup and referenced Secret reads in the plugin, not the host.
14+
- Use local-only Slack credentials for smoke testing. Do not commit any real
15+
token or local token source.
16+
- Make the plugin match the documented Slack subset, including encoded-message
17+
behavior, except for SMTP which stays out of scope.
18+
19+
## Implementation Shape
20+
21+
- Put all plugin-owned source, manifests, tests, and smoke assets under:
22+
- `extended/plugins/send-message/`
23+
- Treat that subtree as a standalone plugin repo:
24+
- own runtime source
25+
- own image build
26+
- own CRDs
27+
- own RBAC manifests
28+
- own tests
29+
- own smoke-test helpers
30+
- own smoke entrypoint script
31+
- It is acceptable for repo-level smoke harness code outside that subtree to
32+
call into the subtree's smoke entrypoint.
33+
- It is not acceptable for plugin runtime code inside that subtree to import or
34+
shell out to helpers elsewhere in this repo.
35+
36+
## Plugin Runtime
37+
38+
- Build a plugin-owned runtime image from `extended/plugins/send-message/`.
39+
- Implement `POST /api/v1/step.execute`.
40+
- Enforce the StepPlugin bearer-token contract:
41+
- read `/var/run/kargo/token`
42+
- require `Authorization: Bearer ...`
43+
- return `403` on bad auth
44+
- Read Kubernetes credentials from the mounted service account projection when
45+
present.
46+
- Use direct Kubernetes API reads from the plugin:
47+
- `MessageChannel` in the Project namespace
48+
- `ClusterMessageChannel` cluster-scoped
49+
- referenced `Secret` in the Project namespace or system-resources namespace
50+
- Call Slack from the plugin runtime, not through the host.
51+
52+
## V1 Behavior
53+
54+
- Scope:
55+
- `send-message`
56+
- `MessageChannel`
57+
- `ClusterMessageChannel`
58+
- Slack only
59+
- Do not implement SMTP.
60+
- Support this step-config subset in v1:
61+
- `channel.kind`
62+
- `channel.name`
63+
- `message`
64+
- `encodingType`
65+
- `slack.channelID`
66+
- `slack.threadTS`
67+
- Support this output subset in v1:
68+
- `slack.threadTS`
69+
- Plaintext behavior:
70+
- `message` is sent as Slack `text`
71+
- `slack.channelID` overrides channel resource `spec.slack.channelID`
72+
- `slack.threadTS` is sent as Slack `thread_ts`
73+
- output `slack.threadTS` is `config.slack.threadTS` if set, else Slack
74+
response `ts`
75+
- Encoded-message behavior:
76+
- support `json`, `yaml`, and `xml`
77+
- when `encodingType` is set, treat the body as the Slack payload object
78+
- ignore `config.slack.channelID` and `config.slack.threadTS`
79+
- if encoded body omits `channel`, fill it from channel resource
80+
`spec.slack.channelID`
81+
- if encoded body sets `thread_ts`, return that value in output
82+
- otherwise return Slack response `ts` in output
83+
- XML behavior:
84+
- XML is a Kargo-owned alternate serialization of the same Slack payload
85+
object used for `json` and `yaml`
86+
- root element name is ignored
87+
- attributes become keys
88+
- repeated sibling element names become arrays
89+
- nested elements become nested objects
90+
- `MessageChannel` lookup rules:
91+
- resource kind is `MessageChannel`
92+
- resource namespace is the Project namespace from the step context
93+
- `secretRef.name` resolves in the same namespace
94+
- `spec.slack.channelID` is required
95+
- Slack token key is `apiKey`
96+
- `ClusterMessageChannel` lookup rules:
97+
- resource kind is `ClusterMessageChannel`
98+
- `secretRef.name` resolves in the system-resources namespace configured for
99+
the plugin install
100+
- `spec.slack.channelID` is required
101+
- Slack token key is `apiKey`
102+
103+
## CRDs And RBAC
104+
105+
- The plugin subtree owns the OSS CRD manifests for:
106+
- `MessageChannel`
107+
- `ClusterMessageChannel`
108+
- Use the existing Akuity API group:
109+
- `ee.kargo.akuity.io/v1alpha1`
110+
- Keep schemas minimal but structurally valid for the Slack-only slice.
111+
- Ship plugin-owned RBAC manifests under the subtree.
112+
- For the local smoke path:
113+
- bind only the test Project default `ServiceAccount`
114+
- grant channel reads and referenced Secret reads needed by the plugin
115+
- keep that setup in the smoke assets or smoke helper, not in host code
116+
117+
## Tests
118+
119+
- Keep plugin tests under `extended/plugins/send-message/`.
120+
- Cover at least:
121+
- bearer-token auth
122+
- `MessageChannel` lookup
123+
- `ClusterMessageChannel` lookup
124+
- referenced Secret lookup
125+
- Slack request shaping for plain text
126+
- `slack.channelID` override
127+
- `slack.threadTS` override
128+
- `slack.threadTS` output
129+
- encoded-body behavior ignores `config.slack.*`
130+
- `xml` decode path
131+
- failure path when channel or Secret is missing
132+
- failure path when Slack returns an error
133+
134+
## Smoke Test
135+
136+
- Put the primary smoke script in:
137+
- `extended/plugins/send-message/smoke/smoke-test.sh`
138+
- That script assumes Kargo is already available and takes its inputs from env.
139+
- Keep the committed smoke path credential-free.
140+
- Expect a local env var for the Slack API token during local smoke testing.
141+
- Build the plugin image from `extended/plugins/send-message/`.
142+
- Load that image into the local kind cluster without touching the user's
143+
global kube context.
144+
- Install plugin CRDs, RBAC, and generated StepPlugin `ConfigMap`.
145+
- Create either a `MessageChannel` or `ClusterMessageChannel` test resource and
146+
the referenced Secret in-cluster from the local-only token source.
147+
- Run a `Stage` with `uses: send-message`.
148+
- Prove in-cluster success from `Promotion` status, including non-empty
149+
`slack.threadTS`.
150+
- For interactive local verification, also confirm the message appeared in the
151+
target Slack channel.
152+
- The repo harness may call this script at the right point in e2e flow, but the
153+
smoke orchestration lives in the plugin subtree.
154+
155+
## Host Changes
156+
157+
- Prefer zero host changes.
158+
- If the plugin proves a host gap, keep any host fix thin and behind
159+
`extended/`.
160+
- Do not move channel lookup, Secret reads, or Slack API calls into the host.
161+
- Host gap actually found:
162+
- non-root sidecars could not read `/var/run/kargo/token`
163+
- keep the fix thin in `extended/pkg/stepplugin/agent/command_bridge.go`
164+
- auth directory mode must permit traversal by non-root sidecars
165+
- auth file mode must permit read by non-root sidecars
166+
167+
## Phase Post-Green: Minimize Diff Of Files Outside ./extended Against Kargo Upstream
168+
169+
1. Get the feature green first.
170+
2. Fetch `upstream/main`.
171+
3. Review every edited file outside `extended/`, if any.
172+
4. Move logic behind `extended/` helpers where that shrinks the conflict
173+
surface.
174+
5. Re-run the matching tests after each cleanup pass.
175+
6. Stop when no obvious helper extraction or edit-block reduction remains.

extended/docs/proposals/0004-send-message-step-plugin/proposal.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ Date: 2026-03-24
2020
`extended/plugins/send-message`.
2121
- Use that subtree to test whether a third party can implement the plugin
2222
without merge permissions in the main repo.
23-
- Match the public Kargo `send-message` step shape for the Slack subset as
24-
closely as public docs allow.
2523
- Keep channel lookup and referenced Secret reads in the plugin, not the host.
2624
- Do not implement SMTP in this slice.
2725

0 commit comments

Comments
 (0)