-
Notifications
You must be signed in to change notification settings - Fork 132
Expand file tree
/
Copy pathwva.py
More file actions
184 lines (159 loc) · 6.44 KB
/
Copy pathwva.py
File metadata and controls
184 lines (159 loc) · 6.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
"""Shared helpers for installing the Workload Variant Autoscaler (WVA).
The WVA controller and its runtime dependencies (ServiceAccount, bearer token
Secret, thanos-querier ClusterRole) are cluster/admin-scoped and must be
provisioned *before* any per-stack work runs. KEDA itself is pre-installed by
cluster admins and not managed by this harness. These helpers are called from
``step_03_workload_monitoring`` once per unique ``wva.namespace`` across all
rendered stacks. Per-stack ScaledObject is rendered from
``28_wva-scaledobject.yaml.j2`` and applied in ``step_09``.
Helpers live in this module (rather than in a step class) so both the
admin step and per-stack step can import them without a cyclic dependency.
"""
from __future__ import annotations
import tempfile
from pathlib import Path
import yaml
from llmdbenchmark.executor.command import CommandExecutor
from llmdbenchmark.executor.context import ExecutionContext
from llmdbenchmark.standup.keda_prometheus_auth import (
verify_keda_installed, # noqa: F401 (re-exported for step_03)
extract_prometheus_ca_cert, # noqa: F401 (re-exported for step_03)
create_prometheus_auth_secret as _create_prometheus_auth_secret,
apply_namespace_label,
_find_yaml,
_has_yaml_content,
)
def install_wva_for_namespace( # pylint: disable=too-many-arguments,too-many-locals,unused-argument
cmd: CommandExecutor,
context: ExecutionContext,
plan_config: dict,
stack_path: Path,
wva_namespace: str,
prom_ca_cert: str | None,
errors: list,
) -> None:
"""Install the WVA controller via kustomize-built upstream manifests.
Reads ``19_wva-kustomize.yaml`` (a Kustomization wrapper) from
*stack_path* and applies it with ``kubectl apply -k``. The wrapper's
``resources:`` field references the upstream
``config/overlays/namespace-scoped/openshift`` overlay over a remote
git URL, so kustomize fetches the upstream tree at apply time -- no
local clone needed. The wrapper layers our namespace + image
overrides on top.
"""
kustomize_yaml = _find_yaml(stack_path, "19_wva-kustomize")
if not kustomize_yaml:
errors.append(
"WVA kustomization template (19_wva-kustomize) not found "
"-- cannot install WVA"
)
return
if not _has_yaml_content(kustomize_yaml):
# Template guarded by `wva.enabled` -- empty content means the
# flag is off for this stack; nothing to install.
return
# `kubectl apply -k <dir>` requires the kustomization file to be
# named exactly `kustomization.yaml`. Our rendered file uses the
# numeric prefix convention (`19_wva-kustomize.yaml`); stage a copy
# under the canonical name in a temp dir so kustomize finds it.
tmp_dir = Path(tempfile.mkdtemp())
(tmp_dir / "kustomization.yaml").write_text(
kustomize_yaml.read_text(encoding="utf-8"), encoding="utf-8"
)
context.logger.log_info(
f"📦 Installing WVA controller via kustomize into ns/{wva_namespace}"
)
result = cmd.kube(
"apply",
"-k",
str(tmp_dir),
check=False,
)
if not result.success:
errors.append(f"Failed to install WVA: {result.stderr}")
return
# Wait for the controller pod(s) to actually become Ready before
# returning, with live ⏳ progress output. Without this, step_03
# returns success while the controller is still scheduling / pulling
# images, and downstream steps race against pod startup.
wait = cmd.wait_for_pods(
label="control-plane=controller-manager",
namespace=wva_namespace,
timeout=300,
poll_interval=5,
description=f"WVA controller in ns/{wva_namespace}",
)
if not wait.success:
errors.append(
f"WVA controller pods did not become Ready in ns/{wva_namespace}: "
f"{wait.stderr}"
)
def create_prometheus_auth_secret(
cmd: CommandExecutor,
context: ExecutionContext,
stack_path: Path,
wva_namespace: str,
prom_ca_cert: str | None,
errors: list,
) -> None:
"""Create per-namespace Prometheus bearer token Secret + TriggerAuthentication.
Wrapper for generic function with WVA-specific defaults (SA name and template stem).
"""
_create_prometheus_auth_secret(
cmd,
context,
stack_path,
wva_namespace,
prom_ca_cert,
sa_name="wva-prometheus-auth",
ta_template_stem="21_keda-triggerauthentication",
errors=errors,
)
def apply_wva_namespace_label(
cmd: CommandExecutor, stack_path: Path, wva_namespace: str
) -> None:
"""Apply rendered 23_wva-namespace YAML (Namespace + user-monitoring label)."""
apply_namespace_label(
cmd, stack_path, wva_namespace, ns_template_stem="23_wva-namespace"
)
def stacks_enabling_wva(rendered_stacks: list[Path]) -> list[tuple[Path, dict]]:
"""Return (stack_path, plan_config) pairs for each stack with wva.enabled."""
pairs: list[tuple[Path, dict]] = []
for stack_path in rendered_stacks:
cfg_file = stack_path / "config.yaml"
if not cfg_file.exists():
continue
try:
with open(cfg_file, encoding="utf-8") as fh:
cfg = yaml.safe_load(fh) or {}
except (OSError, yaml.YAMLError):
continue
if cfg.get("wva", {}).get("enabled", False):
pairs.append((stack_path, cfg))
return pairs
def unique_wva_namespaces(
stacks: list[tuple[Path, dict]],
) -> dict[str, tuple[Path, dict]]:
"""Group stacks by their ``wva.namespace`` (falling back to ``namespace.name``).
Returns a mapping ``{wva_namespace: (first_stack_path, first_plan_config)}``
so the caller can install the controller once per namespace using that
stack's rendered values.
"""
result: dict[str, tuple[Path, dict]] = {}
for stack_path, cfg in stacks:
wva_cfg = cfg.get("wva", {})
wva_ns = wva_cfg.get("namespace") or cfg.get("namespace", {}).get("name", "")
if not wva_ns:
continue
if wva_ns not in result:
result[wva_ns] = (stack_path, cfg)
return result
def _require_config(cfg: dict, *keys: str):
"""Navigate dotted config path, raising if any segment is missing."""
node = cfg
for key in keys:
if not isinstance(node, dict) or key not in node:
dotted = ".".join(keys)
raise KeyError(f"Required config key missing: {dotted}")
node = node[key]
return node