@app-vitals/agent-admin 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/prisma/client/internal/class.ts +4 -4
- package/prisma/client/internal/prismaNamespace.ts +1 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +1 -0
- package/prisma/client/models/Agent.ts +76 -1
- package/prisma/migrations/20261026000000_add_agent_pod_service_account/migration.sql +3 -0
- package/prisma/schema/schema.prisma +4 -0
- package/src/admin-boot-config.ts +2 -0
- package/src/agent-account-assignment.ts +2 -0
- package/src/agent-manifest.ts +24 -1
- package/src/agent-manifest.unit.test.ts +57 -0
- package/src/agent-pod-service-account.integration.test.ts +47 -0
- package/src/agent-provisioner.ts +33 -3
- package/src/agent-provisioner.unit.test.ts +48 -0
- package/src/agents.ts +4 -0
- package/src/main.unit.test.ts +19 -0
- package/src/pod-service-account-config.ts +24 -0
- package/src/pod-service-account-config.unit.test.ts +28 -0
- package/src/startup-policy-reconcile.ts +1 -0
package/src/agent-manifest.ts
CHANGED
|
@@ -318,6 +318,24 @@ export interface AgentDeploymentOpts {
|
|
|
318
318
|
* federation). Omit for an unchanged manifest.
|
|
319
319
|
*/
|
|
320
320
|
anthropicFederation?: AgentAnthropicFederation;
|
|
321
|
+
/**
|
|
322
|
+
* Per-agent pod ServiceAccount (COS-48.1). When set (and `anthropicFederation`
|
|
323
|
+
* is absent) the pod runs as this ServiceAccount. Mutually exclusive with
|
|
324
|
+
* `anthropicFederation`: federation's projected token subject must stay its
|
|
325
|
+
* own ServiceAccount, so supplying both throws
|
|
326
|
+
* PodServiceAccountConflictError. Omit for an unchanged manifest.
|
|
327
|
+
*/
|
|
328
|
+
podServiceAccountName?: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Both a pod ServiceAccount and Anthropic federation were supplied. */
|
|
332
|
+
export class PodServiceAccountConflictError extends Error {
|
|
333
|
+
constructor(agentId: string) {
|
|
334
|
+
super(
|
|
335
|
+
`agent ${agentId}: podServiceAccountName cannot be combined with anthropicFederation (the federation token subject must stay its own ServiceAccount)`,
|
|
336
|
+
);
|
|
337
|
+
this.name = "PodServiceAccountConflictError";
|
|
338
|
+
}
|
|
321
339
|
}
|
|
322
340
|
|
|
323
341
|
/**
|
|
@@ -479,6 +497,9 @@ export function buildAgentDeploymentManifest(
|
|
|
479
497
|
|
|
480
498
|
const policy = opts.claudePolicy;
|
|
481
499
|
const federation = opts.anthropicFederation;
|
|
500
|
+
if (federation && opts.podServiceAccountName) {
|
|
501
|
+
throw new PodServiceAccountConflictError(opts.agentId);
|
|
502
|
+
}
|
|
482
503
|
|
|
483
504
|
return {
|
|
484
505
|
apiVersion: "apps/v1",
|
|
@@ -507,7 +528,9 @@ export function buildAgentDeploymentManifest(
|
|
|
507
528
|
// Federation: the projected token's subject is this ServiceAccount.
|
|
508
529
|
...(federation
|
|
509
530
|
? { serviceAccountName: federation.serviceAccountName }
|
|
510
|
-
:
|
|
531
|
+
: opts.podServiceAccountName
|
|
532
|
+
? { serviceAccountName: opts.podServiceAccountName }
|
|
533
|
+
: {}),
|
|
511
534
|
securityContext: {
|
|
512
535
|
fsGroup: AGENT_RUN_AS,
|
|
513
536
|
fsGroupChangePolicy: "OnRootMismatch",
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
buildAgentSecretManifest,
|
|
18
18
|
resolveAgentContainerResources,
|
|
19
19
|
sanitizeAgentName,
|
|
20
|
+
PodServiceAccountConflictError,
|
|
20
21
|
TENANT_LABEL,
|
|
21
22
|
} from "./agent-manifest.ts";
|
|
22
23
|
|
|
@@ -868,3 +869,59 @@ describe("buildAgentDeploymentManifest — anthropic federation", () => {
|
|
|
868
869
|
expect(base).not.toContain("ANTHROPIC_");
|
|
869
870
|
});
|
|
870
871
|
});
|
|
872
|
+
|
|
873
|
+
describe("buildAgentDeploymentManifest — pod service account", () => {
|
|
874
|
+
const anthropicFederation = {
|
|
875
|
+
serviceAccountName: "shipwright-agent",
|
|
876
|
+
ruleId: "fdrl_123",
|
|
877
|
+
organizationId: "org-uuid",
|
|
878
|
+
serviceAccountId: "svac_456",
|
|
879
|
+
audience: "https://api.anthropic.com",
|
|
880
|
+
expirationSeconds: 600,
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
it("sets spec.template.spec.serviceAccountName from the option", () => {
|
|
884
|
+
const d = buildAgentDeploymentManifest({
|
|
885
|
+
...deployOpts,
|
|
886
|
+
podServiceAccountName: "agent-sa",
|
|
887
|
+
});
|
|
888
|
+
expect(d.spec.template.spec.serviceAccountName).toBe("agent-sa");
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("omits serviceAccountName entirely when undefined", () => {
|
|
892
|
+
const d = buildAgentDeploymentManifest({
|
|
893
|
+
...deployOpts,
|
|
894
|
+
podServiceAccountName: undefined,
|
|
895
|
+
});
|
|
896
|
+
expect("serviceAccountName" in d.spec.template.spec).toBe(false);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it("leaves the base manifest byte-identical when the option is absent", () => {
|
|
900
|
+
expect(
|
|
901
|
+
JSON.stringify(
|
|
902
|
+
buildAgentDeploymentManifest({
|
|
903
|
+
...deployOpts,
|
|
904
|
+
podServiceAccountName: undefined,
|
|
905
|
+
}),
|
|
906
|
+
),
|
|
907
|
+
).toBe(JSON.stringify(buildAgentDeploymentManifest(deployOpts)));
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it("throws a typed error when federation is also supplied", () => {
|
|
911
|
+
expect(() =>
|
|
912
|
+
buildAgentDeploymentManifest({
|
|
913
|
+
...deployOpts,
|
|
914
|
+
podServiceAccountName: "agent-sa",
|
|
915
|
+
anthropicFederation,
|
|
916
|
+
}),
|
|
917
|
+
).toThrow(PodServiceAccountConflictError);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it("keeps the federation service account when only federation is set", () => {
|
|
921
|
+
const d = buildAgentDeploymentManifest({
|
|
922
|
+
...deployOpts,
|
|
923
|
+
anthropicFederation,
|
|
924
|
+
});
|
|
925
|
+
expect(d.spec.template.spec.serviceAccountName).toBe("shipwright-agent");
|
|
926
|
+
});
|
|
927
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-pod-service-account.integration.test.ts
|
|
3
|
+
* COS-48.1: the Agent.podServiceAccountName column exists, is nullable and is
|
|
4
|
+
* independent of k8sServiceAccountName. Requires DATABASE_URL_ADMIN_TEST;
|
|
5
|
+
* skips otherwise.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
9
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
10
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
11
|
+
|
|
12
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
13
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
14
|
+
|
|
15
|
+
describeOrSkip("Agent.podServiceAccountName (integration)", () => {
|
|
16
|
+
let prisma: PrismaClient;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
prisma = createAdminPrismaClient(TEST_DB as string);
|
|
20
|
+
await prisma.agentPhaseMethodology.deleteMany();
|
|
21
|
+
await prisma.agentToken.deleteMany();
|
|
22
|
+
await prisma.agentCronJob.deleteMany();
|
|
23
|
+
await prisma.agentTool.deleteMany();
|
|
24
|
+
await prisma.agentEnv.deleteMany();
|
|
25
|
+
await prisma.agent.deleteMany();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await prisma.$disconnect();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("defaults to null for a new agent", async () => {
|
|
33
|
+
const agent = await prisma.agent.create({ data: { name: "A" } });
|
|
34
|
+
expect(agent.podServiceAccountName).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("round-trips without touching k8sServiceAccountName", async () => {
|
|
38
|
+
const agent = await prisma.agent.create({
|
|
39
|
+
data: { name: "A", podServiceAccountName: "agent-a-sa" },
|
|
40
|
+
});
|
|
41
|
+
const row = await prisma.agent.findUniqueOrThrow({
|
|
42
|
+
where: { id: agent.id },
|
|
43
|
+
});
|
|
44
|
+
expect(row.podServiceAccountName).toBe("agent-a-sa");
|
|
45
|
+
expect(row.k8sServiceAccountName).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/agent-provisioner.ts
CHANGED
|
@@ -80,6 +80,7 @@ import type {
|
|
|
80
80
|
PvcSpec,
|
|
81
81
|
SecretSpec,
|
|
82
82
|
} from "./kubernetes-client.ts";
|
|
83
|
+
import type { PodServiceAccountConfig } from "./pod-service-account-config.ts";
|
|
83
84
|
import {
|
|
84
85
|
provisionableIntegrations,
|
|
85
86
|
type ServiceIntegration,
|
|
@@ -148,6 +149,8 @@ export interface ReconcileTarget extends Partial<AgentAnthropicIdentity> {
|
|
|
148
149
|
id: string;
|
|
149
150
|
slug?: string;
|
|
150
151
|
accountId?: string | null;
|
|
152
|
+
/** The Agent row's pod ServiceAccount (COS-48.1); rendered only when the flag is on. */
|
|
153
|
+
podServiceAccountName?: string | null;
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
export interface AgentProvisioner {
|
|
@@ -163,7 +166,11 @@ export interface AgentProvisioner {
|
|
|
163
166
|
/** Mint a token and create the agent's Secret + Deployment. Idempotent. */
|
|
164
167
|
provision(
|
|
165
168
|
agentId: string,
|
|
166
|
-
opts?: {
|
|
169
|
+
opts?: {
|
|
170
|
+
slug?: string;
|
|
171
|
+
accountId?: string | null;
|
|
172
|
+
podServiceAccountName?: string | null;
|
|
173
|
+
},
|
|
167
174
|
): Promise<ProvisionResult>;
|
|
168
175
|
/**
|
|
169
176
|
* Delete the agent's Deployment, Secret, and PVC. Tolerates already-absent
|
|
@@ -304,6 +311,12 @@ export interface KubernetesAgentProvisionerConfig {
|
|
|
304
311
|
* exclusive with `anthropicFederation` (shared mode).
|
|
305
312
|
*/
|
|
306
313
|
perAgentIdentity?: PerAgentIdentityConfig;
|
|
314
|
+
/**
|
|
315
|
+
* Per-agent pod ServiceAccount flag (COS-48.1). Only when `enabled` is the
|
|
316
|
+
* Agent row's podServiceAccountName passed to the manifest; absent or off
|
|
317
|
+
* means the value is never rendered, even if set in the DB.
|
|
318
|
+
*/
|
|
319
|
+
podServiceAccount?: PodServiceAccountConfig;
|
|
307
320
|
}
|
|
308
321
|
|
|
309
322
|
// ─── perAgent identity ──────────────────────────────────────────────────────
|
|
@@ -841,6 +854,7 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
|
|
|
841
854
|
slug: string | undefined,
|
|
842
855
|
accountId: string | null | undefined,
|
|
843
856
|
federation: AgentAnthropicFederation | undefined,
|
|
857
|
+
podServiceAccountName?: string | null,
|
|
844
858
|
) {
|
|
845
859
|
const resourceName = this.resourceName(agentId);
|
|
846
860
|
return buildAgentDeploymentManifest({
|
|
@@ -862,12 +876,19 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
|
|
|
862
876
|
claudePolicy: this.config.claudePolicy,
|
|
863
877
|
anthropicFederation: federation,
|
|
864
878
|
accountId,
|
|
879
|
+
...(this.config.podServiceAccount?.enabled && podServiceAccountName
|
|
880
|
+
? { podServiceAccountName }
|
|
881
|
+
: {}),
|
|
865
882
|
});
|
|
866
883
|
}
|
|
867
884
|
|
|
868
885
|
async provision(
|
|
869
886
|
agentId: string,
|
|
870
|
-
opts?: {
|
|
887
|
+
opts?: {
|
|
888
|
+
slug?: string;
|
|
889
|
+
accountId?: string | null;
|
|
890
|
+
podServiceAccountName?: string | null;
|
|
891
|
+
},
|
|
871
892
|
): Promise<ProvisionResult> {
|
|
872
893
|
const resourceName = this.resourceName(agentId);
|
|
873
894
|
const secretName = this.secretNameFor(resourceName);
|
|
@@ -1074,7 +1095,13 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
|
|
|
1074
1095
|
/** Steps 2–4: token(s) → Secret → Deployment (the original provision flow). */
|
|
1075
1096
|
private async provisionSecretAndDeployment(
|
|
1076
1097
|
agentId: string,
|
|
1077
|
-
opts:
|
|
1098
|
+
opts:
|
|
1099
|
+
| {
|
|
1100
|
+
slug?: string;
|
|
1101
|
+
accountId?: string | null;
|
|
1102
|
+
podServiceAccountName?: string | null;
|
|
1103
|
+
}
|
|
1104
|
+
| undefined,
|
|
1078
1105
|
result: ProvisionResult,
|
|
1079
1106
|
federation: AgentAnthropicFederation | undefined,
|
|
1080
1107
|
): Promise<ProvisionResult> {
|
|
@@ -1166,6 +1193,7 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
|
|
|
1166
1193
|
opts?.slug,
|
|
1167
1194
|
opts?.accountId,
|
|
1168
1195
|
federation,
|
|
1196
|
+
opts?.podServiceAccountName,
|
|
1169
1197
|
),
|
|
1170
1198
|
);
|
|
1171
1199
|
} catch (err) {
|
|
@@ -1312,6 +1340,7 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
|
|
|
1312
1340
|
await this.provision(agent.id, {
|
|
1313
1341
|
slug: agent.slug,
|
|
1314
1342
|
accountId: agent.accountId,
|
|
1343
|
+
podServiceAccountName: agent.podServiceAccountName,
|
|
1315
1344
|
});
|
|
1316
1345
|
}
|
|
1317
1346
|
recreated.push(agent.id);
|
|
@@ -1376,6 +1405,7 @@ export class KubernetesAgentProvisioner implements AgentProvisioner {
|
|
|
1376
1405
|
agent.slug,
|
|
1377
1406
|
agent.accountId,
|
|
1378
1407
|
desiredFederation,
|
|
1408
|
+
agent.podServiceAccountName,
|
|
1379
1409
|
);
|
|
1380
1410
|
const desired = desiredDeployment.spec.template.spec.containers[0];
|
|
1381
1411
|
const desiredLabels = desiredDeployment.spec.template.metadata.labels;
|
|
@@ -1850,3 +1850,51 @@ describe("perAgentFederation()", () => {
|
|
|
1850
1850
|
).toBe(false);
|
|
1851
1851
|
});
|
|
1852
1852
|
});
|
|
1853
|
+
|
|
1854
|
+
describe("KubernetesAgentProvisioner — pod service account (COS-48.1)", () => {
|
|
1855
|
+
const agentId = "cmqalfjcm000m4101iharq28k";
|
|
1856
|
+
const resourceName = sanitizeAgentName(agentId);
|
|
1857
|
+
|
|
1858
|
+
async function desiredSa(
|
|
1859
|
+
config: KubernetesAgentProvisionerConfig,
|
|
1860
|
+
target: { podServiceAccountName?: string | null },
|
|
1861
|
+
) {
|
|
1862
|
+
const k8s = new RecordedKubernetesClient({
|
|
1863
|
+
deployments: {},
|
|
1864
|
+
secrets: {},
|
|
1865
|
+
pvcs: {},
|
|
1866
|
+
});
|
|
1867
|
+
const provisioner = new KubernetesAgentProvisioner(
|
|
1868
|
+
k8s,
|
|
1869
|
+
stubTokens() as AgentTokenService,
|
|
1870
|
+
config,
|
|
1871
|
+
);
|
|
1872
|
+
await provisioner.reconcile([{ id: agentId, ...target }]);
|
|
1873
|
+
const dep = await k8s.getDeployment(NAMESPACE, resourceName);
|
|
1874
|
+
return dep.spec.template.spec.serviceAccountName;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
it("renders the Agent row value when the flag is on", async () => {
|
|
1878
|
+
expect(
|
|
1879
|
+
await desiredSa(
|
|
1880
|
+
{ ...BASE_CONFIG, podServiceAccount: { enabled: true } },
|
|
1881
|
+
{ podServiceAccountName: "agent-sa" },
|
|
1882
|
+
),
|
|
1883
|
+
).toBe("agent-sa");
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
it("never renders the Agent row value when the flag is off", async () => {
|
|
1887
|
+
expect(
|
|
1888
|
+
await desiredSa(
|
|
1889
|
+
{ ...BASE_CONFIG, podServiceAccount: { enabled: false } },
|
|
1890
|
+
{ podServiceAccountName: "agent-sa" },
|
|
1891
|
+
),
|
|
1892
|
+
).toBeUndefined();
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
it("never renders the value when no flag config is supplied", async () => {
|
|
1896
|
+
expect(
|
|
1897
|
+
await desiredSa(BASE_CONFIG, { podServiceAccountName: "agent-sa" }),
|
|
1898
|
+
).toBeUndefined();
|
|
1899
|
+
});
|
|
1900
|
+
});
|
package/src/agents.ts
CHANGED
|
@@ -76,6 +76,8 @@ export interface AgentRecord {
|
|
|
76
76
|
* strips them from its response.
|
|
77
77
|
*/
|
|
78
78
|
export interface AgentSummary extends Partial<AgentAnthropicIdentity> {
|
|
79
|
+
/** COS-48.1 pod ServiceAccount (read by reconcile when the flag is on). */
|
|
80
|
+
podServiceAccountName?: string | null;
|
|
79
81
|
id: string;
|
|
80
82
|
name: string;
|
|
81
83
|
selfHosted: boolean;
|
|
@@ -255,6 +257,7 @@ const SUMMARY_SELECT = {
|
|
|
255
257
|
anthropicServiceAccountId: true,
|
|
256
258
|
anthropicFederationRuleId: true,
|
|
257
259
|
k8sServiceAccountName: true,
|
|
260
|
+
podServiceAccountName: true,
|
|
258
261
|
} as const;
|
|
259
262
|
|
|
260
263
|
const DETAIL_SELECT = {
|
|
@@ -287,6 +290,7 @@ const DETAIL_SELECT = {
|
|
|
287
290
|
anthropicServiceAccountId: true,
|
|
288
291
|
anthropicFederationRuleId: true,
|
|
289
292
|
k8sServiceAccountName: true,
|
|
293
|
+
podServiceAccountName: true,
|
|
290
294
|
} as const;
|
|
291
295
|
|
|
292
296
|
// ─── Service ──────────────────────────────────────────────────────────────────
|
package/src/main.unit.test.ts
CHANGED
|
@@ -79,6 +79,25 @@ describe("resolvePublicRepo", () => {
|
|
|
79
79
|
// ─── buildProvisioner ───────────────────────────────────────────────────────
|
|
80
80
|
|
|
81
81
|
describe("buildProvisioner", () => {
|
|
82
|
+
it("wires the pod ServiceAccount flag into the provisioner config", () => {
|
|
83
|
+
const on = buildProvisioner(
|
|
84
|
+
{
|
|
85
|
+
SHIPWRIGHT_K8S_PROVISIONING: "enabled",
|
|
86
|
+
SHIPWRIGHT_K8S_AGENT_POD_SERVICE_ACCOUNT_ENABLED: "enabled",
|
|
87
|
+
},
|
|
88
|
+
stubAgentTokenService(),
|
|
89
|
+
);
|
|
90
|
+
expect(configOf(on).podServiceAccount).toEqual({ enabled: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("leaves the pod ServiceAccount flag off by default", () => {
|
|
94
|
+
const off = buildProvisioner(
|
|
95
|
+
{ SHIPWRIGHT_K8S_PROVISIONING: "enabled" },
|
|
96
|
+
stubAgentTokenService(),
|
|
97
|
+
);
|
|
98
|
+
expect(configOf(off).podServiceAccount).toEqual({ enabled: false });
|
|
99
|
+
});
|
|
100
|
+
|
|
82
101
|
it("returns a NoopAgentProvisioner when SHIPWRIGHT_K8S_PROVISIONING is unset", () => {
|
|
83
102
|
const provisioner = buildProvisioner({}, stubAgentTokenService());
|
|
84
103
|
expect(provisioner).toBeInstanceOf(NoopAgentProvisioner);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/pod-service-account-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure parser for the per-agent pod ServiceAccount flag (COS-48.1). Takes an
|
|
5
|
+
* injected env object (no process.env reads) so it is deterministically
|
|
6
|
+
* unit-testable; admin-boot-config threads the resolved config into the
|
|
7
|
+
* provisioner. Same "enabled" convention as agent-ask-config.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Structural env shape — accepts `process.env` or a plain test object. */
|
|
11
|
+
type PodServiceAccountEnv = Readonly<Record<string, string | undefined>>;
|
|
12
|
+
|
|
13
|
+
export interface PodServiceAccountConfig {
|
|
14
|
+
/** True only when SHIPWRIGHT_K8S_AGENT_POD_SERVICE_ACCOUNT_ENABLED === "enabled". */
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parsePodServiceAccountConfig(
|
|
19
|
+
env: PodServiceAccountEnv,
|
|
20
|
+
): PodServiceAccountConfig {
|
|
21
|
+
return {
|
|
22
|
+
enabled: env.SHIPWRIGHT_K8S_AGENT_POD_SERVICE_ACCOUNT_ENABLED === "enabled",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/pod-service-account-config.unit.test.ts
|
|
3
|
+
* Pure parser tests for the per-agent pod ServiceAccount flag (COS-48.1).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "bun:test";
|
|
7
|
+
import { parsePodServiceAccountConfig } from "./pod-service-account-config.ts";
|
|
8
|
+
|
|
9
|
+
describe("parsePodServiceAccountConfig", () => {
|
|
10
|
+
it('is on only for the literal "enabled"', () => {
|
|
11
|
+
expect(
|
|
12
|
+
parsePodServiceAccountConfig({
|
|
13
|
+
SHIPWRIGHT_K8S_AGENT_POD_SERVICE_ACCOUNT_ENABLED: "enabled",
|
|
14
|
+
}).enabled,
|
|
15
|
+
).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('"true", "1", "Enabled", padded, empty and absent are off', () => {
|
|
19
|
+
for (const value of ["true", "1", "Enabled", " enabled", "", undefined]) {
|
|
20
|
+
expect(
|
|
21
|
+
parsePodServiceAccountConfig({
|
|
22
|
+
SHIPWRIGHT_K8S_AGENT_POD_SERVICE_ACCOUNT_ENABLED: value,
|
|
23
|
+
}).enabled,
|
|
24
|
+
).toBe(false);
|
|
25
|
+
}
|
|
26
|
+
expect(parsePodServiceAccountConfig({}).enabled).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -50,6 +50,7 @@ export function managedReconcileTargets(
|
|
|
50
50
|
anthropicServiceAccountId: a.anthropicServiceAccountId,
|
|
51
51
|
anthropicFederationRuleId: a.anthropicFederationRuleId,
|
|
52
52
|
k8sServiceAccountName: a.k8sServiceAccountName,
|
|
53
|
+
podServiceAccountName: a.podServiceAccountName,
|
|
53
54
|
}));
|
|
54
55
|
}
|
|
55
56
|
|