From 638876381c91f21ebc229e67db04e78c96101a75 Mon Sep 17 00:00:00 2001 From: Stefan Reimer Date: Tue, 3 Dec 2024 12:49:16 +0000 Subject: [PATCH] feat: add list_stacks command to list existing pulumi stacks --- cloudbender/cli.py | 17 +++++++++-- cloudbender/core.py | 16 ----------- cloudbender/pulumi.py | 2 ++ cloudbender/stack.py | 2 +- cloudbender/stackgroup.py | 60 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 +- 6 files changed, 78 insertions(+), 22 deletions(-) diff --git a/cloudbender/cli.py b/cloudbender/cli.py index 2ded162..21e17cf 100644 --- a/cloudbender/cli.py +++ b/cloudbender/cli.py @@ -55,12 +55,13 @@ def cli(ctx, profile, region, debug, directory): sys.exit(1) # Only load stackgroups to get profile and region - if ctx.invoked_subcommand == "wrap": + if ctx.invoked_subcommand in ["wrap", "list_stacks"]: cb.read_config(loadStacks=False) else: cb.read_config() - cb.dump_config() + if debug: + cb.dump_config() ctx.obj = cb @@ -349,7 +350,16 @@ def wrap(cb, stack_group, cmd): """Execute custom external program""" sg = cb.sg.get_stackgroup(stack_group) - cb.wrap(sg, " ".join(cmd)) + sg.wrap(" ".join(cmd)) + + +@click.command() +@click.argument("stack_group", nargs=1, required=True) +@click.pass_obj +def list_stacks(cb, stack_group): + """List all Pulumi stacks""" + sg = cb.sg.get_stackgroup(stack_group) + sg.list_stacks() @click.command() @@ -499,6 +509,7 @@ cli.add_command(set_config) cli.add_command(get_config) cli.add_command(_import) cli.add_command(export) +cli.add_command(list_stacks) cli.add_command(assimilate) cli.add_command(execute) cli.add_command(wrap) diff --git a/cloudbender/core.py b/cloudbender/core.py index a2b439b..d775b59 100644 --- a/cloudbender/core.py +++ b/cloudbender/core.py @@ -1,9 +1,7 @@ import pathlib import logging -import pexpect from .stackgroup import StackGroup -from .connection import BotoConnection from .jinja import read_config_file from .exceptions import InvalidProjectDir @@ -133,17 +131,3 @@ class CloudBender(object): matching_stacks.append(s) return matching_stacks - - def wrap(self, stack_group, cmd): - """ - Set AWS environment based on profile before executing a custom command, eg. steampipe - """ - - profile = stack_group.config.get("profile", "default") - region = stack_group.config.get("region", "global") - - connection_manager = BotoConnection(profile, region) - connection_manager.exportProfileEnv() - - child = pexpect.spawn(cmd) - child.interact() diff --git a/cloudbender/pulumi.py b/cloudbender/pulumi.py index 001ca2c..cdc817a 100644 --- a/cloudbender/pulumi.py +++ b/cloudbender/pulumi.py @@ -199,6 +199,8 @@ def pulumi_ws(func): secrets_provider=secrets_provider, ) + # self.pulumi_workspace = pulumi.automation.LocalWorkspace(self.pulumi_ws_opts) + response = func(self, *args, **kwargs) # Cleanup temp workspace diff --git a/cloudbender/stack.py b/cloudbender/stack.py index 55467aa..49f11e2 100644 --- a/cloudbender/stack.py +++ b/cloudbender/stack.py @@ -987,7 +987,7 @@ class Stack(object): def assimilate(self): """Import resources into Pulumi stack""" - pulumi_stack = self._get_pulumi_stack(create=True) + pulumi_stack = self._get_pulumi_stack() # now lets import each defined resource for r in self._pulumi_code.RESOURCES: diff --git a/cloudbender/stackgroup.py b/cloudbender/stackgroup.py index 627f927..29652de 100644 --- a/cloudbender/stackgroup.py +++ b/cloudbender/stackgroup.py @@ -1,6 +1,13 @@ import logging import pprint +import pexpect +import pulumi +import tempfile +import rich.table +import rich.console + +from .connection import BotoConnection from .utils import dict_merge from .jinja import read_config_file from .stack import Stack @@ -25,7 +32,7 @@ class StackGroup(object): for sg in self.sgs: sg.dump_config() - logger.debug( + logger.info( "StackGroup {}: {}".format(self.rel_path, pprint.pformat(self.config)) ) @@ -135,3 +142,54 @@ class StackGroup(object): return s return None + + def wrap(self, cmd): + """ + Set AWS environment based on profile before executing a custom command, eg. steampipe + """ + + profile = self.config.get("profile", "default") + region = self.config.get("region", "global") + + connection_manager = BotoConnection(profile, region) + connection_manager.exportProfileEnv() + + child = pexpect.spawn(cmd) + child.interact() + + def list_stacks(self): + project_name = self.config["parameters"]["Conglomerate"] + pulumi_backend = "{}/{}/{}".format(self.config["pulumi"]["backend"], project_name, self.config["region"]) + + project_settings = pulumi.automation.ProjectSettings( + name=project_name, runtime="python", backend=pulumi.automation.ProjectBackend(url=pulumi_backend) + ) + + work_dir = tempfile.mkdtemp( + dir=tempfile.gettempdir(), prefix="cloudbender-" + ) + + # AWS setup + profile = self.config.get("profile", "default") + region = self.config.get("region", "global") + + connection_manager = BotoConnection(profile, region) + connection_manager.exportProfileEnv() + + pulumi_workspace = pulumi.automation.LocalWorkspace( + work_dir=work_dir, + project_settings=project_settings + ) + + stacks = pulumi_workspace.list_stacks() + + table = rich.table.Table(title="Pulumi stacks") + table.add_column("Name") + table.add_column("Last Update") + table.add_column("Resources") + + for s in stacks: + table.add_row(s.name, str(s.last_update), str(s.resource_count)) + + console = rich.console.Console() + console.print(table) diff --git a/pyproject.toml b/pyproject.toml index 4c9bf7a..a78af85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ description = "Deploy and maintain infrastructure in automated and trackable manner" readme = "README.md" license = "AGPL-3.0-or-later" -requires-python = ">=3.9" +requires-python = ">=3.12" dependencies = [ "boto3==1.35.70", "mock==5.1.0", @@ -21,6 +21,7 @@ dependencies = [ "python-minifier==2.11.3", "cfn-lint==1.20.1", "ruamel.yaml==0.18.6", + "rich==13.9.4", "pulumi==3.142.0", "pulumi-aws==6.61.0", "pulumi-aws-native==1.11.0",