UNPKG

@cloudsnorkel/cdk-github-runners

Version:

CDK construct to create GitHub Actions self-hosted runners. Creates ephemeral runners on demand. Easy to deploy and highly customizable.

776 lines 116 kB
"use strict"; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.AwsImageBuilderFailedBuildNotifier = exports.AwsImageBuilderRunnerImageBuilder = exports.ImageBuilderComponent = void 0; const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti"); const cdk = require("aws-cdk-lib"); const aws_cdk_lib_1 = require("aws-cdk-lib"); const aws_ecr_1 = require("aws-cdk-lib/aws-ecr"); const aws_logs_1 = require("aws-cdk-lib/aws-logs"); const ami_1 = require("./ami"); const base_image_1 = require("./base-image"); const container_1 = require("./container"); const delete_resources_function_1 = require("./delete-resources-function"); const filter_failed_builds_function_1 = require("./filter-failed-builds-function"); const workflow_1 = require("./workflow"); const providers_1 = require("../../providers"); const utils_1 = require("../../utils"); const build_image_function_1 = require("../build-image-function"); const common_1 = require("../common"); /** * Components are a set of commands to run and optional files to add to an image. Components are the building blocks of images built by Image Builder. * * Example: * * ``` * new ImageBuilderComponent(this, 'AWS CLI', { * platform: 'Windows', * displayName: 'AWS CLI', * description: 'Install latest version of AWS CLI', * commands: [ * '$p = Start-Process msiexec.exe -PassThru -Wait -ArgumentList \'/i https://awscli.amazonaws.com/AWSCLIV2.msi /qn\'', * 'if ($p.ExitCode -ne 0) { throw "Exit code is $p.ExitCode" }', * ], * } * ``` * * @deprecated Use `RunnerImageComponent` instead as this be internal soon. */ class ImageBuilderComponent extends cdk.Resource { constructor(scope, id, props) { super(scope, id); this.assets = []; this.platform = props.platform; let steps = []; if (props.assets) { let inputs = []; let extractCommands = []; for (const asset of props.assets) { this.assets.push(asset.asset); if (asset.asset.isFile) { inputs.push({ source: asset.asset.s3ObjectUrl, destination: asset.path, }); } else if (asset.asset.isZipArchive) { inputs.push({ source: asset.asset.s3ObjectUrl, destination: `${asset.path}.zip`, }); if (props.platform === 'Windows') { extractCommands.push(`Expand-Archive "${asset.path}.zip" -DestinationPath "${asset.path}"`); extractCommands.push(`del "${asset.path}.zip"`); } else { extractCommands.push(`unzip "${asset.path}.zip" -d "${asset.path}"`); extractCommands.push(`rm "${asset.path}.zip"`); } } else { throw new Error(`Unknown asset type: ${asset.asset}`); } } steps.push({ name: 'Download', action: 'S3Download', inputs, }); if (extractCommands.length > 0) { steps.push({ name: 'Extract', action: props.platform === 'Linux' ? 'ExecuteBash' : 'ExecutePowerShell', inputs: { commands: this.prefixCommandsWithErrorHandling(props.platform, extractCommands), }, }); } } if (props.commands.length > 0) { steps.push({ name: 'Run', action: props.platform === 'Linux' ? 'ExecuteBash' : 'ExecutePowerShell', inputs: { commands: this.prefixCommandsWithErrorHandling(props.platform, props.commands), }, }); } if (props.reboot ?? false) { steps.push({ name: 'Reboot', action: 'Reboot', inputs: {}, }); } const data = { name: props.displayName, schemaVersion: '1.0', phases: [ { name: 'build', steps, }, ], }; const name = (0, common_1.uniqueImageBuilderName)(this); const component = new aws_cdk_lib_1.aws_imagebuilder.CfnComponent(this, 'Component', { name: name, description: props.description, platform: props.platform, version: '1.0.0', data: JSON.stringify(data), }); this.arn = component.attrArn; } /** * Grants read permissions to the principal on the assets buckets. * * @param grantee */ grantAssetsRead(grantee) { for (const asset of this.assets) { asset.grantRead(grantee); } } prefixCommandsWithErrorHandling(platform, commands) { if (platform == 'Windows') { return [ '$ErrorActionPreference = \'Stop\'', '$ProgressPreference = \'SilentlyContinue\'', 'Set-PSDebug -Trace 1', ].concat(commands); } else { return [ 'set -ex', ].concat(commands); } } } exports.ImageBuilderComponent = ImageBuilderComponent; _a = JSII_RTTI_SYMBOL_1; ImageBuilderComponent[_a] = { fqn: "@cloudsnorkel/cdk-github-runners.ImageBuilderComponent", version: "0.14.21" }; /** * @internal */ class AwsImageBuilderRunnerImageBuilder extends common_1.RunnerImageBuilderBase { constructor(scope, id, props) { super(scope, id, props); this.boundComponents = []; if (props?.codeBuildOptions) { aws_cdk_lib_1.Annotations.of(this).addWarning('codeBuildOptions are ignored when using AWS Image Builder to build runner images.'); } this.os = props?.os ?? providers_1.Os.LINUX_UBUNTU; this.architecture = props?.architecture ?? providers_1.Architecture.X86_64; this.rebuildInterval = props?.rebuildInterval ?? aws_cdk_lib_1.Duration.days(7); this.logRetention = props?.logRetention ?? aws_logs_1.RetentionDays.ONE_MONTH; this.logRemovalPolicy = props?.logRemovalPolicy ?? aws_cdk_lib_1.RemovalPolicy.DESTROY; this.vpc = props?.vpc ?? aws_cdk_lib_1.aws_ec2.Vpc.fromLookup(this, 'VPC', { isDefault: true }); this.securityGroups = props?.securityGroups ?? [new aws_cdk_lib_1.aws_ec2.SecurityGroup(this, 'SG', { vpc: this.vpc })]; this.subnetSelection = props?.subnetSelection; this.instanceType = props?.awsImageBuilderOptions?.instanceType ?? aws_cdk_lib_1.aws_ec2.InstanceType.of(aws_cdk_lib_1.aws_ec2.InstanceClass.M6I, aws_cdk_lib_1.aws_ec2.InstanceSize.LARGE); this.fastLaunchOptions = props?.awsImageBuilderOptions?.fastLaunchOptions; this.storageSize = props?.awsImageBuilderOptions?.storageSize; this.waitOnDeploy = props?.waitOnDeploy ?? true; this.dockerSetupCommands = props?.dockerSetupCommands ?? []; // normalize BaseContainerImageInput to BaseContainerImage (string support is deprecated, only at public API level) const baseDockerImageInput = props?.baseDockerImage ?? (0, container_1.defaultBaseDockerImage)(this.os); this.baseImage = typeof baseDockerImageInput === 'string' ? base_image_1.BaseContainerImage.fromString(baseDockerImageInput) : baseDockerImageInput; // normalize BaseImageInput to BaseImage (string support is deprecated, only at public API level) const baseAmiInput = props?.baseAmi ?? (0, ami_1.defaultBaseAmi)(this, this.os, this.architecture); this.baseAmi = typeof baseAmiInput === 'string' ? base_image_1.BaseImage.fromString(baseAmiInput) : baseAmiInput; // warn if using deprecated string format if (props?.baseDockerImage && typeof props.baseDockerImage === 'string') { aws_cdk_lib_1.Annotations.of(this).addWarning('Passing baseDockerImage as a string is deprecated. Please use BaseContainerImage static factory methods instead, e.g., BaseContainerImage.fromDockerHub("ubuntu", "22.04") or BaseContainerImage.fromString("public.ecr.aws/lts/ubuntu:22.04")'); } if (props?.baseAmi && typeof props.baseAmi === 'string') { aws_cdk_lib_1.Annotations.of(this).addWarning('Passing baseAmi as a string is deprecated. Please use BaseImage static factory methods instead, e.g., BaseImage.fromAmiId("ami-12345") or BaseImage.fromString("arn:aws:...")'); } // tags for finding resources this.tags = { 'GitHubRunners:Stack': cdk.Stack.of(this).stackName, 'GitHubRunners:Builder': this.node.path, }; // confirm instance type if (!this.architecture.instanceTypeMatch(this.instanceType)) { throw new Error(`Builder architecture (${this.architecture.name}) doesn't match selected instance type (${this.instanceType} / ${this.instanceType.architecture})`); } // warn against isolated networks if (props?.subnetSelection?.subnetType == aws_cdk_lib_1.aws_ec2.SubnetType.PRIVATE_ISOLATED) { aws_cdk_lib_1.Annotations.of(this).addWarning('Private isolated subnets cannot pull from public ECR and VPC endpoint is not supported yet. ' + 'See https://github.com/aws/containers-roadmap/issues/1160'); } // role to be used by AWS Image Builder this.role = new aws_cdk_lib_1.aws_iam.Role(this, 'Role', { assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal('ec2.amazonaws.com'), }); // create container workflow if docker setup commands are provided if (this.dockerSetupCommands.length > 0) { this.containerWorkflow = (0, workflow_1.generateBuildWorkflowWithDockerSetupCommands)(this, 'Build', this.os, this.dockerSetupCommands); this.containerWorkflowExecutionRole = aws_cdk_lib_1.aws_iam.Role.fromRoleArn(this, 'Image Builder Role', cdk.Stack.of(this).formatArn({ service: 'iam', region: '', resource: 'role', resourceName: 'aws-service-role/imagebuilder.amazonaws.com/AWSServiceRoleForImageBuilder', })); } } platform() { if (this.os.is(providers_1.Os.WINDOWS)) { return 'Windows'; } if (this.os.isIn(providers_1.Os._ALL_LINUX_VERSIONS)) { return 'Linux'; } throw new Error(`OS ${this.os.name} is not supported by AWS Image Builder`); } /** * Called by IRunnerProvider to finalize settings and create the image builder. */ bindDockerImage() { if (this.boundDockerImage) { return this.boundDockerImage; } // create repository that only keeps one tag const repository = new aws_cdk_lib_1.aws_ecr.Repository(this, 'Repository', { imageScanOnPush: true, imageTagMutability: aws_ecr_1.TagMutability.MUTABLE, removalPolicy: aws_cdk_lib_1.RemovalPolicy.DESTROY, emptyOnDelete: true, }); const dist = new aws_cdk_lib_1.aws_imagebuilder.CfnDistributionConfiguration(this, 'Docker Distribution', { name: (0, common_1.uniqueImageBuilderName)(this), // description: this.description, distributions: [ { region: aws_cdk_lib_1.Stack.of(this).region, containerDistributionConfiguration: { ContainerTags: ['latest'], TargetRepository: { Service: 'ECR', RepositoryName: repository.repositoryName, }, }, }, ], tags: this.tags, }); let dockerfileTemplate = `FROM {{{ imagebuilder:parentImage }}} {{{ imagebuilder:environments }}} {{{ imagebuilder:components }}}`; for (const c of this.components) { const commands = c.getDockerCommands(this.os, this.architecture); if (commands.length > 0) { dockerfileTemplate += '\n' + commands.join('\n') + '\n'; } } const recipe = new container_1.ContainerRecipe(this, 'Container Recipe', { platform: this.platform(), components: this.bindComponents(), targetRepository: repository, dockerfileTemplate: dockerfileTemplate, parentImage: this.baseImage.image, tags: this.tags, }); const log = this.createLog('Docker Log', recipe.name); const infra = this.createInfrastructure([ aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('EC2InstanceProfileForImageBuilderECRContainerBuilds'), ]); if (this.waitOnDeploy) { this.createImage(infra, dist, log, undefined, recipe.arn); } this.dockerImageCleaner(recipe, repository); this.createPipeline(infra, dist, log, undefined, recipe.arn); this.boundDockerImage = { imageRepository: repository, imageTag: 'latest', os: this.os, architecture: this.architecture, logGroup: log, runnerVersion: providers_1.RunnerVersion.specific('unknown'), // no dependable as CloudFormation will fail to get image ARN once the image is deleted (we delete old images daily) }; return this.boundDockerImage; } dockerImageCleaner(recipe, repository) { // this is here to provide safe upgrade from old cdk-github-runners versions // this lambda was used by a custom resource to delete all images builds on cleanup // if we remove the custom resource and the lambda, the old images will be deleted on update // keeping the lambda but removing the permissions will make sure that deletion will fail const oldDeleter = (0, utils_1.singletonLambda)(build_image_function_1.BuildImageFunction, this, 'build-image', { description: 'Custom resource handler that triggers CodeBuild to build runner images', timeout: cdk.Duration.minutes(3), logGroup: (0, utils_1.singletonLogGroup)(this, utils_1.SingletonLogType.RUNNER_IMAGE_BUILD), loggingFormat: aws_cdk_lib_1.aws_lambda.LoggingFormat.JSON, }); oldDeleter.addToRolePolicy(new aws_cdk_lib_1.aws_iam.PolicyStatement({ effect: aws_cdk_lib_1.aws_iam.Effect.DENY, actions: ['imagebuilder:DeleteImage'], resources: ['*'], })); // delete old version on update and on stack deletion this.imageCleaner('Container', recipe.name.toLowerCase(), recipe.version); // delete old docker images + IB resources daily new aws_cdk_lib_1.aws_imagebuilder.CfnLifecyclePolicy(this, 'Lifecycle Policy Docker', { name: (0, common_1.uniqueImageBuilderName)(this), description: `Delete old GitHub Runner Docker images for ${this.node.path}`, executionRole: new aws_cdk_lib_1.aws_iam.Role(this, 'Lifecycle Policy Docker Role', { assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal('imagebuilder.amazonaws.com'), inlinePolicies: { ib: new aws_cdk_lib_1.aws_iam.PolicyDocument({ statements: [ new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['tag:GetResources', 'imagebuilder:DeleteImage'], resources: ['*'], // Image Builder doesn't support scoping this :( }), ], }), ecr: new aws_cdk_lib_1.aws_iam.PolicyDocument({ statements: [ new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['ecr:BatchGetImage', 'ecr:BatchDeleteImage'], resources: [repository.repositoryArn], }), ], }), }, }).roleArn, policyDetails: [{ action: { type: 'DELETE', includeResources: { containers: true, }, }, filter: { type: 'COUNT', value: 2, }, }], resourceType: 'CONTAINER_IMAGE', resourceSelection: { recipes: [ { name: recipe.name, semanticVersion: recipe.version, }, ], }, }); } createLog(id, recipeName) { return new aws_cdk_lib_1.aws_logs.LogGroup(this, id, { logGroupName: `/aws/imagebuilder/${recipeName}`, retention: this.logRetention, removalPolicy: this.logRemovalPolicy, }); } createInfrastructure(managedPolicies) { if (this.infrastructure) { return this.infrastructure; } for (const managedPolicy of managedPolicies) { this.role.addManagedPolicy(managedPolicy); } for (const component of this.boundComponents) { component.grantAssetsRead(this.role); } this.infrastructure = new aws_cdk_lib_1.aws_imagebuilder.CfnInfrastructureConfiguration(this, 'Infrastructure', { name: (0, common_1.uniqueImageBuilderName)(this), // description: this.description, subnetId: this.vpc?.selectSubnets(this.subnetSelection).subnetIds[0], securityGroupIds: this.securityGroups?.map(sg => sg.securityGroupId), instanceTypes: [this.instanceType.toString()], instanceMetadataOptions: { httpTokens: 'required', // Container builds require a minimum of two hops. httpPutResponseHopLimit: 2, }, instanceProfileName: new aws_cdk_lib_1.aws_iam.CfnInstanceProfile(this, 'Instance Profile', { roles: [ this.role.roleName, ], }).ref, }); return this.infrastructure; } workflowConfig(containerRecipeArn) { if (this.containerWorkflow && this.containerWorkflowExecutionRole && containerRecipeArn) { return { workflows: [{ workflowArn: this.containerWorkflow.arn, }], executionRole: this.containerWorkflowExecutionRole.roleArn, }; } return undefined; } createImage(infra, dist, log, imageRecipeArn, containerRecipeArn) { const image = new aws_cdk_lib_1.aws_imagebuilder.CfnImage(this, this.amiOrContainerId('Image', imageRecipeArn, containerRecipeArn), { infrastructureConfigurationArn: infra.attrArn, distributionConfigurationArn: dist.attrArn, imageRecipeArn, containerRecipeArn, imageTestsConfiguration: { imageTestsEnabled: false, }, tags: this.tags, ...this.workflowConfig(containerRecipeArn), }); image.node.addDependency(infra); image.node.addDependency(log); // do not delete the image as it will be deleted by imageCleaner(). // if we delete it here, imageCleaner() won't be able to find the image. // if imageCleaner() can't find the image, it won't be able to delete the linked AMI/Docker image. // use RETAIN_ON_UPDATE_OR_DELETE, so everything is cleaned only on rollback. image.applyRemovalPolicy(aws_cdk_lib_1.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE); return image; } amiOrContainerId(baseId, imageRecipeArn, containerRecipeArn) { if (imageRecipeArn) { return `AMI ${baseId}`; } if (containerRecipeArn) { return `Docker ${baseId}`; } throw new Error('Either imageRecipeArn or containerRecipeArn must be defined'); } createPipeline(infra, dist, log, imageRecipeArn, containerRecipeArn) { // set schedule let scheduleOptions; if (this.rebuildInterval.toDays() > 0) { scheduleOptions = { scheduleExpression: aws_cdk_lib_1.aws_events.Schedule.rate(this.rebuildInterval).expressionString, pipelineExecutionStartCondition: 'EXPRESSION_MATCH_ONLY', }; } // generate pipeline const pipeline = new aws_cdk_lib_1.aws_imagebuilder.CfnImagePipeline(this, this.amiOrContainerId('Pipeline', imageRecipeArn, containerRecipeArn), { name: (0, common_1.uniqueImageBuilderName)(this), // description: this.description, infrastructureConfigurationArn: infra.attrArn, distributionConfigurationArn: dist.attrArn, imageRecipeArn, containerRecipeArn, schedule: scheduleOptions, imageTestsConfiguration: { imageTestsEnabled: false, }, tags: this.tags, ...this.workflowConfig(containerRecipeArn), }); pipeline.node.addDependency(infra); pipeline.node.addDependency(log); return pipeline; } /** * The network connections associated with this resource. */ get connections() { return new aws_cdk_lib_1.aws_ec2.Connections({ securityGroups: this.securityGroups }); } get grantPrincipal() { return this.role; } bindAmi() { if (this.boundAmi) { return this.boundAmi; } const launchTemplate = new aws_cdk_lib_1.aws_ec2.LaunchTemplate(this, 'Launch template', { requireImdsv2: true, }); const launchTemplateConfigs = [{ launchTemplateId: launchTemplate.launchTemplateId, setDefaultVersion: true, }]; const fastLaunchConfigs = []; if (this.fastLaunchOptions?.enabled ?? false) { if (!this.os.is(providers_1.Os.WINDOWS)) { throw new Error('Fast launch is only supported for Windows'); } // create a separate launch template for fast launch so: // - settings don't affect the runners // - enabling fast launch on an existing builder works (without a new launch template, EC2 Image Builder will use the first version of the launch template, which doesn't have instance or VPC config) // - setting vpc + subnet on the main launch template will cause RunInstances to fail // - EC2 Image Builder seems to get confused with which launch template version to base any new version on, so a new template is always best const fastLaunchTemplate = new aws_cdk_lib_1.aws_ec2.CfnLaunchTemplate(this, 'Fast Launch Template', { launchTemplateData: { metadataOptions: { httpTokens: 'required', }, instanceType: this.instanceType.toString(), networkInterfaces: [{ subnetId: this.vpc?.selectSubnets(this.subnetSelection).subnetIds[0], deviceIndex: 0, groups: this.securityGroups.map(sg => sg.securityGroupId), }], tagSpecifications: [ { resourceType: 'instance', tags: [{ key: 'Name', value: `${this.node.path}/Fast Launch Instance`, }], }, { resourceType: 'volume', tags: [{ key: 'Name', value: `${this.node.path}/Fast Launch Instance`, }], }, ], }, tagSpecifications: [{ resourceType: 'launch-template', tags: [{ key: 'Name', value: `${this.node.path}/Fast Launch Template`, }], }], }); launchTemplateConfigs.push({ launchTemplateId: fastLaunchTemplate.attrLaunchTemplateId, setDefaultVersion: true, }); fastLaunchConfigs.push({ enabled: true, launchTemplate: { launchTemplateId: fastLaunchTemplate.attrLaunchTemplateId, }, maxParallelLaunches: this.fastLaunchOptions?.maxParallelLaunches ?? 6, snapshotConfiguration: { targetResourceCount: this.fastLaunchOptions?.targetResourceCount ?? 1, }, }); } const stackName = cdk.Stack.of(this).stackName; const builderName = this.node.path; const dist = new aws_cdk_lib_1.aws_imagebuilder.CfnDistributionConfiguration(this, 'AMI Distribution', { name: (0, common_1.uniqueImageBuilderName)(this), // description: this.description, distributions: [ { region: aws_cdk_lib_1.Stack.of(this).region, amiDistributionConfiguration: { Name: `${cdk.Names.uniqueResourceName(this, { maxLength: 100, separator: '-', allowedSpecialCharacters: '_-', })}-{{ imagebuilder:buildDate }}`, AmiTags: { 'Name': this.node.id, 'GitHubRunners:Stack': stackName, 'GitHubRunners:Builder': builderName, }, }, launchTemplateConfigurations: launchTemplateConfigs, fastLaunchConfigurations: fastLaunchConfigs.length > 0 ? fastLaunchConfigs : undefined, }, ], tags: this.tags, }); const recipe = new ami_1.AmiRecipe(this, 'Ami Recipe', { platform: this.platform(), components: this.bindComponents(), architecture: this.architecture, baseAmi: this.baseAmi, storageSize: this.storageSize, tags: this.tags, }); const log = this.createLog('Ami Log', recipe.name); const infra = this.createInfrastructure([ aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('EC2InstanceProfileForImageBuilder'), ]); if (this.waitOnDeploy) { this.createImage(infra, dist, log, recipe.arn, undefined); } this.createPipeline(infra, dist, log, recipe.arn, undefined); this.boundAmi = { launchTemplate: launchTemplate, architecture: this.architecture, os: this.os, logGroup: log, runnerVersion: providers_1.RunnerVersion.specific('unknown'), }; this.amiCleaner(recipe, stackName, builderName); return this.boundAmi; } amiCleaner(recipe, stackName, builderName) { // this is here to provide safe upgrade from old cdk-github-runners versions // this lambda was used by a custom resource to delete all amis when the builder was removed // if we remove the custom resource, role and lambda, all amis will be deleted on update // keeping the just role but removing the permissions along with the custom resource will make sure that deletion will fail const stack = cdk.Stack.of(this); if (stack.node.tryFindChild('delete-ami-dcc036c8-876b-451e-a2c1-552f9e06e9e1') == undefined) { const role = new aws_cdk_lib_1.aws_iam.Role(stack, 'delete-ami-dcc036c8-876b-451e-a2c1-552f9e06e9e1', { description: 'Empty role to prevent deletion of AMIs on cdk-github-runners upgrade', assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal('lambda.amazonaws.com'), inlinePolicies: { deny: new aws_cdk_lib_1.aws_iam.PolicyDocument({ statements: [ new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['ec2:DeregisterImage', 'ec2:DeleteSnapshot'], resources: ['*'], effect: aws_cdk_lib_1.aws_iam.Effect.DENY, }), ], }), }, }); const l1role = role.node.defaultChild; l1role.overrideLogicalId('deleteamidcc036c8876b451ea2c1552f9e06e9e1ServiceRole1CC58A6F'); } // delete old version on update and on stack deletion this.imageCleaner('Image', recipe.name.toLowerCase(), recipe.version); // delete old AMIs + IB resources daily new aws_cdk_lib_1.aws_imagebuilder.CfnLifecyclePolicy(this, 'Lifecycle Policy AMI', { name: (0, common_1.uniqueImageBuilderName)(this), description: `Delete old GitHub Runner AMIs for ${this.node.path}`, executionRole: new aws_cdk_lib_1.aws_iam.Role(this, 'Lifecycle Policy AMI Role', { assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal('imagebuilder.amazonaws.com'), inlinePolicies: { ib: new aws_cdk_lib_1.aws_iam.PolicyDocument({ statements: [ new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['tag:GetResources', 'imagebuilder:DeleteImage'], resources: ['*'], // Image Builder doesn't support scoping this :( }), ], }), ami: new aws_cdk_lib_1.aws_iam.PolicyDocument({ statements: [ new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['ec2:DescribeImages', 'ec2:DescribeImageAttribute'], resources: ['*'], }), new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['ec2:DeregisterImage', 'ec2:DeleteSnapshot'], resources: ['*'], conditions: { StringEquals: { 'aws:ResourceTag/GitHubRunners:Stack': stackName, 'aws:ResourceTag/GitHubRunners:Builder': builderName, }, }, }), ], }), }, }).roleArn, policyDetails: [{ action: { type: 'DELETE', includeResources: { amis: true, snapshots: true, }, }, filter: { type: 'COUNT', value: 2, }, }], resourceType: 'AMI_IMAGE', resourceSelection: { recipes: [ { name: recipe.name, semanticVersion: recipe.version, // docs say it's optional, but it's not }, ], }, }); } bindComponents() { if (this.boundComponents.length == 0) { this.boundComponents.push(...this.components.map(c => c._asAwsImageBuilderComponent(this, this.os, this.architecture))); } return this.boundComponents; } imageCleaner(type, recipeName, version) { const cleanerFunction = (0, utils_1.singletonLambda)(delete_resources_function_1.DeleteResourcesFunction, this, 'aws-image-builder-delete-resources', { description: 'Custom resource handler that deletes resources of old versions of EC2 Image Builder images', initialPolicy: [ new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: [ 'imagebuilder:ListImageBuildVersions', 'imagebuilder:DeleteImage', ], resources: ['*'], }), new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['ec2:DescribeImages'], resources: ['*'], }), new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['ec2:DeregisterImage', 'ec2:DeleteSnapshot'], resources: ['*'], conditions: { StringEquals: { 'aws:ResourceTag/GitHubRunners:Stack': cdk.Stack.of(this).stackName, }, }, }), new aws_cdk_lib_1.aws_iam.PolicyStatement({ actions: ['ecr:BatchDeleteImage'], resources: ['*'], }), ], logGroup: (0, utils_1.singletonLogGroup)(this, utils_1.SingletonLogType.RUNNER_IMAGE_BUILD), loggingFormat: aws_cdk_lib_1.aws_lambda.LoggingFormat.JSON, timeout: cdk.Duration.minutes(10), }); new aws_cdk_lib_1.CustomResource(this, `${type} Cleaner`, { serviceToken: cleanerFunction.functionArn, resourceType: 'Custom::ImageBuilder-Delete-Resources', properties: { ImageVersionArn: cdk.Stack.of(this).formatArn({ service: 'imagebuilder', resource: 'image', resourceName: `${recipeName}/${version}`, arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, }), }, }); } } exports.AwsImageBuilderRunnerImageBuilder = AwsImageBuilderRunnerImageBuilder; /** * @internal */ class AwsImageBuilderFailedBuildNotifier { static createFilteringTopic(scope, targetTopic) { const topic = new aws_cdk_lib_1.aws_sns.Topic(scope, 'Image Builder Builds'); const filter = new filter_failed_builds_function_1.FilterFailedBuildsFunction(scope, 'Image Builder Builds Filter', { logGroup: (0, utils_1.singletonLogGroup)(scope, utils_1.SingletonLogType.RUNNER_IMAGE_BUILD), loggingFormat: aws_cdk_lib_1.aws_lambda.LoggingFormat.JSON, environment: { TARGET_TOPIC_ARN: targetTopic.topicArn, }, }); topic.addSubscription(new aws_cdk_lib_1.aws_sns_subscriptions.LambdaSubscription(filter)); targetTopic.grantPublish(filter); return topic; } constructor(topic) { this.topic = topic; } visit(node) { if (node instanceof AwsImageBuilderRunnerImageBuilder) { const builder = node; const infraNode = builder.node.tryFindChild('Infrastructure'); if (infraNode) { const infra = infraNode; infra.snsTopicArn = this.topic.topicArn; } else { cdk.Annotations.of(builder).addWarning('Unused builder cannot get notifications of failed builds'); } } } } exports.AwsImageBuilderFailedBuildNotifier = AwsImageBuilderFailedBuildNotifier; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVpbGRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9pbWFnZS1idWlsZGVycy9hd3MtaW1hZ2UtYnVpbGRlci9idWlsZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUEsbUNBQW1DO0FBQ25DLDZDQWdCcUI7QUFDckIsaURBQW9EO0FBQ3BELG1EQUFxRDtBQUVyRCwrQkFBa0Q7QUFDbEQsNkNBQTZEO0FBQzdELDJDQUFzRTtBQUN0RSwyRUFBc0U7QUFFdEUsbUZBQTZFO0FBQzdFLHlDQUFvRjtBQUNwRiwrQ0FBMEY7QUFDMUYsdUNBQW1GO0FBQ25GLGtFQUE2RDtBQUM3RCxzQ0FBb0c7QUFvSHBHOzs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FrQkc7QUFDSCxNQUFhLHFCQUFzQixTQUFRLEdBQUcsQ0FBQyxRQUFRO0lBYXJELFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBc0M7UUFDOUUsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUhGLFdBQU0sR0FBc0IsRUFBRSxDQUFDO1FBSzlDLElBQUksQ0FBQyxRQUFRLEdBQUcsS0FBSyxDQUFDLFFBQVEsQ0FBQztRQUUvQixJQUFJLEtBQUssR0FBVSxFQUFFLENBQUM7UUFFdEIsSUFBSSxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDakIsSUFBSSxNQUFNLEdBQVUsRUFBRSxDQUFDO1lBQ3ZCLElBQUksZUFBZSxHQUFhLEVBQUUsQ0FBQztZQUNuQyxLQUFLLE1BQU0sS0FBSyxJQUFJLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDakMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDO2dCQUU5QixJQUFJLEtBQUssQ0FBQyxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUM7b0JBQ3ZCLE1BQU0sQ0FBQyxJQUFJLENBQUM7d0JBQ1YsTUFBTSxFQUFFLEtBQUssQ0FBQyxLQUFLLENBQUMsV0FBVzt3QkFDL0IsV0FBVyxFQUFFLEtBQUssQ0FBQyxJQUFJO3FCQUN4QixDQUFDLENBQUM7Z0JBQ0wsQ0FBQztxQkFBTSxJQUFJLEtBQUssQ0FBQyxLQUFLLENBQUMsWUFBWSxFQUFFLENBQUM7b0JBQ3BDLE1BQU0sQ0FBQyxJQUFJLENBQUM7d0JBQ1YsTUFBTSxFQUFFLEtBQUssQ0FBQyxLQUFLLENBQUMsV0FBVzt3QkFDL0IsV0FBVyxFQUFFLEdBQUcsS0FBSyxDQUFDLElBQUksTUFBTTtxQkFDakMsQ0FBQyxDQUFDO29CQUNILElBQUksS0FBSyxDQUFDLFFBQVEsS0FBSyxTQUFTLEVBQUUsQ0FBQzt3QkFDakMsZUFBZSxDQUFDLElBQUksQ0FBQyxtQkFBbUIsS0FBSyxDQUFDLElBQUksMkJBQTJCLEtBQUssQ0FBQyxJQUFJLEdBQUcsQ0FBQyxDQUFDO3dCQUM1RixlQUFlLENBQUMsSUFBSSxDQUFDLFFBQVEsS0FBSyxDQUFDLElBQUksT0FBTyxDQUFDLENBQUM7b0JBQ2xELENBQUM7eUJBQU0sQ0FBQzt3QkFDTixlQUFlLENBQUMsSUFBSSxDQUFDLFVBQVUsS0FBSyxDQUFDLElBQUksYUFBYSxLQUFLLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQzt3QkFDckUsZUFBZSxDQUFDLElBQUksQ0FBQyxPQUFPLEtBQUssQ0FBQyxJQUFJLE9BQU8sQ0FBQyxDQUFDO29CQUNqRCxDQUFDO2dCQUNILENBQUM7cUJBQU0sQ0FBQztvQkFDTixNQUFNLElBQUksS0FBSyxDQUFDLHVCQUF1QixLQUFLLENBQUMsS0FBSyxFQUFFLENBQUMsQ0FBQztnQkFDeEQsQ0FBQztZQUNILENBQUM7WUFFRCxLQUFLLENBQUMsSUFBSSxDQUFDO2dCQUNULElBQUksRUFBRSxVQUFVO2dCQUNoQixNQUFNLEVBQUUsWUFBWTtnQkFDcEIsTUFBTTthQUNQLENBQUMsQ0FBQztZQUVILElBQUksZUFBZSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztnQkFDL0IsS0FBSyxDQUFDLElBQUksQ0FBQztvQkFDVCxJQUFJLEVBQUUsU0FBUztvQkFDZixNQUFNLEVBQUUsS0FBSyxDQUFDLFFBQVEsS0FBSyxPQUFPLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsbUJBQW1CO29CQUN4RSxNQUFNLEVBQUU7d0JBQ04sUUFBUSxFQUFFLElBQUksQ0FBQywrQkFBK0IsQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLGVBQWUsQ0FBQztxQkFDaEY7aUJBQ0YsQ0FBQyxDQUFDO1lBQ0wsQ0FBQztRQUNILENBQUM7UUFFRCxJQUFJLEtBQUssQ0FBQyxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQzlCLEtBQUssQ0FBQyxJQUFJLENBQUM7Z0JBQ1QsSUFBSSxFQUFFLEtBQUs7Z0JBQ1gsTUFBTSxFQUFFLEtBQUssQ0FBQyxRQUFRLEtBQUssT0FBTyxDQUFDLENBQUMsQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDLG1CQUFtQjtnQkFDeEUsTUFBTSxFQUFFO29CQUNOLFFBQVEsRUFBRSxJQUFJLENBQUMsK0JBQStCLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxLQUFLLENBQUMsUUFBUSxDQUFDO2lCQUMvRTthQUNGLENBQUMsQ0FBQztRQUNMLENBQUM7UUFFRCxJQUFJLEtBQUssQ0FBQyxNQUFNLElBQUksS0FBSyxFQUFFLENBQUM7WUFDMUIsS0FBSyxDQUFDLElBQUksQ0FBQztnQkFDVCxJQUFJLEVBQUUsUUFBUTtnQkFDZCxNQUFNLEVBQUUsUUFBUTtnQkFDaEIsTUFBTSxFQUFFLEVBQUU7YUFDWCxDQUFDLENBQUM7UUFDTCxDQUFDO1FBRUQsTUFBTSxJQUFJLEdBQUc7WUFDWCxJQUFJLEVBQUUsS0FBSyxDQUFDLFdBQVc7WUFDdkIsYUFBYSxFQUFFLEtBQUs7WUFDcEIsTUFBTSxFQUFFO2dCQUNOO29CQUNFLElBQUksRUFBRSxPQUFPO29CQUNiLEtBQUs7aUJBQ047YUFDRjtTQUNGLENBQUM7UUFFRixNQUFNLElBQUksR0FBRyxJQUFBLCtCQUFzQixFQUFDLElBQUksQ0FBQyxDQUFDO1FBQzFDLE1BQU0sU0FBUyxHQUFHLElBQUksOEJBQVksQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLFdBQVcsRUFBRTtZQUNqRSxJQUFJLEVBQUUsSUFBSTtZQUNWLFdBQVcsRUFBRSxLQUFLLENBQUMsV0FBVztZQUM5QixRQUFRLEVBQUUsS0FBSyxDQUFDLFFBQVE7WUFDeEIsT0FBTyxFQUFFLE9BQU87WUFDaEIsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDO1NBQzNCLENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxHQUFHLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQztJQUMvQixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNILGVBQWUsQ0FBQyxPQUF1QjtRQUNyQyxLQUFLLE1BQU0sS0FBSyxJQUFJLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztZQUNoQyxLQUFLLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQzNCLENBQUM7SUFDSCxDQUFDO0lBRUQsK0JBQStCLENBQUMsUUFBNkIsRUFBRSxRQUFrQjtRQUMvRSxJQUFJLFFBQVEsSUFBSSxTQUFTLEVBQUUsQ0FBQztZQUMxQixPQUFPO2dCQUNMLG1DQUFtQztnQkFDbkMsNENBQTRDO2dCQUM1QyxzQkFBc0I7YUFDdkIsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDckIsQ0FBQzthQUFNLENBQUM7WUFDTixPQUFPO2dCQUNMLFNBQVM7YUFDVixDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUNyQixDQUFDO0lBQ0gsQ0FBQzs7QUFqSUgsc0RBa0lDOzs7QUFFRDs7R0FFRztBQUNILE1BQWEsaUNBQWtDLFNBQVEsK0JBQXNCO0lBeUIzRSxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQStCO1FBQ3ZFLEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBYlQsb0JBQWUsR0FBNEIsRUFBRSxDQUFDO1FBZTdELElBQUksS0FBSyxFQUFFLGdCQUFnQixFQUFFLENBQUM7WUFDNUIseUJBQVcsQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsVUFBVSxDQUFDLG1GQUFtRixDQUFDLENBQUM7UUFDdkgsQ0FBQztRQUVELElBQUksQ0FBQyxFQUFFLEdBQUcsS0FBSyxFQUFFLEVBQUUsSUFBSSxjQUFFLENBQUMsWUFBWSxDQUFDO1FBQ3ZDLElBQUksQ0FBQyxZQUFZLEdBQUcsS0FBSyxFQUFFLFlBQVksSUFBSSx3QkFBWSxDQUFDLE1BQU0sQ0FBQztRQUMvRCxJQUFJLENBQUMsZUFBZSxHQUFHLEtBQUssRUFBRSxlQUFlLElBQUksc0JBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDbEUsSUFBSSxDQUFDLFlBQVksR0FBRyxLQUFLLEVBQUUsWUFBWSxJQUFJLHdCQUFhLENBQUMsU0FBUyxDQUFDO1FBQ25FLElBQUksQ0FBQyxnQkFBZ0IsR0FBRyxLQUFLLEVBQUUsZ0JBQWdCLElBQUksMkJBQWEsQ0FBQyxPQUFPLENBQUM7UUFDekUsSUFBSSxDQUFDLEdBQUcsR0FBRyxLQUFLLEVBQUUsR0FBRyxJQUFJLHFCQUFHLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDOUUsSUFBSSxDQUFDLGNBQWMsR0FBRyxLQUFLLEVBQUUsY0FBYyxJQUFJLENBQUMsSUFBSSxxQkFBRyxDQUFDLGFBQWEsQ0FBQyxJQUFJLEVBQUUsSUFBSSxFQUFFLEVBQUUsR0FBRyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQyxDQUFDLENBQUM7UUFDdEcsSUFBSSxDQUFDLGVBQWUsR0FBRyxLQUFLLEVBQUUsZUFBZSxDQUFDO1FBQzlDLElBQUksQ0FBQyxZQUFZLEdBQUcsS0FBSyxFQUFFLHNCQUFzQixFQUFFLFlBQVksSUFBSSxxQkFBRyxDQUFDLFlBQVksQ0FBQyxFQUFFLENBQUMscUJBQUcsQ0FBQyxhQUFhLENBQUMsR0FBRyxFQUFFLHFCQUFHLENBQUMsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3RJLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxLQUFLLEVBQUUsc0JBQXNCLEVBQUUsaUJBQWlCLENBQUM7UUFDMUUsSUFBSSxDQUFDLFdBQVcsR0FBRyxLQUFLLEVBQUUsc0JBQXNCLEVBQUUsV0FBVyxDQUFDO1FBQzlELElBQUksQ0FBQyxZQUFZLEdBQUcsS0FBSyxFQUFFLFlBQVksSUFBSSxJQUFJLENBQUM7UUFDaEQsSUFBSSxDQUFDLG1CQUFtQixHQUFHLEtBQUssRUFBRSxtQkFBbUIsSUFBSSxFQUFFLENBQUM7UUFFNUQsbUhBQW1IO1FBQ25ILE1BQU0sb0JBQW9CLEdBQUcsS0FBSyxFQUFFLGVBQWUsSUFBSSxJQUFBLGtDQUFzQixFQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUN2RixJQUFJLENBQUMsU0FBUyxHQUFHLE9BQU8sb0JBQW9CLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQywrQkFBa0IsQ0FBQyxVQUFVLENBQUMsb0JBQW9CLENBQUMsQ0FBQyxDQUFDLENBQUMsb0JBQW9CLENBQUM7UUFFdkksaUdBQWlHO1FBQ2pHLE1BQU0sWUFBWSxHQUFHLEtBQUssRUFBRSxPQUFPLElBQUksSUFBQSxvQkFBYyxFQUFDLElBQUksRUFBRSxJQUFJLENBQUMsRUFBRSxFQUFFLElBQUksQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUN4RixJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sWUFBWSxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsc0JBQVMsQ0FBQyxVQUFVLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLFlBQVksQ0FBQztRQUVwRyx5Q0FBeUM7UUFDekMsSUFBSSxLQUFLLEVBQUUsZUFBZSxJQUFJLE9BQU8sS0FBSyxDQUFDLGVBQWUsS0FBSyxRQUFRLEVBQUUsQ0FBQztZQUN4RSx5QkFBVyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxVQUFVLENBQzdCLGdQQUFnUCxDQUNqUCxDQUFDO1FBQ0osQ0FBQztRQUNELElBQUksS0FBSyxFQUFFLE9BQU8sSUFBSSxPQUFPLEtBQUssQ0FBQyxPQUFPLEtBQUssUUFBUSxFQUFFLENBQUM7WUFDeEQseUJBQVcsQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsVUFBVSxDQUM3QiwrS0FBK0ssQ0FDaEwsQ0FBQztRQUNKLENBQUM7UUFFRCw2QkFBNkI7UUFDN0IsSUFBSSxDQUFDLElBQUksR0FBRztZQUNWLHFCQUFxQixFQUFFLEdBQUcsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxDQUFDLFNBQVM7WUFDbkQsdUJBQXVCLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJO1NBQ3hDLENBQUM7UUFFRix3QkFBd0I7UUFDeEIsSUFBSSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsaUJBQWlCLENBQUMsSUFBSSxDQUFDLFlBQVksQ0FBQyxFQUFFLENBQUM7WUFDNUQsTUFBTSxJQUFJLEtBQUssQ0FBQyx5QkFBeUIsSUFBSSxDQUFDLFlBQVksQ0FBQyxJQUFJLDJDQUEyQyxJQUFJLENBQUMsWUFBWSxNQUFNLElBQUksQ0FBQyxZQUFZLENBQUMsWUFBWSxHQUFHLENBQUMsQ0FBQztRQUN0SyxDQUFDO1FBRUQsaUNBQWlDO1FBQ2pDLElBQUksS0FBSyxFQUFFLGVBQWUsRUFBRSxVQUFVLElBQUkscUJBQUcsQ0FBQyxVQUFVLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztZQUMxRSx5QkFBVyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxVQUFVLENBQUMsOEZBQThGO2dCQUM1SCwyREFBMkQsQ0FBQyxDQUFDO1FBQ2pFLENBQUM7UUFFRCx1Q0FBdUM7UUFDdkMsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLHFCQUFHLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUU7WUFDckMsU0FBUyxFQUFFLElBQUkscUJBQUcsQ0FBQyxnQkFBZ0IsQ0FBQyxtQkFBbUIsQ0FBQztTQUN6RCxDQUFDLENBQUM7UUFFSCxrRUFBa0U7UUFDbEUsSUFBSSxJQUFJLENBQUMsbUJBQW1CLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ3hDLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxJQUFBLHVEQUE0QyxFQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsSUFBSSxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsbUJBQW1CLENBQUMsQ0FBQztZQUN4SCxJQUFJLENBQUMsOEJBQThCLEdBQUcscUJBQUcsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLElBQUksRUFBRSxvQkFBb0IsRUFBRSxHQUFHLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxTQUFTLENBQUM7Z0JBQ2xILE9BQU8sRUFBRSxLQUFLO2dCQUNkLE1BQU0sRUFBRSxFQUFFO2dCQUNWLFFBQVEsRUFBRSxNQUFNO2dCQUNoQixZQUFZLEVBQUUsMkVBQTJFO2FBQzFGLENBQUMsQ0FBQyxDQUFDO1FBQ04sQ0FBQztJQUNILENBQUM7SUFFTyxRQUFRO1FBQ2QsSUFBSSxJQUFJLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxjQUFFLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUMzQixPQUFPLFNBQVMsQ0FBQztRQUNuQixDQUFDO1FBQ0QsSUFBSSxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxjQUFFLENBQUMsbUJBQW1CLENBQUMsRUFBRSxDQUFDO1lBQ3pDLE9BQU8sT0FBTyxDQUFDO1FBQ2pCLENBQUM7UUFDRCxNQUFNLElBQUksS0FBSyxDQUFDLE1BQU0sSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLHdDQUF3QyxDQUFDLENBQUM7SUFDOUUsQ0FBQztJQUVEOztPQUVHO0lBQ0gsZUFBZTtRQUNiLElBQUksSUFBSSxDQUFDLGdCQUFnQixFQUFFLENBQUM7WUFDMUIsT0FBTyxJQUFJLENBQUMsZ0JBQWdCLENBQUM7UUFDL0IsQ0FBQztRQUVELDRDQUE0QztRQUM1QyxNQUFNLFVBQVUsR0FBRyxJQUFJLHFCQUFHLENBQUMsVUFBVSxDQUFDLElBQUksRUFBRSxZQUFZLEVBQUU7WUFDeEQsZUFBZSxFQUFFLElBQUk7WUFDckIsa0JBQWtCLEVBQUUsdUJBQWEsQ0FBQyxPQUFPO1lBQ3pDLGFBQWEsRUFBRSwyQkFBYSxDQUFDLE9BQU87WUFDcEMsYUFBYSxFQUFFLElBQUk7U0FDcEIsQ0FBQyxDQUFDO1FBRUgsTUFBTSxJQUFJLEdBQUcsSUFBSSw4QkFBWSxDQUFDLDRCQUE0QixDQUFDLElBQUksRUFBRSxxQkFBcUIsRUFBRTtZQUN0RixJQUFJLEVBQUUsSUFBQSwrQkFBc0IsRUFBQyxJQUFJLENBQUM7WUFDbEMsaUNBQWlDO1lBQ2pDLGFBQWEsRUFBRTtnQkFDYjtvQkFDRSxNQUFNLEVBQUUsbUJBQUssQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsTUFBTTtvQkFDN0Isa0NBQWtDLEVBQUU7d0JBQ2xDLGFBQWEsRUFBRSxDQUFDLFFBQVEsQ0FBQzt3QkFDekIsZ0JBQWdCLEVBQUU7NEJBQ2hCLE9BQU8sRUFBRSxLQUFLOzRCQUNkLGNBQWMsRUFBRSxVQUFVLENBQUMsY0FBYzt5QkFDMUM7cUJBQ0Y7aUJBQ0Y7YUFDRjtZQUNELElBQUksRUFBRSxJQUFJLENBQUMsSUFBSTtTQUNoQixDQUFDLENBQUM7UUFFSCxJQUFJLGtCQUFrQixHQUFHOztnQ0FFRyxDQUFDO1FBRTdCLEtBQUssTUFBTSxDQUFDLElBQUksSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQ2hDLE1BQU0sUUFBUSxHQUFHLENBQUMsQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsRUFBRSxFQUFFLElBQUksQ0FBQyxZQUFZLENBQUMsQ0FBQztZQUNqRSxJQUFJLFFBQVEsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7Z0JBQ3hCLGtCQUFrQixJQUFJLElBQUksR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLElBQUksQ0FBQztZQUMxRCxDQUFDO1FBQ0gsQ0FBQztRQUVELE1BQU0sTUFBTSxHQUFHLElBQUksMkJBQWUsQ0FBQyxJQUFJLEVBQUUsa0JBQWtCLEVBQUU7WUFDM0QsUUFBUSxFQUFFLElBQUksQ0FBQyxRQUFRLEVBQUU7WUFDekIsVUFBVSxFQUFFLElBQUksQ0FBQyxjQUFjLEVBQUU7WUFDakMsZ0JBQWdCLEVBQUUsVUFBVTtZQUM1QixrQkFBa0IsRUFBRSxrQkFBa0I7WUFDdEMsV0FBVyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsS0FBSztZQUNqQyxJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUk7U0FDaEIsQ0FBQyxDQUFDO1FBRUgsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxZQUFZLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3RELE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxvQkFBb0IsQ0FBQztZQUN0QyxxQkFBRyxDQUFDLGFBQWEsQ0FBQyx3QkFBd0IsQ0FBQyw4QkFBOEIsQ0FBQztZQUMxRSxxQkFBRyxDQUFDLGFBQWEsQ0FBQyx3QkFBd0IsQ0FBQyxxREFBcUQsQ0FBQztTQUNsRyxDQUFDLENBQUM7UUFFSCxJQUFJLElBQUksQ0FBQyxZQUFZLEVBQUUsQ0FBQztZQUN0QixJQUFJLENBQUMsV0FBVyxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLFNBQVMsRUFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDNUQsQ0FBQztRQUNELElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxNQUFNLEVBQUUsVUFBVSxDQUFDLENBQUM7UUFFNUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxTQUFTLEVBQUUsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBRTdELElBQUksQ0FBQyxnQkFBZ0IsR0FBRztZQUN0QixlQUFlLEVBQUUsVUFBVTtZQUMzQixRQUFRLEVBQUUsUUFBUTtZQUNsQixFQUFFLEVBQUUsSUFBSSxDQUFDLEVBQUU7WUFDWCxZQUFZLEVBQUUsSUFBSSxDQUFDLFlBQVk7WUFDL0IsUUFBUSxFQUFFLEdBQUc7WUFDYixhQUFhLEVBQUUseUJBQWEsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDO1lBQ2hELG9IQUFvSDtTQUNySCxDQUFDO1FBRUYsT0FBTyxJQUFJLENBQUMsZ0JBQWdCLENBQUM7SUFDL0IsQ0FBQztJQUVPLGtCQUFrQixDQUFDLE1BQXVCLEVBQUUsVUFBMkI7UUFDN0UsNEVBQTRFO1FBQzVFLG1GQUFtRjtRQUNuRiw0RkFBNEY7UUFDNUYseUZBQXlGO1FBQ3pGLE1BQU0sVUFBVSxHQUFHLElBQUEsdUJBQWUsRUFBQyx5Q0FBa0IsRUFBRSxJQUFJLEVBQUUsYUFBYSxFQUFFO1lBQzFFLFdBQVcsRUFBRSx3RUFBd0U7WUFDckYsT0FBTyxFQUFFLEdBQUcsQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUNoQyxRQUFRLEVBQUUsSUFBQSx5QkFBaUIsRUFBQyxJQUFJLEVBQUUsd0JBQWdCLENBQUMsa0JBQWtCLENBQUM7WUFDdEUsYUFBYSxFQUFFLHdCQUFNLENBQUMsYUFBYSxDQUFDLElBQUk7U0FDekMsQ0FBQyxDQUFDO1FBQ0gsVUFBVSxDQUFDLGVBQWUsQ0FBQyxJQUFJLHFCQUFHLENBQUMsZUFBZSxDQUFDO1lBQ2pELE1BQU0sRUFBRSxxQkFBRyxDQUFDLE1BQU0sQ0FBQyxJQUFJO1lBQ3ZCLE9BQU8sRUFBRSxDQUFDLDBCQUEwQixDQUFDO1lBQ3JDLFNBQVMsRUFBRSxDQUFDLEdBQUcsQ0FBQztTQUNqQixDQUFDLENBQUMsQ0FBQztRQUVKLHFEQUFxRDtRQUNyRCxJQUFJLENBQUMsWUFBWSxDQUFDLFdBQVcsRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLFdBQVcsRUFBRSxFQUFFLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUUxRSxnREFBZ0Q7UUFDaEQsSUFBSSw4QkFBWSxDQUFDLGtCQUFrQixDQUFDLElBQUksRUFBRSx5QkFBeUIsRUFBRTtZQUNuRSxJQUFJLEVBQUUsSUFBQSwrQkFBc0IsRUFBQyxJQUFJLENBQUM7WUFDbEMsV0FBVyxFQUFFLDhDQUE4QyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRTtZQUMzRSxhQUFhLEVBQUUsSUFBSSxxQkFBRyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsOEJBQThCLEVBQUU7Z0JBQ2hFLFNBQVMsRUFBRSxJQUFJLHFCQUFHL