Skip to content
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

feat: allow-for-empty-targets-in-deployment-maps #634

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/admin-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ Config has five components in `main-notification-endpoint`, `scp`, `scm`,
`master` instead. We recommend configuring the main scm branch name to
`main`. As new repositories will most likely use this branch name as their
default branch.
- `deployment-maps` tracks all source code management configuration.
- **allow-empty-target** allows you to configure deployment maps with empty targets.
If all targets get evaluated to empty, the ADF CodePipeline is still created
based on the remaining providers (e.g. source and build). It just doesn't have
a deploy stage.
Use cases might include the following:
- target an OU that doesn't have any AWS Accounts (initially or temporarily).
- target AWS Accounts by tag with no AWS Accounts having that tag assigned (yet).

## Accounts

Expand Down
2 changes: 2 additions & 0 deletions src/lambda_codebase/initial_commit/adfconfig.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ config:
scm:
auto-create-repositories: enabled
default-scm-branch: main
deployment-maps:
allow-empty-target: "False" # Needs to be "True" or "False". Defaults to "False", if not set.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ def generate_pipeline_inputs(
list(target_structure.generate_waves()),
)

# Print out final target list...
LOGGER.info("Targets Found:")
LOGGER.info(pipeline_object.template_dictionary["targets"])
number_of_targets = 0
for target in pipeline_object.template_dictionary["targets"]:
for target_accounts in target:
number_of_targets = number_of_targets + len(target_accounts)
if number_of_targets > 0:
LOGGER.info("Found %s targets total", number_of_targets)
else:
LOGGER.info("Creating empty pipeline as there were no targets found")

if DEPLOYMENT_ACCOUNT_REGION not in regions:
pipeline_object.stage_regions.append(DEPLOYMENT_ACCOUNT_REGION)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"ACCOUNT_BOOTSTRAPPING_STATE_MACHINE_ARN"
)
ADF_DEFAULT_SCM_FALLBACK_BRANCH = 'master'
ADF_DEFAULT_ALLOW_EMPTY_DEPLOYMENT_MAPS = False
LOGGER = configure_logger(__name__)


Expand Down Expand Up @@ -151,6 +152,13 @@ def prepare_deployment_account(sts, deployment_account_id, config):
ADF_DEFAULT_SCM_FALLBACK_BRANCH,
)
)
deployment_account_parameter_store.put_parameter(
'/adf/deployment-maps/allow-empty-target',
config.config.get('deployment-maps', {}).get(
'allow-empty-target',
ADF_DEFAULT_ALLOW_EMPTY_DEPLOYMENT_MAPS,
)
)
auto_create_repositories = config.config.get(
'scm', {}).get('auto-create-repositories')
if auto_create_repositories is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
from errors import InvalidDeploymentMapError, NoAccountsFoundError
from logger import configure_logger
from schema_validation import AWS_ACCOUNT_ID_REGEX_STR

from botocore.exceptions import ClientError
from errors import ParameterNotFoundError
from parameter_store import ParameterStore
import boto3

LOGGER = configure_logger(__name__)
ADF_DEPLOYMENT_ACCOUNT_ID = os.environ["ACCOUNT_ID"]
DEPLOYMENT_ACCOUNT_REGION = os.environ["AWS_REGION"]

AWS_ACCOUNT_ID_REGEX = re.compile(AWS_ACCOUNT_ID_REGEX_STR)
RECURSIVE_SUFFIX = "/**/*"

Expand Down Expand Up @@ -94,6 +99,13 @@ def __init__(
)
self.target_structure = target_structure
self.organizations = organizations
# Set allow_empty_deployment_maps as bool
parameter_store = ParameterStore(DEPLOYMENT_ACCOUNT_REGION, boto3)
adf_deployment_maps_allow_empty_bool = parameter_store.fetch_parameter(
"/adf/deployment-maps/allow-empty"
AndyEfaa marked this conversation as resolved.
Show resolved Hide resolved
).lower().capitalize() == "True"
self.allow_empty_deployment_maps = adf_deployment_maps_allow_empty_bool
AndyEfaa marked this conversation as resolved.
Show resolved Hide resolved


@staticmethod
def _account_is_active(account):
Expand Down Expand Up @@ -129,14 +141,28 @@ def _create_response_object(self, responses):
response.get("Name"), str(response.get("Id"))
)
)

if accounts_found == 0:
raise NoAccountsFoundError(f"No accounts found in {self.path}")
if self.allow_empty_deployment_maps is True:
LOGGER.info(
"Create_response_object: AWS accounts found is 0 for path %s. Continue with empty response.", self.path
)
else:
raise NoAccountsFoundError(f"No accounts found in {self.path}.")

def _target_is_account_id(self):
responses = self.organizations.client.describe_account(
AccountId=str(self.path)
).get("Account")
self._create_response_object([responses])
try:
responses = self.organizations.client.describe_account(
AccountId=str(self.path)
).get('Account')
responses_list = [responses]
except ClientError as client_err:
if (client_err.response["Error"]["Code"] == "AccountNotFoundException") and self.allow_empty_deployment_maps is True:
LOGGER.info("IGNORE - Account was not found in AWS Org for id %s", self.path)
responses_list = []
else:
raise
self._create_response_object(responses_list)

def _target_is_tags(self):
responses = self.organizations.get_account_ids_for_tags(self.path)
Expand All @@ -155,16 +181,34 @@ def _target_is_tags(self):
self._create_response_object(accounts)

def _target_is_ou_id(self):
responses = self.organizations.get_accounts_for_parent(str(self.path))
try:
# Check if ou exists - otherwise throw clean exception here
self.organizations.client.list_children(ParentId=self.path, ChildType="ACCOUNT")
responses = self.organizations.get_accounts_for_parent(
str(self.path)
)
except ClientError as client_err:
if client_err.response["Error"]["Code"] == "ParentNotFoundException" and self.allow_empty_deployment_maps is True:
LOGGER.info("IGNORE - Target OU was not found in AWS Org for %s", self.path)
responses = []
else:
raise
self._create_response_object(responses)

def _target_is_ou_path(self, resolve_children=False):
responses = self.organizations.get_accounts_in_path(
self.path,
resolve_children=resolve_children,
ou_id=None,
excluded_paths=[],
)
try:
responses = self.organizations.get_accounts_in_path(
self.path,
resolve_children=resolve_children,
ou_id=None,
excluded_paths=[],
)
except Exception:
if self.allow_empty_deployment_maps is True:
LOGGER.info("IGNORE - Target OU was not found in AWS Org for path %s", self.path)
responses = []
else:
raise
self._create_response_object(responses)

def _target_is_null_path(self):
Expand Down Expand Up @@ -204,6 +248,10 @@ def fetch_accounts_for_target(self):
return self._target_is_ou_path(
resolve_children=str(self.path).endswith(RECURSIVE_SUFFIX)
)

if self.allow_empty_deployment_maps is True:
sbkok marked this conversation as resolved.
Show resolved Hide resolved
return

if self.path is None:
# No path/target has been passed, path will default to /deployment
return self._target_is_null_path()
sbkok marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Loading