CI/CD - CodeBuild + CodeDeploy + CodePipeline
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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', action('Deploy', 'Deploy', 'S3', {BucketName: required(input.values.deployBucket, 'deployBucket'), ObjectKey: required(input.values.deployKey, 'deployKey'), Extract: 'true'}, ['BuildOutput'], [])),
|
||||
],
|
||||
}
|
||||
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 codeDeployActions = new Set(actions.filter((item) => item.actionTypeId?.provider === 'CodeDeploy').map((item) => item.name))
|
||||
return {stages: (state.stageStates ?? []).map((stage) => ({name: stage.stageName ?? 'Unknown', status: stage.latestExecution?.status, actions: (stage.actionStates ?? []).map((actionState) => toActionState(actionState, codeBuildProjects, codeDeployActions))}))}
|
||||
}
|
||||
}
|
||||
|
||||
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>, codeDeployActions: Set<string | undefined>) {
|
||||
const execution = action.latestExecution
|
||||
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: codeDeployActions.has(action.actionName) ? execution?.externalExecutionId : undefined}
|
||||
}
|
||||
|
||||
function stage(name: string, item: ReturnType<typeof action>) { 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 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 }
|
||||
Reference in New Issue
Block a user