Skip to content

feat(supervisor): add ecr support to docker workloads #2424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/supervisor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@aws-sdk/client-ecr": "^3.839.0",
"@kubernetes/client-node": "^1.0.0",
"@trigger.dev/core": "workspace:*",
"dockerode": "^4.0.6",
Expand Down
38 changes: 34 additions & 4 deletions apps/supervisor/src/workloadManager/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import { env } from "../env.js";
import { getDockerHostDomain, getRunnerId, normalizeDockerHostUrl } from "../util.js";
import Docker from "dockerode";
import { tryCatch } from "@trigger.dev/core";
import { ECRAuthService } from "./ecrAuth.js";

export class DockerWorkloadManager implements WorkloadManager {
private readonly logger = new SimpleStructuredLogger("docker-workload-manager");
private readonly docker: Docker;

private readonly runnerNetworks: string[];
private readonly auth?: Docker.AuthConfig;
private readonly staticAuth?: Docker.AuthConfig;
private readonly platformOverride?: string;
private readonly ecrAuthService?: ECRAuthService;

constructor(private opts: WorkloadManagerOptions) {
this.docker = new Docker({
Expand Down Expand Up @@ -44,13 +46,18 @@ export class DockerWorkloadManager implements WorkloadManager {
url: env.DOCKER_REGISTRY_URL,
});

this.auth = {
this.staticAuth = {
username: env.DOCKER_REGISTRY_USERNAME,
password: env.DOCKER_REGISTRY_PASSWORD,
serveraddress: env.DOCKER_REGISTRY_URL,
};
} else if (ECRAuthService.hasAWSCredentials()) {
this.logger.info("🐋 AWS credentials found, initializing ECR auth service");
this.ecrAuthService = new ECRAuthService();
} else {
this.logger.warn("🐋 No Docker registry credentials provided, skipping auth");
this.logger.warn(
"🐋 No Docker registry credentials or AWS credentials provided, skipping auth"
);
}
}

Expand Down Expand Up @@ -160,9 +167,12 @@ export class DockerWorkloadManager implements WorkloadManager {
imageArchitecture: inspectResult?.Architecture,
});

// Get auth config (static or ECR)
const authConfig = await this.getAuthConfig();

// Ensure the image is present
const [createImageError, imageResponseReader] = await tryCatch(
this.docker.createImage(this.auth, {
this.docker.createImage(authConfig, {
fromImage: imageRef,
...(this.platformOverride ? { platform: this.platformOverride } : {}),
})
Expand Down Expand Up @@ -216,6 +226,26 @@ export class DockerWorkloadManager implements WorkloadManager {
logger.debug("create succeeded", { startResult, containerId: container.id });
}

/**
* Get authentication config for Docker operations
* Uses static credentials if available, otherwise attempts ECR auth
*/
private async getAuthConfig(): Promise<Docker.AuthConfig | undefined> {
// Use static credentials if available
if (this.staticAuth) {
return this.staticAuth;
}

// Use ECR auth if service is available
if (this.ecrAuthService) {
const ecrAuth = await this.ecrAuthService.getAuthConfig();
return ecrAuth || undefined;
}

// No auth available
return undefined;
}

private async attachContainerToNetworks({
containerId,
networkNames,
Expand Down
144 changes: 144 additions & 0 deletions apps/supervisor/src/workloadManager/ecrAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { ECRClient, GetAuthorizationTokenCommand } from "@aws-sdk/client-ecr";
import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
import { tryCatch } from "@trigger.dev/core";
import Docker from "dockerode";

interface ECRTokenCache {
token: string;
username: string;
serverAddress: string;
expiresAt: Date;
}

export class ECRAuthService {
private readonly logger = new SimpleStructuredLogger("ecr-auth-service");
private readonly ecrClient: ECRClient;
private tokenCache: ECRTokenCache | null = null;

constructor() {
this.ecrClient = new ECRClient();

this.logger.info("🔐 ECR Auth Service initialized", {
region: this.ecrClient.config.region,
});
}

/**
* Check if we have AWS credentials configured
*/
static hasAWSCredentials(): boolean {
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
return true;
}

if (
process.env.AWS_PROFILE ||
process.env.AWS_ROLE_ARN ||
process.env.AWS_WEB_IDENTITY_TOKEN_FILE
) {
return true;
}

return false;
}

/**
* Check if the current token is still valid with a 10-minute buffer
*/
private isTokenValid(): boolean {
if (!this.tokenCache) {
return false;
}

const now = new Date();
const bufferMs = 10 * 60 * 1000; // 10 minute buffer before expiration
return now < new Date(this.tokenCache.expiresAt.getTime() - bufferMs);
}

/**
* Get a fresh ECR authorization token from AWS
*/
private async fetchNewToken(): Promise<ECRTokenCache | null> {
const [error, response] = await tryCatch(
this.ecrClient.send(new GetAuthorizationTokenCommand({}))
);

if (error) {
this.logger.error("Failed to get ECR authorization token", { error });
return null;
}

const authData = response.authorizationData?.[0];
if (!authData?.authorizationToken || !authData.proxyEndpoint) {
this.logger.error("Invalid ECR authorization response", { authData });
return null;
}

// Decode the base64 token to get username:password
const decoded = Buffer.from(authData.authorizationToken, "base64").toString("utf-8");
const [username, password] = decoded.split(":", 2);

if (!username || !password) {
this.logger.error("Failed to parse ECR authorization token");
return null;
}

const expiresAt = authData.expiresAt || new Date(Date.now() + 12 * 60 * 60 * 1000); // Default 12 hours

const tokenCache: ECRTokenCache = {
token: password,
username,
serverAddress: authData.proxyEndpoint,
expiresAt,
};

this.logger.info("🔐 Successfully fetched ECR token", {
username,
serverAddress: authData.proxyEndpoint,
expiresAt: expiresAt.toISOString(),
});

return tokenCache;
}

/**
* Get ECR auth config for Docker operations
* Returns cached token if valid, otherwise fetches a new one
*/
async getAuthConfig(): Promise<Docker.AuthConfig | null> {
// Check if cached token is still valid
if (this.isTokenValid()) {
this.logger.debug("Using cached ECR token");
return {
username: this.tokenCache!.username,
password: this.tokenCache!.token,
serveraddress: this.tokenCache!.serverAddress,
};
}

// Fetch new token
this.logger.info("Fetching new ECR authorization token");
const newToken = await this.fetchNewToken();

if (!newToken) {
return null;
}

// Cache the new token
this.tokenCache = newToken;

return {
username: newToken.username,
password: newToken.token,
serveraddress: newToken.serverAddress,
};
}

/**
* Clear the cached token (useful for testing or forcing refresh)
*/
clearCache(): void {
this.tokenCache = null;
this.logger.debug("ECR token cache cleared");
}
}
9 changes: 6 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading