This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/superset-kubernetes-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new 8fb153e  chore(deps): replace robfig/cron with adhocore/gronx (#238)
8fb153e is described below

commit 8fb153e9adf8a170ae2fb2fe72142458acb75534
Author: Ville Brofeldt <[email protected]>
AuthorDate: Sat Jul 25 14:34:35 2026 -0700

    chore(deps): replace robfig/cron with adhocore/gronx (#238)
    
    * chore(deps): replace robfig/cron with adhocore/gronx
    
    Renovate flags github.com/robfig/cron/v3 as stale (last commit 2021, no
    releases since). Replace it with the actively-maintained
    github.com/adhocore/gronx, which also natively supports 5-7 field cron
    expressions (optional leading seconds and trailing year).
    
    gronx's native PrevTickBefore/NextTickAfter let us delete the hand-rolled
    findPrevTick geometric-doubling loop. The CRD cronSchedule pattern is
    relaxed from exactly 5 fields to 5-7, and the field doc updated
    accordingly. Existing 5-field schedules compute identical ticks, so this
    is additive, not breaking.
    
    * docs(changelog): add PR reference to cronSchedule entry
---
 api/v1alpha1/superset_types.go                     | 30 ++++++------
 .../crds/superset.apache.org_supersets.yaml        |  2 +-
 .../crd/bases/superset.apache.org_supersets.yaml   |  2 +-
 docs/reference/api-reference.md                    |  4 +-
 docs/reference/releases.md                         |  1 +
 docs/user-guide/lifecycle.md                       |  4 +-
 go.mod                                             |  2 +-
 go.sum                                             |  4 +-
 internal/controller/lifecycle.go                   |  4 +-
 internal/schedule/schedule.go                      | 53 +++++-----------------
 internal/schedule/schedule_test.go                 | 29 ++++++++++++
 11 files changed, 69 insertions(+), 66 deletions(-)

diff --git a/api/v1alpha1/superset_types.go b/api/v1alpha1/superset_types.go
index 6bff1fa..9e6793c 100644
--- a/api/v1alpha1/superset_types.go
+++ b/api/v1alpha1/superset_types.go
@@ -359,23 +359,27 @@ type BaseTaskSpec struct {
 type SchedulableBaseTaskSpec struct {
        BaseTaskSpec `json:",inline"`
 
-       // CronSchedule is a 5-field cron expression (minute hour day-of-month 
month
-       // day-of-week) that triggers periodic re-execution of this task and all
-       // downstream tasks. When the clock crosses a cron boundary, the task
-       // checksum changes and the lifecycle pipeline re-runs.
+       // CronSchedule is a cron expression that triggers periodic 
re-execution of
+       // this task and all downstream tasks. When the clock crosses a cron
+       // boundary, the task checksum changes and the lifecycle pipeline 
re-runs.
        //
-       // Uses standard cron syntax. Examples: "0 2 * * *" (daily 2 AM UTC),
-       // "0 */6 * * *" (every 6 hours), "30 1 * * 1" (Mondays 1:30 AM UTC).
-       // Predefined schedules (e.g. "@daily") are not accepted; use the 
explicit
-       // 5-field form. Pattern validation rejects only malformed *shape* at
-       // admission (e.g. fewer than five fields, disallowed characters);
-       // out-of-range values like "99 99 99 99 99" still pass admission and 
are
-       // caught by the runtime parser, which blocks the lifecycle pipeline 
with
-       // an InvalidCronSchedule condition until the expression is corrected.
+       // Uses standard cron syntax with 5 to 7 whitespace-separated fields: 
the
+       // 5-field form is "minute hour day-of-month month day-of-week"; an 
optional
+       // leading seconds field and/or trailing year field extend it to 6 or 7
+       // fields. Examples: "0 2 * * *" (daily 2 AM UTC), "0 */6 * * *" (every 
6
+       // hours), "30 1 * * 1" (Mondays 1:30 AM UTC), "*/30 * * * * *" (every 
30
+       // seconds).
+       // Predefined schedules (e.g. "@daily") are not accepted; use an 
explicit
+       // field form. Pattern validation rejects only malformed *shape* at
+       // admission (e.g. fewer than five or more than seven fields, disallowed
+       // characters); out-of-range values like "99 99 99 99 99" still pass
+       // admission and are caught by the runtime parser, which blocks the
+       // lifecycle pipeline with an InvalidCronSchedule condition until the
+       // expression is corrected.
        // +optional
        // +kubebuilder:validation:MinLength=9
        // +kubebuilder:validation:MaxLength=256
-       // 
+kubebuilder:validation:Pattern=`^[A-Za-z0-9*/,?-]+(\s+[A-Za-z0-9*/,?-]+){4}$`
+       // 
+kubebuilder:validation:Pattern=`^[A-Za-z0-9*/,?-]+(\s+[A-Za-z0-9*/,?-]+){4,6}$`
        CronSchedule *string `json:"cronSchedule,omitempty"`
 }
 
diff --git a/charts/superset-operator/crds/superset.apache.org_supersets.yaml 
b/charts/superset-operator/crds/superset.apache.org_supersets.yaml
index 3ddec9b..78f144a 100644
--- a/charts/superset-operator/crds/superset.apache.org_supersets.yaml
+++ b/charts/superset-operator/crds/superset.apache.org_supersets.yaml
@@ -19368,7 +19368,7 @@ spec:
                       cronSchedule:
                         maxLength: 256
                         minLength: 9
-                        pattern: ^[A-Za-z0-9*/,?-]+(\s+[A-Za-z0-9*/,?-]+){4}$
+                        pattern: ^[A-Za-z0-9*/,?-]+(\s+[A-Za-z0-9*/,?-]+){4,6}$
                         type: string
                       disabled:
                         type: boolean
diff --git a/config/crd/bases/superset.apache.org_supersets.yaml 
b/config/crd/bases/superset.apache.org_supersets.yaml
index 3ddec9b..78f144a 100644
--- a/config/crd/bases/superset.apache.org_supersets.yaml
+++ b/config/crd/bases/superset.apache.org_supersets.yaml
@@ -19368,7 +19368,7 @@ spec:
                       cronSchedule:
                         maxLength: 256
                         minLength: 9
-                        pattern: ^[A-Za-z0-9*/,?-]+(\s+[A-Za-z0-9*/,?-]+){4}$
+                        pattern: ^[A-Za-z0-9*/,?-]+(\s+[A-Za-z0-9*/,?-]+){4,6}$
                         type: string
                       disabled:
                         type: boolean
diff --git a/docs/reference/api-reference.md b/docs/reference/api-reference.md
index 73914ab..c1ccb5e 100644
--- a/docs/reference/api-reference.md
+++ b/docs/reference/api-reference.md
@@ -934,7 +934,7 @@ _Appears in:_
 | `timeout` 
_[Duration](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration)_ 
| Maximum timeout per attempt. |  | Optional: \{\} <br /> |
 | `maxRetries` _integer_ | Maximum number of retries before permanent failure. 
| 3 | Minimum: 1 <br />Optional: \{\} <br /> |
 | `disabled` _boolean_ | Disabled skips this task entirely when true. |  | 
Optional: \{\} <br /> |
-| `cronSchedule` _string_ | CronSchedule is a 5-field cron expression (minute 
hour day-of-month month<br />day-of-week) that triggers periodic re-execution 
of this task and all<br />downstream tasks. When the clock crosses a cron 
boundary, the task<br />checksum changes and the lifecycle pipeline re-runs.<br 
/>Uses standard cron syntax. Examples: "0 2 * * *" (daily 2 AM UTC),<br />"0 
*/6 * * *" (every 6 hours), "30 1 * * 1" (Mondays 1:30 AM UTC).<br />Predefined 
schedules (e.g. "@daily") [...]
+| `cronSchedule` _string_ | CronSchedule is a cron expression that triggers 
periodic re-execution of<br />this task and all downstream tasks. When the 
clock crosses a cron<br />boundary, the task checksum changes and the lifecycle 
pipeline re-runs.<br />Uses standard cron syntax with 5 to 7 
whitespace-separated fields: the<br />5-field form is "minute hour day-of-month 
month day-of-week"; an optional<br />leading seconds field and/or trailing year 
field extend it to 6 or 7<br />fields. E [...]
 
 
 #### SeedSourceSpec
@@ -982,7 +982,7 @@ _Appears in:_
 | `timeout` 
_[Duration](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration)_ 
| Maximum timeout per attempt. |  | Optional: \{\} <br /> |
 | `maxRetries` _integer_ | Maximum number of retries before permanent failure. 
| 3 | Minimum: 1 <br />Optional: \{\} <br /> |
 | `disabled` _boolean_ | Disabled skips this task entirely when true. |  | 
Optional: \{\} <br /> |
-| `cronSchedule` _string_ | CronSchedule is a 5-field cron expression (minute 
hour day-of-month month<br />day-of-week) that triggers periodic re-execution 
of this task and all<br />downstream tasks. When the clock crosses a cron 
boundary, the task<br />checksum changes and the lifecycle pipeline re-runs.<br 
/>Uses standard cron syntax. Examples: "0 2 * * *" (daily 2 AM UTC),<br />"0 
*/6 * * *" (every 6 hours), "30 1 * * 1" (Mondays 1:30 AM UTC).<br />Predefined 
schedules (e.g. "@daily") [...]
+| `cronSchedule` _string_ | CronSchedule is a cron expression that triggers 
periodic re-execution of<br />this task and all downstream tasks. When the 
clock crosses a cron<br />boundary, the task checksum changes and the lifecycle 
pipeline re-runs.<br />Uses standard cron syntax with 5 to 7 
whitespace-separated fields: the<br />5-field form is "minute hour day-of-month 
month day-of-week"; an optional<br />leading seconds field and/or trailing year 
field extend it to 6 or 7<br />fields. E [...]
 | `source` _[SeedSourceSpec](#seedsourcespec)_ | Source database to seed from 
(typically production, read-only user). |  |  |
 | `excludeTables` _string array_ | Tables to exclude entirely from the dump 
(schema and data). |  | Optional: \{\} <br /> |
 | `excludeTableData` _string array_ | Tables where schema is dumped but data 
is not. Useful for large tables<br />needed by migrations but not for testing 
(e.g., "logs", "query"). |  | Optional: \{\} <br /> |
diff --git a/docs/reference/releases.md b/docs/reference/releases.md
index 9377338..1dd71cd 100644
--- a/docs/reference/releases.md
+++ b/docs/reference/releases.md
@@ -25,6 +25,7 @@ This page tracks notable changes in Apache Superset 
Kubernetes Operator releases
 
 ### Added
 
+- **Seconds and year precision in `cronSchedule`.** Lifecycle task 
`cronSchedule` fields now accept 6- and 7-field cron expressions in addition to 
the classic 5-field form — an optional leading seconds field and/or trailing 
year field (e.g. `*/30 * * * * *` every 30 seconds, `0 0 2 * * * 2027`). 
Existing 5-field schedules are unaffected 
([#238](https://github.com/apache/superset-kubernetes-operator/pull/238), 
[@villebro](https://github.com/villebro)).
 - **Helm `topologySpreadConstraints`.** The Helm chart now exposes a 
`topologySpreadConstraints` value that sets 
`spec.template.spec.topologySpreadConstraints` on the operator Deployment, 
letting operators distribute manager pods across failure domains such as nodes 
and zones 
([#213](https://github.com/apache/superset-kubernetes-operator/pull/213), 
[@younsl](https://github.com/younsl)).
 - **Helm `revisionHistoryLimit`.** The Helm chart now exposes a 
`revisionHistoryLimit` value that allows setting `spec.revisionHistoryLimit` on 
the operator Deployment, capping the number of old ReplicaSets retained for 
rollback ([@younsl](https://github.com/younsl)).
 - **Helm extra manifests.** The Helm chart now supports `extraManifests` for 
rendering trusted, release-scoped Kubernetes manifests with Helm `tpl`. Use it 
for companion resources owned by the operator release, not shared cluster 
infrastructure such as Gateway API controllers, CRDs, or shared Gateways 
([#196](https://github.com/apache/superset-kubernetes-operator/pull/196), 
[@younsl](https://github.com/younsl)).
diff --git a/docs/user-guide/lifecycle.md b/docs/user-guide/lifecycle.md
index 7b6a7ea..85cb903 100644
--- a/docs/user-guide/lifecycle.md
+++ b/docs/user-guide/lifecycle.md
@@ -79,7 +79,7 @@ spec:
 
 ### Scheduled Execution
 
-Tasks that support scheduling (currently seed) accept a `cronSchedule` field — 
a standard 5-field cron expression that triggers periodic re-execution:
+Tasks that support scheduling (currently seed) accept a `cronSchedule` field — 
a standard cron expression (5 to 7 fields; the sixth/seventh fields add 
optional seconds and year precision) that triggers periodic re-execution:
 
 ```yaml
 spec:
@@ -485,7 +485,7 @@ The lifecycle pipeline runs: **seed → migrate → rotate → 
init → componen
 The seed task runs when its checksum changes. Two mechanisms trigger 
re-execution:
 
 - **`trigger` field** — an opaque string (date, UUID, CI build ID). Changing 
it causes a re-seed. Use this for manual or CI-driven refreshes.
-- **`cronSchedule` field** — a 5-field cron expression for periodic 
re-execution. When the clock crosses a cron boundary, the task checksum changes 
automatically.
+- **`cronSchedule` field** — a cron expression (5–7 fields, with optional 
seconds/year precision) for periodic re-execution. When the clock crosses a 
cron boundary, the task checksum changes automatically.
 
 To disable seed without removing its configuration, set `disabled: true`.
 
diff --git a/go.mod b/go.mod
index e5f67e2..b796f02 100644
--- a/go.mod
+++ b/go.mod
@@ -5,9 +5,9 @@ go 1.26.0
 toolchain go1.26.5
 
 require (
+       github.com/adhocore/gronx v1.20.0
        github.com/onsi/ginkgo/v2 v2.32.0
        github.com/onsi/gomega v1.42.1
-       github.com/robfig/cron/v3 v3.0.1
        github.com/stretchr/testify v1.11.1
        k8s.io/api v0.36.3
        k8s.io/apiextensions-apiserver v0.36.3
diff --git a/go.sum b/go.sum
index 9131ee6..81416be 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ cel.dev/expr v0.25.1 
h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
 cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
 github.com/Masterminds/semver/v3 v3.5.0 
h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
 github.com/Masterminds/semver/v3 v3.5.0/go.mod 
h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/adhocore/gronx v1.20.0 
h1:PD13Mo0wekkZ7ZZR9yb1TqeqTfybs7/K3ez9DmjQwEs=
+github.com/adhocore/gronx v1.20.0/go.mod 
h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
 github.com/antlr4-go/antlr/v4 v4.13.1 
h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
 github.com/antlr4-go/antlr/v4 v4.13.1/go.mod 
h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -140,8 +142,6 @@ github.com/prometheus/common v0.67.5 
h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
 github.com/prometheus/common v0.67.5/go.mod 
h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
 github.com/prometheus/procfs v0.19.2 
h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
 github.com/prometheus/procfs v0.19.2/go.mod 
h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
-github.com/robfig/cron/v3 v3.0.1 
h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
-github.com/robfig/cron/v3 v3.0.1/go.mod 
h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/go-internal v1.14.1 
h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod 
h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod 
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
diff --git a/internal/controller/lifecycle.go b/internal/controller/lifecycle.go
index a965d54..895455c 100644
--- a/internal/controller/lifecycle.go
+++ b/internal/controller/lifecycle.go
@@ -305,9 +305,9 @@ func lifecycleParentPhase(upgradeInProgress bool) string {
 
 // gateOnInvalidSeedSchedule returns a terminal result when the user
 // configured seed with a malformed cron schedule. CRD pattern validation only
-// covers the structural shape (5 whitespace-separated fields of allowed
+// covers the structural shape (5-7 whitespace-separated fields of allowed
 // characters); out-of-range values like "99 99 99 99 99" still pass admission
-// and only fail at runtime when robfig/cron parses them. Without this gate,
+// and only fail at runtime when gronx parses them. Without this gate,
 // IsEnabled would treat seed as disabled and downstream tasks would run
 // against the wrong data set.
 func (r *SupersetReconciler) gateOnInvalidSeedSchedule(superset 
*supersetv1alpha1.Superset) (lifecycleResult, bool) {
diff --git a/internal/schedule/schedule.go b/internal/schedule/schedule.go
index 899d3b4..265c1a9 100644
--- a/internal/schedule/schedule.go
+++ b/internal/schedule/schedule.go
@@ -22,66 +22,35 @@ import (
        "fmt"
        "time"
 
-       "github.com/robfig/cron/v3"
+       "github.com/adhocore/gronx"
 )
 
-var parser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | 
cron.Dow)
-
-// CurrentTick returns the most recent past time matching the cron expression,
-// formatted as RFC3339 UTC (e.g., "2026-05-12T02:00:00Z").
-// Returns "" if the expression is invalid or no tick exists within the
-// lookback window (~2 years).
+// CurrentTick returns the most recent time matching the cron expression that 
is
+// at or before now, formatted as RFC3339 UTC (e.g., "2026-05-12T02:00:00Z").
+// Returns "" if the expression is invalid or no matching tick exists.
 func CurrentTick(expr string, now time.Time) string {
-       sched, err := parser.Parse(expr)
+       prev, err := gronx.PrevTickBefore(expr, now, true)
        if err != nil {
                return ""
        }
-       prev := findPrevTick(sched, now)
-       if prev.IsZero() {
-               return ""
-       }
        return prev.UTC().Format(time.RFC3339)
 }
 
-// NextTick returns the next future time matching the cron expression.
-// Returns zero time if the expression is invalid.
+// NextTick returns the next future time matching the cron expression (strictly
+// after now). Returns zero time if the expression is invalid.
 func NextTick(expr string, now time.Time) time.Time {
-       sched, err := parser.Parse(expr)
+       next, err := gronx.NextTickAfter(expr, now, false)
        if err != nil {
                return time.Time{}
        }
-       return sched.Next(now)
+       return next
 }
 
 // Validate checks whether a cron expression is parseable.
 // Returns an error describing the problem, or nil if valid.
 func Validate(expr string) error {
-       _, err := parser.Parse(expr)
-       if err != nil {
-               return fmt.Errorf("invalid cron expression %q: %w", expr, err)
+       if !gronx.IsValid(expr) {
+               return fmt.Errorf("invalid cron expression %q", expr)
        }
        return nil
 }
-
-// findPrevTick finds the most recent time matching the schedule that is <= 
now.
-// Uses geometric doubling of the lookback window for efficiency.
-func findPrevTick(sched cron.Schedule, now time.Time) time.Time {
-       lookback := time.Minute
-       for range 20 {
-               start := now.Add(-lookback)
-               t := sched.Next(start)
-               if !t.After(now) {
-                       // Found a tick in the window. Iterate forward to find 
the latest one <= now.
-                       prev := t
-                       for {
-                               t = sched.Next(t)
-                               if t.After(now) {
-                                       return prev
-                               }
-                               prev = t
-                       }
-               }
-               lookback *= 2
-       }
-       return time.Time{}
-}
diff --git a/internal/schedule/schedule_test.go 
b/internal/schedule/schedule_test.go
index 9e7d4e2..963c2ff 100644
--- a/internal/schedule/schedule_test.go
+++ b/internal/schedule/schedule_test.go
@@ -148,3 +148,32 @@ func TestValidate_Invalid(t *testing.T) {
        require.Error(t, Validate(""))
        require.Error(t, Validate("* * *")) // too few fields
 }
+
+// The following tests cover the extended field support gronx provides beyond
+// the classic 5-field form: an optional leading seconds field (6 fields) and
+// an optional trailing year field (7 fields).
+
+func TestValidate_SecondsAndYearFields(t *testing.T) {
+       assert.NoError(t, Validate("*/30 * * * * *"))   // 6 fields: seconds 
precision
+       assert.NoError(t, Validate("0 0 2 * * * 2027")) // 7 fields: seconds + 
year
+}
+
+func TestNextTick_SecondsField(t *testing.T) {
+       now := time.Date(2026, 5, 11, 14, 30, 10, 0, time.UTC)
+       next := NextTick("*/30 * * * * *", now) // every 30 seconds
+       expected := time.Date(2026, 5, 11, 14, 30, 30, 0, time.UTC)
+       assert.Equal(t, expected, next)
+}
+
+func TestCurrentTick_SecondsField(t *testing.T) {
+       now := time.Date(2026, 5, 11, 14, 30, 10, 0, time.UTC)
+       tick := CurrentTick("*/30 * * * * *", now) // every 30 seconds
+       assert.Equal(t, "2026-05-11T14:30:00Z", tick)
+}
+
+func TestNextTick_YearField(t *testing.T) {
+       now := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
+       next := NextTick("0 0 2 * * * 2027", now) // 2 AM once in 2027
+       expected := time.Date(2027, 1, 1, 2, 0, 0, 0, time.UTC)
+       assert.Equal(t, expected, next)
+}

Reply via email to