@rulebricks/cli 2.1.7 → 2.3.2
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 +1841 -289
- package/dist/lib/helmValues.test.d.ts +1 -0
- package/dist/lib/helmValues.test.js +1012 -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
package/dist/lib/kubernetes.js
CHANGED
|
@@ -194,12 +194,72 @@ function parseMemoryToGi(memory) {
|
|
|
194
194
|
};
|
|
195
195
|
return value * (multipliers[unit] ?? 1 / 1024 ** 3);
|
|
196
196
|
}
|
|
197
|
+
function roundUpForEligibility(value) {
|
|
198
|
+
return Math.ceil(value);
|
|
199
|
+
}
|
|
200
|
+
function normalizeNodeArchitecture(architecture) {
|
|
201
|
+
if (architecture === "amd64" || architecture === "x86_64")
|
|
202
|
+
return "amd64";
|
|
203
|
+
if (architecture === "arm64" || architecture === "aarch64")
|
|
204
|
+
return "arm64";
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function summarizeNodeArchitecture(architectures) {
|
|
208
|
+
if (architectures.size === 0)
|
|
209
|
+
return "unknown";
|
|
210
|
+
if (architectures.size > 1)
|
|
211
|
+
return "mixed";
|
|
212
|
+
return architectures.has("arm64") ? "arm64" : "amd64";
|
|
213
|
+
}
|
|
214
|
+
async function getStorageClasses() {
|
|
215
|
+
try {
|
|
216
|
+
const { stdout } = await execa("kubectl", ["get", "storageclass", "-o", "json"], { timeout: 15000 });
|
|
217
|
+
const data = JSON.parse(stdout);
|
|
218
|
+
return (data.items ?? [])
|
|
219
|
+
.map((storageClass) => {
|
|
220
|
+
const annotations = storageClass.metadata?.annotations ?? {};
|
|
221
|
+
return {
|
|
222
|
+
name: storageClass.metadata?.name || "",
|
|
223
|
+
provisioner: storageClass.provisioner || "",
|
|
224
|
+
isDefault: annotations["storageclass.kubernetes.io/is-default-class"] ===
|
|
225
|
+
"true" ||
|
|
226
|
+
annotations["storageclass.beta.kubernetes.io/is-default-class"] ===
|
|
227
|
+
"true",
|
|
228
|
+
volumeBindingMode: storageClass.volumeBindingMode,
|
|
229
|
+
allowVolumeExpansion: storageClass.allowVolumeExpansion,
|
|
230
|
+
};
|
|
231
|
+
})
|
|
232
|
+
.filter((storageClass) => storageClass.name);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function getPersistentStorageCapacityGi(storageClassName) {
|
|
239
|
+
if (!storageClassName)
|
|
240
|
+
return undefined;
|
|
241
|
+
try {
|
|
242
|
+
const { stdout } = await execa("kubectl", ["get", "csistoragecapacity", "-A", "-o", "json"], { timeout: 15000 });
|
|
243
|
+
const data = JSON.parse(stdout);
|
|
244
|
+
const capacities = data.items
|
|
245
|
+
?.filter((item) => item.storageClassName === storageClassName)
|
|
246
|
+
.map((item) => parseMemoryToGi(item.capacity || "0"))
|
|
247
|
+
.filter((capacity) => capacity > 0) ?? [];
|
|
248
|
+
if (capacities.length === 0)
|
|
249
|
+
return undefined;
|
|
250
|
+
return capacities.reduce((sum, capacity) => sum + capacity, 0);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
197
256
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
257
|
+
* Inspects the current cluster's node architecture, schedulable capacity, and
|
|
258
|
+
* storage classes. The CLI uses this to keep Helm values compatible with the
|
|
259
|
+
* Kubernetes resources the user has already made available (storage class, ARM
|
|
260
|
+
* tolerations, etc.); workload sizing itself follows the chart defaults.
|
|
201
261
|
*/
|
|
202
|
-
export async function
|
|
262
|
+
export async function inferClusterCapabilities() {
|
|
203
263
|
try {
|
|
204
264
|
const { stdout } = await execa("kubectl", ["get", "nodes", "-o", "json"], {
|
|
205
265
|
timeout: 15000,
|
|
@@ -208,17 +268,42 @@ export async function inferClusterTier() {
|
|
|
208
268
|
const schedulableNodes = data.items?.filter((node) => !node.spec?.unschedulable) ?? [];
|
|
209
269
|
let totalCpu = 0;
|
|
210
270
|
let totalMemoryGi = 0;
|
|
271
|
+
let arm64TolerationRequired = false;
|
|
272
|
+
const architectures = new Set();
|
|
211
273
|
for (const node of schedulableNodes) {
|
|
212
274
|
totalCpu += parseCpuToCores(node.status?.allocatable?.cpu || "0");
|
|
213
275
|
totalMemoryGi += parseMemoryToGi(node.status?.allocatable?.memory || "0");
|
|
276
|
+
const architecture = normalizeNodeArchitecture(node.status?.nodeInfo?.architecture ||
|
|
277
|
+
node.metadata?.labels?.["kubernetes.io/arch"] ||
|
|
278
|
+
node.metadata?.labels?.["beta.kubernetes.io/arch"]);
|
|
279
|
+
if (architecture) {
|
|
280
|
+
architectures.add(architecture);
|
|
281
|
+
}
|
|
282
|
+
if (architecture === "arm64" &&
|
|
283
|
+
node.spec?.taints?.some((taint) => taint.key === "kubernetes.io/arch" &&
|
|
284
|
+
taint.value === "arm64" &&
|
|
285
|
+
taint.effect === "NoSchedule")) {
|
|
286
|
+
arm64TolerationRequired = true;
|
|
287
|
+
}
|
|
214
288
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
289
|
+
const storageClasses = await getStorageClasses();
|
|
290
|
+
const defaultStorageClass = storageClasses.find((storageClass) => storageClass.isDefault) ??
|
|
291
|
+
storageClasses[0];
|
|
292
|
+
const totalPersistentStorageGi = await getPersistentStorageCapacityGi(defaultStorageClass?.name);
|
|
293
|
+
return {
|
|
294
|
+
nodeArchitecture: summarizeNodeArchitecture(architectures),
|
|
295
|
+
arm64TolerationRequired,
|
|
296
|
+
schedulableNodeCount: schedulableNodes.length,
|
|
297
|
+
totalCpuCores: totalCpu,
|
|
298
|
+
totalMemoryGi,
|
|
299
|
+
eligibleCpuCores: roundUpForEligibility(totalCpu),
|
|
300
|
+
eligibleMemoryGi: roundUpForEligibility(totalMemoryGi),
|
|
301
|
+
totalPersistentStorageGi,
|
|
302
|
+
storageClasses,
|
|
303
|
+
defaultStorageClass,
|
|
304
|
+
storageClass: defaultStorageClass?.name,
|
|
305
|
+
storageProvisioner: defaultStorageClass?.provisioner,
|
|
306
|
+
};
|
|
222
307
|
}
|
|
223
308
|
catch {
|
|
224
309
|
return null;
|
|
@@ -437,6 +522,203 @@ export async function streamLogs(podName, namespace = DEFAULT_NAMESPACE, options
|
|
|
437
522
|
}
|
|
438
523
|
await execa("kubectl", args, { stdio: "inherit" });
|
|
439
524
|
}
|
|
525
|
+
export async function execInPod(namespace, podName, container, args) {
|
|
526
|
+
const kubectlArgs = ["exec", "-n", namespace, podName];
|
|
527
|
+
if (container) {
|
|
528
|
+
kubectlArgs.push("-c", container);
|
|
529
|
+
}
|
|
530
|
+
kubectlArgs.push("--", ...args);
|
|
531
|
+
try {
|
|
532
|
+
const { stdout } = await execa("kubectl", kubectlArgs);
|
|
533
|
+
return stdout;
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
throw new Error(`Failed to exec into pod ${podName}:\n${getErrorMessage(error)}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
export async function runEphemeralJob(options) {
|
|
540
|
+
const { name, namespace, serviceAccountName, image, command, env = [], volumeMounts = [], volumes = [], initContainers = [], labels = {}, backoffLimit = 0, timeoutSeconds = 3600, } = options;
|
|
541
|
+
const podSpec = {
|
|
542
|
+
restartPolicy: "Never",
|
|
543
|
+
serviceAccountName,
|
|
544
|
+
containers: [
|
|
545
|
+
{
|
|
546
|
+
name: "job",
|
|
547
|
+
image,
|
|
548
|
+
imagePullPolicy: "IfNotPresent",
|
|
549
|
+
command,
|
|
550
|
+
env,
|
|
551
|
+
volumeMounts,
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
volumes,
|
|
555
|
+
};
|
|
556
|
+
if (initContainers.length > 0) {
|
|
557
|
+
podSpec.initContainers = initContainers;
|
|
558
|
+
}
|
|
559
|
+
const manifest = {
|
|
560
|
+
apiVersion: "batch/v1",
|
|
561
|
+
kind: "Job",
|
|
562
|
+
metadata: {
|
|
563
|
+
name,
|
|
564
|
+
namespace,
|
|
565
|
+
labels,
|
|
566
|
+
},
|
|
567
|
+
spec: {
|
|
568
|
+
backoffLimit,
|
|
569
|
+
template: {
|
|
570
|
+
metadata: {
|
|
571
|
+
labels,
|
|
572
|
+
},
|
|
573
|
+
spec: podSpec,
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
try {
|
|
578
|
+
await execa("kubectl", [
|
|
579
|
+
"delete",
|
|
580
|
+
"job",
|
|
581
|
+
name,
|
|
582
|
+
"-n",
|
|
583
|
+
namespace,
|
|
584
|
+
"--ignore-not-found=true",
|
|
585
|
+
]);
|
|
586
|
+
await execa("kubectl", ["apply", "-f", "-"], {
|
|
587
|
+
input: JSON.stringify(manifest),
|
|
588
|
+
});
|
|
589
|
+
await execa("kubectl", [
|
|
590
|
+
"wait",
|
|
591
|
+
"--for=condition=complete",
|
|
592
|
+
`job/${name}`,
|
|
593
|
+
"-n",
|
|
594
|
+
namespace,
|
|
595
|
+
`--timeout=${timeoutSeconds}s`,
|
|
596
|
+
]);
|
|
597
|
+
const logs = await getJobLogs(name, namespace);
|
|
598
|
+
return { jobName: name, logs };
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
const logs = await getJobLogs(name, namespace).catch(() => "");
|
|
602
|
+
const failed = await isJobFailed(name, namespace).catch(() => false);
|
|
603
|
+
if (failed) {
|
|
604
|
+
throw new Error(`Job ${name} failed:\n${logs || getErrorMessage(error)}`);
|
|
605
|
+
}
|
|
606
|
+
throw new Error(`Job ${name} did not complete:\n${logs || getErrorMessage(error)}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
export async function createJobFromCronJob(namespace, cronJobName, jobName) {
|
|
610
|
+
try {
|
|
611
|
+
await execa("kubectl", [
|
|
612
|
+
"delete",
|
|
613
|
+
"job",
|
|
614
|
+
jobName,
|
|
615
|
+
"-n",
|
|
616
|
+
namespace,
|
|
617
|
+
"--ignore-not-found=true",
|
|
618
|
+
]);
|
|
619
|
+
await execa("kubectl", [
|
|
620
|
+
"create",
|
|
621
|
+
"job",
|
|
622
|
+
jobName,
|
|
623
|
+
"-n",
|
|
624
|
+
namespace,
|
|
625
|
+
`--from=cronjob/${cronJobName}`,
|
|
626
|
+
]);
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
throw new Error(`Failed to create backup job:\n${getErrorMessage(error)}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
export async function waitForJobComplete(namespace, jobName, timeoutSeconds = 3600) {
|
|
633
|
+
try {
|
|
634
|
+
await execa("kubectl", [
|
|
635
|
+
"wait",
|
|
636
|
+
"--for=condition=complete",
|
|
637
|
+
`job/${jobName}`,
|
|
638
|
+
"-n",
|
|
639
|
+
namespace,
|
|
640
|
+
`--timeout=${timeoutSeconds}s`,
|
|
641
|
+
]);
|
|
642
|
+
return await getJobLogs(jobName, namespace);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
const logs = await getJobLogs(jobName, namespace).catch(() => "");
|
|
646
|
+
const failed = await isJobFailed(jobName, namespace).catch(() => false);
|
|
647
|
+
if (failed) {
|
|
648
|
+
throw new Error(`Job ${jobName} failed:\n${logs || getErrorMessage(error)}`);
|
|
649
|
+
}
|
|
650
|
+
throw new Error(`Timed out waiting for job ${jobName}:\n${logs || getErrorMessage(error)}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
export async function getJobLogs(jobName, namespace) {
|
|
654
|
+
const { stdout } = await execa("kubectl", [
|
|
655
|
+
"logs",
|
|
656
|
+
`job/${jobName}`,
|
|
657
|
+
"-n",
|
|
658
|
+
namespace,
|
|
659
|
+
"--all-containers=true",
|
|
660
|
+
]);
|
|
661
|
+
return stdout;
|
|
662
|
+
}
|
|
663
|
+
async function isJobFailed(jobName, namespace) {
|
|
664
|
+
const { stdout } = await execa("kubectl", [
|
|
665
|
+
"get",
|
|
666
|
+
"job",
|
|
667
|
+
jobName,
|
|
668
|
+
"-n",
|
|
669
|
+
namespace,
|
|
670
|
+
"-o",
|
|
671
|
+
"jsonpath={.status.failed}",
|
|
672
|
+
]);
|
|
673
|
+
return Number.parseInt(stdout || "0", 10) > 0;
|
|
674
|
+
}
|
|
675
|
+
export async function scaleDeployment(namespace, name, replicas) {
|
|
676
|
+
try {
|
|
677
|
+
await execa("kubectl", [
|
|
678
|
+
"scale",
|
|
679
|
+
"deployment",
|
|
680
|
+
name,
|
|
681
|
+
"-n",
|
|
682
|
+
namespace,
|
|
683
|
+
`--replicas=${replicas}`,
|
|
684
|
+
]);
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
throw new Error(`Failed to scale deployment ${name}:\n${getErrorMessage(error)}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
export async function waitForDeploymentReady(namespace, name, timeoutSeconds = 600) {
|
|
691
|
+
try {
|
|
692
|
+
await execa("kubectl", [
|
|
693
|
+
"rollout",
|
|
694
|
+
"status",
|
|
695
|
+
`deployment/${name}`,
|
|
696
|
+
"-n",
|
|
697
|
+
namespace,
|
|
698
|
+
`--timeout=${timeoutSeconds}s`,
|
|
699
|
+
]);
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
throw new Error(`Deployment ${name} is not ready:\n${getErrorMessage(error)}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
export async function getDeploymentReplicas(namespace, name) {
|
|
706
|
+
try {
|
|
707
|
+
const { stdout } = await execa("kubectl", [
|
|
708
|
+
"get",
|
|
709
|
+
"deployment",
|
|
710
|
+
name,
|
|
711
|
+
"-n",
|
|
712
|
+
namespace,
|
|
713
|
+
"-o",
|
|
714
|
+
"jsonpath={.spec.replicas}",
|
|
715
|
+
]);
|
|
716
|
+
return Number.parseInt(stdout || "0", 10);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
440
722
|
/**
|
|
441
723
|
* Colors for multi-pod log prefixes
|
|
442
724
|
*/
|
|
@@ -682,45 +964,111 @@ export async function deletePVCs(namespace, options = {}) {
|
|
|
682
964
|
}
|
|
683
965
|
}
|
|
684
966
|
}
|
|
967
|
+
// Custom resources whose operator sets a finalizer that only that operator can
|
|
968
|
+
// clear. When the operator is uninstalled with the release, those finalizers are
|
|
969
|
+
// never removed and wedge the namespace (and the CRD) in Terminating forever.
|
|
970
|
+
// Observed blockers: KEDA ScaledObjects, cert-manager ACME Challenges/Orders, and
|
|
971
|
+
// Strimzi Kafka resources.
|
|
972
|
+
const FINALIZER_BLOCKING_CR_TYPES = [
|
|
973
|
+
"scaledobjects.keda.sh",
|
|
974
|
+
"scaledjobs.keda.sh",
|
|
975
|
+
"challenges.acme.cert-manager.io",
|
|
976
|
+
"orders.acme.cert-manager.io",
|
|
977
|
+
"certificaterequests.cert-manager.io",
|
|
978
|
+
"certificates.cert-manager.io",
|
|
979
|
+
"kafkatopics.kafka.strimzi.io",
|
|
980
|
+
"kafkausers.kafka.strimzi.io",
|
|
981
|
+
"kafkanodepools.kafka.strimzi.io",
|
|
982
|
+
"kafkas.kafka.strimzi.io",
|
|
983
|
+
];
|
|
685
984
|
/**
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
985
|
+
* Strips finalizers from the custom resources whose controllers are torn down
|
|
986
|
+
* with the release, so the namespace can finalize instead of hanging in
|
|
987
|
+
* Terminating (NamespaceFinalizersRemaining). Best-effort per type — a missing
|
|
988
|
+
* CRD (feature disabled) or already-gone object is fine.
|
|
689
989
|
*/
|
|
690
|
-
export async function
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
990
|
+
export async function removeBlockingFinalizers(namespace) {
|
|
991
|
+
for (const resourceType of FINALIZER_BLOCKING_CR_TYPES) {
|
|
992
|
+
try {
|
|
993
|
+
const { stdout } = await execa("kubectl", [
|
|
994
|
+
"get",
|
|
995
|
+
resourceType,
|
|
996
|
+
"-n",
|
|
997
|
+
namespace,
|
|
998
|
+
"-o",
|
|
999
|
+
"jsonpath={.items[*].metadata.name}",
|
|
1000
|
+
], { timeout: 15000 });
|
|
1001
|
+
const names = stdout.split(" ").filter(Boolean);
|
|
1002
|
+
for (const name of names) {
|
|
1003
|
+
try {
|
|
1004
|
+
await execa("kubectl", [
|
|
1005
|
+
"patch",
|
|
1006
|
+
resourceType,
|
|
1007
|
+
name,
|
|
1008
|
+
"-n",
|
|
1009
|
+
namespace,
|
|
1010
|
+
"-p",
|
|
1011
|
+
'{"metadata":{"finalizers":null}}',
|
|
1012
|
+
"--type=merge",
|
|
1013
|
+
], { timeout: 15000 });
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
// Ignore — object might already be deleted.
|
|
1017
|
+
}
|
|
715
1018
|
}
|
|
716
|
-
|
|
717
|
-
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
// Ignore — this CRD might not be installed (feature disabled).
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Deletes aggregated APIServices (apiregistration.k8s.io) whose backing service
|
|
1027
|
+
* lives in the given namespace.
|
|
1028
|
+
*
|
|
1029
|
+
* Why this matters for teardown: an aggregated API (e.g. KEDA's
|
|
1030
|
+
* v1beta1.external.metrics.k8s.io, prometheus-adapter's custom.metrics.k8s.io,
|
|
1031
|
+
* etc.) is served by an in-namespace Service. When the namespace is torn down
|
|
1032
|
+
* that Service disappears and the (cluster-scoped) APIService goes Unavailable
|
|
1033
|
+
* with ServiceNotFound. The namespace controller must enumerate every API group
|
|
1034
|
+
* to delete a namespace's contents, so a single broken APIService makes its
|
|
1035
|
+
* discovery step fail and wedges the namespace in Terminating forever
|
|
1036
|
+
* (NamespaceDeletionDiscoveryFailure) - which then rejects any reinstall into
|
|
1037
|
+
* that namespace ("being terminated").
|
|
1038
|
+
*
|
|
1039
|
+
* Deleting these APIServices up front (they are going away with the namespace
|
|
1040
|
+
* anyway) keeps discovery healthy so the namespace can finalize. This is
|
|
1041
|
+
* generalized to ALL APIServices backed by the target namespace, not just KEDA,
|
|
1042
|
+
* and is safe: cluster APIs backed by other namespaces (e.g. metrics-server in
|
|
1043
|
+
* kube-system) are never matched. Listing APIService objects is served directly
|
|
1044
|
+
* by kube-apiserver, so this also works to rescue an already-stuck namespace.
|
|
1045
|
+
*
|
|
1046
|
+
* Returns the names of the APIServices that were deleted.
|
|
1047
|
+
*/
|
|
1048
|
+
export async function cleanupNamespaceAPIServices(namespace) {
|
|
1049
|
+
const deleted = [];
|
|
1050
|
+
try {
|
|
1051
|
+
const { stdout } = await execa("kubectl", ["get", "apiservices", "-o", "json"], { timeout: 30000 });
|
|
1052
|
+
const parsed = JSON.parse(stdout);
|
|
1053
|
+
for (const item of parsed.items ?? []) {
|
|
1054
|
+
const name = item.metadata?.name;
|
|
1055
|
+
if (!name)
|
|
1056
|
+
continue;
|
|
1057
|
+
if (item.spec?.service?.namespace === namespace) {
|
|
1058
|
+
try {
|
|
1059
|
+
await execa("kubectl", ["delete", "apiservice", name, "--ignore-not-found"], { timeout: 30000 });
|
|
1060
|
+
deleted.push(name);
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
// Best-effort: a single failure should not block teardown.
|
|
1064
|
+
}
|
|
718
1065
|
}
|
|
719
1066
|
}
|
|
720
1067
|
}
|
|
721
1068
|
catch {
|
|
722
|
-
//
|
|
1069
|
+
// Best-effort: if APIServices can't be listed, don't block the destroy.
|
|
723
1070
|
}
|
|
1071
|
+
return deleted;
|
|
724
1072
|
}
|
|
725
1073
|
/**
|
|
726
1074
|
* Checks if a namespace exists
|
|
@@ -735,98 +1083,144 @@ export async function namespaceExists(namespace) {
|
|
|
735
1083
|
}
|
|
736
1084
|
}
|
|
737
1085
|
/**
|
|
738
|
-
*
|
|
739
|
-
*
|
|
1086
|
+
* Removes this release's leftovers in the kube-system namespace. The
|
|
1087
|
+
* kube-prometheus-stack prometheus-operator creates a "<release>-...-kubelet"
|
|
1088
|
+
* Service there at runtime (via its --kubelet-service flag); it lives OUTSIDE the
|
|
1089
|
+
* release namespace and is operator-created (not chart-templated), so
|
|
1090
|
+
* `helm uninstall` never deletes it and one accumulates per deployment. Also
|
|
1091
|
+
* sweeps any helm-labeled kube-system objects (exporter Services/Endpoints) a
|
|
1092
|
+
* partial uninstall may have stranded. Scoped strictly to this release; matched
|
|
1093
|
+
* by the release-name prefix so a coexisting deployment's kubelet Service is
|
|
1094
|
+
* never touched. Best-effort — never blocks teardown.
|
|
740
1095
|
*/
|
|
741
|
-
export async function
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1096
|
+
export async function cleanupKubeSystemLeftovers(releaseName) {
|
|
1097
|
+
// 1) helm-labeled kube-system objects from this release (only present if a
|
|
1098
|
+
// prior uninstall didn't finish): the kube-prometheus-stack exporter
|
|
1099
|
+
// Services (coredns/kube-controller-manager/etc.) and their Endpoints.
|
|
1100
|
+
try {
|
|
1101
|
+
await execa("kubectl", [
|
|
1102
|
+
"delete",
|
|
1103
|
+
"service,endpoints",
|
|
1104
|
+
"-n",
|
|
1105
|
+
"kube-system",
|
|
1106
|
+
"-l",
|
|
1107
|
+
`release=${releaseName}`,
|
|
1108
|
+
"--ignore-not-found",
|
|
1109
|
+
], { timeout: 30000 });
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
// best-effort
|
|
1113
|
+
}
|
|
1114
|
+
// 2) the operator-created kubelet Service, matched by name (it carries no
|
|
1115
|
+
// reliable per-release label). Name is "<release>-<kube-prometheus>-kubelet"
|
|
1116
|
+
// (the middle segment is truncated by the helm fullname template). The
|
|
1117
|
+
// trailing "-" in the prefix guard prevents matching a sibling whose name
|
|
1118
|
+
// is a prefix of this one (e.g. az-p0 vs az-p055).
|
|
1119
|
+
try {
|
|
1120
|
+
const { stdout } = await execa("kubectl", [
|
|
1121
|
+
"get",
|
|
1122
|
+
"service",
|
|
1123
|
+
"-n",
|
|
1124
|
+
"kube-system",
|
|
1125
|
+
"-o",
|
|
1126
|
+
"jsonpath={.items[*].metadata.name}",
|
|
1127
|
+
], { timeout: 15000 });
|
|
1128
|
+
const targets = stdout
|
|
1129
|
+
.split(" ")
|
|
1130
|
+
.filter(Boolean)
|
|
1131
|
+
.filter((n) => n.startsWith(`${releaseName}-`) && n.endsWith("-kubelet"));
|
|
1132
|
+
for (const name of targets) {
|
|
1133
|
+
try {
|
|
1134
|
+
await execa("kubectl", ["delete", "service", name, "-n", "kube-system", "--ignore-not-found"], { timeout: 30000 });
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
// best-effort
|
|
752
1138
|
}
|
|
753
|
-
// Wait before next retry
|
|
754
|
-
await sleep(delayMs);
|
|
755
1139
|
}
|
|
756
1140
|
}
|
|
1141
|
+
catch {
|
|
1142
|
+
// best-effort
|
|
1143
|
+
}
|
|
757
1144
|
}
|
|
758
1145
|
/**
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
storageClassYaml = `
|
|
769
|
-
apiVersion: storage.k8s.io/v1
|
|
770
|
-
kind: StorageClass
|
|
771
|
-
metadata:
|
|
772
|
-
name: gp3
|
|
773
|
-
annotations:
|
|
774
|
-
storageclass.kubernetes.io/is-default-class: "true"
|
|
775
|
-
provisioner: ebs.csi.aws.com
|
|
776
|
-
reclaimPolicy: Delete
|
|
777
|
-
volumeBindingMode: WaitForFirstConsumer
|
|
778
|
-
parameters:
|
|
779
|
-
type: gp3
|
|
780
|
-
encrypted: "true"
|
|
781
|
-
`;
|
|
782
|
-
break;
|
|
783
|
-
case "gcp":
|
|
784
|
-
storageClassYaml = `
|
|
785
|
-
apiVersion: storage.k8s.io/v1
|
|
786
|
-
kind: StorageClass
|
|
787
|
-
metadata:
|
|
788
|
-
name: pd-ssd
|
|
789
|
-
annotations:
|
|
790
|
-
storageclass.kubernetes.io/is-default-class: "true"
|
|
791
|
-
provisioner: pd.csi.storage.gke.io
|
|
792
|
-
reclaimPolicy: Delete
|
|
793
|
-
volumeBindingMode: WaitForFirstConsumer
|
|
794
|
-
parameters:
|
|
795
|
-
type: pd-ssd
|
|
796
|
-
`;
|
|
797
|
-
break;
|
|
798
|
-
case "azure":
|
|
799
|
-
storageClassYaml = `
|
|
800
|
-
apiVersion: storage.k8s.io/v1
|
|
801
|
-
kind: StorageClass
|
|
802
|
-
metadata:
|
|
803
|
-
name: managed-premium
|
|
804
|
-
annotations:
|
|
805
|
-
storageclass.kubernetes.io/is-default-class: "true"
|
|
806
|
-
provisioner: disk.csi.azure.com
|
|
807
|
-
reclaimPolicy: Delete
|
|
808
|
-
volumeBindingMode: WaitForFirstConsumer
|
|
809
|
-
parameters:
|
|
810
|
-
skuName: Premium_LRS
|
|
811
|
-
`;
|
|
812
|
-
break;
|
|
813
|
-
default:
|
|
814
|
-
throw new Error(`Unsupported cloud provider: ${provider}`);
|
|
815
|
-
}
|
|
1146
|
+
* True only when no OTHER Rulebricks deployment remains on the cluster (besides
|
|
1147
|
+
* `releaseName`). Gates deletion of cluster-SHARED resources (CRDs) so tearing
|
|
1148
|
+
* down one deployment never cascade-deletes another deployment's custom
|
|
1149
|
+
* resources. Deployments are named `rulebricks-<name>` for both the namespace and
|
|
1150
|
+
* the helm release (see getNamespace/getReleaseName), so the "rulebricks-" prefix
|
|
1151
|
+
* is a sound cluster-side signal. Fails CLOSED (returns false) if the cluster
|
|
1152
|
+
* can't be enumerated — we never purge shared resources on uncertainty.
|
|
1153
|
+
*/
|
|
1154
|
+
export async function isLastRulebricksDeployment(releaseName) {
|
|
816
1155
|
try {
|
|
817
|
-
|
|
818
|
-
|
|
1156
|
+
// Authoritative: helm releases cluster-wide.
|
|
1157
|
+
const { stdout } = await execa("helm", ["list", "-A", "-o", "json"], {
|
|
1158
|
+
timeout: 30000,
|
|
819
1159
|
});
|
|
1160
|
+
const releases = JSON.parse(stdout);
|
|
1161
|
+
const otherReleases = releases.filter((r) => typeof r.name === "string" &&
|
|
1162
|
+
r.name.startsWith("rulebricks-") &&
|
|
1163
|
+
r.name !== releaseName);
|
|
1164
|
+
if (otherReleases.length > 0)
|
|
1165
|
+
return false;
|
|
1166
|
+
// Cross-check namespaces in case a release secret is gone but the ns lingers
|
|
1167
|
+
// (namespace name == release name by convention).
|
|
1168
|
+
const { stdout: nsOut } = await execa("kubectl", ["get", "namespaces", "-o", "jsonpath={.items[*].metadata.name}"], { timeout: 15000 });
|
|
1169
|
+
const otherNamespaces = nsOut
|
|
1170
|
+
.split(" ")
|
|
1171
|
+
.filter(Boolean)
|
|
1172
|
+
.filter((n) => n.startsWith("rulebricks-") && n !== releaseName);
|
|
1173
|
+
return otherNamespaces.length === 0;
|
|
820
1174
|
}
|
|
821
|
-
catch
|
|
822
|
-
|
|
1175
|
+
catch {
|
|
1176
|
+
return false; // fail closed — do not purge shared resources on uncertainty
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
// CRD API-group suffixes the chart ships in crds/ dirs (cert-manager + keda from
|
|
1180
|
+
// the parent crds/, strimzi + kube-prometheus-stack from subchart crds/). helm
|
|
1181
|
+
// NEVER deletes crds/ contents on uninstall, so they leak and accumulate.
|
|
1182
|
+
const RULEBRICKS_CRD_GROUP_SUFFIXES = [
|
|
1183
|
+
".strimzi.io", // kafka.strimzi.io, core.strimzi.io
|
|
1184
|
+
"cert-manager.io", // cert-manager.io, acme.cert-manager.io
|
|
1185
|
+
".keda.sh", // keda.sh, eventing.keda.sh
|
|
1186
|
+
"monitoring.coreos.com", // kube-prometheus-stack
|
|
1187
|
+
];
|
|
1188
|
+
/**
|
|
1189
|
+
* Deletes the cluster-scoped CRDs the chart installs from crds/ dirs (cert-
|
|
1190
|
+
* manager, keda, strimzi, kube-prometheus-stack). CLUSTER-SHARED: deleting a CRD
|
|
1191
|
+
* cascade-deletes every custom resource of that kind across ALL namespaces, so
|
|
1192
|
+
* callers MUST gate this on isLastRulebricksDeployment() (or an explicit
|
|
1193
|
+
* operator --purge) — never call it while another Rulebricks deployment exists.
|
|
1194
|
+
* Best-effort, non-blocking; returns the CRD names removed.
|
|
1195
|
+
*/
|
|
1196
|
+
export async function deleteRulebricksCRDs() {
|
|
1197
|
+
const deleted = [];
|
|
1198
|
+
try {
|
|
1199
|
+
const { stdout } = await execa("kubectl", ["get", "crd", "-o", "jsonpath={.items[*].metadata.name}"], { timeout: 30000 });
|
|
1200
|
+
const targets = stdout
|
|
1201
|
+
.split(" ")
|
|
1202
|
+
.filter(Boolean)
|
|
1203
|
+
.filter((name) => RULEBRICKS_CRD_GROUP_SUFFIXES.some((suffix) => name.endsWith(suffix)));
|
|
1204
|
+
for (const name of targets) {
|
|
1205
|
+
try {
|
|
1206
|
+
await execa("kubectl", ["delete", "crd", name, "--ignore-not-found", "--wait=false"], { timeout: 30000 });
|
|
1207
|
+
deleted.push(name);
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
// best-effort: a single CRD failure should not block teardown
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
823
1213
|
}
|
|
1214
|
+
catch {
|
|
1215
|
+
// best-effort: if CRDs can't be listed, don't block the destroy
|
|
1216
|
+
}
|
|
1217
|
+
return deleted;
|
|
824
1218
|
}
|
|
825
1219
|
/**
|
|
826
1220
|
* Extracts the version tag from a Docker image string.
|
|
827
1221
|
* E.g., "rulebricks/rulebricks:v1.5.8" -> "v1.5.8"
|
|
828
1222
|
*/
|
|
829
|
-
function extractImageTag(image) {
|
|
1223
|
+
export function extractImageTag(image) {
|
|
830
1224
|
if (!image)
|
|
831
1225
|
return null;
|
|
832
1226
|
const parts = image.split(":");
|
|
@@ -834,51 +1228,88 @@ function extractImageTag(image) {
|
|
|
834
1228
|
return null;
|
|
835
1229
|
return parts[parts.length - 1];
|
|
836
1230
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
* @param namespace - The Kubernetes namespace
|
|
843
|
-
* @returns DeployedVersions with app and HPS versions, or null if not found
|
|
844
|
-
*/
|
|
845
|
-
export async function getDeployedImageVersions(releaseName, namespace) {
|
|
846
|
-
const result = {
|
|
847
|
-
appVersion: null,
|
|
848
|
-
hpsVersion: null,
|
|
849
|
-
};
|
|
850
|
-
// Get app deployment image
|
|
1231
|
+
export function extractImageDigest(imageId) {
|
|
1232
|
+
const digest = imageId.split("@").pop();
|
|
1233
|
+
return digest?.startsWith("sha256:") ? digest : null;
|
|
1234
|
+
}
|
|
1235
|
+
async function getWorkloadImage(workloadType, name, namespace) {
|
|
851
1236
|
try {
|
|
852
|
-
const { stdout
|
|
1237
|
+
const { stdout } = await execa("kubectl", [
|
|
853
1238
|
"get",
|
|
854
|
-
|
|
855
|
-
|
|
1239
|
+
workloadType,
|
|
1240
|
+
name,
|
|
856
1241
|
"-n",
|
|
857
1242
|
namespace,
|
|
858
1243
|
"-o",
|
|
859
1244
|
"jsonpath={.spec.template.spec.containers[0].image}",
|
|
860
1245
|
]);
|
|
861
|
-
|
|
1246
|
+
return stdout.trim() || null;
|
|
862
1247
|
}
|
|
863
1248
|
catch {
|
|
864
|
-
|
|
1249
|
+
return null;
|
|
865
1250
|
}
|
|
866
|
-
|
|
1251
|
+
}
|
|
1252
|
+
async function getPodImageDigests(releaseName, workloadName, namespace, containerName) {
|
|
867
1253
|
try {
|
|
868
|
-
const { stdout
|
|
1254
|
+
const { stdout } = await execa("kubectl", [
|
|
869
1255
|
"get",
|
|
870
|
-
"
|
|
871
|
-
`${releaseName}-hps`,
|
|
1256
|
+
"pods",
|
|
872
1257
|
"-n",
|
|
873
1258
|
namespace,
|
|
1259
|
+
"-l",
|
|
1260
|
+
`app.kubernetes.io/name=${workloadName},app.kubernetes.io/instance=${releaseName}`,
|
|
874
1261
|
"-o",
|
|
875
|
-
"
|
|
1262
|
+
"json",
|
|
876
1263
|
]);
|
|
877
|
-
|
|
1264
|
+
const data = JSON.parse(stdout);
|
|
1265
|
+
return Array.from(new Set((data.items || [])
|
|
1266
|
+
.flatMap((pod) => pod.status?.containerStatuses || [])
|
|
1267
|
+
.filter((status) => status.name === containerName)
|
|
1268
|
+
.map((status) => extractImageDigest(status.imageID || ""))
|
|
1269
|
+
.filter((digest) => Boolean(digest))));
|
|
878
1270
|
}
|
|
879
1271
|
catch {
|
|
880
|
-
|
|
1272
|
+
return [];
|
|
881
1273
|
}
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Gets actual deployed image tags and running image digests from Kubernetes.
|
|
1277
|
+
* HPS runs as StatefulSets, so digest checks inspect the pods behind those sets.
|
|
1278
|
+
*
|
|
1279
|
+
* @param releaseName - The Helm release name (e.g., "rulebricks")
|
|
1280
|
+
* @param namespace - The Kubernetes namespace
|
|
1281
|
+
* @returns DeployedVersions with app and HPS versions, or null if not found
|
|
1282
|
+
*/
|
|
1283
|
+
export async function getDeployedImageVersions(releaseName, namespace) {
|
|
1284
|
+
const result = {
|
|
1285
|
+
appVersion: null,
|
|
1286
|
+
hpsVersion: null,
|
|
1287
|
+
hpsWorkerVersion: null,
|
|
1288
|
+
appDigest: null,
|
|
1289
|
+
hpsDigests: [],
|
|
1290
|
+
hpsWorkerDigests: [],
|
|
1291
|
+
};
|
|
1292
|
+
const appName = `${releaseName}-app`;
|
|
1293
|
+
const hpsName = `${releaseName}-hps`;
|
|
1294
|
+
const hpsWorkerName = `${releaseName}-hps-worker`;
|
|
1295
|
+
const [appImage, hpsImage, hpsWorkerImage] = await Promise.all([
|
|
1296
|
+
getWorkloadImage("deployment", appName, namespace),
|
|
1297
|
+
getWorkloadImage("statefulset", hpsName, namespace),
|
|
1298
|
+
getWorkloadImage("statefulset", hpsWorkerName, namespace),
|
|
1299
|
+
]);
|
|
1300
|
+
result.appVersion = appImage ? extractImageTag(appImage) : null;
|
|
1301
|
+
result.hpsVersion = hpsImage ? extractImageTag(hpsImage) : null;
|
|
1302
|
+
result.hpsWorkerVersion = hpsWorkerImage
|
|
1303
|
+
? extractImageTag(hpsWorkerImage)?.replace(/^worker-/, "") || null
|
|
1304
|
+
: null;
|
|
1305
|
+
const [appDigests, hpsDigests, hpsWorkerDigests] = await Promise.all([
|
|
1306
|
+
getPodImageDigests(releaseName, appName, namespace, "app"),
|
|
1307
|
+
getPodImageDigests(releaseName, hpsName, namespace, "hps"),
|
|
1308
|
+
getPodImageDigests(releaseName, hpsWorkerName, namespace, "hps-worker"),
|
|
1309
|
+
]);
|
|
1310
|
+
result.appDigest = appDigests[0] || null;
|
|
1311
|
+
result.hpsDigests = hpsDigests;
|
|
1312
|
+
result.hpsWorkerDigests = hpsWorkerDigests;
|
|
882
1313
|
return result;
|
|
883
1314
|
}
|
|
884
1315
|
/**
|