From 6f4a11c8867d064383ac6498088d5827c6b8f378 Mon Sep 17 00:00:00 2001 From: David Boreham Date: Tue, 7 Apr 2026 10:36:21 -0600 Subject: [PATCH] Add experimental stack manage explain command --- src/stack/deploy/deployment.py | 13 ++++++- src/stack/deploy/explain.py | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 src/stack/deploy/explain.py diff --git a/src/stack/deploy/deployment.py b/src/stack/deploy/deployment.py index 8ac7439..3e755ee 100644 --- a/src/stack/deploy/deployment.py +++ b/src/stack/deploy/deployment.py @@ -35,6 +35,7 @@ ) from stack.deploy.deploy_types import DeployCommandContext from stack.deploy.deployment_context import DeploymentContext +from stack.deploy.explain import explain_op from stack.deploy.stack import Stack from stack.log import output_main from stack.util import error_exit @@ -79,6 +80,14 @@ def make_deploy_context(ctx) -> DeployCommandContext: ) +@command.command() +@click.pass_context +def explain(ctx): + """Explain the deployment (experimental)""" + ctx.obj = make_deploy_context(ctx) + explain_op(ctx) + + @command.command() @click.option( "--stay-attached/--detatch-terminal", @@ -93,7 +102,7 @@ def make_deploy_context(ctx) -> DeployCommandContext: @click.argument("extra_args", nargs=-1) # help: command: start @click.pass_context def start(ctx, stay_attached, skip_cluster_management, extra_args): - """start the stack""" + """start the deployment""" ctx.obj = make_deploy_context(ctx) services_list = list(extra_args) or None up_operation(ctx, services_list, stay_attached, skip_cluster_management) @@ -109,7 +118,7 @@ def start(ctx, stay_attached, skip_cluster_management, extra_args): @click.argument("extra_args", nargs=-1) # help: command: down @click.pass_context def stop(ctx, delete_volumes, skip_cluster_management, extra_args): - """stop the stack and remove the containers""" + """stop the deployment and remove the containers""" # TODO: add cluster name and env file here ctx.obj = make_deploy_context(ctx) down_operation(ctx, delete_volumes, extra_args, skip_cluster_management) diff --git a/src/stack/deploy/explain.py b/src/stack/deploy/explain.py new file mode 100644 index 0000000..0c1aee0 --- /dev/null +++ b/src/stack/deploy/explain.py @@ -0,0 +1,67 @@ +# Copyright © 2026 Bozeman Pass, Inc. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import yaml + +from kubernetes import client + +from stack.deploy.k8s.deploy_k8s import K8sDeployer +from stack.log import output_main +from stack.util import error_exit + + +def _k8s_object_to_yaml(obj): + api_client = client.ApiClient() + sanitized = api_client.sanitize_for_serialization(obj) + return yaml.dump(sanitized, default_flow_style=False, sort_keys=False) + + +def _print_section(title, objects): + if not objects: + return + output_main(f"--- {title} ---") + for obj in objects: + output_main(_k8s_object_to_yaml(obj)) + + +def explain_op(ctx): + deployer = ctx.obj.deployer + + if not isinstance(deployer, K8sDeployer): + error_exit("The explain command is currently only supported for k8s deployments") + + cluster_info = deployer.cluster_info + is_kind = deployer.is_kind() + + pvs = cluster_info.get_pvs() + _print_section("PersistentVolumes", pvs) + + pvcs = cluster_info.get_pvcs() + _print_section("PersistentVolumeClaims", pvcs) + + config_maps = cluster_info.get_configmaps() + _print_section("ConfigMaps", config_maps) + + deployments = cluster_info.get_deployments(image_pull_policy=None if is_kind else "Always") + _print_section("Deployments", deployments) + + services = cluster_info.get_services() + _print_section("Services", services) + + http_proxy_info = cluster_info.spec.get_http_proxy() + use_tls = http_proxy_info and not is_kind + ingress = cluster_info.get_ingress(use_tls=use_tls) + if ingress: + _print_section("Ingress", [ingress])