feat: add Kubernetes Secrets as environment variables to pipeline steps - #891
feat: add Kubernetes Secrets as environment variables to pipeline steps#891cordeirops wants to merge 8 commits into
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
hi @cordeirops I went quickly through the PR and I think it will have to wait for #894 to be implemented - otherwise we would have like 5 configure buttons for step which seems overwhelming ( #889 also adds a configure button for step). |
@ada333 thanks for the feedback! I'll take a look at #889 and try to work on that as well. |
Addresses kubeflow#894. The cell metadata editor had a separate button + dialog per config option (IMAGE, GPU, CACHE), and reviewers on kubeflow#889 and kubeflow#891 pointed out this doesn't scale - each new option adds another button, which was already about to become five with those two PRs pending. Replace the three standalone dialogs (BaseImageDialog, GpuDialog, CacheDialog) with a single StepConfigDialog behind one gear-icon "Configure step" button, using tabs (Base Image / GPU / Caching) instead of separate buttons. Tabs are wired through a `{label, render}[]` array that Tabs/DialogContent map over, so adding a new per-step option going forward means appending one entry to that array, not adding another button/dialog pair or a fourth hardcoded tab index. No backend or tag-format changes - this is a pure UI reorganization. Updated the Playwright e2e assertion that checked for the three separate button titles to check for the new button's aria-label. Tested manually end-to-end in a running JupyterLab: - Opened the Configure dialog and confirmed all three tabs render their original content, pre-populated correctly from existing limit:/cache:/image: tags. - Confirmed Base Image, GPU, and Caching each write the correct notebook tag when changed (verified via live React state and, for GPU, a round trip to the saved .ipynb). - Confirmed the "Reset to Default" button both clears the value and closes the dialog. - Confirmed the dialog always reopens on the Base Image tab, even after being left on a different tab in a previous session, by resetting activeTab whenever the dialog opens. - Confirmed Caching shows the correct radio immediately with no flash of the wrong value, since its value is derived directly from the enableCaching prop instead of synced into local state. - Confirmed the dialog's aria-labelledby/aria-describedby are present (parity with the original GPU dialog's accessibility wiring). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
165d7a6 to
0e09bb7
Compare
Update: rebased onto #908 (closes #894)Depends on: This PR now targets While this was in review, #908 came out of feedback on this same PR: What changed since the last review
Why local state instead of deriving from propsBase Image/GPU/Caching in #908 mostly derive their displayed value Test plan
🤖 Generated with Claude Code, reviewed by @cordeirops |
Split BaseImageSection/GpuSection/CacheSection out of StepConfigDialog.tsx
into their own files under dialogs/sections/, one component per file. Stop
closing the whole dialog when Base Image is reset to default. Fix Tab
label color contrast in dark mode.
Also fixes two bugs found while chasing the dark-mode contrast issue,
scoped to the same files:
- Input.tsx discarded any caller-supplied sx prop whenever the field
had no validation error, since the error-state sx was assigned after
spreading {...rest} in the same JSX element. This silently broke
GpuSection's disabledFieldSx on the GPU Count field.
- GpuSection's disabledFieldSx hardcoded a dark-theme-only white text
color instead of a --jp-* variable, and now also covers the label
and border colors for the disabled GPU Count/GPU Type fields.
Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
…-panel # Conflicts: # labextension/src/widgets/cell-metadata/CellMetadataEditor.tsx
Addresses kubeflow#873 - lets a step consume a Kubernetes Secret value as an env var (`secret:secret_name:secret_key:ENV_VAR_NAME` cell tag) instead of hardcoding credentials in the notebook. Compiles to kfp.kubernetes.use_secret_as_env, following the same tag/StepConfig/ template pattern already used for labels, annotations, and limits. Rebased onto the unified Configure Step panel (kubeflow#894) instead of adding a fifth standalone button/dialog: Secrets is now a tab in StepConfigDialog alongside Base Image / GPU / Caching, wired through the same {label, render} array those tabs already use. Backend: - New SECRET_TAG grammar in nbprocessor.py, parsed both per-cell and as a pipeline-wide steps_defaults entry. - New StepConfig.secrets field with K8sSecretsValidator (validates env var name, Secret name, and Secret key format). - Template renders one use_secret_as_env(...) call per secret. Frontend: - New "Secrets" tab in StepConfigDialog with a dynamic add/remove list of Secret Name / Secret Key / Env Var Name rows. Local row state resyncs from the committed tags via a lazy useState initializer each time the tab is (re)selected, rather than a useEffect keyed to a dialog-open prop - this section only ever mounts when its tab is active, so mount time already is "tab became visible." - TagsUtils/useCellTags updated to parse, serialize, and thread the new `secrets` field alongside the existing limits/baseImage/enableCaching state. Tested end-to-end against a local KFP cluster: tagged a step, created a matching Kubernetes Secret, ran the pipeline, and confirmed the injected value was readable via os.environ inside the step. Also re-verified in a running JupyterLab after the StepConfigDialog rebase: Secrets tab renders alongside the other three, pre-populates from existing secret: tags, adding/editing rows commits correctly, and rows survive switching to another tab and back. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
0e09bb7 to
8c1b73e
Compare
…log (#908) * feat: consolidate per-step config buttons into a single Configure dialog Addresses #894. The cell metadata editor had a separate button + dialog per config option (IMAGE, GPU, CACHE), and reviewers on #889 and #891 pointed out this doesn't scale - each new option adds another button, which was already about to become five with those two PRs pending. Replace the three standalone dialogs (BaseImageDialog, GpuDialog, CacheDialog) with a single StepConfigDialog behind one gear-icon "Configure step" button, using tabs (Base Image / GPU / Caching) instead of separate buttons. Tabs are wired through a `{label, render}[]` array that Tabs/DialogContent map over, so adding a new per-step option going forward means appending one entry to that array, not adding another button/dialog pair or a fourth hardcoded tab index. No backend or tag-format changes - this is a pure UI reorganization. Updated the Playwright e2e assertion that checked for the three separate button titles to check for the new button's aria-label. Tested manually end-to-end in a running JupyterLab: - Opened the Configure dialog and confirmed all three tabs render their original content, pre-populated correctly from existing limit:/cache:/image: tags. - Confirmed Base Image, GPU, and Caching each write the correct notebook tag when changed (verified via live React state and, for GPU, a round trip to the saved .ipynb). - Confirmed the "Reset to Default" button both clears the value and closes the dialog. - Confirmed the dialog always reopens on the Base Image tab, even after being left on a different tab in a previous session, by resetting activeTab whenever the dialog opens. - Confirmed Caching shows the correct radio immediately with no flash of the wrong value, since its value is derived directly from the enableCaching prop instead of synced into local state. - Confirmed the dialog's aria-labelledby/aria-describedby are present (parity with the original GPU dialog's accessibility wiring). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com> * fix: address review feedback on Configure Step dialog Split BaseImageSection/GpuSection/CacheSection out of StepConfigDialog.tsx into their own files under dialogs/sections/, one component per file. Stop closing the whole dialog when Base Image is reset to default. Fix Tab label color contrast in dark mode. Also fixes two bugs found while chasing the dark-mode contrast issue, scoped to the same files: - Input.tsx discarded any caller-supplied sx prop whenever the field had no validation error, since the error-state sx was assigned after spreading {...rest} in the same JSX element. This silently broke GpuSection's disabledFieldSx on the GPU Count field. - GpuSection's disabledFieldSx hardcoded a dark-theme-only white text color instead of a --jp-* variable, and now also covers the label and border colors for the disabled GPU Count/GPU Type fields. Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com> --------- Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…fied-panel # Conflicts: # labextension/src/widgets/cell-metadata/CellMetadataEditor.tsx # labextension/src/widgets/cell-metadata/dialogs/StepConfigDialog.tsx
|
Not a maintainer — just a contributor who's been working in Three observations, one of which is just a heads-up. 1. The same rules are encoded twice, in two files.
They agree today, but nothing keeps them in sync — relax one and the other silently keeps rejecting (or accepting). Building 2. Related: the validators don't run on the pipeline-level path.
That's pre-existing behaviour ( 3. Dotted Secret names are rejected — intentional? Kubernetes Secret names are DNS subdomain names, so Since the tag is Also a heads-up rather than a review point: this adds On the template — the interpolated values end up inside string literals in generated Python, so it's the validators/regex that keep quotes out of |
ada333
left a comment
There was a problem hiding this comment.
@cordeirops great work! I have some comments but otherwise looks good.
| key, value = get_limit_from_tag(parts) | ||
| result["limits"][key] = value | ||
|
|
||
| if conf_type == "secret": |
There was a problem hiding this comment.
I would use string constants especially if string is used in multiple places
There was a problem hiding this comment.
Same problem is also in TagsUtils.py
| ): { [envName: string]: ISecretRef } => { | ||
| const secrets: { [envName: string]: ISecretRef } = {}; | ||
| rows.forEach(row => { | ||
| if (row.envName) { |
There was a problem hiding this comment.
this allows to have secret with only env name - I think all the fields (secret name, secret key) are mandatory so I would check existence of all the fields
| const secrets: { [envName: string]: ISecretRef } = {}; | ||
| rows.forEach(row => { | ||
| if (row.envName) { | ||
| secrets[row.envName] = { |
There was a problem hiding this comment.
I would also add validation here in this function - for the regex. It will be better for the user to see that the secret does not fit the regex in UI rather than after compiling in some backend error.
| if (row.envName) { | ||
| secrets[row.envName] = { | ||
| secretName: row.secretName, | ||
| secretKey: row.secretKey, |
agree especially with the first point |
Per ada333's review: - Derive SECRET_TAG in nbprocessor.py from the same validator regexes applied to StepConfig.secrets (K8sSecretNameValidator/K8sSecretKeyValidator/ EnvVarNameValidator), instead of duplicating the patterns inline. Also extract the 'secret_name'/'secret_key' dict keys into shared constants in validators.py, reused by both call sites in nbprocessor.py. - SecretsSection.tsx: a row is now only written to the notebook's tags once every field (secret name, secret key, env var name) is present and matches its expected format, instead of just checking envName. The same check now also disables "Add Secret" until the current row is complete, so users can't pile up empty or malformed rows. Per harshhh817's review: - K8sSecretNameValidator now validates Secret names as DNS subdomains (dot-separated labels), matching real Kubernetes Secret name rules, instead of inheriting K8sNameValidator's stricter single-label rule. Deriving SECRET_TAG from the validator means the tag regex picks up this fix for free. - Added a template comment noting that secret_name/secret_key/env_name land inside string literals in generated code, so the validators are what keep quote characters out of them. The pipeline-level steps_defaults path bypassing field validators (harshhh817's point 2) and the kale/processors/constants.py move in kubeflow#909 (heads-up, not a review point) are pre-existing/out-of-scope for this PR. Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
SecretsSection unmounts whenever the user leaves the Secrets tab (only the active tab's content is rendered), so a row the user hadn't finished typing yet (excluded from what's committed to tags, since it fails validation) was silently lost on remount. StepConfigDialog now owns a draftRowsRef that survives the section's mount/unmount cycles across tab switches, and clears it only when the whole (modal) dialog closes. Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
|
@ada333 and @harshhh817, thanks for the review and feedback. @ada333
@harshhh817
The pipeline-level Also fixed a UX bug found during testing: an in-progress Secrets row (not yet valid, so not written to tags) was silently lost when you switched to another tab and back, because the Secrets tab's content unmounts on tab switch. It now survives tab switches and only clears when the dialog itself closes. All changes pushed to this branch. Let me know if anything's still off. |
|
Deriving I re-composed the new patterns and checked the cases I had in mind:
So dotted Secret names work and the empty-label case is still rejected. Agreed the Nice catch on the tab-switch state loss too. LGTM from my side (non-binding — not a maintainer). |
ada333
left a comment
There was a problem hiding this comment.
@cordeirops thanks for the changes, I noticed when adding secrets to pipeline steps it is not in the metadata text above the cell, unlike other step properties (GPU request, Base Image, ...) :
Secrets weren't shown in the metadata text above the cell, unlike GPU request, Base Image, Caching, and HTML Report. Adds a "Secrets: ENV_NAME, ..." line listing the configured env var names, following the same pattern as the other properties. Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
|
@harshhh817 @ada333 thanks for the extra look. @harshhh817, appreciate you re-checking the SECRET_TAG/validator patterns and confirming dotted names now work, and the empty-label case is still rejected. Agreed, the steps_defaults validator bypass deserves its own issue rather than riding along here. @ada333 good catch, Secrets weren't showing up in the metadata text above the cell like GPU request, Base Image, Caching, and HTML Report do. Fixed: the inline text now includes a "Secrets: ENV_NAME, ..." line listing the configured env var names, same pattern as the other properties. Pushed in 379e134. |


Summary
Implements #873 — lets a pipeline step consume a Kubernetes Secret value
as an environment variable, instead of hardcoding credentials in the
notebook.
A new cell tag,
secret:secret_name:secret_key:ENV_VAR_NAME, is parsedand compiled to a
kfp.kubernetes.use_secret_as_env(...)call for thecorresponding step — following the exact same tag /
StepConfig/Jinja2-template pattern already used for labels, annotations, and
resource limits.
Changes
Backend
kale/processors/nbprocessor.py: newSECRET_TAGgrammar(
secret:<name>:<key>:<ENV_VAR>), parsed both as a per-cell tag andas a pipeline-wide
steps_defaultsentry.kale/step.py: newStepConfig.secretsfield(
{ENV_VAR_NAME: {secret_name, secret_key}}).kale/config/validators.py: newK8sSecretsValidatorchain(
EnvVarNameValidator,K8sSecretNameValidator,K8sSecretKeyValidator,K8sSecretRefValidator) validating the env var name and theSecret's name/key format.
kale/templates/pipeline_template.jinja2: imports and callsuse_secret_as_env(...)once per configured secret.iris.py,pipeline_parameters_and_metrics.py)updated for the new import line.
Frontend
SecretsDialog.tsx: aSECRETSbutton on the cell metadataeditor opens a dialog with a dynamic add/remove list of
Secret Name / Secret Key / Env Var Name rows.
TagsUtils.ts/useCellTags.ts/CellMetadataEditor.tsx/InlineCellsMetadata.tsx: parse, serialize, and thread the newsecretsfield alongside the existinglimits/baseImage/enableCachingstate, so it survives every other metadata editinstead of being dropped.
Tests
kale/tests/unit_tests/test_secrets.py(new): validator edge cases,StepConfig.secretsdefaults/validation, and thesteps_defaultssecret path.
kale/tests/unit_tests/test_parser.py: tag-parsing success/errorcases for
secret:tags (missing step name, malformed name/key/env,multiple secrets on one cell).
Testing
Went through an 8-angle automated code review pass and fixed everything
it surfaced as a real bug before this PR:
SecretsDialogrow could silently disappear if Secret Name/Key wastyped before Env Var Name (state now sourced locally, not derived
fresh from props every render).
InlineCellsMetadata.tsxnever actually passedsecretsto themounted editor — the dialog always opened empty.
secret:tags used as pipeline-widesteps_defaultswere acceptedby validation but silently dropped (no parsing branch existed).
SECRET_TAG's regex allowed a secret name to start with a digit,inconsistent with
K8sSecretNameValidator(letter-only), causing aconfusing two-stage validation mismatch.
Then tested end-to-end against a local KFP cluster (k3d):
db-credentials/password→DB_PASSWORDmapping via the SECRETS dialog.
make kfp-compileand confirmed the generated DSLcalls
use_secret_as_env(...)with the rightsecret_key_to_env.db-credentialsKubernetes Secret in thecluster's namespace.
make kfp-run) and confirmedos.environ.get("DB_PASSWORD")inside the step returned the actualsecret value (verified via the step's output artifact/logs).
Test plan
make test-backend-unitmake test-labextensionconfirm they persist across dialog close/reopen
Kubernetes Secret
🤖 Generated with Claude Code, commanded/reviewed by @cordeirops