Skip to content

Commit 8e9fd91

Browse files
Merge pull request #80 from Sandor59100/feature/cluster-context-selector
Diagram format additions, cluster improvements, and backend security hardening
2 parents 9ae75d8 + 4d42d26 commit 8e9fd91

42 files changed

Lines changed: 1091 additions & 570 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

webapp/backend/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from routes.helmfile import helmfile_bp
99
from routes.submit import submit_bp
1010
from routes.cluster import cluster_bp
11+
from routes.render import render_bp
1112
from utils.access_logger import log_request, get_real_ip, get_all_ip_headers
1213
from time import time
1314

@@ -91,6 +92,7 @@ def after_request(response):
9192
app.register_blueprint(helmfile_bp)
9293
app.register_blueprint(submit_bp)
9394
app.register_blueprint(cluster_bp)
95+
app.register_blueprint(render_bp)
9496

9597
return app
9698

webapp/backend/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class Config:
88
"""Config App and Logger."""
99

1010
# Flask
11-
DEBUG = True
11+
DEBUG = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
1212
PORT = 5000
1313
HOST = 'localhost'
1414

webapp/backend/constants.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
"pdf": "application/pdf",
1212
"dot": "text/vnd.graphviz",
1313
"dot_json": "application/json",
14-
"drawio": "application/xml"
14+
"drawio": "application/xml",
15+
"mermaid": "text/vnd.mermaid",
16+
"d2": "text/vnd.d2"
1517
}
1618
# no binary format
17-
TEXT_FORMATS = {"svg", "dot", "dot_json", "drawio"}
19+
TEXT_FORMATS = {"svg", "dot", "dot_json", "drawio", "mermaid", "d2"}
1820
# Manifest_detector
1921
MANIFEST_RE = re.compile(r'^\s*apiVersion\s*:\s*.+$', re.MULTILINE)
2022
KIND_RE = re.compile(r'^\s*kind\s*:\s*.+$', re.MULTILINE)
@@ -23,3 +25,23 @@
2325
# file extensions
2426
YAML_EXTENSIONS = ['.yaml', '.yml']
2527
TGZ_EXTENSIONS = ['.tgz', '.tar.gz']
28+
29+
# Allowlist of CLI flags accepted through the free-text "extra args" field, per
30+
# underlying tool. -o/--output, -f/--format and -c/--config are excluded even
31+
# though the tools support them: -o/-f are already managed by the app itself
32+
# (allowing them would let a request override the computed output path/format),
33+
# and -c/--config (plus helm's --values/--set-file) let the tool read an
34+
# arbitrary local file path, which would be a local file disclosure primitive.
35+
EXTRA_ARGS_ALLOWED_FLAGS = {
36+
"kube-diagrams": {
37+
"--embed-all-icons", "-v", "--verbose", "-n", "--namespace", "--without-namespace",
38+
},
39+
"kubectl-diagrams": {
40+
"--embed-all-icons", "--version",
41+
},
42+
"helm-diagrams": {
43+
"--set", "--set-string", "--set-json", "--set-literal",
44+
"-g", "--generate-name", "--include-crds", "-l", "--labels",
45+
"--name-template", "--version", "--embed-all-icons",
46+
},
47+
}

webapp/backend/routes/cluster.py

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
get_namespaces,
88
get_resource_types,
99
get_current_context,
10+
get_contexts,
1011
)
1112
from utils import InputValidator, ResponseBuilder
1213

@@ -16,43 +17,48 @@
1617
@cluster_bp.route('/api/cluster/context', methods=['GET'])
1718
def get_context():
1819
"""Return the name of the currently active kubectl context."""
19-
try:
20-
context = get_current_context()
21-
return ResponseBuilder.success({"context": context})
22-
except RuntimeError as e:
23-
return ResponseBuilder.error(str(e))
24-
except Exception as e:
25-
return ResponseBuilder.error(f"Unexpected error: {str(e)}")
20+
context, error = get_current_context()
21+
if error:
22+
return ResponseBuilder.error(error)
23+
return ResponseBuilder.success({"context": context})
24+
25+
26+
@cluster_bp.route('/api/cluster/contexts', methods=['GET'])
27+
def list_contexts():
28+
"""Return the list of kubectl contexts configured locally, marking which one is current."""
29+
contexts, error = get_contexts()
30+
if error:
31+
return ResponseBuilder.error(error)
32+
return ResponseBuilder.success({
33+
"contexts": contexts,
34+
"count": len(contexts)
35+
})
2636

2737

2838
@cluster_bp.route('/api/cluster/namespaces', methods=['GET'])
2939
def list_namespaces():
3040
"""Return the list of namespaces available in the connected Kubernetes cluster."""
31-
try:
32-
namespaces = get_namespaces()
33-
return ResponseBuilder.success({
34-
"namespaces": namespaces,
35-
"count": len(namespaces)
36-
})
37-
except RuntimeError as e:
38-
return ResponseBuilder.error(str(e))
39-
except Exception as e:
40-
return ResponseBuilder.error(f"Unexpected error: {str(e)}")
41+
context = request.args.get('context') or None
42+
namespaces, error = get_namespaces(context=context)
43+
if error:
44+
return ResponseBuilder.error(error)
45+
return ResponseBuilder.success({
46+
"namespaces": namespaces,
47+
"count": len(namespaces)
48+
})
4149

4250

4351
@cluster_bp.route('/api/cluster/resource-types', methods=['GET'])
4452
def list_resource_types():
4553
"""Return all resource types known by the cluster, tagged with namespace scope and common status."""
46-
try:
47-
resource_types = get_resource_types()
48-
return ResponseBuilder.success({
49-
"resourceTypes": resource_types,
50-
"count": len(resource_types)
51-
})
52-
except RuntimeError as e:
53-
return ResponseBuilder.error(str(e))
54-
except Exception as e:
55-
return ResponseBuilder.error(f"Unexpected error: {str(e)}")
54+
context = request.args.get('context') or None
55+
resource_types, error = get_resource_types(context=context)
56+
if error:
57+
return ResponseBuilder.error(error)
58+
return ResponseBuilder.success({
59+
"resourceTypes": resource_types,
60+
"count": len(resource_types)
61+
})
5662

5763

5864
@cluster_bp.route('/api/cluster/generate', methods=['POST'])
@@ -66,6 +72,7 @@ def generate_cluster_diagram():
6672
output_format = (data.get('outputFormat') or 'png').lower()
6773
extra_args = data.get('extraArgs', '')
6874
without_namespace = data.get('withoutNamespace', False)
75+
context = data.get('context') or None
6976

7077
# Log to CSV
7178
client_ip = request.remote_addr
@@ -76,7 +83,8 @@ def generate_cluster_diagram():
7683
f"allNamespaces={all_namespaces};"
7784
f"format={output_format};"
7885
f"extraArgs={compact_for_log(extra_args)};"
79-
f"withoutNamespace={without_namespace}"
86+
f"withoutNamespace={without_namespace};"
87+
f"context={context}"
8088
)
8189
log_to_csv(client_ip, route, params)
8290

@@ -94,7 +102,7 @@ def generate_cluster_diagram():
94102
return ResponseBuilder.validation_error("outputFormat", error_msg)
95103

96104
# Extra arguments validation
97-
is_valid, error_msg = InputValidator.validate_extra_args(extra_args)
105+
is_valid, error_msg = InputValidator.validate_extra_args(extra_args, "kubectl-diagrams")
98106
if not is_valid:
99107
return ResponseBuilder.validation_error("extraArgs", error_msg)
100108

@@ -105,7 +113,8 @@ def generate_cluster_diagram():
105113
all_namespaces=all_namespaces,
106114
output_format=output_format,
107115
extra_args=extra_args,
108-
without_namespace=without_namespace
116+
without_namespace=without_namespace,
117+
context=context
109118
)
110119

111120
if result.success:

webapp/backend/routes/helm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def generate_helm_diagram():
3232
return ResponseBuilder.validation_error("outputFormat", error_msg)
3333

3434
# Extra arguments validation
35-
is_valid, error_msg = InputValidator.validate_extra_args(extra_args)
35+
is_valid, error_msg = InputValidator.validate_extra_args(extra_args, "helm-diagrams")
3636
if not is_valid:
3737
return ResponseBuilder.validation_error("extraArgs", error_msg)
3838

webapp/backend/routes/helmfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def generate_helmfile_diagram():
4040
return ResponseBuilder.validation_error("outputFormat", error_msg)
4141

4242
# Extra arguments validation
43-
is_valid, error_msg = InputValidator.validate_extra_args(extra_args)
43+
is_valid, error_msg = InputValidator.validate_extra_args(extra_args, "kube-diagrams")
4444
if not is_valid:
4545
return ResponseBuilder.validation_error("extraArgs", error_msg)
4646

webapp/backend/routes/manifest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def generate_diagram():
4444
return ResponseBuilder.validation_error("outputFormat", error_msg)
4545

4646
# Extra arguments validation
47-
is_valid, error_msg = InputValidator.validate_extra_args(extra_args)
47+
is_valid, error_msg = InputValidator.validate_extra_args(extra_args, "kube-diagrams")
4848
if not is_valid:
4949
return ResponseBuilder.validation_error("extraArgs", error_msg)
5050

webapp/backend/routes/render.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Route for on-demand rendering of already-generated diagram source."""
2+
from flask import Blueprint, request
3+
4+
from services import dot_to_svg
5+
from utils import ResponseBuilder
6+
from .utils import compact_for_log, log_to_csv
7+
8+
render_bp = Blueprint('render', __name__)
9+
10+
@render_bp.route('/api/render-dot-svg', methods=['POST'])
11+
def render_dot_svg():
12+
"""Render DOT source (already generated by a previous /api/generate-* call) to SVG."""
13+
data = request.get_json()
14+
dot_content = data.get('dot', '')
15+
16+
client_ip = request.remote_addr
17+
route = request.path
18+
log_to_csv(client_ip, route, f"dot={compact_for_log(dot_content)}")
19+
20+
if not dot_content or not dot_content.strip():
21+
return ResponseBuilder.validation_error("dot", "DOT content cannot be empty.")
22+
23+
svg = dot_to_svg(dot_content)
24+
if svg is None:
25+
return ResponseBuilder.error(
26+
"dot -Tsvg conversion failed (is graphviz installed?).",
27+
status_code=500
28+
)
29+
30+
return ResponseBuilder.success(data={"svg": svg})

webapp/backend/services/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
get_namespaces,
1010
get_resource_types,
1111
get_current_context,
12+
get_contexts,
1213
)
14+
from .utils import dot_to_svg
1315

1416
__all__ = [
1517
'DiagramResult',
@@ -21,5 +23,7 @@
2123
'get_namespaces',
2224
'get_resource_types',
2325
'get_current_context',
26+
'get_contexts',
27+
'dot_to_svg',
2428
]
2529

0 commit comments

Comments
 (0)