-
Notifications
You must be signed in to change notification settings - Fork 8
feat: Adding config validator and anlyzer cli cmd #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
dainovsv
wants to merge
3
commits into
main
Choose a base branch
from
feat/ux-fail-safes
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
packages/@cdklabs/cdk-cicd-wrapper-cli/src/cmds/AnalyzerCommand.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| /* eslint-disable no-console */ | ||
| import * as yargs from 'yargs'; | ||
| import { CodePipeline, GetPipelineCommand } from '@aws-sdk/client-codepipeline'; | ||
| import { CodeBuild, BatchGetProjectsCommand } from '@aws-sdk/client-codebuild'; | ||
| import { KMS, DescribeKeyCommand } from '@aws-sdk/client-kms'; | ||
| import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; | ||
|
|
||
| /** | ||
| * Command class that checks CodePipeline and CodeBuild configurations. | ||
| */ | ||
| class Command implements yargs.CommandModule { | ||
| /** | ||
| * The command name. | ||
| */ | ||
| command = 'analyze'; | ||
|
|
||
| /** | ||
| * A description of the command. | ||
| */ | ||
| describe = 'analyzes the environment and pipeline configuration to spot potential common issues.'; | ||
|
|
||
| /** | ||
| * Builds the command arguments. | ||
| * @param args The argument parser. | ||
| * @returns The argument parser with defined options. | ||
| */ | ||
| builder(args: yargs.Argv) { | ||
| return args | ||
| .option('profile', { | ||
| type: 'string', | ||
| requiresArg: true, | ||
| demandOption: 'AWS profile is required', | ||
| description: 'AWS profile', | ||
| }) | ||
| .option('region', { | ||
| type: 'string', | ||
| requiresArg: true, | ||
| demandOption: 'AWS region is required', | ||
| description: 'AWS region', | ||
| }) | ||
| .option('cdk-qualifier', { | ||
| type: 'string', | ||
| requiresArg: true, | ||
| demandOption: 'CDK Qualifier Required', | ||
| description: 'The qualifier used for the CDK project', | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Handles the command execution. | ||
| * @param args The parsed command arguments. | ||
| */ | ||
| async handler(args: yargs.Arguments) { | ||
| const region = args.region as string; | ||
| const profile = args.profile as string; | ||
| const cdkQualifier = args['cdk-qualifier'] as string; | ||
|
|
||
| const sdkConfig = { | ||
| region, | ||
| credentials: fromNodeProviderChain({ profile }), | ||
| }; | ||
|
|
||
| const pipelineClient = new CodePipeline(sdkConfig); | ||
| const codebuildClient = new CodeBuild(sdkConfig); | ||
| const kmsClient = new KMS(sdkConfig); | ||
|
|
||
| console.log('analyzing for common issues...') | ||
| try { | ||
| const pipelines = await pipelineClient.listPipelines({}); | ||
| for (const pipeline of pipelines.pipelines || []) { | ||
| if (pipeline.name?.includes(cdkQualifier)) { | ||
| console.log('retrieving pipeline details...') | ||
| const pipelineDetails = await pipelineClient.send( | ||
| new GetPipelineCommand({ name: pipeline.name }) | ||
| ); | ||
|
|
||
| if (!pipelineDetails.pipeline?.stages) { | ||
| console.error(`Your pipeline has no stages!`); | ||
| } else { | ||
|
|
||
| //Pipeline sanity check against .env | ||
| console.log('preforming pipeline sanity check against env vars...') | ||
| if (process.env.ACCOUNT_DEV && process.env.ACCOUNT_DEV !== '-') { | ||
| const hasDevStage = pipelineDetails.pipeline?.stages.some(stage => stage.name === 'DEV'); | ||
| if (!hasDevStage) { | ||
| console.warn(`NOTE : pipeline failed sanity check, your .env / env vars specify a DEV stage but your pipeline does not have one`); | ||
| } | ||
| } | ||
|
|
||
| if (process.env.ACCOUNT_INT && process.env.ACCOUNT_INT !== '-') { | ||
| const hasIntStage = pipelineDetails.pipeline?.stages.some(stage => stage.name === 'INT'); | ||
| if (!hasIntStage) { | ||
| console.warn(`NOTE : pipeline failed sanity check, your .env / env vars specify an INT stage but your pipeline does not have one`); | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
||
| console.log('checking codebuilds...') | ||
| for (const stage of pipelineDetails.pipeline?.stages) { | ||
| for (const action of stage.actions || []) { | ||
| // Codebuild Checks | ||
| if (action.actionTypeId?.provider === 'CodeBuild' && action.configuration?.ProjectName) { | ||
| const codebuildProjectName = action.configuration?.ProjectName; | ||
| // Check the env vars of the synth action | ||
| if (action.name === 'Synth') { | ||
| await checkEnvVarsCodeBuildProject(codebuildClient, codebuildProjectName); | ||
| } | ||
|
|
||
| // Check the CodeBuild projects to see if they are using a valid KMS Key | ||
| await checkKMSCodeBuildProject(codebuildClient, kmsClient, codebuildProjectName); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.error(err); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validates if a string is a valid ARN. | ||
| * @param arn The ARN string. | ||
| * @returns True if valid, otherwise false. | ||
| */ | ||
| function isValidArn(arn: string): boolean { | ||
| const arnRegex = /^arn:aws[a-zA-Z-]*:[a-zA-Z0-9-]+:[a-zA-Z0-9-]*:[0-9]*:[^/].*$/; | ||
| return arnRegex.test(arn); | ||
| } | ||
|
|
||
| /** | ||
| * Checks the CodeBuild project for environment variables. | ||
| * @param codebuildClient The CodeBuild client. | ||
| * @param projectName The name of the CodeBuild project. | ||
| */ | ||
| async function checkEnvVarsCodeBuildProject(codebuildClient: CodeBuild, projectName: string) { | ||
| try { | ||
| const projectDetails = await codebuildClient.send( | ||
| new BatchGetProjectsCommand({ names: [projectName] }) | ||
| ); | ||
|
|
||
| const project = projectDetails.projects?.[0]; | ||
| if (project) { | ||
| const envVars = project.environment?.environmentVariables || []; | ||
| console.log('build stage environment variables:', envVars); | ||
|
|
||
| const proxySecretArn = envVars.find(env => env.name === 'PROXY_SECRET_ARN'); | ||
| const codestarConnectionArn = envVars.find(env => env.name === 'CODESTAR_CONNECTION_ARN'); | ||
|
|
||
| console.log('proxySecretArn:', proxySecretArn); | ||
| console.log('codestarConnectionArn:', codestarConnectionArn); | ||
|
|
||
| if (!proxySecretArn || proxySecretArn.value === '' || proxySecretArn.value === '-') { | ||
| console.warn(`NOTE: you do not have a PROXY_SECRET_ARN configured in the synth stage of your pipeline. if your organization uses a network proxy, your CodeBuild may fail.`); | ||
| } else if (proxySecretArn.value && !isValidArn(proxySecretArn.value)) { | ||
| console.error(`ERROR : Invalid ARN format for PROXY_SECRET_ARN in CodeBuild project ${projectName}.`); | ||
| } | ||
|
|
||
| if (!codestarConnectionArn || codestarConnectionArn.value === '' || codestarConnectionArn.value === '-') { | ||
| console.warn(`NOTE: your CODESTAR_CONNECTION_ARN is empty in the Synth stage of your pipeline. If you are not using CodeCommit as your repository, you need to define a valid CodeStar connection ARN.`); | ||
| } else if (codestarConnectionArn.value && !isValidArn(codestarConnectionArn.value)) { | ||
| console.error(`ERROR : invalid arn format for CODESTAR_CONNECTION_ARN in codebuild project ${projectName}.`); | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.error(`ERROR : failed to retrieve details for codebuild project ${projectName}:`, err); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Checks the CodeBuild project for KMS key validity. | ||
| * @param codebuildClient The CodeBuild client. | ||
| * @param kmsClient The KMS client. | ||
| * @param projectName The name of the CodeBuild project. | ||
| */ | ||
| async function checkKMSCodeBuildProject(codebuildClient: CodeBuild, kmsClient: KMS, projectName: string) { | ||
| try { | ||
| const projectDetails = await codebuildClient.send( | ||
| new BatchGetProjectsCommand({ names: [projectName] }) | ||
| ); | ||
|
|
||
| const project = projectDetails.projects?.[0]; | ||
| if (project) { | ||
| const encryptionKey = project.encryptionKey; | ||
|
|
||
| if (encryptionKey) { | ||
| const kmsKeyDetails = await kmsClient.send( | ||
| new DescribeKeyCommand({ KeyId: encryptionKey }) | ||
| ); | ||
|
|
||
| if (kmsKeyDetails.KeyMetadata?.KeyState === 'Disabled') { | ||
| console.error(`ERROR : KMS key ${encryptionKey} for project ${projectName} is disabled.`); | ||
| } | ||
| } else { | ||
| console.warn(`NOTE : no KMS key configured for project ${projectName}.`); | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.error(`ERROR : failed to retrieve details for codebuild project ${projectName}:`, err); | ||
| } | ||
| } | ||
|
|
||
| export default new Command(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,6 +82,61 @@ const defaultConfigs = { | |
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Validates the environment variables and default configurations. | ||
| * Throws an error if any validation fails. | ||
| */ | ||
| function validateConfig(props: IPipelineBlueprintProps, stageDefinitions: AllStage<Partial<IStageDefinition>>, region: string) { | ||
| const envVars = [ | ||
| { name: 'AWS_REGION', pattern: /^[a-z]{2}-[a-z]+-\d{1}$/, errorMessage: 'Invalid AWS region format' }, | ||
| { name: 'ACCOUNT_RES', pattern: /^\d{12}$/, errorMessage: 'Invalid RES account number format' }, | ||
| { name: 'RES_ACCOUNT_AWS_PROFILE', pattern: /^[a-zA-Z0-9-_]+$/, errorMessage: 'Invalid AWS profile name format' }, | ||
| { name: 'ACCOUNT_DEV', pattern: /^\d{12}$/, errorMessage: 'Invalid DEV account number format' }, | ||
| { name: 'DEV_ACCOUNT_AWS_PROFILE', pattern: /^[a-zA-Z0-9-_]+$/, errorMessage: 'Invalid AWS profile name format' }, | ||
| { name: 'ACCOUNT_INT', pattern: /^\d{12}$/, errorMessage: 'Invalid INT account number format' }, | ||
| { name: 'INT_ACCOUNT_AWS_PROFILE', pattern: /^[a-zA-Z0-9-_]+$/, errorMessage: 'Invalid AWS profile name format' }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about other stages? |
||
| { name: 'PROXY_SECRET_ARN', pattern: /^arn:aws:[a-z-]+:[a-z0-9-]+:\d{12}:secret\/[a-zA-Z0-9-_]+$/, errorMessage: 'Invalid ARN format' }, | ||
| ]; | ||
|
|
||
| envVars.forEach((envVar) => { | ||
| const value = process.env[envVar.name]; | ||
| if (value && !envVar.pattern.test(value)) { | ||
| throw new Error(`Environment variable ${envVar.name} is invalid: ${envVar.errorMessage}`); | ||
| } | ||
| }); | ||
|
|
||
| if (!props.applicationName) { | ||
| throw new Error('Application name is required'); | ||
| } | ||
|
|
||
| if (!props.applicationQualifier) { | ||
| throw new Error('Application qualifier is required'); | ||
| } else if (!/^[a-zA-Z0-9-_]+$/.test(props.applicationQualifier)) { | ||
| throw new Error('Invalid application qualifier format'); | ||
| } | ||
|
|
||
| if (!region) { | ||
| throw new Error('AWS region is required'); | ||
| } else if (!/^[a-z]{2}-[a-z]+-\d{1}$/.test(region)) { | ||
| throw new Error('Invalid AWS region format'); | ||
| } | ||
|
|
||
| if (!stageDefinitions.RES) { | ||
| throw new Error('At least RES stage is required'); | ||
| } | ||
|
|
||
| Object.entries(stageDefinitions).forEach(([stage, providedDefinition]) => { | ||
| if (!providedDefinition.account) { | ||
| throw new Error(`Stage ${stage} does not have an associated account.`); | ||
| } else if (!/^\d{12}$/.test(providedDefinition.account)) { | ||
| throw new Error(`Invalid account number format for stage ${stage}`); | ||
| } | ||
| if (providedDefinition.region && !/^[a-z]{2}-[a-z]+-\d{1}$/.test(providedDefinition.region)) { | ||
| throw new Error(`Invalid AWS region format for stage ${stage}`); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Class for building a Pipeline Blueprint. | ||
| */ | ||
|
|
@@ -360,6 +415,10 @@ export class PipelineBlueprintBuilder { | |
| * @returns The created stack. | ||
| */ | ||
| public synth(app: cdk.App) { | ||
|
|
||
| // Validate the entire configuration | ||
| validateConfig(this.props, this.stageDefinitions, this._region!); | ||
|
|
||
| this.props.deploymentDefinition = this.generateDeploymentDefinitions(); | ||
| this.props.plugins = { ...this.props.plugins, ...this._plugins }; | ||
|
|
||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if we have another stage defined which you dont check here, what happens next?