diff --git a/cmd/driver/main.go b/cmd/driver/main.go index 75b3d3a..33f44d5 100644 --- a/cmd/driver/main.go +++ b/cmd/driver/main.go @@ -36,6 +36,10 @@ func main() { "Mount path for the supervisor binary volume in the agent container") flag.StringVar(&cfg.GatewayEndpoint, "gateway-endpoint", cfg.GatewayEndpoint, "Gateway gRPC endpoint for supervisor callback (OPENSHELL_ENDPOINT)") + flag.StringVar(&cfg.SATokenAudience, "sa-token-audience", cfg.SATokenAudience, + "Audience for the projected ServiceAccount token injected into sandbox pods") + flag.Int64Var(&cfg.SATokenTTLSecs, "sa-token-ttl-secs", cfg.SATokenTTLSecs, + "Expiration (seconds) for the projected ServiceAccount token") flag.Parse() if cfg.Tenant == "" { diff --git a/internal/driver/config.go b/internal/driver/config.go index 757f819..64427fc 100644 --- a/internal/driver/config.go +++ b/internal/driver/config.go @@ -8,6 +8,8 @@ type Config struct { DtachBinaryPath string SupervisorMountPath string GatewayEndpoint string + SATokenAudience string // audience for the projected SA token volume + SATokenTTLSecs int64 // expiration seconds for the projected SA token } func DefaultConfig() Config { @@ -17,5 +19,7 @@ func DefaultConfig() Config { SupervisorBinaryPath: "/usr/local/bin/openshell-sandbox", DtachBinaryPath: "/usr/local/bin/dtach", SupervisorMountPath: "/opt/openshell/bin", + SATokenAudience: "openshell-gateway", + SATokenTTLSecs: 3600, } } diff --git a/internal/driver/config_test.go b/internal/driver/config_test.go index 5283603..06b4451 100644 --- a/internal/driver/config_test.go +++ b/internal/driver/config_test.go @@ -15,6 +15,7 @@ func TestDefaultConfig(t *testing.T) { {"SupervisorBinaryPath", cfg.SupervisorBinaryPath, "/usr/local/bin/openshell-sandbox"}, {"DtachBinaryPath", cfg.DtachBinaryPath, "/usr/local/bin/dtach"}, {"SupervisorMountPath", cfg.SupervisorMountPath, "/opt/openshell/bin"}, + {"SATokenAudience", cfg.SATokenAudience, "openshell-gateway"}, } for _, tt := range tests { @@ -22,6 +23,10 @@ func TestDefaultConfig(t *testing.T) { t.Errorf("DefaultConfig().%s = %q, want %q", tt.field, tt.got, tt.want) } } + + if cfg.SATokenTTLSecs != 3600 { + t.Errorf("DefaultConfig().SATokenTTLSecs = %d, want 3600", cfg.SATokenTTLSecs) + } } func TestConfigZeroValue(t *testing.T) { diff --git a/internal/driver/provisioner.go b/internal/driver/provisioner.go index 3ad1598..d4d27e5 100644 --- a/internal/driver/provisioner.go +++ b/internal/driver/provisioner.go @@ -266,6 +266,11 @@ func (p *K8sProvisioner) buildSandboxSpec(sb *pb.DriverSandbox) map[string]inter "mountPath": p.cfg.SupervisorMountPath, "readOnly": true, }, + map[string]interface{}{ + "name": "openshell-sa-token", + "mountPath": "/var/run/secrets/openshell", + "readOnly": true, + }, }, } @@ -282,6 +287,21 @@ func (p *K8sProvisioner) buildSandboxSpec(sb *pb.DriverSandbox) map[string]inter "name": "supervisor-bin", "emptyDir": map[string]interface{}{}, }, + map[string]interface{}{ + "name": "openshell-sa-token", + "projected": map[string]interface{}{ + "sources": []interface{}{ + map[string]interface{}{ + "serviceAccountToken": map[string]interface{}{ + "audience": p.cfg.SATokenAudience, + "expirationSeconds": p.cfg.SATokenTTLSecs, + "path": "token", + }, + }, + }, + "defaultMode": int64(0400), + }, + }, }, } @@ -321,9 +341,10 @@ func (p *K8sProvisioner) buildFullEnvList( envList := buildEnvList(spec.GetEnvironment(), tmpl.GetEnvironment()) gatewayEnv := map[string]string{ - "OPENSHELL_SANDBOX_ID": sb.GetId(), - "OPENSHELL_SANDBOX": sb.GetName(), - "OPENSHELL_SANDBOX_COMMAND": "sleep infinity", + "OPENSHELL_SANDBOX_ID": sb.GetId(), + "OPENSHELL_SANDBOX": sb.GetName(), + "OPENSHELL_SANDBOX_COMMAND": "sleep infinity", + "OPENSHELL_K8S_SA_TOKEN_FILE": "/var/run/secrets/openshell/token", } if p.cfg.GatewayEndpoint != "" { gatewayEnv["OPENSHELL_ENDPOINT"] = p.cfg.GatewayEndpoint diff --git a/internal/driver/provisioner_test.go b/internal/driver/provisioner_test.go index fa1f8ba..4521a7a 100644 --- a/internal/driver/provisioner_test.go +++ b/internal/driver/provisioner_test.go @@ -243,8 +243,8 @@ func TestBuildSandboxSpec_SupervisorInitContainer(t *testing.T) { // Verify volume mounts on agent container. agentMounts := agentC["volumeMounts"].([]interface{}) - if len(agentMounts) != 1 { - t.Fatalf("expected 1 volume mount on agent, got %d", len(agentMounts)) + if len(agentMounts) != 2 { + t.Fatalf("expected 2 volume mounts on agent, got %d", len(agentMounts)) } mount := agentMounts[0].(map[string]interface{}) if mount["name"] != "supervisor-bin" { @@ -253,11 +253,21 @@ func TestBuildSandboxSpec_SupervisorInitContainer(t *testing.T) { if mount["readOnly"] != true { t.Error("expected readOnly=true on agent volume mount") } + saMount := agentMounts[1].(map[string]interface{}) + if saMount["name"] != "openshell-sa-token" { + t.Errorf("expected mount name openshell-sa-token, got %v", saMount["name"]) + } + if saMount["mountPath"] != "/var/run/secrets/openshell" { + t.Errorf("expected mountPath /var/run/secrets/openshell, got %v", saMount["mountPath"]) + } + if saMount["readOnly"] != true { + t.Error("expected readOnly=true on SA token volume mount") + } // Verify volumes. volumes, ok := podSpec["volumes"].([]interface{}) - if !ok || len(volumes) == 0 { - t.Fatal("missing volumes") + if !ok || len(volumes) < 2 { + t.Fatalf("expected at least 2 volumes, got %d", len(volumes)) } vol := volumes[0].(map[string]interface{}) if vol["name"] != "supervisor-bin" { @@ -266,6 +276,32 @@ func TestBuildSandboxSpec_SupervisorInitContainer(t *testing.T) { if _, ok := vol["emptyDir"]; !ok { t.Error("expected emptyDir volume") } + saVol := volumes[1].(map[string]interface{}) + if saVol["name"] != "openshell-sa-token" { + t.Errorf("expected volume name openshell-sa-token, got %v", saVol["name"]) + } + projected, ok := saVol["projected"].(map[string]interface{}) + if !ok { + t.Fatal("expected projected volume") + } + sources, ok := projected["sources"].([]interface{}) + if !ok || len(sources) == 0 { + t.Fatal("expected projected sources") + } + src := sources[0].(map[string]interface{}) + saToken, ok := src["serviceAccountToken"].(map[string]interface{}) + if !ok { + t.Fatal("expected serviceAccountToken source") + } + if saToken["audience"] != cfg.SATokenAudience { + t.Errorf("expected audience %s, got %v", cfg.SATokenAudience, saToken["audience"]) + } + if saToken["expirationSeconds"] != cfg.SATokenTTLSecs { + t.Errorf("expected expirationSeconds %d, got %v", cfg.SATokenTTLSecs, saToken["expirationSeconds"]) + } + if saToken["path"] != "token" { + t.Errorf("expected path token, got %v", saToken["path"]) + } } func TestBuildSandboxSpec_Labels(t *testing.T) { @@ -338,6 +374,42 @@ func TestBuildSandboxSpec_TenantLabels(t *testing.T) { } } +func TestBuildSandboxSpec_SATokenEnvVar(t *testing.T) { + p := newProvisionerForTest(t) + + sb := &pb.DriverSandbox{ + Id: "sb-token", + Name: "token-test", + Spec: &pb.DriverSandboxSpec{ + Template: &pb.DriverSandboxTemplate{ + Image: "agent:latest", + }, + }, + } + + spec := p.buildSandboxSpec(sb) + podTemplate := spec["podTemplate"].(map[string]interface{}) + podSpec := podTemplate["spec"].(map[string]interface{}) + containers := podSpec["containers"].([]interface{}) + agentC := containers[0].(map[string]interface{}) + envList := agentC["env"].([]interface{}) + + found := false + for _, e := range envList { + env := e.(map[string]interface{}) + if env["name"] == "OPENSHELL_K8S_SA_TOKEN_FILE" { + found = true + if env["value"] != "/var/run/secrets/openshell/token" { + t.Errorf("expected /var/run/secrets/openshell/token, got %v", env["value"]) + } + break + } + } + if !found { + t.Error("OPENSHELL_K8S_SA_TOKEN_FILE env var not found in agent container") + } +} + func TestNewWithDeps(t *testing.T) { p := newProvisionerForTest(t) logger := testLogger()