Skip to content

Commit

Permalink
Added resource_update constraint allowing tag_update_on_provisioned_p…
Browse files Browse the repository at this point in the history
…roduct (issue #527)

* Added resource_update constraint allowing tag_update_on_provisioned_product
  • Loading branch information
eamonnfaherty committed Jun 30, 2022
1 parent 3db2550 commit 916ace7
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 8 deletions.
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.177.0"
version = "0.178.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
3 changes: 3 additions & 0 deletions servicecatalog_puppet/manifest_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,9 @@ def get_tasks_for(
sharing_mode=item.get("sharing_mode", constants.SHARING_MODE_DEFAULT),
associations=item.get("associations", list()),
launch_constraints=item.get("constraints", {}).get("launch", []),
resource_update_constraints=item.get("constraints", {}).get(
"resource_update", []
),
portfolio=item.get("portfolio"),
),
"lambda-invocations": dict(
Expand Down
1 change: 1 addition & 0 deletions servicecatalog_puppet/manifest_utils_unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def test_get_provisioning_tasks_for_spoke_local_portfolios_and_region_for_tags(
"region": region,
"associations": [],
"launch_constraints": [],
"resource_update_constraints": [],
"organization": "",
"product_generation_method": "copy",
"sharing_mode": "ACCOUNT",
Expand Down
14 changes: 12 additions & 2 deletions servicecatalog_puppet/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -221,18 +221,28 @@ spoke-local-portfolio:
deploy_to: any(include('tags'), include('accounts'))

constraints:
launch: list(include('constraints_launch'))
launch: list(include('constraints_launch'), required=False)
update_resource: list(include('constraints_update_resource'), required=False)

constraints_launch: any(include('constraints_launch_for_product'), include('constraints_launch_for_products'))
constraints_update_resource: any(include('constraints_update_resource_for_product'), include('constraints_update_resource_for_products'))

constraints_launch_for_product:
product: str()
roles: list(include('constraints_launch_role_arns'))

constraints_launch_for_products:
products: list(str())
products: any(str(), list(str()))
roles: list(include('constraints_launch_role_arns'))

constraints_update_resource_for_product:
product: str()
tag_update_on_provisioned_product: enum("ALLOWED", "NOT_ALLOWED")

constraints_update_resource_for_products:
products: any(str(), list(str()))
tag_update_on_provisioned_product: enum("ALLOWED", "NOT_ALLOWED")

associations_arns: str()
constraints_launch_role_arns: str()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: update resource constraints for {{portfolio.DisplayName}}

Conditions:
NoOpCondition: !Equals [ true, false]

Resources:
NoOpResource:
Type: AWS::S3::Bucket
Description: Resource to ensure that template contains a resource even when there are no shares
Condition: NoOpCondition

{% for update_resource_constraint in update_resource_constraints %}{% for product in update_resource_constraint.products %}
#{{ product }}
L{{ portfolio_id|replace("-", "") }}B{{ product_name_to_id_dict.get(product)|replace("-", "") }}C:
Type: AWS::ServiceCatalog::ResourceUpdateConstraint
Properties:
PortfolioId: {{ portfolio_id }}
ProductId: {{ product_name_to_id_dict.get(product) }}
Description: "TagUpdate = {{update_resource_constraint.get("tag_update_on_provisioned_product")}}"
TagUpdateOnProvisionedProduct: {{update_resource_constraint.get("tag_update_on_provisioned_product")}}{% endfor %}{% endfor %}


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

import re

import luigi

from servicecatalog_puppet import config
from servicecatalog_puppet import utils
from servicecatalog_puppet.workflow.general import delete_cloud_formation_stack_task
from servicecatalog_puppet.workflow.portfolio.portfolio_management import (
copy_into_spoke_local_portfolio_task,
)
from servicecatalog_puppet.workflow.portfolio.portfolio_management import (
import_into_spoke_local_portfolio_task,
)
from servicecatalog_puppet.workflow.portfolio.portfolio_management import (
portfolio_management_task,
)


class CreateUpdateResourceConstraintsForSpokeLocalPortfolioTask(
portfolio_management_task.PortfolioManagementTask
):
spoke_local_portfolio_name = luigi.Parameter()
account_id = luigi.Parameter()
region = luigi.Parameter()
portfolio = luigi.Parameter()
puppet_account_id = luigi.Parameter()
organization = luigi.Parameter()
product_generation_method = luigi.Parameter()
update_resource_constraints = luigi.DictParameter()

sharing_mode = luigi.Parameter()

def params_for_results_display(self):
return {
"puppet_account_id": self.puppet_account_id,
"spoke_local_portfolio_name": self.spoke_local_portfolio_name,
"portfolio": self.portfolio,
"region": self.region,
"account_id": self.account_id,
"cache_invalidator": self.cache_invalidator,
}

def requires(self):
create_spoke_local_portfolio_task_klass = (
import_into_spoke_local_portfolio_task.ImportIntoSpokeLocalPortfolioTask
if self.product_generation_method == "import"
else copy_into_spoke_local_portfolio_task.CopyIntoSpokeLocalPortfolioTask
)

return dict(
create_spoke_local_portfolio_task=create_spoke_local_portfolio_task_klass(
spoke_local_portfolio_name=self.spoke_local_portfolio_name,
manifest_file_path=self.manifest_file_path,
account_id=self.account_id,
region=self.region,
portfolio=self.portfolio,
organization=self.organization,
puppet_account_id=self.puppet_account_id,
sharing_mode=self.sharing_mode,
),
)

def api_calls_used(self):
return [
f"cloudformation.ensure_deleted_{self.account_id}_{self.region}",
f"cloudformation.describe_stacks_{self.account_id}_{self.region}",
f"cloudformation.create_or_update_{self.account_id}_{self.region}",
f"service_catalog.search_products_as_admin_{self.account_id}_{self.region}",
]

def run(self):
dependency_output = self.load_from_input("create_spoke_local_portfolio_task")
spoke_portfolio = dependency_output.get("portfolio")
portfolio_id = spoke_portfolio.get("Id")

product_name_to_id_dict = dependency_output.get("products")
with self.spoke_regional_client("cloudformation") as cloudformation:
new_constraints = self.generate_new_constraints(
portfolio_id, product_name_to_id_dict
)

template = config.env.get_template(
"update_resource_constraints.template.yaml.j2"
).render(
portfolio={"DisplayName": self.portfolio,},
portfolio_id=portfolio_id,
update_resource_constraints=new_constraints,
product_name_to_id_dict=product_name_to_id_dict,
)
stack_name = f"update-resource-constraints-for-{utils.slugify_for_cloudformation_stack_name(self.spoke_local_portfolio_name)}"
cloudformation.create_or_update(
StackName=stack_name,
TemplateBody=template,
NotificationARNs=[
f"arn:{config.get_partition()}:sns:{self.region}:{self.puppet_account_id}:servicecatalog-puppet-cloudformation-regional-events"
]
if self.should_use_sns
else [],
ShouldDeleteRollbackComplete=self.should_delete_rollback_complete_stacks,
Tags=self.initialiser_stack_tags,
)
result = cloudformation.describe_stacks(StackName=stack_name,).get(
"Stacks"
)[0]
self.write_output(result)

def generate_new_constraints(self, portfolio_id, product_name_to_id_dict):
new_constraints = []
for constraint in self.update_resource_constraints:
new_constraint = {
"products": [],
"tag_update_on_provisioned_product": constraint.get(
"tag_update_on_provisioned_product"
),
}
if constraint.get("products", None) is not None:
if isinstance(constraint.get("products"), tuple):
new_constraint["products"] += constraint.get("products")
elif isinstance(constraint.get("products"), str):
with self.spoke_regional_client(
"servicecatalog"
) as service_catalog:
response = service_catalog.search_products_as_admin_single_page(
PortfolioId=portfolio_id
)
for product_view_details in response.get(
"ProductViewDetails", []
):
product_view_summary = product_view_details.get(
"ProductViewSummary"
)
product_name_to_id_dict[
product_view_summary.get("Name")
] = product_view_summary.get("ProductId")
if re.match(
constraint.get("products"),
product_view_summary.get("Name"),
):
new_constraint["products"].append(
product_view_summary.get("Name")
)
else:
raise Exception(
f'Unexpected launch constraint type {type(constraint.get("products"))}'
)

if constraint.get("product", None) is not None:
new_constraint["products"].append(constraint.get("product"))

new_constraints.append(new_constraint)
return new_constraints
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from servicecatalog_puppet.workflow.portfolio.constraints_management import (
create_launch_role_constraints_for_spoke_local_portfolio_task,
create_resource_update_constraints_for_spoke_local_portfolio_task,
)
from servicecatalog_puppet.workflow.portfolio.portfolio_management import (
copy_into_spoke_local_portfolio_task,
Expand Down Expand Up @@ -43,6 +44,7 @@ class DoSharePortfolioWithSpokeTask(
organization = luigi.Parameter()
associations = luigi.ListParameter()
launch_constraints = luigi.DictParameter()
resource_update_constraints = luigi.DictParameter()
portfolio = luigi.Parameter()
region = luigi.Parameter()
account_id = luigi.Parameter()
Expand Down Expand Up @@ -127,18 +129,29 @@ def requires(self):

launch_constraints = task_def.get("constraints", {}).get("launch", [])
if len(launch_constraints) > 0:
create_launch_role_constraints_for_portfolio_task_params = dict(
create_launch_role_constraints_for_portfolio = create_launch_role_constraints_for_spoke_local_portfolio_task.CreateLaunchRoleConstraintsForSpokeLocalPortfolioTask(
**create_spoke_local_portfolio_task_as_dependency_params,
launch_constraints=launch_constraints,
puppet_account_id=self.puppet_account_id,
spoke_local_portfolio_name=self.spoke_local_portfolio_name,
sharing_mode=sharing_mode,
product_generation_method=product_generation_method,
)
create_launch_role_constraints_for_portfolio = create_launch_role_constraints_for_spoke_local_portfolio_task.CreateLaunchRoleConstraintsForSpokeLocalPortfolioTask(
tasks.append(create_launch_role_constraints_for_portfolio)

update_resource_constraints = task_def.get("constraints", {}).get(
"resource_update", []
)
if len(update_resource_constraints) > 0:
create_update_resource_constraints_for_portfolio = create_resource_update_constraints_for_spoke_local_portfolio_task.CreateUpdateResourceConstraintsForSpokeLocalPortfolioTask(
**create_spoke_local_portfolio_task_as_dependency_params,
**create_launch_role_constraints_for_portfolio_task_params,
update_resource_constraints=update_resource_constraints,
puppet_account_id=self.puppet_account_id,
spoke_local_portfolio_name=self.spoke_local_portfolio_name,
sharing_mode=sharing_mode,
product_generation_method=product_generation_method,
)
tasks.append(create_launch_role_constraints_for_portfolio)
tasks.append(create_update_resource_constraints_for_portfolio)

if product_generation_method == "import":
tasks.append(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class DoSharePortfolioWithSpokeTaskTest(tasks_unit_tests_helper.PuppetTaskUnitTe
organization = "organization"
associations = []
launch_constraints = {}
resource_update_constraints = {}
portfolio = "portfolio"
region = "region"
account_id = "account_id"
Expand All @@ -35,6 +36,7 @@ def setUp(self) -> None:
organization=self.organization,
associations=self.associations,
launch_constraints=self.launch_constraints,
resource_update_constraints=self.resource_update_constraints,
portfolio=self.portfolio,
region=self.region,
account_id=self.account_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SharePortfolioWithSpokeTask(
organization = luigi.Parameter()
associations = luigi.ListParameter()
launch_constraints = luigi.DictParameter()
resource_update_constraints = luigi.DictParameter()
portfolio = luigi.Parameter()
region = luigi.Parameter()
account_id = luigi.Parameter()
Expand All @@ -53,6 +54,7 @@ def run(self):
organization=self.organization,
associations=self.associations,
launch_constraints=self.launch_constraints,
resource_update_constraints=self.resource_update_constraints,
portfolio=self.portfolio,
region=self.region,
account_id=self.account_id,
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@

setup_kwargs = {
'name': 'aws-service-catalog-puppet',
'version': '0.177.0',
'version': '0.178.0',
'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 916ace7

Please sign in to comment.