Skip to content
This repository has been archived by the owner on Oct 3, 2024. It is now read-only.

Commit

Permalink
Initial implementation refreshECRToken()
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas Rodriguez committed Dec 6, 2023
1 parent ea72aca commit d238968
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 19 deletions.
2 changes: 2 additions & 0 deletions capabilities/ecr-credential-helper/credential-helper.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,5 +18,6 @@ OnSchedule({
unit: "seconds",
run: async () => {
Log.info("AM I RUNNING?");
await refreshECRToken();
},
});
105 changes: 105 additions & 0 deletions capabilities/ecr-credential-helper/lib/ecr-token.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<string> {
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<void> {
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;
}
}
18 changes: 18 additions & 0 deletions capabilities/ecr-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CreateRepositoryCommandInput,
DescribeRepositoriesCommand,
DescribeRepositoriesCommandInput,
GetAuthorizationTokenCommand,
} from "@aws-sdk/client-ecr";
import { Log } from "pepr";
import { ECRProvider } from "./ecr-provider";
Expand Down Expand Up @@ -101,4 +102,21 @@ export class ECRPrivate implements ECRProvider {
Log.error(`Error creating ECR repositories: ${err}`);
}
}

async fetchECRToken(): Promise<string> {
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 "";
}
}
}
1 change: 1 addition & 0 deletions capabilities/ecr-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
export interface ECRProvider {
listExistingRepositories(repoNames: string[]): Promise<string[]>;
createRepositories(repoNames: string[], accountId?: string): Promise<void>;
fetchECRToken(): Promise<string>;
}
18 changes: 18 additions & 0 deletions capabilities/ecr-public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CreateRepositoryCommandInput,
DescribeRepositoriesCommand,
DescribeRepositoriesCommandInput,
GetAuthorizationTokenCommand,
} from "@aws-sdk/client-ecr-public";
import { Log } from "pepr";
import { ECRProvider } from "./ecr-provider";
Expand Down Expand Up @@ -92,4 +93,21 @@ export class ECRPublic implements ECRProvider {
Log.error(`Error creating public ECR repositories: ${err}`);
}
}

async fetchECRToken(): Promise<string> {
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 "";
}
}
}
5 changes: 3 additions & 2 deletions capabilities/ecr-webhook/lib/ecr.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -20,7 +21,7 @@ interface ECRCheckResult {
export async function isECRregistry(): Promise<ECRCheckResult> {
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(
Expand Down
19 changes: 4 additions & 15 deletions capabilities/ecr-webhook/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
33 changes: 33 additions & 0 deletions capabilities/lib/k8s.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { K8s, kind } from "pepr";

export async function getSecret(
ns: string,
secretName: string,
): Promise<kind.Secret> {
return await K8s(kind.Secret).InNamespace(ns).Get(secretName);
}

export async function updateSecret(
ns: string,
secretName: string,
secretData: string,
): Promise<kind.Secret> {
// 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<kind.Namespace[]> {
const ns = await K8s(kind.Namespace).Get();
return ns.items;
}
7 changes: 5 additions & 2 deletions pepr.ts
Original file line number Diff line number Diff line change
@@ -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,
]);

0 comments on commit d238968

Please sign in to comment.