From f0b9e9be0c840d43a6821341aa734799d6e19d42 Mon Sep 17 00:00:00 2001 From: chenlujjj <953546398@qq.com> Date: Fri, 10 Jan 2025 14:57:35 +0800 Subject: [PATCH] feat(controller): add support for mute timing --- api/v1beta1/grafanamutetiming_types.go | 119 +++++ api/v1beta1/grafanamutetiming_types_test.go | 86 ++++ api/v1beta1/zz_generated.deepcopy.go | 152 +++++++ ...na.integreatly.org_grafanamutetimings.yaml | 239 ++++++++++ config/crd/kustomization.yaml | 1 + config/rbac/role.yaml | 3 + controllers/grafanamutetiming_controller.go | 252 +++++++++++ ...na.integreatly.org_grafanamutetimings.yaml | 239 ++++++++++ deploy/helm/grafana-operator/files/rbac.yaml | 3 + deploy/kustomize/base/crds.yaml | 239 ++++++++++ deploy/kustomize/base/role.yaml | 3 + docs/docs/api.md | 419 ++++++++++++++++++ examples/mute_timing/README.md | 8 + examples/mute_timing/resources.yaml | 18 + main.go | 7 + tests/e2e/example-test/13-assert.yaml | 8 + tests/e2e/example-test/13-mute-timing.yaml | 18 + tests/e2e/example-test/chainsaw-test.yaml | 6 + 18 files changed, 1820 insertions(+) create mode 100644 api/v1beta1/grafanamutetiming_types.go create mode 100644 api/v1beta1/grafanamutetiming_types_test.go create mode 100644 config/crd/bases/grafana.integreatly.org_grafanamutetimings.yaml create mode 100644 controllers/grafanamutetiming_controller.go create mode 100644 deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanamutetimings.yaml create mode 100644 examples/mute_timing/README.md create mode 100644 examples/mute_timing/resources.yaml create mode 100644 tests/e2e/example-test/13-assert.yaml create mode 100644 tests/e2e/example-test/13-mute-timing.yaml diff --git a/api/v1beta1/grafanamutetiming_types.go b/api/v1beta1/grafanamutetiming_types.go new file mode 100644 index 000000000..2b5a22e5f --- /dev/null +++ b/api/v1beta1/grafanamutetiming_types.go @@ -0,0 +1,119 @@ +/* +Copyright 2022. + +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 v1beta1 + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GrafanaMuteTimingSpec defines the desired state of GrafanaMuteTiming +// +kubebuilder:validation:XValidation:rule="((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) && has(self.editable)))", message="spec.editable is immutable" +type GrafanaMuteTimingSpec struct { + GrafanaCommonSpec `json:",inline"` + + // A unique name for the mute timing + Name string `json:"name"` + + // Time intervals for muting + // +kubebuilder:validation:MinItems=1 + TimeIntervals []*TimeInterval `json:"time_intervals"` + + // Whether to enable or disable editing of the mute timing in Grafana UI + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.editable is immutable" + // +optional + Editable *bool `json:"editable,omitempty"` +} + +type TimeInterval struct { + // days of month + // +optional + DaysOfMonth []string `json:"days_of_month,omitempty"` + + // location + // +optional + Location string `json:"location,omitempty"` + + // months + // +optional + Months []string `json:"months,omitempty"` + + // times + // +optional + Times []*TimeRange `json:"times,omitempty"` + + // weekdays + // +optional + Weekdays []string `json:"weekdays,omitempty"` + + // years + // +optional + Years []string `json:"years,omitempty"` +} + +type TimeRange struct { + // start time + StartTime string `json:"start_time"` + + // end time + EndTime string `json:"end_time"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// GrafanaMuteTiming is the Schema for the GrafanaMuteTiming API +// +kubebuilder:resource:categories={grafana-operator} +type GrafanaMuteTiming struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GrafanaMuteTimingSpec `json:"spec,omitempty"` + Status GrafanaCommonStatus `json:"status,omitempty"` +} + +var _ CommonResource = (*GrafanaMuteTiming)(nil) + +func (in *GrafanaMuteTiming) MatchLabels() *metav1.LabelSelector { + return in.Spec.InstanceSelector +} + +func (in *GrafanaMuteTiming) MatchNamespace() string { + return in.ObjectMeta.Namespace +} + +func (in *GrafanaMuteTiming) AllowCrossNamespace() bool { + return in.Spec.AllowCrossNamespaceImport +} + +func (np *GrafanaMuteTiming) NamespacedResource() string { + return fmt.Sprintf("%v/%v/%v", np.ObjectMeta.Namespace, np.ObjectMeta.Name, np.ObjectMeta.UID) +} + +//+kubebuilder:object:root=true + +// GrafanaMuteTimingList contains a list of GrafanaMuteTiming +type GrafanaMuteTimingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GrafanaMuteTiming `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GrafanaMuteTiming{}, &GrafanaMuteTimingList{}) +} diff --git a/api/v1beta1/grafanamutetiming_types_test.go b/api/v1beta1/grafanamutetiming_types_test.go new file mode 100644 index 000000000..9ac793402 --- /dev/null +++ b/api/v1beta1/grafanamutetiming_types_test.go @@ -0,0 +1,86 @@ +package v1beta1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func newMuteTiming(name string, editable *bool) *GrafanaMuteTiming { + return &GrafanaMuteTiming{ + TypeMeta: v1.TypeMeta{ + APIVersion: APIVersion, + Kind: "GrafanaMuteTiming", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: GrafanaMuteTimingSpec{ + Editable: editable, + GrafanaCommonSpec: GrafanaCommonSpec{ + InstanceSelector: &v1.LabelSelector{ + MatchLabels: map[string]string{ + "test": "mutetiming", + }, + }, + }, + Name: name, + TimeIntervals: []*TimeInterval{ + { + DaysOfMonth: []string{"1"}, + Location: "Asia/Shanghai", + Months: []string{"1"}, + Times: []*TimeRange{ + { + StartTime: "00:00", + EndTime: "02:00", + }, + }, + Weekdays: []string{"1"}, + Years: []string{"2025"}, + }, + }, + }, + } +} + +var _ = Describe("MuteTiming type", func() { + Context("Ensure MuteTiming spec.editable is immutable", func() { + ctx := context.Background() + refTrue := true + refFalse := false + + It("Should block adding editable field when missing", func() { + mutetiming := newMuteTiming("missing-editable", nil) + By("Create new MuteTiming without editable") + Expect(k8sClient.Create(ctx, mutetiming)).To(Succeed()) + + By("Adding a editable") + mutetiming.Spec.Editable = &refTrue + Expect(k8sClient.Update(ctx, mutetiming)).To(HaveOccurred()) + }) + + It("Should block removing editable field when set", func() { + mutetiming := newMuteTiming("existing-editable", &refTrue) + By("Creating MuteTiming with existing editable") + Expect(k8sClient.Create(ctx, mutetiming)).To(Succeed()) + + By("And setting editable to ''") + mutetiming.Spec.Editable = nil + Expect(k8sClient.Update(ctx, mutetiming)).To(HaveOccurred()) + }) + + It("Should block changing value of editable", func() { + mutetiming := newMuteTiming("removing-editable", &refTrue) + By("Create new MuteTiming with existing editable") + Expect(k8sClient.Create(ctx, mutetiming)).To(Succeed()) + + By("Changing the existing editable") + mutetiming.Spec.Editable = &refFalse + Expect(k8sClient.Update(ctx, mutetiming)).To(HaveOccurred()) + }) + }) +}) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index f661b1bbd..fc48949ff 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1215,6 +1215,97 @@ func (in *GrafanaList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaMuteTiming) DeepCopyInto(out *GrafanaMuteTiming) { + *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 GrafanaMuteTiming. +func (in *GrafanaMuteTiming) DeepCopy() *GrafanaMuteTiming { + if in == nil { + return nil + } + out := new(GrafanaMuteTiming) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GrafanaMuteTiming) 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 *GrafanaMuteTimingList) DeepCopyInto(out *GrafanaMuteTimingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GrafanaMuteTiming, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaMuteTimingList. +func (in *GrafanaMuteTimingList) DeepCopy() *GrafanaMuteTimingList { + if in == nil { + return nil + } + out := new(GrafanaMuteTimingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GrafanaMuteTimingList) 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 *GrafanaMuteTimingSpec) DeepCopyInto(out *GrafanaMuteTimingSpec) { + *out = *in + in.GrafanaCommonSpec.DeepCopyInto(&out.GrafanaCommonSpec) + if in.TimeIntervals != nil { + in, out := &in.TimeIntervals, &out.TimeIntervals + *out = make([]*TimeInterval, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(TimeInterval) + (*in).DeepCopyInto(*out) + } + } + } + if in.Editable != nil { + in, out := &in.Editable, &out.Editable + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaMuteTimingSpec. +func (in *GrafanaMuteTimingSpec) DeepCopy() *GrafanaMuteTimingSpec { + if in == nil { + return nil + } + out := new(GrafanaMuteTimingSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaNotificationPolicy) DeepCopyInto(out *GrafanaNotificationPolicy) { *out = *in @@ -2037,6 +2128,67 @@ func (in *TLSConfig) DeepCopy() *TLSConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TimeInterval) DeepCopyInto(out *TimeInterval) { + *out = *in + if in.DaysOfMonth != nil { + in, out := &in.DaysOfMonth, &out.DaysOfMonth + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Months != nil { + in, out := &in.Months, &out.Months + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Times != nil { + in, out := &in.Times, &out.Times + *out = make([]*TimeRange, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(TimeRange) + **out = **in + } + } + } + if in.Weekdays != nil { + in, out := &in.Weekdays, &out.Weekdays + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Years != nil { + in, out := &in.Years, &out.Years + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeInterval. +func (in *TimeInterval) DeepCopy() *TimeInterval { + if in == nil { + return nil + } + out := new(TimeInterval) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TimeRange) DeepCopyInto(out *TimeRange) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeRange. +func (in *TimeRange) DeepCopy() *TimeRange { + if in == nil { + return nil + } + out := new(TimeRange) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ValueFrom) DeepCopyInto(out *ValueFrom) { *out = *in diff --git a/config/crd/bases/grafana.integreatly.org_grafanamutetimings.yaml b/config/crd/bases/grafana.integreatly.org_grafanamutetimings.yaml new file mode 100644 index 000000000..5365b3275 --- /dev/null +++ b/config/crd/bases/grafana.integreatly.org_grafanamutetimings.yaml @@ -0,0 +1,239 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: grafanamutetimings.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + categories: + - grafana-operator + kind: GrafanaMuteTiming + listKind: GrafanaMuteTimingList + plural: grafanamutetimings + singular: grafanamutetiming + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GrafanaMuteTiming is the Schema for the GrafanaMuteTiming 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: GrafanaMuteTimingSpec defines the desired state of GrafanaMuteTiming + properties: + allowCrossNamespaceImport: + default: false + description: Allow the Operator to match this resource with Grafanas + outside the current namespace + type: boolean + editable: + description: Whether to enable or disable editing of the mute timing + in Grafana UI + type: boolean + x-kubernetes-validations: + - message: spec.editable is immutable + rule: self == oldSelf + instanceSelector: + description: Selects Grafana instances for import + 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 + x-kubernetes-validations: + - message: spec.instanceSelector is immutable + rule: self == oldSelf + name: + description: A unique name for the mute timing + type: string + resyncPeriod: + default: 10m0s + description: How often the resource is synced, defaults to 10m0s if + not set + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + time_intervals: + description: Time intervals for muting + items: + properties: + days_of_month: + description: days of month + items: + type: string + type: array + location: + description: location + type: string + months: + description: months + items: + type: string + type: array + times: + description: times + items: + properties: + end_time: + description: end time + type: string + start_time: + description: start time + type: string + required: + - end_time + - start_time + type: object + type: array + weekdays: + description: weekdays + items: + type: string + type: array + years: + description: years + items: + type: string + type: array + type: object + minItems: 1 + type: array + required: + - instanceSelector + - name + - time_intervals + type: object + x-kubernetes-validations: + - message: spec.editable is immutable + rule: ((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) + && has(self.editable))) + status: + description: The most recent observed state of a Grafana resource + properties: + conditions: + description: Results when synchonizing resource with Grafana instances + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + 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 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + 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 foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastResync: + description: Last time the resource was synchronized with Grafana + instances + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 49b06ad37..518957387 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -10,6 +10,7 @@ resources: - bases/grafana.integreatly.org_grafanacontactpoints.yaml - bases/grafana.integreatly.org_grafananotificationpolicies.yaml - bases/grafana.integreatly.org_grafananotificationtemplates.yaml +- bases/grafana.integreatly.org_grafanamutetimings.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b51df1806..10f461cd9 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -62,6 +62,7 @@ rules: - grafanadashboards - grafanadatasources - grafanafolders + - grafanamutetimings - grafananotificationpolicies - grafananotificationtemplates - grafanas @@ -81,6 +82,7 @@ rules: - grafanadashboards/finalizers - grafanadatasources/finalizers - grafanafolders/finalizers + - grafanamutetimings/finalizers - grafananotificationpolicies/finalizers - grafananotificationtemplates/finalizers - grafanas/finalizers @@ -94,6 +96,7 @@ rules: - grafanadashboards/status - grafanadatasources/status - grafanafolders/status + - grafanamutetimings/status - grafananotificationpolicies/status - grafananotificationtemplates/status - grafanas/status diff --git a/controllers/grafanamutetiming_controller.go b/controllers/grafanamutetiming_controller.go new file mode 100644 index 000000000..f660ac07e --- /dev/null +++ b/controllers/grafanamutetiming_controller.go @@ -0,0 +1,252 @@ +/* +Copyright 2022. + +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 controllers + +import ( + "context" + "errors" + "fmt" + "time" + + kuberr "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/go-logr/logr" + "github.com/grafana/grafana-openapi-client-go/client/provisioning" + "github.com/grafana/grafana-openapi-client-go/models" + grafanav1beta1 "github.com/grafana/grafana-operator/v5/api/v1beta1" + client2 "github.com/grafana/grafana-operator/v5/controllers/client" +) + +const ( + conditionMuteTimingSynchronized = "MuteTimingSynchronized" +) + +// GrafanaMuteTimingReconciler reconciles a GrafanaMuteTiming object +type GrafanaMuteTimingReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanamutetimings,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanamutetimings/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanamutetimings/finalizers,verbs=update + +func (r *GrafanaMuteTimingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + controllerLog := log.FromContext(ctx).WithName("GrafanaMuteTimingReconciler") + r.Log = controllerLog + + muteTiming := &grafanav1beta1.GrafanaMuteTiming{} + err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: req.Namespace, + Name: req.Name, + }, muteTiming) + if err != nil { + if kuberr.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to get GrafanaMuteTiming: %w", err) + } + + if muteTiming.GetDeletionTimestamp() != nil { + // Check if resource needs clean up + if controllerutil.ContainsFinalizer(muteTiming, grafanaFinalizer) { + if err := r.finalize(ctx, muteTiming); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to finalize GrafanaMuteTiming: %w", err) + } + if err := removeFinalizer(ctx, r.Client, muteTiming); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err) + } + } + return ctrl.Result{}, nil + } + + defer func() { + muteTiming.Status.LastResync = metav1.Time{Time: time.Now()} + if err := r.Client.Status().Update(ctx, muteTiming); err != nil { + r.Log.Error(err, "updating status") + } + if meta.IsStatusConditionTrue(muteTiming.Status.Conditions, conditionNoMatchingInstance) { + if err := removeFinalizer(ctx, r.Client, muteTiming); err != nil { + r.Log.Error(err, "failed to remove finalizer") + } + } else { + if err := addFinalizer(ctx, r.Client, muteTiming); err != nil { + r.Log.Error(err, "failed to set finalizer") + } + } + }() + + instances, err := GetScopedMatchingInstances(controllerLog, ctx, r.Client, muteTiming) + if err != nil { + setNoMatchingInstancesCondition(&muteTiming.Status.Conditions, muteTiming.Generation, err) + meta.RemoveStatusCondition(&muteTiming.Status.Conditions, conditionMuteTimingSynchronized) + return ctrl.Result{}, fmt.Errorf("could not find matching instances: %w", err) + } + + if len(instances) == 0 { + setNoMatchingInstancesCondition(&muteTiming.Status.Conditions, muteTiming.Generation, err) + meta.RemoveStatusCondition(&muteTiming.Status.Conditions, conditionMuteTimingSynchronized) + return ctrl.Result{RequeueAfter: RequeueDelay}, nil + } + + removeNoMatchingInstance(&muteTiming.Status.Conditions) + controllerLog.Info("found matching Grafana instances for mute timing", "count", len(instances)) + + applyErrors := make(map[string]string) + for _, grafana := range instances { + // can be removed in go 1.22+ + grafana := grafana + + err := r.reconcileWithInstance(ctx, &grafana, muteTiming) + if err != nil { + applyErrors[fmt.Sprintf("%s/%s", grafana.Namespace, grafana.Name)] = err.Error() + } + } + if len(applyErrors) > 0 { + return ctrl.Result{}, fmt.Errorf("failed to apply to all instances: %v", applyErrors) + } + + condition := buildSynchronizedCondition("Mute timing", conditionMuteTimingSynchronized, muteTiming.Generation, applyErrors, len(instances)) + meta.SetStatusCondition(&muteTiming.Status.Conditions, condition) + return ctrl.Result{RequeueAfter: muteTiming.Spec.ResyncPeriod.Duration}, nil +} + +func (r *GrafanaMuteTimingReconciler) reconcileWithInstance(ctx context.Context, instance *grafanav1beta1.Grafana, muteTiming *grafanav1beta1.GrafanaMuteTiming) error { + cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) + if err != nil { + return fmt.Errorf("building grafana client: %w", err) + } + + _, err = r.getMuteTimingByName(ctx, muteTiming.Spec.Name, instance) + shouldCreate := false + if err != nil { + if errors.Is(err, provisioning.NewGetMuteTimingNotFound()) { + shouldCreate = true + } else { + return fmt.Errorf("getting mute timing by name: %w", err) + } + } + + trueRef := "true" //nolint:goconst + editable := true + if muteTiming.Spec.Editable != nil && !*muteTiming.Spec.Editable { + editable = false + } + + var payload models.MuteTimeInterval + payload.Name = muteTiming.Spec.Name + payload.TimeIntervals = make([]*models.TimeIntervalItem, 0, len(muteTiming.Spec.TimeIntervals)) + for _, ti := range muteTiming.Spec.TimeIntervals { + times := make([]*models.TimeIntervalTimeRange, 0, len(ti.Times)) + for _, tr := range ti.Times { + times = append(times, &models.TimeIntervalTimeRange{ + StartTime: tr.StartTime, + EndTime: tr.EndTime, + }) + } + payload.TimeIntervals = append(payload.TimeIntervals, &models.TimeIntervalItem{ + DaysOfMonth: ti.DaysOfMonth, + Location: ti.Location, + Months: ti.Months, + Weekdays: ti.Weekdays, + Years: ti.Years, + Times: times, + }) + } + if shouldCreate { + params := provisioning.NewPostMuteTimingParams().WithBody(&payload) + if editable { + params.SetXDisableProvenance(&trueRef) + } + _, err = cl.Provisioning.PostMuteTiming(params) //nolint:errcheck + if err != nil { + return fmt.Errorf("creating mute timing: %w", err) + } + } else { + params := provisioning.NewPutMuteTimingParams().WithName(muteTiming.Spec.Name).WithBody(&payload) + if editable { + params.SetXDisableProvenance(&trueRef) + } + _, err = cl.Provisioning.PutMuteTiming(params) //nolint:errcheck + if err != nil { + return fmt.Errorf("updating mute timing: %w", err) + } + } + + return nil +} + +func (r *GrafanaMuteTimingReconciler) getMuteTimingByName(ctx context.Context, name string, instance *grafanav1beta1.Grafana) (*models.MuteTimeInterval, error) { + cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) + if err != nil { + return nil, fmt.Errorf("building grafana client: %w", err) + } + + muteTiming, err := cl.Provisioning.GetMuteTiming(name) + if err != nil { + return nil, fmt.Errorf("getting mute timing: %w", err) + } + + return muteTiming.Payload, nil +} + +func (r *GrafanaMuteTimingReconciler) finalize(ctx context.Context, muteTiming *grafanav1beta1.GrafanaMuteTiming) error { + r.Log.Info("Finalizing GrafanaMuteTiming") + + instances, err := GetAllMatchingInstances(ctx, r.Client, muteTiming) + if err != nil { + return fmt.Errorf("fetching instances: %w", err) + } + for _, i := range instances { + instance := i + if err := r.removeFromInstance(ctx, &instance, muteTiming); err != nil { + return fmt.Errorf("removing mute timing from instance: %w", err) + } + } + + return nil +} + +func (r *GrafanaMuteTimingReconciler) removeFromInstance(ctx context.Context, instance *grafanav1beta1.Grafana, muteTiming *grafanav1beta1.GrafanaMuteTiming) error { + cl, err := client2.NewGeneratedGrafanaClient(ctx, r.Client, instance) + if err != nil { + return fmt.Errorf("building grafana client: %w", err) + } + + _, err = cl.Provisioning.DeleteMuteTiming(muteTiming.Spec.Name) //nolint:errcheck + if err != nil { + return fmt.Errorf("deleting mute timing: %w", err) + } + + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GrafanaMuteTimingReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&grafanav1beta1.GrafanaMuteTiming{}). + WithEventFilter(ignoreStatusUpdates()). + Complete(r) +} diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanamutetimings.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanamutetimings.yaml new file mode 100644 index 000000000..5365b3275 --- /dev/null +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanamutetimings.yaml @@ -0,0 +1,239 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: grafanamutetimings.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + categories: + - grafana-operator + kind: GrafanaMuteTiming + listKind: GrafanaMuteTimingList + plural: grafanamutetimings + singular: grafanamutetiming + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GrafanaMuteTiming is the Schema for the GrafanaMuteTiming 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: GrafanaMuteTimingSpec defines the desired state of GrafanaMuteTiming + properties: + allowCrossNamespaceImport: + default: false + description: Allow the Operator to match this resource with Grafanas + outside the current namespace + type: boolean + editable: + description: Whether to enable or disable editing of the mute timing + in Grafana UI + type: boolean + x-kubernetes-validations: + - message: spec.editable is immutable + rule: self == oldSelf + instanceSelector: + description: Selects Grafana instances for import + 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 + x-kubernetes-validations: + - message: spec.instanceSelector is immutable + rule: self == oldSelf + name: + description: A unique name for the mute timing + type: string + resyncPeriod: + default: 10m0s + description: How often the resource is synced, defaults to 10m0s if + not set + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + time_intervals: + description: Time intervals for muting + items: + properties: + days_of_month: + description: days of month + items: + type: string + type: array + location: + description: location + type: string + months: + description: months + items: + type: string + type: array + times: + description: times + items: + properties: + end_time: + description: end time + type: string + start_time: + description: start time + type: string + required: + - end_time + - start_time + type: object + type: array + weekdays: + description: weekdays + items: + type: string + type: array + years: + description: years + items: + type: string + type: array + type: object + minItems: 1 + type: array + required: + - instanceSelector + - name + - time_intervals + type: object + x-kubernetes-validations: + - message: spec.editable is immutable + rule: ((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) + && has(self.editable))) + status: + description: The most recent observed state of a Grafana resource + properties: + conditions: + description: Results when synchonizing resource with Grafana instances + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + 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 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + 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 foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastResync: + description: Last time the resource was synchronized with Grafana + instances + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/helm/grafana-operator/files/rbac.yaml b/deploy/helm/grafana-operator/files/rbac.yaml index 6b01728b5..391b928aa 100644 --- a/deploy/helm/grafana-operator/files/rbac.yaml +++ b/deploy/helm/grafana-operator/files/rbac.yaml @@ -62,6 +62,7 @@ rules: - grafanadashboards - grafanadatasources - grafanafolders + - grafanamutetimings - grafananotificationpolicies - grafananotificationtemplates - grafanas @@ -81,6 +82,7 @@ rules: - grafanadashboards/finalizers - grafanadatasources/finalizers - grafanafolders/finalizers + - grafanamutetimings/finalizers - grafananotificationpolicies/finalizers - grafananotificationtemplates/finalizers - grafanas/finalizers @@ -94,6 +96,7 @@ rules: - grafanadashboards/status - grafanadatasources/status - grafanafolders/status + - grafanamutetimings/status - grafananotificationpolicies/status - grafananotificationtemplates/status - grafanas/status diff --git a/deploy/kustomize/base/crds.yaml b/deploy/kustomize/base/crds.yaml index 0bd451c9d..a02fa8220 100644 --- a/deploy/kustomize/base/crds.yaml +++ b/deploy/kustomize/base/crds.yaml @@ -1655,6 +1655,245 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: grafanamutetimings.grafana.integreatly.org +spec: + group: grafana.integreatly.org + names: + categories: + - grafana-operator + kind: GrafanaMuteTiming + listKind: GrafanaMuteTimingList + plural: grafanamutetimings + singular: grafanamutetiming + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GrafanaMuteTiming is the Schema for the GrafanaMuteTiming 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: GrafanaMuteTimingSpec defines the desired state of GrafanaMuteTiming + properties: + allowCrossNamespaceImport: + default: false + description: Allow the Operator to match this resource with Grafanas + outside the current namespace + type: boolean + editable: + description: Whether to enable or disable editing of the mute timing + in Grafana UI + type: boolean + x-kubernetes-validations: + - message: spec.editable is immutable + rule: self == oldSelf + instanceSelector: + description: Selects Grafana instances for import + 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 + x-kubernetes-validations: + - message: spec.instanceSelector is immutable + rule: self == oldSelf + name: + description: A unique name for the mute timing + type: string + resyncPeriod: + default: 10m0s + description: How often the resource is synced, defaults to 10m0s if + not set + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + time_intervals: + description: Time intervals for muting + items: + properties: + days_of_month: + description: days of month + items: + type: string + type: array + location: + description: location + type: string + months: + description: months + items: + type: string + type: array + times: + description: times + items: + properties: + end_time: + description: end time + type: string + start_time: + description: start time + type: string + required: + - end_time + - start_time + type: object + type: array + weekdays: + description: weekdays + items: + type: string + type: array + years: + description: years + items: + type: string + type: array + type: object + minItems: 1 + type: array + required: + - instanceSelector + - name + - time_intervals + type: object + x-kubernetes-validations: + - message: spec.editable is immutable + rule: ((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) + && has(self.editable))) + status: + description: The most recent observed state of a Grafana resource + properties: + conditions: + description: Results when synchonizing resource with Grafana instances + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + 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 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + 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 foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastResync: + description: Last time the resource was synchronized with Grafana + instances + format: date-time + 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.16.3 diff --git a/deploy/kustomize/base/role.yaml b/deploy/kustomize/base/role.yaml index b51df1806..10f461cd9 100644 --- a/deploy/kustomize/base/role.yaml +++ b/deploy/kustomize/base/role.yaml @@ -62,6 +62,7 @@ rules: - grafanadashboards - grafanadatasources - grafanafolders + - grafanamutetimings - grafananotificationpolicies - grafananotificationtemplates - grafanas @@ -81,6 +82,7 @@ rules: - grafanadashboards/finalizers - grafanadatasources/finalizers - grafanafolders/finalizers + - grafanamutetimings/finalizers - grafananotificationpolicies/finalizers - grafananotificationtemplates/finalizers - grafanas/finalizers @@ -94,6 +96,7 @@ rules: - grafanadashboards/status - grafanadatasources/status - grafanafolders/status + - grafanamutetimings/status - grafananotificationpolicies/status - grafananotificationtemplates/status - grafanas/status diff --git a/docs/docs/api.md b/docs/docs/api.md index eab5e70c7..1c5d5f920 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -21,6 +21,8 @@ Resource Types: - [GrafanaFolder](#grafanafolder) +- [GrafanaMuteTiming](#grafanamutetiming) + - [GrafanaNotificationPolicy](#grafananotificationpolicy) - [GrafanaNotificationTemplate](#grafananotificationtemplate) @@ -3205,6 +3207,423 @@ GrafanaFolderStatus defines the observed state of GrafanaFolder +Condition contains details for one aspect of the current state of this API Resource. + +
Name | +Type | +Description | +Required | +
---|---|---|---|
lastTransitionTime | +string | +
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + + Format: date-time + |
+ true | +
message | +string | +
+ message is a human readable message indicating details about the transition.
+This may be an empty string. + |
+ true | +
reason | +string | +
+ 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. + |
+ true | +
status | +enum | +
+ status of the condition, one of True, False, Unknown. + + Enum: True, False, Unknown + |
+ true | +
type | +string | +
+ type of condition in CamelCase or in foo.example.com/CamelCase. + |
+ true | +
observedGeneration | +integer | +
+ 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 + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
apiVersion | +string | +grafana.integreatly.org/v1beta1 | +true | +
kind | +string | +GrafanaMuteTiming | +true | +
metadata | +object | +Refer to the Kubernetes API documentation for the fields of the `metadata` field. | +true | +
spec | +object | +
+ GrafanaMuteTimingSpec defines the desired state of GrafanaMuteTiming + + Validations: |
+ false | +
status | +object | +
+ The most recent observed state of a Grafana resource + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
instanceSelector | +object | +
+ Selects Grafana instances for import + + Validations: |
+ true | +
name | +string | +
+ A unique name for the mute timing + |
+ true | +
time_intervals | +[]object | +
+ Time intervals for muting + |
+ true | +
allowCrossNamespaceImport | +boolean | +
+ Allow the Operator to match this resource with Grafanas outside the current namespace + + Default: false + |
+ false | +
editable | +boolean | +
+ Whether to enable or disable editing of the mute timing in Grafana UI + + Validations: |
+ false | +
resyncPeriod | +string | +
+ How often the resource is synced, defaults to 10m0s if not set + + Format: duration + Default: 10m0s + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
matchExpressions | +[]object | +
+ matchExpressions is a list of label selector requirements. The requirements are ANDed. + |
+ false | +
matchLabels | +map[string]string | +
+ 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. + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
key | +string | +
+ key is the label key that the selector applies to. + |
+ true | +
operator | +string | +
+ operator represents a key's relationship to a set of values.
+Valid operators are In, NotIn, Exists and DoesNotExist. + |
+ true | +
values | +[]string | +
+ 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. + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
days_of_month | +[]string | +
+ days of month + |
+ false | +
location | +string | +
+ location + |
+ false | +
months | +[]string | +
+ months + |
+ false | +
times | +[]object | +
+ times + |
+ false | +
weekdays | +[]string | +
+ weekdays + |
+ false | +
years | +[]string | +
+ years + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
end_time | +string | +
+ end time + |
+ true | +
start_time | +string | +
+ start time + |
+ true | +
Name | +Type | +Description | +Required | +
---|---|---|---|
conditions | +[]object | +
+ Results when synchonizing resource with Grafana instances + |
+ false | +
lastResync | +string | +
+ Last time the resource was synchronized with Grafana instances + + Format: date-time + |
+ false | +