From d238968bc670ee1ce89c064b307b106352847dcc Mon Sep 17 00:00:00 2001 From: Lucas Rodriguez Date: Wed, 6 Dec 2023 01:00:14 -0600 Subject: [PATCH] Initial implementation refreshECRToken() --- .../credential-helper.ts | 2 + .../ecr-credential-helper/lib/ecr-token.ts | 105 ++++++++++++++++++ capabilities/ecr-private.ts | 18 +++ capabilities/ecr-provider.ts | 1 + capabilities/ecr-public.ts | 18 +++ capabilities/ecr-webhook/lib/ecr.ts | 5 +- capabilities/ecr-webhook/lib/utils.ts | 19 +--- capabilities/lib/k8s.ts | 33 ++++++ pepr.ts | 7 +- 9 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 capabilities/ecr-credential-helper/lib/ecr-token.ts create mode 100644 capabilities/lib/k8s.ts diff --git a/capabilities/ecr-credential-helper/credential-helper.ts b/capabilities/ecr-credential-helper/credential-helper.ts index d5bb810..2c6bbcd 100644 --- a/capabilities/ecr-credential-helper/credential-helper.ts +++ b/capabilities/ecr-credential-helper/credential-helper.ts @@ -1,4 +1,5 @@ import { Capability, Log } from "pepr"; +import { refreshECRToken } from "./lib/ecr-token"; /** * The ECR Credential Helper Capability refreshes ECR tokens for Zarf image pull secrets. @@ -17,5 +18,6 @@ OnSchedule({ unit: "seconds", run: async () => { Log.info("AM I RUNNING?"); + await refreshECRToken(); }, }); diff --git a/capabilities/ecr-credential-helper/lib/ecr-token.ts b/capabilities/ecr-credential-helper/lib/ecr-token.ts new file mode 100644 index 0000000..b417433 --- /dev/null +++ b/capabilities/ecr-credential-helper/lib/ecr-token.ts @@ -0,0 +1,105 @@ +import { Log } from "pepr"; +import { ECRPrivate, privateECRURLPattern } from "../../ecr-private"; +import { ECRPublic, publicECRURLPattern } from "../../ecr-public"; +import { getSecret, listNamespaces, updateSecret } from "../../lib/k8s"; +import { ZarfState } from "../../zarf-types"; + +const zarfNamespace = "zarf"; +const zarfImagePullSecret = "private-registry"; +const zarfStateSecret = "zarf-state"; +const zarfAgentLabel = "zarf.dev/agent"; +const zarfManagedByLabel = "app.kubernetes.io/managed-by"; + +export async function refreshECRToken(): Promise { + let authToken: string = ""; + const region = process.env.AWS_REGION; + + if (!region) { + Log.error("AWS_REGION environment variable is not defined."); + return; + } + + try { + const ecrURL = await getECRURL(); + + if (privateECRURLPattern.test(ecrURL)) { + const ecrPrivate = new ECRPrivate(region); + authToken = await ecrPrivate.fetchECRToken(); + } + + if (publicECRURLPattern.test(ecrURL)) { + const ecrPublic = new ECRPublic(region); + authToken = await ecrPublic.fetchECRToken(); + } + + await updateZarfManagedImageSecrets(ecrURL, authToken); + } catch (err) { + Log.error( + `Error: unable to update ECR token in Zarf image pull secrets: ${err}`, + ); + return; + } +} + +async function getECRURL(): Promise { + try { + const secret = await getSecret(zarfNamespace, zarfStateSecret); + const secretString = atob(secret.data!.state); + const zarfState: ZarfState = JSON.parse(secretString); + return zarfState.registryInfo.address; + } catch (err) { + Log.error(`unable to get ECR URL from ${zarfStateSecret} secret: ${err}`); + return ""; + } +} + +async function updateZarfManagedImageSecrets( + ecrURL: string, + authToken: string, +): Promise { + try { + const namespaces = await listNamespaces(); + + for (const ns of namespaces) { + const registrySecret = await getSecret( + ns.metadata!.name!, + zarfImagePullSecret, + ); + + // Check if this is a Zarf managed secret or is in a namespace the Zarf agent will take action in + if ( + registrySecret.metadata!.labels && + (registrySecret.metadata!.labels[zarfManagedByLabel] === "zarf" || + (ns.metadata!.labels && + ns.metadata!.labels[zarfAgentLabel] !== "skip" && + ns.metadata!.labels[zarfAgentLabel] !== "ignore")) + ) { + // Update the secret with the new ECR auth token + const dockerConfigJSON = { + Auths: { + [ecrURL]: { + Auth: authToken, + }, + }, + }; + const dockerConfigData = btoa(JSON.stringify(dockerConfigJSON)); + registrySecret.data![".dockerconfigjson"] = dockerConfigData; + + const updatedRegistrySecret = await updateSecret( + ns.metadata!.name!, + zarfImagePullSecret, + registrySecret.data!.data, + ); + + Log.info( + `Successfully updated secret '${ + updatedRegistrySecret.metadata!.name + }' in namespace '${ns.metadata!.name}'`, + ); + } + } + } catch (err) { + Log.error(`"unable to update secret Zarf image pull secret: ${err}`); + return; + } +} diff --git a/capabilities/ecr-private.ts b/capabilities/ecr-private.ts index 7d94e1e..f66301b 100644 --- a/capabilities/ecr-private.ts +++ b/capabilities/ecr-private.ts @@ -4,6 +4,7 @@ import { CreateRepositoryCommandInput, DescribeRepositoriesCommand, DescribeRepositoriesCommandInput, + GetAuthorizationTokenCommand, } from "@aws-sdk/client-ecr"; import { Log } from "pepr"; import { ECRProvider } from "./ecr-provider"; @@ -101,4 +102,21 @@ export class ECRPrivate implements ECRProvider { Log.error(`Error creating ECR repositories: ${err}`); } } + + async fetchECRToken(): Promise { + try { + const authOutput = await this.ecr.send( + new GetAuthorizationTokenCommand({}), + ); + + if (authOutput.authorizationData.length === 0) { + throw new Error("No authorization data received from ECR"); + } + + return authOutput.authorizationData[0].authorizationToken; + } catch (error) { + Log.error(`Error calling GetAuthorizationTokenCommand(): ${error}`); + return ""; + } + } } diff --git a/capabilities/ecr-provider.ts b/capabilities/ecr-provider.ts index 8e0548b..b682dc7 100644 --- a/capabilities/ecr-provider.ts +++ b/capabilities/ecr-provider.ts @@ -4,4 +4,5 @@ export interface ECRProvider { listExistingRepositories(repoNames: string[]): Promise; createRepositories(repoNames: string[], accountId?: string): Promise; + fetchECRToken(): Promise; } diff --git a/capabilities/ecr-public.ts b/capabilities/ecr-public.ts index e5d771a..7350564 100644 --- a/capabilities/ecr-public.ts +++ b/capabilities/ecr-public.ts @@ -4,6 +4,7 @@ import { CreateRepositoryCommandInput, DescribeRepositoriesCommand, DescribeRepositoriesCommandInput, + GetAuthorizationTokenCommand, } from "@aws-sdk/client-ecr-public"; import { Log } from "pepr"; import { ECRProvider } from "./ecr-provider"; @@ -92,4 +93,21 @@ export class ECRPublic implements ECRProvider { Log.error(`Error creating public ECR repositories: ${err}`); } } + + async fetchECRToken(): Promise { + try { + const authOutput = await this.ecr.send( + new GetAuthorizationTokenCommand({}), + ); + + if (!authOutput.authorizationData) { + throw new Error("No authorization data received from ECR"); + } + + return authOutput.authorizationData.authorizationToken; + } catch (error) { + Log.error(`Error calling GetAuthorizationTokenCommand(): ${error}`); + return ""; + } + } } diff --git a/capabilities/ecr-webhook/lib/ecr.ts b/capabilities/ecr-webhook/lib/ecr.ts index 1090658..44f70fe 100644 --- a/capabilities/ecr-webhook/lib/ecr.ts +++ b/capabilities/ecr-webhook/lib/ecr.ts @@ -1,8 +1,9 @@ -import { K8s, kind, Log } from "pepr"; +import { Log } from "pepr"; import { getRepositoryNames } from "./utils"; import { ZarfState, DeployedComponent, ZarfComponent } from "../../zarf-types"; import { privateECRURLPattern, ECRPrivate } from "../../ecr-private"; import { publicECRURLPattern, ECRPublic } from "../../ecr-public"; +import { getSecret } from "../../lib/k8s"; /** * Represents the result of checking whether the Zarf registry is an ECR registry. @@ -20,7 +21,7 @@ interface ECRCheckResult { export async function isECRregistry(): Promise { try { // Fetch the Zarf state secret - const secret = await K8s(kind.Secret).InNamespace("zarf").Get("zarf-state"); + const secret = await getSecret("zarf", "zarf-state"); if (!secret.data || !secret.data.state) { throw new Error( diff --git a/capabilities/ecr-webhook/lib/utils.ts b/capabilities/ecr-webhook/lib/utils.ts index d925fca..f140371 100644 --- a/capabilities/ecr-webhook/lib/utils.ts +++ b/capabilities/ecr-webhook/lib/utils.ts @@ -1,10 +1,11 @@ -import { K8s, kind, Log } from "pepr"; +import { Log } from "pepr"; import { createRepos } from "./ecr"; import { ZarfComponent, DeployedComponent, DeployedPackage, } from "../../zarf-types"; +import { getSecret, updateSecret } from "../../lib/k8s"; /** * Represents a component check result, indicating whether a component is ready for a webhook to execute. @@ -184,7 +185,7 @@ export async function updateWebhookStatus( try { // Fetch the package secret - const secret = await K8s(kind.Secret).InNamespace(ns).Get(secretName); + const secret = await getSecret(ns, secretName); if (!secret.data) { Log.error( @@ -205,19 +206,7 @@ export async function updateWebhookStatus( secret.data.data = btoa(JSON.stringify(deployedPackage)); - // Use Server-Side force apply to forcefully take ownership of the package secret data.data field - await K8s(kind.Secret).Apply( - { - metadata: { - name: secretName, - namespace: ns, - }, - data: { - data: secret.data.data, - }, - }, - { force: true }, - ); + await updateSecret(ns, secretName, secret.data.data); } catch (err) { Log.error( `Error: Failed to update webhook status in package secret '${secretName}' in namespace '${ns}': ${JSON.stringify( diff --git a/capabilities/lib/k8s.ts b/capabilities/lib/k8s.ts new file mode 100644 index 0000000..238bfb6 --- /dev/null +++ b/capabilities/lib/k8s.ts @@ -0,0 +1,33 @@ +import { K8s, kind } from "pepr"; + +export async function getSecret( + ns: string, + secretName: string, +): Promise { + return await K8s(kind.Secret).InNamespace(ns).Get(secretName); +} + +export async function updateSecret( + ns: string, + secretName: string, + secretData: string, +): Promise { + // Use Server-Side force apply to forcefully take ownership of the package secret data.data field + return await K8s(kind.Secret).Apply( + { + metadata: { + name: secretName, + namespace: ns, + }, + data: { + data: secretData, + }, + }, + { force: true }, + ); +} + +export async function listNamespaces(): Promise { + const ns = await K8s(kind.Namespace).Get(); + return ns.items; +} diff --git a/pepr.ts b/pepr.ts index e77fb3e..cd9573c 100644 --- a/pepr.ts +++ b/pepr.ts @@ -1,11 +1,14 @@ import { PeprModule } from "pepr"; // cfg loads your pepr configuration from package.json import cfg from "./package.json"; -import { ECRhook } from "./capabilities/ecr-webhook/webhook"; +// import { ECRhook } from "./capabilities/ecr-webhook/webhook"; import { ECRCredentialHelper } from "./capabilities/ecr-credential-helper/credential-helper"; /** * This is the main entrypoint for this Pepr module. It is run when the module is started. * This is where you register your Pepr configurations and capabilities. */ -new PeprModule(cfg, [ECRhook, ECRCredentialHelper]); +new PeprModule(cfg, [ + // ECRhook, + ECRCredentialHelper, +]);