Skip to content
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
35 changes: 34 additions & 1 deletion apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,47 @@ const EnvironmentSchema = z.object({
DEPOT_ORG_ID: z.string().optional(),
DEPOT_REGION: z.string().default("us-east-1"),

// Deployment registry
// Deployment registry (v3)
DEPLOY_REGISTRY_HOST: z.string().min(1),
DEPLOY_REGISTRY_USERNAME: z.string().optional(),
DEPLOY_REGISTRY_PASSWORD: z.string().optional(),
DEPLOY_REGISTRY_NAMESPACE: z.string().min(1).default("trigger"),
DEPLOY_REGISTRY_ECR_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2"
DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z.string().optional(),
DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z.string().optional(),

// Deployment registry (v4) - falls back to v3 registry if not specified
V4_DEPLOY_REGISTRY_HOST: z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_HOST)
.pipe(z.string().min(1)), // Ensure final type is required string
V4_DEPLOY_REGISTRY_USERNAME: z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_USERNAME),
V4_DEPLOY_REGISTRY_PASSWORD: z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_PASSWORD),
V4_DEPLOY_REGISTRY_NAMESPACE: z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_NAMESPACE)
.pipe(z.string().min(1).default("trigger")), // Ensure final type is required string
V4_DEPLOY_REGISTRY_ECR_TAGS: z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_TAGS),
V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN),
V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z
.string()
.optional()
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID),

DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"),
DEPLOY_TIMEOUT_MS: z.coerce
.number()
Expand Down
28 changes: 13 additions & 15 deletions apps/webapp/app/v3/getDeploymentImageRef.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
import { tryCatch } from "@trigger.dev/core";
import { logger } from "~/services/logger.server";
import { type RegistryConfig } from "./registryConfig.server";

// Optional configuration for cross-account access
export type AssumeRoleConfig = {
Expand Down Expand Up @@ -97,30 +98,24 @@ export async function createEcrClient({
}

export async function getDeploymentImageRef({
host,
namespace,
registry,
projectRef,
nextVersion,
environmentSlug,
registryTags,
assumeRole,
}: {
host: string;
namespace: string;
registry: RegistryConfig;
projectRef: string;
nextVersion: string;
environmentSlug: string;
registryTags?: string;
assumeRole?: AssumeRoleConfig;
}): Promise<{
imageRef: string;
isEcr: boolean;
repoCreated: boolean;
}> {
const repositoryName = `${namespace}/${projectRef}`;
const imageRef = `${host}/${repositoryName}:${nextVersion}.${environmentSlug}`;
const repositoryName = `${registry.namespace}/${projectRef}`;
const imageRef = `${registry.host}/${repositoryName}:${nextVersion}.${environmentSlug}`;

if (!isEcrRegistry(host)) {
if (!isEcrRegistry(registry.host)) {
return {
imageRef,
isEcr: false,
Expand All @@ -131,16 +126,19 @@ export async function getDeploymentImageRef({
const [ecrRepoError, ecrData] = await tryCatch(
ensureEcrRepositoryExists({
repositoryName,
registryHost: host,
registryTags,
assumeRole,
registryHost: registry.host,
registryTags: registry.ecrTags,
assumeRole: {
roleArn: registry.ecrAssumeRoleArn,
externalId: registry.ecrAssumeRoleExternalId,
},
})
);

if (ecrRepoError) {
logger.error("Failed to ensure ECR repository exists", {
repositoryName,
host,
host: registry.host,
ecrRepoError: ecrRepoError.message,
});
throw ecrRepoError;
Expand Down
35 changes: 35 additions & 0 deletions apps/webapp/app/v3/registryConfig.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { env } from "~/env.server";

export type RegistryConfig = {
host: string;
username?: string;
password?: string;
namespace: string;
ecrTags?: string;
ecrAssumeRoleArn?: string;
ecrAssumeRoleExternalId?: string;
};

export function getRegistryConfig(isV4Deployment: boolean): RegistryConfig {
if (isV4Deployment) {
return {
host: env.V4_DEPLOY_REGISTRY_HOST,
username: env.V4_DEPLOY_REGISTRY_USERNAME,
password: env.V4_DEPLOY_REGISTRY_PASSWORD,
namespace: env.V4_DEPLOY_REGISTRY_NAMESPACE,
ecrTags: env.V4_DEPLOY_REGISTRY_ECR_TAGS,
ecrAssumeRoleArn: env.V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
ecrAssumeRoleExternalId: env.V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
};
}

return {
host: env.DEPLOY_REGISTRY_HOST,
username: env.DEPLOY_REGISTRY_USERNAME,
password: env.DEPLOY_REGISTRY_PASSWORD,
namespace: env.DEPLOY_REGISTRY_NAMESPACE,
ecrTags: env.DEPLOY_REGISTRY_ECR_TAGS,
ecrAssumeRoleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
ecrAssumeRoleExternalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
};
}
65 changes: 31 additions & 34 deletions apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ExternalBuildData, FinalizeDeploymentRequestBody } from "@trigger.dev/core/v3/schemas";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import {
ExternalBuildData,
type FinalizeDeploymentRequestBody,
} from "@trigger.dev/core/v3/schemas";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { join } from "node:path";
Expand All @@ -11,6 +14,7 @@ import { FinalizeDeploymentService } from "./finalizeDeployment.server";
import { remoteBuildsEnabled } from "../remoteImageBuilder.server";
import { getEcrAuthToken, isEcrRegistry } from "../getDeploymentImageRef.server";
import { tryCatch } from "@trigger.dev/core";
import { getRegistryConfig, type RegistryConfig } from "../registryConfig.server";

export class FinalizeDeploymentV2Service extends BaseService {
public async call(
Expand All @@ -37,6 +41,7 @@ export class FinalizeDeploymentV2Service extends BaseService {
externalBuildData: true,
environment: true,
imageReference: true,
type: true,
worker: {
select: {
project: true,
Expand Down Expand Up @@ -78,10 +83,13 @@ export class FinalizeDeploymentV2Service extends BaseService {
throw new ServiceValidationError("External build data is invalid");
}

const isV4Deployment = deployment.type === "MANAGED";
const registryConfig = getRegistryConfig(isV4Deployment);

// For non-ECR registries, username and password are required upfront
if (
!env.DEPLOY_REGISTRY_HOST ||
!env.DEPLOY_REGISTRY_USERNAME ||
!env.DEPLOY_REGISTRY_PASSWORD
!isEcrRegistry(registryConfig.host) &&
(!registryConfig.username || !registryConfig.password)
) {
throw new ServiceValidationError("Missing deployment registry credentials");
}
Expand All @@ -104,12 +112,7 @@ export class FinalizeDeploymentV2Service extends BaseService {
orgToken: env.DEPOT_TOKEN,
projectId: externalBuildData.data.projectId,
},
registry: {
host: env.DEPLOY_REGISTRY_HOST,
namespace: env.DEPLOY_REGISTRY_NAMESPACE,
username: env.DEPLOY_REGISTRY_USERNAME,
password: env.DEPLOY_REGISTRY_PASSWORD,
},
registry: registryConfig,
deployment: {
version: deployment.version,
environmentSlug: deployment.environment.slug,
Expand Down Expand Up @@ -144,12 +147,7 @@ type ExecutePushToRegistryOptions = {
orgToken: string;
projectId: string;
};
registry: {
host: string;
namespace: string;
username: string;
password: string;
};
registry: RegistryConfig;
deployment: {
version: string;
environmentSlug: string;
Expand All @@ -175,12 +173,7 @@ async function executePushToRegistry(
writer?: WritableStreamDefaultWriter
): Promise<ExecutePushResult> {
// Step 1: We need to "login" to the registry
const [loginError, configDir] = await tryCatch(
ensureLoggedIntoDockerRegistry(registry.host, {
username: registry.username,
password: registry.password,
})
);
const [loginError, configDir] = await tryCatch(ensureLoggedIntoDockerRegistry(registry));

if (loginError) {
logger.error("Failed to login to registry", {
Expand Down Expand Up @@ -260,31 +253,35 @@ async function executePushToRegistry(
}
}

async function ensureLoggedIntoDockerRegistry(
registryHost: string,
auth: { username: string; password: string } | undefined = undefined
) {
async function ensureLoggedIntoDockerRegistry(registryConfig: RegistryConfig) {
const tmpDir = await createTempDir();
const dockerConfigPath = join(tmpDir, "config.json");

let auth: { username: string; password: string };

// If this is an ECR registry, get fresh credentials
if (isEcrRegistry(registryHost)) {
if (isEcrRegistry(registryConfig.host)) {
auth = await getEcrAuthToken({
registryHost,
assumeRole: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN
registryHost: registryConfig.host,
assumeRole: registryConfig.ecrAssumeRoleArn
? {
roleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
externalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
roleArn: registryConfig.ecrAssumeRoleArn,
externalId: registryConfig.ecrAssumeRoleExternalId,
}
: undefined,
});
} else if (!auth) {
} else if (!registryConfig.username || !registryConfig.password) {
throw new Error("Authentication required for non-ECR registry");
} else {
auth = {
username: registryConfig.username,
password: registryConfig.password,
};
}

await writeJSONFile(dockerConfigPath, {
auths: {
[registryHost]: {
[registryConfig.host]: {
auth: Buffer.from(`${auth.username}:${auth.password}`).toString("base64"),
},
},
Expand Down
14 changes: 5 additions & 9 deletions apps/webapp/app/v3/services/initializeDeployment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { BaseService, ServiceValidationError } from "./baseService.server";
import { TimeoutDeploymentService } from "./timeoutDeployment.server";
import { getDeploymentImageRef } from "../getDeploymentImageRef.server";
import { tryCatch } from "@trigger.dev/core";
import { getRegistryConfig } from "../registryConfig.server";

const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8);

Expand Down Expand Up @@ -69,20 +70,15 @@ export class InitializeDeploymentService extends BaseService {
})
: undefined;

const isV4Deployment = payload.type === "MANAGED";
const registryConfig = getRegistryConfig(isV4Deployment);

const [imageRefError, imageRefResult] = await tryCatch(
getDeploymentImageRef({
host: env.DEPLOY_REGISTRY_HOST,
namespace: env.DEPLOY_REGISTRY_NAMESPACE,
registry: registryConfig,
projectRef: environment.project.externalRef,
nextVersion,
environmentSlug: environment.slug,
registryTags: env.DEPLOY_REGISTRY_ECR_TAGS,
assumeRole: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN
? {
roleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
externalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
}
: undefined,
})
);

Expand Down
Loading
Loading