diff --git a/apps/alerting/notifications/template.cue b/apps/alerting/notifications/template.cue new file mode 100644 index 00000000000..56c2507cd1e --- /dev/null +++ b/apps/alerting/notifications/template.cue @@ -0,0 +1,25 @@ +package core + +templateGroup: { + kind: "TemplateGroup" + group: "notifications" + apiResource: { + groupOverride: "notifications.alerting.grafana.app" + } + codegen: { + frontend: false + backend: true + } + pluralName: "TemplatesGroups" + current: "v0alpha1" + versions: { + "v0alpha1": { + schema: { + spec: { + title: string + content: string + } + } + } + } +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/register.go b/pkg/apis/alerting_notifications/v0alpha1/register.go index d59b6fdd1da..05eab2f7ba3 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/register.go +++ b/pkg/apis/alerting_notifications/v0alpha1/register.go @@ -67,6 +67,25 @@ var ( }, }, ) + TemplateGroupResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "templategroups", "templategroup", "TemplateGroup", + func() runtime.Object { return &TemplateGroup{} }, + func() runtime.Object { return &TemplateGroupList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + }, + Reader: func(obj any) ([]interface{}, error) { + r, ok := obj.(*TemplateGroup) + if !ok { + return nil, fmt.Errorf("expected resource or info") + } + return []interface{}{ + r.Name, + }, nil + }, + }, + ) // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} // SchemaBuilder is used by standard codegen @@ -87,6 +106,8 @@ func AddKnownTypesGroup(scheme *runtime.Scheme, g schema.GroupVersion) error { &TimeIntervalList{}, &Receiver{}, &ReceiverList{}, + &TemplateGroup{}, + &TemplateGroupList{}, ) metav1.AddToGroupVersion(scheme, g) @@ -122,6 +143,22 @@ func AddKnownTypesGroup(scheme *runtime.Scheme, g schema.GroupVersion) error { return err } + err = scheme.AddFieldLabelConversionFunc( + TemplateGroupResourceInfo.GroupVersionKind(), + func(label, value string) (string, string, error) { + fieldSet := SelectableTemplateGroupFields(&TemplateGroup{}) + for key := range fieldSet { + if label == key { + return label, value, nil + } + } + return "", "", fmt.Errorf("field label not supported for %s: %s", scope.ScopeNodeResourceInfo.GroupVersionKind(), label) + }, + ) + if err != nil { + return err + } + return nil } @@ -145,6 +182,16 @@ func SelectableReceiverFields(obj *Receiver) fields.Set { }) } +func SelectableTemplateGroupFields(obj *TemplateGroup) fields.Set { + if obj == nil { + return nil + } + return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{ + "metadata.provenance": obj.GetProvenanceStatus(), + "spec.title": obj.Spec.Title, + }) +} + // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() diff --git a/pkg/apis/alerting_notifications/v0alpha1/templategroup_spec.go b/pkg/apis/alerting_notifications/v0alpha1/templategroup_spec.go new file mode 100644 index 00000000000..0731380f602 --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/templategroup_spec.go @@ -0,0 +1,8 @@ +package v0alpha1 + +// TemplateGroupSpec defines model for TemplateGroupSpec. +// +k8s:openapi-gen=true +type TemplateGroupSpec struct { + Title string `json:"title"` + Content string `json:"content"` +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/types.go b/pkg/apis/alerting_notifications/v0alpha1/types.go index ec5edaf6e6e..e674f58ebd7 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/types.go +++ b/pkg/apis/alerting_notifications/v0alpha1/types.go @@ -7,6 +7,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// region TimeInterval + // +genclient // +k8s:openapi-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -86,7 +88,9 @@ type TimeIntervalList struct { Items []TimeInterval `json:"items"` } -// Receivers --------------------------------- +// endregion + +// region Receivers // +genclient // +k8s:openapi-gen=true @@ -166,3 +170,88 @@ type ReceiverList struct { metav1.ListMeta `json:"metadata"` Items []Receiver `json:"items"` } + +// endregion + +// region Templates + +// +genclient +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TemplateGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec TemplateGroupSpec `json:"spec"` +} + +func (o *TemplateGroup) GetSpec() any { + return o.Spec +} + +func (o *TemplateGroup) SetSpec(spec any) error { + cast, ok := spec.(TemplateGroupSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *TemplateGroup) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *TemplateGroup) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *TemplateGroup) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(o.ObjectMeta.Annotations["grafana.com/updateTimestamp"], time.RFC3339) + return parsed +} + +func (o *TemplateGroup) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *TemplateGroup) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *TemplateGroup) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TemplateGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []TemplateGroup `json:"items"` +} + +// endregion diff --git a/pkg/apis/alerting_notifications/v0alpha1/types_ext.go b/pkg/apis/alerting_notifications/v0alpha1/types_ext.go index 70a3e9a8b1d..d3b97edcd19 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/types_ext.go +++ b/pkg/apis/alerting_notifications/v0alpha1/types_ext.go @@ -76,3 +76,24 @@ func (o *Receiver) SetInUse(routesCnt int, rules []string) { func InUseAnnotation(resource string) string { return fmt.Sprintf("%s%s/%s", InternalPrefix, "inUse", resource) } + +func (o *TemplateGroup) GetProvenanceStatus() string { + if o == nil || o.Annotations == nil { + return "" + } + s, ok := o.Annotations[ProvenanceStatusAnnotationKey] + if !ok || s == "" { + return ProvenanceStatusNone + } + return s +} + +func (o *TemplateGroup) SetProvenanceStatus(status string) { + if o.Annotations == nil { + o.Annotations = make(map[string]string, 1) + } + if status == "" { + status = ProvenanceStatusNone + } + o.Annotations[ProvenanceStatusAnnotationKey] = status +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go index ca7b6aa8a6d..b92ad6d360d 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go @@ -174,6 +174,82 @@ func (in *ReceiverSpec) DeepCopy() *ReceiverSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateGroup) DeepCopyInto(out *TemplateGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateGroup. +func (in *TemplateGroup) DeepCopy() *TemplateGroup { + if in == nil { + return nil + } + out := new(TemplateGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TemplateGroup) 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 *TemplateGroupList) DeepCopyInto(out *TemplateGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TemplateGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateGroupList. +func (in *TemplateGroupList) DeepCopy() *TemplateGroupList { + if in == nil { + return nil + } + out := new(TemplateGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TemplateGroupList) 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 *TemplateGroupSpec) DeepCopyInto(out *TemplateGroupSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateGroupSpec. +func (in *TemplateGroupSpec) DeepCopy() *TemplateGroupSpec { + if in == nil { + return nil + } + out := new(TemplateGroupSpec) + in.DeepCopyInto(out) + 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 diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go index c2ce1fbc925..6703e64406c 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go @@ -14,15 +14,18 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Integration": schema_pkg_apis_alerting_notifications_v0alpha1_Integration(ref), - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval": schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref), - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Receiver": schema_pkg_apis_alerting_notifications_v0alpha1_Receiver(ref), - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverList": schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverList(ref), - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverSpec": schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverSpec(ref), - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval": schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref), - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalList(ref), - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalSpec(ref), - "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeRange": schema_pkg_apis_alerting_notifications_v0alpha1_TimeRange(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Integration": schema_pkg_apis_alerting_notifications_v0alpha1_Integration(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval": schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Receiver": schema_pkg_apis_alerting_notifications_v0alpha1_Receiver(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverList": schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverList(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverSpec": schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverSpec(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroup": schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroup(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroupList": schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroupList(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroupSpec": schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroupSpec(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval": schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalList(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalSpec(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeRange": schema_pkg_apis_alerting_notifications_v0alpha1_TimeRange(ref), } } @@ -336,6 +339,123 @@ func schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverSpec(ref common.Ref } } +func schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroupSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroupSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroupList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + 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{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroup"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroup", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TemplateGroupSpec defines model for TemplateGroupSpec.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "content": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"title", "content"}, + }, + }, + } +} + func schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/templategroup.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/templategroup.go new file mode 100644 index 00000000000..b4340dcde50 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/templategroup.go @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// TemplateGroupApplyConfiguration represents a declarative configuration of the TemplateGroup type for use +// with apply. +type TemplateGroupApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *TemplateGroupSpecApplyConfiguration `json:"spec,omitempty"` +} + +// TemplateGroup constructs a declarative configuration of the TemplateGroup type for use with +// apply. +func TemplateGroup(name, namespace string) *TemplateGroupApplyConfiguration { + b := &TemplateGroupApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("TemplateGroup") + b.WithAPIVersion("notifications.alerting.grafana.app/v0alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithKind(value string) *TemplateGroupApplyConfiguration { + b.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithAPIVersion(value string) *TemplateGroupApplyConfiguration { + b.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithName(value string) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithGenerateName(value string) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithNamespace(value string) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithUID(value types.UID) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithResourceVersion(value string) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithGeneration(value int64) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithCreationTimestamp(value metav1.Time) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *TemplateGroupApplyConfiguration) WithLabels(entries map[string]string) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *TemplateGroupApplyConfiguration) WithAnnotations(entries map[string]string) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *TemplateGroupApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.OwnerReferences = append(b.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *TemplateGroupApplyConfiguration) WithFinalizers(values ...string) *TemplateGroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.Finalizers = append(b.Finalizers, values[i]) + } + return b +} + +func (b *TemplateGroupApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *TemplateGroupApplyConfiguration) WithSpec(value *TemplateGroupSpecApplyConfiguration) *TemplateGroupApplyConfiguration { + b.Spec = value + return b +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *TemplateGroupApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.Name +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/templategroupspec.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/templategroupspec.go new file mode 100644 index 00000000000..42fcad447be --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/templategroupspec.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// TemplateGroupSpecApplyConfiguration represents a declarative configuration of the TemplateGroupSpec type for use +// with apply. +type TemplateGroupSpecApplyConfiguration struct { + Title *string `json:"title,omitempty"` + Content *string `json:"content,omitempty"` +} + +// TemplateGroupSpecApplyConfiguration constructs a declarative configuration of the TemplateGroupSpec type for use with +// apply. +func TemplateGroupSpec() *TemplateGroupSpecApplyConfiguration { + return &TemplateGroupSpecApplyConfiguration{} +} + +// WithTitle sets the Title field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Title field is set to the value of the last call. +func (b *TemplateGroupSpecApplyConfiguration) WithTitle(value string) *TemplateGroupSpecApplyConfiguration { + b.Title = &value + return b +} + +// WithContent sets the Content field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Content field is set to the value of the last call. +func (b *TemplateGroupSpecApplyConfiguration) WithContent(value string) *TemplateGroupSpecApplyConfiguration { + b.Content = &value + return b +} diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go index 55ac136e4fc..8221f95b1da 100644 --- a/pkg/generated/applyconfiguration/utils.go +++ b/pkg/generated/applyconfiguration/utils.go @@ -28,6 +28,10 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &alertingnotificationsv0alpha1.ReceiverApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("ReceiverSpec"): return &alertingnotificationsv0alpha1.ReceiverSpecApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("TemplateGroup"): + return &alertingnotificationsv0alpha1.TemplateGroupApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("TemplateGroupSpec"): + return &alertingnotificationsv0alpha1.TemplateGroupSpecApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("TimeInterval"): return &alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("TimeIntervalSpec"): diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go index 53dfb5ad0d8..f77cbc0c51a 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go @@ -15,6 +15,7 @@ import ( type NotificationsV0alpha1Interface interface { RESTClient() rest.Interface ReceiversGetter + TemplateGroupsGetter TimeIntervalsGetter } @@ -27,6 +28,10 @@ func (c *NotificationsV0alpha1Client) Receivers(namespace string) ReceiverInterf return newReceivers(c, namespace) } +func (c *NotificationsV0alpha1Client) TemplateGroups(namespace string) TemplateGroupInterface { + return newTemplateGroups(c, namespace) +} + func (c *NotificationsV0alpha1Client) TimeIntervals(namespace string) TimeIntervalInterface { return newTimeIntervals(c, namespace) } diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go index 84bcadcd8e2..f2695e6e8a3 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go @@ -18,6 +18,10 @@ func (c *FakeNotificationsV0alpha1) Receivers(namespace string) v0alpha1.Receive return &FakeReceivers{c, namespace} } +func (c *FakeNotificationsV0alpha1) TemplateGroups(namespace string) v0alpha1.TemplateGroupInterface { + return &FakeTemplateGroups{c, namespace} +} + func (c *FakeNotificationsV0alpha1) TimeIntervals(namespace string) v0alpha1.TimeIntervalInterface { return &FakeTimeIntervals{c, namespace} } diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_templategroup.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_templategroup.go new file mode 100644 index 00000000000..ffbd0668e4b --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_templategroup.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeTemplateGroups implements TemplateGroupInterface +type FakeTemplateGroups struct { + Fake *FakeNotificationsV0alpha1 + ns string +} + +var templategroupsResource = v0alpha1.SchemeGroupVersion.WithResource("templategroups") + +var templategroupsKind = v0alpha1.SchemeGroupVersion.WithKind("TemplateGroup") + +// Get takes name of the templateGroup, and returns the corresponding templateGroup object, and an error if there is any. +func (c *FakeTemplateGroups) Get(ctx context.Context, name string, options v1.GetOptions) (result *v0alpha1.TemplateGroup, err error) { + emptyResult := &v0alpha1.TemplateGroup{} + obj, err := c.Fake. + Invokes(testing.NewGetActionWithOptions(templategroupsResource, c.ns, name, options), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.TemplateGroup), err +} + +// List takes label and field selectors, and returns the list of TemplateGroups that match those selectors. +func (c *FakeTemplateGroups) List(ctx context.Context, opts v1.ListOptions) (result *v0alpha1.TemplateGroupList, err error) { + emptyResult := &v0alpha1.TemplateGroupList{} + obj, err := c.Fake. + Invokes(testing.NewListActionWithOptions(templategroupsResource, templategroupsKind, c.ns, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v0alpha1.TemplateGroupList{ListMeta: obj.(*v0alpha1.TemplateGroupList).ListMeta} + for _, item := range obj.(*v0alpha1.TemplateGroupList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested templateGroups. +func (c *FakeTemplateGroups) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchActionWithOptions(templategroupsResource, c.ns, opts)) + +} + +// Create takes the representation of a templateGroup and creates it. Returns the server's representation of the templateGroup, and an error, if there is any. +func (c *FakeTemplateGroups) Create(ctx context.Context, templateGroup *v0alpha1.TemplateGroup, opts v1.CreateOptions) (result *v0alpha1.TemplateGroup, err error) { + emptyResult := &v0alpha1.TemplateGroup{} + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(templategroupsResource, c.ns, templateGroup, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.TemplateGroup), err +} + +// Update takes the representation of a templateGroup and updates it. Returns the server's representation of the templateGroup, and an error, if there is any. +func (c *FakeTemplateGroups) Update(ctx context.Context, templateGroup *v0alpha1.TemplateGroup, opts v1.UpdateOptions) (result *v0alpha1.TemplateGroup, err error) { + emptyResult := &v0alpha1.TemplateGroup{} + obj, err := c.Fake. + Invokes(testing.NewUpdateActionWithOptions(templategroupsResource, c.ns, templateGroup, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.TemplateGroup), err +} + +// Delete takes name of the templateGroup and deletes it. Returns an error if one occurs. +func (c *FakeTemplateGroups) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(templategroupsResource, c.ns, name, opts), &v0alpha1.TemplateGroup{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeTemplateGroups) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionActionWithOptions(templategroupsResource, c.ns, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v0alpha1.TemplateGroupList{}) + return err +} + +// Patch applies the patch and returns the patched templateGroup. +func (c *FakeTemplateGroups) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.TemplateGroup, err error) { + emptyResult := &v0alpha1.TemplateGroup{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(templategroupsResource, c.ns, name, pt, data, opts, subresources...), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.TemplateGroup), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied templateGroup. +func (c *FakeTemplateGroups) Apply(ctx context.Context, templateGroup *alertingnotificationsv0alpha1.TemplateGroupApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.TemplateGroup, err error) { + if templateGroup == nil { + return nil, fmt.Errorf("templateGroup provided to Apply must not be nil") + } + data, err := json.Marshal(templateGroup) + if err != nil { + return nil, err + } + name := templateGroup.Name + if name == nil { + return nil, fmt.Errorf("templateGroup.Name must be provided to Apply") + } + emptyResult := &v0alpha1.TemplateGroup{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(templategroupsResource, c.ns, *name, types.ApplyPatchType, data, opts.ToPatchOptions()), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.TemplateGroup), err +} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go index c30d1403330..f76e1bfa65d 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go @@ -6,4 +6,6 @@ package v0alpha1 type ReceiverExpansion interface{} +type TemplateGroupExpansion interface{} + type TimeIntervalExpansion interface{} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/templategroup.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/templategroup.go new file mode 100644 index 00000000000..5eb06224934 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/templategroup.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "context" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + scheme "github.com/grafana/grafana/pkg/generated/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// TemplateGroupsGetter has a method to return a TemplateGroupInterface. +// A group's client should implement this interface. +type TemplateGroupsGetter interface { + TemplateGroups(namespace string) TemplateGroupInterface +} + +// TemplateGroupInterface has methods to work with TemplateGroup resources. +type TemplateGroupInterface interface { + Create(ctx context.Context, templateGroup *v0alpha1.TemplateGroup, opts v1.CreateOptions) (*v0alpha1.TemplateGroup, error) + Update(ctx context.Context, templateGroup *v0alpha1.TemplateGroup, opts v1.UpdateOptions) (*v0alpha1.TemplateGroup, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v0alpha1.TemplateGroup, error) + List(ctx context.Context, opts v1.ListOptions) (*v0alpha1.TemplateGroupList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.TemplateGroup, err error) + Apply(ctx context.Context, templateGroup *alertingnotificationsv0alpha1.TemplateGroupApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.TemplateGroup, err error) + TemplateGroupExpansion +} + +// templateGroups implements TemplateGroupInterface +type templateGroups struct { + *gentype.ClientWithListAndApply[*v0alpha1.TemplateGroup, *v0alpha1.TemplateGroupList, *alertingnotificationsv0alpha1.TemplateGroupApplyConfiguration] +} + +// newTemplateGroups returns a TemplateGroups +func newTemplateGroups(c *NotificationsV0alpha1Client, namespace string) *templateGroups { + return &templateGroups{ + gentype.NewClientWithListAndApply[*v0alpha1.TemplateGroup, *v0alpha1.TemplateGroupList, *alertingnotificationsv0alpha1.TemplateGroupApplyConfiguration]( + "templategroups", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *v0alpha1.TemplateGroup { return &v0alpha1.TemplateGroup{} }, + func() *v0alpha1.TemplateGroupList { return &v0alpha1.TemplateGroupList{} }), + } +} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go index 64c33c7fc5e..a84b5042189 100644 --- a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go +++ b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go @@ -12,6 +12,8 @@ import ( type Interface interface { // Receivers returns a ReceiverInformer. Receivers() ReceiverInformer + // TemplateGroups returns a TemplateGroupInformer. + TemplateGroups() TemplateGroupInformer // TimeIntervals returns a TimeIntervalInformer. TimeIntervals() TimeIntervalInformer } @@ -32,6 +34,11 @@ func (v *version) Receivers() ReceiverInformer { return &receiverInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// TemplateGroups returns a TemplateGroupInformer. +func (v *version) TemplateGroups() TemplateGroupInformer { + return &templateGroupInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // TimeIntervals returns a TimeIntervalInformer. func (v *version) TimeIntervals() TimeIntervalInformer { return &timeIntervalInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/templategroup.go b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/templategroup.go new file mode 100644 index 00000000000..2a04ec9ea4e --- /dev/null +++ b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/templategroup.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by informer-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "context" + time "time" + + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + versioned "github.com/grafana/grafana/pkg/generated/clientset/versioned" + internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces" + v0alpha1 "github.com/grafana/grafana/pkg/generated/listers/alerting_notifications/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// TemplateGroupInformer provides access to a shared informer and lister for +// TemplateGroups. +type TemplateGroupInformer interface { + Informer() cache.SharedIndexInformer + Lister() v0alpha1.TemplateGroupLister +} + +type templateGroupInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewTemplateGroupInformer constructs a new informer for TemplateGroup type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTemplateGroupInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredTemplateGroupInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredTemplateGroupInformer constructs a new informer for TemplateGroup type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredTemplateGroupInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NotificationsV0alpha1().TemplateGroups(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NotificationsV0alpha1().TemplateGroups(namespace).Watch(context.TODO(), options) + }, + }, + &alertingnotificationsv0alpha1.TemplateGroup{}, + resyncPeriod, + indexers, + ) +} + +func (f *templateGroupInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredTemplateGroupInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *templateGroupInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&alertingnotificationsv0alpha1.TemplateGroup{}, f.defaultInformer) +} + +func (f *templateGroupInformer) Lister() v0alpha1.TemplateGroupLister { + return v0alpha1.NewTemplateGroupLister(f.Informer().GetIndexer()) +} diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go index 36b920d2c47..9c16b6eab97 100644 --- a/pkg/generated/informers/externalversions/generic.go +++ b/pkg/generated/informers/externalversions/generic.go @@ -42,6 +42,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=notifications.alerting.grafana.app, Version=v0alpha1 case v0alpha1.SchemeGroupVersion.WithResource("receivers"): return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().Receivers().Informer()}, nil + case v0alpha1.SchemeGroupVersion.WithResource("templategroups"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().TemplateGroups().Informer()}, nil case v0alpha1.SchemeGroupVersion.WithResource("timeintervals"): return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().TimeIntervals().Informer()}, nil diff --git a/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go b/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go index ed4d4f239ae..8f4585658eb 100644 --- a/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go +++ b/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go @@ -12,6 +12,14 @@ type ReceiverListerExpansion interface{} // ReceiverNamespaceLister. type ReceiverNamespaceListerExpansion interface{} +// TemplateGroupListerExpansion allows custom methods to be added to +// TemplateGroupLister. +type TemplateGroupListerExpansion interface{} + +// TemplateGroupNamespaceListerExpansion allows custom methods to be added to +// TemplateGroupNamespaceLister. +type TemplateGroupNamespaceListerExpansion interface{} + // TimeIntervalListerExpansion allows custom methods to be added to // TimeIntervalLister. type TimeIntervalListerExpansion interface{} diff --git a/pkg/generated/listers/alerting_notifications/v0alpha1/templategroup.go b/pkg/generated/listers/alerting_notifications/v0alpha1/templategroup.go new file mode 100644 index 00000000000..67d7df50f96 --- /dev/null +++ b/pkg/generated/listers/alerting_notifications/v0alpha1/templategroup.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by lister-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/listers" + "k8s.io/client-go/tools/cache" +) + +// TemplateGroupLister helps list TemplateGroups. +// All objects returned here must be treated as read-only. +type TemplateGroupLister interface { + // List lists all TemplateGroups in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v0alpha1.TemplateGroup, err error) + // TemplateGroups returns an object that can list and get TemplateGroups. + TemplateGroups(namespace string) TemplateGroupNamespaceLister + TemplateGroupListerExpansion +} + +// templateGroupLister implements the TemplateGroupLister interface. +type templateGroupLister struct { + listers.ResourceIndexer[*v0alpha1.TemplateGroup] +} + +// NewTemplateGroupLister returns a new TemplateGroupLister. +func NewTemplateGroupLister(indexer cache.Indexer) TemplateGroupLister { + return &templateGroupLister{listers.New[*v0alpha1.TemplateGroup](indexer, v0alpha1.Resource("templategroup"))} +} + +// TemplateGroups returns an object that can list and get TemplateGroups. +func (s *templateGroupLister) TemplateGroups(namespace string) TemplateGroupNamespaceLister { + return templateGroupNamespaceLister{listers.NewNamespaced[*v0alpha1.TemplateGroup](s.ResourceIndexer, namespace)} +} + +// TemplateGroupNamespaceLister helps list and get TemplateGroups. +// All objects returned here must be treated as read-only. +type TemplateGroupNamespaceLister interface { + // List lists all TemplateGroups in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v0alpha1.TemplateGroup, err error) + // Get retrieves the TemplateGroup from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v0alpha1.TemplateGroup, error) + TemplateGroupNamespaceListerExpansion +} + +// templateGroupNamespaceLister implements the TemplateGroupNamespaceLister +// interface. +type templateGroupNamespaceLister struct { + listers.ResourceIndexer[*v0alpha1.TemplateGroup] +} diff --git a/pkg/registry/apis/alerting/notifications/register.go b/pkg/registry/apis/alerting/notifications/register.go index be565852adc..f073a3c7d93 100644 --- a/pkg/registry/apis/alerting/notifications/register.go +++ b/pkg/registry/apis/alerting/notifications/register.go @@ -16,6 +16,7 @@ import ( notificationsModels "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" receiver "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/receiver" + "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/template_group" timeInterval "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" @@ -81,9 +82,15 @@ func (t *NotificationsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiser return fmt.Errorf("failed to initialize receiver storage: %w", err) } + templ, err := template_group.NewStorage(t.ng.Api.Templates, t.namespacer, scheme, optsGetter, dualWriteBuilder) + if err != nil { + return fmt.Errorf("failed to initialize templates group storage: %w", err) + } + apiGroupInfo.VersionedResourcesStorageMap[notificationsModels.VERSION] = map[string]rest.Storage{ - notificationsModels.TimeIntervalResourceInfo.StoragePath(): intervals, - notificationsModels.ReceiverResourceInfo.StoragePath(): recvStorage, + notificationsModels.TimeIntervalResourceInfo.StoragePath(): intervals, + notificationsModels.ReceiverResourceInfo.StoragePath(): recvStorage, + notificationsModels.TemplateGroupResourceInfo.StoragePath(): templ, } return nil } @@ -107,6 +114,7 @@ func (t *NotificationsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3 // Hide the ability to list or watch across all tenants delete(oas.Paths.Paths, root+notificationsModels.ReceiverResourceInfo.GroupResource().Resource) delete(oas.Paths.Paths, root+notificationsModels.TimeIntervalResourceInfo.GroupResource().Resource) + delete(oas.Paths.Paths, root+notificationsModels.TemplateGroupResourceInfo.GroupResource().Resource) // The root API discovery list sub := oas.Paths.Paths[root] @@ -120,6 +128,8 @@ func (t *NotificationsAPIBuilder) GetAuthorizer() authorizer.Authorizer { return authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { switch a.GetResource() { + case notificationsModels.TemplateGroupResourceInfo.GroupResource().Resource: + return template_group.Authorize(ctx, t.authz, a) case notificationsModels.TimeIntervalResourceInfo.GroupResource().Resource: return timeInterval.Authorize(ctx, t.authz, a) case notificationsModels.ReceiverResourceInfo.GroupResource().Resource: diff --git a/pkg/registry/apis/alerting/notifications/template_group/authorize.go b/pkg/registry/apis/alerting/notifications/template_group/authorize.go new file mode 100644 index 00000000000..b9a3f92224a --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/template_group/authorize.go @@ -0,0 +1,54 @@ +package template_group + +import ( + "context" + + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if attr.GetResource() != resourceInfo.GroupResource().Resource { + return authorizer.DecisionNoOpinion, "", nil + } + user, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + var action accesscontrol.Evaluator + switch attr.GetVerb() { + case "patch": + fallthrough + case "create": + fallthrough + case "update": + action = accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTemplatesWrite), + ) + case "deletecollection": + fallthrough + case "delete": + action = accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTemplatesDelete), + ) + } + + eval := accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsRead), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTemplatesRead), + ) + if action != nil { + eval = accesscontrol.EvalAll(eval, action) + } + + ok, err := ac.Evaluate(ctx, user, eval) + if ok { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", err +} diff --git a/pkg/registry/apis/alerting/notifications/template_group/conversions.go b/pkg/registry/apis/alerting/notifications/template_group/conversions.go new file mode 100644 index 00000000000..4988f3187f5 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/template_group/conversions.go @@ -0,0 +1,52 @@ +package template_group + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/types" + + model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +func convertToK8sResources(orgID int64, list []definitions.NotificationTemplate, namespacer request.NamespaceMapper, selector fields.Selector) (*model.TemplateGroupList, error) { + result := &model.TemplateGroupList{} + for _, t := range list { + item := convertToK8sResource(orgID, t, namespacer) + if selector != nil && !selector.Empty() && !selector.Matches(model.SelectableTemplateGroupFields(item)) { + continue + } + result.Items = append(result.Items, *item) + } + return result, nil +} + +func convertToK8sResource(orgID int64, template definitions.NotificationTemplate, namespacer request.NamespaceMapper) *model.TemplateGroup { + result := &model.TemplateGroup{ + TypeMeta: resourceInfo.TypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(template.UID), + Name: template.UID, + Namespace: namespacer(orgID), + ResourceVersion: template.ResourceVersion, + }, + Spec: model.TemplateGroupSpec{ + Title: template.Name, + Content: template.Template, + }, + } + result.SetProvenanceStatus(string(template.Provenance)) + return result +} + +func convertToDomainModel(template *model.TemplateGroup) definitions.NotificationTemplate { + return definitions.NotificationTemplate{ + UID: template.ObjectMeta.Name, + Name: template.Spec.Title, + Template: template.Spec.Content, + ResourceVersion: template.ResourceVersion, + Provenance: definitions.Provenance(ngmodels.ProvenanceNone), + } +} diff --git a/pkg/registry/apis/alerting/notifications/template_group/legacy_storage.go b/pkg/registry/apis/alerting/notifications/template_group/legacy_storage.go new file mode 100644 index 00000000000..09a783e2af1 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/template_group/legacy_storage.go @@ -0,0 +1,187 @@ +package template_group + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + notifications "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +var ( + _ grafanarest.LegacyStorage = (*legacyStorage)(nil) +) + +type TemplateService interface { + GetTemplate(ctx context.Context, orgID int64, nameOrUid string) (definitions.NotificationTemplate, error) + GetTemplates(ctx context.Context, orgID int64) ([]definitions.NotificationTemplate, error) + CreateTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) + UpdateTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) + DeleteTemplate(ctx context.Context, orgID int64, nameOrUid string, provenance definitions.Provenance, version string) error +} + +var resourceInfo = notifications.TemplateGroupResourceInfo + +type legacyStorage struct { + service TemplateService + namespacer request.NamespaceMapper + tableConverter rest.TableConvertor +} + +func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { + return nil, errors.NewMethodNotSupported(resourceInfo.GroupResource(), "deleteCollection") +} + +func (s *legacyStorage) New() runtime.Object { + return resourceInfo.NewFunc() +} + +func (s *legacyStorage) Destroy() {} + +func (s *legacyStorage) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *legacyStorage) GetSingularName() string { + return resourceInfo.GetSingularName() +} + +func (s *legacyStorage) NewList() runtime.Object { + return resourceInfo.NewListFunc() +} + +func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOptions) (runtime.Object, error) { + orgId, err := request.OrgIDForList(ctx) + if err != nil { + return nil, err + } + + res, err := s.service.GetTemplates(ctx, orgId) + if err != nil { + return nil, err + } + + return convertToK8sResources(orgId, res, s.namespacer, opts.FieldSelector) +} + +func (s *legacyStorage) Get(ctx context.Context, name string, _ *metav1.GetOptions) (runtime.Object, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + dto, err := s.service.GetTemplate(ctx, info.OrgID, name) + if err != nil { + return nil, err + } + return convertToK8sResource(info.OrgID, dto, s.namespacer), nil +} + +func (s *legacyStorage) Create(ctx context.Context, + obj runtime.Object, + createValidation rest.ValidateObjectFunc, + _ *metav1.CreateOptions, +) (runtime.Object, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + if createValidation != nil { + if err := createValidation(ctx, obj.DeepCopyObject()); err != nil { + return nil, err + } + } + p, ok := obj.(*notifications.TemplateGroup) + if !ok { + return nil, fmt.Errorf("expected template but got %s", obj.GetObjectKind().GroupVersionKind()) + } + if p.ObjectMeta.Name != "" { // TODO remove when metadata.name can be defined by user + return nil, errors.NewBadRequest("object's metadata.name should be empty") + } + out, err := s.service.CreateTemplate(ctx, info.OrgID, convertToDomainModel(p)) + if err != nil { + return nil, err + } + return convertToK8sResource(info.OrgID, out, s.namespacer), nil +} + +func (s *legacyStorage) Update(ctx context.Context, + name string, + objInfo rest.UpdatedObjectInfo, + createValidation rest.ValidateObjectFunc, + updateValidation rest.ValidateObjectUpdateFunc, + _ bool, + _ *metav1.UpdateOptions, +) (runtime.Object, bool, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + dto, err := s.service.GetTemplate(ctx, info.OrgID, name) + if err != nil { + return nil, false, err + } + old := convertToK8sResource(info.OrgID, dto, s.namespacer) + + obj, err := objInfo.UpdatedObject(ctx, old) + if err != nil { + return old, false, err + } + + if updateValidation != nil { + if err := updateValidation(ctx, obj, old); err != nil { + return nil, false, err + } + } + + p, ok := obj.(*notifications.TemplateGroup) + if !ok { + return nil, false, fmt.Errorf("expected template but got %s", obj.GetObjectKind().GroupVersionKind()) + } + + domainModel := convertToDomainModel(p) + updated, err := s.service.UpdateTemplate(ctx, info.OrgID, domainModel) + if err != nil { + return nil, false, err + } + + r := convertToK8sResource(info.OrgID, updated, s.namespacer) + return r, false, nil +} + +// GracefulDeleter +func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + old, err := s.Get(ctx, name, nil) + if err != nil { + return old, false, err + } + version := "" + if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil { + version = *options.Preconditions.ResourceVersion + } + if deleteValidation != nil { + if err = deleteValidation(ctx, old); err != nil { + return nil, false, err + } + } + err = s.service.DeleteTemplate(ctx, info.OrgID, name, definitions.Provenance(models.ProvenanceNone), version) // TODO add support for dry-run option + return old, false, err // false - will be deleted async +} diff --git a/pkg/registry/apis/alerting/notifications/template_group/storage.go b/pkg/registry/apis/alerting/notifications/template_group/storage.go new file mode 100644 index 00000000000..e1f6a9f6886 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/template_group/storage.go @@ -0,0 +1,81 @@ +package template_group + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + apistore "k8s.io/apiserver/pkg/storage" + + model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" +) + +var _ grafanarest.Storage = (*storage)(nil) + +type storage struct { + *genericregistry.Store +} + +func (s storage) Compare(storageObj, legacyObj runtime.Object) bool { + // TODO implement when supported dual write mode is not Mode0 + return false +} + +func NewStorage( + legacySvc TemplateService, + namespacer request.NamespaceMapper, + scheme *runtime.Scheme, + optsGetter generic.RESTOptionsGetter, + dualWriteBuilder grafanarest.DualWriteBuilder, +) (rest.Storage, error) { + legacyStore := &legacyStorage{ + service: legacySvc, + namespacer: namespacer, + tableConverter: resourceInfo.TableConverter(), + } + if optsGetter != nil && dualWriteBuilder != nil { + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) + s := &genericregistry.Store{ + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, + KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()), + KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()), + PredicateFunc: Matcher, + DefaultQualifiedResource: resourceInfo.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), + TableConvertor: legacyStore.tableConverter, + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + } + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs} + if err := s.CompleteWithOptions(options); err != nil { + return nil, err + } + return dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, storage{Store: s}) + } + return legacyStore, nil +} + +func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { + if s, ok := obj.(*model.TemplateGroup); ok { + return s.Labels, model.SelectableTemplateGroupFields(s), nil + } + return nil, nil, fmt.Errorf("object of type %T is not supported", obj) +} + +// Matcher returns a generic.SelectionPredicate that matches on label and field selectors. +func Matcher(label labels.Selector, field fields.Selector) apistore.SelectionPredicate { + return apistore.SelectionPredicate{ + Label: label, + Field: field, + GetAttrs: GetAttrs, + } +} diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 4ab28eba0b3..83e7b390f73 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -434,10 +434,15 @@ const ( ActionAlertingSilencesCreate = "alert.silences:create" ActionAlertingSilencesWrite = "alert.silences:write" - // Alerting Notification policies actions + // Alerting Notification actions (legacy) ActionAlertingNotificationsRead = "alert.notifications:read" ActionAlertingNotificationsWrite = "alert.notifications:write" + // Alerting notifications template actions + ActionAlertingNotificationsTemplatesRead = "alert.notifications.templates:read" + ActionAlertingNotificationsTemplatesWrite = "alert.notifications.templates:write" + ActionAlertingNotificationsTemplatesDelete = "alert.notifications.templates:delete" + // Alerting notifications time interval actions ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read" ActionAlertingNotificationsTimeIntervalsWrite = "alert.notifications.time-intervals:write" diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index 53c4ba79c5f..1aa3eb8f4c5 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -153,13 +153,38 @@ var ( }, } + templatesReaderRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.templates:reader", + DisplayName: "Templates Reader", + Description: "Read all templates in Grafana alerting", + Group: AlertRolesGroup, + Permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingNotificationsTemplatesRead}, + }, + }, + } + + templatesWriterRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.templates:writer", + DisplayName: "Templates Writer", + Description: "Create, update, and delete all templates in Grafana alerting", + Group: AlertRolesGroup, + Permissions: accesscontrol.ConcatPermissions(templatesReaderRole.Role.Permissions, []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingNotificationsTemplatesWrite}, + {Action: accesscontrol.ActionAlertingNotificationsTemplatesDelete}, + }), + }, + } + notificationsReaderRole = accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ Name: accesscontrol.FixedRolePrefix + "alerting.notifications:reader", DisplayName: "Notifications Reader", Description: "Read notification policies and contact points in Grafana and external providers", Group: AlertRolesGroup, - Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, []accesscontrol.Permission{ + Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, templatesReaderRole.Role.Permissions, []accesscontrol.Permission{ { Action: accesscontrol.ActionAlertingNotificationsRead, }, @@ -180,7 +205,7 @@ var ( DisplayName: "Notifications Writer", Description: "Add, update, and delete contact points and notification policies in Grafana and external providers", Group: AlertRolesGroup, - Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, []accesscontrol.Permission{ + Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, templatesWriterRole.Role.Permissions, []accesscontrol.Permission{ { Action: accesscontrol.ActionAlertingNotificationsWrite, }, @@ -312,7 +337,7 @@ func DeclareFixedRoles(service accesscontrol.Service, features featuremgmt.Featu } if features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { - fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole) + fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole) } return service.DeclareFixedRoles(fixedRoles...) diff --git a/pkg/tests/apis/alerting/notifications/template_group/templates_group_test.go b/pkg/tests/apis/alerting/notifications/template_group/templates_group_test.go new file mode 100644 index 00000000000..6a942075285 --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/template_group/templates_group_test.go @@ -0,0 +1,629 @@ +package templateGroup + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/generated/clientset/versioned" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" + "github.com/grafana/grafana/pkg/util" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func getTestHelper(t *testing.T) *apis.K8sTestHelper { + return apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + EnableFeatureToggles: []string{ + featuremgmt.FlagAlertingApiServer, + }, + }) +} + +func TestIntegrationResourceIdentifier(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + client := adminK8sClient.NotificationsV0alpha1().TemplateGroups("default") + + newTemplate := &v0alpha1.TemplateGroup{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TemplateGroupSpec{ + Title: "templateGroup", + Content: `{{ define "test" }} test {{ end }}`, + }, + } + + t.Run("create should fail if object name is specified", func(t *testing.T) { + template := newTemplate.DeepCopy() + template.Name = "new-templateGroup" + _, err := client.Create(ctx, template, v1.CreateOptions{}) + assert.Error(t, err) + require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err) + }) + + var resourceID string + t.Run("create should succeed and provide resource name", func(t *testing.T) { + actual, err := client.Create(ctx, newTemplate, v1.CreateOptions{}) + require.NoError(t, err) + require.NotEmptyf(t, actual.Name, "Resource name should not be empty") + require.NotEmptyf(t, actual.UID, "Resource UID should not be empty") + resourceID = actual.Name + }) + + var existingTemplateGroup *v0alpha1.TemplateGroup + t.Run("resource should be available by the identifier", func(t *testing.T) { + actual, err := client.Get(ctx, resourceID, v1.GetOptions{}) + require.NoError(t, err) + require.NotEmptyf(t, actual.Name, "Resource name should not be empty") + require.Equal(t, newTemplate.Spec, actual.Spec) + existingTemplateGroup = actual + }) + + t.Run("update should rename template if name in the specification changes", func(t *testing.T) { + if existingTemplateGroup == nil { + t.Skip() + } + updated := existingTemplateGroup.DeepCopy() + updated.Spec.Title = "another-templateGroup" + actual, err := client.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.Equal(t, updated.Spec, actual.Spec) + require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't") + + resource, err := client.Get(ctx, actual.Name, v1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, actual, resource) + }) +} + +func TestIntegrationAccessControl(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + org1 := helper.Org1 + + type testCase struct { + user apis.User + canRead bool + canUpdate bool + canCreate bool + canDelete bool + } + + reader := helper.CreateUser("TemplatesReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingNotificationsTemplatesRead, + }, + }, + }) + writer := helper.CreateUser("TemplatesWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingNotificationsTemplatesRead, + accesscontrol.ActionAlertingNotificationsTemplatesWrite, + }, + }, + }) + + deleter := helper.CreateUser("TemplatesDeleter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingNotificationsTemplatesRead, + accesscontrol.ActionAlertingNotificationsTemplatesDelete, + }, + }, + }) + + testCases := []testCase{ + { + user: org1.Admin, + canRead: true, + canUpdate: true, + canCreate: true, + canDelete: true, + }, + { + user: org1.Editor, + canRead: true, + canUpdate: true, + canCreate: true, + canDelete: true, + }, + { + user: org1.Viewer, + canRead: true, + }, + { + user: reader, + canRead: true, + }, + { + user: writer, + canRead: true, + canCreate: true, + canUpdate: true, + }, + { + user: deleter, + canRead: true, + canDelete: true, + }, + } + + admin := org1.Admin + adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TemplateGroups("default") + + for _, tc := range testCases { + t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) { + k8sClient, err := versioned.NewForConfig(tc.user.NewRestConfig()) + require.NoError(t, err) + client := k8sClient.NotificationsV0alpha1().TemplateGroups("default") + + var expected = &v0alpha1.TemplateGroup{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TemplateGroupSpec{ + Title: fmt.Sprintf("template-group-1-%s", tc.user.Identity.GetLogin()), + Content: `{{ define "test" }} test {{ end }}`, + }, + } + expected.SetProvenanceStatus("") + d, err := json.Marshal(expected) + require.NoError(t, err) + + if tc.canCreate { + t.Run("should be able to create template group", func(t *testing.T) { + actual, err := client.Create(ctx, expected, v1.CreateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + require.Equal(t, expected.Spec, actual.Spec) + + t.Run("should fail if already exists", func(t *testing.T) { + _, err := client.Create(ctx, actual, v1.CreateOptions{}) + require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err) + }) + + expected = actual + }) + } else { + t.Run("should be forbidden to create", func(t *testing.T) { + _, err := client.Create(ctx, expected, v1.CreateOptions{}) + require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d)) + }) + + // create resource to proceed with other tests + expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + require.NotNil(t, expected) + } + + if tc.canRead { + t.Run("should be able to list template groups", func(t *testing.T) { + list, err := client.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + require.Len(t, list.Items, 1) + }) + + t.Run("should be able to read template group by resource identifier", func(t *testing.T) { + got, err := client.Get(ctx, expected.Name, v1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, expected, got) + + t.Run("should get NotFound if resource does not exist", func(t *testing.T) { + _, err := client.Get(ctx, "Notfound", v1.GetOptions{}) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to list template groups", func(t *testing.T) { + _, err := client.List(ctx, v1.ListOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + + t.Run("should be forbidden to read template group by name", func(t *testing.T) { + _, err := client.Get(ctx, expected.Name, v1.GetOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should get forbidden even if name does not exist", func(t *testing.T) { + _, err := client.Get(ctx, "Notfound", v1.GetOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + } + + updatedExpected := expected.DeepCopy() + updatedExpected.Spec.Content = `{{ define "another-test" }} test {{ end }}` + + d, err = json.Marshal(updatedExpected) + require.NoError(t, err) + + if tc.canUpdate { + t.Run("should be able to update template group", func(t *testing.T) { + updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + + expected = updated + + t.Run("should get NotFound if name does not exist", func(t *testing.T) { + up := updatedExpected.DeepCopy() + up.Name = "notFound" + _, err := client.Update(ctx, up, v1.UpdateOptions{}) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to update template group", func(t *testing.T) { + _, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should get forbidden even if resource does not exist", func(t *testing.T) { + up := updatedExpected.DeepCopy() + up.Name = "notFound" + _, err := client.Update(ctx, up, v1.UpdateOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + } + + deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}} + + if tc.canDelete { + t.Run("should be able to delete template group", func(t *testing.T) { + err := client.Delete(ctx, expected.Name, deleteOptions) + require.NoError(t, err) + + t.Run("should get NotFound if name does not exist", func(t *testing.T) { + err := client.Delete(ctx, "notfound", v1.DeleteOptions{}) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to delete template group", func(t *testing.T) { + err := client.Delete(ctx, expected.Name, deleteOptions) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should be forbidden even if resource does not exist", func(t *testing.T) { + err := client.Delete(ctx, "notfound", v1.DeleteOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{})) + } + + if tc.canRead { + t.Run("should get empty list if no mute timings", func(t *testing.T) { + list, err := client.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + require.Len(t, list.Items, 0) + }) + } + }) + } +} + +func TestIntegrationProvisioning(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + org := helper.Org1 + + admin := org.Admin + adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TemplateGroups("default") + + env := helper.GetEnv() + ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + require.NoError(t, err) + + created, err := adminClient.Create(ctx, &v0alpha1.TemplateGroup{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TemplateGroupSpec{ + Title: "template-group-1", + Content: `{{ define "test" }} test {{ end }}`, + }, + }, v1.CreateOptions{}) + require.NoError(t, err) + require.Equal(t, "none", created.GetProvenanceStatus()) + + t.Run("should provide provenance status", func(t *testing.T) { + require.NoError(t, db.SetProvenance(ctx, &definitions.NotificationTemplate{ + Name: created.Spec.Title, + }, admin.Identity.GetOrgID(), "API")) + + got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "API", got.GetProvenanceStatus()) + }) + t.Run("should not let update if provisioned", func(t *testing.T) { + updated := created.DeepCopy() + updated.Spec.Content = `{{ define "another-test" }} test {{ end }}` + + _, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + + t.Run("should not let delete if provisioned", func(t *testing.T) { + err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) +} + +func TestIntegrationOptimisticConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TemplateGroups("default") + + template := v0alpha1.TemplateGroup{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TemplateGroupSpec{ + Title: "template-group-1", + Content: `{{ define "test" }} test {{ end }}`, + }, + } + + created, err := adminClient.Create(ctx, &template, v1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, created) + require.NotEmpty(t, created.ResourceVersion) + + t.Run("should forbid if version does not match", func(t *testing.T) { + updated := created.DeepCopy() + updated.ResourceVersion = "test" + _, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) + }) + t.Run("should update if version matches", func(t *testing.T) { + updated := created.DeepCopy() + updated.Spec.Content = `{{ define "test-another" }} test {{ end }}` + actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.EqualValues(t, updated.Spec, actualUpdated.Spec) + require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion) + }) + t.Run("should update if version is empty", func(t *testing.T) { + updated := created.DeepCopy() + updated.ResourceVersion = "" + updated.Spec.Content = `{{ define "test-another-2" }} test {{ end }}` + + actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.EqualValues(t, updated.Spec, actualUpdated.Spec) + require.NotEqual(t, created.ResourceVersion, actualUpdated.ResourceVersion) + }) + t.Run("should fail to delete if version does not match", func(t *testing.T) { + actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) + require.NoError(t, err) + + err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ + Preconditions: &v1.Preconditions{ + ResourceVersion: util.Pointer("something"), + }, + }) + require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) + }) + t.Run("should succeed if version matches", func(t *testing.T) { + actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) + require.NoError(t, err) + + err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ + Preconditions: &v1.Preconditions{ + ResourceVersion: util.Pointer(actual.ResourceVersion), + }, + }) + require.NoError(t, err) + }) + t.Run("should succeed if version is empty", func(t *testing.T) { + actual, err := adminClient.Create(ctx, &template, v1.CreateOptions{}) + require.NoError(t, err) + + err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ + Preconditions: &v1.Preconditions{ + ResourceVersion: util.Pointer(actual.ResourceVersion), + }, + }) + require.NoError(t, err) + }) +} + +func TestIntegrationPatch(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TemplateGroups("default") + + template := v0alpha1.TemplateGroup{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TemplateGroupSpec{ + Title: "template-group", + Content: `{{ define "test" }} test {{ end }}`, + }, + } + + current, err := adminClient.Create(ctx, &template, v1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, current) + require.NotEmpty(t, current.ResourceVersion) + + t.Run("should patch with merge patch", func(t *testing.T) { + patch := `{ + "spec": { + "content" : "{{ define \"test-another\" }} test {{ end }}" + } + }` + + result, err := adminClient.Patch(ctx, current.Name, types.MergePatchType, []byte(patch), v1.PatchOptions{}) + require.NoError(t, err) + require.Equal(t, `{{ define "test-another" }} test {{ end }}`, result.Spec.Content) + current = result + }) + + t.Run("should patch with json patch", func(t *testing.T) { + expected := `{{ define "test-json-patch" }} test {{ end }}` + + patch := []map[string]interface{}{ + { + "op": "replace", + "path": "/spec/content", + "value": expected, + }, + } + + patchData, err := json.Marshal(patch) + require.NoError(t, err) + + result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{}) + require.NoError(t, err) + expectedSpec := *current.Spec.DeepCopy() + expectedSpec.Content = expected + require.EqualValues(t, expectedSpec, result.Spec) + current = result + }) +} + +func TestIntegrationListSelector(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TemplateGroups("default") + + template1 := &v0alpha1.TemplateGroup{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TemplateGroupSpec{ + Title: "test1", + Content: `{{ define "test1" }} test {{ end }}`, + }, + } + template1, err = adminClient.Create(ctx, template1, v1.CreateOptions{}) + require.NoError(t, err) + + template2 := &v0alpha1.TemplateGroup{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TemplateGroupSpec{ + Title: "test2", + Content: `{{ define "test2" }} test {{ end }}`, + }, + } + template2, err = adminClient.Create(ctx, template2, v1.CreateOptions{}) + require.NoError(t, err) + env := helper.GetEnv() + ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + require.NoError(t, err) + require.NoError(t, db.SetProvenance(ctx, &definitions.NotificationTemplate{ + Name: template2.Spec.Title, + }, helper.Org1.Admin.Identity.GetOrgID(), "API")) + template2, err = adminClient.Get(ctx, template2.Name, v1.GetOptions{}) + + require.NoError(t, err) + + templates, err := adminClient.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + require.Len(t, templates.Items, 2) + + t.Run("should filter by template name", func(t *testing.T) { + list, err := adminClient.List(ctx, v1.ListOptions{ + FieldSelector: "spec.title=" + template1.Spec.Title, + }) + require.NoError(t, err) + require.Len(t, list.Items, 1) + require.Equal(t, template1.Name, list.Items[0].Name) + }) + + t.Run("should filter by template metadata name", func(t *testing.T) { + list, err := adminClient.List(ctx, v1.ListOptions{ + FieldSelector: "metadata.name=" + template2.Name, + }) + require.NoError(t, err) + require.Len(t, list.Items, 1) + require.Equal(t, template2.Name, list.Items[0].Name) + }) + + t.Run("should filter by multiple filters", func(t *testing.T) { + list, err := adminClient.List(ctx, v1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s,metadata.provenance=%s", template2.Name, "API"), + }) + require.NoError(t, err) + require.Len(t, list.Items, 1) + require.Equal(t, template2.Name, list.Items[0].Name) + }) + + t.Run("should be empty when filter does not match", func(t *testing.T) { + list, err := adminClient.List(ctx, v1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s,metadata.provenance=%s", template2.Name, "unknown"), + }) + require.NoError(t, err) + require.Empty(t, list.Items) + }) +}