PR 19766 review, round two

rancher/dashboard PR 19766 (codyrancher), re-requested after e11c386, reviewed 2026-11-04. Round one is here, though its preview links now show the new code.

Preview: rancherpr19766r.mf-created-dashboard.ui.rancher.space, built from e11c386, backend mf Rancher (v2.16.0-7758e3e-head). Baseline: the shared dashboard-master preview, rebuilt today from c13add8ed, the current tip.

Verdict: approve

Test it yourself

The login page defaults to OpenLDAP, so click Log in as a local user first, then use admin and RANCHER_PASSWORD.

  1. Create: open PR: Create Custom, etcd and type any name. Set Cron Schedule to 0 0 * * 7: Create disables, and the icon's tooltip gives the 0-6 range. Then try @every 6h and TZ=UTC 0 0 * * * (Create enabled, hint says Invalid) and TZ=UTC (disabled).
  2. Stored schedules: open PR: Edit, @every 6h. Save is now enabled on load, and it was disabled in round one. PR: Edit, invalid schedule is still disabled with the etcd tab flagged.
  3. Imported: open PR: Imported test-k3s and try the same values under Day 2 Operations. Don't click Save here: test-k3s is a real cluster entry, and my runs intercepted that write.
Your review, Cody's answers, and what I found
Your commentCody's answerChecked on the new build
allowSevenAsSunday lets 0 0 * * 7 through, and K3s and RKE2 then fail silently. The hint even says "only on Sunday"."day of week is 0-6 now, so 7, 1-7, 5-7 and 0,7 block Create, and the error gives the range (0-6, Sunday is 0). The grey hint still reads 'only on Sunday'."True. All four block on Create, Edit and imported, and 0 0 * * 7 on Amazon EC2. The tooltip ends "day of week (0-6, Sunday is 0)", and the etcd tab is flagged. The hint does still read "only on Sunday".
Accept @every and CRON_TZ=, or add a release note. A timezone prefix has to require a space, because TZ=UTC alone crashes the server."@every <duration> and CRON_TZ=/TZ= save (the prefix needs a space after it, so a bare TZ=UTC stays blocked), and every row of your table is a test, including the odd forms robfig parses."True. They save and the payload is unchanged. TZ=UTC, CRON_TZ=UTC and the tab form stay blocked. 28 of the 29 rows have a test. The one without is the unknown timezone, which saves.
Cody's own note on the hint"The grey hint under the field still flags these as it does on master, since it is LabeledInput's own check."True. The hint text is identical on master and the PR for all 105 schedules. See card 3 for what that looks like.
Create Custom with 0 0 * * 7: Create disabled, tooltip gives the 0-6 range, hint says only on Sunday
0 0 * * 7: Create disabled, and the tooltip gives the range. The hint still says Sunday.
Edit form for a cluster storing @every 6h: Save enabled, hint says Invalid cron schedule
Stored @every 6h: Save enabled, disabled in round one. The hint says Invalid, as on master.
Imported cluster with 0 0 * * 7: error icons on the field, sections and Jump to, Save disabled
Imported, 0 0 * * 7: icons on the field, the sections and Jump to, with Save disabled.
What you can write, now
RightWrongDebatable
master11135
Round one of this PR1865
The fix I suggested in round one1955
Now, at e11c3862315

Green means the form does the right thing for the cluster, red means it doesn't, and uncoloured cells are debatable. Saves means Create or Save stays enabled. The small text under "Now" is the grey hint, which is the same on master.

What you writeOn the clustermasterRound one405c0e2Nowe11c386
Accepted everywhere
Five numeric fields
0 */5 * * *0 */12 * * ** * * * *0 0 * * 00 0 * * 6
Snapshots runK3s and RKE2✅ Saves✅ Saves✅ Saveshint: reads fine
Steps, ranges and lists
*/5 * * * *0/5 * * * *5/15 * * * *1-5/2 * * * *1,2,3 * * * **/60 * * * *0 0 * * */7
Snapshots runK3s for some, parser for the rest✅ Saves✅ Saves✅ Saveshint: reads fine
Day and month names, any case
0 0 * * MON0 0 * * mon0 0 * * Mon0 0 * * MON-FRI0 0 * * mon-fri0 0 * * MON,WED,FRI0 0 * * SUN0 0 * JAN *0 0 * jan *0 0 * JAN-MAR *0 0 1 JAN,JUN *
Snapshots runK3s for some, parser for the rest✅ Saves✅ Saves✅ Saveshint: reads fine
Shortcuts
@yearly@annually@monthly@weekly@daily@midnight@hourly
@daily, @midnight and @hourly fired once each at a real local midnight. @weekly, @monthly, @yearly and @annually can't fire in a short test. In the parser they match their numeric forms for 20 years.
Snapshots runK3s and RKE2 for some, parser for the rest✅ Saves✅ Saves✅ Saveshint: reads fine
? in one day field
0 0 ? * MON0 0 1 * ?
Snapshots runK3s for some, parser for the rest✅ Saves✅ Saves✅ Saveshint: reads fine
Leading zeros and extra spaces
00 00 * * *007 * * * * 0 0 * * *0 0 * * * 0 0 * * *
Snapshots runK3s for some, parser for the rest✅ Saves✅ Saves✅ Saveshint: reads fine
A date that never comes (Feb 31)
0 0 31 2 *
Blocking it would need a calendar check. The server runs normally and just never takes a snapshot.
Parses, never firesParser; K3s stays up➖ Saves➖ Saves➖ Saveshint: reads fine
Empty field
(empty)
Rancher sends nothing, so the cluster keeps its default schedule.
Keeps its defaultRancher source➖ Saves➖ Saves➖ Saveshint: no complaint
Where master, round one and this round differ
Day of week 7, alone or in a range
0 0 * * 70 0 * * 1-70 0 * * 5-70 0 * * 0,7
Your first comment, fixed. The grey hint still describes it as Sunday.
No snapshots, no errorK3s and RKE2❌ Saves❌ Saves✅ Blockshint: reads fine
@every with a duration
@every 6h@every 1h30m
Your second comment, fixed. The grey hint still says Invalid, as on master.
Snapshots runK3s and RKE2✅ Saves❌ Blocks✅ Saveshint: Invalid
Timezone prefix with UTC
CRON_TZ=UTC 0 0 * * *TZ=UTC 0 0 * * *CRON_TZ=UTC @dailyTZ=UTC @every 6h
Your second comment, fixed. UTC works with or without timezone data on the node.
Snapshots runK3s and RKE2✅ Saves❌ Blocks✅ Saveshint: Invalid
Timezone prefix with a named zone
CRON_TZ=Asia/Tokyo 0 6 * * *
With timezone data on the node it fires, at the right local time. Without it there are no snapshots and no error, on both K3s and RKE2. The form can't see the node.
Runs only if the node has timezone dataK3s and RKE2➖ Saves➖ Blocks➖ Saveshint: Invalid
Unknown timezone name
TZ=Nope 0 0 * * *
The one wrong row left. The code comment says the zone isn't checked, for the reason in the row above. On K3s: node Ready, 0 snapshots, no error.
No snapshots, no errorK3s❌ Saves✅ Blocks❌ Saveshint: Invalid
@every too long to parse
@every 999999999h
Now blocked, at the parser's real limit: @every 2562047h saves and @every 2562048h blocks. On K3s this round: 0 snapshots, no error.
No snapshots, no errorK3s❌ Saves✅ Blocks✅ Blockshint: Invalid
@every with zero or under a second
@every 0s@every 500ms
About 70 snapshots in 4 minutes on K3s. Valid, but rarely what anyone means. The PR now behaves like master here.
A snapshot every few secondsK3s➖ Saves➖ Blocks➖ Saveshint: Invalid
@every with a negative duration
@every -1h
Same burst of snapshots. The PR has a test row that blocks it. That differs from the parser and is the better result.
A snapshot every few secondsK3s➖ Saves➖ Blocks➖ Blockshint: Invalid
? in both day fields or in minute or hour
0 0 ? * ?? ? ? ? ?0 ? * * *0 0 ? ? *
Now saves. These fired on K3s in round one.
Snapshots runK3s✅ Saves❌ Blocks✅ Saveshint: Invalid
Stray commas
1,,3 * * * *1, * * * *
Now saves. These fired on K3s in round one.
Snapshots runK3s✅ Saves❌ Blocks✅ Saveshint: Invalid
Other odd forms that happen to parse
+1 * * * **-5 * * * *?/2 * * * *@every .5h@every 1.hTZ=UTC @daily
Now saves, so I ran them this round: *-5, ?/2, a leading + and a .5 duration took snapshots on K3s and RKE2. A 1. duration and a trailing space after a timezone did on K3s.
Snapshots runK3s and RKE2 for some, parser for the rest✅ Saves❌ Blocks✅ Saveshint: Invalid
Crash: master saves these, and the K3s or RKE2 server stops at startup
Timezone with nothing after it, or a tab instead of the space
TZ=UTCCRON_TZ=UTCTZ=UTC<tab>@daily
The parser panics rather than returning an error, and the server exits. In the generated comparison 10,449 schedules panic the parser, and the PR saves none of them.
Server stops at startupK3s and RKE2❌ Saves✅ Blocks✅ Blockshint: Invalid
Broken: master saves these, and the cluster takes no snapshots
Six or four fields
* * * * * ** * * *0 0 * * * 2026
The issue's case.
No snapshots, no errorK3s for some, parser for the rest❌ Saves✅ Blocks✅ Blockshint: Invalid
Out of range
60 * * * *0 24 * * *0 0 0 * *0 0 32 * *0 0 * 0 *0 0 * 13 *0 0 * * 8*/0 * * * *-1 * * * *
No snapshots, no errorK3s for some, parser for the rest❌ Saves✅ Blocks✅ Blockshint: Invalid
Backward ranges
5-1 * * * *0 22-2 * * *0 0 * * FRI-MON
No snapshots, no errorParser only❌ Saves✅ Blocks✅ Blockshint: Invalid
Other cron dialects
0 0 L * *0 0 * * 5L0 0 * * 1#10 0 15W * *H * * * *@reboot
No snapshots, no errorK3s for some, parser for the rest❌ Saves✅ Blocks✅ Blockshint: Invalid
Not numbers
1.5 * * * *0x10 * * * *
No snapshots, no errorParser only❌ Saves✅ Blocks✅ Blockshint: Invalid
Malformed @every
@every@every 90@every 1d@every 6h
No snapshots, no errorK3s for some, parser for the rest❌ Saves✅ Blocks✅ Blockshint: Invalid
Timezone prefix on a broken schedule
CRON_TZ=UTC * * * * * *
No snapshots, no errorParser only❌ Saves✅ Blocks✅ Blockshint: Invalid
Shortcut in the wrong case, or made up
@DAILY@Daily@foo
No snapshots, no errorParser only❌ Saves✅ Blocks✅ Blockshint: Invalid
Not a schedule, or only spaces
not-a-crondaily(spaces)(spaces)
No snapshots, no errorParser only❌ Saves✅ Blocks✅ Blockshint: Invalid

How this was measured: I typed all 105 schedules into the real Create form on master and on this build, and into Edit and imported on this build. The round-one column is from the same run at 405c0e2. Each row's schedules gave identical results on every build. Under "On the cluster", small text says what the result rests on. These came from test servers in containers using Rancher's config file, not clusters provisioned through Rancher.

Findings
1

It's now a hand-written parser. Is that what you want?

shell/utils/validators/etcd-snapshot-schedule.ts (new, 114 lines) and its test (119 lines)
Your call

What happened

Your two comments needed about 15 lines around cron-validator, which is what my suggested fix did. Cody's reply says every row of the table is now a test, including the odd forms. cron-validator can't express those, so my read is that's why the parser is hand-written. Cody didn't say so. If that's right, the table drove this. You wrote that some edge cases seemed OK, but the table marked forms like 1,,3 * * * * as ❌, and a ❌ reads as something to fix. I should have marked those rows as not worth fixing.

It gets 4 more rows right than my fix: the absurdly long @every, ? in odd places, stray commas and the other odd forms. Nobody writes any of those. In all it answers 13 of the 105 schedules differently from my fix.

Why I'd still accept it

Why you might not

Issue 13202 (open, v2.18.0) wants cron validation merged into one, and this adds another with a different grammar from the hint next to it. If you'd rather have less code, the 15-line version is still deployed as dashboard-pr19766fix. Say so and I'll draft the comment.

2

Three lines that can't change any answer

Optional nit

When a range has a step but no high value, this sets end to the maximum. In the Go parser that decides which bits get set. Here end only feeds the range check on the next line, and N/step passes or fails that check the same way whether end is N or the maximum.

I deleted the branch and reran the whole comparison: 0 of 1,731,642 answers changed. It's also the one mutation here that no test could ever catch. It's harmless, and it does keep the code closer to the Go original, so this is only worth mentioning alongside an approve.

3

Noted, no action needed

FYI
The new validator against the real parser

The PR now claims to match what K3s and RKE2 parse, so this tests that claim directly. Of 1,731,642 schedules, 1,726,175 get the same answer. These are all the differences:

Where they differCountExampleWhat it means
The PR saves, the parser rejects3,562
A timezone name that doesn't exist932TZ=Nope @dailyOn purpose, and in the code comment. This is the one a person could type.
A step too big for a 64-bit integer1,313*/99999999999999999999 * * * *Nobody writes it. K3s: 0 snapshots.
A byte order mark used as a space1,279U+FEFF before * * * * *JavaScript treats it as whitespace and Go doesn't. K3s: 0 snapshots. Only a paste from an odd file could cause it.
Within rounding of the longest duration38@every 2562047h47m16.854775808sOne nanosecond past the limit, about 292 years.
The PR blocks, the parser accepts1,905
U+0085 used as a space1,303U+0085 between fieldsWhitespace in Go only. Nobody types it.
An empty timezone248TZ= * * * * *Go reads it as UTC, and K3s took 4 snapshots. Blocking it is harmless.
A field of only commas258, * * * *Mostly never fires (K3s: 0 snapshots), so blocking is right. 29 fire through the other day field.
@every with a sign76@every +1h, @every -1hNobody writes the plus. The minus is the debatable row in the list.
@every 0 with no unit19@every 0It would mean every second, so blocking is fine.
A letter that lowercases differently in Go10 0 * * FRİA Turkish dotted İ. Nobody types it on purpose.
On real servers, this round
SchedulePR formK3s v1.37.1 snapshotsRKE2 v1.37.1 snapshots
*-5 * * * *Saves44
?/2 * * * *Saves22
+0/1 * * * *Saves44
@every .5mSaves78
@every 1.mSaves3
TZ=UTC @every 1m Saves3
* * * * * (control)Saves44
, * * * *Blocks0
TZ= * * * * *Blocks4
*/99999999999999999999 * * * *Saves0
U+FEFF, then * * * * *Saves0
@every 999999999hBlocks0

17 more throwaway servers: 12 on K3s v1.37.1 and 5 on RKE2 v1.37.1, mf's default versions, which makes 84 with round one. Each had the schedule in config.yaml.d/50-rancher.yaml, and all stayed running with no cron error in their logs. K3s ran for about 4 minutes. RKE2 ran until the control had 3 snapshots, plus a minute. RKE2 had no CNI, so its node stayed NotReady, which doesn't affect etcd snapshots. Red means the form's answer doesn't match what the server did. Those are the same rare forms as in the comparison below.

Live results
CaseResult on the new build
105 schedules typed into Create Custom, Edit and importedAll three give the same answers: 61 save, 44 block. The icon and tab flag appear on exactly the blocked ones. Each differs from the real parser only on TZ=Nope and @every -1h.
The same 105 on masterAll 105 save, with no icon or flag. Same hint text as the PR on all 105.
Amazon EC2: default schedule, then six typed* * * * * *, 0 0 * * 7 and TZ=UTC disable Create and flag the tab. @every 6h, CRON_TZ=UTC 0 0 * * * and 0 */5 * * * enable it.
What Create sends, captured and stopped8 schedules, byte for byte, including the trailing space in TZ=UTC @daily .
What Edit sends for stored @every 6hSave enabled on load (round one: disabled). The PUT carries it unchanged.
What imported sends, the Save round one never reachedPUT /v3/clusters/c-p2bmg carries @every 6h, CRON_TZ=UTC 0 0 * * * and 0 */5 * * * unchanged. 0 0 * * 7 and TZ=UTC keep Save disabled and send nothing.
Real keystrokes, not a paste5 schedules typed key by key end in the right state, with no uncaught errors from the partial values.
Stored values on load *0 0 * * 7 and TZ=UTC disable Save and flag the tab. TZ=UTC 0 0 * * * and @every 0s leave it enabled.
Automatic Backups off, then on againTZ=UTC, 0 0 * * 7 and * * * * * *: disabled, enabled, disabled.
S3 and backups flows on Create, 38 steps from round oneOnly 4 steps changed, all intended: 0 0 * * 7 and 0 0 * * 1-7 now block, @every 6h and CRON_TZ=UTC 0 0 * * * now save. The S3 re-select bug stays gone.
Imported: Day 2 Ops off and on, collapsed section, S3, remount, stored values *All correct. Compared by hand with round one's table, only the same intended changes differ.
View mode with a stored invalid scheduleNo icon and no tab flag. The grey hint still shows, same as master.
Edit as YAML with a stored invalid scheduleForm Save disabled, YAML button and YAML Save enabled.
Simplified ChineseThe UI switches. The tooltip falls back to the English text, with no missing-key text. Create is disabled. Expected for a new key.
Clone by URLLands on the provider picker on both master and the PR, so it never reaches this form. Not a PR issue.

* Stored values come from a rewritten GET response, except * * * * * * and @every 6h on the Edit fixtures, which are really stored.

What else was checked
CheckResult
Parser comparison1,731,642 schedules against robfig/cron v3.0.1: round one's set, every token sequence up to length 4 in each field, and whole specs built from unusual separators. 1,726,175 agree. 10,449 panic the parser, and the PR saves none of them. No input made the validator throw. The PR saves 20 schedules that parse but never fire, all dates like Feb 31.
Rancher APIA server-side dry run on mf accepted all 22 schedules I tried, including TZ=UTC and 0 0 * * 7, and returned each unchanged. Nothing was created. So Rancher's admission doesn't check this, and the form is the only check before the cluster.
Unit testsRun locally on the head: validators, composable, etcd forms, rke2.test.ts and pkg/imported: 13 suites, 466 passed. 2 skipped, from a describe.skip that is also on master.
Do the tests catch breakage?41 deliberate breakages of the validator and composable: 37 fail a test. The other 4 are card 2's dead branch and the three details in card 3.
Other users of the old codecron-schedule.js and its test are back to matching master, so the PR no longer changes them. cron-validator was already a dependency and four other files still use it, so nothing is orphaned.
CIAll checks pass. The first attempt failed only kubewarden.spec.ts (the live extension catalog), and it passed on the automatic retry. The branch is 5 commits behind master and none of them touch these files.
CodeTypeScript, no any, no v-html, no issue references in comments. The one new string is in en-us.yaml.
SafetyEvery write in the browser runs was intercepted. Cluster writes were aborted, and no real resource changed. The dry run stored nothing. All 17 server containers are removed and the Docker VM is back off.
Not testedA cluster actually provisioned through Rancher. Saves were captured in the browser, so none reached Rancher for real. Servers on a full host OS, since these all ran in containers. @weekly, @monthly, @yearly and @annually firing on a server (parser only). Non-admin roles, as the composable has no permission logic.

Suggested review (not posted)

In your voice, in the usual shape: short body, no code blocks, no preview link. Approve. Resolve both threads when you submit, since the fixes are in. Drop the inline if you'd rather not nit. I can add this as a pending review if you want it.

Body

Retested on e11c386c2e on the Create, Edit, Amazon EC2 and imported forms. Day of week 7 blocks, @every and CRON_TZ= save, and TZ=UTC on its own stays blocked. I also ran the new validator against robfig/cron v3.0.1 on about 1.7 million generated schedules. Nothing that crashes the parser gets through, and apart from the timezone name you called out, the differences are things nobody types, like a 20-digit step or a byte order mark. The odd forms took snapshots on K3s and RKE2 test servers too. Looks good.

Optional inline on shell/utils/validators/etcd-snapshot-schedule.ts:65

Optional: I think this branch can go. In robfig end decides which bits get set, but here it only feeds the range check below, and N/step gets the same answer with end at N or at the maximum. Removing it changed none of the answers in my comparison.

Cleanup when the PR merges
Left behindRemove with
Preview dashboard-pr19766 on created-dashboard-2bash preview/cleanup-branch.sh dashboard pr19766
Preview dashboard-pr19766fix (my round-one fix, not pushed). Only needed if you take the other side of card 1bash preview/cleanup-branch.sh dashboard pr19766fix
Fixture clusters zz19766-bad-cron and zz19766-every on mf (no nodes)kubectl -n fleet-default delete clusters.provisioning.cattle.io zz19766-bad-cron zz19766-every