diff --git a/docs/providers-guide.md b/docs/providers-guide.md index 3031dbb59..4f689d1e4 100644 --- a/docs/providers-guide.md +++ b/docs/providers-guide.md @@ -197,12 +197,25 @@ Provider type: `codebuild`. > If you wish to pass in a custom inline Buildspec as a string for the > CodeBuild Project this would override any `buildspec.yml` file. > Read more [here](https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec-ref-example). + > + > Note: Either specify the `spec_inline` or the `spec_filename` in the + > properties block. If both are supplied, the pipeline generator will throw + > an error instead. - *spec_filename* *(String)* default: `buildspec.yml`. > If you wish to pass in a custom Buildspec file that is within the > repository. This is useful for custom deploy type actions where CodeBuild > will perform the execution of the commands. Path is relational to the > root of the repository, so `build/buidlspec.yml` refers to the > `buildspec.yml` stored in the `build` directory of the repository. + > + > In case CodeBuild is used as a deployment provider, the default BuildSpec + > file name is `deployspec.yml` instead. In case you would like to test + > a given environment using CodeBuild, you can rename it to `testspec.yml` + > or something similar using this property. + > + > Note: Either specify the `spec_inline` or the `spec_filename` in the + > properties block. If both are supplied, the pipeline generator will throw + > an error instead. ### Jenkins diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_constructs/adf_codebuild.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_constructs/adf_codebuild.py index b7dfce9ce..b9a42195a 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_constructs/adf_codebuild.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_constructs/adf_codebuild.py @@ -19,6 +19,8 @@ ADF_DEPLOYMENT_REGION = os.environ["AWS_REGION"] ADF_DEPLOYMENT_ACCOUNT_ID = os.environ["ACCOUNT_ID"] DEFAULT_CODEBUILD_IMAGE = "UBUNTU_14_04_PYTHON_3_7_1" +DEFAULT_BUILD_SPEC_FILENAME = 'buildspec.yml' +DEFAULT_DEPLOY_SPEC_FILENAME = 'deployspec.yml' class CodeBuild(core.Construct): # pylint: disable=no-value-for-parameter @@ -45,10 +47,10 @@ def __init__(self, scope: core.Construct, id: str, shared_modules_bucket: str, d environment_variables=CodeBuild.generate_build_env_variables(_codebuild, shared_modules_bucket, map_params, target), privileged=target.get('properties', {}).get('privileged', False) or map_params['default_providers']['build'].get('properties', {}).get('privileged', False) ) - _spec_filename = ( - target.get('properties', {}).get('spec_filename') or - map_params['default_providers']['deploy'].get('properties', {}).get('spec_filename') or - 'deployspec.yml' + build_spec = CodeBuild.determine_build_spec( + id, + map_params['default_providers']['deploy'].get('properties', {}), + target, ) _codebuild.PipelineProject( self, @@ -59,7 +61,7 @@ def __init__(self, scope: core.Construct, id: str, shared_modules_bucket: str, d project_name="adf-deploy-{0}".format(id), timeout=core.Duration.minutes(_timeout), role=_iam.Role.from_role_arn(self, 'build_role', role_arn=_build_role, mutable=False), - build_spec=_codebuild.BuildSpec.from_source_filename(_spec_filename) + build_spec=build_spec, ) self.deploy = Action( name="{0}".format(id), @@ -85,18 +87,10 @@ def __init__(self, scope: core.Construct, id: str, shared_modules_bucket: str, d ) if map_params['default_providers']['build'].get('properties', {}).get('role'): ADF_DEFAULT_BUILD_ROLE = 'arn:aws:iam::{0}:role/{1}'.format(ADF_DEPLOYMENT_ACCOUNT_ID, map_params['default_providers']['build'].get('properties', {}).get('role')) - _build_stage_spec = map_params['default_providers']['build'].get('properties', {}).get('spec_filename') - _build_inline_spec = map_params['default_providers']['build'].get( - 'properties', {}).get( - 'spec_inline', '') or map_params['default_providers']['build'].get( - 'properties', {}).get( - 'spec_inline', '') - if _build_stage_spec: - _spec = _codebuild.BuildSpec.from_source_filename(_build_stage_spec) - elif _build_inline_spec: - _spec = _codebuild.BuildSpec.from_object(_build_inline_spec) - else: - _spec = None + build_spec = CodeBuild.determine_build_spec( + id, + map_params['default_providers']['build'].get('properties', {}) + ) _codebuild.PipelineProject( self, 'project', @@ -105,7 +99,7 @@ def __init__(self, scope: core.Construct, id: str, shared_modules_bucket: str, d description="ADF CodeBuild Project for {0}".format(map_params['name']), project_name="adf-build-{0}".format(map_params['name']), timeout=core.Duration.minutes(_timeout), - build_spec=_spec, + build_spec=build_spec, role=_iam.Role.from_role_arn(self, 'default_build_role', role_arn=_build_role, mutable=False) ) self.build = _codepipeline.CfnPipeline.StageDeclarationProperty( @@ -122,6 +116,49 @@ def __init__(self, scope: core.Construct, id: str, shared_modules_bucket: str, d ] ) + @staticmethod + def _determine_stage_build_spec(codebuild_id, props, stage_name, default_filename): + filename = props.get('spec_filename') + spec_inline = props.get('spec_inline', {}) + if filename and spec_inline: + raise Exception( + "The spec_filename and spec_inline are both present " + "inside the {0} stage definition of {1}. " + "Whereas only one of these two is allowed.".format( + stage_name, + codebuild_id, + ), + ) + + if spec_inline: + return _codebuild.BuildSpec.from_object(spec_inline) + + return _codebuild.BuildSpec.from_source_filename( + filename or default_filename, + ) + + @staticmethod + def determine_build_spec(codebuild_id, default_props, target=None): + if target: + target_props = target.get('properties', {}) + if 'spec_inline' in target_props or 'spec_filename' in target_props: + return CodeBuild._determine_stage_build_spec( + codebuild_id=codebuild_id, + props=target_props, + stage_name='deploy target', + default_filename=DEFAULT_DEPLOY_SPEC_FILENAME, + ) + return CodeBuild._determine_stage_build_spec( + codebuild_id=codebuild_id, + props=default_props, + stage_name='default {}'.format('deploy' if target else 'build'), + default_filename=( + DEFAULT_DEPLOY_SPEC_FILENAME + if target + else DEFAULT_BUILD_SPEC_FILENAME + ), + ) + @staticmethod def determine_build_image(scope, target, map_params): specific_image = None diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_constructs/tests/test_adf_codebuild_buildspec.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_constructs/tests/test_adf_codebuild_buildspec.py new file mode 100644 index 000000000..dff272865 --- /dev/null +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/cdk/cdk_constructs/tests/test_adf_codebuild_buildspec.py @@ -0,0 +1,293 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +# pylint: skip-file + +import pytest +from mock import patch +from cdk_constructs.adf_codebuild import CodeBuild, DEFAULT_BUILD_SPEC_FILENAME, DEFAULT_DEPLOY_SPEC_FILENAME + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_file_and_inline_specified_no_target(buildspec_mock): + codebuild_id = 'some-id' + spec_inline = { + 'Some-Object': 'Some-Value', + } + spec_filename = 'some-file-name.yml' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + correct_error_message = ( + "The spec_filename and spec_inline are both present " + "inside the default build stage definition of {0}. " + "Whereas only one of these two is allowed.".format(codebuild_id) + ) + + with pytest.raises(Exception) as excinfo: + CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={ + 'spec_filename': spec_filename, + 'spec_inline': spec_inline, + }, + ) + + + error_message = str(excinfo.value) + assert error_message.find(correct_error_message) >= 0 + + buildspec_mock.from_object.assert_not_called() + buildspec_mock.from_source_filename.assert_not_called() + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_file_and_inline_specified_in_target(buildspec_mock): + codebuild_id = 'some-id' + spec_inline = { + 'Some-Object': 'Some-Value', + } + spec_filename = 'some-file-name.yml' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + correct_error_message = ( + "The spec_filename and spec_inline are both present " + "inside the deploy target stage definition of {0}. " + "Whereas only one of these two is allowed.".format(codebuild_id) + ) + + with pytest.raises(Exception) as excinfo: + CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={}, + target={ + 'properties': { + 'spec_filename': spec_filename, + 'spec_inline': spec_inline, + }, + }, + ) + + + error_message = str(excinfo.value) + assert error_message.find(correct_error_message) >= 0 + + buildspec_mock.from_object.assert_not_called() + buildspec_mock.from_source_filename.assert_not_called() + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_file_and_inline_specified_in_deploy(buildspec_mock): + codebuild_id = 'some-id' + spec_inline = { + 'Some-Object': 'Some-Value', + } + spec_filename = 'some-file-name.yml' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + correct_error_message = ( + "The spec_filename and spec_inline are both present " + "inside the default deploy stage definition of {0}. " + "Whereas only one of these two is allowed.".format(codebuild_id) + ) + + with pytest.raises(Exception) as excinfo: + CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={ + 'spec_filename': spec_filename, + 'spec_inline': spec_inline, + }, + target={ + 'properties': {}, + } + ) + + + error_message = str(excinfo.value) + assert error_message.find(correct_error_message) >= 0 + + buildspec_mock.from_object.assert_not_called() + buildspec_mock.from_source_filename.assert_not_called() + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_inline_specified_no_target(buildspec_mock): + codebuild_id = 'some-id' + spec_inline = { + 'Some-Object': 'Some-Value', + } + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + + return_value = CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={ + 'spec_inline': spec_inline, + }, + ) + + assert return_value == buildspec_mock.from_object.return_value + buildspec_mock.from_object.assert_called_once_with(spec_inline) + buildspec_mock.from_source_filename.assert_not_called() + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_inline_specified_in_target(buildspec_mock): + codebuild_id = 'some-id' + spec_inline = { + 'Some-Object': 'Some-Value', + } + spec_filename = 'some-file-name.yml' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + + return_value = CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={ + 'spec_filename': spec_filename, + }, + target={ + 'properties': { + 'spec_inline': spec_inline, + }, + }, + ) + + assert return_value == buildspec_mock.from_object.return_value + buildspec_mock.from_object.assert_called_once_with(spec_inline) + buildspec_mock.from_source_filename.assert_not_called() + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_inline_specified_in_deploy(buildspec_mock): + codebuild_id = 'some-id' + spec_inline = { + 'Some-Object': 'Some-Value', + } + spec_filename = 'some-file-name.yml' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + + return_value = CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={ + 'spec_inline': spec_inline, + }, + target={ + 'properties': {}, + }, + ) + + assert return_value == buildspec_mock.from_object.return_value + buildspec_mock.from_object.assert_called_once_with(spec_inline) + buildspec_mock.from_source_filename.assert_not_called() + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_filename_specified_no_target(buildspec_mock): + codebuild_id = 'some-id' + spec_filename = 'some-file-name.yml' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + + return_value = CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={ + 'spec_filename': spec_filename, + }, + ) + + assert return_value == buildspec_mock.from_source_filename.return_value + buildspec_mock.from_object.assert_not_called() + buildspec_mock.from_source_filename.assert_called_once_with(spec_filename) + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_filename_specified_in_target(buildspec_mock): + codebuild_id = 'some-id' + spec_inline = { + 'Some-Object': 'Some-Value', + } + spec_filename = 'some-file-name.yml' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + + return_value = CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={ + 'spec_inline': spec_inline, + }, + target={ + 'properties': { + 'spec_filename': spec_filename, + }, + }, + ) + + assert return_value == buildspec_mock.from_source_filename.return_value + buildspec_mock.from_object.assert_not_called() + buildspec_mock.from_source_filename.assert_called_once_with(spec_filename) + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_filename_specified_in_deploy(buildspec_mock): + codebuild_id = 'some-id' + spec_inline = { + 'Some-Object': 'Some-Value', + } + spec_filename = 'some-file-name.yml' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + + return_value = CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={ + 'spec_filename': spec_filename, + }, + target={ + 'properties': {}, + }, + ) + + assert return_value == buildspec_mock.from_source_filename.return_value + buildspec_mock.from_object.assert_not_called() + buildspec_mock.from_source_filename.assert_called_once_with(spec_filename) + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_no_spec_no_target(buildspec_mock): + codebuild_id = 'some-id' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + + return_value = CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={}, + ) + + assert return_value == buildspec_mock.from_source_filename.return_value + buildspec_mock.from_object.assert_not_called() + buildspec_mock.from_source_filename.assert_called_once_with( + DEFAULT_BUILD_SPEC_FILENAME, + ) + + +@patch('cdk_constructs.adf_codebuild._codebuild.BuildSpec') +def test_determine_build_spec_with_no_spec_in_target_and_deploy(buildspec_mock): + codebuild_id = 'some-id' + buildspec_mock.from_object.return_value = 'From-Object' + buildspec_mock.from_source_filename.return_value = 'From-Source' + + return_value = CodeBuild.determine_build_spec( + codebuild_id=codebuild_id, + default_props={}, + target={ + 'properties': {}, + }, + ) + + assert return_value == buildspec_mock.from_source_filename.return_value + buildspec_mock.from_object.assert_not_called() + buildspec_mock.from_source_filename.assert_called_once_with( + DEFAULT_DEPLOY_SPEC_FILENAME, + ) diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/schema_validation.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/schema_validation.py index 3a7b4b3de..60b2d0be6 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/schema_validation.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/shared/schema_validation.py @@ -92,7 +92,7 @@ Optional("role"): str, Optional("timeout"): int, Optional("privileged"): bool, - Optional("spec_inline"): str + Optional("spec_inline"): object, } DEFAULT_CODEBUILD_BUILD = { Optional("provider"): 'codebuild',