source/idea/pipeline/stack.py (919 lines of code) (raw):
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import os
import pathlib
import typing
from typing import Optional, Union
import aws_cdk
from aws_cdk import (
CfnOutput,
Duration,
Fn,
IStackSynthesizer,
RemovalPolicy,
Stack,
Stage,
)
from aws_cdk import aws_codebuild as codebuild
from aws_cdk import aws_codecommit as codecommit
from aws_cdk import aws_ecr as ecr
from aws_cdk import aws_iam as iam
from aws_cdk import aws_lambda as lambda_
from aws_cdk import pipelines
from constructs import Construct
from idea.batteries_included.parameters.parameters import BIParameters
from idea.batteries_included.stack import BiStack
from idea.constants import (
ARTIFACTS_BUCKET_PREFIX_NAME,
BATTERIES_INCLUDED_STACK_NAME,
DEFAULT_ECR_REPOSITORY_NAME,
INSTALL_STACK_NAME,
)
from idea.infrastructure.install.parameters.parameters import RESParameters
from idea.infrastructure.install.stacks.install_stack import InstallStack
from idea.pipeline.integ_tests.integ_test_step_builder import IntegTestStepBuilder
from idea.pipeline.utils import get_commands_for_scripts
UNIT_TESTS = [
"tests.administrator",
"tests.cluster-manager",
"tests.virtual-desktop-controller",
"tests.library",
"tests.sdk",
"tests.pipeline",
"tests.infrastructure",
"tests.lambda-functions",
]
COVERAGEREPORTS = ["coverage"]
COMPONENT_INTEG_TESTS = ["integ-tests.cluster-manager"]
SCANS = ["npm_audit", "bandit", "viperlight_scan"]
PUBLICECRRepository = "public.ecr.aws/l6g7n3r5/research-engineering-studio"
ONBOARDED_REGIONS = "ap-northeast-1,ap-northeast-2,ap-south-1,ap-southeast-1,ap-southeast-2,ca-central-1,eu-central-1,eu-north-1,eu-south-1,eu-west-1,eu-west-2,eu-west-3,us-east-1,us-east-2,us-west-1,us-west-2"
ONBOARDED_REGIONS_GOVCLOUD = "us-gov-west-1"
class PipelineStack(Stack):
# Set Default Values if Not in ENV
_repository_name: str = "DigitalEngineeringPlatform"
_branch_name: str = "develop"
_deploy: bool = False
_integ_tests: bool = False
_bi: bool = False
_use_bi_parameters_from_ssm: bool = False
_destroy: bool = False
_destroy_bi: bool = False
_publish_templates: bool = False
@property
def repository_name(self) -> str:
return self._repository_name
@property
def branch_name(self) -> str:
return self._branch_name
@property
def pipeline(self) -> pipelines.CodePipeline:
return self._pipeline
def __init__(
self,
scope: Construct,
construct_id: str,
synthesizer: typing.Optional[IStackSynthesizer] = None,
env: Union[aws_cdk.Environment, dict[str, typing.Any], None] = None,
) -> None:
super().__init__(scope, construct_id, synthesizer=synthesizer, env=env)
context_repository_name = self.node.try_get_context("repository_name")
if context_repository_name:
self._repository_name = context_repository_name
context_branch_name = self.node.try_get_context("branch_name")
if context_branch_name:
self._branch_name = context_branch_name
context_deploy = self.node.try_get_context("deploy")
if context_deploy:
self._deploy = context_deploy.lower() == "true"
context_bi = self.node.try_get_context("batteries_included")
if context_bi:
self._bi = context_bi.lower() == "true"
context_use_bi_parameters_from_ssm = self.node.try_get_context(
"use_bi_parameters_from_ssm"
)
context_use_bi_parameters_from_ssm = (
context_use_bi_parameters_from_ssm
if context_use_bi_parameters_from_ssm
else ""
)
self._use_bi_parameters_from_ssm = (
context_use_bi_parameters_from_ssm.lower() == "true"
)
context_portal_domain_name = self.node.try_get_context("portal_domain_name")
self._portal_domain_name = (
context_portal_domain_name if context_portal_domain_name else ""
)
context_destroy = self.node.try_get_context("destroy")
context_destroy_bi = self.node.try_get_context("destroy_batteries_included")
context_integ_tests = self.node.try_get_context("integration_tests")
self._integ_tests = (
context_integ_tests.lower() == "true" if context_integ_tests else "true"
)
if context_destroy:
self._destroy = context_destroy.lower() == "true"
if context_destroy_bi:
self._destroy_bi = context_destroy_bi.lower() == "true"
context_publish_templates = self.node.try_get_context("publish_templates")
if context_publish_templates:
self._publish_templates = context_publish_templates.lower() == "true"
self.params: Union[RESParameters, BIParameters]
if self._bi or self._use_bi_parameters_from_ssm:
self.params = BIParameters.from_context(self)
else:
self.params = RESParameters.from_context(self)
if self.params.cluster_name is None:
self.params.cluster_name = "res-deploy"
bi_stack_template_url = self.node.try_get_context("BIStackTemplateURL")
bi_stack_template_url = bi_stack_template_url if bi_stack_template_url else ""
ecr_public_repository_name = self.node.try_get_context(
"ecr_public_repository_name"
)
ecr_public_repository_name = (
ecr_public_repository_name if ecr_public_repository_name else ""
)
ecr_actions = [
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart",
"ecr:DescribeRepositories",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
]
codebuild_ecr_access_actions = [
"ecr:GetAuthorizationToken",
]
# Create the ECR repository
ecr_repository = ecr.Repository(
self,
DEFAULT_ECR_REPOSITORY_NAME,
removal_policy=RemovalPolicy.DESTROY,
)
ecr_repository_name = ecr_repository.repository_name
ecr_repository_arn = ecr_repository.repository_arn
ecr_repository.grant_pull(
iam.Role(
self,
"PrivateEcrPull",
assumed_by=iam.ServicePrincipal("codebuild.amazonaws.com"),
)
)
codebuild_ecr_access = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=codebuild_ecr_access_actions,
resources=["*"],
)
codebuild_ecr_push = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=ecr_actions,
resources=[ecr_repository_arn],
)
ssm_access = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"ssm:GetParameter",
],
resources=["*"],
)
vpc_access = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"ec2:DescribeVpcs",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeRouteTables",
"ec2:DescribeNetworkAcls",
"ec2:DescribeInternetGateways",
"ec2:DescribeVpnGateways",
],
resources=["*"],
)
# Create a CodeBuild project
self._pipeline = pipelines.CodePipeline(
self,
"Pipeline",
synth=self.get_synth_step(
ecr_repository_name,
ecr_public_repository_name,
bi_stack_template_url,
context_use_bi_parameters_from_ssm,
),
code_build_defaults=pipelines.CodeBuildOptions(
build_environment=codebuild.BuildEnvironment(
build_image=codebuild.LinuxBuildImage.STANDARD_7_0,
compute_type=codebuild.ComputeType.LARGE,
privileged=True,
),
role_policy=[
codebuild_ecr_access,
codebuild_ecr_push,
ssm_access,
vpc_access,
],
),
)
audit_wave = self._pipeline.add_wave("SecurityAudit")
audit_wave.add_post(*self.get_steps_from_tox(SCANS))
unittest_wave = self._pipeline.add_wave("UnitTests")
unittest_wave.add_post(*self.get_steps_for_unit_tests(UNIT_TESTS))
coverage_wave = self._pipeline.add_wave("Coverage")
coverage_wave.add_post(*self.get_steps_for_unit_tests(COVERAGEREPORTS))
if self._deploy:
deploy_stage = DeployStage(
self,
"Deploy",
use_bi_parameters_from_ssm=self._use_bi_parameters_from_ssm,
parameters=self.params,
)
post_steps = []
create_web_and_vdi_record_step = self.get_create_web_and_vdi_record_step()
if self._portal_domain_name != "":
post_steps.append(create_web_and_vdi_record_step)
if self._integ_tests:
component_integ_test_steps = self.get_component_integ_test_steps(
COMPONENT_INTEG_TESTS
)
component_integ_test_steps.append(self.get_ad_sync_integ_test_step())
smoke_test_step = self.get_smoke_test_step()
# Smoke test cannot run with other integ tests in parallel, as it requires to relaunch the web servers
# and the servers will become temporarily unresponsive to other integ tests.
# And all integ tests should run after create_web_and_vdi_record_step.
for component_integ_test_step in component_integ_test_steps:
smoke_test_step.add_step_dependency(component_integ_test_step)
if self._portal_domain_name != "":
component_integ_test_step.add_step_dependency(
create_web_and_vdi_record_step
)
post_steps += component_integ_test_steps
post_steps.append(smoke_test_step)
if self._destroy:
destroy_step = self.get_destroy_step()
# Destroy step should only execute after all the other steps complete
for step in post_steps:
destroy_step.add_step_dependency(step)
post_steps.append(destroy_step)
self._pipeline.add_stage(deploy_stage, post=post_steps)
if self._publish_templates:
is_classic_region = aws_cdk.CfnCondition(
self,
"IsClassicRegion",
expression=Fn.condition_equals(self.partition, "aws"),
)
self.onboarded_regions = Fn.condition_if(
is_classic_region.logical_id,
ONBOARDED_REGIONS,
ONBOARDED_REGIONS_GOVCLOUD,
).to_string()
publish_wave = self._pipeline.add_wave("Publish")
publish_steps = self.get_publish_steps(ecr_public_repository_name)
publish_wave.add_post(publish_steps)
# After the artifacts gets published into each region's "RELEASE_VERSION" prefixed bucket, we will release
# the change to "/latest" bucket after a manual approval.
manual_approval_step = pipelines.ManualApprovalStep(
"ManualApprovalForLatestBucketRefresh"
)
manual_approval_step.add_step_dependency(publish_steps)
publish_wave.add_post(manual_approval_step)
artifacts_release_steps = self.get_latest_bucket_refresh_steps()
artifacts_release_steps.add_step_dependency(manual_approval_step)
publish_wave.add_post(artifacts_release_steps)
self._pipeline.build_pipeline()
if self._publish_templates and publish_steps and publish_steps.project.role:
CfnOutput(
self,
"PublishCodeBuildRole",
value=publish_steps.project.role.role_name,
)
def get_connection(self) -> pipelines.CodePipelineSource:
return pipelines.CodePipelineSource.code_commit(
repository=codecommit.Repository.from_repository_name(
scope=self,
id="CodeCommitSource",
repository_name=self._repository_name,
),
branch=self._branch_name,
)
def get_synth_step(
self,
ecr_repository_name: str,
ecr_public_repository_name: str,
bi_stack_template_url: str,
use_bi_parameters_from_ssm: str,
) -> pipelines.CodeBuildStep:
return pipelines.CodeBuildStep(
"Synth",
input=self.get_connection(),
env=dict(
REPOSITORY_NAME=self._repository_name,
BRANCH=self._branch_name,
DEPLOY="true" if self._deploy else "false",
INTEGRATION_TESTS="true" if self._integ_tests else "false",
DESTROY="true" if self._destroy else "false",
DESTROY_BATTERIES_INCLUDED="true" if self._destroy_bi else "false",
BATTERIES_INCLUDED="true" if self._bi else "false",
BIStackTemplateURL=bi_stack_template_url,
ECR_REPOSITORY=ecr_repository_name,
ECR_PUBLIC_REPOSITORY_NAME=ecr_public_repository_name,
USE_BI_PARAMETERS_FROM_SSM=use_bi_parameters_from_ssm,
PUBLISH_TEMPLATES="true" if self._publish_templates else "false",
SKIP_ENV_UPDATE="true",
PORTAL_DOMAIN_NAME=self._portal_domain_name,
**self.params.to_context(),
),
install_commands=get_commands_for_scripts(
[
"source/idea/pipeline/scripts/common/install_commands.sh",
"source/idea/pipeline/scripts/synth/install_commands.sh",
]
),
commands=get_commands_for_scripts(
[
"source/idea/pipeline/scripts/synth/commands.sh",
]
),
partial_build_spec=self.get_reports_partial_build_spec("pytest-report.xml"),
)
def get_web_and_vdi_record_policy(
self,
) -> tuple[iam.PolicyStatement, iam.PolicyStatement]:
codebuild_read_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"route53:ListHostedZones",
"elasticloadbalancing:DescribeLoadBalancers",
],
resources=["*"],
)
codebuild_route53_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=["route53:ChangeResourceRecordSets", "route53:GetChange"],
resources=["arn:aws:route53:::hostedzone/*", "arn:aws:route53:::change/*"],
)
return codebuild_read_policy, codebuild_route53_policy
def get_create_web_and_vdi_record_step(self) -> pipelines.CodeBuildStep:
codebuild_read_policy, codebuild_route53_policy = (
self.get_web_and_vdi_record_policy()
)
return pipelines.CodeBuildStep(
"CreateWebAndVdiDNSRecords",
env=dict(
PORTAL_DOMAIN=self._portal_domain_name,
WEB_PORTAL_DOMAIN=self.params.custom_domain_name_for_web_ui,
VDI_PORTAL_DOMAIN=self.params.custom_domain_name_for_vdi,
CLUSTER_NAME=self.params.cluster_name,
WEB_AND_VDI_RECORD_ACTION="UPSERT",
),
commands=get_commands_for_scripts(
["source/idea/pipeline/scripts/common/web_and_vdi_record_commands.sh"]
),
role_policy_statements=[codebuild_read_policy, codebuild_route53_policy],
)
def get_component_integ_test_steps(
self, integ_test_envs: list[str]
) -> list[pipelines.CodeBuildStep]:
steps: list[pipelines.CodeBuildStep] = []
clusteradmin_username = "clusteradmin" # bootstrap user
clusteradmin_password = "RESPassword1." # fixed password for running tests
for _env in integ_test_envs:
_step = (
IntegTestStepBuilder(_env, self.params.cluster_name, self.region, True)
.test_specific_invoke_command_argument(
f"admin-username={clusteradmin_username}",
f"admin-password={clusteradmin_password}",
)
.test_specific_env(
CLUSTERADMIN_USERNAME=clusteradmin_username,
CLUSTERADMIN_PASSWORD=clusteradmin_password,
)
.test_specific_role_policy_statement(
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"cognito-idp:ListUserPools",
"cognito-idp:AdminSetUserPassword",
],
"Resource": "*",
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"dynamodb:DescribeTable",
"dynamodb:Scan",
"dynamodb:GetItem",
],
"Resource": [
f"arn:{self.partition}:dynamodb:{self.region}:{self.account}:table/{self.params.cluster_name}.cluster-settings",
f"arn:{self.partition}:dynamodb:{self.region}:{self.account}:table/{self.params.cluster_name}.modules",
],
}
),
)
.build()
)
steps.append(_step)
return steps
def get_smoke_test_step(self) -> pipelines.CodeBuildStep:
step = (
IntegTestStepBuilder(
"integ-tests.smoke", self.params.cluster_name, self.region
)
.test_specific_install_command(
*get_commands_for_scripts(
["source/idea/pipeline/scripts/chrome/install_commands.sh"]
)
)
.test_specific_role_policy_statement(
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand",
],
"Resource": [
f"arn:{self.partition}:ssm:{self.region}:*:document/*",
],
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand",
],
"Resource": [
f"arn:{self.partition}:ec2:{self.region}:{self.account}:instance/*"
],
"Condition": {
"StringLike": {
"ssm:resourceTag/res:EnvironmentName": [
self.params.cluster_name
]
}
},
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"ssm:GetCommandInvocation",
],
"Resource": "*",
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
],
# TODO: Specify the bucket to which SSM writes command outputs
"Resource": "*",
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"elasticloadbalancing:DescribeLoadBalancers",
],
"Resource": "*",
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"elasticloadbalancing:ModifyLoadBalancerAttributes",
],
"Resource": f"arn:{self.partition}:elasticloadbalancing:{self.region}:{self.account}:loadbalancer/app/{self.params.cluster_name}-external-alb/*",
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"autoscaling:DescribeAutoScalingGroups",
],
"Resource": "*",
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
],
"Resource": [
f"arn:{self.partition}:dynamodb:{self.region}:{self.account}:table/{self.params.cluster_name}.cluster-settings",
f"arn:{self.partition}:dynamodb:{self.region}:{self.account}:table/{self.params.cluster_name}.ad-sync.distributed-lock",
f"arn:{self.partition}:dynamodb:{self.region}:{self.account}:table/{self.params.cluster_name}.ad-sync.status",
],
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"ecs:RunTask",
"ecs:StopTask",
"ecs:ListTasks",
],
"Resource": "*",
"Condition": {
"ArnEquals": {
"ecs:cluster": f"arn:{self.partition}:ecs:{self.region}:{self.account}:cluster/{self.params.cluster_name}-ad-sync-cluster",
}
},
},
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": [
"iam:PassRole",
],
"Resource": f"arn:{self.partition}:iam::{self.account}:role/{self.params.cluster_name}-ad-sync-task-role",
}
),
iam.PolicyStatement.from_json(
{
"Effect": "Allow",
"Action": ["ec2:DescribeSecurityGroups", "ec2:DeregisterImage"],
"Resource": "*",
}
),
)
.build()
)
return step
def get_ad_sync_integ_test_step(self) -> pipelines.CodeBuildStep:
step = (
IntegTestStepBuilder(
"integ-tests.ad-sync",
self.params.cluster_name,
self.region,
requires_alb=False,
)
.test_specific_install_command(
*get_commands_for_scripts(
["source/idea/ad-sync/tests/integration/scripts/run_slapd.sh"]
),
# Allow communication with the local AD
"echo '127.0.0.1 corp.res.com' | sudo tee -a /etc/hosts",
)
.build()
)
return step
@staticmethod
def get_steps_from_tox(tox_env: list[str]) -> list[pipelines.CodeBuildStep]:
steps: list[pipelines.CodeBuildStep] = []
for _env in tox_env:
_step = pipelines.CodeBuildStep(
_env,
install_commands=get_commands_for_scripts(
[
"source/idea/pipeline/scripts/common/install_commands.sh",
"source/idea/pipeline/scripts/tox/install_commands.sh",
]
),
commands=[
f"tox -e {_env}",
],
)
steps.append(_step)
return steps
@staticmethod
def get_steps_for_unit_tests(test_env: list[str]) -> list[pipelines.CodeBuildStep]:
steps: list[pipelines.CodeBuildStep] = []
for _env in test_env:
_step = pipelines.CodeBuildStep(
_env,
install_commands=get_commands_for_scripts(
[
"source/idea/pipeline/scripts/common/install_commands.sh",
"source/idea/pipeline/scripts/unit_tests/install_commands.sh",
"source/idea/pipeline/scripts/unit_tests/commands.sh",
]
),
commands=[
f"tox -e {_env}",
],
)
steps.append(_step)
return steps
def get_destroy_step(self) -> pipelines.CodeBuildStep:
codebuild_cloudformation_read_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"cloudformation:ListStacks",
"cloudformation:DescribeStacks",
],
resources=[
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/Deploy-{INSTALL_STACK_NAME}/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-bootstrap/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-cluster/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-metrics/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-directoryservice/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-identity-provider/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-shared-storage/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-cluster-manager/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-vdc/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/{self.params.cluster_name}-bastion-host/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/Deploy-{BATTERIES_INCLUDED_STACK_NAME}*",
],
)
codebuild_cloudformation_delete_stack_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"cloudformation:DeleteStack",
],
resources=[
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/Deploy-{INSTALL_STACK_NAME}/*",
f"arn:{self.partition}:cloudformation:{self.region}:{self.account}:stack/Deploy-{BATTERIES_INCLUDED_STACK_NAME}*",
],
)
codebuild_read_ssm_parameter_vpc_id_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"ssm:GetParameter",
],
resources=[
f"arn:{self.partition}:ssm:{self.region}:{self.account}:parameter{self.params.vpc_id}"
],
)
codebuild_read_file_systems_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"elasticfilesystem:DescribeFileSystems",
"elasticfilesystem:DescribeMountTargets",
"fsx:DescribeFileSystems",
"fsx:DescribeStorageVirtualMachines",
"fsx:DescribeVolumes",
],
resources=["*"],
)
codebuild_efs_delete_file_systems_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"elasticfilesystem:DeleteMountTarget",
"elasticfilesystem:DeleteFileSystem",
],
resources=["*"],
conditions={
"StringEquals": {
"aws:ResourceTag/res:EnvironmentName": [self.params.cluster_name],
},
},
)
codebuild_efs_filesystem_ec2_delete_eni_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"ec2:DeleteNetworkInterface",
],
resources=["*"],
)
codebuild_fsx_delete_file_systems_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"fsx:DeleteFileSystem",
],
resources=["*"],
conditions={
"StringEquals": {
"aws:ResourceTag/res:EnvironmentName": [self.params.cluster_name],
},
},
)
codebuild_fsx_delete_svms_volumes_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"fsx:DeleteVolume",
"fsx:DeleteStorageVirtualMachine",
"fsx:CreateBackup",
"fsx:TagResource",
],
resources=["*"],
)
codebuild_shared_storage_security_group_read_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"ec2:DescribeSecurityGroups",
],
resources=["*"],
)
codebuild_shared_storage_security_group_delete_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"ec2:DeleteSecurityGroup",
],
resources=["*"],
conditions={
"StringEquals": {
"aws:ResourceTag/Name": [
f"{self.params.cluster_name}-shared-storage-security-group"
],
},
},
)
(
codebuild_destroy_records_read_policy,
codebuild_destroy_records_route53_policy,
) = self.get_web_and_vdi_record_policy()
commands = ["source/idea/pipeline/scripts/destroy/commands.sh"]
if self._portal_domain_name != "":
commands.insert(
0, "source/idea/pipeline/scripts/common/web_and_vdi_record_commands.sh"
)
return pipelines.CodeBuildStep(
"Destroy",
build_environment=codebuild.BuildEnvironment(
build_image=codebuild.LinuxBuildImage.STANDARD_7_0,
compute_type=codebuild.ComputeType.SMALL,
privileged=True,
),
env=dict(
CLUSTER_NAME=self.params.cluster_name,
AWS_REGION=self.region,
BATTERIES_INCLUDED="true" if self._bi else "false",
USE_BI_PARAMETERS_FROM_SSM=(
"true" if self._use_bi_parameters_from_ssm else "false"
),
DESTROY_BATTERIES_INCLUDED="true" if self._destroy_bi else "false",
VPC_ID=self.params.vpc_id,
INSTALL_STACK_NAME=INSTALL_STACK_NAME,
BATTERIES_INCLUDED_STACK_NAME=f"Deploy-{BATTERIES_INCLUDED_STACK_NAME}",
PORTAL_DOMAIN=self._portal_domain_name,
WEB_PORTAL_DOMAIN=self.params.custom_domain_name_for_web_ui,
VDI_PORTAL_DOMAIN=self.params.custom_domain_name_for_vdi,
WEB_AND_VDI_RECORD_ACTION="DELETE",
),
install_commands=get_commands_for_scripts(
[
"source/idea/pipeline/scripts/common/install_commands.sh",
"source/idea/pipeline/scripts/destroy/install_commands.sh",
]
),
commands=get_commands_for_scripts(commands),
role_policy_statements=[
codebuild_cloudformation_read_policy,
codebuild_cloudformation_delete_stack_policy,
codebuild_read_ssm_parameter_vpc_id_policy,
codebuild_read_file_systems_policy,
codebuild_efs_delete_file_systems_policy,
codebuild_efs_filesystem_ec2_delete_eni_policy,
codebuild_fsx_delete_file_systems_policy,
codebuild_fsx_delete_svms_volumes_policy,
codebuild_shared_storage_security_group_read_policy,
codebuild_shared_storage_security_group_delete_policy,
codebuild_destroy_records_read_policy,
codebuild_destroy_records_route53_policy,
],
timeout=Duration.hours(2),
)
def get_publish_steps(
self, ecr_public_repository_name: str
) -> pipelines.CodeBuildStep:
ecr_public_repository_uri = (
"" if ecr_public_repository_name else PUBLICECRRepository
)
ecr_public_repository_actions = [
"ecr-public:BatchCheckLayerAvailability",
"ecr-public:CompleteLayerUpload",
"ecr-public:InitiateLayerUpload",
"ecr-public:PutImage",
"ecr-public:UploadLayerPart",
"ecr-public:DescribeRepositories",
]
ecr_public_access_actions = [
"ecr-public:GetAuthorizationToken",
"sts:GetServiceBearerToken",
]
ecr_repository_arn = f"arn:{self.partition}:ecr-public::{self.account}:repository/{ecr_public_repository_name}"
codebuild_ecr_access = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=ecr_public_access_actions,
resources=["*"],
)
codebuild_ecr_repository = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=ecr_public_repository_actions,
resources=[ecr_repository_arn],
)
codebuild_describe_regions = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=["ec2:DescribeRegions"],
resources=["*"],
)
return pipelines.CodeBuildStep(
"Publish templates and docker image",
build_environment=codebuild.BuildEnvironment(
build_image=codebuild.LinuxBuildImage.STANDARD_7_0,
compute_type=codebuild.ComputeType.SMALL,
privileged=True,
),
env=dict(
ARTIFACTS_BUCKET_PREFIX_NAME=ARTIFACTS_BUCKET_PREFIX_NAME,
INSTALL_STACK_NAME=INSTALL_STACK_NAME,
ECR_REPOSITORY=ecr_public_repository_name,
PUBLISH_TEMPLATES="true" if self._publish_templates else "false",
ECR_REPOSITORY_URI_PARAMETER=ecr_public_repository_uri,
SKIP_ENV_UPDATE="true",
ONBOARDED_REGIONS=self.onboarded_regions,
),
install_commands=get_commands_for_scripts(
[
"source/idea/pipeline/scripts/common/install_commands.sh",
"source/idea/pipeline/scripts/publish/install_commands.sh",
]
),
commands=get_commands_for_scripts(
[
"source/idea/pipeline/scripts/publish/commands.sh",
]
),
role_policy_statements=[
codebuild_ecr_access,
codebuild_ecr_repository,
codebuild_describe_regions,
],
)
def get_latest_bucket_refresh_steps(self) -> pipelines.CodeBuildStep:
codebuild_s3_all_access = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"s3:PutObject",
"s3:GetObjectTagging",
"s3:getBucketLocation",
"s3:ListBucket",
"s3:GetObject",
"s3:DeleteObject",
],
resources=[
f"arn:{self.partition}:s3:::{ARTIFACTS_BUCKET_PREFIX_NAME}-*",
],
)
return pipelines.CodeBuildStep(
"Refresh the /latest bucket",
env=dict(
ARTIFACTS_BUCKET_PREFIX_NAME=ARTIFACTS_BUCKET_PREFIX_NAME,
ONBOARDED_REGIONS=self.onboarded_regions,
),
commands=get_commands_for_scripts(
[
"source/idea/pipeline/scripts/publish/latest_bucket_refresh.sh",
]
),
role_policy_statements=[codebuild_s3_all_access],
)
@staticmethod
def get_commands_for_scripts(paths: list[str]) -> list[str]:
commands = []
root = pathlib.Path("source").parent
scripts = root / "source/idea/pipeline/scripts"
for raw_path in paths:
path = pathlib.Path(raw_path)
if not path.exists():
raise ValueError(f"script path doesn't exist: {path}")
if not path.is_relative_to(scripts):
raise ValueError(f"script path isn't in {scripts}: {path}")
relative = path.relative_to(root)
commands.append(f"chmod +x {relative}")
commands.append(str(relative))
return commands
def get_reports_partial_build_spec(self, filename: str) -> codebuild.BuildSpec:
return codebuild.BuildSpec.from_object(
{
"reports": {
"pytest_reports": {
"files": [filename],
"file-format": "JUNITXML",
}
}
}
)
class DeployStage(Stage):
def __init__(
self,
scope: Construct,
construct_id: str,
use_bi_parameters_from_ssm: bool,
parameters: Union[RESParameters, BIParameters],
):
super().__init__(scope, construct_id)
installer_registry_name = self.node.try_get_context("installer_registry_name")
ad_sync_registry_name = self.node.try_get_context("ad_sync_registry_name")
self.batteries_included_stack = None
if isinstance(parameters, BIParameters) and not use_bi_parameters_from_ssm:
bi_stack_template_url = self.node.try_get_context("BIStackTemplateURL")
self.batteries_included_stack = BiStack(
self,
BATTERIES_INCLUDED_STACK_NAME,
template_url=bi_stack_template_url,
parameters=parameters,
)
self.install_stack = InstallStack(
self,
INSTALL_STACK_NAME,
parameters=parameters,
installer_registry_name=installer_registry_name,
ad_sync_registry_name=ad_sync_registry_name,
)
if self.batteries_included_stack:
self.install_stack.add_dependency(target=self.batteries_included_stack)