Skip to content

[SPARK-59519] Add opt-in Kueue RBAC rules to Helm chart - #827

Closed
dongjoon-hyun wants to merge 4 commits into
apache:mainfrom
dongjoon-hyun:SPARK-59519
Closed

dongjoon-hyun wants to merge 4 commits into
apache:mainfrom
dongjoon-hyun:SPARK-59519

Conversation

@dongjoon-hyun

@dongjoon-hyun dongjoon-hyun commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

What changes were proposed in this pull request?

This PR adds an opt-in Helm value operatorRbac.kueue.enabled (default false). When enabled, the operator is granted the RBAC rules that Kueue requires from an external framework integration:

Group / Resource Verbs Granted via
kueue.x-k8s.io / workloads get, list, watch, create, update, patch, delete ClusterRole and Role
kueue.x-k8s.io / workloads/status get, update, patch ClusterRole and Role
kueue.x-k8s.io / workloads/finalizers update ClusterRole and Role
kueue.x-k8s.io / resourceflavors, workloadpriorityclasses get, list, watch ClusterRole only
scheduling.k8s.io / priorityclasses get, list, watch ClusterRole only

The namespaced workloads rules live in the shared operatorRbacRules block under {{- if }}, like the existing leases rule. The cluster-scoped resources go into a new operatorClusterRbacRules wrapper used only by the ClusterRole, since a namespaced Role cannot grant them. events.k8s.io/events from the Kueue doc is intentionally left out: the operator only emits core events, which are already granted.

Also included: helm test assertions for the new grants, a helm-tests CI group kueue that installs Kueue v0.19.4 and runs them, a check that workloads create is denied with the default values, and a docs/operations.md entry.

Why are the changes needed?

This is the deployment-side preparation for the Kueue integration (SPARK-59486, SPARK-59490, SPARK-59503). Keeping the grant opt-in avoids widening the operator ClusterRole for users who do not run Kueue. The operator runtime changes and the Kueue-side integrations.externalFrameworks configuration are out of scope.

Does this PR introduce any user-facing change?

Yes, a new Helm value operatorRbac.kueue.enabled (default false). With the default, the rendered manifests are identical to the current chart.

How was this patch tested?

  • helm lint --strict passes.
  • helm template output with the default values is identical to main; with operatorRbac.kueue.enabled=true the ClusterRole gets all five rules and, when operatorRbac.role.create=true, each workload-namespace Role gets only the three workloads rules.
  • --set operatorRbac.kueue=null is now rejected by the values schema like every other operatorRbac block.
  • New helm-tests / kueue CI job: installs Kueue, runs helm test with the value enabled, then upgrades to the default values and asserts the operator service account is denied create on workloads.kueue.x-k8s.io.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Fable 5.1

@peter-toth peter-toth 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.

Thanks for the PR, @dongjoon-hyun!

The four rules match Kueue's external-framework marker set and the gate is off by default. I rendered the chart with the default values and the output is byte-identical to main, so the "no user-facing change" claim holds. Two things I'd fix before merge. Nothing in CI ever renders or runs the new helm test assertions. And the grant is a subset of the doc it cites, missing scheduling.k8s.io/priorityclasses, which the already-merged WorkloadSpec.priority field will need. Two smaller points below, on where the cluster-scoped rules land and on the schema's required list.

Blocking

Non-blocking

Minor

  • 5. operations.md row omits SparkCluster and overstates the clusterRole.create dependency: The workloads half works through a namespaced Role too. [inline: docs/operations.md:111]


# The Kueue grant is opt-in and, like Gateway API, can only be asserted
# where the Kueue CRDs are installed.
if kubectl api-resources --api-group=kueue.x-k8s.io --no-headers -o name | grep -q workloads; then

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.

Finding 1. These assertions cannot run in any CI job as things stand, so the new rules ship with no automated coverage.

Two gates have to open and neither does:

  • The Helm {{- if .Values.operatorRbac.kueue.enabled }} at :80. No workflow or values file sets it, grep -rni kueue .github/ tests/ returns nothing.
  • The kubectl api-resources --api-group=kueue.x-k8s.io check on this line. No CI cluster installs the Kueue CRDs.

The repo already has the mechanism. .github/workflows/build_and_test.yml:270-279 runs a helm-tests matrix that installs the chart with tests/e2e/helm/helm-test-values/<group>/values.yaml and then helm test spark. A new group opens the first gate:

# tests/e2e/helm/helm-test-values/kueue/values.yaml
operatorRbac:
  kueue:
    enabled: true

plus - kueue in the test-group list at :239-242. That alone proves the chart renders and installs with the flag on. To open the second gate, the job needs the Kueue CRDs, which is one guarded step before helm install:

      - name: Install Kueue CRDs
        if: matrix.test-group == 'kueue'
        run: |
          kubectl apply --server-side -f https://github.com/kubernetes-sigs/kueue/releases/download/v0.19.4/manifests.yaml

One more assertion worth adding in that job: with the default enabled: false the operator should be denied create on workloads.kueue.x-k8s.io. Nothing currently proves the opt-in gate actually gates. That check is only meaningful on a cluster that serves the CRDs, so it belongs here rather than in the default helm test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 8225336. Added tests/e2e/helm/helm-test-values/kueue/values.yaml and a kueue group in the helm-tests matrix. The group installs Kueue v0.19.4 before helm install, runs helm test with the value enabled, then helm upgrades back to the default values and asserts that the operator service account is denied create on workloads.kueue.x-k8s.io via kubectl auth can-i --as. I put the denial check in the workflow step rather than the helm test hook since it needs a cluster that serves the CRDs and the value turned off.

- get
- list
- watch
{{- end }}

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.

Finding 2. The description says this grants "the RBAC rules that Kueue requires from an external framework integration" and links the custom-job doc. That doc lists seven +kubebuilder:rbac markers under "Extend your existing RBAC Authorizations". This block ships five.

Missing:

  • scheduling.k8s.io/priorityclasses with get;list;watch. This is the one that matters. WorkloadSpec already carries priority and priorityClassRef (spark-operator/src/main/java/org/apache/spark/k8s/operator/kueue/v1beta2/WorkloadSpec.java:47-48). As soon as the runtime resolves a pod's priorityClassName into a Workload priority it will 403 here.
  • events.k8s.io/events with create;watch;update;patch. RBAC matches API groups literally, so the apiGroups: [""] rule at :21-36 does not cover this group. The operator emits no Events today, so leaving it out is defensible. Then it is worth saying so, rather than letting the table read as the complete set.

The suggestion below adds the first one. I rendered it with --set operatorRbac.kueue.enabled=true and ran helm lint --strict, both pass.

Suggested change
{{- end }}
- apiGroups:
- "scheduling.k8s.io"
resources:
- priorityclasses
verbs:
- get
- list
- watch
{{- end }}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added scheduling.k8s.io/priorityclasses (get, list, watch) in 8225336. Since PriorityClass is cluster-scoped too, I placed it in the new ClusterRole-only block from Finding 3 instead of the shared one. events.k8s.io/events stays out, and the PR description now says why: the operator only emits core events, which are already granted.

- apiGroups:
- "kueue.x-k8s.io"
resources:
- resourceflavors

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.

Finding 3. Both of these are cluster-scoped. Kueue marks them +kubebuilder:resource:scope=Cluster in apis/kueue/v1beta2/resourceflavor_types.go:28 and workloadpriorityclass_types.go:27. This define also backs the per-workload-namespace Role at :263, and a rule for a cluster-scoped resource in a namespaced Role is silently inert.

Rendering with --set operatorRbac.kueue.enabled=true --set operatorRbac.role.create=true --set workloadResources.namespaces.data={spark-1} puts both into the spark-1 Role, where they grant nothing. Every other rule in this block is for a namespaced resource, so this is the first one that splits.

The values.yaml comment already says clusterRole.create is required for these two. The template can say it instead. Moving the pair into a ClusterRole-only wrapper works:

{{/*
Rules used only by the operator ClusterRole, for cluster-scoped resources
*/}}
{{- define "spark-operator.operatorClusterRbacRules" }}
{{- include "spark-operator.operatorRbacRules" . }}
{{- if .Values.operatorRbac.kueue.enabled }}
  - apiGroups:
      - "kueue.x-k8s.io"
    resources:
      - resourceflavors
      - workloadpriorityclasses
    verbs:
      - get
      - list
      - watch
{{- end }}
{{- end }}

with :210 calling the wrapper and :263 keeping the plain define. The workloads, workloads/status and workloads/finalizers rules stay shared, since Workload is namespaced. I applied this and re-rendered: the operator ClusterRole keeps both resources, the spark-1 Role keeps workloads and drops them, and helm lint --strict passes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 8225336. Added spark-operator.operatorClusterRbacRules as you sketched it, used by the ClusterRole only. It carries resourceflavors, workloadpriorityclasses and the new priorityclasses rule; the per-namespace Role keeps only the three workloads rules. Re-rendered with role.create=true to confirm the split, and the default render is still identical to main.

}
}
},
"kueue": {

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.

Finding 4. kueue should also join the operatorRbac.required list at :427. Both operator-rbac.yaml:125 and tests/test-rbac.yaml:80 dereference .Values.operatorRbac.kueue.enabled unconditionally, and every other sub-block they dereference is already required.

The difference shows up when the block goes missing:

$ helm template t . --set 'operatorRbac.clusterRole=null'
Error: values don't meet the specifications of the schema(s) in the following chart(s):
spark-kubernetes-operator:
- at '/operatorRbac': missing property 'clusterRole'

$ helm template t . --set 'operatorRbac.kueue=null'
Error: template: spark-kubernetes-operator/templates/tests/test-rbac.yaml:80:24:
executing "..." at <.Values.operatorRbac.kueue.enabled>: nil pointer evaluating interface {}.enabled

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 8225336. kueue is now in operatorRbac.required, and --set operatorRbac.kueue=null fails with missing property 'kueue' like the sibling blocks.

Comment thread docs/operations.md Outdated
| operatorRbac.configManagement.create | Enable this to create a Role for operator configuration management (hot property loading and leader election). | true |
| operatorRbac.configManagement.roleName | Role name for operator configuration management. | `spark-operator-config-role` |
| operatorRbac.configManagement.roleBinding | RoleBinding name for operator configuration management. | `"spark-operator-config-monitor-role-binding"` |
| operatorRbac.kueue.enabled | Grant the operator access to Kueue `workloads`, `resourceflavors` and `workloadpriorityclasses`. Needs `clusterRole.create`. Also register `SparkApplication` in Kueue. | false |

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.

Finding 5. Two things in this cell.

KueueWorkloadFactory.buildWorkload has a SparkCluster overload (spark-operator/src/main/java/org/apache/spark/k8s/operator/kueue/KueueWorkloadFactory.java:124), so the registration note should name both kinds. Kueue takes them in integrations.externalFrameworks as Kind.version.group, which is worth spelling out because it is not guessable from the cell.

"Needs clusterRole.create" is true only for resourceflavors and workloadpriorityclasses. Workload is namespaced, so the workloads half works through the per-namespace Role too.

Suggested change
| operatorRbac.kueue.enabled | Grant the operator access to Kueue `workloads`, `resourceflavors` and `workloadpriorityclasses`. Needs `clusterRole.create`. Also register `SparkApplication` in Kueue. | false |
| operatorRbac.kueue.enabled | Grant the operator access to Kueue `workloads`, `resourceflavors` and `workloadpriorityclasses`. The two cluster-scoped ones need `clusterRole.create`. Also register `SparkApplication.v1.spark.apache.org` and `SparkCluster.v1.spark.apache.org` in Kueue's `integrations.externalFrameworks`. | false |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 8225336. The row now names both SparkApplication.v1.spark.apache.org and SparkCluster.v1.spark.apache.org, limits the clusterRole.create note to the cluster-scoped resources, and also lists priorityclasses. The values.yaml comment was updated to match.

@dongjoon-hyun

Copy link
Copy Markdown
Member Author

Thank you for the review, @peter-toth. All five findings are addressed in 8225336 and the PR description is updated. The new helm-tests / kueue job is the first one to actually run the Kueue assertions, so I'll watch that CI result.

@peter-toth peter-toth 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.

Re-checked through 7f5aeac — findings 1-5 resolved, nothing regressed. The new helm-tests / kueue job passed on this head, the per-namespace Role renders with only the three workloads rules, and --set operatorRbac.kueue=null now gives the clean schema error. Four new points, none blocking.

Non-blocking

  • 6. Description's events.k8s.io/events sentence is wrong (new): The Kueue doc's seventh marker is groups="",resources=events — the core group, which operator-rbac.yaml:21-36 already grants with a superset of create;watch;update;patch. Nothing from that list is left out. My round-1 finding 2 had the group wrong and the sentence inherited it.
  • 7. The default-denial check passes on any command failure (new): kubectl auth can-i exits non-zero for a denial and for an error alike, so a wrong impersonated subject keeps the assertion green. One --as call before the upgrade pins it. [inline: .github/workflows/build_and_test.yml:293]
  • 8. Kueue is not in Optional Prerequisites (late catch): docs/operations.md:31-39 is the section for an optional feature whose CRDs the chart does not bundle, and Gateway API already has an entry there. Kueue is the same shape but appears only as a values-table cell. [inline: docs/operations.md:111]

Minor

Comment on lines +291 to +293
helm upgrade spark -f build-tools/helm/spark-kubernetes-operator/values.yaml \
build-tools/helm/spark-kubernetes-operator/
if kubectl auth can-i create workloads.kueue.x-k8s.io --as=system:serviceaccount:default:spark-operator; then exit 1; fi

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.

Finding 7. kubectl auth can-i exits non-zero for a denial and for an error alike. So this assertion is satisfied by anything that makes the command fail: a typo in the service account name, a release namespace other than default, an API error.

Nothing else in the job pins that subject string. helm test does prove the grant works, but it runs inside the pod as the service account itself, never through --as, so the two halves share no identity.

Running the same impersonated check once before the upgrade fixes it. It must answer "yes" while the value is still on:

Suggested change
helm upgrade spark -f build-tools/helm/spark-kubernetes-operator/values.yaml \
build-tools/helm/spark-kubernetes-operator/
if kubectl auth can-i create workloads.kueue.x-k8s.io --as=system:serviceaccount:default:spark-operator; then exit 1; fi
# The same impersonated check must answer "yes" first, otherwise a wrong subject
# would make the denial below vacuous.
kubectl auth can-i create workloads.kueue.x-k8s.io --as=system:serviceaccount:default:spark-operator
helm upgrade spark -f build-tools/helm/spark-kubernetes-operator/values.yaml \
build-tools/helm/spark-kubernetes-operator/
if kubectl auth can-i create workloads.kueue.x-k8s.io --as=system:serviceaccount:default:spark-operator; then exit 1; fi

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in e53b287. The same impersonated kubectl auth can-i create workloads.kueue.x-k8s.io --as=... now runs before the helm upgrade and must answer "yes", so a wrong subject fails there instead of making the denial vacuous.

Comment thread docs/operations.md Outdated
| operatorRbac.configManagement.create | Enable this to create a Role for operator configuration management (hot property loading and leader election). | true |
| operatorRbac.configManagement.roleName | Role name for operator configuration management. | `spark-operator-config-role` |
| operatorRbac.configManagement.roleBinding | RoleBinding name for operator configuration management. | `"spark-operator-config-monitor-role-binding"` |
| operatorRbac.kueue.enabled | Grant the operator access to Kueue `workloads`, `resourceflavors`, `workloadpriorityclasses` and to `priorityclasses`. The cluster-scoped ones need `clusterRole.create`. Also register `SparkApplication.v1.spark.apache.org` and `SparkCluster.v1.spark.apache.org` in Kueue's `integrations.externalFrameworks`.| false |

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.

Finding 8. Optional Prerequisites at :31-39 is the section for exactly this: an optional feature whose CRDs the chart does not bundle. Gateway API has an entry there saying what to install and what breaks without it. Kueue is the same shape and gets only a values-table cell, which is also where the integrations.externalFrameworks registration ended up — three sentences in a table column next to a one-word false.

Suggested bullet after the Gateway API one:

- **Kueue** (`workloads.kueue.x-k8s.io`, `resourceflavors.kueue.x-k8s.io`,
  `workloadpriorityclasses.kueue.x-k8s.io`) — required only when `operatorRbac.kueue.enabled` is
  set. Kueue is not bundled with the operator; install it from
  [kueue.sigs.k8s.io](https://kueue.sigs.k8s.io/docs/installation/), and register
  `SparkApplication.v1.spark.apache.org` and `SparkCluster.v1.spark.apache.org` in Kueue's
  `integrations.externalFrameworks`.

The row can then shrink to the grant itself. It is also missing the space before its closing |:

Suggested change
| operatorRbac.kueue.enabled | Grant the operator access to Kueue `workloads`, `resourceflavors`, `workloadpriorityclasses` and to `priorityclasses`. The cluster-scoped ones need `clusterRole.create`. Also register `SparkApplication.v1.spark.apache.org` and `SparkCluster.v1.spark.apache.org` in Kueue's `integrations.externalFrameworks`.| false |
| operatorRbac.kueue.enabled | Grant the operator access to Kueue `workloads`, `resourceflavors`, `workloadpriorityclasses` and to `priorityclasses`. The cluster-scoped ones need `clusterRole.create`. See [Optional Prerequisites](#optional-prerequisites). | false |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in e53b287. Added the Kueue bullet to Optional Prerequisites right after the Gateway API one, moved the integrations.externalFrameworks registration there, and shrank the table row to the grant itself with a link to that section. The missing space before the closing pipe is fixed too.

kubectl auth can-i watch resourceflavors.kueue.x-k8s.io
kubectl auth can-i watch workloadpriorityclasses.kueue.x-k8s.io
fi
kubectl auth can-i watch priorityclasses.scheduling.k8s.io

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.

Finding 9. This one sits after the fi because PriorityClass is a built-in API and needs no CRD-presence guard. Every other placement in this file carries a comment saying why, and without one it reads as a bracket that slipped.

Suggested change
kubectl auth can-i watch priorityclasses.scheduling.k8s.io
# PriorityClass is built in, so this one needs no CRD-presence guard.
kubectl auth can-i watch priorityclasses.scheduling.k8s.io

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in e53b287. Added the comment explaining that PriorityClass is built in and so needs no CRD-presence guard.

@dongjoon-hyun

Copy link
Copy Markdown
Member Author

For Finding 6: I re-checked the doc source rather than the rendered page. The seventh marker in integrate_a_custom_job.md is groups=events.k8s.io,resources=events,verbs=create;watch;update;patch, and the built-in JobSet integration carries the same group at jobset_controller.go#L59. So the group in the description is right: the chart grants core events only, which does not cover events.k8s.io, and the operator does not use that API. I'll keep the sentence as is.

@dongjoon-hyun dongjoon-hyun added this to the 1.1.0 milestone Sep 15, 2026
@dongjoon-hyun

Copy link
Copy Markdown
Member Author

Merged to main

@dongjoon-hyun
dongjoon-hyun deleted the SPARK-59519 branch September 15, 2026 15:35
@dongjoon-hyun

Copy link
Copy Markdown
Member Author

Thank you again~

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