Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies = [
"requests",
"expandvars",
"mergedeep",
"mermaid-builder",
"termcolor",
"flake8",
"black"
Expand Down
Empty file added src/stack/chart/__init__.py
Empty file.
129 changes: 129 additions & 0 deletions src/stack/chart/chart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright © 2025 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 <http:#www.gnu.org/licenses/>.


import click

from mermaid_builder.flowchart import Chart, Node, NodeShape, Subgraph, ChartDir, ClassDef

from stack.config.util import get_config_setting
from stack.deploy.stack import resolve_stack
from stack.log import output_main


_theme = {
"super_stack": "stroke:#FFF176,fill:#FFFEEF,color:#6B5E13,stroke-width:2px,font-size:small;",
"stack": "stroke:#00C9A7,fill:#EDFDFB,color:#1A3A38,stroke-width:2px,font-size:small;",
"service": "stroke:#43E97B,fill:#F5FFF7,color:#236247,stroke-width:2px;",
"http_service": "stroke:#FFB236,fill:#FFFAF4,color:#7A5800,stroke-width:2px;",
"http_target": "stroke:#FF6363,fill:#FFF5F5,color:#7C2323,stroke-width:2px;",
"port": "stroke:#26C6DA,fill:#E6FAFB,color:#074953,stroke-width:2px,font-size:x-small;",
"volume": "stroke:#A259DF,fill:#F4EEFB,color:#320963,stroke-width:2px,font-size:x-small;",
}


@click.command()
@click.option("--stack", help="name or path of the stack", required=False)
@click.option(
"--deploy-to",
help="cluster system to deploy to (compose or k8s or k8s-kind)",
default=get_config_setting("deploy-to", "compose"),
)
@click.option("--show-ports/--no-show-ports", default=False)
@click.option("--show-http-targets/--no-show-http-targets", default=True)
@click.option("--show-volumes/--no-show-volumes", default=True)
@click.pass_context
def command(ctx, stack, deploy_to, show_ports, show_http_targets, show_volumes):
"""generate a mermaid graph of the stack"""

parent_stack = resolve_stack(stack)
chart = Chart(direction=ChartDir.RL)

for cls, style in _theme.items():
chart.add_class_def(ClassDef(cls, f"{style}"))

def add_stack(stack, show_http_targets=True, show_ports=False, show_volumes=False, parent_graph=None, parent_stack=None):
subgraph = Subgraph(stack.name)
subgraph.get_id() # we need this to be set

if stack.is_super_stack():
chart.attach_class(subgraph.title, "super_stack")
for child in stack.get_required_stacks_paths():
child = resolve_stack(child)
add_stack(child, show_http_targets, show_ports, show_volumes, parent_graph=subgraph, parent_stack=stack)
else:
chart.attach_class(subgraph.title, "stack")

for svc in stack.get_services():
svc_node = Node(id=f"{stack.name}-{svc}", title=svc, shape=NodeShape.SUBROUTINE, class_name="service")
subgraph.add_node(svc_node)

shown_http_ports = {}
if show_http_targets:
http_targets = stack.get_http_proxy_targets()
for ht in http_targets:
if ht["service"] == svc:
title = f":{ht["port"]}"
if "k8s" in deploy_to:
title = ht.get("path", "/")
if parent_stack:
http_prefix = parent_stack.http_prefix_for(stack.file_path.parent)
if http_prefix and http_prefix != "/":
title = f"{http_prefix}{title}"
else:
shown_http_ports[svc] = ht["port"]

http_node = Node(
id=f"{stack.name}-{svc}-http", title=title, shape=NodeShape.ASSYMETRIC, class_name="http_target"
)
chart.add_node(http_node)
chart.add_link_between(http_node, svc_node)
svc_node.class_name = "http_service"

if show_ports:
ports = stack.get_ports().get(svc, [])
for port in ports:
if svc in shown_http_ports and port == shown_http_ports[svc]:
continue
port_node = Node(
id=f"{stack.name}-{svc}-port-{port}", title=f":{port}", shape=NodeShape.ASSYMETRIC, class_name="port"
)
chart.add_node(port_node)
chart.add_link_between(port_node, svc_node)

if show_volumes:
volumes = stack.get_volumes().get(svc, [])
for volume in volumes:
volume_node = Node(
id=f"{stack.name}-{svc}-volume-{volume}", title=f"{volume}", shape=NodeShape.RECT_ROUND, class_name="volume"
)
subgraph.add_node(volume_node)
subgraph.add_link_between(svc_node, volume_node)

if parent_graph:
for s in parent_graph.subgraphs:
if s.id != subgraph.id:
chart.add_link_between(s.id, subgraph.id)
chart.add_link_between(subgraph.id, s.id)
parent_graph.add_subgraph(subgraph)
else:
chart.add_subgraph(subgraph)

add_stack(parent_stack, show_http_targets, show_ports, show_volumes)

out = str(chart)
for line in out.splitlines():
if "direction" not in line:
output_main(line)
4 changes: 2 additions & 2 deletions src/stack/checklist/checklist.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from stack.util import get_yaml


def constainer_dispostion(parent_stack, image_registry, git_ssh):
def container_disposition(parent_stack, image_registry, git_ssh):
ret = {}
required_stacks = parent_stack.get_required_stacks_paths()

Expand Down Expand Up @@ -149,7 +149,7 @@ def command(ctx, stack, image_registry, git_ssh):
"""check if stack containers are ready"""

stack = resolve_stack(stack)
what_needs_done = constainer_dispostion(stack, image_registry, git_ssh)
what_needs_done = container_disposition(stack, image_registry, git_ssh)

padding = 8
max_name_len = 0
Expand Down
32 changes: 32 additions & 0 deletions src/stack/deploy/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,17 @@ def get_ports(self):
ports[svc_name] = [str(x) for x in svc[constants.ports_key]]
return ports

def get_volumes(self):
volumes = {}
pods = self.get_pod_list()
for pod in pods:
parsed_pod_file = self.load_pod_file(pod)
if constants.services_key in parsed_pod_file:
for svc_name, svc in parsed_pod_file[constants.services_key].items():
if constants.volumes_key in svc:
volumes[svc_name] = svc[constants.volumes_key]
return volumes

def get_http_proxy_targets(self, prefix=None):
if prefix == "/":
prefix = None
Expand Down Expand Up @@ -280,6 +291,20 @@ def get_plugin_code_paths(self) -> List[Path]:

return list(result)

def http_prefix_for(self, stack_path_or_name):
if not self.is_super_stack():
return None

if not os.path.exists(str(stack_path_or_name)):
stack_path_or_name = resolve_stack(stack_path_or_name).file_path.parent

stacks = self.get_required_stacks()
for s in stacks:
stack_path = determine_fs_path_for_stack(s[constants.ref_key], s[constants.path_key])
if stack_path == stack_path_or_name:
return s.get(constants.http_proxy_prefix_key, None)
return None

def __str__(self):
return str(self.__dict__)

Expand Down Expand Up @@ -386,6 +411,13 @@ def locate_single_stack(stack_name, search_path=get_dev_root_path(), fail_on_mul
def resolve_stack(stack_name):
if not stack_name:
error_exit("stack name cannot be empty")

if isinstance(stack_name, Stack):
return stack_name

if isinstance(stack_name, Path):
stack_name = str(stack_name)

stack = None
if stack_name.startswith("/") or (os.path.exists(stack_name) and os.path.isdir(stack_name)):
stack = get_parsed_stack_config(stack_name)
Expand Down
4 changes: 3 additions & 1 deletion src/stack/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Copyright © 2022, 2023 Vulcanize
# Copyright © 2025 Bozeman Pass, Inc.
import os

# 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
Expand All @@ -16,13 +15,15 @@
# along with this program. If not, see <http:#www.gnu.org/licenses/>.

import click
import os
import sys

from stack import opts
from stack import update
from stack import version

from stack.build import build, prepare
from stack.chart import chart
from stack.checklist import list_stack, checklist
from stack.cli_util import StackCLI, load_subcommands_from_stack
from stack.command_types import CommandOptions
Expand Down Expand Up @@ -79,6 +80,7 @@ def cli(ctx, profile, quiet, verbose, log_file, dry_run, debug, stack):


cli.add_command(build.command, "build")
cli.add_command(chart.command, "chart")
cli.add_command(checklist.command, "checklist")
cli.add_command(config.command, "config")
cli.add_command(deployment_create.create, "deploy")
Expand Down