diff --git a/api/operator/v1beta1/vmagent_types.go b/api/operator/v1beta1/vmagent_types.go index 3dde588b1..d588fa505 100644 --- a/api/operator/v1beta1/vmagent_types.go +++ b/api/operator/v1beta1/vmagent_types.go @@ -644,7 +644,7 @@ func (cr *VMAgent) ScrapeSelectors(scrape client.Object) (*metav1.LabelSelector, // IsUnmanaged checks if object should managed any config objects func (cr *VMAgent) IsUnmanaged(scrape client.Object) bool { - if !cr.DeletionTimestamp.IsZero() || cr.Status.ParsingSpecError != "" { + if !cr.DeletionTimestamp.IsZero() || (cr.Status.ParsingSpecError != "" && !HasUnknownFields(cr.Status.ParsingSpecError)) { return true } if scrape == nil { diff --git a/api/operator/v1beta1/vmagent_types_test.go b/api/operator/v1beta1/vmagent_types_test.go index cfdd1fdce..6812f8416 100644 --- a/api/operator/v1beta1/vmagent_types_test.go +++ b/api/operator/v1beta1/vmagent_types_test.go @@ -163,3 +163,22 @@ func TestVMAgent_PrefixedName(t *testing.T) { f("myapp", false, "vmagent-myapp") f("myapp", true, "myapp") } + +// TestVMAgent_IsUnmanaged is the VMAgent counterpart of TestVMAlert_IsUnmanaged. +func TestVMAgent_IsUnmanaged(t *testing.T) { + f := func(cr VMAgent, want bool) { + t.Helper() + assert.Equal(t, want, cr.IsUnmanaged(nil)) + } + + f(VMAgent{Spec: VMAgentSpec{CommonScrapeParams: CommonScrapeParams{SelectAllByDefault: true}}}, false) + f(VMAgent{}, true) + f(VMAgent{ + Status: VMAgentStatus{ParsingSpecError: `json: unknown field "foo"`}, + Spec: VMAgentSpec{CommonScrapeParams: CommonScrapeParams{SelectAllByDefault: true}}, + }, false) + f(VMAgent{ + Status: VMAgentStatus{ParsingSpecError: "some other unrelated parse failure"}, + Spec: VMAgentSpec{CommonScrapeParams: CommonScrapeParams{SelectAllByDefault: true}}, + }, true) +} diff --git a/api/operator/v1beta1/vmalert_types.go b/api/operator/v1beta1/vmalert_types.go index 0775b59f7..9550d1c4f 100644 --- a/api/operator/v1beta1/vmalert_types.go +++ b/api/operator/v1beta1/vmalert_types.go @@ -527,7 +527,7 @@ func (cr *VMAlert) AsURL(isExtra bool) string { // IsUnmanaged checks if object should managed any config objects func (cr *VMAlert) IsUnmanaged() bool { - if !cr.DeletionTimestamp.IsZero() || cr.Status.ParsingSpecError != "" { + if !cr.DeletionTimestamp.IsZero() || (cr.Status.ParsingSpecError != "" && !HasUnknownFields(cr.Status.ParsingSpecError)) { return true } return !cr.Spec.SelectAllByDefault && cr.Spec.RuleSelector == nil && cr.Spec.RuleNamespaceSelector == nil diff --git a/api/operator/v1beta1/vmalert_types_test.go b/api/operator/v1beta1/vmalert_types_test.go index d37b0fda5..ac9ebc6b1 100644 --- a/api/operator/v1beta1/vmalert_types_test.go +++ b/api/operator/v1beta1/vmalert_types_test.go @@ -121,3 +121,25 @@ func TestVMAlert_PrefixedName(t *testing.T) { f("myapp", false, "vmalert-myapp") f("myapp", true, "myapp") } + +// TestVMAlert_IsUnmanaged: an unknown-fields-only ParsingSpecError (the operator's +// strict-unmarshal warning that every other ParsingSpecError check in the codebase treats +// as non-fatal) must not make an otherwise fully-configured VMAlert unmanaged, or rule +// selection silently never runs. +func TestVMAlert_IsUnmanaged(t *testing.T) { + f := func(cr VMAlert, want bool) { + t.Helper() + assert.Equal(t, want, cr.IsUnmanaged()) + } + + f(VMAlert{Spec: VMAlertSpec{SelectAllByDefault: true}}, false) + f(VMAlert{}, true) + f(VMAlert{ + Status: VMAlertStatus{ParsingSpecError: `json: unknown field "foo"`}, + Spec: VMAlertSpec{SelectAllByDefault: true}, + }, false) + f(VMAlert{ + Status: VMAlertStatus{ParsingSpecError: "some other unrelated parse failure"}, + Spec: VMAlertSpec{SelectAllByDefault: true}, + }, true) +} diff --git a/api/operator/v1beta1/vmalertmanager_types.go b/api/operator/v1beta1/vmalertmanager_types.go index 73e12f3c1..72ae7b421 100644 --- a/api/operator/v1beta1/vmalertmanager_types.go +++ b/api/operator/v1beta1/vmalertmanager_types.go @@ -494,7 +494,7 @@ func (*VMAlertmanager) ProbeNeedLiveness() bool { // IsUnmanaged checks if alertmanager should managed any alertmanager config objects func (cr *VMAlertmanager) IsUnmanaged() bool { - if !cr.DeletionTimestamp.IsZero() || cr.Status.ParsingSpecError != "" { + if !cr.DeletionTimestamp.IsZero() || (cr.Status.ParsingSpecError != "" && !HasUnknownFields(cr.Status.ParsingSpecError)) { return true } return !cr.Spec.SelectAllByDefault && cr.Spec.ConfigSelector == nil && cr.Spec.ConfigNamespaceSelector == nil diff --git a/api/operator/v1beta1/vmalertmanager_types_test.go b/api/operator/v1beta1/vmalertmanager_types_test.go index 874035b09..431494d85 100644 --- a/api/operator/v1beta1/vmalertmanager_types_test.go +++ b/api/operator/v1beta1/vmalertmanager_types_test.go @@ -74,3 +74,22 @@ func TestVMAlertmanager_PrefixedName(t *testing.T) { f("myapp", false, "vmalertmanager-myapp") f("myapp", true, "myapp") } + +// TestVMAlertmanager_IsUnmanaged is the VMAlertmanager counterpart of TestVMAlert_IsUnmanaged. +func TestVMAlertmanager_IsUnmanaged(t *testing.T) { + f := func(cr VMAlertmanager, want bool) { + t.Helper() + assert.Equal(t, want, cr.IsUnmanaged()) + } + + f(VMAlertmanager{Spec: VMAlertmanagerSpec{SelectAllByDefault: true}}, false) + f(VMAlertmanager{}, true) + f(VMAlertmanager{ + Status: VMAlertmanagerStatus{ParsingSpecError: `json: unknown field "foo"`}, + Spec: VMAlertmanagerSpec{SelectAllByDefault: true}, + }, false) + f(VMAlertmanager{ + Status: VMAlertmanagerStatus{ParsingSpecError: "some other unrelated parse failure"}, + Spec: VMAlertmanagerSpec{SelectAllByDefault: true}, + }, true) +} diff --git a/api/operator/v1beta1/vmauth_types.go b/api/operator/v1beta1/vmauth_types.go index 465efa226..bce71b8d4 100644 --- a/api/operator/v1beta1/vmauth_types.go +++ b/api/operator/v1beta1/vmauth_types.go @@ -772,7 +772,7 @@ func (cr *VMAuth) IsOwnsServiceAccount() bool { // IsUnmanaged checks if object should managed any config objects func (cr *VMAuth) IsUnmanaged() bool { - if !cr.DeletionTimestamp.IsZero() || cr.Status.ParsingSpecError != "" { + if !cr.DeletionTimestamp.IsZero() || (cr.Status.ParsingSpecError != "" && !HasUnknownFields(cr.Status.ParsingSpecError)) { return true } return (!cr.Spec.SelectAllByDefault && cr.Spec.UserSelector == nil && cr.Spec.UserNamespaceSelector == nil) || diff --git a/api/operator/v1beta1/vmauth_types_test.go b/api/operator/v1beta1/vmauth_types_test.go index 0b192a2a4..4ea8dcd64 100644 --- a/api/operator/v1beta1/vmauth_types_test.go +++ b/api/operator/v1beta1/vmauth_types_test.go @@ -141,3 +141,22 @@ func TestVMAuth_PrefixedName(t *testing.T) { f("myapp", false, "vmauth-myapp") f("myapp", true, "myapp") } + +// TestVMAuth_IsUnmanaged is the VMAuth counterpart of TestVMAlert_IsUnmanaged. +func TestVMAuth_IsUnmanaged(t *testing.T) { + f := func(cr VMAuth, want bool) { + t.Helper() + assert.Equal(t, want, cr.IsUnmanaged()) + } + + f(VMAuth{Spec: VMAuthSpec{SelectAllByDefault: true}}, false) + f(VMAuth{}, true) + f(VMAuth{ + Status: VMAuthStatus{ParsingSpecError: `json: unknown field "foo"`}, + Spec: VMAuthSpec{SelectAllByDefault: true}, + }, false) + f(VMAuth{ + Status: VMAuthStatus{ParsingSpecError: "some other unrelated parse failure"}, + Spec: VMAuthSpec{SelectAllByDefault: true}, + }, true) +} diff --git a/api/operator/v1beta1/vmsingle_types.go b/api/operator/v1beta1/vmsingle_types.go index 35c1aa248..c191ffe2b 100644 --- a/api/operator/v1beta1/vmsingle_types.go +++ b/api/operator/v1beta1/vmsingle_types.go @@ -141,7 +141,7 @@ func (cr *VMSingle) ScrapeSelectors(scrape client.Object) (*metav1.LabelSelector // IsUnmanaged checks if object should managed any config objects func (cr *VMSingle) IsUnmanaged(scrape client.Object) bool { - if !cr.DeletionTimestamp.IsZero() || cr.Status.ParsingSpecError != "" { + if !cr.DeletionTimestamp.IsZero() || (cr.Status.ParsingSpecError != "" && !HasUnknownFields(cr.Status.ParsingSpecError)) { return true } if scrape == nil { diff --git a/api/operator/v1beta1/vmsingle_types_test.go b/api/operator/v1beta1/vmsingle_types_test.go index 7362ccb7c..e9b4b7833 100644 --- a/api/operator/v1beta1/vmsingle_types_test.go +++ b/api/operator/v1beta1/vmsingle_types_test.go @@ -228,3 +228,22 @@ func TestVMSingle_PrefixedName(t *testing.T) { // useLegacyNaming — CR name used as-is f("myapp", true, "myapp") } + +// TestVMSingle_IsUnmanaged is the VMSingle counterpart of TestVMAlert_IsUnmanaged. +func TestVMSingle_IsUnmanaged(t *testing.T) { + f := func(cr VMSingle, want bool) { + t.Helper() + assert.Equal(t, want, cr.IsUnmanaged(nil)) + } + + f(VMSingle{Spec: VMSingleSpec{CommonScrapeParams: CommonScrapeParams{SelectAllByDefault: true}}}, false) + f(VMSingle{}, true) + f(VMSingle{ + Status: VMSingleStatus{ParsingSpecError: `json: unknown field "foo"`}, + Spec: VMSingleSpec{CommonScrapeParams: CommonScrapeParams{SelectAllByDefault: true}}, + }, false) + f(VMSingle{ + Status: VMSingleStatus{ParsingSpecError: "some other unrelated parse failure"}, + Spec: VMSingleSpec{CommonScrapeParams: CommonScrapeParams{SelectAllByDefault: true}}, + }, true) +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 20518e98c..bcef34969 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -45,6 +45,7 @@ aliases: * BUGFIX: [helm-converter](https://docs.victoriametrics.com/operator/helm-converter/): fix `extraVolumes`/`extraVolumeMounts` being silently dropped during conversion for vmsingle, vmagent, vmalert, vmanomaly, vmcluster, vlcluster, vtcluster, vtsingle, vlogs, and vmauth charts. See [#2424](https://github.com/VictoriaMetrics/operator/issues/2424). * BUGFIX: [vmagent](https://docs.victoriametrics.com/operator/resources/vmagent/), [vmanomaly](https://docs.victoriametrics.com/operator/resources/vmanomaly/): target `spec.vpa` at the `VMAgent`/`VMAnomaly` custom resource itself instead of its underlying `Deployment`/`StatefulSet`. VPA rejects a `targetRef` whose owner chain includes another scalable controller, so a `VerticalPodAutoscaler` targeting the workload directly was silently non-functional whenever the CR exposed a `scale` subresource. See [#2415](https://github.com/VictoriaMetrics/operator/issues/2415). * BUGFIX: [vlagent](https://docs.victoriametrics.com/operator/resources/vlagent/): remove a vestigial `scale` subresource declaration referencing nonexistent `spec`/`status` fields, which unconditionally broke `spec.vpa` for `VLAgent` the same way as [#2415](https://github.com/VictoriaMetrics/operator/issues/2415). +* BUGFIX: [vmalert](https://docs.victoriametrics.com/operator/resources/vmalert/), [vmagent](https://docs.victoriametrics.com/operator/resources/vmagent/), [vmalertmanager](https://docs.victoriametrics.com/operator/resources/vmalertmanager/), [vmauth](https://docs.victoriametrics.com/operator/resources/vmauth/), [vmsingle](https://docs.victoriametrics.com/operator/resources/vmsingle/): fix child object selection (`VMRule`, scrape objects, `VMAlertmanagerConfig`, `VMUser`) being silently skipped whenever the parent CR's spec contains a field the running operator version doesn't recognize (e.g. after a CRD/operator version mismatch), even though the CR otherwise reconciles successfully with no errors. See [#2444](https://github.com/VictoriaMetrics/operator/issues/2444). ## [v0.73.1](https://github.com/VictoriaMetrics/operator/releases/tag/v0.73.1) **Release date:** 08 July 2026