Skip to content

Commit

Permalink
chore: DBTP-1619 - refactor environment command to classes (#694)
Browse files Browse the repository at this point in the history
  • Loading branch information
ksugden authored Dec 16, 2024
1 parent 7fa168d commit 38f04b8
Show file tree
Hide file tree
Showing 9 changed files with 546 additions and 437 deletions.
3 changes: 0 additions & 3 deletions dbt_platform_helper/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,9 +551,6 @@ platform-helper pipeline generate
This command does the following in relation to the codebase pipelines:
- Generates the copilot pipeline manifest.yml for copilot/pipelines/<codebase_pipeline_name>

(Deprecated) This command does the following for non terraform projects (legacy AWS Copilot):
- Generates the copilot manifest.yml for copilot/environments/<environment>

## Usage

```
Expand Down
186 changes: 12 additions & 174 deletions dbt_platform_helper/commands/environment.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import boto3
import click
from schema import SchemaError

from dbt_platform_helper.constants import DEFAULT_TERRAFORM_PLATFORM_MODULES_VERSION
from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
from dbt_platform_helper.domain.copilot_environment import CopilotEnvironment
from dbt_platform_helper.domain.maintenance_page import MaintenancePageProvider
from dbt_platform_helper.domain.terraform_environment import TerraformEnvironment
from dbt_platform_helper.platform_exception import PlatformException
from dbt_platform_helper.providers.load_balancers import find_https_listener
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
from dbt_platform_helper.utils.click import ClickDocOptGroup
from dbt_platform_helper.utils.files import apply_environment_defaults
from dbt_platform_helper.utils.files import mkfile
from dbt_platform_helper.utils.template import setup_templates
from dbt_platform_helper.utils.validation import load_and_validate_platform_config
from dbt_platform_helper.utils.versioning import (
check_platform_helper_version_needs_update,
Expand Down Expand Up @@ -39,97 +35,24 @@ def environment():
@click.option("--vpc", type=str)
def offline(app, env, svc, template, vpc):
"""Take load-balanced web services offline with a maintenance page."""
maintenance_page = MaintenancePageProvider()
maintenance_page.activate(app, env, svc, template, vpc)
try:
MaintenancePageProvider().activate(app, env, svc, template, vpc)
except PlatformException as err:
click.secho(str(err), fg="red")
raise click.Abort


@environment.command()
@click.option("--app", type=str, required=True)
@click.option("--env", type=str, required=True)
def online(app, env):
"""Remove a maintenance page from an environment."""
maintenance_page = MaintenancePageProvider()
maintenance_page.deactivate(app, env)


def get_vpc_id(session, env_name, vpc_name=None):
if not vpc_name:
vpc_name = f"{session.profile_name}-{env_name}"

filters = [{"Name": "tag:Name", "Values": [vpc_name]}]
vpcs = session.client("ec2").describe_vpcs(Filters=filters)["Vpcs"]

if not vpcs:
filters[0]["Values"] = [session.profile_name]
vpcs = session.client("ec2").describe_vpcs(Filters=filters)["Vpcs"]

if not vpcs:
click.secho(
f"No VPC found with name {vpc_name} in AWS account {session.profile_name}.", fg="red"
)
raise click.Abort

return vpcs[0]["VpcId"]


def get_subnet_ids(session, vpc_id, environment_name):
subnets = session.client("ec2").describe_subnets(
Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]
)["Subnets"]

if not subnets:
click.secho(f"No subnets found for VPC with id: {vpc_id}.", fg="red")
raise click.Abort

public_tag = {"Key": "subnet_type", "Value": "public"}
public_subnets = [subnet["SubnetId"] for subnet in subnets if public_tag in subnet["Tags"]]
private_tag = {"Key": "subnet_type", "Value": "private"}
private_subnets = [subnet["SubnetId"] for subnet in subnets if private_tag in subnet["Tags"]]

# This call and the method declaration can be removed when we stop using AWS Copilot to deploy the services
public_subnets, private_subnets = _match_subnet_id_order_to_cloudformation_exports(
session,
environment_name,
public_subnets,
private_subnets,
)

return public_subnets, private_subnets


def _match_subnet_id_order_to_cloudformation_exports(
session, environment_name, public_subnets, private_subnets
):
public_subnet_exports = []
private_subnet_exports = []
for page in session.client("cloudformation").get_paginator("list_exports").paginate():
for export in page["Exports"]:
if f"-{environment_name}-" in export["Name"]:
if export["Name"].endswith("-PublicSubnets"):
public_subnet_exports = export["Value"].split(",")
if export["Name"].endswith("-PrivateSubnets"):
private_subnet_exports = export["Value"].split(",")

# If the elements match, regardless of order, use the list from the CloudFormation exports
if set(public_subnets) == set(public_subnet_exports):
public_subnets = public_subnet_exports
if set(private_subnets) == set(private_subnet_exports):
private_subnets = private_subnet_exports

return public_subnets, private_subnets


def get_cert_arn(session, application, env_name):
try:
arn = find_https_certificate(session, application, env_name)
except:
click.secho(
f"No certificate found with domain name matching environment {env_name}.", fg="red"
)
MaintenancePageProvider().deactivate(app, env)
except PlatformException as err:
click.secho(str(err), fg="red")
raise click.Abort

return arn


@environment.command()
@click.option("--vpc-name", hidden=True)
Expand All @@ -141,19 +64,13 @@ def generate(name, vpc_name):
fg="red",
)
raise click.Abort

try:
conf = load_and_validate_platform_config()
except SchemaError as ex:
click.secho(f"Invalid `{PLATFORM_CONFIG_FILE}` file: {str(ex)}", fg="red")
raise click.Abort

env_config = apply_environment_defaults(conf)["environments"][name]
profile_for_environment = env_config.get("accounts", {}).get("deploy", {}).get("name")
click.secho(f"Using {profile_for_environment} for this AWS session")
session = get_aws_session_or_abort(profile_for_environment)

_generate_copilot_environment_manifests(name, conf["application"], env_config, session)
CopilotEnvironment.generate(conf, name)


@environment.command(help="Generate terraform manifest for the specified environment.")
Expand All @@ -166,83 +83,4 @@ def generate(name, vpc_name):
)
def generate_terraform(name, terraform_platform_modules_version):
conf = load_and_validate_platform_config()

env_config = apply_environment_defaults(conf)["environments"][name]
_generate_terraform_environment_manifests(
conf["application"], name, env_config, terraform_platform_modules_version
)


def _generate_copilot_environment_manifests(environment_name, application, env_config, session):
env_template = setup_templates().get_template("env/manifest.yml")
vpc_name = env_config.get("vpc", None)
vpc_id = get_vpc_id(session, environment_name, vpc_name)
pub_subnet_ids, priv_subnet_ids = get_subnet_ids(session, vpc_id, environment_name)
cert_arn = get_cert_arn(session, application, environment_name)
contents = env_template.render(
{
"name": environment_name,
"vpc_id": vpc_id,
"pub_subnet_ids": pub_subnet_ids,
"priv_subnet_ids": priv_subnet_ids,
"certificate_arn": cert_arn,
}
)
click.echo(
mkfile(
".", f"copilot/environments/{environment_name}/manifest.yml", contents, overwrite=True
)
)


def _generate_terraform_environment_manifests(
application, env, env_config, cli_terraform_platform_modules_version
):
env_template = setup_templates().get_template("environments/main.tf")

terraform_platform_modules_version = _determine_terraform_platform_modules_version(
env_config, cli_terraform_platform_modules_version
)

contents = env_template.render(
{
"application": application,
"environment": env,
"config": env_config,
"terraform_platform_modules_version": terraform_platform_modules_version,
}
)

click.echo(mkfile(".", f"terraform/environments/{env}/main.tf", contents, overwrite=True))


def _determine_terraform_platform_modules_version(env_conf, cli_terraform_platform_modules_version):
cli_terraform_platform_modules_version = cli_terraform_platform_modules_version
env_conf_terraform_platform_modules_version = env_conf.get("versions", {}).get(
"terraform-platform-modules"
)
version_preference_order = [
cli_terraform_platform_modules_version,
env_conf_terraform_platform_modules_version,
DEFAULT_TERRAFORM_PLATFORM_MODULES_VERSION,
]
return [version for version in version_preference_order if version][0]


def find_https_certificate(session: boto3.Session, app: str, env: str) -> str:
listener_arn = find_https_listener(session, app, env)
cert_client = session.client("elbv2")
certificates = cert_client.describe_listener_certificates(ListenerArn=listener_arn)[
"Certificates"
]

try:
certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
except StopIteration:
raise CertificateNotFoundException()

return certificate_arn


class CertificateNotFoundException(PlatformException):
pass
TerraformEnvironment.generate(conf, name, terraform_platform_modules_version)
3 changes: 0 additions & 3 deletions dbt_platform_helper/commands/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ def generate(terraform_platform_modules_version, deploy_branch):
This command does the following in relation to the codebase pipelines:
- Generates the copilot pipeline manifest.yml for copilot/pipelines/<codebase_pipeline_name>
(Deprecated) This command does the following for non terraform projects (legacy AWS Copilot):
- Generates the copilot manifest.yml for copilot/environments/<environment>
"""
pipeline_config = load_and_validate_platform_config()

Expand Down
145 changes: 145 additions & 0 deletions dbt_platform_helper/domain/copilot_environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import boto3
import click

from dbt_platform_helper.platform_exception import PlatformException
from dbt_platform_helper.providers.load_balancers import find_https_listener
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
from dbt_platform_helper.utils.files import apply_environment_defaults
from dbt_platform_helper.utils.files import mkfile
from dbt_platform_helper.utils.template import setup_templates


# TODO - move helper functions into suitable provider classes
def get_subnet_ids(session, vpc_id, environment_name):
subnets = session.client("ec2").describe_subnets(
Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]
)["Subnets"]

if not subnets:
click.secho(f"No subnets found for VPC with id: {vpc_id}.", fg="red")
raise click.Abort

public_tag = {"Key": "subnet_type", "Value": "public"}
public_subnets = [subnet["SubnetId"] for subnet in subnets if public_tag in subnet["Tags"]]
private_tag = {"Key": "subnet_type", "Value": "private"}
private_subnets = [subnet["SubnetId"] for subnet in subnets if private_tag in subnet["Tags"]]

# This call and the method declaration can be removed when we stop using AWS Copilot to deploy the services
public_subnets, private_subnets = _match_subnet_id_order_to_cloudformation_exports(
session,
environment_name,
public_subnets,
private_subnets,
)

return public_subnets, private_subnets


def _match_subnet_id_order_to_cloudformation_exports(
session, environment_name, public_subnets, private_subnets
):
public_subnet_exports = []
private_subnet_exports = []
for page in session.client("cloudformation").get_paginator("list_exports").paginate():
for export in page["Exports"]:
if f"-{environment_name}-" in export["Name"]:
if export["Name"].endswith("-PublicSubnets"):
public_subnet_exports = export["Value"].split(",")
if export["Name"].endswith("-PrivateSubnets"):
private_subnet_exports = export["Value"].split(",")

# If the elements match, regardless of order, use the list from the CloudFormation exports
if set(public_subnets) == set(public_subnet_exports):
public_subnets = public_subnet_exports
if set(private_subnets) == set(private_subnet_exports):
private_subnets = private_subnet_exports

return public_subnets, private_subnets


def get_cert_arn(session, application, env_name):
try:
arn = find_https_certificate(session, application, env_name)
except:
click.secho(
f"No certificate found with domain name matching environment {env_name}.", fg="red"
)
raise click.Abort

return arn


def get_vpc_id(session, env_name, vpc_name=None):
if not vpc_name:
vpc_name = f"{session.profile_name}-{env_name}"

filters = [{"Name": "tag:Name", "Values": [vpc_name]}]
vpcs = session.client("ec2").describe_vpcs(Filters=filters)["Vpcs"]

if not vpcs:
filters[0]["Values"] = [session.profile_name]
vpcs = session.client("ec2").describe_vpcs(Filters=filters)["Vpcs"]

if not vpcs:
click.secho(
f"No VPC found with name {vpc_name} in AWS account {session.profile_name}.", fg="red"
)
raise click.Abort

return vpcs[0]["VpcId"]


def _generate_copilot_environment_manifests(
environment_name, application_name, env_config, session
):
env_template = setup_templates().get_template("env/manifest.yml")
vpc_name = env_config.get("vpc", None)
vpc_id = get_vpc_id(session, environment_name, vpc_name)
pub_subnet_ids, priv_subnet_ids = get_subnet_ids(session, vpc_id, environment_name)
cert_arn = get_cert_arn(session, application_name, environment_name)
contents = env_template.render(
{
"name": environment_name,
"vpc_id": vpc_id,
"pub_subnet_ids": pub_subnet_ids,
"priv_subnet_ids": priv_subnet_ids,
"certificate_arn": cert_arn,
}
)
click.echo(
mkfile(
".", f"copilot/environments/{environment_name}/manifest.yml", contents, overwrite=True
)
)


def find_https_certificate(session: boto3.Session, app: str, env: str) -> str:
listener_arn = find_https_listener(session, app, env)
cert_client = session.client("elbv2")
certificates = cert_client.describe_listener_certificates(ListenerArn=listener_arn)[
"Certificates"
]

try:
certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
except StopIteration:
raise CertificateNotFoundException()

return certificate_arn


class CertificateNotFoundException(PlatformException):
pass


class CopilotEnvironment:
@staticmethod
def generate(platform_config, environment_name):
env_config = apply_environment_defaults(platform_config)["environments"][environment_name]
profile_for_environment = env_config.get("accounts", {}).get("deploy", {}).get("name")
click.secho(f"Using {profile_for_environment} for this AWS session")
session = get_aws_session_or_abort(profile_for_environment)

_generate_copilot_environment_manifests(
environment_name, platform_config["application"], env_config, session
)
Loading

0 comments on commit 38f04b8

Please sign in to comment.