@rulebricks/cli 2.1.6 → 2.3.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.
Files changed (114) hide show
  1. package/README.md +75 -14
  2. package/cluster-setup/aws/README.md +123 -0
  3. package/cluster-setup/aws/check-aws-access.sh +242 -0
  4. package/cluster-setup/aws/parameters.json +13 -0
  5. package/cluster-setup/aws/rulebricks-cluster.cfn.yaml +355 -0
  6. package/cluster-setup/azure/README.md +141 -0
  7. package/cluster-setup/azure/check-aks-prereqs.sh +276 -0
  8. package/cluster-setup/azure/parameters.json +30 -0
  9. package/cluster-setup/azure/rulebricks-cluster.bicep +546 -0
  10. package/cluster-setup/gcp/README.md +189 -0
  11. package/cluster-setup/gcp/check-gke-prereqs.sh +260 -0
  12. package/dist/commands/backup.d.ts +5 -0
  13. package/dist/commands/backup.js +104 -0
  14. package/dist/commands/deploy.d.ts +3 -1
  15. package/dist/commands/deploy.js +226 -326
  16. package/dist/commands/destroy.d.ts +1 -1
  17. package/dist/commands/destroy.js +73 -123
  18. package/dist/commands/init.d.ts +5 -1
  19. package/dist/commands/init.js +78 -47
  20. package/dist/commands/list.d.ts +1 -0
  21. package/dist/commands/list.js +74 -0
  22. package/dist/commands/open.d.ts +1 -1
  23. package/dist/commands/open.js +4 -12
  24. package/dist/commands/redeploy.d.ts +6 -0
  25. package/dist/commands/redeploy.js +310 -0
  26. package/dist/commands/restore.d.ts +5 -0
  27. package/dist/commands/restore.js +338 -0
  28. package/dist/commands/status.js +62 -49
  29. package/dist/commands/upgrade.js +74 -51
  30. package/dist/components/DNSWaitScreen.d.ts +5 -1
  31. package/dist/components/DNSWaitScreen.js +47 -41
  32. package/dist/components/Wizard/WizardContext.d.ts +174 -29
  33. package/dist/components/Wizard/WizardContext.js +896 -91
  34. package/dist/components/Wizard/steps/CloudProviderStep.js +192 -102
  35. package/dist/components/Wizard/steps/DomainStep.js +5 -24
  36. package/dist/components/Wizard/steps/ExternalServicesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/ExternalServicesStep.js +645 -0
  38. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +2 -1
  39. package/dist/components/Wizard/steps/FeatureConfigStep.js +959 -248
  40. package/dist/components/Wizard/steps/FeaturesStep.js +31 -35
  41. package/dist/components/Wizard/steps/ObservabilityStep.d.ts +6 -0
  42. package/dist/components/Wizard/steps/ObservabilityStep.js +137 -0
  43. package/dist/components/Wizard/steps/ReviewStep.d.ts +2 -1
  44. package/dist/components/Wizard/steps/ReviewStep.js +56 -7
  45. package/dist/components/Wizard/steps/StorageStep.d.ts +9 -0
  46. package/dist/components/Wizard/steps/StorageStep.js +592 -0
  47. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +20 -21
  48. package/dist/components/Wizard/steps/VersionStep.js +45 -23
  49. package/dist/components/Wizard/steps/index.d.ts +3 -3
  50. package/dist/components/Wizard/steps/index.js +3 -3
  51. package/dist/components/common/CommandApproval.d.ts +12 -0
  52. package/dist/components/common/CommandApproval.js +91 -0
  53. package/dist/components/common/DeploymentPicker.d.ts +14 -0
  54. package/dist/components/common/DeploymentPicker.js +16 -0
  55. package/dist/components/common/index.d.ts +2 -0
  56. package/dist/components/common/index.js +2 -0
  57. package/dist/index.js +94 -62
  58. package/dist/lib/cloudCli.d.ts +134 -63
  59. package/dist/lib/cloudCli.js +512 -220
  60. package/dist/lib/clusterSetupDefaults.d.ts +30 -0
  61. package/dist/lib/clusterSetupDefaults.js +64 -0
  62. package/dist/lib/commandApproval.d.ts +26 -0
  63. package/dist/lib/commandApproval.js +114 -0
  64. package/dist/lib/config.d.ts +12 -10
  65. package/dist/lib/config.js +91 -33
  66. package/dist/lib/configFixtures.d.ts +5 -0
  67. package/dist/lib/configFixtures.js +513 -0
  68. package/dist/lib/deploymentHealth.d.ts +32 -0
  69. package/dist/lib/deploymentHealth.js +157 -0
  70. package/dist/lib/dns.d.ts +1 -1
  71. package/dist/lib/dns.js +19 -1
  72. package/dist/lib/dns.test.d.ts +1 -0
  73. package/dist/lib/dns.test.js +27 -0
  74. package/dist/lib/dockerHub.d.ts +12 -1
  75. package/dist/lib/dockerHub.js +18 -8
  76. package/dist/lib/helm.d.ts +4 -0
  77. package/dist/lib/helm.js +16 -0
  78. package/dist/lib/helmValues.d.ts +25 -0
  79. package/dist/lib/helmValues.js +1937 -259
  80. package/dist/lib/helmValues.test.d.ts +1 -0
  81. package/dist/lib/helmValues.test.js +966 -0
  82. package/dist/lib/htpasswd.d.ts +1 -0
  83. package/dist/lib/htpasswd.js +15 -0
  84. package/dist/lib/kubernetes.d.ts +126 -13
  85. package/dist/lib/kubernetes.js +624 -134
  86. package/dist/lib/secrets.d.ts +23 -0
  87. package/dist/lib/secrets.js +158 -0
  88. package/dist/lib/validateValues.d.ts +31 -0
  89. package/dist/lib/validateValues.js +253 -0
  90. package/dist/lib/versions.d.ts +82 -11
  91. package/dist/lib/versions.js +131 -31
  92. package/dist/lib/versions.test.d.ts +1 -0
  93. package/dist/lib/versions.test.js +81 -0
  94. package/dist/lib/wizardSteps.d.ts +14 -0
  95. package/dist/lib/wizardSteps.js +23 -0
  96. package/dist/lib/workloadIdentity.d.ts +26 -0
  97. package/dist/lib/workloadIdentity.js +323 -0
  98. package/dist/lib/workloadIdentity.test.d.ts +1 -0
  99. package/dist/lib/workloadIdentity.test.js +57 -0
  100. package/dist/types/index.d.ts +2152 -95
  101. package/dist/types/index.js +554 -286
  102. package/package.json +10 -4
  103. package/schema/values.schema.json +1934 -0
  104. package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
  105. package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
  106. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
  107. package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
  108. package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
  109. package/dist/components/Wizard/steps/TierStep.js +0 -29
  110. package/dist/lib/terraform.d.ts +0 -66
  111. package/dist/lib/terraform.js +0 -754
  112. package/terraform/aws/main.tf +0 -355
  113. package/terraform/azure/main.tf +0 -371
  114. package/terraform/gcp/main.tf +0 -407
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Deploy-time workload-identity federation.
3
+ *
4
+ * cluster-setup provisions the deployment-independent infrastructure (one
5
+ * identity / role / service account, plus the bucket and DCR). The trust between
6
+ * that identity and a specific Kubernetes ServiceAccount is namespace-scoped, so
7
+ * it can only be created once the deployment namespace is known. This module
8
+ * creates that trust at `rulebricks deploy` time, which keeps cluster-setup
9
+ * generic and lets one cluster host many deployments.
10
+ *
11
+ * Azure -> federated identity credential (subject = system:serviceaccount:ns:sa)
12
+ * AWS -> EKS Pod Identity association (namespace + serviceAccount -> role)
13
+ * GCP -> IAM workloadIdentityUser binding (member = ns/sa -> service account)
14
+ *
15
+ * All operations are idempotent, so it is safe to run on every deploy.
16
+ */
17
+ import { exec } from "child_process";
18
+ import { promisify } from "util";
19
+ import { getNamespace, getReleaseName, } from "../types/index.js";
20
+ import { approveCloudCommandOrThrow } from "./commandApproval.js";
21
+ const execAsync = promisify(exec);
22
+ const CLI_TIMEOUT = 60000;
23
+ async function run(command, options) {
24
+ await approveCloudCommandOrThrow({
25
+ command,
26
+ intent: options.intent,
27
+ provider: options.provider,
28
+ mutating: options.mutating,
29
+ });
30
+ try {
31
+ const { stdout, stderr } = await execAsync(command, { timeout: CLI_TIMEOUT });
32
+ return { stdout, stderr, code: 0 };
33
+ }
34
+ catch (error) {
35
+ const e = error;
36
+ return {
37
+ stdout: e.stdout || "",
38
+ stderr: e.stderr || e.message || "command failed",
39
+ code: typeof e.code === "number" ? e.code : 1,
40
+ };
41
+ }
42
+ }
43
+ function shq(value) {
44
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
45
+ }
46
+ export function isAwsPodIdentityCliUnsupported(stderr) {
47
+ return (/Invalid choice/i.test(stderr) &&
48
+ !/list-pod-identity-associations|create-pod-identity-association/i.test(stderr));
49
+ }
50
+ export function isAwsPodIdentityTrustPolicyInvalid(stderr) {
51
+ return /InvalidParameterException/i.test(stderr) && /Trust policy/i.test(stderr);
52
+ }
53
+ function awsPodIdentityUnsupportedMessage(stderr) {
54
+ const detail = stderr.trim().split("\n").slice(0, 4).join("\n");
55
+ return [
56
+ "Your installed AWS CLI does not support EKS Pod Identity association commands.",
57
+ "",
58
+ "Rulebricks AWS cluster setup uses EKS Pod Identity, so deploy needs AWS CLI v2 with:",
59
+ " aws eks list-pod-identity-associations",
60
+ " aws eks create-pod-identity-association",
61
+ "",
62
+ "Update or install AWS CLI v2, then rerun the deploy/init command.",
63
+ "",
64
+ "First check which AWS CLI your shell is using:",
65
+ " which aws && aws --version",
66
+ "",
67
+ "On macOS with Homebrew:",
68
+ " brew install awscli",
69
+ " # or, if Homebrew already owns it:",
70
+ " brew upgrade awscli",
71
+ "",
72
+ "Or install the official AWS CLI v2 package:",
73
+ " curl \"https://awscli.amazonaws.com/AWSCLIV2.pkg\" -o \"/tmp/AWSCLIV2.pkg\"",
74
+ " sudo installer -pkg /tmp/AWSCLIV2.pkg -target /",
75
+ "",
76
+ "If aws --version still shows an older binary after installing, update your PATH so the new aws comes first.",
77
+ "",
78
+ detail ? `AWS CLI output:\n${detail}` : "",
79
+ ]
80
+ .filter(Boolean)
81
+ .join("\n");
82
+ }
83
+ function awsPodIdentityInvalidTrustMessage(input) {
84
+ const expectedRole = `${input.cluster}-rulebricks`;
85
+ const detail = input.stderr.trim();
86
+ return [
87
+ `The IAM role selected for ${input.subject} cannot be used with EKS Pod Identity.`,
88
+ "",
89
+ `Selected role: ${input.roleArn}`,
90
+ `Expected role from Rulebricks cluster-setup: ${expectedRole}`,
91
+ "",
92
+ "The role trust policy must allow the EKS Pod Identity service principal:",
93
+ " Principal: { Service: pods.eks.amazonaws.com }",
94
+ " Actions: sts:AssumeRole and sts:TagSession",
95
+ "",
96
+ "Fix by selecting the RulebricksRoleArn output from the AWS cluster-setup stack,",
97
+ `or update that role's trust policy to match cluster-setup/aws/rulebricks-cluster.cfn.yaml.`,
98
+ "",
99
+ detail ? `AWS CLI output:\n${detail}` : "",
100
+ ]
101
+ .filter(Boolean)
102
+ .join("\n");
103
+ }
104
+ /**
105
+ * The SAs that need workload-identity trust, given the deployment config. Vector
106
+ * and the backup job use the storage identity; Prometheus uses the metrics
107
+ * identity (the consolidated setup makes these the same principal, but we read
108
+ * them independently so split setups still work).
109
+ */
110
+ export function plannedBindings(config) {
111
+ const bindings = [];
112
+ const storage = config.storage;
113
+ const releaseName = getReleaseName(config.name);
114
+ const usesSecretAuth = storage?.cloudAuthMode === "secret";
115
+ const storagePrincipal = storage?.provider === "s3"
116
+ ? storage.awsIamRoleArn
117
+ : storage?.provider === "gcs"
118
+ ? storage.gcpServiceAccountEmail
119
+ : storage?.azureBlobClientId;
120
+ if (storage && !usesSecretAuth && storagePrincipal) {
121
+ bindings.push({ serviceAccount: "vector", principal: storagePrincipal });
122
+ // ClickHouse reads the decision-log archive straight from object storage
123
+ // (the rulebricks.decision_logs view / named collection), so it needs the
124
+ // same storage identity as Vector. Without this trust the cloud IdP rejects
125
+ // ClickHouse's token and every decision_logs query fails to authenticate.
126
+ bindings.push({
127
+ serviceAccount: `${releaseName}-clickhouse`,
128
+ principal: storagePrincipal,
129
+ });
130
+ if (config.backup?.enabled && config.database.type === "self-hosted") {
131
+ bindings.push({
132
+ serviceAccount: `${releaseName}-backup`,
133
+ principal: storagePrincipal,
134
+ });
135
+ }
136
+ }
137
+ // Workloads that talk directly to the managed broker each need the Kafka cloud
138
+ // identity under a token mechanism (AWS MSK IAM / GCP OAUTHBEARER). We give each
139
+ // its OWN service account and bind it here via Pod Identity - the chart no
140
+ // longer stamps an eks.amazonaws.com/role-arn annotation, so the association is
141
+ // the single source of credentials (no IRSA/annotation tug-of-war on a shared
142
+ // SA). HPS + the worker fleet produce/consume; the kafka-topic-provision
143
+ // pre-install hook creates the topics. (When no identity role is set the broker
144
+ // uses SCRAM/PLAIN secret auth, so there is no principal to bind.)
145
+ const kafka = config.externalServices?.kafka;
146
+ const kafkaPrincipal = kafka?.mode === "external"
147
+ ? (kafka.external?.identity?.awsRoleArn ??
148
+ kafka.external?.identity?.gcpServiceAccountEmail ??
149
+ kafka.external?.identity?.azureClientId)
150
+ : undefined;
151
+ if (kafkaPrincipal) {
152
+ for (const serviceAccount of [
153
+ `${releaseName}-hps`,
154
+ `${releaseName}-hps-worker`,
155
+ `${releaseName}-kafka-topic-provision`,
156
+ ]) {
157
+ bindings.push({ serviceAccount, principal: kafkaPrincipal });
158
+ }
159
+ }
160
+ const rw = config.features.monitoring?.remoteWrite;
161
+ const metricsPrincipal = rw?.destination === "aws-amp"
162
+ ? rw.awsRoleArn
163
+ : rw?.authType === "workload-identity"
164
+ ? rw.clientId
165
+ : undefined;
166
+ if (config.features.monitoring?.enabled &&
167
+ rw &&
168
+ rw.destination !== "generic" &&
169
+ rw.destination !== "grafana-cloud" &&
170
+ metricsPrincipal) {
171
+ bindings.push({ serviceAccount: "prometheus", principal: metricsPrincipal });
172
+ }
173
+ return bindings;
174
+ }
175
+ /**
176
+ * Ensures the per-namespace workload-identity trust exists for this deployment.
177
+ * No-op (with a `skipped` reason) for non-cloud providers or secret-based auth.
178
+ */
179
+ export async function ensureWorkloadIdentityFederation(config) {
180
+ const provider = config.infrastructure.provider;
181
+ if (provider !== "azure" && provider !== "aws" && provider !== "gcp") {
182
+ return { created: [], existing: [], skipped: "non-cloud provider" };
183
+ }
184
+ const bindings = plannedBindings(config);
185
+ if (bindings.length === 0) {
186
+ return { created: [], existing: [], skipped: "no workload-identity service accounts" };
187
+ }
188
+ const namespace = getNamespace(config.name);
189
+ switch (provider) {
190
+ case "azure":
191
+ return ensureAzure(config, namespace, bindings);
192
+ case "aws":
193
+ return ensureAws(config, namespace, bindings);
194
+ case "gcp":
195
+ return ensureGcp(config, namespace, bindings);
196
+ default:
197
+ return { created: [], existing: [], skipped: "non-cloud provider" };
198
+ }
199
+ }
200
+ // ---------------------------------------------------------------------------
201
+ // Azure: federated identity credentials on the user-assigned managed identity
202
+ // ---------------------------------------------------------------------------
203
+ async function ensureAzure(config, namespace, bindings) {
204
+ const rg = config.infrastructure.azureResourceGroup;
205
+ const cluster = config.infrastructure.clusterName;
206
+ if (!rg || !cluster) {
207
+ throw new Error("Azure resource group and cluster name are required to create federated credentials.");
208
+ }
209
+ const intent = "Configure workload identity (Azure)";
210
+ const issuerRes = await run(`az aks show --name ${shq(cluster)} --resource-group ${shq(rg)} --query oidcIssuerProfile.issuerUrl --output tsv`, { intent, provider: "azure" });
211
+ const issuer = issuerRes.stdout.trim();
212
+ if (!issuer) {
213
+ throw new Error(`Could not read the AKS OIDC issuer for ${cluster}/${rg}. Ensure the cluster has the OIDC issuer enabled. (${issuerRes.stderr.trim()})`);
214
+ }
215
+ // Resolve identity name once per distinct clientId (principal).
216
+ const identityNameByClientId = new Map();
217
+ const created = [];
218
+ const existing = [];
219
+ for (const binding of bindings) {
220
+ const clientId = binding.principal;
221
+ let identityName = identityNameByClientId.get(clientId);
222
+ if (!identityName) {
223
+ const nameRes = await run(`az identity list --resource-group ${shq(rg)} --query "[?clientId=='${clientId}'].name | [0]" --output tsv`, { intent, provider: "azure" });
224
+ identityName = nameRes.stdout.trim();
225
+ if (!identityName) {
226
+ throw new Error(`No user-assigned identity with client ID ${clientId} found in resource group ${rg}. Run cluster-setup first.`);
227
+ }
228
+ identityNameByClientId.set(clientId, identityName);
229
+ }
230
+ const subject = `system:serviceaccount:${namespace}:${binding.serviceAccount}`;
231
+ // Unique per (namespace, SA) so several deployments can share one identity.
232
+ const ficName = `${namespace}-${binding.serviceAccount}`.slice(0, 120);
233
+ const listRes = await run(`az identity federated-credential list --identity-name ${shq(identityName)} --resource-group ${shq(rg)} --query "[?subject=='${subject}'] | length(@)" --output tsv`, { intent, provider: "azure" });
234
+ if (listRes.stdout.trim() !== "0" && listRes.stdout.trim() !== "") {
235
+ existing.push(subject);
236
+ continue;
237
+ }
238
+ const createRes = await run(`az identity federated-credential create --name ${shq(ficName)} ` +
239
+ `--identity-name ${shq(identityName)} --resource-group ${shq(rg)} ` +
240
+ `--issuer ${shq(issuer)} --subject ${shq(subject)} ` +
241
+ `--audiences api://AzureADTokenExchange`, { intent, provider: "azure", mutating: true });
242
+ if (createRes.code !== 0) {
243
+ throw new Error(`Failed to create federated credential for ${subject}: ${createRes.stderr.trim()}`);
244
+ }
245
+ created.push(subject);
246
+ }
247
+ return { created, existing };
248
+ }
249
+ // ---------------------------------------------------------------------------
250
+ // AWS: EKS Pod Identity associations
251
+ // ---------------------------------------------------------------------------
252
+ async function ensureAws(config, namespace, bindings) {
253
+ const cluster = config.infrastructure.clusterName;
254
+ const region = config.infrastructure.region;
255
+ if (!cluster || !region) {
256
+ throw new Error("EKS cluster name and region are required to create Pod Identity associations.");
257
+ }
258
+ const created = [];
259
+ const existing = [];
260
+ const intent = "Configure workload identity (AWS)";
261
+ for (const binding of bindings) {
262
+ const roleArn = binding.principal;
263
+ const subject = `${namespace}/${binding.serviceAccount}`;
264
+ const listRes = await run(`aws eks list-pod-identity-associations --cluster-name ${shq(cluster)} ` +
265
+ `--namespace ${shq(namespace)} --service-account ${shq(binding.serviceAccount)} ` +
266
+ `--region ${shq(region)} --query "associations | length(@)" --output text`, { intent, provider: "aws" });
267
+ if (listRes.code !== 0 && isAwsPodIdentityCliUnsupported(listRes.stderr)) {
268
+ throw new Error(awsPodIdentityUnsupportedMessage(listRes.stderr));
269
+ }
270
+ if (listRes.code === 0 && listRes.stdout.trim() !== "0" && listRes.stdout.trim() !== "") {
271
+ existing.push(subject);
272
+ continue;
273
+ }
274
+ const createRes = await run(`aws eks create-pod-identity-association --cluster-name ${shq(cluster)} ` +
275
+ `--namespace ${shq(namespace)} --service-account ${shq(binding.serviceAccount)} ` +
276
+ `--role-arn ${shq(roleArn)} --region ${shq(region)}`, { intent, provider: "aws", mutating: true });
277
+ if (createRes.code !== 0) {
278
+ if (isAwsPodIdentityCliUnsupported(createRes.stderr)) {
279
+ throw new Error(awsPodIdentityUnsupportedMessage(createRes.stderr));
280
+ }
281
+ if (isAwsPodIdentityTrustPolicyInvalid(createRes.stderr)) {
282
+ throw new Error(awsPodIdentityInvalidTrustMessage({
283
+ stderr: createRes.stderr,
284
+ subject,
285
+ roleArn,
286
+ cluster,
287
+ }));
288
+ }
289
+ // Treat an existing association as success (race / prior run).
290
+ if (/ResourceInUse|already exists/i.test(createRes.stderr)) {
291
+ existing.push(subject);
292
+ continue;
293
+ }
294
+ throw new Error(`Failed to create Pod Identity association for ${subject}: ${createRes.stderr.trim()}`);
295
+ }
296
+ created.push(subject);
297
+ }
298
+ return { created, existing };
299
+ }
300
+ // ---------------------------------------------------------------------------
301
+ // GCP: IAM workloadIdentityUser bindings on the Google service account
302
+ // ---------------------------------------------------------------------------
303
+ async function ensureGcp(config, namespace, bindings) {
304
+ const project = config.infrastructure.gcpProjectId;
305
+ if (!project) {
306
+ throw new Error("GCP project ID is required to create Workload Identity bindings.");
307
+ }
308
+ const created = [];
309
+ const intent = "Configure workload identity (GCP)";
310
+ for (const binding of bindings) {
311
+ const gsa = binding.principal;
312
+ const member = `serviceAccount:${project}.svc.id.goog[${namespace}/${binding.serviceAccount}]`;
313
+ // add-iam-policy-binding is idempotent; re-adding an existing member is a no-op.
314
+ const res = await run(`gcloud iam service-accounts add-iam-policy-binding ${shq(gsa)} ` +
315
+ `--project ${shq(project)} --role roles/iam.workloadIdentityUser ` +
316
+ `--member ${shq(member)} --quiet`, { intent, provider: "gcp", mutating: true });
317
+ if (res.code !== 0) {
318
+ throw new Error(`Failed to bind Workload Identity for ${namespace}/${binding.serviceAccount}: ${res.stderr.trim()}`);
319
+ }
320
+ created.push(`${namespace}/${binding.serviceAccount}`);
321
+ }
322
+ return { created, existing: [] };
323
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { isAwsPodIdentityCliUnsupported, isAwsPodIdentityTrustPolicyInvalid, plannedBindings, } from "./workloadIdentity.js";
4
+ test("detects AWS CLI builds without EKS Pod Identity operations", () => {
5
+ const stderr = `usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
6
+ aws: error: argument operation: Invalid choice, valid choices are:
7
+ associate-encryption-config | create-addon | update-kubeconfig | get-token | wait | help`;
8
+ assert.equal(isAwsPodIdentityCliUnsupported(stderr), true);
9
+ });
10
+ test("does not flag regular Pod Identity command errors as unsupported CLI", () => {
11
+ assert.equal(isAwsPodIdentityCliUnsupported("An error occurred (AccessDeniedException) when calling the CreatePodIdentityAssociation operation"), false);
12
+ assert.equal(isAwsPodIdentityCliUnsupported("An error occurred (ResourceInUseException) when calling the CreatePodIdentityAssociation operation"), false);
13
+ });
14
+ test("detects AWS Pod Identity invalid trust policy failures", () => {
15
+ assert.equal(isAwsPodIdentityTrustPolicyInvalid("An error occurred (InvalidParameterException) when calling the CreatePodIdentityAssociation operation: Trust policy of the role provided is invalid."), true);
16
+ assert.equal(isAwsPodIdentityTrustPolicyInvalid("An error occurred (AccessDeniedException) when calling the CreatePodIdentityAssociation operation"), false);
17
+ });
18
+ test("external MSK IAM binds hps, worker, and topic-provision SAs (one association each)", () => {
19
+ const cfg = {
20
+ name: "aws-p1",
21
+ infrastructure: { provider: "aws", region: "us-east-1" },
22
+ database: { type: "self-hosted" },
23
+ features: { monitoring: {} },
24
+ externalServices: {
25
+ kafka: {
26
+ mode: "external",
27
+ external: {
28
+ preset: "aws-msk-iam",
29
+ identity: {
30
+ awsRoleArn: "arn:aws:iam::123456789012:role/rulebricks-cluster-rulebricks",
31
+ },
32
+ },
33
+ },
34
+ },
35
+ };
36
+ const bindings = plannedBindings(cfg);
37
+ const sas = bindings.map((b) => b.serviceAccount);
38
+ assert.ok(sas.some((s) => s.endsWith("-hps")), sas.join(","));
39
+ assert.ok(sas.some((s) => s.endsWith("-hps-worker")), sas.join(","));
40
+ assert.ok(sas.some((s) => s.endsWith("-kafka-topic-provision")), sas.join(","));
41
+ // Each kafka SA gets exactly one association, to the configured MSK role.
42
+ for (const b of bindings.filter((x) => x.serviceAccount.includes("-hps"))) {
43
+ assert.match(b.principal, /:role\/rulebricks-cluster-rulebricks$/);
44
+ }
45
+ });
46
+ test("embedded kafka creates no HPS/worker kafka bindings", () => {
47
+ const cfg = {
48
+ name: "aws-p1",
49
+ infrastructure: { provider: "aws", region: "us-east-1" },
50
+ database: { type: "self-hosted" },
51
+ features: { monitoring: {} },
52
+ externalServices: { kafka: { mode: "embedded" } },
53
+ };
54
+ const sas = plannedBindings(cfg).map((b) => b.serviceAccount);
55
+ assert.ok(!sas.some((s) => s.endsWith("-hps")), sas.join(","));
56
+ assert.ok(!sas.some((s) => s.endsWith("-hps-worker")), sas.join(","));
57
+ });