Skip to content

🐛 Fix CI E2E: clean up orphaned cluster-scoped WVA resources - #715

Closed
clubanderson wants to merge 1 commit into
mainfrom
fix/ci-orphan-clusterrole-cleanup
Closed

🐛 Fix CI E2E: clean up orphaned cluster-scoped WVA resources#715
clubanderson wants to merge 1 commit into
mainfrom
fix/ci-orphan-clusterrole-cleanup

Conversation

@clubanderson

Copy link
Copy Markdown
Contributor

Summary

  • Adds orphan detection for cluster-scoped WVA resources (ClusterRoles, ClusterRoleBindings) whose owning namespace no longer exists
  • These stale resources block fresh helm installs because Helm refuses to adopt resources owned by a different release
  • Same fix as llm-d/llm-d-infra#10 (nightly workflow), applied to the CI E2E workflow

Root Cause

When a WVA installation's namespace is deleted (e.g., failed CI run cleanup, manual deletion), cluster-scoped resources like ClusterRoles are left behind. The next CI run tries to create the same ClusterRole, but Helm rejects it:

ClusterRole "workload-variant-autoscaler-metrics-auth-role" exists and cannot be imported
into the current release: invalid ownership metadata; key "meta.helm.sh/release-namespace"
must equal "llm-d-nightly-wva": current value is "llm-d-autoscaler"

Changes

Pre-cleanup (before deploy):

  • Scans all WVA-labeled ClusterRoles/ClusterRoleBindings
  • For each, checks if the owning namespace (from meta.helm.sh/release-namespace) still exists
  • Deletes only orphans where the namespace is gone — active installations are never touched

Post-cleanup (after tests):

  • Existing: deletes cluster-scoped resources by app.kubernetes.io/instance label (matches WVA controller release)
  • New: also deletes cluster-scoped resources owned by this PR's namespaces (covers helmfile-created resources with different instance labels)

Test plan

  • CI E2E passes on this PR
  • Verify other WVA installations on the cluster are not affected
  • Verify orphaned resources from deleted namespaces are cleaned up

Copilot AI review requested due to automatic review settings February 13, 2026 04:02
@github-actions

Copy link
Copy Markdown
Contributor

GPU Pre-flight Check ✅

GPUs are available for e2e-openshift tests. Proceeding with deployment.

Resource Total Allocated Available
GPUs 50 32 18
Cluster Value
Nodes 16 (7 with GPUs)
Total CPU 993 cores
Total Memory 10383 Gi
GPUs required 4 (min) / 6 (recommended)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the OpenShift CI E2E workflow to proactively remove stale, cluster-scoped Workload Variant Autoscaler (WVA) RBAC resources that can block subsequent Helm installs when their owning namespace has been deleted.

Changes:

  • Add a pre-deploy scan that deletes WVA-labeled ClusterRoles/ClusterRoleBindings whose meta.helm.sh/release-namespace no longer exists.
  • Extend post-test cleanup to also delete cluster-scoped WVA resources annotated as owned by this PR’s namespaces (in addition to the existing instance-label-based deletion).

Comment thread .github/workflows/ci-e2e-openshift.yaml Outdated
Comment on lines +567 to +578
echo "Checking for orphaned cluster-scoped WVA resources..."
for kind in clusterrole clusterrolebinding; do
for resource in $(kubectl get "$kind" -l app.kubernetes.io/name=workload-variant-autoscaler -o jsonpath='{range .items[*]}{.metadata.name}={.metadata.annotations.meta\.helm\.sh/release-namespace}{"\n"}{end}' 2>/dev/null); do
name="${resource%%=*}"
ns="${resource##*=}"
if [ -n "$ns" ] && ! kubectl get namespace "$ns" &>/dev/null; then
echo " Deleting orphaned $kind/$name (owning namespace '$ns' no longer exists)"
kubectl delete "$kind" "$name" --ignore-not-found || true
fi
done
done

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This orphan/namespace-owned cluster-scope cleanup logic is duplicated (same kubectl get ... jsonpath + parsing) in both pre-cleanup and post-cleanup sections. To reduce drift and make future changes safer, consider factoring it into a small helper script or a reusable step/function within the workflow.

Suggested change
echo "Checking for orphaned cluster-scoped WVA resources..."
for kind in clusterrole clusterrolebinding; do
for resource in $(kubectl get "$kind" -l app.kubernetes.io/name=workload-variant-autoscaler -o jsonpath='{range .items[*]}{.metadata.name}={.metadata.annotations.meta\.helm\.sh/release-namespace}{"\n"}{end}' 2>/dev/null); do
name="${resource%%=*}"
ns="${resource##*=}"
if [ -n "$ns" ] && ! kubectl get namespace "$ns" &>/dev/null; then
echo " Deleting orphaned $kind/$name (owning namespace '$ns' no longer exists)"
kubectl delete "$kind" "$name" --ignore-not-found || true
fi
done
done
cleanup_orphaned_wva_cluster_resources() {
echo "Checking for orphaned cluster-scoped WVA resources..."
for kind in clusterrole clusterrolebinding; do
for resource in $(kubectl get "$kind" -l app.kubernetes.io/name=workload-variant-autoscaler -o jsonpath='{range .items[*]}{.items[*]}{.metadata.name}={.metadata.annotations.meta\.helm\.sh/release-namespace}{"\n"}{end}' 2>/dev/null); do
name="${resource%%=*}"
ns="${resource##*=}"
if [ -n "$ns" ] && ! kubectl get namespace "$ns" &>/dev/null; then
echo " Deleting orphaned $kind/$name (owning namespace '$ns' no longer exists)"
kubectl delete "$kind" "$name" --ignore-not-found || true
fi
done
done
}
cleanup_orphaned_wva_cluster_resources

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci-e2e-openshift.yaml Outdated
Comment on lines +803 to +810
for kind in clusterrole clusterrolebinding; do
for resource in $(kubectl get "$kind" -l app.kubernetes.io/name=workload-variant-autoscaler -o jsonpath='{range .items[*]}{.metadata.name}={.metadata.annotations.meta\.helm\.sh/release-namespace}{"\n"}{end}' 2>/dev/null); do
name="${resource%%=*}"
ns="${resource##*=}"
if [ "$ns" = "$LLMD_NAMESPACE" ] || [ "$ns" = "$LLMD_NAMESPACE_B" ] || [ "$ns" = "$WVA_NAMESPACE" ]; then
echo " Deleting $kind/$name (owned by PR namespace '$ns')"
kubectl delete "$kind" "$name" --ignore-not-found || true
fi

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too, kubectl get stderr is fully suppressed (2>/dev/null), which can mask listing failures and lead to incomplete cleanup while still printing success messages. Consider preserving errors (or checking exit status) so troubleshooting doesn’t require re-running with extra logging.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci-e2e-openshift.yaml Outdated
Comment on lines +568 to +575
for kind in clusterrole clusterrolebinding; do
for resource in $(kubectl get "$kind" -l app.kubernetes.io/name=workload-variant-autoscaler -o jsonpath='{range .items[*]}{.metadata.name}={.metadata.annotations.meta\.helm\.sh/release-namespace}{"\n"}{end}' 2>/dev/null); do
name="${resource%%=*}"
ns="${resource##*=}"
if [ -n "$ns" ] && ! kubectl get namespace "$ns" &>/dev/null; then
echo " Deleting orphaned $kind/$name (owning namespace '$ns' no longer exists)"
kubectl delete "$kind" "$name" --ignore-not-found || true
fi

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both loops silence all kubectl get stderr (2>/dev/null), which can hide real failures (e.g., RBAC/auth issues or API errors) and make the orphan-cleanup appear to run while doing nothing. Consider handling the empty-result case without suppressing errors, or explicitly checking the command exit code and logging a warning/error when the list operation fails.

Copilot uses AI. Check for mistakes.
@clubanderson
clubanderson force-pushed the fix/ci-orphan-clusterrole-cleanup branch from 2e022e0 to f25d301 Compare February 13, 2026 04:16
@github-actions

Copy link
Copy Markdown
Contributor

GPU Pre-flight Check ✅

GPUs are available for e2e-openshift tests. Proceeding with deployment.

Resource Total Allocated Available
GPUs 50 32 18
Cluster Value
Nodes 16 (7 with GPUs)
Total CPU 993 cores
Total Memory 10383 Gi
GPUs required 4 (min) / 6 (recommended)

Copilot AI review requested due to automatic review settings February 13, 2026 04:21
@clubanderson
clubanderson force-pushed the fix/ci-orphan-clusterrole-cleanup branch from f25d301 to 923aa70 Compare February 13, 2026 04:21
@github-actions

Copy link
Copy Markdown
Contributor

GPU Pre-flight Check ✅

GPUs are available for e2e-openshift tests. Proceeding with deployment.

Resource Total Allocated Available
GPUs 50 32 18
Cluster Value
Nodes 16 (7 with GPUs)
Total CPU 993 cores
Total Memory 10383 Gi
GPUs required 4 (min) / 6 (recommended)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment thread .github/workflows/ci-e2e-openshift.yaml Outdated
# Search by name pattern (not labels — helmfile deployments may use different labels)
# Use jq to reliably extract annotation keys containing dots/slashes
kubectl get "$kind" -o json 2>/dev/null | \
jq -r '.items[] | select(.metadata.name | contains("workload-variant-autoscaler")) | "\(.metadata.name)\t\(.metadata.annotations["meta.helm.sh/release-namespace"] // "")"' 2>/dev/null | \

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jq filter uses contains("workload-variant-autoscaler") which will match any resource name containing this substring, potentially including unrelated resources if they happen to have similar names. Consider using a more specific filter such as startswith("workload-variant-autoscaler") or matching against specific known prefixes to reduce the risk of false positives.

Suggested change
jq -r '.items[] | select(.metadata.name | contains("workload-variant-autoscaler")) | "\(.metadata.name)\t\(.metadata.annotations["meta.helm.sh/release-namespace"] // "")"' 2>/dev/null | \
jq -r '.items[] | select(.metadata.name | startswith("workload-variant-autoscaler")) | "\(.metadata.name)\t\(.metadata.annotations["meta.helm.sh/release-namespace"] // "")"' 2>/dev/null | \

Copilot uses AI. Check for mistakes.
# (covers helmfile-created resources whose instance label differs from WVA_RELEASE_NAME)
for kind in clusterrole clusterrolebinding; do
kubectl get "$kind" -o json 2>/dev/null | \
jq -r '.items[] | select(.metadata.name | contains("workload-variant-autoscaler")) | "\(.metadata.name)\t\(.metadata.annotations["meta.helm.sh/release-namespace"] // "")"' 2>/dev/null | \

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jq filter uses contains("workload-variant-autoscaler") which will match any resource name containing this substring, potentially including unrelated resources if they happen to have similar names. Consider using a more specific filter such as startswith("workload-variant-autoscaler") or matching against specific known prefixes to reduce the risk of false positives.

Suggested change
jq -r '.items[] | select(.metadata.name | contains("workload-variant-autoscaler")) | "\(.metadata.name)\t\(.metadata.annotations["meta.helm.sh/release-namespace"] // "")"' 2>/dev/null | \
jq -r '.items[] | select(.metadata.name | startswith("workload-variant-autoscaler-")) | "\(.metadata.name)\t\(.metadata.annotations["meta.helm.sh/release-namespace"] // "")"' 2>/dev/null | \

Copilot uses AI. Check for mistakes.
Comment on lines +806 to +813
kubectl get "$kind" -o json 2>/dev/null | \
jq -r '.items[] | select(.metadata.name | contains("workload-variant-autoscaler")) | "\(.metadata.name)\t\(.metadata.annotations["meta.helm.sh/release-namespace"] // "")"' 2>/dev/null | \
while IFS=$'\t' read -r name ns; do
if [ "$ns" = "$LLMD_NAMESPACE" ] || [ "$ns" = "$LLMD_NAMESPACE_B" ] || [ "$ns" = "$WVA_NAMESPACE" ]; then
echo " Deleting $kind/$name (owned by PR namespace '$ns')"
kubectl delete "$kind" "$name" --ignore-not-found || true
fi
done

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jq command and loop logic at lines 806-813 are identical to lines 571-578 except for the namespace checking condition. Consider extracting this into a shell function to reduce code duplication and improve maintainability. For example, define a function cleanup_wva_resources() that takes the kind and a namespace-matching function as parameters.

Copilot uses AI. Check for mistakes.
The helmfile uses a generic release name "workload-variant-autoscaler"
which produces non-unique ClusterRole names. On shared clusters, these
may be owned by another namespace's release, causing Helm ownership
conflicts.

Pre-cleanup: adopt shared resources by patching Helm release annotations
to our namespace. Post-cleanup: delete resources owned by our namespaces.
Uses jq (not jsonpath) to handle annotation keys with dots/slashes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Andrew Anderson <andy@clubanderson.com>
@clubanderson
clubanderson force-pushed the fix/ci-orphan-clusterrole-cleanup branch from 923aa70 to e3edd2c Compare February 13, 2026 04:37
@github-actions

Copy link
Copy Markdown
Contributor

GPU Pre-flight Check ✅

GPUs are available for e2e-openshift tests. Proceeding with deployment.

Resource Total Allocated Available
GPUs 96 63 33
Cluster Value
Nodes 18 (12 with GPUs)
Total CPU 2721 cores
Total Memory 24307 Gi
GPUs required 4 (min) / 6 (recommended)

@clubanderson

Copy link
Copy Markdown
Contributor Author

@lionelvillard @ev-shindin — This PR adds orphan ClusterRole cleanup to the CI E2E workflow, preventing Helm ownership conflicts when running on shared OpenShift clusters. Would appreciate a review/merge to unblock CI. Thanks!

@clubanderson

Copy link
Copy Markdown
Contributor Author

Consolidated into #719

clubanderson added a commit that referenced this pull request Feb 13, 2026
- Fix CI E2E: clean up orphaned cluster-scoped WVA resources
- Fix Helm chart: make ClusterRole names unique per namespace
- Fix broken reusable workflow references (@2b273d6 → @main)

Consolidates #715, #716, and #718.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Andrew Anderson <andy@clubanderson.com>
clubanderson added a commit that referenced this pull request Feb 13, 2026
- Fix CI E2E: clean up orphaned cluster-scoped WVA resources
- Fix Helm chart: make ClusterRole names unique per namespace
- Fix broken reusable workflow references (@2b273d6 → @main)

Consolidates #715, #716, and #718.

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
zdtsw pushed a commit to zdtsw-forking/llm-d-autoscaling that referenced this pull request Mar 3, 2026
…-d#719)

- Fix CI E2E: clean up orphaned cluster-scoped WVA resources
- Fix Helm chart: make ClusterRole names unique per namespace
- Fix broken reusable workflow references (@2b273d6 → @main)

Consolidates llm-d#715, llm-d#716, and llm-d#718.

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
@lionelvillard
lionelvillard deleted the fix/ci-orphan-clusterrole-cleanup branch May 27, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants