From 1915dd3f5cf72731451b5798ec8998190a669b28 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Sat, 11 Jul 2026 10:00:05 -0400 Subject: [PATCH 1/2] refactor(sablier): extract typed InstanceConfig from the InstanceInfo god struct InstanceInfo is four things at once: provider-reported state, a parsed-label configuration bag, the API wire format, and the persisted session record. This is the first stage of pulling those apart, scoped to the configuration concern and deliberately wire-compatible. All sablier.* label parsing now lives in one function, InstanceConfigFromLabels, producing a typed InstanceConfig (Enabled is a bool, durations are durations, window values are validated). It takes a logger instead of using the global slog, restoring the instance context on warnings. PopulateEnabledAndGroup becomes a thin adapter that providers keep calling: it stores the typed config under the new additive `config` field and mirrors it into the flat legacy fields, which stay byte-identical for API consumers and persisted session records (pinned by a wire-compat test; every pre-existing PopulateEnabledAndGroup test passes unchanged). The scattered `Enabled == "true"` string comparisons (six call sites across autostop, autowarm, expiration, request handling and the running-hours watcher) collapse into IsEnabled() accessors on InstanceInfo and InstanceConfiguration. The InstanceInfo accessor prefers the parsed config and falls back to the flat field for records persisted by older versions. Next stages (separate PRs): an API DTO so the wire format stops being the domain struct, and a versioned session record so the store stops persisting label config. --- docs/static/openapi.json | 59 ++++++++++++ pkg/sablier/autostop.go | 2 +- pkg/sablier/autowarm.go | 2 +- pkg/sablier/instance.go | 104 +++++++++------------ pkg/sablier/instance_config.go | 114 +++++++++++++++++++++++ pkg/sablier/instance_config_test.go | 139 ++++++++++++++++++++++++++++ pkg/sablier/instance_expired.go | 2 +- pkg/sablier/instance_request.go | 2 +- pkg/sablier/running_hours_watch.go | 2 +- 9 files changed, 359 insertions(+), 67 deletions(-) create mode 100644 pkg/sablier/instance_config.go create mode 100644 pkg/sablier/instance_config_test.go diff --git a/docs/static/openapi.json b/docs/static/openapi.json index 5583c832..acc26e96 100644 --- a/docs/static/openapi.json +++ b/docs/static/openapi.json @@ -80,6 +80,57 @@ }, "type": "object" }, + "sablier.InstanceConfig": { + "properties": { + "antiAffinity": { + "description": "AntiAffinity lists the groups this instance backs off from\n(sablier.anti-affinity).", + "items": { + "type": "string" + }, + "type": "array" + }, + "enabled": { + "description": "Enabled reports whether the instance opted into Sablier management\n(sablier.enable set to exactly \"true\").", + "type": "boolean" + }, + "groups": { + "description": "Groups the instance belongs to (sablier.group). Only populated for\nenabled instances; defaults to [\"default\"] when the label is absent.", + "items": { + "type": "string" + }, + "type": "array" + }, + "readyAfter": { + "allOf": [ + { + "$ref": "#/components/schemas/time.Duration" + } + ], + "description": "ReadyAfter is the settling delay after the instance first reports ready\n(sablier.ready-after). Zero means no extra wait." + }, + "readyOnStart": { + "description": "ReadyOnStart marks the instance ready as soon as its start is\ndispatched, skipping the health check (sablier.ready-on-start).", + "type": "boolean" + }, + "runningDays": { + "description": "RunningDays restricts RunningHours to specific weekdays\n(sablier.running-days). Empty means every day.", + "type": "string" + }, + "runningHours": { + "description": "RunningHours is the validated daily keep-warm window\n(sablier.running-hours, \"HH:MM-HH:MM\"). It is kept in its validated\nstring form because that is the serializable canonical representation;\nparse it with ParseRunningHours where the window is evaluated.", + "type": "string" + }, + "scale": { + "allOf": [ + { + "$ref": "#/components/schemas/sablier.ScaleConfig" + } + ], + "description": "Scale holds the idle/active resource profiles when any non-default\nscale-mode label is present (sablier.idle.* / sablier.active.*)." + } + }, + "type": "object" + }, "sablier.InstanceInfo": { "properties": { "antiAffinity": { @@ -89,6 +140,14 @@ }, "type": "array" }, + "config": { + "allOf": [ + { + "$ref": "#/components/schemas/sablier.InstanceConfig" + } + ], + "description": "Config is the typed, parsed form of the instance's sablier.* labels,\nproduced by InstanceConfigFromLabels at the provider boundary. Prefer it\n(and the accessor methods such as IsEnabled) over the flat fields above,\nwhich are kept for API and store compatibility and will move behind a\ndedicated API DTO in a later stage. Nil on records that predate this\nfield (e.g. sessions persisted by an older version); accessors fall back\nto the flat fields in that case." + }, "currentReplicas": { "type": "integer" }, diff --git a/pkg/sablier/autostop.go b/pkg/sablier/autostop.go index f2ee5ed4..0c1444a3 100644 --- a/pkg/sablier/autostop.go +++ b/pkg/sablier/autostop.go @@ -100,7 +100,7 @@ func (s *Sablier) WatchAndStopExternallyStarted(ctx context.Context) { continue } // Only act on Sablier-managed instances. - if info.Info.Enabled != "true" { + if !info.Info.IsEnabled() { continue } if s.isStartedByUs(ctx, info.Info.Name) { diff --git a/pkg/sablier/autowarm.go b/pkg/sablier/autowarm.go index 9a94d4ca..7201fe49 100644 --- a/pkg/sablier/autowarm.go +++ b/pkg/sablier/autowarm.go @@ -104,7 +104,7 @@ func (s *Sablier) WatchAndWarmExternallyStarted(ctx context.Context) { continue } // Only act on Sablier-managed instances. - if info.Info.Enabled != "true" { + if !info.Info.IsEnabled() { continue } if s.isStartedByUs(ctx, info.Info.Name) { diff --git a/pkg/sablier/instance.go b/pkg/sablier/instance.go index 2c02347b..97c79f9c 100644 --- a/pkg/sablier/instance.go +++ b/pkg/sablier/instance.go @@ -83,6 +83,25 @@ type InstanceInfo struct { // ScaleConfig configures resource-based scale mode for this instance. // When present, Sablier throttles CPU/memory instead of stopping the container. ScaleConfig *ScaleConfig `json:"scaleConfig,omitempty"` + + // Config is the typed, parsed form of the instance's sablier.* labels, + // produced by InstanceConfigFromLabels at the provider boundary. Prefer it + // (and the accessor methods such as IsEnabled) over the flat fields above, + // which are kept for API and store compatibility and will move behind a + // dedicated API DTO in a later stage. Nil on records that predate this + // field (e.g. sessions persisted by an older version); accessors fall back + // to the flat fields in that case. + Config *InstanceConfig `json:"config,omitempty"` +} + +// IsEnabled reports whether the instance opted into Sablier management +// (sablier.enable set to exactly "true"). It is the single source of that +// semantics; do not compare the flat Enabled string directly. +func (instance InstanceInfo) IsEnabled() bool { + if instance.Config != nil { + return instance.Config.Enabled + } + return instance.Enabled == "true" } // BlkioWeightDevice holds a per-device I/O scheduling weight override. @@ -163,6 +182,12 @@ type InstanceConfiguration struct { Enabled string } +// IsEnabled reports whether the listed instance opted into Sablier management +// (sablier.enable set to exactly "true"), mirroring InstanceInfo.IsEnabled. +func (c InstanceConfiguration) IsEnabled() bool { + return c.Enabled == "true" +} + func (instance InstanceInfo) IsReady() bool { if instance.ReadyOnStart { return true @@ -366,68 +391,23 @@ func ParseAntiAffinity(label string) []string { return out } -// PopulateEnabledAndGroup reads the sablier.enable and sablier.group labels from -// labels and writes the results into info. Centralising this logic avoids -// duplicating the same map lookups in every provider's Inspect implementation. +// PopulateEnabledAndGroup parses the sablier.* labels into info.Config and +// mirrors the result into the flat legacy fields, which stay byte-identical +// on the wire (API responses and persisted session records). All parsing +// lives in InstanceConfigFromLabels; this is only the compatibility adapter +// providers call from their Inspect implementations. func PopulateEnabledAndGroup(info *InstanceInfo, labels map[string]string) { + cfg := InstanceConfigFromLabels(labels, slog.Default().With(slog.String("instance", info.Name))) + info.Config = &cfg + + // Legacy flat fields. Enabled keeps the raw label value (not the parsed + // boolean) because the API has always exposed it verbatim. info.Enabled = labels[LabelEnable] - if info.Enabled == "true" { - info.Groups = ParseGroups(labels[LabelGroup]) - } - if v := labels[LabelReadyAfter]; v != "" { - if d, err := time.ParseDuration(v); err == nil { - info.ReadyAfter = d - } else { - slog.Warn("invalid sablier.ready-after label value, ignoring", - slog.String("instance", info.Name), - slog.String("value", v), - slog.Any("error", err), - ) - } - } - if v := labels[LabelRunningHours]; v != "" { - if _, err := ParseRunningHours(v); err == nil { - info.RunningHours = v - } else { - slog.Warn("invalid sablier.running-hours label value, ignoring", - slog.String("instance", info.Name), - slog.String("value", v), - slog.Any("error", err), - ) - } - } - if v := labels[LabelRunningDays]; v != "" { - if _, err := ParseRunningDays(v); err == nil { - info.RunningDays = v - } else { - slog.Warn("invalid sablier.running-days label value, ignoring", - slog.String("instance", info.Name), - slog.String("value", v), - slog.Any("error", err), - ) - } - } - if v := labels[LabelReadyOnStart]; v != "" { - b, err := strconv.ParseBool(v) - if err != nil { - slog.Warn("invalid sablier.ready-on-start label value, ignoring", - slog.String("instance", info.Name), - slog.String("value", v), - slog.Any("error", err), - ) - } else { - info.ReadyOnStart = b - } - } - if v := labels[LabelAntiAffinity]; v != "" { - info.AntiAffinity = ParseAntiAffinity(v) - } - // Only expose ScaleConfig in the response when at least one non-default - // scale label is present. Detects configuration by checking for values - // that differ from the zero-value defaults (Idle.Replicas=0, Active.Replicas=1). - sc := ScaleConfigFromLabels(labels) - if sc.Idle.Replicas > 0 || sc.Idle.HasResources() || - sc.Active.Replicas > 1 || sc.Active.HasResources() { - info.ScaleConfig = &sc - } + info.Groups = cfg.Groups + info.ReadyAfter = cfg.ReadyAfter + info.RunningHours = cfg.RunningHours + info.RunningDays = cfg.RunningDays + info.ReadyOnStart = cfg.ReadyOnStart + info.AntiAffinity = cfg.AntiAffinity + info.ScaleConfig = cfg.Scale } diff --git a/pkg/sablier/instance_config.go b/pkg/sablier/instance_config.go new file mode 100644 index 00000000..70d93bf1 --- /dev/null +++ b/pkg/sablier/instance_config.go @@ -0,0 +1,114 @@ +package sablier + +import ( + "log/slog" + "strconv" + "time" +) + +// InstanceConfig is the typed, parsed form of the sablier.* labels an +// instance carries. It is produced in one place, InstanceConfigFromLabels, at +// the provider boundary; nothing else should interpret raw label strings. +// +// This is the first stage of splitting InstanceInfo (which today mixes +// provider-reported state, parsed label configuration, the API wire format +// and the persisted session record): the configuration now has one typed +// home, while InstanceInfo keeps its flat legacy fields byte-identical on the +// wire for API and store compatibility. +type InstanceConfig struct { + // Enabled reports whether the instance opted into Sablier management + // (sablier.enable set to exactly "true"). + Enabled bool `json:"enabled,omitempty"` + // Groups the instance belongs to (sablier.group). Only populated for + // enabled instances; defaults to ["default"] when the label is absent. + Groups []string `json:"groups,omitempty"` + // ReadyAfter is the settling delay after the instance first reports ready + // (sablier.ready-after). Zero means no extra wait. + ReadyAfter time.Duration `json:"readyAfter,omitempty"` + // ReadyOnStart marks the instance ready as soon as its start is + // dispatched, skipping the health check (sablier.ready-on-start). + ReadyOnStart bool `json:"readyOnStart,omitempty"` + // RunningHours is the validated daily keep-warm window + // (sablier.running-hours, "HH:MM-HH:MM"). It is kept in its validated + // string form because that is the serializable canonical representation; + // parse it with ParseRunningHours where the window is evaluated. + RunningHours string `json:"runningHours,omitempty"` + // RunningDays restricts RunningHours to specific weekdays + // (sablier.running-days). Empty means every day. + RunningDays string `json:"runningDays,omitempty"` + // AntiAffinity lists the groups this instance backs off from + // (sablier.anti-affinity). + AntiAffinity []string `json:"antiAffinity,omitempty"` + // Scale holds the idle/active resource profiles when any non-default + // scale-mode label is present (sablier.idle.* / sablier.active.*). + Scale *ScaleConfig `json:"scale,omitempty"` +} + +// InstanceConfigFromLabels parses every sablier.* label into a typed +// InstanceConfig. Invalid values are logged through l and ignored, keeping +// the previous per-label warn-and-skip behavior. A nil logger falls back to +// slog.Default(). +func InstanceConfigFromLabels(labels map[string]string, l *slog.Logger) InstanceConfig { + if l == nil { + l = slog.Default() + } + + var cfg InstanceConfig + cfg.Enabled = labels[LabelEnable] == "true" + if cfg.Enabled { + cfg.Groups = ParseGroups(labels[LabelGroup]) + } + if v := labels[LabelReadyAfter]; v != "" { + if d, err := time.ParseDuration(v); err == nil { + cfg.ReadyAfter = d + } else { + l.Warn("invalid sablier.ready-after label value, ignoring", + slog.String("value", v), + slog.Any("error", err), + ) + } + } + if v := labels[LabelRunningHours]; v != "" { + if _, err := ParseRunningHours(v); err == nil { + cfg.RunningHours = v + } else { + l.Warn("invalid sablier.running-hours label value, ignoring", + slog.String("value", v), + slog.Any("error", err), + ) + } + } + if v := labels[LabelRunningDays]; v != "" { + if _, err := ParseRunningDays(v); err == nil { + cfg.RunningDays = v + } else { + l.Warn("invalid sablier.running-days label value, ignoring", + slog.String("value", v), + slog.Any("error", err), + ) + } + } + if v := labels[LabelReadyOnStart]; v != "" { + b, err := strconv.ParseBool(v) + if err != nil { + l.Warn("invalid sablier.ready-on-start label value, ignoring", + slog.String("value", v), + slog.Any("error", err), + ) + } else { + cfg.ReadyOnStart = b + } + } + if v := labels[LabelAntiAffinity]; v != "" { + cfg.AntiAffinity = ParseAntiAffinity(v) + } + // Only expose Scale when at least one non-default scale label is present, + // detected by values that differ from the zero-value defaults + // (Idle.Replicas=0, Active.Replicas=1). + sc := ScaleConfigFromLabels(labels) + if sc.Idle.Replicas > 0 || sc.Idle.HasResources() || + sc.Active.Replicas > 1 || sc.Active.HasResources() { + cfg.Scale = &sc + } + return cfg +} diff --git a/pkg/sablier/instance_config_test.go b/pkg/sablier/instance_config_test.go new file mode 100644 index 00000000..ceea0d2b --- /dev/null +++ b/pkg/sablier/instance_config_test.go @@ -0,0 +1,139 @@ +package sablier + +import ( + "encoding/json" + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestInstanceConfigFromLabels(t *testing.T) { + tests := []struct { + name string + labels map[string]string + want InstanceConfig + }{ + { + name: "no labels", + labels: map[string]string{}, + want: InstanceConfig{}, + }, + { + name: "enabled with default group", + labels: map[string]string{LabelEnable: "true"}, + want: InstanceConfig{Enabled: true, Groups: []string{"default"}}, + }, + { + name: "enable accepts only the exact string true", + labels: map[string]string{LabelEnable: "1"}, + want: InstanceConfig{}, + }, + { + name: "groups are not parsed for disabled instances", + labels: map[string]string{LabelEnable: "false", LabelGroup: "team-a"}, + want: InstanceConfig{}, + }, + { + name: "full configuration", + labels: map[string]string{ + LabelEnable: "true", + LabelGroup: "team-a,team-b", + LabelReadyAfter: "30s", + LabelReadyOnStart: "true", + LabelRunningHours: "09:00-18:00", + LabelRunningDays: "Mon,Tue", + LabelAntiAffinity: "streaming", + }, + want: InstanceConfig{ + Enabled: true, + Groups: []string{"team-a", "team-b"}, + ReadyAfter: 30 * time.Second, + ReadyOnStart: true, + RunningHours: "09:00-18:00", + RunningDays: "Mon,Tue", + AntiAffinity: []string{"streaming"}, + }, + }, + { + name: "invalid values are ignored, valid ones kept", + labels: map[string]string{ + LabelEnable: "true", + LabelReadyAfter: "soon", + LabelReadyOnStart: "yes-please", + LabelRunningHours: "9am-6pm", + LabelRunningDays: "Someday", + }, + want: InstanceConfig{Enabled: true, Groups: []string{"default"}}, + }, + { + name: "scale config only when a non-default scale label is present", + labels: map[string]string{ + LabelEnable: "true", + LabelIdleReplicas: "1", + }, + want: InstanceConfig{ + Enabled: true, + Groups: []string{"default"}, + Scale: &ScaleConfig{Idle: ResourceProfile{Replicas: 1}, Active: ResourceProfile{Replicas: 1}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := InstanceConfigFromLabels(tt.labels, nil) + assert.DeepEqual(t, tt.want, got) + }) + } +} + +func TestInstanceInfoIsEnabled(t *testing.T) { + t.Run("parsed config wins", func(t *testing.T) { + info := InstanceInfo{Enabled: "false", Config: &InstanceConfig{Enabled: true}} + assert.Assert(t, info.IsEnabled()) + }) + t.Run("falls back to the flat field on old records", func(t *testing.T) { + // A session record persisted by a version that predates Config. + old := `{"name":"web","enabled":"true","status":"ready","currentReplicas":1,"desiredReplicas":1}` + var info InstanceInfo + assert.NilError(t, json.Unmarshal([]byte(old), &info)) + assert.Assert(t, info.Config == nil) + assert.Assert(t, info.IsEnabled()) + }) + t.Run("disabled without config", func(t *testing.T) { + assert.Assert(t, !InstanceInfo{Enabled: "1"}.IsEnabled()) + assert.Assert(t, !InstanceInfo{}.IsEnabled()) + }) + t.Run("list entries share the semantics", func(t *testing.T) { + assert.Assert(t, InstanceConfiguration{Enabled: "true"}.IsEnabled()) + assert.Assert(t, !InstanceConfiguration{Enabled: "1"}.IsEnabled()) + }) +} + +// TestPopulateEnabledAndGroup_WireCompat pins the compatibility contract of +// the first InstanceInfo split stage: the flat legacy fields keep their exact +// names and raw values on the wire, and the typed config is strictly +// additive under the "config" key. +func TestPopulateEnabledAndGroup_WireCompat(t *testing.T) { + info := InstanceInfo{Name: "web"} + PopulateEnabledAndGroup(&info, map[string]string{ + LabelEnable: "true", + LabelGroup: "team-a", + LabelRunningHours: "09:00-18:00", + }) + + raw, err := json.Marshal(info) + assert.NilError(t, err) + var m map[string]any + assert.NilError(t, json.Unmarshal(raw, &m)) + + // Legacy fields: identical names, raw string semantics preserved. + assert.Equal(t, "true", m["enabled"], "legacy enabled must stay the raw label string") + assert.DeepEqual(t, []any{"team-a"}, m["groups"]) + assert.Equal(t, "09:00-18:00", m["runningHours"]) + + // Typed config: additive, under its own key, with the parsed boolean. + cfg, ok := m["config"].(map[string]any) + assert.Assert(t, ok, "config must be serialized additively") + assert.Equal(t, true, cfg["enabled"]) +} diff --git a/pkg/sablier/instance_expired.go b/pkg/sablier/instance_expired.go index a219d747..bf566e14 100644 --- a/pkg/sablier/instance_expired.go +++ b/pkg/sablier/instance_expired.go @@ -38,7 +38,7 @@ func onInstanceExpired(ctx context.Context, provider Provider, recorder metrics. logger.WarnContext(ctx, "instance expired could not be inspected before stop", slog.String("instance", key), slog.Any("error", err)) return } - if info.Enabled != "true" { + if !info.IsEnabled() { logger.WarnContext(ctx, "instance expired but is not managed by sablier, skipping stop", slog.String("instance", key)) return } diff --git a/pkg/sablier/instance_request.go b/pkg/sablier/instance_request.go index ee953c34..4f736da0 100644 --- a/pkg/sablier/instance_request.go +++ b/pkg/sablier/instance_request.go @@ -103,7 +103,7 @@ func (s *Sablier) requestStart(ctx context.Context, name string, rejectUnlabeled s.l.DebugContext(ctx, "pre-start inspect failed, using bare info", slog.String("instance", name), slog.Any("error", err)) info = InstanceInfo{Name: name, CurrentReplicas: 0, DesiredReplicas: 1} } - if rejectUnlabeled && info.Enabled != "true" { + if rejectUnlabeled && !info.IsEnabled() { return InstanceInfo{}, ErrInstanceNotManaged{Name: name} } info.Status = InstanceStatusStarting diff --git a/pkg/sablier/running_hours_watch.go b/pkg/sablier/running_hours_watch.go index e00e8206..20951f91 100644 --- a/pkg/sablier/running_hours_watch.go +++ b/pkg/sablier/running_hours_watch.go @@ -37,7 +37,7 @@ func (s *Sablier) reconcileRunningHours(ctx context.Context) { now := time.Now() for _, configured := range instances { - if configured.Enabled != "true" { + if !configured.IsEnabled() { continue } From d51de0b7724eefca3152ae0a74abb9aa2228a87e Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Sat, 11 Jul 2026 10:50:54 -0400 Subject: [PATCH 2/2] test(provider): expect the additive Config field in inspect assertions The provider inspect integration tests compare the full InstanceInfo with DeepEqual; they now fail because inspect results carry the new additive Config field. Since PopulateEnabledAndGroup mirrors the parsed config 1:1 into the flat fields each test case already declares, the expectation is derived from those fields at the existing per-case fixup block instead of hand-writing ~70 Config literals. This also keeps working for the Kubernetes cases that configure sablier via annotations. Verified against the real runtimes locally: docker (dind), swarm (dind), kubernetes (k3s, including the annotation cases) and podman (pind) inspect suites all pass. --- pkg/provider/docker/container_inspect_test.go | 13 +++++++++++++ pkg/provider/dockerswarm/service_inspect_test.go | 13 +++++++++++++ pkg/provider/kubernetes/deployment_inspect_test.go | 13 +++++++++++++ pkg/provider/kubernetes/statefulset_inspect_test.go | 13 +++++++++++++ pkg/provider/podman/container_inspect_test.go | 13 +++++++++++++ 5 files changed, 65 insertions(+) diff --git a/pkg/provider/docker/container_inspect_test.go b/pkg/provider/docker/container_inspect_test.go index a97d1e28..3dfbb7ba 100644 --- a/pkg/provider/docker/container_inspect_test.go +++ b/pkg/provider/docker/container_inspect_test.go @@ -505,6 +505,19 @@ func TestDockerClassicProvider_GetState(t *testing.T) { ID: name, Image: "sablierapp/mimic:v0.3.3", } + // The provider mirrors the parsed label config into Config with the + // same values as the flat fields each case already declares, so + // derive the expectation instead of repeating it per case. + tt.want.Config = &sablier.InstanceConfig{ + Enabled: tt.want.Enabled == "true", + Groups: tt.want.Groups, + ReadyAfter: tt.want.ReadyAfter, + ReadyOnStart: tt.want.ReadyOnStart, + RunningHours: tt.want.RunningHours, + RunningDays: tt.want.RunningDays, + AntiAffinity: tt.want.AntiAffinity, + Scale: tt.want.ScaleConfig, + } got, err := p.InstanceInspect(ctx, name) if !cmp.Equal(err, tt.wantErr) { t.Errorf("DockerClassicProvider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/provider/dockerswarm/service_inspect_test.go b/pkg/provider/dockerswarm/service_inspect_test.go index e71a878b..9f0e0125 100644 --- a/pkg/provider/dockerswarm/service_inspect_test.go +++ b/pkg/provider/dockerswarm/service_inspect_test.go @@ -193,6 +193,19 @@ func TestDockerSwarmProvider_GetState(t *testing.T) { tt.want.Name = name tt.want.Provider = "swarm" tt.want.Swarm = &sablier.SwarmServiceInfo{} + // The provider mirrors the parsed label config into Config with the + // same values as the flat fields each case already declares, so + // derive the expectation instead of repeating it per case. + tt.want.Config = &sablier.InstanceConfig{ + Enabled: tt.want.Enabled == "true", + Groups: tt.want.Groups, + ReadyAfter: tt.want.ReadyAfter, + ReadyOnStart: tt.want.ReadyOnStart, + RunningHours: tt.want.RunningHours, + RunningDays: tt.want.RunningDays, + AntiAffinity: tt.want.AntiAffinity, + Scale: tt.want.ScaleConfig, + } got, err := p.InstanceInspect(ctx, name) if !cmp.Equal(err, tt.wantErr) { t.Errorf("Provider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/provider/kubernetes/deployment_inspect_test.go b/pkg/provider/kubernetes/deployment_inspect_test.go index 9a2fcf6f..2ddd2dc6 100644 --- a/pkg/provider/kubernetes/deployment_inspect_test.go +++ b/pkg/provider/kubernetes/deployment_inspect_test.go @@ -264,6 +264,19 @@ func TestKubernetesProvider_DeploymentInspect(t *testing.T) { Image: "sablierapp/mimic:v0.3.3", Labels: labels, } + // The provider mirrors the parsed label config into Config with the + // same values as the flat fields each case already declares, so + // derive the expectation instead of repeating it per case. + tt.want.Config = &sablier.InstanceConfig{ + Enabled: tt.want.Enabled == "true", + Groups: tt.want.Groups, + ReadyAfter: tt.want.ReadyAfter, + ReadyOnStart: tt.want.ReadyOnStart, + RunningHours: tt.want.RunningHours, + RunningDays: tt.want.RunningDays, + AntiAffinity: tt.want.AntiAffinity, + Scale: tt.want.ScaleConfig, + } got, err := p.InstanceInspect(ctx, name) if !cmp.Equal(err, tt.wantErr) { t.Errorf("Provider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/provider/kubernetes/statefulset_inspect_test.go b/pkg/provider/kubernetes/statefulset_inspect_test.go index a44b5e89..66a096a0 100644 --- a/pkg/provider/kubernetes/statefulset_inspect_test.go +++ b/pkg/provider/kubernetes/statefulset_inspect_test.go @@ -221,6 +221,19 @@ func TestKubernetesProvider_InspectStatefulSet(t *testing.T) { Image: "sablierapp/mimic:v0.3.3", Labels: labels, } + // The provider mirrors the parsed label config into Config with the + // same values as the flat fields each case already declares, so + // derive the expectation instead of repeating it per case. + tt.want.Config = &sablier.InstanceConfig{ + Enabled: tt.want.Enabled == "true", + Groups: tt.want.Groups, + ReadyAfter: tt.want.ReadyAfter, + ReadyOnStart: tt.want.ReadyOnStart, + RunningHours: tt.want.RunningHours, + RunningDays: tt.want.RunningDays, + AntiAffinity: tt.want.AntiAffinity, + Scale: tt.want.ScaleConfig, + } got, err := p.InstanceInspect(ctx, name) if !cmp.Equal(err, tt.wantErr) { t.Errorf("DockerSwarmProvider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/provider/podman/container_inspect_test.go b/pkg/provider/podman/container_inspect_test.go index ecec3f9b..7f31cf10 100644 --- a/pkg/provider/podman/container_inspect_test.go +++ b/pkg/provider/podman/container_inspect_test.go @@ -308,6 +308,19 @@ func TestPodmanProvider_GetState(t *testing.T) { ID: name, Image: "docker.io/sablierapp/mimic:v0.3.3", } + // The provider mirrors the parsed label config into Config with the + // same values as the flat fields each case already declares, so + // derive the expectation instead of repeating it per case. + tt.want.Config = &sablier.InstanceConfig{ + Enabled: tt.want.Enabled == "true", + Groups: tt.want.Groups, + ReadyAfter: tt.want.ReadyAfter, + ReadyOnStart: tt.want.ReadyOnStart, + RunningHours: tt.want.RunningHours, + RunningDays: tt.want.RunningDays, + AntiAffinity: tt.want.AntiAffinity, + Scale: tt.want.ScaleConfig, + } got, err := p.InstanceInspect(ctx, name) if !cmp.Equal(err, tt.wantErr) { t.Errorf("PodmanProvider.InstanceInspect() error = %v, wantErr %v", err, tt.wantErr)