CI/CD - CodeBuild + CodeDeploy + CodePipeline
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
BatchGetBuildsCommand,
|
||||
BatchGetProjectsCommand,
|
||||
CodeBuildClient,
|
||||
ComputeType,
|
||||
CreateProjectCommand,
|
||||
DeleteProjectCommand,
|
||||
ListBuildsForProjectCommand,
|
||||
ListProjectsCommand,
|
||||
StartBuildCommand,
|
||||
type Build,
|
||||
type Project,
|
||||
} from '@aws-sdk/client-codebuild'
|
||||
import {RuntimeError, ValidationError} from '../cloud-spi/errors'
|
||||
import {awsCodeBuildSchema} from '../cloud-spi/codebuildSchema'
|
||||
import type {CodeBuildBuild, StartCodeBuildInput} from '../cloud-spi/codebuild'
|
||||
import type {CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema} from '../cloud-spi/types'
|
||||
|
||||
export class AwsCodeBuildAdapter implements CloudServiceAdapter {
|
||||
readonly cloud = 'aws' as const
|
||||
readonly service = 'codebuild' as const
|
||||
|
||||
constructor(private readonly client: CodeBuildClient) {}
|
||||
|
||||
schema(): ServiceSchema {
|
||||
return awsCodeBuildSchema()
|
||||
}
|
||||
|
||||
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
|
||||
const names: string[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const response = await this.client.send(new ListProjectsCommand(nextToken ? {nextToken} : {}))
|
||||
names.push(...(response.projects ?? []))
|
||||
nextToken = response.nextToken
|
||||
} while (nextToken)
|
||||
const projects = await this.projectsByName(names)
|
||||
const normalized = query.search?.trim().toLowerCase()
|
||||
return projects.map(toResource).filter((project) => !normalized || project.name.toLowerCase().includes(normalized))
|
||||
}
|
||||
|
||||
async get(id: string): Promise<CloudResource | null> {
|
||||
const [project] = await this.projectsByName([id])
|
||||
return project ? toResource(project) : null
|
||||
}
|
||||
|
||||
async create(input: CreateResourceInput): Promise<CloudResource> {
|
||||
const name = required(input.values.name, 'name')
|
||||
const serviceRole = required(input.values.serviceRole, 'serviceRole')
|
||||
const image = optional(input.values.environmentImage) ?? 'aws/codebuild/standard:7.0'
|
||||
const computeType = optional(input.values.computeType) ?? ComputeType.BUILD_GENERAL1_SMALL
|
||||
if (computeType !== ComputeType.BUILD_GENERAL1_SMALL) throw new ValidationError('computeType must be BUILD_GENERAL1_SMALL')
|
||||
const response = await this.client.send(new CreateProjectCommand({
|
||||
name,
|
||||
serviceRole,
|
||||
source: {type: 'NO_SOURCE'},
|
||||
artifacts: {type: 'NO_ARTIFACTS'},
|
||||
environment: {type: 'LINUX_CONTAINER', image, computeType},
|
||||
}))
|
||||
if (!response.project) throw new RuntimeError(`CodeBuild did not return project ${name}`)
|
||||
return toResource(response.project)
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.client.send(new DeleteProjectCommand({name: id}))
|
||||
}
|
||||
|
||||
async startBuild(projectName: string, input: StartCodeBuildInput): Promise<CodeBuildBuild> {
|
||||
const response = await this.client.send(new StartBuildCommand({
|
||||
projectName,
|
||||
environmentVariablesOverride: input.environmentVariablesOverride,
|
||||
}))
|
||||
if (!response.build) throw new RuntimeError(`CodeBuild did not return a build for ${projectName}`)
|
||||
return toBuild(response.build)
|
||||
}
|
||||
|
||||
async listBuilds(projectName: string): Promise<CodeBuildBuild[]> {
|
||||
const ids: string[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const response = await this.client.send(new ListBuildsForProjectCommand(nextToken ? {projectName, nextToken} : {projectName}))
|
||||
ids.push(...(response.ids ?? []))
|
||||
nextToken = response.nextToken
|
||||
} while (nextToken)
|
||||
return this.buildsById(ids)
|
||||
}
|
||||
|
||||
async getBuild(id: string): Promise<CodeBuildBuild | null> {
|
||||
const [build] = await this.buildsById([id])
|
||||
return build ?? null
|
||||
}
|
||||
|
||||
private async projectsByName(names: string[]): Promise<Project[]> {
|
||||
const projects: Project[] = []
|
||||
for (let index = 0; index < names.length; index += 100) {
|
||||
const response = await this.client.send(new BatchGetProjectsCommand({names: names.slice(index, index + 100)}))
|
||||
projects.push(...(response.projects ?? []))
|
||||
}
|
||||
return projects
|
||||
}
|
||||
|
||||
private async buildsById(ids: string[]): Promise<CodeBuildBuild[]> {
|
||||
const builds: CodeBuildBuild[] = []
|
||||
for (let index = 0; index < ids.length; index += 100) {
|
||||
const response = await this.client.send(new BatchGetBuildsCommand({ids: ids.slice(index, index + 100)}))
|
||||
builds.push(...(response.builds ?? []).map(toBuild))
|
||||
}
|
||||
return builds
|
||||
}
|
||||
}
|
||||
|
||||
function toResource(project: Project): CloudResource {
|
||||
const name = project.name ?? ''
|
||||
return {
|
||||
id: name, name, cloud: 'aws', service: 'codebuild', type: 'codebuild-project', region: regionFromArn(project.arn),
|
||||
createdAt: project.created ? project.created.toISOString() : null,
|
||||
metadata: {
|
||||
arn: project.arn,
|
||||
serviceRole: project.serviceRole,
|
||||
sourceType: project.source?.type,
|
||||
artifactsType: project.artifacts?.type,
|
||||
environmentType: project.environment?.type,
|
||||
environmentImage: project.environment?.image,
|
||||
computeType: project.environment?.computeType,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function toBuild(build: Build): CodeBuildBuild {
|
||||
const id = build.id ?? build.arn
|
||||
if (!id || !build.projectName) throw new RuntimeError('CodeBuild returned a build without an id or project name')
|
||||
return {
|
||||
id, arn: build.arn, projectName: build.projectName, buildNumber: build.buildNumber, buildStatus: build.buildStatus,
|
||||
currentPhase: build.currentPhase, startTime: build.startTime, endTime: build.endTime,
|
||||
phases: (build.phases ?? []).map((phase) => ({phaseType: phase.phaseType ?? 'UNKNOWN', phaseStatus: phase.phaseStatus, startTime: phase.startTime, endTime: phase.endTime, contexts: phase.contexts ?? []})),
|
||||
logGroupName: build.logs?.groupName, logStreamName: build.logs?.streamName,
|
||||
}
|
||||
}
|
||||
|
||||
function required(value: unknown, field: string): string {
|
||||
const result = optional(value)
|
||||
if (!result) throw new ValidationError(`${field} is required`)
|
||||
return result
|
||||
}
|
||||
|
||||
function optional(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
||||
}
|
||||
|
||||
function regionFromArn(arn?: string): string | null {
|
||||
return arn?.split(':')[3] || null
|
||||
}
|
||||
Reference in New Issue
Block a user