import { CreatePipelineCommand, DeletePipelineCommand, GetPipelineCommand, GetPipelineStateCommand, ListPipelinesCommand, ListPipelineExecutionsCommand, StartPipelineExecutionCommand, type ActionState, type CodePipelineClient, type PipelineDeclaration, } from '@aws-sdk/client-codepipeline' import {RuntimeError, ValidationError} from '../cloud-spi/errors' import {awsCodePipelineSchema} from '../cloud-spi/codepipelineSchema' import type {PipelineExecutionSummary, PipelineState} from '../cloud-spi/codepipeline' import type {CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema} from '../cloud-spi/types' export class AwsCodePipelineAdapter implements CloudServiceAdapter { readonly cloud = 'aws' as const readonly service = 'codepipeline' as const constructor(private readonly client: CodePipelineClient) {} schema(): ServiceSchema { return awsCodePipelineSchema() } async list(query: ResourceQuery = {}): Promise { const pipelines: CloudResource[] = [] let nextToken: string | undefined do { const response = await this.client.send(new ListPipelinesCommand(nextToken ? {nextToken} : {})) pipelines.push(...(response.pipelines ?? []).map((pipeline) => toSummaryResource(pipeline))) nextToken = response.nextToken } while (nextToken) const search = query.search?.trim().toLowerCase() return pipelines.filter((pipeline) => !search || pipeline.name.toLowerCase().includes(search)) } async get(id: string): Promise { const response = await this.client.send(new GetPipelineCommand({name: id})) return response.pipeline ? toResource(response.pipeline, response.metadata) : null } async create(input: CreateResourceInput): Promise { const name = required(input.values.name, 'name') const declaration: PipelineDeclaration = { name, roleArn: required(input.values.roleArn, 'roleArn'), artifactStore: {type: 'S3', location: required(input.values.artifactBucket, 'artifactBucket')}, stages: [ stage('Source', action('Source', 'Source', 'S3', {S3Bucket: required(input.values.sourceBucket, 'sourceBucket'), S3ObjectKey: required(input.values.sourceKey, 'sourceKey'), PollForSourceChanges: 'false'}, [], ['SourceOutput'])), stage('Build', action('Build', 'Build', 'CodeBuild', {ProjectName: required(input.values.codeBuildProject, 'codeBuildProject')}, ['SourceOutput'], ['BuildOutput'])), stage('Deploy', ecsDeployAction(input.values)), ], } const response = await this.client.send(new CreatePipelineCommand({pipeline: declaration})) if (!response.pipeline) throw new RuntimeError(`CodePipeline did not return pipeline ${name}`) return toResource(response.pipeline) } async delete(id: string): Promise { await this.client.send(new DeletePipelineCommand({name: id})) } async startPipeline(id: string): Promise<{id: string}> { const response = await this.client.send(new StartPipelineExecutionCommand({name: id})) if (!response.pipelineExecutionId) throw new RuntimeError(`CodePipeline did not return an execution for ${id}`) return {id: response.pipelineExecutionId} } async listPipelineExecutions(id: string): Promise { const executions: PipelineExecutionSummary[] = [] let nextToken: string | undefined do { const response = await this.client.send(new ListPipelineExecutionsCommand(nextToken ? {pipelineName: id, nextToken} : {pipelineName: id})) for (const execution of response.pipelineExecutionSummaries ?? []) { if (execution.pipelineExecutionId) executions.push({id: execution.pipelineExecutionId, status: execution.status, statusSummary: execution.statusSummary, startTime: execution.startTime, lastUpdateTime: execution.lastUpdateTime}) } nextToken = response.nextToken } while (nextToken) return executions } async getPipelineState(id: string): Promise { const [pipeline, state] = await Promise.all([this.client.send(new GetPipelineCommand({name: id})), this.client.send(new GetPipelineStateCommand({name: id}))]) const actions = (pipeline.pipeline?.stages ?? []).flatMap((stage) => stage.actions ?? []) const codeBuildProjects = new Map(actions.filter((item) => item.actionTypeId?.provider === 'CodeBuild').map((item) => [item.name, item.configuration?.ProjectName] as const)) const ecsDeployActions = new Map(actions.filter((item) => item.actionTypeId?.provider === 'CodeDeployToECS').map((item) => [item.name, {application: item.configuration?.ApplicationName, deploymentGroup: item.configuration?.DeploymentGroupName}] as const)) return {stages: (state.stageStates ?? []).map((stage) => ({name: stage.stageName ?? 'Unknown', status: stage.latestExecution?.status, actions: (stage.actionStates ?? []).map((actionState) => toActionState(actionState, codeBuildProjects, ecsDeployActions))}))} } } function toResource(pipeline: PipelineDeclaration, metadata?: {pipelineArn?: string; created?: Date; updated?: Date}): CloudResource { const name = pipeline.name ?? '' return {id: name, name, cloud: 'aws', service: 'codepipeline', type: 'codepipeline-pipeline', region: regionFromArn(metadata?.pipelineArn), createdAt: metadata?.created?.toISOString() ?? null, version: pipeline.version?.toString(), metadata: {arn: metadata?.pipelineArn, roleArn: pipeline.roleArn, executionMode: pipeline.executionMode, pipelineType: pipeline.pipelineType, artifactBucket: pipeline.artifactStore?.location, updatedAt: metadata?.updated?.toISOString()}} } function toSummaryResource(pipeline: {name?: string; version?: number; created?: Date; updated?: Date; executionMode?: string; pipelineType?: string}): CloudResource { const name = pipeline.name ?? '' return {id: name, name, cloud: 'aws', service: 'codepipeline', type: 'codepipeline-pipeline', region: null, createdAt: pipeline.created?.toISOString() ?? null, version: pipeline.version?.toString(), metadata: {executionMode: pipeline.executionMode, pipelineType: pipeline.pipelineType, updatedAt: pipeline.updated?.toISOString()}} } function toActionState(action: ActionState, codeBuildProjects: Map, ecsDeployActions: Map) { const execution = action.latestExecution const ecsDeploy = ecsDeployActions.get(action.actionName) return {name: action.actionName ?? 'Unknown', status: execution?.status, summary: execution?.summary, lastStatusChange: execution?.lastStatusChange, externalExecutionId: execution?.externalExecutionId, externalExecutionUrl: execution?.externalExecutionUrl, entityUrl: action.entityUrl, revisionUrl: action.revisionUrl, codeBuildProject: codeBuildProjects.get(action.actionName), codeDeployDeploymentId: ecsDeploy ? execution?.externalExecutionId : undefined, codeDeployApplication: ecsDeploy?.application, codeDeployDeploymentGroup: ecsDeploy?.deploymentGroup} } function stage(name: string, item: {name: string; actionTypeId: {category: 'Source' | 'Build' | 'Deploy'; owner: 'AWS'; provider: string; version: string}; configuration: Record; inputArtifacts: Array<{name: string}>; outputArtifacts: Array<{name: string}>}) { return {name, actions: [item]} } function action(name: string, category: 'Source' | 'Build' | 'Deploy', provider: 'S3' | 'CodeBuild', configuration: Record, inputArtifacts: string[], outputArtifacts: string[]) { return {name, actionTypeId: {category, owner: 'AWS' as const, provider, version: '1'}, configuration, inputArtifacts: inputArtifacts.map((name) => ({name})), outputArtifacts: outputArtifacts.map((name) => ({name}))} } function ecsDeployAction(values: Record) { return {name: 'Deploy', actionTypeId: {category: 'Deploy' as const, owner: 'AWS' as const, provider: 'CodeDeployToECS', version: '1'}, configuration: {ApplicationName: required(values.codeDeployApplication, 'codeDeployApplication'), DeploymentGroupName: required(values.codeDeployDeploymentGroup, 'codeDeployDeploymentGroup'), TaskDefinitionTemplateArtifact: 'BuildOutput', TaskDefinitionTemplatePath: required(values.taskDefinitionPath, 'taskDefinitionPath'), AppSpecTemplateArtifact: 'BuildOutput', AppSpecTemplatePath: required(values.appSpecPath, 'appSpecPath'), Image1ArtifactName: 'BuildOutput', Image1ContainerName: required(values.imageContainerName, 'imageContainerName')}, inputArtifacts: [{name: 'BuildOutput'}], outputArtifacts: []} } function required(value: unknown, field: string): string { if (typeof value !== 'string' || !value.trim()) throw new ValidationError(`${field} is required`); return value.trim() } function regionFromArn(arn?: string): string | null { return arn?.split(':')[3] || null }