11//! K8s implementation of `ProvisionDriver` (ADR #63 slice 3b).
22//!
33//! Deliberately narrow for this slice: `apply`/`scale`/`delete` against a k8s
4- //! `Deployment`, mirroring the shape `EcsDriver` already has. Two things a
5- //! manifest can carry are explicitly **not yet supported** and fail loudly
6- //! rather than silently mis-deploying:
4+ //! `Deployment`, mirroring the shape `EcsDriver` already has.
75//!
8- //! - `spec.bundleFrom` (the composed persona/skills bundle) — ECS gets this
9- //! for free via its S3 file carrier; k8s needs a ConfigMap/volume carrier,
10- //! tracked as sub-slice 3c.
11- //! - `spec.secrets` — ECS resolves these into `Secret.valueFrom` ARNs; a k8s
12- //! target needs a different output shape (a Secret key selector), tracked
13- //! as sub-slice 3d.
6+ //! `spec.secrets` is wired (sub-slice 3d): each value must be
7+ //! `k8s-secret://<secret-name>#<key>` (see `secrets::parse_k8s_secret_uri`)
8+ //! and becomes an `env[].valueFrom.secretKeyRef` — the Secret object itself
9+ //! must already exist in the target namespace; creating it is a separate
10+ //! concern (same non-creating shape `aws-sm://` already has for ECS). ECS's
11+ //! `aws-sm://`/raw-ARN values in a k8s-runtime manifest fail loudly at apply
12+ //! time — a manifest error, not a silent no-op.
13+ //!
14+ //! `spec.bundleFrom` (the composed persona/skills bundle) is explicitly
15+ //! **not yet supported** and fails loudly rather than silently
16+ //! mis-deploying — ECS gets this for free via its S3 file carrier, k8s needs
17+ //! a ConfigMap/volume carrier, tracked as sub-slice 3c.
1418//!
1519//! Observing k8s state into the canonical 6-state (the `apply`/`scale`
1620//! counterpart to `status.rs`'s ECS `service_status`/`instance_status`) is
@@ -26,7 +30,8 @@ use anyhow::{Context, Result};
2630use async_trait:: async_trait;
2731use k8s_openapi:: api:: apps:: v1:: { Deployment , DeploymentSpec } ;
2832use k8s_openapi:: api:: core:: v1:: {
29- Container , EnvVar , PodSpec , PodTemplateSpec , ResourceRequirements , Toleration ,
33+ Container , EnvVar , EnvVarSource , PodSpec , PodTemplateSpec , ResourceRequirements ,
34+ SecretKeySelector , Toleration ,
3035} ;
3136use k8s_openapi:: apimachinery:: pkg:: api:: resource:: Quantity ;
3237use k8s_openapi:: apimachinery:: pkg:: apis:: meta:: v1:: { LabelSelector , ObjectMeta } ;
@@ -83,8 +88,8 @@ fn require_kubernetes_runtime(m: &OABServiceManifest) -> Result<&crate::manifest
8388 }
8489}
8590
86- /// Reject the two not-yet-supported manifest features explicitly (see module
87- /// docs) instead of silently dropping them .
91+ /// Reject the still- not-yet-supported manifest feature explicitly (see
92+ /// module docs) instead of silently dropping it .
8893fn reject_unsupported ( m : & OABServiceManifest ) -> Result < ( ) > {
8994 if m. spec . bundle_from . is_some ( ) {
9095 anyhow:: bail!(
@@ -93,16 +98,44 @@ fn reject_unsupported(m: &OABServiceManifest) -> Result<()> {
9398 m. metadata. name
9499 ) ;
95100 }
96- if !m. spec . secrets . is_empty ( ) {
97- anyhow:: bail!(
98- "k8s secret refs not implemented yet (studio#97 sub-slice 3d) — '{}/{}' has spec.secrets set" ,
99- m. metadata. namespace,
100- m. metadata. name
101- ) ;
102- }
103101 Ok ( ( ) )
104102}
105103
104+ /// Build the `env[]` entries for `spec.secrets`: each value must be a
105+ /// `k8s-secret://<secret-name>#<key>` ref, which becomes a `secretKeyRef` —
106+ /// kubelet resolves it at pod-start time, no API call needed here (unlike
107+ /// ECS's `aws-sm://`, which resolves to an ARN up front). Any other scheme
108+ /// (an ECS `aws-sm://` ref left over from copy-pasting an ECS manifest, a
109+ /// raw ARN, ...) is a manifest error, not silently dropped.
110+ fn secret_env_vars ( m : & OABServiceManifest ) -> Result < Vec < EnvVar > > {
111+ m. spec
112+ . secrets
113+ . iter ( )
114+ . map ( |( env_name, value) | {
115+ let ( secret_name, key) = crate :: secrets:: parse_k8s_secret_uri ( value)
116+ . with_context ( || {
117+ format ! (
118+ "spec.secrets['{env_name}'] for k8s runtime must use \
119+ k8s-secret://<secret-name>#<key> (got '{value}') — '{}/{}'",
120+ m. metadata. namespace, m. metadata. name
121+ )
122+ } ) ??;
123+ Ok ( EnvVar {
124+ name : env_name. clone ( ) ,
125+ value_from : Some ( EnvVarSource {
126+ secret_key_ref : Some ( SecretKeySelector {
127+ name : secret_name. to_string ( ) ,
128+ key : key. to_string ( ) ,
129+ optional : None ,
130+ } ) ,
131+ ..Default :: default ( )
132+ } ) ,
133+ ..Default :: default ( )
134+ } )
135+ } )
136+ . collect ( )
137+ }
138+
106139fn resource_requirements ( resources : & crate :: manifest:: Resources ) -> ResourceRequirements {
107140 let mut quantities = BTreeMap :: new ( ) ;
108141 quantities. insert ( "cpu" . to_string ( ) , Quantity ( resources. cpu . clone ( ) ) ) ;
@@ -141,6 +174,7 @@ fn build_deployment(m: &OABServiceManifest) -> Result<Deployment> {
141174 ..Default :: default ( )
142175 } ) ;
143176 }
177+ env. extend ( secret_env_vars ( m) ?) ;
144178
145179 // Same convention as EcsDriver (apply.rs): the image's default CMD points
146180 // at a config.toml nothing populates, so override it to load configFrom
@@ -338,10 +372,34 @@ mod tests {
338372 }
339373
340374 #[ test]
341- fn reject_unsupported_bails_on_secrets ( ) {
375+ fn reject_unsupported_passes_manifests_with_k8s_secrets ( ) {
376+ let m = k8s_manifest ( None , & [ ( "DISCORD_BOT_TOKEN" , "k8s-secret://oab-orca#DISCORD_BOT_TOKEN" ) ] ) ;
377+ reject_unsupported ( & m) . unwrap ( ) ;
378+ }
379+
380+ #[ test]
381+ fn build_deployment_wires_secret_key_ref ( ) {
382+ let m = k8s_manifest ( None , & [ ( "DISCORD_BOT_TOKEN" , "k8s-secret://oab-orca#DISCORD_BOT_TOKEN" ) ] ) ;
383+ let dep = build_deployment ( & m) . unwrap ( ) ;
384+ let pod = dep. spec . unwrap ( ) . template . spec . unwrap ( ) ;
385+ let env = pod. containers [ 0 ] . env . as_ref ( ) . unwrap ( ) ;
386+ let secret_env = env. iter ( ) . find ( |e| e. name == "DISCORD_BOT_TOKEN" ) . unwrap ( ) ;
387+ let secret_ref = secret_env
388+ . value_from
389+ . as_ref ( )
390+ . unwrap ( )
391+ . secret_key_ref
392+ . as_ref ( )
393+ . unwrap ( ) ;
394+ assert_eq ! ( secret_ref. name, "oab-orca" ) ;
395+ assert_eq ! ( secret_ref. key, "DISCORD_BOT_TOKEN" ) ;
396+ }
397+
398+ #[ test]
399+ fn build_deployment_rejects_non_k8s_secret_scheme ( ) {
342400 let m = k8s_manifest ( None , & [ ( "DISCORD_BOT_TOKEN" , "aws-sm://oab/prod/orca#DISCORD_BOT_TOKEN" ) ] ) ;
343- let err = reject_unsupported ( & m) . unwrap_err ( ) ;
344- assert ! ( err. to_string( ) . contains( "3d " ) ) ;
401+ let err = build_deployment ( & m) . unwrap_err ( ) ;
402+ assert ! ( err. to_string( ) . contains( "k8s-secret:// " ) ) ;
345403 }
346404
347405 #[ test]
0 commit comments