111 lines
9.0 KiB
TypeScript
111 lines
9.0 KiB
TypeScript
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<CloudResource[]> {
|
|
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<CloudResource | null> {
|
|
const response = await this.client.send(new GetPipelineCommand({name: id}))
|
|
return response.pipeline ? toResource(response.pipeline, response.metadata) : null
|
|
}
|
|
|
|
async create(input: CreateResourceInput): Promise<CloudResource> {
|
|
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<void> { 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<PipelineExecutionSummary[]> {
|
|
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<PipelineState> {
|
|
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<string | undefined, string | undefined>, ecsDeployActions: Map<string | undefined, {application?: string; deploymentGroup?: string; cluster?: string; service?: string}>) {
|
|
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<string, string>; 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<string, string>, 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<string, unknown>) { 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 }
|