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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
lastTransitionTimestring + 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
messagestring + message is a human readable message indicating details about the transition. +This may be an empty string.
+
true
reasonstring + 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
statusenum + status of the condition, one of True, False, Unknown.
+
+ Enum: True, False, Unknown
+
true
typestring + type of condition in CamelCase or in foo.example.com/CamelCase.
+
true
observedGenerationinteger + 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
+ +## GrafanaMuteTiming +[↩ Parent](#grafanaintegreatlyorgv1beta1 ) + + + + + + +GrafanaMuteTiming is the Schema for the GrafanaMuteTiming API + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringgrafana.integreatly.org/v1beta1true
kindstringGrafanaMuteTimingtrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + GrafanaMuteTimingSpec defines the desired state of GrafanaMuteTiming
+
+ Validations:
  • ((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) && has(self.editable))): spec.editable is immutable
  • +
    false
    statusobject + The most recent observed state of a Grafana resource
    +
    false
    + + +### GrafanaMuteTiming.spec +[↩ Parent](#grafanamutetiming) + + + +GrafanaMuteTimingSpec defines the desired state of GrafanaMuteTiming + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    instanceSelectorobject + Selects Grafana instances for import
    +
    + Validations:
  • self == oldSelf: spec.instanceSelector is immutable
  • +
    true
    namestring + A unique name for the mute timing
    +
    true
    time_intervals[]object + Time intervals for muting
    +
    true
    allowCrossNamespaceImportboolean + Allow the Operator to match this resource with Grafanas outside the current namespace
    +
    + Default: false
    +
    false
    editableboolean + Whether to enable or disable editing of the mute timing in Grafana UI
    +
    + Validations:
  • self == oldSelf: spec.editable is immutable
  • +
    false
    resyncPeriodstring + How often the resource is synced, defaults to 10m0s if not set
    +
    + Format: duration
    + Default: 10m0s
    +
    false
    + + +### GrafanaMuteTiming.spec.instanceSelector +[↩ Parent](#grafanamutetimingspec) + + + +Selects Grafana instances for import + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    matchExpressions[]object + matchExpressions is a list of label selector requirements. The requirements are ANDed.
    +
    false
    matchLabelsmap[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
    + + +### GrafanaMuteTiming.spec.instanceSelector.matchExpressions[index] +[↩ Parent](#grafanamutetimingspecinstanceselector) + + + +A label selector requirement is a selector that contains values, a key, and an operator that +relates the key and values. + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    keystring + key is the label key that the selector applies to.
    +
    true
    operatorstring + 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
    + + +### GrafanaMuteTiming.spec.time_intervals[index] +[↩ Parent](#grafanamutetimingspec) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    days_of_month[]string + days of month
    +
    false
    locationstring + location
    +
    false
    months[]string + months
    +
    false
    times[]object + times
    +
    false
    weekdays[]string + weekdays
    +
    false
    years[]string + years
    +
    false
    + + +### GrafanaMuteTiming.spec.time_intervals[index].times[index] +[↩ Parent](#grafanamutetimingspectime_intervalsindex) + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    end_timestring + end time
    +
    true
    start_timestring + start time
    +
    true
    + + +### GrafanaMuteTiming.status +[↩ Parent](#grafanamutetiming) + + + +The most recent observed state of a Grafana resource + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    conditions[]object + Results when synchonizing resource with Grafana instances
    +
    false
    lastResyncstring + Last time the resource was synchronized with Grafana instances
    +
    + Format: date-time
    +
    false
    + + +### GrafanaMuteTiming.status.conditions[index] +[↩ Parent](#grafanamutetimingstatus) + + + Condition contains details for one aspect of the current state of this API Resource. diff --git a/examples/mute_timing/README.md b/examples/mute_timing/README.md new file mode 100644 index 000000000..857a96cd1 --- /dev/null +++ b/examples/mute_timing/README.md @@ -0,0 +1,8 @@ +--- +title: "Mute timing" +linkTitle: "Mute timing" +--- + +Shows how to create a mute timing. + +{{< readfile file="resources.yaml" code="true" lang="yaml" >}} diff --git a/examples/mute_timing/resources.yaml b/examples/mute_timing/resources.yaml new file mode 100644 index 000000000..d346a08ff --- /dev/null +++ b/examples/mute_timing/resources.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaMuteTiming +metadata: + name: mutetiming-sample +spec: + instanceSelector: + matchLabels: + dashboards: "grafana" + name: mutetiming-sample + editable: false + time_intervals: + - times: + - start_time: "00:00" + end_time: "06:00" + weekdays: ["saturday", "sunday"] + days_of_month: ["1", "10"] + location: "Asia/Shanghai" diff --git a/main.go b/main.go index 9d3cb6490..3aeb5f71f 100644 --- a/main.go +++ b/main.go @@ -264,6 +264,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "GrafanaNotificationTemplate") os.Exit(1) } + if err = (&controllers.GrafanaMuteTimingReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "GrafanaMuteTiming") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/tests/e2e/example-test/13-assert.yaml b/tests/e2e/example-test/13-assert.yaml new file mode 100644 index 000000000..e31809715 --- /dev/null +++ b/tests/e2e/example-test/13-assert.yaml @@ -0,0 +1,8 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaMuteTiming +metadata: + name: mutetiming-sample +status: + conditions: + - type: MuteTimingSynchronized + status: "True" diff --git a/tests/e2e/example-test/13-mute-timing.yaml b/tests/e2e/example-test/13-mute-timing.yaml new file mode 100644 index 000000000..1a497b87b --- /dev/null +++ b/tests/e2e/example-test/13-mute-timing.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaMuteTiming +metadata: + name: mutetiming-sample +spec: + instanceSelector: + matchLabels: + dashboards: "grafana" + name: mutetiming-sample + editable: false + time_intervals: + - times: + - start_time: "00:00" + end_time: "06:00" + weekdays: [saturday] + days_of_month: ["1", "15"] + location: Asia/Shanghai diff --git a/tests/e2e/example-test/chainsaw-test.yaml b/tests/e2e/example-test/chainsaw-test.yaml index 405abc672..34100d80b 100755 --- a/tests/e2e/example-test/chainsaw-test.yaml +++ b/tests/e2e/example-test/chainsaw-test.yaml @@ -86,3 +86,9 @@ spec: file: 12-notification-template.yaml - assert: file: 12-assert.yaml + - name: step-13 + try: + - apply: + file: 13-mute-timing.yaml + - assert: + file: 13-assert.yaml