@rulebricks/cli 2.1.7 → 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.
- package/README.md +51 -16
- package/cluster-setup/aws/README.md +96 -47
- package/cluster-setup/aws/check-aws-access.sh +216 -52
- package/cluster-setup/aws/parameters.json +13 -0
- package/cluster-setup/aws/rulebricks-cluster.cfn.yaml +355 -0
- package/cluster-setup/azure/README.md +103 -55
- package/cluster-setup/azure/check-aks-prereqs.sh +236 -56
- package/cluster-setup/azure/parameters.json +30 -0
- package/cluster-setup/azure/rulebricks-cluster.bicep +546 -0
- package/cluster-setup/gcp/README.md +51 -34
- package/cluster-setup/gcp/check-gke-prereqs.sh +222 -60
- package/dist/commands/backup.d.ts +5 -0
- package/dist/commands/backup.js +104 -0
- package/dist/commands/deploy.d.ts +3 -1
- package/dist/commands/deploy.js +226 -326
- package/dist/commands/destroy.d.ts +1 -1
- package/dist/commands/destroy.js +73 -123
- package/dist/commands/init.d.ts +5 -1
- package/dist/commands/init.js +78 -54
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +74 -0
- package/dist/commands/open.d.ts +1 -1
- package/dist/commands/open.js +4 -12
- package/dist/commands/redeploy.d.ts +6 -0
- package/dist/commands/redeploy.js +310 -0
- package/dist/commands/restore.d.ts +5 -0
- package/dist/commands/restore.js +338 -0
- package/dist/commands/status.js +62 -49
- package/dist/commands/upgrade.js +74 -51
- package/dist/components/DNSWaitScreen.d.ts +5 -1
- package/dist/components/DNSWaitScreen.js +47 -41
- package/dist/components/Wizard/WizardContext.d.ts +157 -36
- package/dist/components/Wizard/WizardContext.js +872 -160
- package/dist/components/Wizard/steps/CloudProviderStep.js +192 -107
- package/dist/components/Wizard/steps/DomainStep.js +5 -24
- package/dist/components/Wizard/steps/ExternalServicesStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ExternalServicesStep.js +645 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +2 -1
- package/dist/components/Wizard/steps/FeatureConfigStep.js +739 -425
- package/dist/components/Wizard/steps/FeaturesStep.js +31 -35
- package/dist/components/Wizard/steps/ObservabilityStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ObservabilityStep.js +137 -0
- package/dist/components/Wizard/steps/ReviewStep.d.ts +2 -1
- package/dist/components/Wizard/steps/ReviewStep.js +56 -12
- package/dist/components/Wizard/steps/StorageStep.d.ts +9 -0
- package/dist/components/Wizard/steps/StorageStep.js +592 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +20 -21
- package/dist/components/Wizard/steps/VersionStep.js +45 -23
- package/dist/components/Wizard/steps/index.d.ts +3 -3
- package/dist/components/Wizard/steps/index.js +3 -3
- package/dist/components/common/CommandApproval.d.ts +12 -0
- package/dist/components/common/CommandApproval.js +91 -0
- package/dist/components/common/DeploymentPicker.d.ts +14 -0
- package/dist/components/common/DeploymentPicker.js +16 -0
- package/dist/components/common/index.d.ts +2 -0
- package/dist/components/common/index.js +2 -0
- package/dist/index.js +94 -62
- package/dist/lib/cloudCli.d.ts +134 -63
- package/dist/lib/cloudCli.js +512 -220
- package/dist/lib/clusterSetupDefaults.d.ts +30 -0
- package/dist/lib/clusterSetupDefaults.js +64 -0
- package/dist/lib/commandApproval.d.ts +26 -0
- package/dist/lib/commandApproval.js +114 -0
- package/dist/lib/config.d.ts +12 -10
- package/dist/lib/config.js +91 -33
- package/dist/lib/configFixtures.d.ts +5 -0
- package/dist/lib/configFixtures.js +513 -0
- package/dist/lib/deploymentHealth.d.ts +32 -0
- package/dist/lib/deploymentHealth.js +157 -0
- package/dist/lib/dns.d.ts +1 -1
- package/dist/lib/dns.js +19 -1
- package/dist/lib/dns.test.d.ts +1 -0
- package/dist/lib/dns.test.js +27 -0
- package/dist/lib/dockerHub.d.ts +12 -1
- package/dist/lib/dockerHub.js +18 -8
- package/dist/lib/helm.d.ts +4 -0
- package/dist/lib/helm.js +16 -0
- package/dist/lib/helmValues.d.ts +25 -0
- package/dist/lib/helmValues.js +1762 -289
- package/dist/lib/helmValues.test.d.ts +1 -0
- package/dist/lib/helmValues.test.js +966 -0
- package/dist/lib/htpasswd.d.ts +1 -0
- package/dist/lib/htpasswd.js +15 -0
- package/dist/lib/kubernetes.d.ts +124 -17
- package/dist/lib/kubernetes.js +576 -145
- package/dist/lib/secrets.d.ts +23 -0
- package/dist/lib/secrets.js +158 -0
- package/dist/lib/validateValues.d.ts +31 -0
- package/dist/lib/validateValues.js +253 -0
- package/dist/lib/versions.d.ts +82 -11
- package/dist/lib/versions.js +131 -31
- package/dist/lib/versions.test.d.ts +1 -0
- package/dist/lib/versions.test.js +81 -0
- package/dist/lib/wizardSteps.d.ts +14 -0
- package/dist/lib/wizardSteps.js +23 -0
- package/dist/lib/workloadIdentity.d.ts +26 -0
- package/dist/lib/workloadIdentity.js +323 -0
- package/dist/lib/workloadIdentity.test.d.ts +1 -0
- package/dist/lib/workloadIdentity.test.js +57 -0
- package/dist/types/index.d.ts +1860 -164
- package/dist/types/index.js +518 -295
- package/package.json +9 -4
- package/schema/values.schema.json +1934 -0
- package/cluster-setup/aws/cluster.yaml +0 -33
- package/cluster-setup/azure/main.bicep +0 -282
- package/cluster-setup/azure/main.parameters.json +0 -21
- package/dist/components/Wizard/steps/CredentialsStep.d.ts +0 -6
- package/dist/components/Wizard/steps/CredentialsStep.js +0 -22
- package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +0 -5
- package/dist/components/Wizard/steps/DeploymentModeStep.js +0 -26
- package/dist/components/Wizard/steps/TierStep.d.ts +0 -6
- package/dist/components/Wizard/steps/TierStep.js +0 -29
- package/dist/lib/terraform.d.ts +0 -66
- package/dist/lib/terraform.js +0 -754
- package/terraform/aws/main.tf +0 -355
- package/terraform/azure/main.tf +0 -371
- 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
|
+
});
|