hybrid-nodes-cdk/lib/nodeadm-stack.ts (456 lines of code) (raw):

import codebuild = require('aws-cdk-lib/aws-codebuild'); import cdk = require('aws-cdk-lib'); import secretsmanager = require('aws-cdk-lib/aws-secretsmanager'); import codepipeline = require('aws-cdk-lib/aws-codepipeline'); import s3 = require('aws-cdk-lib/aws-s3'); import iam = require('aws-cdk-lib/aws-iam'); import codepipeline_actions = require('aws-cdk-lib/aws-codepipeline-actions'); import * as fs from 'fs'; import * as constants from './constants'; import { createNodeadmTestsCreationCleanupPolicy } from './nodeadm/policies'; import { createNodeadmE2EPipeline, createTestAction } from './nodeadm/e2e'; export class NodeadmBuildStack extends cdk.Stack { devStackConfig: any; githubProject: GitHubProject; cleanupAction: codepipeline_actions.CodeBuildAction | undefined; ecrCacheAction: codepipeline_actions.CodeBuildAction | undefined; githubSourceAction: codepipeline_actions.GitHubSourceAction | undefined; githubSourceOutput: codepipeline.Artifact | undefined; githubTokenSecret: secretsmanager.ISecret | undefined; goproxySecret: secretsmanager.Secret | undefined; integrationTestProject: codebuild.PipelineProject | undefined; nodeadmBinaryBucket: s3.Bucket | undefined; nodeadmBuildAction: codepipeline_actions.CodeBuildAction | undefined; nodeadmBuildOutput: codepipeline.Artifact | undefined; nodeadmLogsBucket: s3.Bucket | undefined; nodeadmVersionVariable: codepipeline.Variable | undefined; testsCleanupProject: codebuild.PipelineProject | undefined; testCreationCleanupPolicy: iam.Policy | undefined; constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); this.devStackConfig = JSON.parse( fs.readFileSync('cdk_dev_env.json', 'utf-8') ); for (const envVar of constants.requiredEnvVars) { if (process.env[envVar] === undefined) { throw new Error(`Required environment variable '${envVar}' not set`); } } this.githubProject = { Owner: this.devStackConfig.github_username, Repo: constants.githubRepo, Branch: constants.githubBranch, }; this.createSecrets(); this.createBinaryBucket(); this.createLogsBucket(); this.createCreationCleanupPolicy(); this.createSourceAction(); this.createNodeadmBuild(this.goproxySecret!.secretArn, constants.eksReleaseManifestHost); this.createECRCacheBuild(); this.createCleanupBuild(); this.createIntegrationTestBuild(); this.createE2EPipeline(); this.createConformancePipelime(); } createSecrets() { let goproxy = 'direct'; if (process.env['GOPROXY'] !== undefined && process.env['GOPROXY'] !== '') { goproxy = process.env['GOPROXY']! } else { console.warn(`GOPROXY env var not set or is empty. Defaulting to '${goproxy}'`); } this.githubTokenSecret = new secretsmanager.Secret(this, 'NodeadmE2ETestsGitHubToken', { secretName: 'nodeadm-e2e-tests-github-token', description: 'Personal Access Token for authenticating to GitHub', secretObjectValue: { 'github-token': cdk.SecretValue.unsafePlainText(process.env.HYBRID_GITHUB_TOKEN!), } }); this.goproxySecret = new secretsmanager.Secret(this, 'NodeadmE2ETestsGoproxy', { secretName: 'nodeadm-e2e-tests-goproxy', description: 'Go module proxy endpoint or mode', secretObjectValue: { endpoint: cdk.SecretValue.unsafePlainText(goproxy), } }); let rhelUsername = ''; let rhelPassword = ''; if (process.env['RHEL_USERNAME'] !== undefined && process.env['RHEL_USERNAME'] !== '') { rhelUsername = process.env['RHEL_USERNAME']! } else { console.warn(`'RHEL_USERNAME' env var not set or is empty. This will cause Red Hat credentials secret creation to get skipped, which could cause RHEL tests to fail'`); } if (process.env['RHEL_PASSWORD'] !== undefined && process.env['RHEL_PASSWORD'] !== '') { rhelPassword = process.env['RHEL_PASSWORD']! } else { console.warn(`'RHEL_PASSWORD' env var not set or is empty. This will cause Red Hat credentials secret creation to get skipped, which could cause RHEL tests to fail'`); } if (rhelUsername !== '' && rhelPassword !== '') { const redhatCredentialsSecret = new secretsmanager.Secret(this, 'NodeadmE2ERedHatCredentials', { secretName: 'nodeadm-e2e-tests-redhat-credentials', description: 'Username and password for authenticating with Red Hat Subscription Manager ', secretObjectValue: { 'username': cdk.SecretValue.unsafePlainText(rhelUsername), 'password': cdk.SecretValue.unsafePlainText(rhelPassword), }, }); } else { console.warn(`Red Hat credentials secret creation has been skipped due to empty username and/or password environment variables'`); } } createBinaryBucket() { this.nodeadmBinaryBucket = new s3.Bucket(this, `nodeadm-binaries-${this.account}`, { bucketName: `nodeadm-binaries-${this.account}`, enforceSSL: true, versioned: true, encryption: s3.BucketEncryption.S3_MANAGED, }); this.addStandardLifecycleRules(this.nodeadmBinaryBucket); } createLogsBucket() { this.nodeadmLogsBucket = new s3.Bucket(this, `nodeadm-logs-${this.account}`, { bucketName: `nodeadm-logs-${this.account}`, enforceSSL: true, versioned: true, encryption: s3.BucketEncryption.S3_MANAGED, }); this.addStandardLifecycleRules(this.nodeadmLogsBucket); } createCreationCleanupPolicy() { if (this.nodeadmBinaryBucket === undefined) { throw new Error('Nodeadm binary bucket is not defined'); } this.testCreationCleanupPolicy = createNodeadmTestsCreationCleanupPolicy( this, constants.testClusterTagKey, constants.testClusterPrefix, this.nodeadmBinaryBucket.bucketArn, constants.podIdentityS3BucketPrefix, ); } createGitHubSourceAction(trigger: codepipeline_actions.GitHubTrigger) { if (this.githubTokenSecret === undefined) { throw new Error('`githubTokenSecret` is not defined'); } if (this.githubSourceOutput === undefined) { throw new Error('`githubSourceOutput` is not defined'); } return new codepipeline_actions.GitHubSourceAction({ actionName: 'GitHubSource', owner: this.githubProject.Owner, repo: this.githubProject.Repo, branch: this.githubProject.Branch, oauthToken: this.githubTokenSecret.secretValueFromJson('github-token'), output: this.githubSourceOutput, trigger: trigger, }); } createSourceAction() { this.githubSourceOutput = new codepipeline.Artifact(); this.githubSourceAction = this.createGitHubSourceAction(codepipeline_actions.GitHubTrigger.NONE); } createNodeadmBuild(goproxySecretArn: string, eksReleaseManifestHost: string) { if (this.nodeadmBinaryBucket === undefined) { throw new Error('`nodeadmBinaryBucket` is not defined'); } if (this.githubSourceOutput === undefined) { throw new Error('`githubSourceOutput` is not defined'); } const codeBuildProject = new codebuild.PipelineProject(this, 'nodeadm-build', { projectName: 'nodeadm-build', buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspecs/build-nodeadm.yml'), environmentVariables: { GOPROXY: { type: codebuild.BuildEnvironmentVariableType.SECRETS_MANAGER, value: `${goproxySecretArn}:endpoint`, }, ARTIFACTS_BUCKET: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: this.nodeadmBinaryBucket.bucketName, }, MANIFEST_HOST: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: eksReleaseManifestHost, }, }, environment: { buildImage: codebuild.LinuxBuildImage.fromDockerRegistry(constants.builderBaseImage), computeType: codebuild.ComputeType.LARGE, }, }); codeBuildProject.role!.addToPrincipalPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:PutObject*', 's3:ListBucket'], resources: [this.nodeadmBinaryBucket.bucketArn, `${this.nodeadmBinaryBucket.bucketArn}/*`], }), ); this.nodeadmVersionVariable = new codepipeline.Variable({ variableName: 'nodeadmVersion', description: 'semantic version for nodeadm', defaultValue: 'v1.0.4-dev', }); this.nodeadmBuildOutput = new codepipeline.Artifact(); this.nodeadmBuildAction = new codepipeline_actions.CodeBuildAction({ actionName: 'Build', input: this.githubSourceOutput, outputs: [this.nodeadmBuildOutput], project: codeBuildProject, environmentVariables: { GIT_VERSION: { value: '#{variables.nodeadmVersion}', }, }, }); } createECRCacheBuild() { if (this.nodeadmBuildOutput === undefined) { throw new Error('`nodeadmBuildOutput` is not defined'); } const testsECRCacheProject = new codebuild.PipelineProject(this, 'ecr-cache', { projectName: 'ecr-cache', buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspecs/ecr-cache.yml'), environment: { buildImage: codebuild.LinuxBuildImage.fromDockerRegistry(constants.builderBaseImage), computeType: codebuild.ComputeType.SMALL, }, }); testsECRCacheProject.role!.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryPullOnly'), ); this.ecrCacheAction = new codepipeline_actions.CodeBuildAction({ actionName: 'ECR-Cache', input: this.nodeadmBuildOutput, project: testsECRCacheProject, }); } createCleanupBuild() { if (this.nodeadmBuildOutput === undefined) { throw new Error('`nodeadmBuildOutput` is not defined'); } if (this.testCreationCleanupPolicy === undefined) { throw new Error('`testCreationCleanupPolicy` is not defined'); } this.testsCleanupProject = new codebuild.PipelineProject(this, 'nodeadm-cleanup', { projectName: 'nodeadm-cleanup', buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspecs/cleanup-nodeadm.yml'), environment: { buildImage: codebuild.LinuxBuildImage.fromDockerRegistry(constants.builderBaseImage), computeType: codebuild.ComputeType.SMALL, }, }); this.cleanupAction = new codepipeline_actions.CodeBuildAction({ actionName: 'Cleanup', input: this.nodeadmBuildOutput, project: this.testsCleanupProject, }); this.testsCleanupProject.role!.attachInlinePolicy(this.testCreationCleanupPolicy); } vpcParams() { return { CLUSTER_VPC_CIDR: { value: constants.clusterVpcCidr, }, CLUSTER_PUBLIC_SUBNET_CIDR: { value: constants.clusterPublicSubnetCidr, }, CLUSTER_PRIVATE_SUBNET_CIDR: { value: constants.clusterPrivateSubnetCidr, }, HYBRID_VPC_CIDR: { value: constants.hybridVpcCidr, }, HYBRID_PUBLIC_SUBNET_CIDR: { value: constants.hybridPublicSubnetCidr, }, HYBRID_PRIVATE_SUBNET_CIDR: { value: constants.hybridPrivateSubnetCidr, }, HYBRID_POD_CIDR: { value: constants.hybridPodCidr, }, } } createIntegrationTestBuild() { if (this.nodeadmBinaryBucket === undefined) { throw new Error('`nodeadmBinaryBucket` is not defined'); } if (this.goproxySecret === undefined) { throw new Error('`goproxySecret` is not defined'); } if (this.nodeadmLogsBucket === undefined) { throw new Error('`nodeadmLogsBucket` is not defined'); } if (this.testCreationCleanupPolicy === undefined) { throw new Error('`testCreationCleanupPolicy` is not defined'); } this.integrationTestProject = new codebuild.PipelineProject(this, 'nodeadm-e2e-tests-project', { projectName: 'nodeadm-e2e-tests', buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspecs/test-nodeadm.yml'), environment: { buildImage: codebuild.LinuxBuildImage.fromDockerRegistry(constants.builderBaseImage), environmentVariables: { AWS_REGION: { value: this.region, }, ARTIFACTS_BUCKET: { value: this.nodeadmBinaryBucket.bucketName, }, LOGS_BUCKET: { value: this.nodeadmLogsBucket.bucketName, }, GOPROXY: { type: codebuild.BuildEnvironmentVariableType.SECRETS_MANAGER, value: `${this.goproxySecret.secretArn}:endpoint`, }, ...this.vpcParams(), }, }, }); this.integrationTestProject.role!.addToPrincipalPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:PutObject*'], resources: [this.nodeadmLogsBucket.bucketArn, `${this.nodeadmLogsBucket.bucketArn}/*`], }), ); this.integrationTestProject.role!.addToPrincipalPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:PutObject*', 's3:ListBucket'], resources: [this.nodeadmBinaryBucket.bucketArn, `${this.nodeadmBinaryBucket.bucketArn}/*`], }), ); this.integrationTestProject.role!.attachInlinePolicy(this.testCreationCleanupPolicy); } createE2EPipeline() { if (this.nodeadmBuildOutput === undefined) { throw new Error('`nodeadmBuildOutput` is not defined'); } if (this.nodeadmBuildAction === undefined) { throw new Error('`nodeadmBuildAction` is not defined'); } if (this.ecrCacheAction === undefined) { throw new Error('`ecrCacheAction` is not defined'); } if (this.cleanupAction === undefined) { throw new Error('`cleanupAction` is not defined'); } if (this.githubSourceAction === undefined) { throw new Error('`githubSourceAction` is not defined'); } if (this.integrationTestProject === undefined) { throw new Error('`integrationTestProject` is not defined'); } if (this.nodeadmVersionVariable === undefined) { throw new Error('`nodeadmVersionVariable` is not defined'); } const e2eTestsActions: Array<codepipeline_actions.CodeBuildAction> = []; for (const kubeVersion of constants.kubernetesVersions) { for (const cni of constants.cnis) { let additionalEnvironmentVariables = {}; if (constants.betaKubeVersions.includes(kubeVersion)) { additionalEnvironmentVariables = this.betaEnvironmentVariables(); } const e2eTestsAction = createTestAction( kubeVersion, cni, this.nodeadmBuildOutput, this.integrationTestProject, additionalEnvironmentVariables, ); e2eTestsActions.push(e2eTestsAction); } } // Create the CodePipeline with the private GitHub source const e2ePipeline = createNodeadmE2EPipeline( this, 'e2e-tests', this.githubSourceAction, this.nodeadmBuildAction, this.cleanupAction, this.ecrCacheAction, e2eTestsActions, [this.nodeadmVersionVariable], ); this.addStandardLifecycleRules(e2ePipeline.artifactBucket as s3.Bucket); } createConformancePipelime() { if (this.nodeadmBuildOutput === undefined) { throw new Error('`nodeadmBuildOutput` is not defined'); } if (this.nodeadmBuildAction === undefined) { throw new Error('`nodeadmBuildAction` is not defined'); } if (this.ecrCacheAction === undefined) { throw new Error('`ecrCacheAction` is not defined'); } if (this.cleanupAction === undefined) { throw new Error('`cleanupAction` is not defined'); } if (this.githubSourceAction === undefined) { throw new Error('`githubSourceAction` is not defined'); } if (this.integrationTestProject === undefined) { throw new Error('`integrationTestProject` is not defined'); } if (this.nodeadmVersionVariable === undefined) { throw new Error('`nodeadmVersionVariable` is not defined'); } const conformanceActions: Array<codepipeline_actions.CodeBuildAction> = []; for (const kubeVersion of constants.kubernetesVersions) { const cni = 'cilium'; let additionalEnvironmentVariables = { E2E_SUITE: { value: 'conformance.test', }, E2E_FILTER: { value: 'conformance', }, }; if (constants.betaKubeVersions.includes(kubeVersion)) { additionalEnvironmentVariables = { ...additionalEnvironmentVariables, ...this.betaEnvironmentVariables() }; } const e2eTestsAction = createTestAction( kubeVersion, cni, this.nodeadmBuildOutput, this.integrationTestProject, additionalEnvironmentVariables, ); conformanceActions.push(e2eTestsAction); } const conformancePipeline = createNodeadmE2EPipeline( this, 'conformance', this.githubSourceAction, this.nodeadmBuildAction, this.cleanupAction, this.ecrCacheAction, conformanceActions, [this.nodeadmVersionVariable], ); this.addStandardLifecycleRules(conformancePipeline.artifactBucket as s3.Bucket); } addStandardLifecycleRules(bucket: s3.Bucket) { bucket.addLifecycleRule({ enabled: true, expiration: cdk.Duration.days(30), noncurrentVersionExpiration: cdk.Duration.days(1), }); } betaEnvironmentVariables() { let betaEksEndpoint = process.env['BETA_EKS_ENDPOINT'] ?? ''; let betaDefaultClusterRoleSP = process.env['BETA_EKS_CLUSTER_ROLE_SP'] ?? ''; let betaDefaultPodIdentitySP = process.env['BETA_EKS_POD_IDENTITY_SP'] ?? ''; if (betaEksEndpoint === '' || betaDefaultClusterRoleSP === '' || betaDefaultPodIdentitySP === '') { throw new Error('BETA_EKS_ENDPOINT, BETA_EKS_CLUSTER_ROLE_SP, and BETA_EKS_POD_IDENTITY_SP must be set when isBeta is true'); } return { betaEksEndpoint, betaDefaultClusterRoleSP, betaDefaultPodIdentitySP }; } } export interface GitHubProject { readonly Owner: string; readonly Repo: string; readonly Branch: string; }