diff --git a/PROJECT b/PROJECT index cbdbea245..0beee2827 100644 --- a/PROJECT +++ b/PROJECT @@ -237,6 +237,30 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: victoriametrics.com + group: operator + kind: VMAnomalyModel + path: github.com/VictoriaMetrics/operator/api/operator/v1 + version: v1 + webhooks: + validation: true + webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: victoriametrics.com + group: operator + kind: VMAnomalyScheduler + path: github.com/VictoriaMetrics/operator/api/operator/v1 + version: v1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/api/operator/v1/common.go b/api/operator/v1/common.go index 3ca70c1e7..42dd46527 100644 --- a/api/operator/v1/common.go +++ b/api/operator/v1/common.go @@ -6,6 +6,7 @@ import ( "net/url" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( @@ -89,6 +90,20 @@ func (o *OAuth2) Validate() error { return nil } +// Selector defines object and namespace selectors +type Selector struct { + // ObjectSelector defines object to be selected for discovery. + // +optional + ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` + // NamespaceSelector defines namespaces to be selected for object discovery. + // +optional + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` +} + +func (s *Selector) IsUnmanaged() bool { + return s == nil || (s.ObjectSelector == nil && s.NamespaceSelector == nil) +} + // TLSConfig specifies TLS configuration parameters // with optional references to secrets with corresponding sensitive values type TLSConfig struct { diff --git a/api/operator/v1/vmanomaly_types.go b/api/operator/v1/vmanomaly_types.go index 0f373c656..d570a68b1 100644 --- a/api/operator/v1/vmanomaly_types.go +++ b/api/operator/v1/vmanomaly_types.go @@ -94,6 +94,12 @@ type VMAnomalySpec struct { // Monitoring configures how expose anomaly metrics // See https://docs.victoriametrics.com/anomaly-detection/components/monitoring/ Monitoring *VMAnomalyMonitoringSpec `json:"monitoring,omitempty"` + // ModelSelector defines VMAnomalyModel's object and namespace selectors. + // +optional + ModelSelector *Selector `json:"modelSelector,omitempty"` + // SchedulerSelector defines VMAnomalyScheduler's object and namespace selectors. + // +optional + SchedulerSelector *Selector `json:"schedulerSelector,omitempty"` // License allows to configure license key to be used for enterprise features. // Using license key is supported starting from VictoriaMetrics v1.94.0. // See [here](https://docs.victoriametrics.com/victoriametrics/enterprise/) @@ -457,6 +463,11 @@ func (cr *VMAnomalySpec) UnmarshalJSON(src []byte) error { return nil } +// IsUnmanaged checks if object should managed any config objects +func (cr *VMAnomaly) IsUnmanaged() bool { + return cr.Spec.ModelSelector.IsUnmanaged() && cr.Spec.SchedulerSelector.IsUnmanaged() +} + // +kubebuilder:object:root=true // VMAnomalyList contains a list of VMAnomaly. diff --git a/api/operator/v1/vmanomalymodel_types.go b/api/operator/v1/vmanomalymodel_types.go new file mode 100644 index 000000000..0efc133f7 --- /dev/null +++ b/api/operator/v1/vmanomalymodel_types.go @@ -0,0 +1,87 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" +) + +// VMAnomalyModelSpec defines the desired state of VMAnomalyModel. +type VMAnomalyModelSpec struct { + // ParsingError contents error with context if operator was failed to parse json object from kubernetes api server + ParsingError string `json:"-" yaml:"-"` + // Class defines anomaly detection model class + Class string `json:"class" yaml:"class"` + // Params defines anomaly detection model params + Params runtime.RawExtension `json:"params,omitempty" yaml:"params,omitempty"` +} + +// UnmarshalJSON implements json.Unmarshaler interface +func (cr *VMAnomalyModelSpec) UnmarshalJSON(src []byte) error { + type pcr VMAnomalyModelSpec + if err := json.Unmarshal(src, (*pcr)(cr)); err != nil { + cr.ParsingError = fmt.Sprintf("cannot parse spec: %s, err: %s", string(src), err) + return nil + } + return nil +} + +// VMAnomalyModelStatus defines the observed state of VMAnomalyModel. +type VMAnomalyModelStatus struct { + vmv1beta1.StatusMetadata `json:",inline"` +} + +// GetStatusMetadata returns metadata for object status +func (cr *VMAnomalyModel) GetStatusMetadata() *vmv1beta1.StatusMetadata { + return &cr.Status.StatusMetadata +} + +// AsKey returns unique key for object +func (cr *VMAnomalyModel) AsKey(_ bool) string { + return cr.Namespace + "/" + cr.Name +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// VMAnomalyModel is the Schema for the vmanomalymodels API. +type VMAnomalyModel struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VMAnomalyModelSpec `json:"spec,omitempty"` + Status VMAnomalyModelStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// VMAnomalyModelList contains a list of VMAnomalyModel. +type VMAnomalyModelList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VMAnomalyModel `json:"items"` +} + +func init() { + SchemeBuilder.Register(&VMAnomalyModel{}, &VMAnomalyModelList{}) +} diff --git a/api/operator/v1/vmanomalyscheduler_types.go b/api/operator/v1/vmanomalyscheduler_types.go new file mode 100644 index 000000000..0a15b01ac --- /dev/null +++ b/api/operator/v1/vmanomalyscheduler_types.go @@ -0,0 +1,87 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" +) + +// VMAnomalySchedulerSpec defines the desired state of VMAnomalyScheduler. +type VMAnomalySchedulerSpec struct { + // ParsingError contents error with context if operator was failed to parse json object from kubernetes api server + ParsingError string `json:"-" yaml:"-"` + // Class defines anomaly detection scheduler class + Class string `json:"class" yaml:"class"` + // Params defines anomaly detection scheduler params + Params runtime.RawExtension `json:"params,omitempty" yaml:"params,omitempty"` +} + +// VMAnomalySchedulerStatus defines the observed state of VMAnomalyScheduler. +type VMAnomalySchedulerStatus struct { + vmv1beta1.StatusMetadata `json:",inline"` +} + +// GetStatusMetadata returns metadata for object status +func (cr *VMAnomalyScheduler) GetStatusMetadata() *vmv1beta1.StatusMetadata { + return &cr.Status.StatusMetadata +} + +// AsKey returns unique key for object +func (cr *VMAnomalyScheduler) AsKey(_ bool) string { + return cr.Namespace + "/" + cr.Name +} + +// UnmarshalJSON implements json.Unmarshaler interface +func (cr *VMAnomalySchedulerSpec) UnmarshalJSON(src []byte) error { + type pcr VMAnomalySchedulerSpec + if err := json.Unmarshal(src, (*pcr)(cr)); err != nil { + cr.ParsingError = fmt.Sprintf("cannot parse spec: %s, err: %s", string(src), err) + return nil + } + return nil +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// VMAnomalyScheduler is the Schema for the vmanomalyschedulers API. +type VMAnomalyScheduler struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VMAnomalySchedulerSpec `json:"spec,omitempty"` + Status VMAnomalySchedulerStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// VMAnomalySchedulerList contains a list of VMAnomalyScheduler. +type VMAnomalySchedulerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VMAnomalyScheduler `json:"items"` +} + +func init() { + SchemeBuilder.Register(&VMAnomalyScheduler{}, &VMAnomalySchedulerList{}) +} diff --git a/api/operator/v1/zz_generated.deepcopy.go b/api/operator/v1/zz_generated.deepcopy.go index ead5889b4..8e3fa05f7 100644 --- a/api/operator/v1/zz_generated.deepcopy.go +++ b/api/operator/v1/zz_generated.deepcopy.go @@ -24,7 +24,8 @@ import ( "github.com/VictoriaMetrics/operator/api/operator/v1beta1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -64,6 +65,31 @@ func (in *OAuth2) DeepCopy() *OAuth2 { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Selector) DeepCopyInto(out *Selector) { + *out = *in + if in.ObjectSelector != nil { + in, out := &in.ObjectSelector, &out.ObjectSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.NamespaceSelector != nil { + in, out := &in.NamespaceSelector, &out.NamespaceSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Selector. +func (in *Selector) DeepCopy() *Selector { + if in == nil { + return nil + } + out := new(Selector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SyslogServerSpec) DeepCopyInto(out *SyslogServerSpec) { *out = *in @@ -1049,6 +1075,97 @@ func (in *VMAnomalyList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMAnomalyModel) DeepCopyInto(out *VMAnomalyModel) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAnomalyModel. +func (in *VMAnomalyModel) DeepCopy() *VMAnomalyModel { + if in == nil { + return nil + } + out := new(VMAnomalyModel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VMAnomalyModel) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMAnomalyModelList) DeepCopyInto(out *VMAnomalyModelList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VMAnomalyModel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAnomalyModelList. +func (in *VMAnomalyModelList) DeepCopy() *VMAnomalyModelList { + if in == nil { + return nil + } + out := new(VMAnomalyModelList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VMAnomalyModelList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMAnomalyModelSpec) DeepCopyInto(out *VMAnomalyModelSpec) { + *out = *in + in.Params.DeepCopyInto(&out.Params) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAnomalyModelSpec. +func (in *VMAnomalyModelSpec) DeepCopy() *VMAnomalyModelSpec { + if in == nil { + return nil + } + out := new(VMAnomalyModelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMAnomalyModelStatus) DeepCopyInto(out *VMAnomalyModelStatus) { + *out = *in + in.StatusMetadata.DeepCopyInto(&out.StatusMetadata) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAnomalyModelStatus. +func (in *VMAnomalyModelStatus) DeepCopy() *VMAnomalyModelStatus { + if in == nil { + return nil + } + out := new(VMAnomalyModelStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VMAnomalyMonitoringPullSpec) DeepCopyInto(out *VMAnomalyMonitoringPullSpec) { *out = *in @@ -1138,6 +1255,97 @@ func (in *VMAnomalyReadersSpec) DeepCopy() *VMAnomalyReadersSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMAnomalyScheduler) DeepCopyInto(out *VMAnomalyScheduler) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAnomalyScheduler. +func (in *VMAnomalyScheduler) DeepCopy() *VMAnomalyScheduler { + if in == nil { + return nil + } + out := new(VMAnomalyScheduler) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VMAnomalyScheduler) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMAnomalySchedulerList) DeepCopyInto(out *VMAnomalySchedulerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VMAnomalyScheduler, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAnomalySchedulerList. +func (in *VMAnomalySchedulerList) DeepCopy() *VMAnomalySchedulerList { + if in == nil { + return nil + } + out := new(VMAnomalySchedulerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VMAnomalySchedulerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMAnomalySchedulerSpec) DeepCopyInto(out *VMAnomalySchedulerSpec) { + *out = *in + in.Params.DeepCopyInto(&out.Params) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAnomalySchedulerSpec. +func (in *VMAnomalySchedulerSpec) DeepCopy() *VMAnomalySchedulerSpec { + if in == nil { + return nil + } + out := new(VMAnomalySchedulerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMAnomalySchedulerStatus) DeepCopyInto(out *VMAnomalySchedulerStatus) { + *out = *in + in.StatusMetadata.DeepCopyInto(&out.StatusMetadata) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMAnomalySchedulerStatus. +func (in *VMAnomalySchedulerStatus) DeepCopy() *VMAnomalySchedulerStatus { + if in == nil { + return nil + } + out := new(VMAnomalySchedulerStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VMAnomalySpec) DeepCopyInto(out *VMAnomalySpec) { *out = *in @@ -1208,6 +1416,16 @@ func (in *VMAnomalySpec) DeepCopyInto(out *VMAnomalySpec) { *out = new(VMAnomalyMonitoringSpec) (*in).DeepCopyInto(*out) } + if in.ModelSelector != nil { + in, out := &in.ModelSelector, &out.ModelSelector + *out = new(Selector) + (*in).DeepCopyInto(*out) + } + if in.SchedulerSelector != nil { + in, out := &in.SchedulerSelector, &out.SchedulerSelector + *out = new(Selector) + (*in).DeepCopyInto(*out) + } if in.License != nil { in, out := &in.License, &out.License *out = new(v1beta1.License) diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index f85fd86cd..ffb94e30e 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -24,6 +24,8 @@ resources: - bases/operator.victoriametrics.com_vtsingles.yaml - bases/operator.victoriametrics.com_vtclusters.yaml - bases/operator.victoriametrics.com_vmanomalies.yaml +- bases/operator.victoriametrics.com_vmanomalymodels.yaml +- bases/operator.victoriametrics.com_vmanomalyschedulers.yaml patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD diff --git a/config/crd/overlay/crd.specless.yaml b/config/crd/overlay/crd.specless.yaml index 3b6a79e13..06103d84c 100644 --- a/config/crd/overlay/crd.specless.yaml +++ b/config/crd/overlay/crd.specless.yaml @@ -1319,6 +1319,263 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: vmanomalymodels.operator.victoriametrics.com +spec: + group: operator.victoriametrics.com + names: + kind: VMAnomalyModel + listKind: VMAnomalyModelList + plural: vmanomalymodels + singular: vmanomalymodel + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: VMAnomalyModel is the Schema for the vmanomalymodels API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VMAnomalyModelSpec defines the desired state of VMAnomalyModel. + required: + - class + type: object + x-kubernetes-preserve-unknown-fields: true + status: + description: VMAnomalyModelStatus defines the observed state of VMAnomalyModel. + properties: + conditions: + description: 'Known .status.conditions.type are: "Available", "Progressing", + and "Degraded"' + items: + description: Condition defines status condition of the resource + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the last time of given type update. + This value is used for status TTL update and removal + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of condition in CamelCase or in name.namespace.resource.victoriametrics.com/CamelCase. + maxLength: 316 + type: string + required: + - lastTransitionTime + - lastUpdateTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: |- + ObservedGeneration defines current generation picked by operator for the + reconcile + format: int64 + type: integer + reason: + description: Reason defines human readable error reason + type: string + updateStatus: + description: UpdateStatus defines a status for update rollout + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: vmanomalyschedulers.operator.victoriametrics.com +spec: + group: operator.victoriametrics.com + names: + kind: VMAnomalyScheduler + listKind: VMAnomalySchedulerList + plural: vmanomalyschedulers + singular: vmanomalyscheduler + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: VMAnomalyScheduler is the Schema for the vmanomalyschedulers + API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VMAnomalySchedulerSpec defines the desired state of VMAnomalyScheduler. + required: + - class + type: object + x-kubernetes-preserve-unknown-fields: true + status: + description: VMAnomalySchedulerStatus defines the observed state of VMAnomalyScheduler. + properties: + conditions: + description: 'Known .status.conditions.type are: "Available", "Progressing", + and "Degraded"' + items: + description: Condition defines status condition of the resource + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the last time of given type update. + This value is used for status TTL update and removal + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of condition in CamelCase or in name.namespace.resource.victoriametrics.com/CamelCase. + maxLength: 316 + type: string + required: + - lastTransitionTime + - lastUpdateTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: |- + ObservedGeneration defines current generation picked by operator for the + reconcile + format: int64 + type: integer + reason: + description: Reason defines human readable error reason + type: string + updateStatus: + description: UpdateStatus defines a status for update rollout + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 diff --git a/config/crd/overlay/crd.yaml b/config/crd/overlay/crd.yaml index 59e0d253c..325dc2c3c 100644 --- a/config/crd/overlay/crd.yaml +++ b/config/crd/overlay/crd.yaml @@ -21909,6 +21909,105 @@ spec: Has no effect for VLogs and VMSingle format: int32 type: integer + modelSelector: + description: ModelSelector defines VMAnomalyModel's object and namespace + selectors. + properties: + namespaceSelector: + description: NamespaceSelector defines namespaces to be selected + for object discovery. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + objectSelector: + description: ObjectSelector defines object to be selected for + discovery. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object monitoring: description: |- Monitoring configures how expose anomaly metrics @@ -22729,6 +22828,105 @@ spec: schedulerName: description: SchedulerName - defines kubernetes scheduler name type: string + schedulerSelector: + description: SchedulerSelector defines VMAnomalyScheduler's object + and namespace selectors. + properties: + namespaceSelector: + description: NamespaceSelector defines namespaces to be selected + for object discovery. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + objectSelector: + description: ObjectSelector defines object to be selected for + discovery. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object secrets: description: |- Secrets is a list of Secrets in the same namespace as the Application @@ -23776,6 +23974,277 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: vmanomalymodels.operator.victoriametrics.com +spec: + group: operator.victoriametrics.com + names: + kind: VMAnomalyModel + listKind: VMAnomalyModelList + plural: vmanomalymodels + singular: vmanomalymodel + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: VMAnomalyModel is the Schema for the vmanomalymodels API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VMAnomalyModelSpec defines the desired state of VMAnomalyModel. + properties: + class: + description: Class defines anomaly detection model class + type: string + params: + description: Params defines anomaly detection model params + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - class + type: object + status: + description: VMAnomalyModelStatus defines the observed state of VMAnomalyModel. + properties: + conditions: + description: 'Known .status.conditions.type are: "Available", "Progressing", + and "Degraded"' + items: + description: Condition defines status condition of the resource + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the last time of given type update. + This value is used for status TTL update and removal + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of condition in CamelCase or in name.namespace.resource.victoriametrics.com/CamelCase. + maxLength: 316 + type: string + required: + - lastTransitionTime + - lastUpdateTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: |- + ObservedGeneration defines current generation picked by operator for the + reconcile + format: int64 + type: integer + reason: + description: Reason defines human readable error reason + type: string + updateStatus: + description: UpdateStatus defines a status for update rollout + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: vmanomalyschedulers.operator.victoriametrics.com +spec: + group: operator.victoriametrics.com + names: + kind: VMAnomalyScheduler + listKind: VMAnomalySchedulerList + plural: vmanomalyschedulers + singular: vmanomalyscheduler + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: VMAnomalyScheduler is the Schema for the vmanomalyschedulers + API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VMAnomalySchedulerSpec defines the desired state of VMAnomalyScheduler. + properties: + class: + description: Class defines anomaly detection scheduler class + type: string + params: + description: Params defines anomaly detection scheduler params + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - class + type: object + status: + description: VMAnomalySchedulerStatus defines the observed state of VMAnomalyScheduler. + properties: + conditions: + description: 'Known .status.conditions.type are: "Available", "Progressing", + and "Degraded"' + items: + description: Condition defines status condition of the resource + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the last time of given type update. + This value is used for status TTL update and removal + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of condition in CamelCase or in name.namespace.resource.victoriametrics.com/CamelCase. + maxLength: 316 + type: string + required: + - lastTransitionTime + - lastUpdateTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: |- + ObservedGeneration defines current generation picked by operator for the + reconcile + format: int64 + type: integer + reason: + description: Reason defines human readable error reason + type: string + updateStatus: + description: UpdateStatus defines a status for update rollout + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ec6058681..0af25f2a6 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -198,6 +198,12 @@ rules: - vmanomalies - vmanomalies/finalizers - vmanomalies/status + - vmanomalymodels + - vmanomalymodels/finalizers + - vmanomalymodels/status + - vmanomalyschedulers + - vmanomalyschedulers/finalizers + - vmanomalyschedulers/status verbs: - '*' - apiGroups: diff --git a/config/samples/operator_v1_vmanomalymodel.yaml b/config/samples/operator_v1_vmanomalymodel.yaml new file mode 100644 index 000000000..929c2b28d --- /dev/null +++ b/config/samples/operator_v1_vmanomalymodel.yaml @@ -0,0 +1,9 @@ +apiVersion: operator.victoriametrics.com/v1 +kind: VMAnomalyModel +metadata: + labels: + app.kubernetes.io/name: victoriametrics-operator + app.kubernetes.io/managed-by: kustomize + name: vmanomalymodel-sample +spec: + # TODO(user): Add fields here diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d6913adcc..9920997f6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,7 @@ aliases: * FEATURE: [vmagent](https://docs.victoriametrics.com/operator/resources/vmagent/): support `namespace` parameter in `attach_metadata` section for all scrape configurations. See [#1654](https://github.com/VictoriaMetrics/operator/issues/1654). * FEATURE: [vlagent](https://docs.victoriametrics.com/operator/resources/vlagent): support logs collection. See [#1501](https://github.com/VictoriaMetrics/operator/issues/1501). * FEATURE: [vmoperator](https://docs.victoriametrics.com/operator/): use `operator_bad_objects_total` metric with `object_namespace` and `crd` labels to track invalid objects managed by VMAgent, VMAuth, VMAlert and VMAlertmanager. Old `operator_alertmanager_bad_objects_count` and `operator_vmalert_bad_objects_count` are deprecated and will be removed in next releases. +* FEATURE: [vmoperator](https://docs.victoriametrics.com/operator/): add `object_namespace` label to the `operator_alertmanager_bad_objects_count` and `operator_vmalert_bad_objects_count` metrics. * BUGFIX: [vmoperator](https://docs.victoriametrics.com/operator/): fixed HPA cleanup logic for all cluster resources, before it was constantly recreated. Bug introduced in [this commit](https://github.com/VictoriaMetrics/operator/commit/983d1678c37497a7d03d2f57821219fd4975deec). @@ -104,6 +105,7 @@ This change could be reverted by providing env variable `VM_USECUSTOMCONFIGRELOA * BUGFIX: [vmoperator](https://docs.victoriametrics.com/operator/): fix an issue where the return value from a couple of controllers was always `nil`. See [#1532](https://github.com/VictoriaMetrics/operator/pull/1532) for details. * BUGFIX: [VMCluster](https://docs.victoriametrics.com/operator/resources/vmcluster/): emit warning if `vmcluster.spec.vmselect.persistentVolume` is set, previously it was emitted for `vmcluster.spec.vmselect.storage`. * BUGFIX: [vmoperator](https://docs.victoriametrics.com/operator/): Prevent endless Service reconcile loop by correctly track changes to Service.spec.LoadBalancerClass. See this issue [#1550](https://github.com/VictoriaMetrics/operator/issues/1550) for details. +* BUGFIX: [vmagent](https://docs.victoriametrics.com/operator/resources/vmagent/) and [vmanomaly](https://docs.victoriametrics.com/operator/resources/vmanomaly/): create PDB per shard to guarantee proper application protection. See [#1548](https://github.com/VictoriaMetrics/operator/issues/1548). ## [v0.63.0](https://github.com/VictoriaMetrics/operator/releases/tag/v0.63.0) diff --git a/docs/api.md b/docs/api.md index d2d7a8457..fbd4be4b7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -30,6 +30,8 @@ Package v1 contains API Schema definitions for the operator v1 API group - [VLCluster](#vlcluster) - [VLSingle](#vlsingle) - [VMAnomaly](#vmanomaly) +- [VMAnomalyModel](#vmanomalymodel) +- [VMAnomalyScheduler](#vmanomalyscheduler) - [VTCluster](#vtcluster) - [VTSingle](#vtsingle) @@ -66,6 +68,20 @@ Appears in: [VLAgentRemoteWriteSpec](#vlagentremotewritespec) | tokenURL#
_string_ | _(Required)_
TokenURL defines URL to fetch the token from | +#### Selector + + + +Selector defines object and namespace selectors + +Appears in: [VMAnomalySpec](#vmanomalyspec) + +| Field | Description | +| --- | --- | +| namespaceSelector#
_[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#labelselector-v1-meta)_ | _(Optional)_
NamespaceSelector defines namespaces to be selected for object discovery. | +| objectSelector#
_[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#labelselector-v1-meta)_ | _(Optional)_
ObjectSelector defines object to be selected for discovery. | + + #### SyslogServerSpec @@ -650,6 +666,38 @@ Appears in: [VMAnomalyMonitoringPushSpec](#vmanomalymonitoringpushspec), [VMAnom | tlsConfig#
_[TLSConfig](#tlsconfig)_ | _(Required)_
TLSConfig defines tls connection configuration | +#### VMAnomalyModel + + + +VMAnomalyModel is the Schema for the vmanomalymodels API. + + + +| Field | Description | +| --- | --- | +| apiVersion
_string_ | (Required)
`operator.victoriametrics.com/v1` | +| kind
_string_ | (Required)
`VMAnomalyModel` | +| metadata#
_[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#objectmeta-v1-meta)_ | _(Required)_
Refer to Kubernetes API documentation for fields of `metadata`. | +| spec#
_[VMAnomalyModelSpec](#vmanomalymodelspec)_ | _(Required)_
| + + +#### VMAnomalyModelSpec + + + +VMAnomalyModelSpec defines the desired state of VMAnomalyModel. + +Appears in: [VMAnomalyModel](#vmanomalymodel) + +| Field | Description | +| --- | --- | +| class#
_string_ | _(Required)_
Class defines anomaly detection model class | +| params#
_[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#rawextension-runtime-pkg)_ | _(Required)_
Params defines anomaly detection model params | + + + + #### VMAnomalyMonitoringPullSpec @@ -729,6 +777,38 @@ Appears in: [VMAnomalySpec](#vmanomalyspec) | tz#
_string_ | _(Required)_
Optional argumentspecifies the IANA timezone to account for local shifts, like DST, in models sensitive to seasonal patterns | +#### VMAnomalyScheduler + + + +VMAnomalyScheduler is the Schema for the vmanomalyschedulers API. + + + +| Field | Description | +| --- | --- | +| apiVersion
_string_ | (Required)
`operator.victoriametrics.com/v1` | +| kind
_string_ | (Required)
`VMAnomalyScheduler` | +| metadata#
_[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#objectmeta-v1-meta)_ | _(Required)_
Refer to Kubernetes API documentation for fields of `metadata`. | +| spec#
_[VMAnomalySchedulerSpec](#vmanomalyschedulerspec)_ | _(Required)_
| + + +#### VMAnomalySchedulerSpec + + + +VMAnomalySchedulerSpec defines the desired state of VMAnomalyScheduler. + +Appears in: [VMAnomalyScheduler](#vmanomalyscheduler) + +| Field | Description | +| --- | --- | +| class#
_string_ | _(Required)_
Class defines anomaly detection scheduler class | +| params#
_[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#rawextension-runtime-pkg)_ | _(Required)_
Params defines anomaly detection scheduler params | + + + + #### VMAnomalySpec @@ -762,6 +842,7 @@ Appears in: [VMAnomaly](#vmanomaly) | logLevel#
_string_ | _(Optional)_
LogLevel for VMAnomaly to be configured with.
INFO, WARN, ERROR, FATAL, PANIC | | managedMetadata#
_[ManagedObjectsMetadata](#managedobjectsmetadata)_ | _(Required)_
ManagedMetadata defines metadata that will be added to the all objects
created by operator for the given CustomResource | | minReadySeconds#
_integer_ | _(Optional)_
MinReadySeconds defines a minimum number of seconds to wait before starting update next pod
if previous in healthy state
Has no effect for VLogs and VMSingle | +| modelSelector#
_[Selector](#selector)_ | _(Optional)_
ModelSelector defines VMAnomalyModel's object and namespace selectors. | | monitoring#
_[VMAnomalyMonitoringSpec](#vmanomalymonitoringspec)_ | _(Required)_
Monitoring configures how expose anomaly metrics
See https://docs.victoriametrics.com/anomaly-detection/components/monitoring/ | | nodeSelector#
_object (keys:string, values:string)_ | _(Optional)_
NodeSelector Define which Nodes the Pods are scheduled on. | | paused#
_boolean_ | _(Optional)_
Paused If set to true all actions on the underlying managed objects are not
going to be performed, except for delete actions. | @@ -778,6 +859,7 @@ Appears in: [VMAnomaly](#vmanomaly) | rollingUpdateStrategy#
_[StatefulSetUpdateStrategyType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#statefulsetupdatestrategytype-v1-apps)_ | _(Optional)_
RollingUpdateStrategy allows configuration for strategyType
set it to RollingUpdate for disabling operator statefulSet rollingUpdate | | runtimeClassName#
_string_ | _(Optional)_
RuntimeClassName - defines runtime class for kubernetes pod.
https://kubernetes.io/docs/concepts/containers/runtime-class/ | | schedulerName#
_string_ | _(Optional)_
SchedulerName - defines kubernetes scheduler name | +| schedulerSelector#
_[Selector](#selector)_ | _(Optional)_
SchedulerSelector defines VMAnomalyScheduler's object and namespace selectors. | | secrets#
_string array_ | _(Optional)_
Secrets is a list of Secrets in the same namespace as the Application
object, which shall be mounted into the Application container
at /etc/vm/secrets/SECRET_NAME folder | | securityContext#
_[SecurityContext](#securitycontext)_ | _(Optional)_
SecurityContext holds pod-level security attributes and common container settings.
This defaults to the default PodSecurityContext. | | serviceAccountName#
_string_ | _(Optional)_
ServiceAccountName is the name of the ServiceAccount to use to run the pods | @@ -1502,7 +1584,7 @@ Appears in: [VLAgentSpec](#vlagentspec), [VLInsert](#vlinsert), [VLSelect](#vlse Condition defines status condition of the resource -Appears in: [ScrapeObjectStatus](#scrapeobjectstatus), [StatusMetadata](#statusmetadata), [VLAgentStatus](#vlagentstatus), [VLClusterStatus](#vlclusterstatus), [VLSingleStatus](#vlsinglestatus), [VLogsStatus](#vlogsstatus), [VMAgentStatus](#vmagentstatus), [VMAlertStatus](#vmalertstatus), [VMAlertmanagerConfigStatus](#vmalertmanagerconfigstatus), [VMAlertmanagerStatus](#vmalertmanagerstatus), [VMAnomalyStatus](#vmanomalystatus), [VMAuthStatus](#vmauthstatus), [VMClusterStatus](#vmclusterstatus), [VMRuleStatus](#vmrulestatus), [VMSingleStatus](#vmsinglestatus), [VMUserStatus](#vmuserstatus), [VTClusterStatus](#vtclusterstatus), [VTSingleStatus](#vtsinglestatus) +Appears in: [ScrapeObjectStatus](#scrapeobjectstatus), [StatusMetadata](#statusmetadata), [VLAgentStatus](#vlagentstatus), [VLClusterStatus](#vlclusterstatus), [VLSingleStatus](#vlsinglestatus), [VLogsStatus](#vlogsstatus), [VMAgentStatus](#vmagentstatus), [VMAlertStatus](#vmalertstatus), [VMAlertmanagerConfigStatus](#vmalertmanagerconfigstatus), [VMAlertmanagerStatus](#vmalertmanagerstatus), [VMAnomalyModelStatus](#vmanomalymodelstatus), [VMAnomalySchedulerStatus](#vmanomalyschedulerstatus), [VMAnomalyStatus](#vmanomalystatus), [VMAuthStatus](#vmauthstatus), [VMClusterStatus](#vmclusterstatus), [VMRuleStatus](#vmrulestatus), [VMSingleStatus](#vmsinglestatus), [VMUserStatus](#vmuserstatus), [VTClusterStatus](#vtclusterstatus), [VTSingleStatus](#vtsinglestatus) | Field | Description | | --- | --- | @@ -2971,7 +3053,7 @@ Appears in: [TargetRef](#targetref) StatusMetadata holds metadata of application update status -Appears in: [ScrapeObjectStatus](#scrapeobjectstatus), [VLAgentStatus](#vlagentstatus), [VLClusterStatus](#vlclusterstatus), [VLSingleStatus](#vlsinglestatus), [VLogsStatus](#vlogsstatus), [VMAgentStatus](#vmagentstatus), [VMAlertStatus](#vmalertstatus), [VMAlertmanagerConfigStatus](#vmalertmanagerconfigstatus), [VMAlertmanagerStatus](#vmalertmanagerstatus), [VMAnomalyStatus](#vmanomalystatus), [VMAuthStatus](#vmauthstatus), [VMClusterStatus](#vmclusterstatus), [VMRuleStatus](#vmrulestatus), [VMSingleStatus](#vmsinglestatus), [VMUserStatus](#vmuserstatus), [VTClusterStatus](#vtclusterstatus), [VTSingleStatus](#vtsinglestatus) +Appears in: [ScrapeObjectStatus](#scrapeobjectstatus), [VLAgentStatus](#vlagentstatus), [VLClusterStatus](#vlclusterstatus), [VLSingleStatus](#vlsinglestatus), [VLogsStatus](#vlogsstatus), [VMAgentStatus](#vmagentstatus), [VMAlertStatus](#vmalertstatus), [VMAlertmanagerConfigStatus](#vmalertmanagerconfigstatus), [VMAlertmanagerStatus](#vmalertmanagerstatus), [VMAnomalyModelStatus](#vmanomalymodelstatus), [VMAnomalySchedulerStatus](#vmanomalyschedulerstatus), [VMAnomalyStatus](#vmanomalystatus), [VMAuthStatus](#vmauthstatus), [VMClusterStatus](#vmclusterstatus), [VMRuleStatus](#vmrulestatus), [VMSingleStatus](#vmsinglestatus), [VMUserStatus](#vmuserstatus), [VTClusterStatus](#vtclusterstatus), [VTSingleStatus](#vtsinglestatus) | Field | Description | | --- | --- | @@ -3310,7 +3392,7 @@ _Underlying type:_ _string_ UpdateStatus defines status for application -Appears in: [ScrapeObjectStatus](#scrapeobjectstatus), [StatusMetadata](#statusmetadata), [VLAgentStatus](#vlagentstatus), [VLClusterStatus](#vlclusterstatus), [VLSingleStatus](#vlsinglestatus), [VLogsStatus](#vlogsstatus), [VMAgentStatus](#vmagentstatus), [VMAlertStatus](#vmalertstatus), [VMAlertmanagerConfigStatus](#vmalertmanagerconfigstatus), [VMAlertmanagerStatus](#vmalertmanagerstatus), [VMAnomalyStatus](#vmanomalystatus), [VMAuthStatus](#vmauthstatus), [VMClusterStatus](#vmclusterstatus), [VMRuleStatus](#vmrulestatus), [VMSingleStatus](#vmsinglestatus), [VMUserStatus](#vmuserstatus), [VTClusterStatus](#vtclusterstatus), [VTSingleStatus](#vtsinglestatus) +Appears in: [ScrapeObjectStatus](#scrapeobjectstatus), [StatusMetadata](#statusmetadata), [VLAgentStatus](#vlagentstatus), [VLClusterStatus](#vlclusterstatus), [VLSingleStatus](#vlsinglestatus), [VLogsStatus](#vlogsstatus), [VMAgentStatus](#vmagentstatus), [VMAlertStatus](#vmalertstatus), [VMAlertmanagerConfigStatus](#vmalertmanagerconfigstatus), [VMAlertmanagerStatus](#vmalertmanagerstatus), [VMAnomalyModelStatus](#vmanomalymodelstatus), [VMAnomalySchedulerStatus](#vmanomalyschedulerstatus), [VMAnomalyStatus](#vmanomalystatus), [VMAuthStatus](#vmauthstatus), [VMClusterStatus](#vmclusterstatus), [VMRuleStatus](#vmrulestatus), [VMSingleStatus](#vmsinglestatus), [VMUserStatus](#vmuserstatus), [VTClusterStatus](#vtclusterstatus), [VTSingleStatus](#vtsinglestatus) diff --git a/docs/resources/vmanomaly.md b/docs/resources/vmanomaly.md index 14f61f2aa..6c5bc52fb 100644 --- a/docs/resources/vmanomaly.md +++ b/docs/resources/vmanomaly.md @@ -326,6 +326,43 @@ spec: z_threshold: 2.5 ``` +## Dynamic configuration + +`VMAnomaly` supports discovering of configuration sections: + +- [VMAnomalyModel](https://docs.victoriametrics.com/operator/resources/vmanomalymodel/) - discovers anomaly detection [models](https://docs.victoriametrics.com/anomaly-detection/components/models/). +- [VMAnomalyScheduler](https://docs.victoriametrics.com/operator/resources/vmanomalyscheduler/) - discovers anomaly detection [schedulers](https://docs.victoriametrics.com/anomaly-detection/components/schedulers/). + +For filtering scrape objects `VMAnomaly` uses selectors. +Selectors are defined with suffixes - `namespaceSelector` and `objectSelector` for each type of configuration objects in spec of `VMAnomaly`: + +- `spec.modelSelector.namespaceSelector` and `spec.modelSelector.objectSelector` for selecting [VMAnomalyModel](https://docs.victoriametrics.com/operator/resources/vmanomalymodel/) objects +- `spec.schedulerSelector.namespaceSelector` and `spec.schedulerSelector.objectSelector` for selecting [VMAnomalyScheduler](https://docs.victoriametrics.com/operator/resources/vmanomalyscheduler/) objects + +It allows configuring objects access control across namespaces and different environments. +Specification of selectors you can see in [this doc](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#labelselector-v1-meta/). + +Following rules are applied: + +- If `namespaceSelector` and `objectSelector` both undefined, then by default select nothing. +- If `namespaceSelector` defined, `objectSelector` undefined, then all objects are matching at namespaces for given `namespaceSelector`. +- If `namespaceSelector` undefined, `objectSelector` defined, then all objects at `VMAnomaly`'s namespaces are matching for given `objectSelector`. +- If `namespaceSelector` and `objectSelector` both defined, then only objects at namespaces matched `namespaceSelector` for given `objectSelector` are matching. + +Here's a more visual and more detailed view: + +| `namespaceSelector` | `objectSelector` | `WATCH_NAMESPACE` | Selected objects | +|------------------------|------------------|-------------------|-------------------------------------------------------------------------------------------------------------| +| undefined | undefined | undefined | nothing | +| undefined | undefined | undefined | all objects of given type (`...`) in the cluster | +| **defined** | undefined | undefined | all objects of given type (`...`) at namespaces for given `namespaceSelector` | +| undefined | **defined** | undefined | all objects of given type (`...`) only at `VMAnomaly`'s namespace are matching for given `objectSelector` | +| **defined** | **defined** | undefined | all objects of given type (`...`) only at namespaces matched `namespaceSelector` for given `objectSelector` | +| *any* | undefined | **defined** | all objects of given type (`...`) only at `VMAnomaly`'s namespace | +| *any* | **defined** | **defined** | all objects of given type (`...`) only at `VMAnomaly`'s namespace for given `objectSelector` | + +More details about `WATCH_NAMESPACE` variable you can read in [this doc](https://docs.victoriametrics.com/operator/configuration/#namespaced-mode). + ## Version management To set `VMAnomaly` version add `spec.image.tag` name from [releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) diff --git a/docs/resources/vmanomalymodel.md b/docs/resources/vmanomalymodel.md new file mode 100644 index 000000000..b1a203d35 --- /dev/null +++ b/docs/resources/vmanomalymodel.md @@ -0,0 +1,112 @@ +--- +weight: 15 +title: VMAnomalyModel +menu: + docs: + identifier: operator-cr-vmanomalymodel + parent: operator-cr + weight: 8 +aliases: + - /operator/resources/vmanomalymodel/ + - /operator/resources/vmanomalymodel/index.html +tags: + - kubernetes + - metrics + - anomaly +--- +The `VMAnomalyModel` CRD allows to declaratively define anomaly detection [models](https://docs.victoriametrics.com/anomaly-detection/components/models/). + +`VMAnomalyModel` object updates [model](https://docs.victoriametrics.com/anomaly-detection/components/models/) section of [VMAnomaly](https://docs.victoriametrics.com/anomaly-detection/) +configuration by adding item with `{metadata.namespace}-{metadata.name}` key and `spec.params` value. If item with given key exists operator will override it: + +With given `VMAnomaly` CR: + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMAnomaly +metadata: + name: example + namespace: test +spec: + replicaCount: 2 + license: + key: "xx" + modelSelector: + objectSelector: + matchExpressions: + - key: app + operator: In + values: [test] + configRawYaml: | + reader: + queries: + ingestion_rate: + expr: 'sum(rate(vm_rows_inserted_total[5m])) by (type) > 0' + step: '1m' + schedulers: + scheduler_periodic_1m: + class: "periodic" + # class: "periodic" # or class: "scheduler.periodic.PeriodicScheduler" until v1.13.0 with class alias support) + infer_every: "1m" + fit_every: "2m" + fit_window: "3h" + models: + test-example-univariate: + class: 'zscore' + z_threshold: 2.5 + reader: + datasourceURL: http://vmsingle-read-example:8428 + samplingPeriod: 10s + writer: + datasourceURL: http://vmsingle-write-example:8428 + monitoring: + push: + url: http://vmsingle-monitoring-example:8428 +``` + +and `VMAnomalyModel` CR + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMAnomalyModel +metadata: + name: example-univariate + namespace: test + labels: + app: test +spec: + class: zscore + params: + z_threshold: 3.0 +``` + +result anomaly detection configuration is: + +```yaml +schedulers: + scheduler_periodic_1m: + class: "periodic" + infer_every: "1m" + fit_every: "2m" + fit_window: "3h" +models: + test-example-univariate: + class: 'zscore' + z_threshold: 3.0 +reader: + datasourceURL: http://vmsingle-read-example:8428 + samplingPeriod: 10s + queries: + ingestion_rate: + expr: 'sum(rate(vm_rows_inserted_total[5m])) by (type) > 0' + step: '1m' +writer: + datasourceURL: http://vmsingle-write-example:8428 +``` + +## Specification + +You can see the full actual specification of the `VMAnomalyModel` resource in +the **[API docs -> VMAnomalyModel](https://docs.victoriametrics.com/operator/api/#vmanomalymodel)**. + + diff --git a/docs/resources/vmanomalyscheduler.md b/docs/resources/vmanomalyscheduler.md new file mode 100644 index 000000000..abec2a5f8 --- /dev/null +++ b/docs/resources/vmanomalyscheduler.md @@ -0,0 +1,113 @@ +--- +weight: 15 +title: VMAnomalyScheduler +menu: + docs: + identifier: operator-cr-vmanomalyscheduler + parent: operator-cr + weight: 8 +aliases: + - /operator/resources/vmanomalyscheduler/ + - /operator/resources/vmanomalyscheduler/index.html +tags: + - kubernetes + - metrics + - anomaly +--- +The `VMAnomalyScheduler` CRD allows to declaratively define anomaly detection [schedulers](https://docs.victoriametrics.com/anomaly-detection/components/schedulers/). + +`VMAnomalyScheduler` object updates [scheduler](https://docs.victoriametrics.com/anomaly-detection/components/schedulers/) section of [VMAnomaly](https://docs.victoriametrics.com/anomaly-detection/) +configuration by adding item with `{metadata.namespace}-{metadata.name}` key and `spec.params` value. If item with given key exists operator will override it: + +With given `VMAnomaly` CR: + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMAnomaly +metadata: + name: example + namespace: test +spec: + replicaCount: 2 + license: + key: "xx" + schedulerSelector: + objectSelector: + matchExpressions: + - key: app + operator: In + values: [test] + configRawYaml: | + reader: + queries: + ingestion_rate: + expr: 'sum(rate(vm_rows_inserted_total[5m])) by (type) > 0' + step: '1m' + schedulers: + test-example-periodic: + class: "periodic" + infer_every: "1m" + fit_every: "2m" + fit_window: "3h" + models: + test: + class: 'zscore' + z_threshold: 2.5 + reader: + datasourceURL: http://vmsingle-read-example:8428 + samplingPeriod: 10s + writer: + datasourceURL: http://vmsingle-write-example:8428 + monitoring: + push: + url: http://vmsingle-monitoring-example:8428 +``` + +and `VMAnomalyScheduler` CR + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMAnomalyScheduler +metadata: + name: example-periodic + namespace: test + labels: + app: test +spec: + class: periodic + params: + infer_every: 2m + fit_every: 4m + fit_window: 5h +``` + +result anomaly detection configuration is: + +```yaml +schedulers: + test-example-periodic: + class: "periodic" + infer_every: 2m + fit_every: 4m + fit_window: 5h +models: + test: + class: 'zscore' + z_threshold: 2.5 +reader: + datasourceURL: http://vmsingle-read-example:8428 + samplingPeriod: 10s + queries: + ingestion_rate: + expr: 'sum(rate(vm_rows_inserted_total[5m])) by (type) > 0' + step: '1m' +writer: + datasourceURL: http://vmsingle-write-example:8428 +``` + +## Specification + +You can see the full actual specification of the `VMAnomalyScheduler` resource in +the **[API docs -> VMAnomalyScheduler](https://docs.victoriametrics.com/operator/api/#vmanomalyscheduler)**. + + diff --git a/internal/controller/operator/factory/build/pdb.go b/internal/controller/operator/factory/build/pdb.go index 8886b8366..a70d8d460 100644 --- a/internal/controller/operator/factory/build/pdb.go +++ b/internal/controller/operator/factory/build/pdb.go @@ -1,6 +1,9 @@ package build import ( + "fmt" + "strconv" + policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -9,6 +12,11 @@ import ( // PodDisruptionBudget creates object for given CRD func PodDisruptionBudget(cr builderOpts, spec *vmv1beta1.EmbeddedPodDisruptionBudgetSpec) *policyv1.PodDisruptionBudget { + return PodDisruptionBudgetSharded(cr, spec, nil) +} + +// PodDisruptionBudgetSharded creates object for given CRD and shard num +func PodDisruptionBudgetSharded(cr builderOpts, spec *vmv1beta1.EmbeddedPodDisruptionBudgetSpec, num *int) *policyv1.PodDisruptionBudget { pdb := policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Name: cr.PrefixedName(), @@ -25,6 +33,10 @@ func PodDisruptionBudget(cr builderOpts, spec *vmv1beta1.EmbeddedPodDisruptionBu }, }, } + if num != nil { + pdb.Name = fmt.Sprintf("%s-%d", pdb.Name, *num) + pdb.Spec.Selector.MatchLabels[shardLabelName] = strconv.Itoa(*num) + } if len(spec.UnhealthyPodEvictionPolicy) > 0 { p := policyv1.UnhealthyPodEvictionPolicyType(spec.UnhealthyPodEvictionPolicy) pdb.Spec.UnhealthyPodEvictionPolicy = &p diff --git a/internal/controller/operator/factory/k8stools/test_helpers.go b/internal/controller/operator/factory/k8stools/test_helpers.go index 89f6066df..7daf48a17 100644 --- a/internal/controller/operator/factory/k8stools/test_helpers.go +++ b/internal/controller/operator/factory/k8stools/test_helpers.go @@ -45,6 +45,8 @@ func testGetScheme() *runtime.Scheme { &vmv1.VLClusterList{}, &vmv1.VTClusterList{}, &vmv1.VMAnomalyList{}, + &vmv1.VMAnomalyModelList{}, + &vmv1.VMAnomalySchedulerList{}, &vmv1.VLAgentList{}, &gwapiv1.HTTPRouteList{}, &apiextensionsv1.CustomResourceDefinitionList{}, @@ -72,6 +74,8 @@ func testGetScheme() *runtime.Scheme { &vmv1.VTSingle{}, &vmv1.VTCluster{}, &vmv1.VMAnomaly{}, + &vmv1.VMAnomalyModel{}, + &vmv1.VMAnomalyScheduler{}, &vmv1.VLAgent{}, &gwapiv1.HTTPRoute{}, &apiextensionsv1.CustomResourceDefinition{}, @@ -113,6 +117,8 @@ func GetTestClientWithClientObjects(predefinedObjects []client.Object) client.Cl &vmv1.VTSingle{}, &vmv1.VTCluster{}, &vmv1.VMAnomaly{}, + &vmv1.VMAnomalyModel{}, + &vmv1.VMAnomalyScheduler{}, &vmv1.VLAgent{}, &gwapiv1.HTTPRoute{}, ). diff --git a/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go b/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go index c5600ee32..478c24036 100644 --- a/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go +++ b/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go @@ -248,11 +248,7 @@ func createOrUpdateConfigurationSecret(ctx context.Context, rclient client.Clien } } - if err := pos.updateStatusesForScrapeObjects(ctx, rclient, cr, childObject); err != nil { - return err - } - - return nil + return pos.updateStatusesForScrapeObjects(ctx, rclient, cr, childObject) } func (pos *parsedObjects) updateStatusesForScrapeObjects(ctx context.Context, rclient client.Client, cr *vmv1beta1.VMAgent, childObject client.Object) error { diff --git a/internal/controller/operator/factory/vmanomaly/config.go b/internal/controller/operator/factory/vmanomaly/config.go index 6a23facab..19a5c73de 100644 --- a/internal/controller/operator/factory/vmanomaly/config.go +++ b/internal/controller/operator/factory/vmanomaly/config.go @@ -16,16 +16,34 @@ import ( "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/vmanomaly/config" ) +// CreateOrUpdateConfig builds configuration for VMAnomaly +func CreateOrUpdateConfig(ctx context.Context, rclient client.Client, cr *vmv1.VMAnomaly, childObject client.Object) error { + var prevCR *vmv1.VMAnomaly + if cr.ParsedLastAppliedSpec != nil { + prevCR = cr.DeepCopy() + prevCR.Spec = *cr.ParsedLastAppliedSpec + } + ac := getAssetsCache(ctx, rclient, cr) + if _, err := createOrUpdateConfig(ctx, rclient, cr, prevCR, childObject, ac); err != nil { + return err + } + return nil +} + // createOrUpdateConfig reconcile configuration for vmanomaly and returns configuration consistent hash -func createOrUpdateConfig(ctx context.Context, rclient client.Client, cr, prevCR *vmv1.VMAnomaly, ac *build.AssetsCache) (string, error) { - data, err := config.Load(cr, ac) +func createOrUpdateConfig(ctx context.Context, rclient client.Client, cr, prevCR *vmv1.VMAnomaly, childObject client.Object, ac *build.AssetsCache) (string, error) { + pos, err := config.NewParsedObjects(ctx, rclient, cr) + if err != nil { + return "", err + } + data, err := pos.Load(cr, ac) if err != nil { return "", err } newSecretConfig := &corev1.Secret{ ObjectMeta: build.ResourceMeta(build.SecretConfigResourceKind, cr), Data: map[string][]byte{ - secretConfigKey: data, + configEnvsubstFilename: data, }, } @@ -49,6 +67,10 @@ func createOrUpdateConfig(ctx context.Context, rclient client.Client, cr, prevCR return "", err } + if err := pos.UpdateStatusesForChildObjects(ctx, rclient, cr, childObject); err != nil { + return "", err + } + hash := sha256.New() hash.Write(data) hashBytes := hash.Sum(nil) diff --git a/internal/controller/operator/factory/vmanomaly/config/config.go b/internal/controller/operator/factory/vmanomaly/config/config.go index 1e4e8144f..28fb3ef6c 100644 --- a/internal/controller/operator/factory/vmanomaly/config/config.go +++ b/internal/controller/operator/factory/vmanomaly/config/config.go @@ -1,17 +1,92 @@ package config import ( + "context" "fmt" + "reflect" "strconv" "strings" "github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil" "gopkg.in/yaml.v2" + "sigs.k8s.io/controller-runtime/pkg/client" vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/build" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/reconcile" ) +func NewParsedObjects(ctx context.Context, rclient client.Client, cr *vmv1.VMAnomaly) (*ParsedObjects, error) { + models, err := selectModels(ctx, rclient, cr) + if err != nil { + return nil, fmt.Errorf("selecting VMAnomalyModels failed: %w", err) + } + schedulers, err := selectSchedulers(ctx, rclient, cr) + if err != nil { + return nil, fmt.Errorf("selecting VMAnomalySchedulers failed: %w", err) + } + return &ParsedObjects{ + models: models, + schedulers: schedulers, + }, nil +} + +type ParsedObjects struct { + models *build.ChildObjects[*vmv1.VMAnomalyModel] + schedulers *build.ChildObjects[*vmv1.VMAnomalyScheduler] +} + +// Load returns vmanomaly config merged with provided secrets +func (pos *ParsedObjects) Load(cr *vmv1.VMAnomaly, ac *build.AssetsCache) ([]byte, error) { + var data []byte + switch { + case cr.Spec.ConfigSecret != nil: + secret, err := ac.LoadKeyFromSecret(cr.Namespace, cr.Spec.ConfigSecret) + if err != nil { + return nil, fmt.Errorf("cannot fetch secret content for anomaly config secret, name=%q: %w", cr.Name, err) + } + data = []byte(secret) + case cr.Spec.ConfigRawYaml != "": + data = []byte(cr.Spec.ConfigRawYaml) + default: + return nil, fmt.Errorf(`either "configRawYaml" or "configSecret" are required`) + } + c := &config{} + err := yaml.UnmarshalStrict(data, c) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal anomaly configuration, name=%q: %w", cr.Name, err) + } + if err = c.override(cr, pos, ac); err != nil { + return nil, fmt.Errorf("failed to update secret values with values from anomaly instance, name=%q: %w", cr.Name, err) + } + if err = c.validate(); err != nil { + return nil, fmt.Errorf("failed to validate anomaly configuration, name=%q: %w", cr.Name, err) + } + output := c.marshal() + if data, err = yaml.Marshal(output); err != nil { + return nil, fmt.Errorf("failed to marshal anomaly configuration, name=%q: %w", cr.Name, err) + } + return data, nil +} + +func (pos *ParsedObjects) UpdateStatusesForChildObjects(ctx context.Context, rclient client.Client, cr *vmv1.VMAnomaly, childObject client.Object) error { + parentObject := fmt.Sprintf("%s.%s.vmanomaly", cr.Name, cr.Namespace) + if childObject != nil && !reflect.ValueOf(childObject).IsNil() { + // fast path + switch obj := childObject.(type) { + case *vmv1.VMAnomalyModel: + if o := pos.models.Get(obj); o != nil { + return reconcile.StatusForChildObjects(ctx, rclient, parentObject, []*vmv1.VMAnomalyModel{o}) + } + case *vmv1.VMAnomalyScheduler: + if o := pos.schedulers.Get(obj); o != nil { + return reconcile.StatusForChildObjects(ctx, rclient, parentObject, []*vmv1.VMAnomalyScheduler{o}) + } + } + } + return nil +} + type header struct { Class string `yaml:"class"` Fields map[string]any `yaml:",inline"` @@ -22,8 +97,8 @@ type validatable interface { } type config struct { - Schedulers map[string]*scheduler `yaml:"schedulers,omitempty"` - Models map[string]*model `yaml:"models,omitempty"` + Schedulers map[string]*Scheduler `yaml:"schedulers,omitempty"` + Models map[string]*Model `yaml:"models,omitempty"` Reader *reader `yaml:"reader,omitempty"` Writer *writer `yaml:"writer,omitempty"` Monitoring *monitoring `yaml:"monitoring,omitempty"` @@ -38,23 +113,23 @@ type settings struct { RestoreState bool `yaml:"restore_state,omitempty"` } -func (c *config) override(cr *vmv1.VMAnomaly, ac *build.AssetsCache) error { +func (c *config) override(cr *vmv1.VMAnomaly, pos *ParsedObjects, ac *build.AssetsCache) error { c.Preset = strings.ToLower(c.Preset) if strings.HasPrefix(c.Preset, "ui") { + s := new(noopScheduler) + s.setClass("noop") c.Reader = &reader{ Class: "noop", } c.Writer = &writer{ Class: "noop", } - c.Schedulers = map[string]*scheduler{ + c.Schedulers = map[string]*Scheduler{ "noop": { - validatable: &noopScheduler{ - Class: "noop", - }, + anomalyScheduler: s, }, } - c.Models = map[string]*model{ + c.Models = map[string]*Model{ "placeholder": { anomalyModel: &zScoreModel{ commonModelParams: commonModelParams{ @@ -140,6 +215,28 @@ func (c *config) override(cr *vmv1.VMAnomaly, ac *build.AssetsCache) error { } c.Monitoring = &m } + + // override models + pos.models.ForEachCollectSkipInvalid(func(m *vmv1.VMAnomalyModel) error { + name := fmt.Sprintf("%s-%s", m.Namespace, m.Name) + nm, err := modelFromSpec(&m.Spec) + if err != nil { + return fmt.Errorf("failed to unmarshal model=%q: %w", name, err) + } + c.Models[name] = nm + return nil + }) + + // override schedulers + pos.schedulers.ForEachCollectSkipInvalid(func(s *vmv1.VMAnomalyScheduler) error { + name := fmt.Sprintf("%s-%s", s.Namespace, s.Name) + ns, err := schedulerFromSpec(&s.Spec) + if err != nil { + return fmt.Errorf("failed to unmarshal scheduler=%q: %w", name, err) + } + c.Schedulers[name] = ns + return nil + }) return nil } @@ -289,36 +386,3 @@ func (c *clientConfig) override(cr *vmv1.VMAnomaly, cfg *vmv1.VMAnomalyHTTPClien } return nil } - -// Load returns vmanomaly config merged with provided secrets -func Load(cr *vmv1.VMAnomaly, ac *build.AssetsCache) ([]byte, error) { - var data []byte - switch { - case cr.Spec.ConfigSecret != nil: - secret, err := ac.LoadKeyFromSecret(cr.Namespace, cr.Spec.ConfigSecret) - if err != nil { - return nil, fmt.Errorf("cannot fetch secret content for anomaly config secret, name=%q: %w", cr.Name, err) - } - data = []byte(secret) - case cr.Spec.ConfigRawYaml != "": - data = []byte(cr.Spec.ConfigRawYaml) - default: - return nil, fmt.Errorf(`either "configRawYaml" or "configSecret" are required`) - } - c := &config{} - err := yaml.UnmarshalStrict(data, c) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal anomaly configuration, name=%q: %w", cr.Name, err) - } - if err = c.override(cr, ac); err != nil { - return nil, fmt.Errorf("failed to update secret values with values from anomaly instance, name=%q: %w", cr.Name, err) - } - if err = c.validate(); err != nil { - return nil, fmt.Errorf("failed to validate anomaly configuration, name=%q: %w", cr.Name, err) - } - output := c.marshal() - if data, err = yaml.Marshal(output); err != nil { - return nil, fmt.Errorf("failed to marshal anomaly configuration, name=%q: %w", cr.Name, err) - } - return data, nil -} diff --git a/internal/controller/operator/factory/vmanomaly/config/config_test.go b/internal/controller/operator/factory/vmanomaly/config/config_test.go index 289cb24d5..4336e3d02 100644 --- a/internal/controller/operator/factory/vmanomaly/config/config_test.go +++ b/internal/controller/operator/factory/vmanomaly/config/config_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -38,7 +39,9 @@ func TestLoad(t *testing.T) { }, } ac := build.NewAssetsCache(ctx, fclient, cfg) - loaded, err := Load(o.cr, ac) + pos, err := NewParsedObjects(ctx, fclient, o.cr) + assert.NoError(t, err) + loaded, err := pos.Load(o.cr, ac) if (err != nil) != o.wantErr { t.Fatalf("Load() error = %v, wantErr %v", err, o.wantErr) } @@ -307,6 +310,180 @@ monitoring: label1: value1 settings: restore_state: true +`, + }) + + // with external models + f(opts{ + cr: &vmv1.VMAnomaly{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-anomaly", + Namespace: "default", + }, + Spec: vmv1.VMAnomalySpec{ + License: &vmv1beta1.License{ + Key: ptr.To("test"), + }, + ConfigRawYaml: ` +reader: + class: vm + datasource_url: "http://test.com" + sampling_period: 1m + queries: + test: + expr: vm_metric + data_range: [0, inf] +writer: + class: vm + datasource_url: "http://test.com" +models: + model_univariate_1: + class: 'zscore' + z_threshold: 2.5 + queries: ['test'] +schedulers: + scheduler_periodic_1m: + class: "scheduler.periodic.PeriodicScheduler" + infer_every: 1m + fit_every: 2m + fit_window: 3h +settings: + restore_state: true +`, + ModelSelector: &vmv1.Selector{ + ObjectSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "app", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"prod"}, + }, + }, + }, + }, + SchedulerSelector: &vmv1.Selector{ + ObjectSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "app", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"prod"}, + }, + }, + }, + }, + Writer: &vmv1.VMAnomalyWritersSpec{ + DatasourceURL: "http://write.endpoint", + MetricFormat: vmv1.VMAnomalyVMWriterMetricFormatSpec{ + Name: "metrics_$VAR", + For: "custom_$QUERY_KEY", + ExtraLabels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + }, + VMAnomalyHTTPClientSpec: vmv1.VMAnomalyHTTPClientSpec{ + TenantID: "0:2", + }, + }, + Reader: &vmv1.VMAnomalyReadersSpec{ + DatasourceURL: "http://custom.ds", + QueryRangePath: "/api/v1/query_range", + SamplingPeriod: "10s", + VMAnomalyHTTPClientSpec: vmv1.VMAnomalyHTTPClientSpec{ + TenantID: "0:1", + }, + }, + }, + }, + predefinedObjects: []runtime.Object{ + &vmv1.VMAnomalyModel{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-anomaly", + Labels: map[string]string{ + "app": "prod", + }, + Namespace: "default", + }, + Spec: vmv1.VMAnomalyModelSpec{ + Class: "zscore", + Params: runtime.RawExtension{ + Raw: []byte(`{ + "queries": ["test"], + "z_threshold": 2.5 +}`), + }, + }, + }, + &vmv1.VMAnomalyScheduler{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-anomaly", + Labels: map[string]string{ + "app": "prod", + }, + Namespace: "default", + }, + Spec: vmv1.VMAnomalySchedulerSpec{ + Class: "periodic", + Params: runtime.RawExtension{ + Raw: []byte(`{ + "fit_every": "12m", + "fit_window": "13h", + "infer_every": "11m" +}`), + }, + }, + }, + }, + expected: ` +models: + default-test-anomaly: + class: zscore + queries: + - test + z_threshold: 2.5 + model_univariate_1: + class: zscore + queries: + - test + z_threshold: 2.5 +schedulers: + default-test-anomaly: + class: periodic + fit_every: 12m + fit_window: 13h + infer_every: 11m + scheduler_periodic_1m: + class: scheduler.periodic.PeriodicScheduler + fit_every: 2m + fit_window: 3h + infer_every: 1m +reader: + class: vm + datasource_url: http://custom.ds + sampling_period: 10s + query_range_path: /api/v1/query_range + queries: + test: + expr: vm_metric + data_range: + - "0" + - inf + tenant_id: "0:1" +writer: + class: vm + datasource_url: http://write.endpoint + metric_format: + __name__: metrics_$VAR + for: custom_$QUERY_KEY + label1: value1 + label2: value2 + tenant_id: "0:2" +monitoring: + pull: + port: "8080" +settings: + restore_state: true `, }) } diff --git a/internal/controller/operator/factory/vmanomaly/config/models.go b/internal/controller/operator/factory/vmanomaly/config/models.go index ca68fccd6..016660278 100644 --- a/internal/controller/operator/factory/vmanomaly/config/models.go +++ b/internal/controller/operator/factory/vmanomaly/config/models.go @@ -1,10 +1,16 @@ package config import ( + "context" "fmt" "time" "gopkg.in/yaml.v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/build" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools" ) type modelDetectionDirection string @@ -36,47 +42,24 @@ func (p commonModelParams) schedulers() []string { return p.Schedulers } +func (p *commonModelParams) setClass(class string) { + p.Class = class +} + type anomalyModel interface { validatable + setClass(string) schedulers() []string queries() []string } -type model struct { +type Model struct { anomalyModel } -var ( - _ yaml.Marshaler = (*model)(nil) - _ yaml.Unmarshaler = (*model)(nil) -) - -// MarshalYAML implements yaml.Marshaller interface -func (m *model) MarshalYAML() (any, error) { - return m.anomalyModel, nil -} - -type onlineModel struct { - Decay float64 `yaml:"decay,omitempty"` -} - -func (m *onlineModel) validate() error { - // See https://docs.victoriametrics.com/anomaly-detection/components/models/#decay - // Valid values are in the range [0, 1]. - if m.Decay < 0 || m.Decay > 1 { - return fmt.Errorf("decay must be in range [0, 1], got %f", m.Decay) - } - return nil -} - -// UnmarshalYAML implements yaml.Unmarshaler interface -func (m *model) UnmarshalYAML(unmarshal func(any) error) error { - var h header - if err := unmarshal(&h); err != nil { - return err - } +func (m *Model) init(class string) error { var mdl anomalyModel - switch h.Class { + switch class { case "model.auto.AutoTunedModel", "auto": mdl = new(autoTunedModel) case "model.prophet.ProphetModel", "prophet": @@ -102,12 +85,67 @@ func (m *model) UnmarshalYAML(unmarshal func(any) error) error { case "model.isolation_forest.IsolationForestMultivariateModel", "isolation_forest_multivariate": mdl = new(isolationForestMultivariateModel) default: - return fmt.Errorf("model class=%q is not supported", h.Class) + return fmt.Errorf("model class=%q is not supported", class) } - if err := unmarshal(mdl); err != nil { + m.anomalyModel = mdl + return nil +} + +var ( + _ yaml.Marshaler = (*Model)(nil) + _ yaml.Unmarshaler = (*Model)(nil) +) + +// Validate validates raw config +func (m *Model) Validate(data []byte) error { + if err := yaml.Unmarshal(data, m); err != nil { + return err + } + return m.validate() +} + +func modelFromSpec(spec *vmv1.VMAnomalyModelSpec) (*Model, error) { + var m Model + if err := m.init(spec.Class); err != nil { + return nil, err + } + if err := yaml.Unmarshal(spec.Params.Raw, m.anomalyModel); err != nil { + return nil, err + } + m.setClass(spec.Class) + return &m, nil +} + +// MarshalYAML implements yaml.Marshaller interface +func (m *Model) MarshalYAML() (any, error) { + return m.anomalyModel, nil +} + +// UnmarshalYAML implements yaml.Unmarshaler interface +func (m *Model) UnmarshalYAML(unmarshal func(any) error) error { + var h header + if err := unmarshal(&h); err != nil { + return err + } + if err := m.init(h.Class); err != nil { + return err + } + if err := unmarshal(m.anomalyModel); err != nil { return err } - m.anomalyModel = mdl + return nil +} + +type onlineModel struct { + Decay float64 `yaml:"decay,omitempty"` +} + +func (m *onlineModel) validate() error { + // See https://docs.victoriametrics.com/anomaly-detection/components/models/#decay + // Valid values are in the range [0, 1]. + if m.Decay < 0 || m.Decay > 1 { + return fmt.Errorf("decay must be in range [0, 1], got %f", m.Decay) + } return nil } @@ -200,6 +238,7 @@ type onlineQuantileModel struct { SeasonStartsFrom time.Time `yaml:"season_starts_from,omitempty"` MinSamplesSeen int `yaml:"min_n_samples_seen,omitempty"` Compression int `yaml:"compression,omitempty"` + IqrThreshold float64 `yaml:"iqr_threshold,omitempty"` } func (m *onlineQuantileModel) validate() error { @@ -271,3 +310,29 @@ type stdModel struct { func (m *stdModel) validate() error { return nil } + +func selectModels(ctx context.Context, rclient client.Client, cr *vmv1.VMAnomaly) (*build.ChildObjects[*vmv1.VMAnomalyModel], error) { + var selectedConfigs []*vmv1.VMAnomalyModel + var nsn []string + opts := &k8stools.SelectorOpts{ + DefaultNamespace: cr.Namespace, + } + if cr.Spec.ModelSelector != nil { + opts.ObjectSelector = cr.Spec.ModelSelector.ObjectSelector + opts.NamespaceSelector = cr.Spec.ModelSelector.NamespaceSelector + } + if err := k8stools.VisitSelected(ctx, rclient, opts, func(list *vmv1.VMAnomalyModelList) { + for i := range list.Items { + item := &list.Items[i] + if !item.DeletionTimestamp.IsZero() { + continue + } + rclient.Scheme().Default(item) + nsn = append(nsn, fmt.Sprintf("%s/%s", item.Namespace, item.Name)) + selectedConfigs = append(selectedConfigs, item) + } + }); err != nil { + return nil, err + } + return build.NewChildObjects("vmanomalymodels", selectedConfigs, nsn), nil +} diff --git a/internal/controller/operator/factory/vmanomaly/config/schedulers.go b/internal/controller/operator/factory/vmanomaly/config/schedulers.go index fab5b32fb..76f242bfc 100644 --- a/internal/controller/operator/factory/vmanomaly/config/schedulers.go +++ b/internal/controller/operator/factory/vmanomaly/config/schedulers.go @@ -1,29 +1,58 @@ package config import ( + "context" "fmt" "time" "gopkg.in/yaml.v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/build" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools" ) -type scheduler struct { +type anomalyScheduler interface { validatable + setClass(string) +} + +type Scheduler struct { + anomalyScheduler } var ( - _ yaml.Marshaler = (*scheduler)(nil) - _ yaml.Unmarshaler = (*scheduler)(nil) + _ yaml.Marshaler = (*Scheduler)(nil) + _ yaml.Unmarshaler = (*Scheduler)(nil) ) -// UnmarshalYAML implements yaml.Unmarshaller interface -func (s *scheduler) UnmarshalYAML(unmarshal func(interface{}) error) error { +// Validate validates raw config +func (s *Scheduler) Validate(data []byte) error { + if err := yaml.Unmarshal(data, s); err != nil { + return err + } + return s.validate() +} + +// UnmarshalYAML implements yaml.Unmarshaler interface +func (s *Scheduler) UnmarshalYAML(unmarshal func(any) error) error { var h header if err := unmarshal(&h); err != nil { return err } - var sch validatable - switch h.Class { + if err := s.init(h.Class); err != nil { + return err + } + if err := unmarshal(s.anomalyScheduler); err != nil { + return err + } + return nil +} + +func (s *Scheduler) init(class string) error { + var sch anomalyScheduler + switch class { case "scheduler.periodic.PeriodicScheduler", "periodic": sch = new(periodicScheduler) case "scheduler.oneoff.OneoffScheduler", "oneoff": @@ -31,35 +60,52 @@ func (s *scheduler) UnmarshalYAML(unmarshal func(interface{}) error) error { case "scheduler.backtesting.BacktestingScheduler", "backtesting": sch = new(backtestingScheduler) default: - return fmt.Errorf("anomaly scheduler class=%q is not supported", h.Class) - } - if err := unmarshal(sch); err != nil { - return err + return fmt.Errorf("anomaly scheduler class=%q is not supported", class) } - s.validatable = sch + s.anomalyScheduler = sch return nil } // MarshalYAML implements yaml.Marshaler interface -func (s *scheduler) MarshalYAML() (any, error) { - return s.validatable, nil +func (s *Scheduler) MarshalYAML() (any, error) { + return s.anomalyScheduler, nil } -type noopScheduler struct { +func schedulerFromSpec(spec *vmv1.VMAnomalySchedulerSpec) (*Scheduler, error) { + var s Scheduler + if err := s.init(spec.Class); err != nil { + return nil, err + } + if err := yaml.Unmarshal(spec.Params.Raw, s.anomalyScheduler); err != nil { + return nil, err + } + s.setClass(spec.Class) + return &s, nil +} + +type commonSchedulerParams struct { Class string `yaml:"class"` } +func (p *commonSchedulerParams) setClass(class string) { + p.Class = class +} + +type noopScheduler struct { + commonSchedulerParams `yaml:",inline"` +} + func (s *noopScheduler) validate() error { return nil } type periodicScheduler struct { - Class string `yaml:"class"` - FitEvery *duration `yaml:"fit_every,omitempty"` - FitWindow *duration `yaml:"fit_window"` - InferEvery *duration `yaml:"infer_every"` - StartFrom time.Time `yaml:"start_from,omitempty"` - Timezone time.Location `yaml:"tz,omitempty"` + commonSchedulerParams `yaml:",inline"` + FitEvery *duration `yaml:"fit_every,omitempty"` + FitWindow *duration `yaml:"fit_window"` + InferEvery *duration `yaml:"infer_every"` + StartFrom time.Time `yaml:"start_from,omitempty"` + Timezone time.Location `yaml:"tz,omitempty"` } func (s *periodicScheduler) validate() error { @@ -67,15 +113,15 @@ func (s *periodicScheduler) validate() error { } type oneoffScheduler struct { - Class string `yaml:"class"` - InferStartISO time.Time `yaml:"infer_start_iso,omitempty"` - InferStartS int64 `yaml:"infer_start_s,omitempty"` - InferEndISO time.Time `yaml:"infer_end_iso,omitempty"` - InferEndS int64 `yaml:"infer_end_s,omitempty"` - FitStartISO time.Time `yaml:"fit_start_iso"` - FitStartS int64 `yaml:"fit_start_s"` - FitEndISO time.Time `yaml:"fit_end_iso"` - FitEndS int64 `yaml:"fit_end_s"` + commonSchedulerParams `yaml:",inline"` + InferStartISO time.Time `yaml:"infer_start_iso,omitempty"` + InferStartS int64 `yaml:"infer_start_s,omitempty"` + InferEndISO time.Time `yaml:"infer_end_iso,omitempty"` + InferEndS int64 `yaml:"infer_end_s,omitempty"` + FitStartISO time.Time `yaml:"fit_start_iso"` + FitStartS int64 `yaml:"fit_start_s"` + FitEndISO time.Time `yaml:"fit_end_iso"` + FitEndS int64 `yaml:"fit_end_s"` } func (s *oneoffScheduler) validate() error { @@ -124,16 +170,20 @@ func (s *oneoffScheduler) validate() error { return nil } +func (s *oneoffScheduler) setClass(class string) { + s.Class = class +} + type backtestingScheduler struct { - Class string `yaml:"class"` - FitWindow *duration `yaml:"fit_window"` - FromISO time.Time `yaml:"from_iso"` - FromS int64 `yaml:"from_s"` - ToISO time.Time `yaml:"to_iso"` - ToS int64 `yaml:"to_s"` - FitEvery *duration `yaml:"fit_every"` - Jobs int `yaml:"n_jobs,omitempty"` - InferenceOnly bool `yaml:"inference_only,omitempty"` + commonSchedulerParams `yaml:",inline"` + FitWindow *duration `yaml:"fit_window"` + FromISO time.Time `yaml:"from_iso"` + FromS int64 `yaml:"from_s"` + ToISO time.Time `yaml:"to_iso"` + ToS int64 `yaml:"to_s"` + FitEvery *duration `yaml:"fit_every"` + Jobs int `yaml:"n_jobs,omitempty"` + InferenceOnly bool `yaml:"inference_only,omitempty"` } func (s *backtestingScheduler) validate() error { @@ -163,3 +213,29 @@ func (s *backtestingScheduler) validate() error { } return nil } + +func selectSchedulers(ctx context.Context, rclient client.Client, cr *vmv1.VMAnomaly) (*build.ChildObjects[*vmv1.VMAnomalyScheduler], error) { + var selectedConfigs []*vmv1.VMAnomalyScheduler + var nsn []string + opts := &k8stools.SelectorOpts{ + DefaultNamespace: cr.Namespace, + } + if cr.Spec.SchedulerSelector != nil { + opts.ObjectSelector = cr.Spec.SchedulerSelector.ObjectSelector + opts.NamespaceSelector = cr.Spec.SchedulerSelector.NamespaceSelector + } + if err := k8stools.VisitSelected(ctx, rclient, opts, func(list *vmv1.VMAnomalySchedulerList) { + for i := range list.Items { + item := &list.Items[i] + if !item.DeletionTimestamp.IsZero() { + continue + } + rclient.Scheme().Default(item) + nsn = append(nsn, fmt.Sprintf("%s/%s", item.Namespace, item.Name)) + selectedConfigs = append(selectedConfigs, item) + } + }); err != nil { + return nil, err + } + return build.NewChildObjects("vmanomalyschedulers", selectedConfigs, nsn), nil +} diff --git a/internal/controller/operator/factory/vmanomaly/pod.go b/internal/controller/operator/factory/vmanomaly/pod.go index c146be690..4f24cf87c 100644 --- a/internal/controller/operator/factory/vmanomaly/pod.go +++ b/internal/controller/operator/factory/vmanomaly/pod.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/Masterminds/semver/v3" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -18,16 +19,25 @@ import ( "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools" ) +var reloadMinVersion = semver.MustParse("v1.25.0") + const ( - secretConfigKey = "vmanomaly.yaml" - anomalyDir = "/etc/vmanomaly" - confDir = anomalyDir + "/config" - confFile = confDir + "/vmanomaly.yaml" - tlsAssetsDir = anomalyDir + "/tls" - storageDir = "/storage" - configVolumeName = "config-volume" + anomalyDir = "/etc/vmanomaly" + confDir = anomalyDir + "/config" + tlsAssetsDir = anomalyDir + "/tls" + storageDir = "/storage" + configVolumeName = "config" + configEnvsubstFilename = "vmanomaly.env.yaml" ) +func reloadSupported(cr *vmv1.VMAnomaly) bool { + anomalyVersion, err := semver.NewVersion(cr.Spec.Image.Tag) + if err == nil { + return anomalyVersion.GreaterThanEqual(reloadMinVersion) + } + return false +} + func newPodSpec(cr *vmv1.VMAnomaly, ac *build.AssetsCache) (*corev1.PodSpec, error) { image := fmt.Sprintf("%s:%s", cr.Spec.Image.Repository, cr.Spec.Image.Tag) @@ -167,9 +177,11 @@ func newPodSpec(cr *vmv1.VMAnomaly, ac *build.AssetsCache) (*corev1.PodSpec, err } } } - // vmanomaly accepts configuration file as a last element of args - args = append(args, confFile) + if reloadSupported(cr) { + args = append(args, "--watch") + } + args = append(args, path.Join(confDir, configEnvsubstFilename)) container := corev1.Container{ Args: args, diff --git a/internal/controller/operator/factory/vmanomaly/statefulset.go b/internal/controller/operator/factory/vmanomaly/statefulset.go index 4ec9b7cdc..eb3b7a1b7 100644 --- a/internal/controller/operator/factory/vmanomaly/statefulset.go +++ b/internal/controller/operator/factory/vmanomaly/statefulset.go @@ -53,14 +53,8 @@ func CreateOrUpdate(ctx context.Context, cr *vmv1.VMAnomaly, rclient client.Clie } } - rcfg := map[build.ResourceKind]*build.ResourceCfg{ - build.TLSAssetsResourceKind: { - MountDir: tlsAssetsDir, - SecretName: build.ResourceName(build.TLSAssetsResourceKind, cr), - }, - } - ac := build.NewAssetsCache(ctx, rclient, rcfg) - configHash, err := createOrUpdateConfig(ctx, rclient, cr, prevCR, ac) + ac := getAssetsCache(ctx, rclient, cr) + configHash, err := createOrUpdateConfig(ctx, rclient, cr, prevCR, nil, ac) if err != nil { return err } @@ -117,7 +111,7 @@ func newK8sApp(cr *vmv1.VMAnomaly, configHash string, ac *build.AssetsCache) (*a cfg := config.MustGetBaseConfig() useStrictSecurity := ptr.Deref(cr.Spec.UseStrictSecurity, cfg.EnableStrictSecurity) podAnnotations := cr.PodAnnotations() - if len(configHash) > 0 { + if len(configHash) > 0 && !reloadSupported(cr) { podAnnotations = labels.Merge(podAnnotations, map[string]string{ "checksum/config": configHash, }) @@ -284,3 +278,13 @@ func getShard(cr *vmv1.VMAnomaly, appTpl *appsv1.StatefulSet, num int) (*appsv1. patchShardContainers(app.Spec.Template.Spec.Containers, num, cr.GetShardCount()) return app, nil } + +func getAssetsCache(ctx context.Context, rclient client.Client, cr *vmv1.VMAnomaly) *build.AssetsCache { + cfg := map[build.ResourceKind]*build.ResourceCfg{ + build.TLSAssetsResourceKind: { + MountDir: tlsAssetsDir, + SecretName: build.ResourceName(build.TLSAssetsResourceKind, cr), + }, + } + return build.NewAssetsCache(ctx, rclient, cfg) +} diff --git a/internal/controller/operator/factory/vmanomaly/statefulset_test.go b/internal/controller/operator/factory/vmanomaly/statefulset_test.go index e8051cc79..a27431bf8 100644 --- a/internal/controller/operator/factory/vmanomaly/statefulset_test.go +++ b/internal/controller/operator/factory/vmanomaly/statefulset_test.go @@ -195,23 +195,12 @@ schedulers: }, }, validate: func(set *appsv1.StatefulSet) error { - if len(set.Spec.Template.Spec.Containers) != 1 { - return fmt.Errorf("unexpected count of container, got: %d, want: %d", len(set.Spec.Template.Spec.Containers), 2) - } + assert.Len(t, set.Spec.Template.Spec.Containers, 1) container := set.Spec.Template.Spec.Containers[0] - if container.Name != "vmanomaly" { - return fmt.Errorf("unexpected container name, got: %s, want: %s", container.Name, "vmanomaly") - } - if container.LivenessProbe.TimeoutSeconds != 20 { - return fmt.Errorf("unexpected liveness probe config, want timeout: %d, got: %d", container.LivenessProbe.TimeoutSeconds, 20) - } - if container.LivenessProbe.HTTPGet.Path != "/health" { - return fmt.Errorf("unexpected path for probe, got: %s, want: %s", container.LivenessProbe.HTTPGet.Path, "/health") - } - if container.ReadinessProbe.HTTPGet.Path != "/health" { - return fmt.Errorf("unexpected path for probe, got: %s, want: %s", container.ReadinessProbe.HTTPGet.Path, "/health") - } - + assert.Equal(t, container.Name, "vmanomaly") + assert.Equal(t, container.LivenessProbe.TimeoutSeconds, int32(20)) + assert.Equal(t, container.LivenessProbe.HTTPGet.Path, "/health") + assert.Equal(t, container.ReadinessProbe.HTTPGet.Path, "/health") return nil }, }) @@ -235,7 +224,7 @@ func Test_createDefaultConfig(t *testing.T) { } ctx := context.TODO() ac := build.NewAssetsCache(ctx, fclient, cfg) - if _, err := createOrUpdateConfig(ctx, fclient, o.cr, nil, ac); (err != nil) != o.wantErr { + if _, err := createOrUpdateConfig(ctx, fclient, o.cr, nil, nil, ac); (err != nil) != o.wantErr { t.Fatalf("createOrUpdateConfig() error = %v, wantErr %v", err, o.wantErr) } if o.wantErr { diff --git a/internal/controller/operator/objects_stat.go b/internal/controller/operator/objects_stat.go index 712206011..f35ef262c 100644 --- a/internal/controller/operator/objects_stat.go +++ b/internal/controller/operator/objects_stat.go @@ -48,8 +48,8 @@ func newCollector() *objectCollector { registeredObjects := []string{ "vmagent", "vmalert", "vmsingle", "vmcluster", "vmalertmanager", "vmauth", "vlogs", "vlsingle", "vlcluster", "vmalertmanagerconfig", "vmrule", "vmuser", "vmservicescrape", "vmstaticscrape", - "vmnodescrape", "vmpodscrape", "vmprobescrape", "vmscrapeconfig", "vmanomaly", "vlagent", - "vtsingle", "vtcluster", + "vmnodescrape", "vmpodscrape", "vmprobescrape", "vmscrapeconfig", "vmanomaly", "vmanomalymodel", + "vmanomalyscheduler", "vlagent", "vtsingle", "vtcluster", } for _, controller := range registeredObjects { oc.objectsByController[controller] = map[string]struct{}{} diff --git a/internal/controller/operator/vmanomaly_controller.go b/internal/controller/operator/vmanomaly_controller.go index ca7002a06..0e9a5d26d 100644 --- a/internal/controller/operator/vmanomaly_controller.go +++ b/internal/controller/operator/vmanomaly_controller.go @@ -18,6 +18,7 @@ package operator import ( "context" + "sync" "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" @@ -29,10 +30,16 @@ import ( vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" "github.com/VictoriaMetrics/operator/internal/config" "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/finalize" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/limiter" "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/logger" "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/vmanomaly" ) +var ( + anomalySync sync.Mutex + anomalyReconcileLimit = limiter.NewRateLimiter("vmanomaly", 5) +) + // VMAnomalyReconciler reconciles a VMAnomaly object type VMAnomalyReconciler struct { client.Client @@ -72,6 +79,11 @@ func (r *VMAnomalyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return result, &getError{origin: err, controller: "vmanomaly", requestObject: req} } + if !instance.IsUnmanaged() { + anomalySync.Lock() + defer anomalySync.Unlock() + } + RegisterObjectStat(instance, "vmanomaly") if !instance.DeletionTimestamp.IsZero() { if err := finalize.OnVMAnomalyDelete(ctx, r.Client, instance); err != nil { diff --git a/internal/controller/operator/vmanomalymodel_controller.go b/internal/controller/operator/vmanomalymodel_controller.go new file mode 100644 index 000000000..ef5a135d5 --- /dev/null +++ b/internal/controller/operator/vmanomalymodel_controller.go @@ -0,0 +1,133 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package operator + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + "github.com/VictoriaMetrics/operator/internal/config" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/logger" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/vmanomaly" +) + +// VMAnomalyModelReconciler reconciles a VMAnomalyModel object +type VMAnomalyModelReconciler struct { + client.Client + Log logr.Logger + OriginScheme *runtime.Scheme + BaseConf *config.BaseOperatorConf +} + +// Init implements crdController interface +func (r *VMAnomalyModelReconciler) Init(rclient client.Client, l logr.Logger, sc *runtime.Scheme, cf *config.BaseOperatorConf) { + r.Client = rclient + r.Log = l.WithName("controller.VMAnomalyModel") + r.OriginScheme = sc + r.BaseConf = cf +} + +// Scheme implements interface. +func (r *VMAnomalyModelReconciler) Scheme() *runtime.Scheme { + return r.OriginScheme +} + +// Reconcile general reconcile method for controller +// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmanomalymodels,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmanomalymodels/status,verbs=get;update;patch +func (r *VMAnomalyModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + instance := &vmv1.VMAnomalyModel{} + l := r.Log.WithValues("vmanomalymodel", req.Name, "namespace", req.Namespace) + ctx = logger.AddToContext(ctx, l) + defer func() { + result, err = handleReconcileErrWithoutStatus(ctx, r.Client, instance, result, err) + }() + + // Fetch the VMAnomalyModel instance + if err := r.Get(ctx, req.NamespacedName, instance); err != nil { + return result, &getError{err, "vmanomalymodel", req} + } + + RegisterObjectStat(instance, "vmanomalymodel") + if instance.Spec.ParsingError != "" { + return result, &parsingError{instance.Spec.ParsingError, "vmanomalymodel"} + } + + if anomalyReconcileLimit.MustThrottleReconcile() { + // fast path, rate limited + return + } + + anomalySync.Lock() + defer anomalySync.Unlock() + var objects vmv1.VMAnomalyList + if err := k8stools.ListObjectsByNamespace(ctx, r.Client, r.BaseConf.WatchNamespaces, func(dst *vmv1.VMAnomalyList) { + objects.Items = append(objects.Items, dst.Items...) + }); err != nil { + return result, fmt.Errorf("cannot list vmanomalies for vmanomalymodel: %w", err) + } + + for i := range objects.Items { + item := &objects.Items[i] + if !item.DeletionTimestamp.IsZero() || item.Spec.ParsingError != "" || item.Spec.ModelSelector.IsUnmanaged() { + continue + } + l := l.WithValues("vmanomaly", item.Name, "parent_namespace", item.Namespace) + ctx := logger.AddToContext(ctx, l) + // only check selector when deleting object, + // since labels can be changed when updating and we can't tell if it was selected before, and we can't tell if it's creating or updating. + if !instance.DeletionTimestamp.IsZero() { + opts := &k8stools.SelectorOpts{ + DefaultNamespace: instance.Namespace, + } + if item.Spec.ModelSelector != nil { + opts.ObjectSelector = item.Spec.ModelSelector.ObjectSelector + opts.NamespaceSelector = item.Spec.ModelSelector.NamespaceSelector + } + match, err := isSelectorsMatchesTargetCRD(ctx, r.Client, instance, item, opts) + if err != nil { + l.Error(err, "cannot match vmanomaly and vmanomalymodel") + continue + } + if !match { + continue + } + } + + if err := vmanomaly.CreateOrUpdateConfig(ctx, r, item, instance); err != nil { + continue + } + } + return +} + +// SetupWithManager general setup method +func (r *VMAnomalyModelReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&vmv1.VMAnomalyModel{}). + WithEventFilter(predicate.TypedGenerationChangedPredicate[client.Object]{}). + WithOptions(getDefaultOptions()). + Complete(r) +} diff --git a/internal/controller/operator/vmanomalyscheduler_controller.go b/internal/controller/operator/vmanomalyscheduler_controller.go new file mode 100644 index 000000000..518dbf2c6 --- /dev/null +++ b/internal/controller/operator/vmanomalyscheduler_controller.go @@ -0,0 +1,133 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package operator + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + "github.com/VictoriaMetrics/operator/internal/config" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/logger" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/vmanomaly" +) + +// VMAnomalySchedulerReconciler reconciles a VMAnomalyScheduler object +type VMAnomalySchedulerReconciler struct { + client.Client + Log logr.Logger + OriginScheme *runtime.Scheme + BaseConf *config.BaseOperatorConf +} + +// Init implements crdController interface +func (r *VMAnomalySchedulerReconciler) Init(rclient client.Client, l logr.Logger, sc *runtime.Scheme, cf *config.BaseOperatorConf) { + r.Client = rclient + r.Log = l.WithName("controller.VMAnomalyScheduler") + r.OriginScheme = sc + r.BaseConf = cf +} + +// Scheme implements interface. +func (r *VMAnomalySchedulerReconciler) Scheme() *runtime.Scheme { + return r.OriginScheme +} + +// Reconcile general reconcile method for controller +// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmanomalyschedulers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmanomalyschedulers/status,verbs=get;update;patch +func (r *VMAnomalySchedulerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + instance := &vmv1.VMAnomalyScheduler{} + l := r.Log.WithValues("vmanomalyscheduler", req.Name, "namespace", req.Namespace) + ctx = logger.AddToContext(ctx, l) + defer func() { + result, err = handleReconcileErrWithoutStatus(ctx, r.Client, instance, result, err) + }() + + // Fetch the VMAnomalyScheduler instance + if err := r.Get(ctx, req.NamespacedName, instance); err != nil { + return result, &getError{err, "vmanomalyscheduler", req} + } + + RegisterObjectStat(instance, "vmanomalyscheduler") + if instance.Spec.ParsingError != "" { + return result, &parsingError{instance.Spec.ParsingError, "vmanomalyscheduler"} + } + + if anomalyReconcileLimit.MustThrottleReconcile() { + // fast path, rate limited + return + } + + anomalySync.Lock() + defer anomalySync.Unlock() + var objects vmv1.VMAnomalyList + if err := k8stools.ListObjectsByNamespace(ctx, r.Client, r.BaseConf.WatchNamespaces, func(dst *vmv1.VMAnomalyList) { + objects.Items = append(objects.Items, dst.Items...) + }); err != nil { + return result, fmt.Errorf("cannot list vmanomalies for vmanomalyscheduler: %w", err) + } + + for i := range objects.Items { + item := &objects.Items[i] + if !item.DeletionTimestamp.IsZero() || item.Spec.ParsingError != "" || item.Spec.SchedulerSelector.IsUnmanaged() { + continue + } + l := l.WithValues("vmanomaly", item.Name, "parent_namespace", item.Namespace) + ctx := logger.AddToContext(ctx, l) + // only check selector when deleting object, + // since labels can be changed when updating and we can't tell if it was selected before, and we can't tell if it's creating or updating. + if !instance.DeletionTimestamp.IsZero() { + opts := &k8stools.SelectorOpts{ + DefaultNamespace: instance.Namespace, + } + if item.Spec.SchedulerSelector != nil { + opts.ObjectSelector = item.Spec.SchedulerSelector.ObjectSelector + opts.NamespaceSelector = item.Spec.SchedulerSelector.NamespaceSelector + } + match, err := isSelectorsMatchesTargetCRD(ctx, r.Client, instance, item, opts) + if err != nil { + l.Error(err, "cannot match vmanomaly and vmanomalyscheduler") + continue + } + if !match { + continue + } + } + + if err := vmanomaly.CreateOrUpdateConfig(ctx, r, item, instance); err != nil { + continue + } + } + return +} + +// SetupWithManager general setup method +func (r *VMAnomalySchedulerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&vmv1.VMAnomalyScheduler{}). + WithEventFilter(predicate.TypedGenerationChangedPredicate[client.Object]{}). + WithOptions(getDefaultOptions()). + Complete(r) +} diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 820260fe1..4b83a3555 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -367,6 +367,8 @@ func addWebhooks(mgr ctrl.Manager) error { webhookv1beta1.SetupVMAgentWebhookWithManager, webhookv1beta1.SetupVMAlertWebhookWithManager, webhookv1.SetupVMAnomalyWebhookWithManager, + webhookv1.SetupVMAnomalyModelWebhookWithManager, + webhookv1.SetupVMAnomalySchedulerWebhookWithManager, webhookv1beta1.SetupVMSingleWebhookWithManager, webhookv1beta1.SetupVMClusterWebhookWithManager, webhookv1beta1.SetupVLogsWebhookWithManager, @@ -469,6 +471,8 @@ var controllersByName = map[string]crdController{ "VMCluster": &vmcontroller.VMClusterReconciler{}, "VMAgent": &vmcontroller.VMAgentReconciler{}, "VMAnomaly": &vmcontroller.VMAnomalyReconciler{}, + "VMAnomalyModel": &vmcontroller.VMAnomalyModelReconciler{}, + "VMAnomalyScheduler": &vmcontroller.VMAnomalySchedulerReconciler{}, "VMAuth": &vmcontroller.VMAuthReconciler{}, "VMSingle": &vmcontroller.VMSingleReconciler{}, "VLAgent": &vmcontroller.VLAgentReconciler{}, diff --git a/internal/webhook/operator/v1/vmanomalymodel_webhook.go b/internal/webhook/operator/v1/vmanomalymodel_webhook.go new file mode 100644 index 000000000..8e498d7c0 --- /dev/null +++ b/internal/webhook/operator/v1/vmanomalymodel_webhook.go @@ -0,0 +1,80 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/vmanomaly/config" +) + +// SetupVMAnomalyModelWebhookWithManager will setup the manager to manage the webhooks +func SetupVMAnomalyModelWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&vmv1.VMAnomalyModel{}). + WithValidator(&VMAnomalyModelCustomValidator{}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-operator-victoriametrics-com-v1-vmanomalymodel,mutating=false,failurePolicy=fail,sideEffects=None,groups=operator.victoriametrics.com,resources=vmanomalymodels,verbs=create;update,versions=v1,name=vvmanomalymodel-v1.kb.io,admissionReviewVersions=v1 +type VMAnomalyModelCustomValidator struct{} + +var _ admission.CustomValidator = &VMAnomalyModelCustomValidator{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (*VMAnomalyModelCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*vmv1.VMAnomalyModel) + if !ok { + return nil, fmt.Errorf("BUG: unexpected type: %T", obj) + } + if r.Spec.ParsingError != "" { + return nil, errors.New(r.Spec.ParsingError) + } + var m config.Model + if err := m.Validate(r.Spec.Params.Raw); err != nil { + return nil, err + } + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (*VMAnomalyModelCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + r, ok := newObj.(*vmv1.VMAnomalyModel) + if !ok { + return nil, fmt.Errorf("BUG: unexpected type: %T", newObj) + } + if r.Spec.ParsingError != "" { + return nil, errors.New(r.Spec.ParsingError) + } + var m config.Model + if err := m.Validate(r.Spec.Params.Raw); err != nil { + return nil, err + } + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (*VMAnomalyModelCustomValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/internal/webhook/operator/v1/vmanomalyscheduler_webhook.go b/internal/webhook/operator/v1/vmanomalyscheduler_webhook.go new file mode 100644 index 000000000..c67bba85a --- /dev/null +++ b/internal/webhook/operator/v1/vmanomalyscheduler_webhook.go @@ -0,0 +1,80 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/vmanomaly/config" +) + +// SetupVMAnomalySchedulerWebhookWithManager will setup the manager to manage the webhooks +func SetupVMAnomalySchedulerWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&vmv1.VMAnomalyScheduler{}). + WithValidator(&VMAnomalySchedulerCustomValidator{}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-operator-victoriametrics-com-v1-vmanomalyscheduler,mutating=false,failurePolicy=fail,sideEffects=None,groups=operator.victoriametrics.com,resources=vmanomalyschedulers,verbs=create;update,versions=v1,name=vvmanomalyscheduler-v1.kb.io,admissionReviewVersions=v1 +type VMAnomalySchedulerCustomValidator struct{} + +var _ admission.CustomValidator = &VMAnomalySchedulerCustomValidator{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (*VMAnomalySchedulerCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*vmv1.VMAnomalyScheduler) + if !ok { + return nil, fmt.Errorf("BUG: unexpected type: %T", obj) + } + if r.Spec.ParsingError != "" { + return nil, errors.New(r.Spec.ParsingError) + } + var s config.Scheduler + if err := s.Validate(r.Spec.Params.Raw); err != nil { + return nil, err + } + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (*VMAnomalySchedulerCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + r, ok := newObj.(*vmv1.VMAnomalyScheduler) + if !ok { + return nil, fmt.Errorf("BUG: unexpected type: %T", newObj) + } + if r.Spec.ParsingError != "" { + return nil, errors.New(r.Spec.ParsingError) + } + var s config.Scheduler + if err := s.Validate(r.Spec.Params.Raw); err != nil { + return nil, err + } + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (*VMAnomalySchedulerCustomValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +}