diff --git a/api/v1beta1/grafanaalertrulegroup_types.go b/api/v1beta1/grafanaalertrulegroup_types.go index c9a1da46e..d00d81f1e 100644 --- a/api/v1beta1/grafanaalertrulegroup_types.go +++ b/api/v1beta1/grafanaalertrulegroup_types.go @@ -25,6 +25,7 @@ import ( // GrafanaAlertRuleGroupSpec defines the desired state of GrafanaAlertRuleGroup // +kubebuilder:validation:XValidation:rule="(has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID)))", message="Only one of FolderUID or FolderRef can be set" +// +kubebuilder:validation:XValidation:rule="((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) && has(self.editable)))", message="spec.editable is immutable" type GrafanaAlertRuleGroupSpec struct { // +optional // Name of the alert rule group. If not specified, the resource name will be used. diff --git a/api/v1beta1/grafanaalertrulegroup_types_test.go b/api/v1beta1/grafanaalertrulegroup_types_test.go new file mode 100644 index 000000000..1f3824e68 --- /dev/null +++ b/api/v1beta1/grafanaalertrulegroup_types_test.go @@ -0,0 +1,71 @@ +package v1beta1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func newAlertRuleGroup(name string, editable *bool) *GrafanaAlertRuleGroup { + return &GrafanaAlertRuleGroup{ + TypeMeta: v1.TypeMeta{ + APIVersion: APIVersion, + Kind: "GrafanaAlertRuleGroup", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: GrafanaAlertRuleGroupSpec{ + Name: name, + Editable: editable, + FolderRef: "DummyFolderRef", + InstanceSelector: &v1.LabelSelector{ + MatchLabels: map[string]string{ + "test": "alertrulegroup", + }, + }, + Rules: []AlertRule{}, + }, + } +} + +var _ = Describe("AlertRuleGroup type", func() { + Context("Ensure AlertRuleGroup spec.editable is immutable", func() { + ctx := context.Background() + refTrue := true + refFalse := false + + It("Should block adding editable field when missing", func() { + arg := newAlertRuleGroup("missing-editable", nil) + By("Create new AlertRuleGroup without editable") + Expect(k8sClient.Create(ctx, arg)).To(Succeed()) + + By("Adding a editable") + arg.Spec.Editable = &refTrue + Expect(k8sClient.Update(ctx, arg)).To(HaveOccurred()) + }) + + It("Should block removing editable field when set", func() { + arg := newAlertRuleGroup("existing-editable", &refTrue) + By("Creating AlertRuleGroup with existing editable") + Expect(k8sClient.Create(ctx, arg)).To(Succeed()) + + By("And setting editable to ''") + arg.Spec.Editable = nil + Expect(k8sClient.Update(ctx, arg)).To(HaveOccurred()) + }) + + It("Should block changing value of editable", func() { + arg := newAlertRuleGroup("removing-editable", &refTrue) + By("Create new AlertRuleGroup with existing editable") + Expect(k8sClient.Create(ctx, arg)).To(Succeed()) + + By("Changing the existing editable") + arg.Spec.Editable = &refFalse + Expect(k8sClient.Update(ctx, arg)).To(HaveOccurred()) + }) + }) +}) diff --git a/api/v1beta1/grafanacontactpoint_types_test.go b/api/v1beta1/grafanacontactpoint_types_test.go new file mode 100644 index 000000000..665c485a4 --- /dev/null +++ b/api/v1beta1/grafanacontactpoint_types_test.go @@ -0,0 +1,74 @@ +package v1beta1 + +import ( + "context" + "encoding/json" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + // apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func newContactPoint(name string, uid string) *GrafanaContactPoint { + settings := new(apiextensionsv1.JSON) + json.Unmarshal([]byte("{}"), settings) //nolint:errcheck + + return &GrafanaContactPoint{ + TypeMeta: v1.TypeMeta{ + APIVersion: APIVersion, + Kind: "GrafanaContactPoint", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: GrafanaContactPointSpec{ + CustomUID: uid, + InstanceSelector: &v1.LabelSelector{ + MatchLabels: map[string]string{ + "test": "datasource", + }, + }, + Settings: settings, + }, + } +} + +var _ = Describe("ContactPoint type", func() { + Context("Ensure ContactPoint spec.uid is immutable", func() { + ctx := context.Background() + + It("Should block adding uid field when missing", func() { + contactpoint := newContactPoint("missing-uid", "") + By("Create new ContactPoint without uid") + Expect(k8sClient.Create(ctx, contactpoint)).To(Succeed()) + + By("Adding a uid") + contactpoint.Spec.CustomUID = "new-contactpoint-uid" + Expect(k8sClient.Update(ctx, contactpoint)).To(HaveOccurred()) + }) + + It("Should block removing uid field when set", func() { + contactpoint := newContactPoint("existing-uid", "existing-uid") + By("Creating ContactPoint with existing UID") + Expect(k8sClient.Create(ctx, contactpoint)).To(Succeed()) + + By("And setting UID to ''") + contactpoint.Spec.CustomUID = "" + Expect(k8sClient.Update(ctx, contactpoint)).To(HaveOccurred()) + }) + + It("Should block changing value of uid", func() { + contactpoint := newContactPoint("removing-uid", "existing-uid") + By("Create new ContactPoint with existing UID") + Expect(k8sClient.Create(ctx, contactpoint)).To(Succeed()) + + By("Changing the existing UID") + contactpoint.Spec.CustomUID = "new-contactpoint-uid" + Expect(k8sClient.Update(ctx, contactpoint)).To(HaveOccurred()) + }) + }) +}) diff --git a/api/v1beta1/grafanadashboard_types_test.go b/api/v1beta1/grafanadashboard_types_test.go index cff9d7b04..c5e7dc298 100644 --- a/api/v1beta1/grafanadashboard_types_test.go +++ b/api/v1beta1/grafanadashboard_types_test.go @@ -1,12 +1,16 @@ package v1beta1 import ( + "context" "encoding/json" "testing" "time" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) @@ -276,3 +280,61 @@ func getDashboardCR(t *testing.T, crUID string, statusUID string, specUID string return cr } + +func newDashboard(name string, uid string) *GrafanaDashboard { + return &GrafanaDashboard{ + TypeMeta: v1.TypeMeta{ + APIVersion: APIVersion, + Kind: "GrafanaDashboard", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: GrafanaDashboardSpec{ + CustomUID: uid, + InstanceSelector: &v1.LabelSelector{ + MatchLabels: map[string]string{ + "test": "datasource", + }, + }, + Json: "", + }, + } +} + +var _ = Describe("Dashboard type", func() { + Context("Ensure Dashboard spec.uid is immutable", func() { + ctx := context.Background() + + It("Should block adding uid field when missing", func() { + dash := newDashboard("missing-uid", "") + By("Create new Dashboard without uid") + Expect(k8sClient.Create(ctx, dash)).To(Succeed()) + + By("Adding a uid") + dash.Spec.CustomUID = "new-dash-uid" + Expect(k8sClient.Update(ctx, dash)).To(HaveOccurred()) + }) + + It("Should block removing uid field when set", func() { + dash := newDashboard("existing-uid", "existing-uid") + By("Creating Dashboard with existing UID") + Expect(k8sClient.Create(ctx, dash)).To(Succeed()) + + By("And setting UID to ''") + dash.Spec.CustomUID = "" + Expect(k8sClient.Update(ctx, dash)).To(HaveOccurred()) + }) + + It("Should block changing value of uid", func() { + dash := newDashboard("removing-uid", "existing-uid") + By("Create new Dashboard with existing UID") + Expect(k8sClient.Create(ctx, dash)).To(Succeed()) + + By("Changing the existing UID") + dash.Spec.CustomUID = "new-dash-uid" + Expect(k8sClient.Update(ctx, dash)).To(HaveOccurred()) + }) + }) +}) diff --git a/api/v1beta1/grafanadatasource_types_test.go b/api/v1beta1/grafanadatasource_types_test.go index 97daedc04..a7872d49e 100644 --- a/api/v1beta1/grafanadatasource_types_test.go +++ b/api/v1beta1/grafanadatasource_types_test.go @@ -37,13 +37,14 @@ func newDatasource(name string, uid string) *GrafanaDatasource { var _ = Describe("Datasource type", func() { Context("Ensure Datasource spec.uid is immutable", func() { ctx := context.Background() + It("Should block adding uid field when missing", func() { ds := newDatasource("missing-uid", "") By("Create new Datasource without uid") Expect(k8sClient.Create(ctx, ds)).To(Succeed()) By("Adding a uid") - ds.Spec.CustomUID = "new-uid" + ds.Spec.CustomUID = "new-ds-uid" Expect(k8sClient.Update(ctx, ds)).To(HaveOccurred()) }) @@ -63,7 +64,7 @@ var _ = Describe("Datasource type", func() { Expect(k8sClient.Create(ctx, ds)).To(Succeed()) By("Changing the existing UID") - ds.Spec.CustomUID = "new-uid" + ds.Spec.CustomUID = "new-ds-uid" Expect(k8sClient.Update(ctx, ds)).To(HaveOccurred()) }) }) diff --git a/api/v1beta1/grafanafolder_types_test.go b/api/v1beta1/grafanafolder_types_test.go index 4c931c948..5b043a083 100644 --- a/api/v1beta1/grafanafolder_types_test.go +++ b/api/v1beta1/grafanafolder_types_test.go @@ -1,10 +1,14 @@ package v1beta1 import ( + "context" "testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestGrafanaFolder_GetTitle(t *testing.T) { @@ -72,3 +76,60 @@ func TestGrafanaFolder_GetUID(t *testing.T) { }) } } + +func newFolder(name string, uid string) *GrafanaFolder { + return &GrafanaFolder{ + TypeMeta: v1.TypeMeta{ + APIVersion: APIVersion, + Kind: "GrafanaFolder", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: GrafanaFolderSpec{ + CustomUID: uid, + InstanceSelector: &v1.LabelSelector{ + MatchLabels: map[string]string{ + "test": "folder", + }, + }, + }, + } +} + +var _ = Describe("Folder type", func() { + Context("Ensure Folder spec.uid is immutable", func() { + ctx := context.Background() + + It("Should block adding uid field when missing", func() { + folder := newFolder("missing-uid", "") + By("Create new Folder without uid") + Expect(k8sClient.Create(ctx, folder)).To(Succeed()) + + By("Adding a uid") + folder.Spec.CustomUID = "new-folder-uid" + Expect(k8sClient.Update(ctx, folder)).To(HaveOccurred()) + }) + + It("Should block removing uid field when set", func() { + folder := newFolder("existing-uid", "existing-uid") + By("Creating Folder with existing UID") + Expect(k8sClient.Create(ctx, folder)).To(Succeed()) + + By("And setting UID to ''") + folder.Spec.CustomUID = "" + Expect(k8sClient.Update(ctx, folder)).To(HaveOccurred()) + }) + + It("Should block changing value of uid", func() { + folder := newFolder("removing-uid", "existing-uid") + By("Create new Folder with existing UID") + Expect(k8sClient.Create(ctx, folder)).To(Succeed()) + + By("Changing the existing UID") + folder.Spec.CustomUID = "new-folder-uid" + Expect(k8sClient.Update(ctx, folder)).To(HaveOccurred()) + }) + }) +}) diff --git a/api/v1beta1/grafananotificationpolicy_types.go b/api/v1beta1/grafananotificationpolicy_types.go index c2efef410..6c74643e1 100644 --- a/api/v1beta1/grafananotificationpolicy_types.go +++ b/api/v1beta1/grafananotificationpolicy_types.go @@ -24,6 +24,7 @@ import ( ) // GrafanaNotificationPolicySpec defines the desired state of GrafanaNotificationPolicy +// +kubebuilder:validation:XValidation:rule="((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) && has(self.editable)))", message="spec.editable is immutable" type GrafanaNotificationPolicySpec struct { // +optional // +kubebuilder:validation:Type=string diff --git a/api/v1beta1/grafananotificationpolicy_types_test.go b/api/v1beta1/grafananotificationpolicy_types_test.go new file mode 100644 index 000000000..b5c43b376 --- /dev/null +++ b/api/v1beta1/grafananotificationpolicy_types_test.go @@ -0,0 +1,74 @@ +package v1beta1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func newNotificationPolicy(name string, editable *bool) *GrafanaNotificationPolicy { + return &GrafanaNotificationPolicy{ + TypeMeta: v1.TypeMeta{ + APIVersion: APIVersion, + Kind: "GrafanaNotificationPolicy", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: GrafanaNotificationPolicySpec{ + Editable: editable, + InstanceSelector: &v1.LabelSelector{ + MatchLabels: map[string]string{ + "test": "notificationpolicy", + }, + }, + Route: &Route{ + Continue: false, + GroupBy: []string{"group_name", "alert_name"}, + MuteTimeIntervals: []string{}, + Routes: []*Route{}, + }, + }, + } +} + +var _ = Describe("NotificationPolicy type", func() { + Context("Ensure NotificationPolicy spec.editable is immutable", func() { + ctx := context.Background() + refTrue := true + refFalse := false + + It("Should block adding editable field when missing", func() { + notificationpolicy := newNotificationPolicy("missing-editable", nil) + By("Create new NotificationPolicy without editable") + Expect(k8sClient.Create(ctx, notificationpolicy)).To(Succeed()) + + By("Adding a editable") + notificationpolicy.Spec.Editable = &refTrue + Expect(k8sClient.Update(ctx, notificationpolicy)).To(HaveOccurred()) + }) + + It("Should block removing editable field when set", func() { + notificationpolicy := newNotificationPolicy("existing-editable", &refTrue) + By("Creating NotificationPolicy with existing editable") + Expect(k8sClient.Create(ctx, notificationpolicy)).To(Succeed()) + + By("And setting editable to ''") + notificationpolicy.Spec.Editable = nil + Expect(k8sClient.Update(ctx, notificationpolicy)).To(HaveOccurred()) + }) + + It("Should block changing value of editable", func() { + notificationpolicy := newNotificationPolicy("removing-editable", &refTrue) + By("Create new NotificationPolicy with existing editable") + Expect(k8sClient.Create(ctx, notificationpolicy)).To(Succeed()) + + By("Changing the existing editable") + notificationpolicy.Spec.Editable = &refFalse + Expect(k8sClient.Update(ctx, notificationpolicy)).To(HaveOccurred()) + }) + }) +}) diff --git a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml index 29f25d77d..cce7dc33c 100644 --- a/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -239,6 +239,9 @@ spec: - message: Only one of FolderUID or FolderRef can be set rule: (has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID))) + - message: spec.editable is immutable + rule: ((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) + && has(self.editable))) status: description: GrafanaAlertRuleGroupStatus defines the observed state of GrafanaAlertRuleGroup diff --git a/config/crd/bases/grafana.integreatly.org_grafananotificationpolicies.yaml b/config/crd/bases/grafana.integreatly.org_grafananotificationpolicies.yaml index c83cdc85b..9270f4559 100644 --- a/config/crd/bases/grafana.integreatly.org_grafananotificationpolicies.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafananotificationpolicies.yaml @@ -180,6 +180,10 @@ spec: - instanceSelector - route 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: GrafanaNotificationPolicyStatus defines the observed state of GrafanaNotificationPolicy diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml index 29f25d77d..cce7dc33c 100644 --- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanaalertrulegroups.yaml @@ -239,6 +239,9 @@ spec: - message: Only one of FolderUID or FolderRef can be set rule: (has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID))) + - message: spec.editable is immutable + rule: ((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) + && has(self.editable))) status: description: GrafanaAlertRuleGroupStatus defines the observed state of GrafanaAlertRuleGroup diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafananotificationpolicies.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafananotificationpolicies.yaml index c83cdc85b..9270f4559 100644 --- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafananotificationpolicies.yaml +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafananotificationpolicies.yaml @@ -180,6 +180,10 @@ spec: - instanceSelector - route 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: GrafanaNotificationPolicyStatus defines the observed state of GrafanaNotificationPolicy diff --git a/deploy/kustomize/base/crds.yaml b/deploy/kustomize/base/crds.yaml index b03bc4b21..f8ad98dc9 100644 --- a/deploy/kustomize/base/crds.yaml +++ b/deploy/kustomize/base/crds.yaml @@ -238,6 +238,9 @@ spec: - message: Only one of FolderUID or FolderRef can be set rule: (has(self.folderUID) && !(has(self.folderRef))) || (has(self.folderRef) && !(has(self.folderUID))) + - message: spec.editable is immutable + rule: ((!has(oldSelf.editable) && !has(self.editable)) || (has(oldSelf.editable) + && has(self.editable))) status: description: GrafanaAlertRuleGroupStatus defines the observed state of GrafanaAlertRuleGroup @@ -1754,6 +1757,10 @@ spec: - instanceSelector - route 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: GrafanaNotificationPolicyStatus defines the observed state of GrafanaNotificationPolicy diff --git a/docs/docs/api.md b/docs/docs/api.md index 86e7a9dba..a5d283a5f 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -70,7 +70,7 @@ GrafanaAlertRuleGroup is the Schema for the grafanaalertrulegroups API