constructor()

in cicd/lib/cicd-stack.ts [30:194]


    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        const context = this.getContext();

        // Extract configuration from GDK configuration to prevent mismatches
        const componentName = Object.keys(gdkConfig['component'])[0];
        const bucketName = gdkConfig['component'][componentName as keyof typeof gdkConfig['component']]['publish']['bucket'];
        
        const buildProject = new codebuild.PipelineProject(this, `${Names.PREFIX_CAMEL}Build`, {
            projectName: `${Names.PREFIX_DASH}-build`,
            buildSpec: codebuild.BuildSpec.fromSourceFilename('cicd/buildspec.yaml'),
            environment: {
                buildImage: codebuild.LinuxBuildImage.STANDARD_5_0
            },
            timeout: Duration.minutes(5),
        });

        const deployProject = new codebuild.PipelineProject(this, `${Names.PREFIX_CAMEL}Deploy`, {
            projectName: `${Names.PREFIX_DASH}-deploy`,
            buildSpec: codebuild.BuildSpec.fromSourceFilename('cicd/deployspec.yaml'),
            environment: {
                buildImage: codebuild.LinuxBuildImage.STANDARD_5_0
            },
            timeout: Duration.minutes(5),
        });

        const pipelineBucket = new s3.Bucket(this, `${Names.PREFIX_CAMEL}Bucket`, {
            bucketName: `${Names.PREFIX_DASH}-${this.account}-${this.region}`,
            blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
            versioned: false,
        });

        const topic = new sns.Topic(this, `${Names.PREFIX_CAMEL}Notification`, {
            topicName: `${Names.PREFIX_DASH}-notification`,
            displayName: 'GG Home Assistant CI/CD Notification'
        });

        // We create the unit tests report group explicitly, rather than let CodeBuild do it, so that we can define the raw results export
        new codebuild.CfnReportGroup(this, `${Names.PREFIX_CAMEL}UnitTestReportGroup`, {
            type: 'TEST',
            name: `${buildProject.projectName}-UnitTestsReport`,
            exportConfig: {
                exportConfigType: 'S3',
                s3Destination: {
                    bucket: pipelineBucket.bucketName,
                    encryptionDisabled: true,
                    packaging: 'NONE'
                }
            }
        });

        // The build project needs some extra rights
        buildProject.addToRolePolicy(new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['greengrass:CreateComponentVersion','greengrass:ListComponentVersions'],
            resources: [`arn:aws:greengrass:${this.region}:${this.account}:components:${componentName}`]
        }));
        buildProject.addToRolePolicy(new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['s3:CreateBucket','s3:GetBucketLocation'],
            resources: [`arn:aws:s3:::${bucketName}-${this.region}-${this.account}`]
        }));
        buildProject.addToRolePolicy(new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['s3:PutObject','s3:GetObject'],
            resources: [`arn:aws:s3:::${bucketName}-${this.region}-${this.account}/*`]
        }));

        // The deploy project needs some extra rights
        deployProject.addToRolePolicy(new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['greengrass:GetCoreDevice'],
            resources: [`arn:aws:greengrass:${this.region}:${this.account}:coreDevices:${context.greengrassCoreName}`]
        }));
        deployProject.addToRolePolicy(new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['greengrass:ListComponentVersions'],
            resources: [`arn:aws:greengrass:${this.region}:${this.account}:components:*`]
        }));
        deployProject.addToRolePolicy(new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['greengrass:CreateDeployment'],
            resources: ['*']
        }));
        deployProject.addToRolePolicy(new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['greengrass:GetDeployment', 'greengrass:ListDeployments'],
            resources: [`arn:aws:greengrass:${this.region}:${this.account}:deployments:*`]
        }));
        deployProject.addToRolePolicy(new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['iot:*'],
            resources: ['*']
        }));

        // All projects need to be able to get the secret value
        const secretPolicy = new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['secretsmanager:GetSecretValue'],
            resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:${Names.SECRET}-*`]
        });
        buildProject.addToRolePolicy(secretPolicy);
        deployProject.addToRolePolicy(secretPolicy);

        const source = new codepipeline.Artifact('Source');
        const build = new codepipeline.Artifact('Build');
        const deploy = new codepipeline.Artifact('Deploy');

        const codeCommitRepository = codecommit.Repository.fromRepositoryName(this, `${Names.PREFIX_CAMEL}CCRepo`, context.repositoryName);

        const sourceAction = new codepipeline_actions.CodeCommitSourceAction({
            actionName: `${Names.PREFIX_DASH}-source`,
            output: source,
            repository: codeCommitRepository,
            branch: context.branchName
        });

        const buildAction = new codepipeline_actions.CodeBuildAction({
            actionName: `${Names.PREFIX_DASH}-build`,
            project: buildProject,
            input: source,
            outputs: [build]
        });

        const deployAction = new codepipeline_actions.CodeBuildAction({
            actionName: `${Names.PREFIX_DASH}-deploy`,
            project: deployProject,
            input: source,
            extraInputs: [build],
            outputs: [deploy],
            environmentVariables: {
                "GREENGRASS_CORE_NAME": { value: context.greengrassCoreName }
            }
        });

        const pipeline = new codepipeline.Pipeline(this, `${Names.PREFIX_CAMEL}Pipeline`, {
            pipelineName: `${Names.PREFIX_DASH}-pipeline`,
            artifactBucket: pipelineBucket,
            stages: [
                {
                    stageName: 'Source',
                    actions: [sourceAction],
                },
                {
                    stageName: 'Build',
                    actions: [buildAction],
                },
                {
                    stageName: 'Deploy',
                    actions: [deployAction],
                }
            ],
        });

        // Send only SUCCEEDED and FAILED states to the SNS topic, to give a pipeline execution result
        const notificationRule = pipeline.onStateChange(`${Names.PREFIX_CAMEL}StateChange`);
        notificationRule.addEventPattern({ detail: { state: ['SUCCEEDED','FAILED'] } });
        const state = events.EventField.fromPath('$.detail.state')
        const executionId = events.EventField.fromPath('$.detail.execution-id')
        const account = events.EventField.fromPath('$.account')
        notificationRule.addTarget(new events_targets.SnsTopic(topic, {
            message: events.RuleTargetInput.fromText(`Account ${account} ${state} for execution ID ${executionId}`)
        }));
    }