Alerting: Notifications Templates API (#91349)

pull/93741/head
Yuri Tseretyan 9 months ago committed by GitHub
parent 5472478ee8
commit 10582e48f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 25
      apps/alerting/notifications/template.cue
  2. 47
      pkg/apis/alerting_notifications/v0alpha1/register.go
  3. 8
      pkg/apis/alerting_notifications/v0alpha1/templategroup_spec.go
  4. 91
      pkg/apis/alerting_notifications/v0alpha1/types.go
  5. 21
      pkg/apis/alerting_notifications/v0alpha1/types_ext.go
  6. 76
      pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go
  7. 138
      pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go
  8. 202
      pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/templategroup.go
  9. 34
      pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/templategroupspec.go
  10. 4
      pkg/generated/applyconfiguration/utils.go
  11. 5
      pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go
  12. 4
      pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go
  13. 146
      pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_templategroup.go
  14. 2
      pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go
  15. 55
      pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/templategroup.go
  16. 7
      pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go
  17. 76
      pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/templategroup.go
  18. 2
      pkg/generated/informers/externalversions/generic.go
  19. 8
      pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go
  20. 56
      pkg/generated/listers/alerting_notifications/v0alpha1/templategroup.go
  21. 14
      pkg/registry/apis/alerting/notifications/register.go
  22. 54
      pkg/registry/apis/alerting/notifications/template_group/authorize.go
  23. 52
      pkg/registry/apis/alerting/notifications/template_group/conversions.go
  24. 187
      pkg/registry/apis/alerting/notifications/template_group/legacy_storage.go
  25. 81
      pkg/registry/apis/alerting/notifications/template_group/storage.go
  26. 7
      pkg/services/accesscontrol/models.go
  27. 31
      pkg/services/ngalert/accesscontrol.go
  28. 629
      pkg/tests/apis/alerting/notifications/template_group/templates_group_test.go

@ -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
}
}
}
}
}

@ -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()

@ -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"`
}

@ -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

@ -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
}

@ -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

@ -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{

@ -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
}

@ -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
}

@ -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"):

@ -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)
}

@ -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}
}

@ -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
}

@ -6,4 +6,6 @@ package v0alpha1
type ReceiverExpansion interface{}
type TemplateGroupExpansion interface{}
type TimeIntervalExpansion interface{}

@ -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{} }),
}
}

@ -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}

@ -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())
}

@ -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

@ -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{}

@ -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]
}

@ -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:

@ -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
}

@ -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),
}
}

@ -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
}

@ -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,
}
}

@ -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"

@ -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...)

@ -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)
})
}
Loading…
Cancel
Save