Skip to content

Commit

Permalink
Merge pull request #655 from awslabs/fix/cyclic-depends-on-for-boto3
Browse files Browse the repository at this point in the history
fix: boto3 parameter generation was causing a cyclic dependency for c…
  • Loading branch information
eamonnfaherty committed May 28, 2023
2 parents 9a812d0 + 15d0af7 commit 3a94153
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 95 deletions.
26 changes: 13 additions & 13 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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.0"
version = "0.228.1"
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 @@ -7,6 +7,9 @@
from servicecatalog_puppet import config, constants, serialisation_utils
from servicecatalog_puppet.commands import graph
from servicecatalog_puppet.commands.task_reference_helpers.generators import generator
from servicecatalog_puppet.commands.task_reference_helpers.generators.boto3_parameter_handler import (
boto3_parameter_handler,
)
from servicecatalog_puppet.workflow import workflow_utils
from servicecatalog_puppet.workflow.dependencies import resources_factory

Expand Down Expand Up @@ -288,86 +291,13 @@ def generate(puppet_account_id, manifest, output_file_path):
ssm_parameter_task_reference
)
# HANDLE BOTO3 PARAMS
if parameter_details.get("boto3"):
boto3_parameter_details = parameter_details.get("boto3")
account_id_to_use_for_boto3_call = (
str(
boto3_parameter_details.get("account_id", puppet_account_id)
)
.replace("${AWS::AccountId}", task.get("account_id"))
.replace("${AWS::PuppetAccountId}", puppet_account_id)
)
region_to_use_for_boto3_call = boto3_parameter_details.get(
"region", constants.HOME_REGION
).replace("${AWS::Region}", task.get("region"))

dependencies = list()
if parameter_details.get("cloudformation_stack_output"):
cloudformation_stack_output = parameter_details[
"cloudformation_stack_output"
]
stack_ref_account_id = (
str(cloudformation_stack_output.get("account_id"))
.replace("${AWS::AccountId}", task.get("account_id"))
.replace("${AWS::PuppetAccountId}", puppet_account_id)
)
stack_ref_region = cloudformation_stack_output.get(
"region"
).replace("${AWS::Region}", task.get("region"))
stack_ref_stack = cloudformation_stack_output.get("stack_name")
stack_ref = f"{constants.STACKS}_{stack_ref_stack}_{stack_ref_account_id}-{stack_ref_region}"
if all_tasks.get(stack_ref):
dependencies.append(stack_ref)
section_name_to_use = constants.STACKS
item_name_to_use = stack_ref_stack

boto3_parameter_task_reference = f"{constants.BOTO3_PARAMETERS}-{section_name_to_use}-{item_name_to_use}-{parameter_name}-{account_id_to_use_for_boto3_call}-{region_to_use_for_boto3_call}"
task_execution = task.get(
"execution", constants.EXECUTION_MODE_DEFAULT
)
if task.get(task_execution) in [
constants.EXECUTION_MODE_HUB,
constants.EXECUTION_MODE_ASYNC,
]:
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"
)
if not new_tasks.get(boto3_parameter_task_reference):
new_tasks[boto3_parameter_task_reference] = dict(
status=task.get("status"),
execution=task_execution,
task_reference=boto3_parameter_task_reference,
dependencies_by_reference=dependencies,
dependencies=list(),
manifest_section_names=dict(),
manifest_item_names=dict(),
manifest_account_ids=dict(),
account_id=account_id_to_use_for_boto3_call,
region=region_to_use_for_boto3_call,
arguments=boto3_parameter_details.get("arguments"),
call=boto3_parameter_details.get("call"),
client=boto3_parameter_details.get("client"),
filter=boto3_parameter_details.get("filter"),
use_paginator=boto3_parameter_details.get("use_paginator"),
section_name=constants.BOTO3_PARAMETERS,
)

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

task["dependencies_by_reference"].append(
boto3_parameter_task_reference
)
boto3_parameter_handler(
new_tasks,
parameter_details,
parameter_name,
puppet_account_id,
task,
)

all_tasks.update(new_tasks)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

from servicecatalog_puppet import constants


def boto3_parameter_handler(
new_tasks, parameter_details, parameter_name, puppet_account_id, task
):
if parameter_details.get("boto3"):
boto3_parameter_details = parameter_details.get("boto3")
account_id_to_use_for_boto3_call = (
str(boto3_parameter_details.get("account_id", puppet_account_id))
.replace("${AWS::AccountId}", task.get("account_id"))
.replace("${AWS::PuppetAccountId}", puppet_account_id)
)
region_to_use_for_boto3_call = boto3_parameter_details.get(
"region", constants.HOME_REGION
).replace("${AWS::Region}", task.get("region"))

boto3_parameter_task_reference = (
f"{constants.BOTO3_PARAMETERS}"
f"-{task.get('section_name')}"
f"-{task.get('item_name')}"
f"-{parameter_name}"
f"-{account_id_to_use_for_boto3_call}"
f"-{region_to_use_for_boto3_call}"
)
task_execution = task.get("execution", constants.EXECUTION_MODE_DEFAULT)
if task.get(task_execution) in [
constants.EXECUTION_MODE_HUB,
constants.EXECUTION_MODE_ASYNC,
]:
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"
)
if not new_tasks.get(boto3_parameter_task_reference):
new_tasks[boto3_parameter_task_reference] = dict(
status=task.get("status"),
execution=task_execution,
task_reference=boto3_parameter_task_reference,
dependencies_by_reference=[],
dependencies=[],
manifest_section_names=dict(),
manifest_item_names=dict(),
manifest_account_ids=dict(),
account_id=account_id_to_use_for_boto3_call,
region=region_to_use_for_boto3_call,
arguments=boto3_parameter_details.get("arguments"),
call=boto3_parameter_details.get("call"),
client=boto3_parameter_details.get("client"),
filter=boto3_parameter_details.get("filter"),
use_paginator=boto3_parameter_details.get("use_paginator"),
section_name=constants.BOTO3_PARAMETERS,
)

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

task["dependencies_by_reference"].append(boto3_parameter_task_reference)
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

import unittest

from servicecatalog_puppet import constants


class CompleteGeneratorTest(unittest.TestCase):
def setUp(self):
self.maxDiff = None
from servicecatalog_puppet.commands.task_reference_helpers.generators import (
boto3_parameter_handler,
)

self.sut = boto3_parameter_handler

def test_for_boto3_parameters(self):
# setup
puppet_account_id = "hub_account_id"
account_id = "spoke_account_id"
region = "eu-west-0"
item_name = "depsrefactor"
dependency_item_name = "something_else"

section_name = constants.STACKS
task = {
"stack_name": item_name,
"launch_name": "",
"stack_set_name": "",
"capabilities": ["CAPABILITY_NAMED_IAM"],
"ssm_param_outputs": [],
"bucket": f"sc-puppet-stacks-repository-{puppet_account_id}",
"key": f"stack/{item_name}/v1/stack.template-${{AWS::Region}}.yaml",
"version_id": "",
"execution": "spoke",
"use_service_role": True,
"tags": [],
"puppet_account_id": puppet_account_id,
"status": None,
"requested_priority": 0,
"dependencies": [
{"affinity": "stack", "name": dependency_item_name, "type": "stack",}
],
"account_id": account_id,
"region": region,
"manifest_section_names": {"stacks": True},
"manifest_item_names": {item_name: True},
"manifest_account_ids": {account_id: True},
"section_name": "stacks",
"item_name": item_name,
"dependencies_by_reference": [
"create-policies",
f"prepare-account-for-stacks-{account_id}",
f"get-template-from-s3-stacks-{item_name}",
],
"task_reference": f"stacks_ccoe-{item_name}_{account_id}_{region}",
"get_s3_template_ref": f"get-template-from-s3-stacks-{item_name}",
}
task_reference = task["task_reference"]
new_tasks = {task_reference: task}

describe_stacks_filter = "xyz"

parameter_details = {
"boto3": {
"account_id": "${AWS::AccountId}",
"arguments": {"StackName": item_name},
"call": "describe_stacks",
"client": "cloudformation",
"filter": describe_stacks_filter,
"region": "${AWS::Region}",
"use_paginator": True,
},
"cloudformation_stack_output": {
"account_id": "${AWS::AccountId}",
"output_key": "SomeOutputKey",
"region": "${AWS::Region}",
"stack_name": dependency_item_name,
},
}

parameter_name = "some_parameter"

# exercise
self.sut.boto3_parameter_handler(
new_tasks, parameter_details, parameter_name, puppet_account_id, task,
)

# verify
n_new_tasks = len(new_tasks.keys())
self.assertEqual(
[
"create-policies",
f"prepare-account-for-stacks-{account_id}",
f"get-template-from-s3-stacks-{item_name}",
f"{constants.BOTO3_PARAMETERS}-{section_name}-{item_name}-{parameter_name}-{account_id}-{region}",
],
new_tasks[task_reference].get("dependencies_by_reference"),
"assert new dependency is added to the task needing the parameter",
)

expected_boto3_task_ref = f"{constants.BOTO3_PARAMETERS}-{section_name}-{item_name}-{parameter_name}-{account_id}-{region}"
self.assertEqual(
{
"status": None,
"execution": "spoke",
"task_reference": expected_boto3_task_ref,
"dependencies_by_reference": [],
"dependencies": [
{"affinity": "stack", "name": dependency_item_name, "type": "stack"}
],
"manifest_section_names": {constants.STACKS: True},
"manifest_item_names": {item_name: True},
"manifest_account_ids": {account_id: True},
"account_id": account_id,
"region": region,
"arguments": {"StackName": item_name},
"call": "describe_stacks",
"client": "cloudformation",
"filter": describe_stacks_filter,
"use_paginator": True,
"section_name": constants.BOTO3_PARAMETERS,
},
new_tasks[expected_boto3_task_ref],
"assert the boto3 task is generated correctly",
)

self.assertEqual(2, n_new_tasks, "assert the correct number of tasks exist")
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@

setup_kwargs = {
'name': 'aws-service-catalog-puppet',
'version': '0.228.0',
'version': '0.228.1',
'description': 'Making it easier to deploy ServiceCatalog products',
'long_description': '# aws-service-catalog-puppet\n\n![logo](./docs/logo.png) \n\n## Badges\n\n[![codecov](https://codecov.io/gh/awslabs/aws-service-catalog-puppet/branch/master/graph/badge.svg?token=e8M7mdsmy0)](https://codecov.io/gh/awslabs/aws-service-catalog-puppet)\n\n\n## What is it?\nThis is a python3 framework that makes it easier to share multi region AWS Service Catalog portfolios and makes it \npossible to provision products into accounts declaratively using a metadata based rules engine.\n\nWith this framework you define your accounts in a YAML file. You give each account a set of tags, a default region and \na set of enabled regions.\n\nOnce you have done this you can define portfolios should be shared with each set of accounts using the tags and you \ncan specify which regions the shares occur in.\n\nIn addition to this, you can also define products that should be provisioned into accounts using the same tag based \napproach. The framework will assume role into the target account and provision the product on your behalf.\n\n\n## Getting started\n\nYou can read the [installation how to](https://service-catalog-tools-workshop.com/30-how-tos/10-installation/30-service-catalog-puppet.html)\nor you can read through the [every day use](https://service-catalog-tools-workshop.com/30-how-tos/50-every-day-use.html)\nguides.\n\nYou can read the [documentation](https://aws-service-catalog-puppet.readthedocs.io/en/latest/) to understand the inner \nworkings. \n\n\n## Going further\n\nThe framework is one of a pair. The other is [aws-service-catalog-factory](https://github.com/awslabs/aws-service-catalog-factory).\nWith Service Catalog Factory you can create pipelines that deploy multi region portfolios very easily. \n\n## License\n\nThis library is licensed under the Apache 2.0 License. \n \n',
'author': 'Eamonn Faherty',
Expand Down

0 comments on commit 3a94153

Please sign in to comment.