Skip to content

Commit

Permalink
Fixing dependency coordination for SSM output/parameter setups and no…
Browse files Browse the repository at this point in the history
…w allowing account_id to be set to AWS::AccountId within outputs and parameters
  • Loading branch information
eamonnfaherty authored Jun 5, 2023
2 parents 3a94153 + a352ed6 commit ca9c38f
Show file tree
Hide file tree
Showing 11 changed files with 552 additions and 128 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ ignored.html
*.html
state1.json
.fleet
GetSSMParamTask.zip
manifest-expanded.yaml
manifest-task-reference.json
resources.json
tasks/
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[tool.poetry]
name = "aws-service-catalog-puppet"
version = "0.228.1"
version = "0.229.0"
description = "Making it easier to deploy ServiceCatalog products"
classifiers = ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Natural Language :: English"]
homepage = "https://service-catalog-tools-workshop.com/"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
from servicecatalog_puppet.commands.task_reference_helpers.generators.boto3_parameter_handler import (
boto3_parameter_handler,
)
from servicecatalog_puppet.commands.task_reference_helpers.generators.ssm_outputs_handler import (
ssm_outputs_handler,
)
from servicecatalog_puppet.commands.task_reference_helpers.generators.ssm_parameter_handler import (
ssm_parameter_handler,
)
from servicecatalog_puppet.workflow import workflow_utils
from servicecatalog_puppet.workflow.dependencies import resources_factory

Expand Down Expand Up @@ -136,54 +142,15 @@ def generate(puppet_account_id, manifest, output_file_path):
account_and_region
].append(all_tasks_task_reference)

# ssm outputs
for ssm_parameter_output in task_to_add.get("ssm_param_outputs", []):
output_region = ssm_parameter_output.get("region", default_region)
output_account_id = ssm_parameter_output.get(
"account_id", puppet_account_id
)
ssm_parameter_output_task_reference = f'{constants.SSM_OUTPUTS}-{task_to_add.get("account_id")}-{output_region}-{ssm_parameter_output.get("param_name")}'
ssm_parameter_output_task_reference = ssm_parameter_output_task_reference.replace(
"${AWS::Region}", task_to_add.get("region")
).replace(
"${AWS::AccountId}", task_to_add.get("account_id")
)
if all_tasks.get(ssm_parameter_output_task_reference):
raise Exception(
f"You have two tasks outputting the same SSM parameter output: {ssm_parameter_output.get('param_name')}"
)

else:
all_tasks[ssm_parameter_output_task_reference] = dict(
manifest_section_names=dict(),
manifest_item_names=dict(),
manifest_account_ids=dict(),
task_reference=ssm_parameter_output_task_reference,
param_name=ssm_parameter_output.get("param_name")
.replace("${AWS::Region}", task_to_add.get("region"))
.replace(
"${AWS::AccountId}", task_to_add.get("account_id")
),
stack_output=ssm_parameter_output.get("stack_output"),
force_operation=ssm_parameter_output.get(
"force_operation", False
),
account_id=output_account_id,
region=output_region,
dependencies_by_reference=[all_tasks_task_reference],
task_generating_output=all_tasks_task_reference,
status=task_to_add.get("status"),
section_name=constants.SSM_OUTPUTS,
)
all_tasks[ssm_parameter_output_task_reference][
"manifest_section_names"
][section_name] = True
all_tasks[ssm_parameter_output_task_reference][
"manifest_item_names"
][item_name] = True
all_tasks[ssm_parameter_output_task_reference][
"manifest_account_ids"
][task_to_add.get("account_id")] = True
ssm_outputs_handler(
all_tasks,
all_tasks_task_reference,
default_region,
item_name,
puppet_account_id,
section_name,
task_to_add,
)

generator.generate(
all_tasks,
Expand Down Expand Up @@ -216,81 +183,14 @@ def generate(puppet_account_id, manifest, output_file_path):

if task.get("status") != constants.TERMINATED:
for parameter_name, parameter_details in parameters.items():
if parameter_details.get("ssm"):
ssm_parameter_details = parameter_details.get("ssm")
interpolation_output_account = task.get("account_id")
interpolation_output_region = task.get("region")
owning_account = ssm_parameter_details.get(
"account_id", puppet_account_id
)
owning_region = ssm_parameter_details.get("region", default_region)
task_reference = f"{owning_account}-{owning_region}"
param_name = (
ssm_parameter_details.get("name")
.replace("${AWS::Region}", interpolation_output_region)
.replace("${AWS::AccountId}", interpolation_output_account)
)

task_def = dict(
account_id=owning_account,
region=owning_region,
manifest_section_names=dict(
**task.get("manifest_section_names")
),
manifest_item_names=dict(**task.get("manifest_item_names")),
manifest_account_ids=dict(**task.get("manifest_account_ids")),
)
path = ssm_parameter_details.get("path")
if path is None:
ssm_parameter_task_reference = (
f"{constants.SSM_PARAMETERS}-{task_reference}-{param_name}"
)
task_def["param_name"] = param_name
task_def["section_name"] = constants.SSM_PARAMETERS
else:
ssm_parameter_task_reference = f"{constants.SSM_PARAMETERS_WITH_A_PATH}-{task_reference}-{path}"
task_def["path"] = path
task_def["section_name"] = constants.SSM_PARAMETERS_WITH_A_PATH
task_def["task_reference"] = ssm_parameter_task_reference

potential_output_task_ref = f"{constants.SSM_PARAMETERS}-{task_reference}-{param_name}".replace(
f"{constants.SSM_PARAMETERS}-", f"{constants.SSM_OUTPUTS}-"
)
if all_tasks.get(potential_output_task_ref):
dependency = [potential_output_task_ref]
else:
dependency = []
task_def["dependencies_by_reference"] = dependency

# IF THERE ARE TWO TASKS USING THE SAME PARAMETER AND THE OTHER TASK ADDED IT FIRST
if new_tasks.get(ssm_parameter_task_reference):
existing_task_def = new_tasks[ssm_parameter_task_reference]
# AVOID DUPLICATE DEPENDENCIES IN THE SAME LIST
for dep in dependency:
if (
dep
not in existing_task_def["dependencies_by_reference"]
):
existing_task_def["dependencies_by_reference"].append(
dep
)
else:
new_tasks[ssm_parameter_task_reference] = task_def

new_tasks[ssm_parameter_task_reference][
"manifest_section_names"
].update(task.get("manifest_section_names"))
new_tasks[ssm_parameter_task_reference][
"manifest_item_names"
].update(task.get("manifest_item_names"))
new_tasks[ssm_parameter_task_reference][
"manifest_account_ids"
].update(task.get("manifest_account_ids"))

task["dependencies_by_reference"].append(
ssm_parameter_task_reference
)
# HANDLE BOTO3 PARAMS
ssm_parameter_handler(
all_tasks,
default_region,
new_tasks,
parameter_details,
puppet_account_id,
task,
)
boto3_parameter_handler(
new_tasks,
parameter_details,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def boto3_parameter_handler(
]:
if account_id_to_use_for_boto3_call != puppet_account_id:
raise Exception(
f"Cannot use {task_execution} for a task that is not in the puppet account"
f"Cannot use cross account Boto3 Parameters in execution mode: {task_execution}"
)
if not new_tasks.get(boto3_parameter_task_reference):
new_tasks[boto3_parameter_task_reference] = dict(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from servicecatalog_puppet import constants


class CompleteGeneratorTest(unittest.TestCase):
class Boto3ParameterHandlerTest(unittest.TestCase):
def setUp(self):
self.maxDiff = None
from servicecatalog_puppet.commands.task_reference_helpers.generators import (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

from servicecatalog_puppet import constants


def ssm_outputs_handler(
all_tasks,
all_tasks_task_reference,
default_region,
item_name,
puppet_account_id,
section_name,
task_to_add,
):
for ssm_parameter_output in task_to_add.get("ssm_param_outputs", []):
output_region = ssm_parameter_output.get("region", default_region)
task_account_id = task_to_add.get("account_id")
output_account_id = ssm_parameter_output.get(
"account_id", puppet_account_id
).replace("${AWS::AccountId}", task_account_id)
ssm_parameter_output_task_reference = f'{constants.SSM_OUTPUTS}-{output_account_id}-{output_region}-{ssm_parameter_output.get("param_name")}'
ssm_parameter_output_task_reference = ssm_parameter_output_task_reference.replace(
"${AWS::Region}", task_to_add.get("region")
).replace(
"${AWS::AccountId}", task_account_id
)
if all_tasks.get(ssm_parameter_output_task_reference):
raise Exception(
f"You have two tasks outputting the same SSM parameter output: {ssm_parameter_output.get('param_name')}: {ssm_parameter_output_task_reference}"
)

else:
all_tasks[ssm_parameter_output_task_reference] = dict(
manifest_section_names=dict(),
manifest_item_names=dict(),
manifest_account_ids=dict(),
task_reference=ssm_parameter_output_task_reference,
param_name=ssm_parameter_output.get("param_name")
.replace("${AWS::Region}", task_to_add.get("region"))
.replace("${AWS::AccountId}", task_account_id),
stack_output=ssm_parameter_output.get("stack_output"),
force_operation=ssm_parameter_output.get("force_operation", False),
account_id=output_account_id,
region=output_region,
dependencies_by_reference=[all_tasks_task_reference],
task_generating_output=all_tasks_task_reference,
status=task_to_add.get("status"),
section_name=constants.SSM_OUTPUTS,
execution=task_to_add.get(
"execution", constants.EXECUTION_MODE_DEFAULT
),
)
all_tasks[ssm_parameter_output_task_reference]["manifest_section_names"][
section_name
] = True
all_tasks[ssm_parameter_output_task_reference]["manifest_item_names"][
item_name
] = True
all_tasks[ssm_parameter_output_task_reference]["manifest_account_ids"][
output_account_id
] = True
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

from servicecatalog_puppet import constants


def assertCrossAccountAccessWillWork(
owning_account, task, task_execution, puppet_account_id
):
if task_execution not in [
constants.EXECUTION_MODE_HUB,
constants.EXECUTION_MODE_ASYNC,
]:
if (
owning_account != task.get("account_id")
and owning_account != puppet_account_id
):
message = f"Cannot use cross account SSM parameters in execution mode: {task_execution}."
message += f"For task {task.get('task_reference')}, "
message += f"parameter is in account {owning_account} and task will execute in {task.get('account_id')}."
raise Exception(message)


def ssm_parameter_handler(
all_tasks, default_region, new_tasks, parameter_details, puppet_account_id, task
):
if parameter_details.get("ssm"):
ssm_parameter_details = parameter_details.get("ssm")
interpolation_output_account = task.get("account_id")
interpolation_output_region = task.get("region")
owning_account = ssm_parameter_details.get(
"account_id", puppet_account_id
).replace("${AWS::AccountId}", interpolation_output_account)
owning_region = ssm_parameter_details.get("region", default_region).replace(
"${AWS::Region}", interpolation_output_region
)
task_reference = f"{owning_account}-{owning_region}"
param_name = (
ssm_parameter_details.get("name")
.replace("${AWS::Region}", interpolation_output_region)
.replace("${AWS::AccountId}", interpolation_output_account)
)

task_execution = task.get("execution", constants.EXECUTION_MODE_DEFAULT)
if owning_account == puppet_account_id:
task_execution = constants.EXECUTION_MODE_HUB
assertCrossAccountAccessWillWork(
owning_account, task, task_execution, puppet_account_id
)

if task.get(task_execution) in [
constants.EXECUTION_MODE_HUB,
constants.EXECUTION_MODE_ASYNC,
]:
if owning_account != puppet_account_id:
raise Exception(
f"Cannot use cross account SSM parameters in execution mode: {task_execution}"
)

task_def = dict(
account_id=owning_account,
region=owning_region,
manifest_section_names=dict(**task.get("manifest_section_names")),
manifest_item_names=dict(**task.get("manifest_item_names")),
manifest_account_ids=dict(**task.get("manifest_account_ids")),
dependencies=[],
execution=task_execution,
)
path = ssm_parameter_details.get("path")
if path is None:
ssm_parameter_task_reference = (
f"{constants.SSM_PARAMETERS}-{task_reference}-{param_name}"
)
task_def["param_name"] = param_name
task_def["section_name"] = constants.SSM_PARAMETERS
else:
ssm_parameter_task_reference = (
f"{constants.SSM_PARAMETERS_WITH_A_PATH}-{task_reference}-{path}"
)
task_def["path"] = path
task_def["section_name"] = constants.SSM_PARAMETERS_WITH_A_PATH
task_def["task_reference"] = ssm_parameter_task_reference

potential_output_task_ref = f"{constants.SSM_PARAMETERS}-{task_reference}-{param_name}".replace(
f"{constants.SSM_PARAMETERS}-", f"{constants.SSM_OUTPUTS}-"
)
if all_tasks.get(potential_output_task_ref):
dependency = [potential_output_task_ref]
else:
dependency = []
task_def["dependencies_by_reference"] = dependency

# IF THERE ARE TWO TASKS USING THE SAME PARAMETER AND THE OTHER TASK ADDED IT FIRST
if new_tasks.get(ssm_parameter_task_reference):
existing_task_def = new_tasks[ssm_parameter_task_reference]
# AVOID DUPLICATE DEPENDENCIES IN THE SAME LIST
for dep in dependency:
if dep not in existing_task_def["dependencies_by_reference"]:
existing_task_def["dependencies_by_reference"].append(dep)
else:
new_tasks[ssm_parameter_task_reference] = task_def

ssm_parameter_task = new_tasks[ssm_parameter_task_reference]
ssm_parameter_task["manifest_section_names"].update(
task.get("manifest_section_names")
)
ssm_parameter_task["manifest_item_names"].update(
task.get("manifest_item_names")
)
ssm_parameter_task["manifest_account_ids"].update(
task.get("manifest_account_ids")
)
ssm_parameter_task["dependencies"].extend(task.get("dependencies"))

task["dependencies_by_reference"].append(ssm_parameter_task_reference)
Loading

0 comments on commit ca9c38f

Please sign in to comment.