Skip to content

Commit a9bd623

Browse files
committed
fix/gen-feature-variants.py: handle dict-format namespaces and Vault JWT roles within an override file
Signed-off-by: Manuel Lorenzo <mlorenzofr@redhat.com>
1 parent 3a26281 commit a9bd623

7 files changed

Lines changed: 167 additions & 76 deletions

File tree

scripts/features/pipelines.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Required for the secure supply chain pipeline flow
33
clusterGroup:
44
namespaces:
5-
- openshift-pipelines
5+
openshift-pipelines:
66

77
subscriptions:
88
openshift-pipelines:

scripts/features/quay.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
# Depends on: storage (ODF + NooBaa MCG for backend)
33
clusterGroup:
44
namespaces:
5-
- quay-enterprise:
6-
annotations:
7-
argocd.argoproj.io/sync-wave: "32"
8-
labels:
9-
openshift.io/cluster-monitoring: "true"
5+
quay-enterprise:
6+
annotations:
7+
argocd.argoproj.io/sync-wave: "32"
8+
labels:
9+
openshift.io/cluster-monitoring: "true"
1010

1111
subscriptions:
1212
quay-operator:

scripts/features/registry/option-1-quay.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ global:
1515

1616
clusterGroup:
1717
namespaces:
18-
- quay-enterprise:
19-
annotations:
20-
argocd.argoproj.io/sync-wave: "32"
21-
labels:
22-
openshift.io/cluster-monitoring: "true"
18+
quay-enterprise:
19+
annotations:
20+
argocd.argoproj.io/sync-wave: "32"
21+
labels:
22+
openshift.io/cluster-monitoring: "true"
2323

2424
subscriptions:
2525
quay-operator:

scripts/features/rhtas.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
# Depends on: Vault, SPIRE, Keycloak (all in base config)
33
clusterGroup:
44
namespaces:
5-
- trusted-artifact-signer:
6-
annotations:
7-
argocd.argoproj.io/sync-wave: "32"
8-
labels:
9-
openshift.io/cluster-monitoring: "true"
5+
trusted-artifact-signer:
6+
annotations:
7+
argocd.argoproj.io/sync-wave: "32"
8+
labels:
9+
openshift.io/cluster-monitoring: "true"
1010

1111
subscriptions:
1212
rhtas-operator:

scripts/features/rhtpa.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
# Depends on: storage (NooBaa MCG), Vault, SPIRE, Keycloak
33
clusterGroup:
44
namespaces:
5-
- rhtpa-operator:
6-
operatorGroup: true
7-
targetNamespace: rhtpa-operator
8-
annotations:
9-
argocd.argoproj.io/sync-wave: "26"
10-
- trusted-profile-analyzer:
11-
annotations:
12-
argocd.argoproj.io/sync-wave: "32"
13-
labels:
14-
openshift.io/cluster-monitoring: "true"
5+
rhtpa-operator:
6+
operatorGroup: true
7+
targetNamespace: rhtpa-operator
8+
annotations:
9+
argocd.argoproj.io/sync-wave: "26"
10+
trusted-profile-analyzer:
11+
annotations:
12+
argocd.argoproj.io/sync-wave: "32"
13+
labels:
14+
openshift.io/cluster-monitoring: "true"
1515

1616
subscriptions:
1717
rhtpa-operator:

scripts/features/storage.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
# Required for RHTPA and Quay (provides S3-compatible storage via NooBaa MCG)
33
clusterGroup:
44
namespaces:
5-
- openshift-storage:
6-
operatorGroup: true
7-
targetNamespace: openshift-storage
8-
annotations:
9-
openshift.io/cluster-monitoring: "true"
10-
argocd.argoproj.io/sync-wave: "26"
5+
openshift-storage:
6+
operatorGroup: true
7+
targetNamespace: openshift-storage
8+
annotations:
9+
openshift.io/cluster-monitoring: "true"
10+
argocd.argoproj.io/sync-wave: "26"
1111

1212
subscriptions:
1313
odf:

scripts/gen-feature-variants.py

Lines changed: 135 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,22 @@ def visit(name):
107107
return list(resolved.keys())
108108

109109

110-
def _namespace_key(item):
111-
"""Extract the unique key from a namespace list entry (string or mapping)."""
112-
if isinstance(item, str):
113-
return item
114-
if isinstance(item, dict):
115-
keys = list(item.keys())
116-
return keys[0] if keys else None
117-
return None
118-
119-
120-
def _merge_namespace_lists(base_list, fragment_list):
121-
"""Append namespace entries from fragment_list that are not already in base_list."""
122-
existing = {_namespace_key(item) for item in base_list}
123-
for item in fragment_list:
124-
key = _namespace_key(item)
125-
if key not in existing:
126-
base_list.append(item)
127-
existing.add(key)
110+
def _merge_namespace_dicts(base_dict, fragment_dict):
111+
"""Merge namespace entries from fragment_dict into base_dict.
112+
113+
Namespaces are now dictionaries where keys are namespace names and values
114+
are their configurations (or empty/None for namespaces without config).
115+
"""
116+
for ns_name, ns_config in fragment_dict.items():
117+
if ns_name not in base_dict:
118+
# Add new namespace
119+
base_dict[ns_name] = copy.deepcopy(ns_config) if ns_config else None
120+
elif ns_config:
121+
# Merge configuration for existing namespace
122+
if base_dict[ns_name] is None:
123+
base_dict[ns_name] = copy.deepcopy(ns_config)
124+
elif isinstance(base_dict[ns_name], dict) and isinstance(ns_config, dict):
125+
_deep_merge_mappings(base_dict[ns_name], copy.deepcopy(ns_config))
128126

129127

130128
def _is_named_list(lst):
@@ -168,7 +166,7 @@ def _deep_merge_mappings(base, overlay):
168166
base[key] = overlay[key]
169167

170168

171-
def _apply_merge_into(base_apps, merge_into_spec):
169+
def _apply_merge_into(base_apps, merge_into_spec, vault_jwt_roles_accumulator):
172170
"""Handle merge_into_applications: merge fragment data into existing app configs.
173171
174172
merge_into_spec is a mapping like:
@@ -181,8 +179,27 @@ def _apply_merge_into(base_apps, merge_into_spec):
181179
For each target app, recursively merge into the existing app config.
182180
Named lists (items with a 'name' key) use upsert semantics; plain lists
183181
are appended.
182+
183+
Special handling for vault JWT roles: instead of merging them into
184+
clusterGroup.applications.vault, accumulate them in vault_jwt_roles_accumulator
185+
for later merging into the overrides/values-vault-jwt.yaml structure.
184186
"""
185187
for app_name, additions in merge_into_spec.items():
188+
# Special handling for vault JWT roles
189+
if app_name == "vault" and "jwt" in additions:
190+
jwt_config = additions.get("jwt", {})
191+
if "roles" in jwt_config:
192+
# Accumulate JWT roles for later merging into vault
193+
# override file
194+
vault_jwt_roles_accumulator.extend(copy.deepcopy(jwt_config["roles"]))
195+
# Remove jwt from additions to prevent it from being
196+
# merged into app config
197+
additions = copy.deepcopy(additions)
198+
del additions["jwt"]
199+
# If nothing else to merge, continue to next app
200+
if not additions:
201+
continue
202+
186203
if app_name not in base_apps:
187204
print(
188205
f"WARNING: merge_into_applications target '{app_name}'"
@@ -213,14 +230,20 @@ def _insert_key_before(mapping, new_key, new_value, before_key):
213230
mapping[k] = v
214231

215232

216-
def merge_fragment(base, fragment):
217-
"""Merge a single feature fragment into the base YAML tree."""
233+
def merge_fragment(base, fragment, vault_jwt_roles_accumulator):
234+
"""Merge a single feature fragment into the base YAML tree.
235+
236+
vault_jwt_roles_accumulator is a list that collects JWT roles from all fragments
237+
for later merging into the vault override file.
238+
"""
218239
if fragment is None:
219240
return
220241

221242
for top_key in fragment:
222243
if top_key == "clusterGroup":
223-
_merge_cluster_group(base, fragment["clusterGroup"])
244+
_merge_cluster_group(
245+
base, fragment["clusterGroup"], vault_jwt_roles_accumulator
246+
)
224247
elif top_key in base and isinstance(base[top_key], dict):
225248
_deep_merge_mappings(base[top_key], copy.deepcopy(fragment[top_key]))
226249
elif top_key not in base:
@@ -234,13 +257,26 @@ def merge_fragment(base, fragment):
234257
base[top_key] = copy.deepcopy(fragment[top_key])
235258

236259

237-
def _merge_cluster_group(base, frag_cg):
238-
"""Merge clusterGroup sections with type-aware strategies."""
260+
def _merge_cluster_group(base, frag_cg, vault_jwt_roles_accumulator):
261+
"""Merge clusterGroup sections with type-aware strategies.
262+
263+
vault_jwt_roles_accumulator is a list that collects JWT roles from all fragments
264+
for later merging into the vault override file.
265+
"""
239266
base_cg = base.setdefault("clusterGroup", {})
240267

241268
if "namespaces" in frag_cg:
242-
base_ns = base_cg.setdefault("namespaces", [])
243-
_merge_namespace_lists(base_ns, frag_cg["namespaces"])
269+
base_ns = base_cg.setdefault("namespaces", {})
270+
# Ensure namespaces is a dict
271+
if not isinstance(base_ns, dict):
272+
print(
273+
f"WARNING: base namespaces is not a dict (type: {type(base_ns)}), "
274+
"converting to empty dict",
275+
file=sys.stderr,
276+
)
277+
base_ns = {}
278+
base_cg["namespaces"] = base_ns
279+
_merge_namespace_dicts(base_ns, frag_cg["namespaces"])
244280

245281
if "subscriptions" in frag_cg:
246282
base_subs = base_cg.setdefault("subscriptions", {})
@@ -256,20 +292,25 @@ def _merge_cluster_group(base, frag_cg):
256292

257293
if "merge_into_applications" in frag_cg:
258294
base_apps = base_cg.get("applications", {})
259-
_apply_merge_into(base_apps, frag_cg["merge_into_applications"])
295+
_apply_merge_into(
296+
base_apps, frag_cg["merge_into_applications"], vault_jwt_roles_accumulator
297+
)
260298

261299

262300
def validate_output(data):
263301
"""Run basic sanity checks on the merged YAML tree."""
264302
cg = data.get("clusterGroup", {})
265303

266-
ns_list = cg.get("namespaces", [])
267-
seen = set()
268-
for item in ns_list:
269-
key = _namespace_key(item)
270-
if key in seen:
271-
print(f"WARNING: duplicate namespace '{key}'", file=sys.stderr)
272-
seen.add(key)
304+
ns_dict = cg.get("namespaces", {})
305+
if isinstance(ns_dict, dict):
306+
# Namespaces are now a dict, so duplicate checking is implicit
307+
# (dict keys are unique by definition)
308+
pass
309+
else:
310+
print(
311+
f"WARNING: namespaces is not a dict (type: {type(ns_dict)})",
312+
file=sys.stderr,
313+
)
273314

274315
apps = cg.get("applications", {})
275316
for app_name, app_val in apps.items():
@@ -286,14 +327,8 @@ def validate_output(data):
286327
if name:
287328
override_names.add(name)
288329

289-
vault = apps.get("vault", {})
290-
jwt_roles = vault.get("jwt", {}).get("roles", [])
291-
role_names = set()
292-
for role in jwt_roles:
293-
name = role.get("name")
294-
if name in role_names:
295-
print(f"WARNING: duplicate vault JWT role '{name}'", file=sys.stderr)
296-
role_names.add(name)
330+
# Vault JWT roles are now in overrides/values-vault-jwt.yaml
331+
# No need to validate them here as they're not in the generated variant
297332

298333

299334
def _substitute_repository_placeholders(base, org=None, image_name=None):
@@ -343,6 +378,51 @@ def _substitute_git_overrides(base, git_repo_url, git_host, git_auth_type):
343378
override["value"] = entry[1]
344379

345380

381+
def _update_vault_jwt_override_file(override_file_path, new_roles):
382+
"""Update the vault JWT override file with new roles from feature fragments.
383+
384+
Merges new_roles into the vault_jwt_roles list in the override file.
385+
Uses named list semantics (upsert by role name).
386+
"""
387+
if not new_roles:
388+
return
389+
390+
yaml = YAML()
391+
yaml.preserve_quotes = True
392+
yaml.default_flow_style = False
393+
yaml.width = 4096
394+
395+
# Load existing override file
396+
if os.path.isfile(override_file_path):
397+
with open(override_file_path) as fh:
398+
override_data = yaml.load(fh)
399+
else:
400+
# Create new structure if file doesn't exist
401+
oidc_url = (
402+
"https://spire-spiffe-oidc-discovery-provider"
403+
".zero-trust-workload-identity-manager.svc.cluster.local"
404+
)
405+
override_data = {
406+
"vault_jwt_config": True,
407+
"vault_jwt_policies": [],
408+
"vault_jwt_roles": [],
409+
"oidc_discovery_url": oidc_url,
410+
}
411+
412+
# Get existing roles list
413+
existing_roles = override_data.setdefault("vault_jwt_roles", [])
414+
415+
# Merge new roles using named list semantics
416+
_merge_named_lists(existing_roles, new_roles)
417+
418+
# Write back to file
419+
with open(override_file_path, "w") as fh:
420+
yaml.dump(override_data, fh)
421+
422+
role_names = [r.get("name", "unknown") for r in new_roles]
423+
print(f" Updated {override_file_path} with roles: {', '.join(role_names)}")
424+
425+
346426
def generate_variant(
347427
base_path,
348428
features_dir,
@@ -362,13 +442,16 @@ def generate_variant(
362442
with open(base_path) as fh:
363443
base = yaml.load(fh)
364444

445+
# Accumulator for vault JWT roles from feature fragments
446+
vault_jwt_roles_accumulator = []
447+
365448
for feat_name in resolved_features:
366449
frag_path = os.path.join(features_dir, f"{feat_name}.yaml")
367450
if not os.path.isfile(frag_path):
368451
print(f"ERROR: fragment file not found: {frag_path}", file=sys.stderr)
369452
sys.exit(1)
370453
fragment = load_yaml_file(frag_path)
371-
merge_fragment(base, fragment)
454+
merge_fragment(base, fragment, vault_jwt_roles_accumulator)
372455

373456
if registry_fragment_path:
374457
if not os.path.isfile(registry_fragment_path):
@@ -378,7 +461,15 @@ def generate_variant(
378461
)
379462
sys.exit(1)
380463
registry_frag = load_yaml_file(registry_fragment_path)
381-
merge_fragment(base, registry_frag)
464+
merge_fragment(base, registry_frag, vault_jwt_roles_accumulator)
465+
466+
# Update vault JWT override file with roles from feature fragments
467+
if vault_jwt_roles_accumulator:
468+
repo_root = os.path.dirname(SCRIPT_DIR)
469+
override_file_path = os.path.join(
470+
repo_root, "overrides", "values-vault-jwt.yaml"
471+
)
472+
_update_vault_jwt_override_file(override_file_path, vault_jwt_roles_accumulator)
382473

383474
if org or image_name:
384475
_substitute_repository_placeholders(base, org=org, image_name=image_name)

0 commit comments

Comments
 (0)