Compare commits

...

9 Commits

Author SHA1 Message Date
Gilles De Mey d75506cafe move integrations to separate integrations property 2025-04-30 11:28:00 -04:00
Yuri Tseretyan 927654a65d redact secrets 2025-04-30 11:27:59 -04:00
Yuri Tseretyan 5dcd241577 temporary disable internal 2025-04-30 11:27:59 -04:00
Yuri Tseretyan 4e2f5436f8 remove unused 2025-04-30 11:27:58 -04:00
Yuri Tseretyan 46843cb335 implement v0alpha2 2025-04-30 11:27:58 -04:00
Yuri Tseretyan b80901cce0 move to v0alpha1 2025-04-30 11:27:57 -04:00
Yuri Tseretyan 6ee94ef94e add ext.go 2025-04-30 11:27:57 -04:00
Yuri Tseretyan 6e0d8fd426 generate openapi 2025-04-30 11:27:56 -04:00
Yuri Tseretyan 335de805b7 add v2 integrations 2025-04-30 11:27:55 -04:00
39 changed files with 8258 additions and 1371 deletions
@@ -2,6 +2,7 @@ package kinds
import (
"github.com/grafana/grafana/apps/alerting/notifications/kinds/v0alpha1"
"github.com/grafana/grafana/apps/alerting/notifications/kinds/v0alpha2"
)
receiver: {
@@ -24,5 +25,13 @@ receiver: {
"spec.title",
]
}
"v0alpha2": {
schema: {
spec: v0alpha2.ReceiverSpec
}
selectableFields: [
"spec.title",
]
}
}
}
@@ -0,0 +1,332 @@
package v0alpha2
BaseIntegration: {
uid?: string
disable_resolve_message?: bool
}
#SecretString: string
#RedactedSecret: {
specified: bool
}
// A string that contain sensitive information.
#Secret: string
AlertmanagerIntegration: BaseIntegration & {
url: string
basicAuthUser?: string
basicAuthPassword?: #Secret
}
DingdingIntegration: BaseIntegration & {
url?: string
msgType?: string
title?: string
message?: string
}
DiscordIntegration: BaseIntegration & {
url: #Secret
title?: string
message?: string
avatar_url?: string
use_discord_username?: bool
}
EmailIntegration: BaseIntegration & {
addresses: [...string]
singleEmail?: bool
message?: string
subject?: string
}
GooglechatIntegration: BaseIntegration & {
url: #Secret
title?: string
message?: string
}
JiraIntegration: BaseIntegration & {
api_url: string
project: string
issue_type: string
summary?: string
description?: string
labels?: [...string]
priority?: string
reopen_transition?: string
resolve_transition?: string
wont_fix_resolution?: string
reopen_duration?: string
dedup_key_field?: string
fields?: {}
user?: #Secret
password?: #Secret
api_token?: #Secret
}
KafkaIntegration: BaseIntegration & {
kafkaRestProxy: #Secret
kafkaTopic: string
description?: string
details?: string
username?: string
password?: #Secret
apiVersion?: string
kafkaClusterId?: string
}
LineIntegration: BaseIntegration & {
token: #Secret
title?: string
description?: string
}
#TLSConfig: {
insecureSkipVerify?: bool
caCertificate?: #Secret
clientCertificate?: #Secret
clientKey?: #Secret
}
MqttIntegration: BaseIntegration & {
brokerUrl?: string
clientId?: string
topic?: string
message?: string
messageFormat?: string
username?: string
password?: #Secret
qos?: int64
retain?: bool
tlsConfig?: #TLSConfig
}
OnCallIntegration: BaseIntegration & {
url: string
httpMethod?: string
maxAlerts?: int64
authorization_scheme?: string
authorization_credentials?: #Secret
username?: string
password?: #Secret
title?: string
message?: string
}
OpsgenieIntegrationResponder: {
id?: string
name?: string
username?: string
type: string
}
OpsgenieIntegration: BaseIntegration & {
apiKey: #Secret
apiUrl?: string
message?: string
description?: string
autoClose?: bool
overridePriority?: bool
sendTagsAs?: string
responders?: [...OpsgenieIntegrationResponder]
}
PagerdutyIntegration: BaseIntegration & {
integrationKey: #Secret
severity?: string
class?: string
component?: string
group?: string
summary?: string
source?: string
client?: string
client_url?: string
details?: {[string]: string}
url?: string
}
PushoverIntegration: BaseIntegration & {
userKey: #Secret
apiToken: #Secret
priority?: int64
okPriority?: int64
retry?: int64
expire?: int64
device?: string
sound?: string
okSound?: string
title?: string
message?: string
uploadImage?: bool
}
SensugoIntegration: BaseIntegration & {
url: string
apikey: #Secret
entity?: string
check?: string
namespace?: string
handler?: string
message?: string
}
#SigV4Config: {
region?: string
access_key?: #Secret
secret_key?: #Secret
profile?: string
role_arn?: string
}
SnsIntegration: BaseIntegration & {
api_url?: string
sigv4: #SigV4Config
topic_arn?: string
phone_number?: string
target_arn?: string
subject?: string
message?: string
attributes?: {[string]: string}
}
SlackIntegration: BaseIntegration & {
endpointUrl?: string
url?: #Secret
token?: #Secret
recipient?: string
text?: string
title?: string
username?: string
icon_emoji?: string
icon_url?: string
mentionChannel?: string
mentionUsers?: string
mentionGroups?: string
color?: string
}
TelegramIntegration: BaseIntegration & {
bottoken: #Secret
chatid: string
message_thread_id: string
message?: string
parse_mode?: string
disable_web_page_preview?: bool
protect_content?: bool
disable_notifications?: bool
}
TeamsIntegration: BaseIntegration & {
url: #Secret
message?: string
title?: string
sectiontitle?: string
}
ThreemaIntegration: BaseIntegration & {
gateway_id: string
recipient_id: string
api_secret: #Secret
title?: string
description?: string
}
VictoropsIntegration: BaseIntegration & {
url: #Secret
messageType?: string
title?: string
description?: string
}
WebexIntegration: BaseIntegration & {
bot_token: #Secret
api_url?: string
message?: string
room_id?: string
}
#CustomPayload: {
template?: string
vars?: {[string]: string}
}
#HMACConfig: {
secret?: #Secret
header: string
timestampHeader: string
}
WebhookIntegration: BaseIntegration & {
url: string
httpMethod?: string
maxAlerts?: int64
authorization_scheme?: string
authorization_credentials?: #Secret
username?: string
password?: #Secret
headers?: {[string]: string}
title?: string
message?: string
tlsConfig?: #TLSConfig
hmacConfig?: #HMACConfig
payload?: #CustomPayload
}
WecomIntegration: BaseIntegration & {
endpointUrl?: string
url?: #Secret
secret?: #Secret
agent_id?: string
corp_id?: string
message?: string
title?: string
msgtype?: string
touser?: string
}
ReceiverSpec: {
title: string
integrations: {
alertmanager?: [...AlertmanagerIntegration]
dingding?: [...DingdingIntegration]
discord?: [...DiscordIntegration]
email?: [...EmailIntegration]
googlechat?: [...GooglechatIntegration]
jira?: [...JiraIntegration]
kafka?: [...KafkaIntegration]
line?: [...LineIntegration]
mqtt?: [...MqttIntegration]
opsgenie?: [...OpsgenieIntegration]
pagerduty?: [...PagerdutyIntegration]
oncall?: [...OnCallIntegration]
pushover?: [...PushoverIntegration]
sensugo?: [...SensugoIntegration]
slack?: [...SlackIntegration]
sns?: [...SnsIntegration]
teams?: [...TeamsIntegration]
telegram?: [...TelegramIntegration]
threema?: [...ThreemaIntegration]
victorops?: [...VictoropsIntegration]
webhook?: [...WebhookIntegration]
wecom?: [...WecomIntegration]
webex?: [...WebexIntegration]
}
}
@@ -28,6 +28,13 @@ var appManifestData = app.ManifestData{
"spec.title",
},
},
{
Name: "v0alpha2",
SelectableFields: []string{
"spec.title",
},
},
},
},
@@ -0,0 +1,18 @@
package v0alpha2
import "k8s.io/apimachinery/pkg/runtime/schema"
const (
// Group is the API group used by all kinds in this package
Group = "notifications.alerting.grafana.app"
// Version is the API version used by all kinds in this package
Version = "v0alpha2"
)
var (
// GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package
GroupVersion = schema.GroupVersion{
Group: Group,
Version: Version,
}
)
@@ -0,0 +1,139 @@
package v0alpha2
import (
"fmt"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apiserver/pkg/registry/generic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1"
)
func (o *Receiver) GetProvenanceStatus() string {
if o == nil || o.Annotations == nil {
return ""
}
s, ok := o.Annotations[v0alpha1.ProvenanceStatusAnnotationKey]
if !ok || s == "" {
return v0alpha1.ProvenanceStatusNone
}
return s
}
func (o *Receiver) SetProvenanceStatus(status string) {
if o.Annotations == nil {
o.Annotations = make(map[string]string, 1)
}
if status == "" {
status = v0alpha1.ProvenanceStatusNone
}
o.Annotations[v0alpha1.ProvenanceStatusAnnotationKey] = status
}
func (o *Receiver) SetAccessControl(action string) {
if o.Annotations == nil {
o.Annotations = make(map[string]string, 1)
}
o.Annotations[AccessControlAnnotation(action)] = "true"
}
// AccessControlAnnotation returns the key for the access control annotation for the given action.
// Ex. grafana.com/access/canDelete.
func AccessControlAnnotation(action string) string {
return fmt.Sprintf("%s%s/%s", v0alpha1.InternalPrefix, "access", action)
}
func (o *Receiver) SetInUse(routesCnt int, rules []string) {
if o.Annotations == nil {
o.Annotations = make(map[string]string, 2)
}
o.Annotations[InUseAnnotation("routes")] = fmt.Sprintf("%d", routesCnt)
o.Annotations[InUseAnnotation("rules")] = fmt.Sprintf("%d", len(rules))
}
func (s *Spec) GetIntegrationsTypes() []string {
response := make([]string, 0, 5)
if len(s.Integrations.Alertmanager) > 0 {
response = append(response, "alertmanager")
}
if len(s.Integrations.Discord) > 0 {
response = append(response, "discord")
}
if len(s.Integrations.Email) > 0 {
response = append(response, "email")
}
if len(s.Integrations.Googlechat) > 0 {
response = append(response, "googlechat")
}
if len(s.Integrations.Kafka) > 0 {
response = append(response, "kafka")
}
if len(s.Integrations.Line) > 0 {
response = append(response, "line")
}
if len(s.Integrations.Opsgenie) > 0 {
response = append(response, "opsgenie")
}
if len(s.Integrations.Pagerduty) > 0 {
response = append(response, "pagerduty")
}
if len(s.Integrations.Pushover) > 0 {
response = append(response, "pushover")
}
if len(s.Integrations.Sensugo) > 0 {
response = append(response, "sensu")
}
if len(s.Integrations.Slack) > 0 {
response = append(response, "slack")
}
if len(s.Integrations.Teams) > 0 {
response = append(response, "teams")
}
if len(s.Integrations.Telegram) > 0 {
response = append(response, "telegram")
}
if len(s.Integrations.Threema) > 0 {
response = append(response, "threema")
}
if len(s.Integrations.Victorops) > 0 {
response = append(response, "victorops")
}
if len(s.Integrations.Webhook) > 0 {
response = append(response, "webhook")
}
if len(s.Integrations.Wecom) > 0 {
response = append(response, "wecom")
}
return response
}
func (s *Spec) IntegrationsCount() int {
return len(s.Integrations.Alertmanager) + len(s.Integrations.Dingding) + len(s.Integrations.Discord) + len(s.Integrations.Email) +
len(s.Integrations.Googlechat) + len(s.Integrations.Kafka) + len(s.Integrations.Line) + len(s.Integrations.Opsgenie) +
len(s.Integrations.Pagerduty) + len(s.Integrations.Oncall) + len(s.Integrations.Pushover) + len(s.Integrations.Sensugo) +
len(s.Integrations.Sns) + len(s.Integrations.Slack) + len(s.Integrations.Teams) + len(s.Integrations.Telegram) +
len(s.Integrations.Threema) + len(s.Integrations.Victorops) + len(s.Integrations.Webhook) + len(s.Integrations.Wecom) +
len(s.Integrations.Webex) + len(s.Integrations.Mqtt)
}
// InUseAnnotation returns the key for the in-use annotation for the given resource.
// Ex. grafana.com/inUse/routes, grafana.com/inUse/rules.
func InUseAnnotation(resource string) string {
return fmt.Sprintf("%s%s/%s", v0alpha1.InternalPrefix, "inUse", resource)
}
func SelectableFields(obj *Receiver) fields.Set {
if obj == nil {
return nil
}
selectable := Schema().SelectableFields()
set := make(fields.Set, len(selectable))
for _, field := range selectable {
f, err := field.FieldValueFunc(obj)
if err != nil {
continue
}
set[field.FieldSelector] = f
}
return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), set)
}
@@ -0,0 +1,28 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha2
import (
"encoding/json"
"io"
"github.com/grafana/grafana-app-sdk/resource"
)
// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type JSONCodec struct{}
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
func (*JSONCodec) Read(reader io.Reader, into resource.Object) error {
return json.NewDecoder(reader).Decode(into)
}
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
func (*JSONCodec) Write(writer io.Writer, from resource.Object) error {
return json.NewEncoder(writer).Encode(from)
}
// Interface compliance checks
var _ resource.Codec = &JSONCodec{}
@@ -0,0 +1,28 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha2
import (
time "time"
)
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
type Metadata struct {
UpdateTimestamp time.Time `json:"updateTimestamp"`
CreatedBy string `json:"createdBy"`
Uid string `json:"uid"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
ResourceVersion string `json:"resourceVersion"`
Generation int64 `json:"generation"`
UpdatedBy string `json:"updatedBy"`
Labels map[string]string `json:"labels"`
}
// NewMetadata creates a new Metadata object.
func NewMetadata() *Metadata {
return &Metadata{}
}
@@ -0,0 +1,319 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha2
import (
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"time"
)
// +k8s:openapi-gen=true
type Receiver struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the Receiver
Spec Spec `json:"spec" yaml:"spec"`
Status Status `json:"status" yaml:"status"`
}
func (o *Receiver) GetSpec() any {
return o.Spec
}
func (o *Receiver) SetSpec(spec any) error {
cast, ok := spec.(Spec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
o.Spec = cast
return nil
}
func (o *Receiver) GetSubresources() map[string]any {
return map[string]any{
"status": o.Status,
}
}
func (o *Receiver) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.Status, true
default:
return nil, false
}
}
func (o *Receiver) SetSubresource(name string, value any) error {
switch name {
case "status":
cast, ok := value.(Status)
if !ok {
return fmt.Errorf("cannot set status type %#v, not of type Status", value)
}
o.Status = cast
return nil
default:
return fmt.Errorf("subresource '%s' does not exist", name)
}
}
func (o *Receiver) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
Namespace: o.ObjectMeta.Namespace,
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
}
}
func (o *Receiver) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
Group: metadata.Group,
Version: metadata.Version,
Kind: metadata.Kind,
})
}
func (o *Receiver) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
deletionTimestamp = &dt.Time
}
// Legacy ExtraFields support
extraFields := make(map[string]any)
if o.Annotations != nil {
extraFields["annotations"] = o.Annotations
}
if o.ManagedFields != nil {
extraFields["managedFields"] = o.ManagedFields
}
if o.OwnerReferences != nil {
extraFields["ownerReferences"] = o.OwnerReferences
}
return resource.CommonMetadata{
UID: string(o.UID),
ResourceVersion: o.ResourceVersion,
Generation: o.Generation,
Labels: o.Labels,
CreationTimestamp: o.CreationTimestamp.Time,
DeletionTimestamp: deletionTimestamp,
Finalizers: o.Finalizers,
UpdateTimestamp: o.GetUpdateTimestamp(),
CreatedBy: o.GetCreatedBy(),
UpdatedBy: o.GetUpdatedBy(),
ExtraFields: extraFields,
}
}
func (o *Receiver) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
o.Labels = metadata.Labels
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
if metadata.DeletionTimestamp != nil {
dt := metav1.NewTime(*metadata.DeletionTimestamp)
o.DeletionTimestamp = &dt
} else {
o.DeletionTimestamp = nil
}
o.Finalizers = metadata.Finalizers
if o.Annotations == nil {
o.Annotations = make(map[string]string)
}
if !metadata.UpdateTimestamp.IsZero() {
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
}
if metadata.CreatedBy != "" {
o.SetCreatedBy(metadata.CreatedBy)
}
if metadata.UpdatedBy != "" {
o.SetUpdatedBy(metadata.UpdatedBy)
}
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
if metadata.ExtraFields != nil {
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
if cast, ok := annotations.(map[string]string); ok {
o.Annotations = cast
}
}
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
o.ManagedFields = cast
}
}
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
o.OwnerReferences = cast
}
}
}
}
func (o *Receiver) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *Receiver) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *Receiver) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
return parsed
}
func (o *Receiver) 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 *Receiver) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *Receiver) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *Receiver) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *Receiver) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *Receiver) DeepCopy() *Receiver {
cpy := &Receiver{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *Receiver) DeepCopyInto(dst *Receiver) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
o.Status.DeepCopyInto(&dst.Status)
}
// Interface compliance compile-time check
var _ resource.Object = &Receiver{}
// +k8s:openapi-gen=true
type ReceiverList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []Receiver `json:"items" yaml:"items"`
}
func (o *ReceiverList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *ReceiverList) Copy() resource.ListObject {
cpy := &ReceiverList{
TypeMeta: o.TypeMeta,
Items: make([]Receiver, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*Receiver); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *ReceiverList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
}
return items
}
func (o *ReceiverList) SetItems(items []resource.Object) {
o.Items = make([]Receiver, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*Receiver)
}
}
func (o *ReceiverList) DeepCopy() *ReceiverList {
cpy := &ReceiverList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *ReceiverList) DeepCopyInto(dst *ReceiverList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &ReceiverList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *Spec) DeepCopy() *Spec {
cpy := &Spec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *Spec) DeepCopyInto(dst *Spec) {
resource.CopyObjectInto(dst, s)
}
// DeepCopy creates a full deep copy of Status
func (s *Status) DeepCopy() *Status {
cpy := &Status{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Status into another Status object
func (s *Status) DeepCopyInto(dst *Status) {
resource.CopyObjectInto(dst, s)
}
@@ -0,0 +1,46 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha2
import (
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha2", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"),
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{
FieldSelector: "spec.title",
FieldValueFunc: func(o resource.Object) (string, error) {
cast, ok := o.(*Receiver)
if !ok {
return "", fmt.Errorf("provided object must be of type *Receiver")
}
return cast.Spec.Title, nil
},
},
}))
kindReceiver = resource.Kind{
Schema: schemaReceiver,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &JSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func Kind() resource.Kind {
return kindReceiver
}
// Schema returns a resource.SimpleSchema representation of Receiver
func Schema() *resource.SimpleSchema {
return schemaReceiver
}
// Interface compliance checks
var _ resource.Schema = kindReceiver
@@ -0,0 +1,541 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha2
// +k8s:openapi-gen=true
type AlertmanagerIntegration struct {
Uid *string `json:"uid,omitempty"`
Url string `json:"url"`
BasicAuthUser *string `json:"basicAuthUser,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
BasicAuthPassword *Secret `json:"basicAuthPassword,omitempty"`
}
// NewAlertmanagerIntegration creates a new AlertmanagerIntegration object.
func NewAlertmanagerIntegration() *AlertmanagerIntegration {
return &AlertmanagerIntegration{}
}
// A string that contain sensitive information.
// +k8s:openapi-gen=true
type Secret string
// +k8s:openapi-gen=true
type DingdingIntegration struct {
Uid *string `json:"uid,omitempty"`
Url *string `json:"url,omitempty"`
MsgType *string `json:"msgType,omitempty"`
Title *string `json:"title,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Message *string `json:"message,omitempty"`
}
// NewDingdingIntegration creates a new DingdingIntegration object.
func NewDingdingIntegration() *DingdingIntegration {
return &DingdingIntegration{}
}
// +k8s:openapi-gen=true
type DiscordIntegration struct {
Uid *string `json:"uid,omitempty"`
Url Secret `json:"url"`
Title *string `json:"title,omitempty"`
Message *string `json:"message,omitempty"`
AvatarUrl *string `json:"avatar_url,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
UseDiscordUsername *bool `json:"use_discord_username,omitempty"`
}
// NewDiscordIntegration creates a new DiscordIntegration object.
func NewDiscordIntegration() *DiscordIntegration {
return &DiscordIntegration{}
}
// +k8s:openapi-gen=true
type EmailIntegration struct {
Uid *string `json:"uid,omitempty"`
Addresses []string `json:"addresses"`
SingleEmail *bool `json:"singleEmail,omitempty"`
Message *string `json:"message,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Subject *string `json:"subject,omitempty"`
}
// NewEmailIntegration creates a new EmailIntegration object.
func NewEmailIntegration() *EmailIntegration {
return &EmailIntegration{}
}
// +k8s:openapi-gen=true
type GooglechatIntegration struct {
Uid *string `json:"uid,omitempty"`
Url Secret `json:"url"`
Title *string `json:"title,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Message *string `json:"message,omitempty"`
}
// NewGooglechatIntegration creates a new GooglechatIntegration object.
func NewGooglechatIntegration() *GooglechatIntegration {
return &GooglechatIntegration{}
}
// +k8s:openapi-gen=true
type JiraIntegration struct {
Uid *string `json:"uid,omitempty"`
ApiUrl string `json:"api_url"`
Project string `json:"project"`
IssueType string `json:"issue_type"`
Summary *string `json:"summary,omitempty"`
Description *string `json:"description,omitempty"`
Labels []string `json:"labels,omitempty"`
Priority *string `json:"priority,omitempty"`
ReopenTransition *string `json:"reopen_transition,omitempty"`
ResolveTransition *string `json:"resolve_transition,omitempty"`
WontFixResolution *string `json:"wont_fix_resolution,omitempty"`
ReopenDuration *string `json:"reopen_duration,omitempty"`
DedupKeyField *string `json:"dedup_key_field,omitempty"`
Fields interface{} `json:"fields,omitempty"`
User *Secret `json:"user,omitempty"`
Password *Secret `json:"password,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
ApiToken *Secret `json:"api_token,omitempty"`
}
// NewJiraIntegration creates a new JiraIntegration object.
func NewJiraIntegration() *JiraIntegration {
return &JiraIntegration{}
}
// +k8s:openapi-gen=true
type KafkaIntegration struct {
Uid *string `json:"uid,omitempty"`
KafkaRestProxy Secret `json:"kafkaRestProxy"`
KafkaTopic string `json:"kafkaTopic"`
Description *string `json:"description,omitempty"`
Details *string `json:"details,omitempty"`
Username *string `json:"username,omitempty"`
Password *Secret `json:"password,omitempty"`
ApiVersion *string `json:"apiVersion,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
KafkaClusterId *string `json:"kafkaClusterId,omitempty"`
}
// NewKafkaIntegration creates a new KafkaIntegration object.
func NewKafkaIntegration() *KafkaIntegration {
return &KafkaIntegration{}
}
// +k8s:openapi-gen=true
type LineIntegration struct {
Uid *string `json:"uid,omitempty"`
Token Secret `json:"token"`
Title *string `json:"title,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Description *string `json:"description,omitempty"`
}
// NewLineIntegration creates a new LineIntegration object.
func NewLineIntegration() *LineIntegration {
return &LineIntegration{}
}
// +k8s:openapi-gen=true
type MqttIntegration struct {
Uid *string `json:"uid,omitempty"`
BrokerUrl *string `json:"brokerUrl,omitempty"`
ClientId *string `json:"clientId,omitempty"`
Topic *string `json:"topic,omitempty"`
Message *string `json:"message,omitempty"`
MessageFormat *string `json:"messageFormat,omitempty"`
Username *string `json:"username,omitempty"`
Password *Secret `json:"password,omitempty"`
Qos *int64 `json:"qos,omitempty"`
Retain *bool `json:"retain,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
TlsConfig *TLSConfig `json:"tlsConfig,omitempty"`
}
// NewMqttIntegration creates a new MqttIntegration object.
func NewMqttIntegration() *MqttIntegration {
return &MqttIntegration{}
}
// +k8s:openapi-gen=true
type TLSConfig struct {
InsecureSkipVerify *bool `json:"insecureSkipVerify,omitempty"`
CaCertificate *Secret `json:"caCertificate,omitempty"`
ClientCertificate *Secret `json:"clientCertificate,omitempty"`
ClientKey *Secret `json:"clientKey,omitempty"`
}
// NewTLSConfig creates a new TLSConfig object.
func NewTLSConfig() *TLSConfig {
return &TLSConfig{}
}
// +k8s:openapi-gen=true
type OpsgenieIntegration struct {
Uid *string `json:"uid,omitempty"`
ApiKey Secret `json:"apiKey"`
ApiUrl *string `json:"apiUrl,omitempty"`
Message *string `json:"message,omitempty"`
Description *string `json:"description,omitempty"`
AutoClose *bool `json:"autoClose,omitempty"`
OverridePriority *bool `json:"overridePriority,omitempty"`
SendTagsAs *string `json:"sendTagsAs,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Responders []OpsgenieIntegrationResponder `json:"responders,omitempty"`
}
// NewOpsgenieIntegration creates a new OpsgenieIntegration object.
func NewOpsgenieIntegration() *OpsgenieIntegration {
return &OpsgenieIntegration{}
}
// +k8s:openapi-gen=true
type OpsgenieIntegrationResponder struct {
Id *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Username *string `json:"username,omitempty"`
Type string `json:"type"`
}
// NewOpsgenieIntegrationResponder creates a new OpsgenieIntegrationResponder object.
func NewOpsgenieIntegrationResponder() *OpsgenieIntegrationResponder {
return &OpsgenieIntegrationResponder{}
}
// +k8s:openapi-gen=true
type PagerdutyIntegration struct {
Uid *string `json:"uid,omitempty"`
IntegrationKey Secret `json:"integrationKey"`
Severity *string `json:"severity,omitempty"`
Class *string `json:"class,omitempty"`
Component *string `json:"component,omitempty"`
Group *string `json:"group,omitempty"`
Summary *string `json:"summary,omitempty"`
Source *string `json:"source,omitempty"`
Client *string `json:"client,omitempty"`
ClientUrl *string `json:"client_url,omitempty"`
Details map[string]string `json:"details,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Url *string `json:"url,omitempty"`
}
// NewPagerdutyIntegration creates a new PagerdutyIntegration object.
func NewPagerdutyIntegration() *PagerdutyIntegration {
return &PagerdutyIntegration{}
}
// +k8s:openapi-gen=true
type OnCallIntegration struct {
Uid *string `json:"uid,omitempty"`
Url string `json:"url"`
HttpMethod *string `json:"httpMethod,omitempty"`
MaxAlerts *int64 `json:"maxAlerts,omitempty"`
AuthorizationScheme *string `json:"authorization_scheme,omitempty"`
AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty"`
Username *string `json:"username,omitempty"`
Password *Secret `json:"password,omitempty"`
Title *string `json:"title,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Message *string `json:"message,omitempty"`
}
// NewOnCallIntegration creates a new OnCallIntegration object.
func NewOnCallIntegration() *OnCallIntegration {
return &OnCallIntegration{}
}
// +k8s:openapi-gen=true
type PushoverIntegration struct {
Uid *string `json:"uid,omitempty"`
UserKey Secret `json:"userKey"`
ApiToken Secret `json:"apiToken"`
Priority *int64 `json:"priority,omitempty"`
OkPriority *int64 `json:"okPriority,omitempty"`
Retry *int64 `json:"retry,omitempty"`
Expire *int64 `json:"expire,omitempty"`
Device *string `json:"device,omitempty"`
Sound *string `json:"sound,omitempty"`
OkSound *string `json:"okSound,omitempty"`
Title *string `json:"title,omitempty"`
Message *string `json:"message,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
UploadImage *bool `json:"uploadImage,omitempty"`
}
// NewPushoverIntegration creates a new PushoverIntegration object.
func NewPushoverIntegration() *PushoverIntegration {
return &PushoverIntegration{}
}
// +k8s:openapi-gen=true
type SensugoIntegration struct {
Uid *string `json:"uid,omitempty"`
Url string `json:"url"`
Apikey Secret `json:"apikey"`
Entity *string `json:"entity,omitempty"`
Check *string `json:"check,omitempty"`
Namespace *string `json:"namespace,omitempty"`
Handler *string `json:"handler,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Message *string `json:"message,omitempty"`
}
// NewSensugoIntegration creates a new SensugoIntegration object.
func NewSensugoIntegration() *SensugoIntegration {
return &SensugoIntegration{}
}
// +k8s:openapi-gen=true
type SlackIntegration struct {
Uid *string `json:"uid,omitempty"`
EndpointUrl *string `json:"endpointUrl,omitempty"`
Url *Secret `json:"url,omitempty"`
Token *Secret `json:"token,omitempty"`
Recipient *string `json:"recipient,omitempty"`
Text *string `json:"text,omitempty"`
Title *string `json:"title,omitempty"`
Username *string `json:"username,omitempty"`
IconEmoji *string `json:"icon_emoji,omitempty"`
IconUrl *string `json:"icon_url,omitempty"`
MentionChannel *string `json:"mentionChannel,omitempty"`
MentionUsers *string `json:"mentionUsers,omitempty"`
MentionGroups *string `json:"mentionGroups,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Color *string `json:"color,omitempty"`
}
// NewSlackIntegration creates a new SlackIntegration object.
func NewSlackIntegration() *SlackIntegration {
return &SlackIntegration{}
}
// +k8s:openapi-gen=true
type SnsIntegration struct {
Uid *string `json:"uid,omitempty"`
ApiUrl *string `json:"api_url,omitempty"`
Sigv4 SigV4Config `json:"sigv4"`
TopicArn *string `json:"topic_arn,omitempty"`
PhoneNumber *string `json:"phone_number,omitempty"`
TargetArn *string `json:"target_arn,omitempty"`
Subject *string `json:"subject,omitempty"`
Message *string `json:"message,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
}
// NewSnsIntegration creates a new SnsIntegration object.
func NewSnsIntegration() *SnsIntegration {
return &SnsIntegration{
Sigv4: *NewSigV4Config(),
}
}
// +k8s:openapi-gen=true
type SigV4Config struct {
Region *string `json:"region,omitempty"`
AccessKey *Secret `json:"access_key,omitempty"`
SecretKey *Secret `json:"secret_key,omitempty"`
Profile *string `json:"profile,omitempty"`
RoleArn *string `json:"role_arn,omitempty"`
}
// NewSigV4Config creates a new SigV4Config object.
func NewSigV4Config() *SigV4Config {
return &SigV4Config{}
}
// +k8s:openapi-gen=true
type TeamsIntegration struct {
Uid *string `json:"uid,omitempty"`
Url Secret `json:"url"`
Message *string `json:"message,omitempty"`
Title *string `json:"title,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Sectiontitle *string `json:"sectiontitle,omitempty"`
}
// NewTeamsIntegration creates a new TeamsIntegration object.
func NewTeamsIntegration() *TeamsIntegration {
return &TeamsIntegration{}
}
// +k8s:openapi-gen=true
type TelegramIntegration struct {
Uid *string `json:"uid,omitempty"`
Bottoken Secret `json:"bottoken"`
Chatid string `json:"chatid"`
MessageThreadId string `json:"message_thread_id"`
Message *string `json:"message,omitempty"`
ParseMode *string `json:"parse_mode,omitempty"`
DisableWebPagePreview *bool `json:"disable_web_page_preview,omitempty"`
ProtectContent *bool `json:"protect_content,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
DisableNotifications *bool `json:"disable_notifications,omitempty"`
}
// NewTelegramIntegration creates a new TelegramIntegration object.
func NewTelegramIntegration() *TelegramIntegration {
return &TelegramIntegration{}
}
// +k8s:openapi-gen=true
type ThreemaIntegration struct {
Uid *string `json:"uid,omitempty"`
GatewayId string `json:"gateway_id"`
RecipientId string `json:"recipient_id"`
ApiSecret Secret `json:"api_secret"`
Title *string `json:"title,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Description *string `json:"description,omitempty"`
}
// NewThreemaIntegration creates a new ThreemaIntegration object.
func NewThreemaIntegration() *ThreemaIntegration {
return &ThreemaIntegration{}
}
// +k8s:openapi-gen=true
type VictoropsIntegration struct {
Uid *string `json:"uid,omitempty"`
Url Secret `json:"url"`
MessageType *string `json:"messageType,omitempty"`
Title *string `json:"title,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Description *string `json:"description,omitempty"`
}
// NewVictoropsIntegration creates a new VictoropsIntegration object.
func NewVictoropsIntegration() *VictoropsIntegration {
return &VictoropsIntegration{}
}
// +k8s:openapi-gen=true
type WebhookIntegration struct {
Uid *string `json:"uid,omitempty"`
Url string `json:"url"`
HttpMethod *string `json:"httpMethod,omitempty"`
MaxAlerts *int64 `json:"maxAlerts,omitempty"`
AuthorizationScheme *string `json:"authorization_scheme,omitempty"`
AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty"`
Username *string `json:"username,omitempty"`
Password *Secret `json:"password,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Title *string `json:"title,omitempty"`
Message *string `json:"message,omitempty"`
TlsConfig *TLSConfig `json:"tlsConfig,omitempty"`
HmacConfig *HMACConfig `json:"hmacConfig,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Payload *CustomPayload `json:"payload,omitempty"`
}
// NewWebhookIntegration creates a new WebhookIntegration object.
func NewWebhookIntegration() *WebhookIntegration {
return &WebhookIntegration{}
}
// +k8s:openapi-gen=true
type HMACConfig struct {
Secret *Secret `json:"secret,omitempty"`
Header string `json:"header"`
TimestampHeader string `json:"timestampHeader"`
}
// NewHMACConfig creates a new HMACConfig object.
func NewHMACConfig() *HMACConfig {
return &HMACConfig{}
}
// +k8s:openapi-gen=true
type CustomPayload struct {
Template *string `json:"template,omitempty"`
Vars map[string]string `json:"vars,omitempty"`
}
// NewCustomPayload creates a new CustomPayload object.
func NewCustomPayload() *CustomPayload {
return &CustomPayload{}
}
// +k8s:openapi-gen=true
type WecomIntegration struct {
Uid *string `json:"uid,omitempty"`
EndpointUrl *string `json:"endpointUrl,omitempty"`
Url *Secret `json:"url,omitempty"`
Secret *Secret `json:"secret,omitempty"`
AgentId *string `json:"agent_id,omitempty"`
CorpId *string `json:"corp_id,omitempty"`
Message *string `json:"message,omitempty"`
Title *string `json:"title,omitempty"`
Msgtype *string `json:"msgtype,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
Touser *string `json:"touser,omitempty"`
}
// NewWecomIntegration creates a new WecomIntegration object.
func NewWecomIntegration() *WecomIntegration {
return &WecomIntegration{}
}
// +k8s:openapi-gen=true
type WebexIntegration struct {
Uid *string `json:"uid,omitempty"`
BotToken Secret `json:"bot_token"`
ApiUrl *string `json:"api_url,omitempty"`
Message *string `json:"message,omitempty"`
DisableResolveMessage *bool `json:"disable_resolve_message,omitempty"`
RoomId *string `json:"room_id,omitempty"`
}
// NewWebexIntegration creates a new WebexIntegration object.
func NewWebexIntegration() *WebexIntegration {
return &WebexIntegration{}
}
// +k8s:openapi-gen=true
type Spec struct {
Title string `json:"title"`
Integrations V0alpha2SpecIntegrations `json:"integrations"`
}
// NewSpec creates a new Spec object.
func NewSpec() *Spec {
return &Spec{
Integrations: *NewV0alpha2SpecIntegrations(),
}
}
// +k8s:openapi-gen=true
type V0alpha2SpecIntegrations struct {
Alertmanager []AlertmanagerIntegration `json:"alertmanager,omitempty"`
Dingding []DingdingIntegration `json:"dingding,omitempty"`
Discord []DiscordIntegration `json:"discord,omitempty"`
Email []EmailIntegration `json:"email,omitempty"`
Googlechat []GooglechatIntegration `json:"googlechat,omitempty"`
Jira []JiraIntegration `json:"jira,omitempty"`
Kafka []KafkaIntegration `json:"kafka,omitempty"`
Line []LineIntegration `json:"line,omitempty"`
Mqtt []MqttIntegration `json:"mqtt,omitempty"`
Opsgenie []OpsgenieIntegration `json:"opsgenie,omitempty"`
Pagerduty []PagerdutyIntegration `json:"pagerduty,omitempty"`
Oncall []OnCallIntegration `json:"oncall,omitempty"`
Pushover []PushoverIntegration `json:"pushover,omitempty"`
Sensugo []SensugoIntegration `json:"sensugo,omitempty"`
Slack []SlackIntegration `json:"slack,omitempty"`
Sns []SnsIntegration `json:"sns,omitempty"`
Teams []TeamsIntegration `json:"teams,omitempty"`
Telegram []TelegramIntegration `json:"telegram,omitempty"`
Threema []ThreemaIntegration `json:"threema,omitempty"`
Victorops []VictoropsIntegration `json:"victorops,omitempty"`
Webhook []WebhookIntegration `json:"webhook,omitempty"`
Wecom []WecomIntegration `json:"wecom,omitempty"`
Webex []WebexIntegration `json:"webex,omitempty"`
}
// NewV0alpha2SpecIntegrations creates a new V0alpha2SpecIntegrations object.
func NewV0alpha2SpecIntegrations() *V0alpha2SpecIntegrations {
return &V0alpha2SpecIntegrations{}
}
@@ -0,0 +1,44 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha2
// +k8s:openapi-gen=true
type StatusOperatorState struct {
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State StatusOperatorStateState `json:"state"`
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]interface{} `json:"details,omitempty"`
}
// NewStatusOperatorState creates a new StatusOperatorState object.
func NewStatusOperatorState() *StatusOperatorState {
return &StatusOperatorState{}
}
// +k8s:openapi-gen=true
type Status struct {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"`
// additionalFields is reserved for future use
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
}
// NewStatus creates a new Status object.
func NewStatus() *Status {
return &Status{}
}
// +k8s:openapi-gen=true
type StatusOperatorStateState string
const (
StatusOperatorStateStateSuccess StatusOperatorStateState = "success"
StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress"
StatusOperatorStateStateFailed StatusOperatorStateState = "failed"
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,86 @@
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,EmailIntegration,Addresses
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,Labels
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,OpsgenieIntegration,Responders
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Alertmanager
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Dingding
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Discord
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Email
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Googlechat
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Jira
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Kafka
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Line
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Mqtt
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Oncall
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Opsgenie
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Pagerduty
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Pushover
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Sensugo
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Slack
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Sns
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Teams
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Telegram
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Threema
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Victorops
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Webex
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Webhook
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,V0alpha2SpecIntegrations,Wecom
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,AlertmanagerIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,DingdingIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,DiscordIntegration,AvatarUrl
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,DiscordIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,DiscordIntegration,UseDiscordUsername
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,EmailIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,GooglechatIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,ApiToken
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,ApiUrl
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,DedupKeyField
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,IssueType
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,ReopenDuration
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,ReopenTransition
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,ResolveTransition
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,JiraIntegration,WontFixResolution
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,KafkaIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,LineIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,MqttIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,OnCallIntegration,AuthorizationCredentials
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,OnCallIntegration,AuthorizationScheme
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,OnCallIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,OpsgenieIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,PagerdutyIntegration,ClientUrl
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,PagerdutyIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,PushoverIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SensugoIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SigV4Config,AccessKey
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SigV4Config,RoleArn
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SigV4Config,SecretKey
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SlackIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SlackIntegration,IconEmoji
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SlackIntegration,IconUrl
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SnsIntegration,ApiUrl
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SnsIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SnsIntegration,PhoneNumber
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SnsIntegration,TargetArn
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,SnsIntegration,TopicArn
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,TeamsIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,TelegramIntegration,DisableNotifications
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,TelegramIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,TelegramIntegration,DisableWebPagePreview
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,TelegramIntegration,MessageThreadId
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,TelegramIntegration,ParseMode
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,TelegramIntegration,ProtectContent
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,ThreemaIntegration,ApiSecret
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,ThreemaIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,ThreemaIntegration,GatewayId
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,ThreemaIntegration,RecipientId
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,VictoropsIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WebexIntegration,ApiUrl
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WebexIntegration,BotToken
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WebexIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WebexIntegration,RoomId
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WebhookIntegration,AuthorizationCredentials
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WebhookIntegration,AuthorizationScheme
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WebhookIntegration,DisableResolveMessage
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WecomIntegration,AgentId
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WecomIntegration,CorpId
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2,WecomIntegration,DisableResolveMessage
+9 -3
View File
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1"
receiverv0alpha1 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha1"
receiverv0alpha2 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2"
routingtreev0alpha1 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1"
templategroupv0alpha1 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/templategroup/v0alpha1"
timeintervalv0alpha1 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/timeinterval/v0alpha1"
@@ -17,12 +18,14 @@ import (
func GetOpenAPIDefinitions(c common.ReferenceCallback) map[string]common.OpenAPIDefinition {
tmpl := templategroupv0alpha1.GetOpenAPIDefinitions(c)
tin := timeintervalv0alpha1.GetOpenAPIDefinitions(c)
recv := receiverv0alpha1.GetOpenAPIDefinitions(c)
recv1 := receiverv0alpha1.GetOpenAPIDefinitions(c)
recv2 := receiverv0alpha2.GetOpenAPIDefinitions(c)
rest := routingtreev0alpha1.GetOpenAPIDefinitions(c)
result := make(map[string]common.OpenAPIDefinition, len(tmpl)+len(tin)+len(recv)+len(rest))
result := make(map[string]common.OpenAPIDefinition, len(tmpl)+len(tin)+len(recv1)+len(rest)+len(recv2))
maps.Copy(result, tmpl)
maps.Copy(result, tin)
maps.Copy(result, recv)
maps.Copy(result, recv1)
maps.Copy(result, recv2)
maps.Copy(result, rest)
return result
}
@@ -35,6 +38,9 @@ func GetKinds() map[schema.GroupVersion][]sdkResource.Kind {
templategroupv0alpha1.Kind(),
timeintervalv0alpha1.Kind(),
},
receiverv0alpha2.GroupVersion: {
receiverv0alpha2.Kind(),
},
}
return result
}
+2
View File
@@ -11,6 +11,8 @@ import (
"github.com/grafana/grafana-app-sdk/app"
)
var ()
var appManifestData = app.ManifestData{
AppName: "folder",
Group: "folder.grafana.app",
+1 -1
View File
@@ -9,7 +9,7 @@ import { resolve } from 'node:path';
// these snapshots are generated by running "go test pkg/tests/apis/openapi_test.go", see the README in the "openapi_snapshots" directory
const OPENAPI_SCHEMA_LOCATION = resolve(
'../../../pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json'
'../../../pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha2.json'
);
export default {
File diff suppressed because it is too large Load Diff
@@ -1,19 +1,9 @@
import { fetchBaseQuery, TypedUseQueryHookResult } from '@reduxjs/toolkit/query/react';
import { config } from '@grafana/runtime';
import { alertingAPI, ListReceiverApiArg } from '../../api.gen';
import { EnhancedListReceiverResponse } from '../types';
import { alertingAPI } from '../../api.gen';
const { namespace } = config;
// this is a workaround for the fact that the generated types are not narrow enough
type EnhancedHookResult = TypedUseQueryHookResult<
EnhancedListReceiverResponse,
ListReceiverApiArg,
ReturnType<typeof fetchBaseQuery>
>;
/**
* useListContactPoints is a hook that fetches a list of contact points
*
@@ -23,7 +13,7 @@ type EnhancedHookResult = TypedUseQueryHookResult<
* It automatically uses the configured namespace for the query.
*/
function useListContactPoints() {
return alertingAPI.useListReceiverQuery<EnhancedHookResult>({ namespace });
return alertingAPI.useListReceiverQuery({ namespace });
}
export { useListContactPoints };
@@ -1,76 +1,6 @@
/**
* This file contains enhanced and type-narrowed versions of the types generated by the RTKQ codegen package.
*/
import { MergeDeep, MergeExclusive, OverrideProperties } from 'type-fest';
import { ComGithubGrafanaGrafanaAppsAlertingNotificationsPkgApisReceiverV0Alpha2Receiver } from '../api.gen';
import {
ComGithubGrafanaGrafanaAppsAlertingNotificationsPkgApisReceiverV0Alpha1Receiver as ContactPointV0Alpha1,
ComGithubGrafanaGrafanaAppsAlertingNotificationsPkgApisReceiverV0Alpha1Integration as IntegrationV0Alpha1,
ListReceiverApiResponse,
} from '../api.gen';
type GenericIntegration = OverrideProperties<
IntegrationV0Alpha1,
{
settings: Record<string, unknown>;
}
>;
// Based on https://github.com/grafana/alerting/blob/main/receivers/email/config.go#L20-L25
type EmailIntegration = OverrideProperties<
GenericIntegration,
{
type: 'email';
settings: {
singleEmail?: boolean;
addresses: string;
message?: string;
subject?: string;
};
secureFields: never; // email doesn't have any secure fields
}
>;
// Based on https://github.com/grafana/alerting/blob/main/receivers/slack/config.go
type SlackIntegration = OverrideProperties<
GenericIntegration,
{
type: 'slack';
settings: {
endpointUrl?: string;
url?: string;
recipient?: string;
text?: string;
title?: string;
username?: string;
icon_emoji?: string;
icon_url?: string;
mentionChannel?: string;
mentionUsers?: string; // comma separated string
mentionGroups?: string; // comma separated string
color?: string;
};
// secureFields is a union type that can be either a token or a URL but you can't have both
secureFields: MergeExclusive<{ token: string }, { url: string }>;
}
>;
export type Integration = EmailIntegration | SlackIntegration | GenericIntegration;
// Enhanced version of ContactPoint with typed integrations
// ⚠️ MergeDeep does not check if the property you are overriding exists in the base type and there is no "DeepOverrideProperties" helper
export type ContactPoint = MergeDeep<
ContactPointV0Alpha1,
{
spec: {
integrations: Integration[];
};
}
>;
export type EnhancedListReceiverResponse = OverrideProperties<
ListReceiverApiResponse,
{
items: ContactPoint[];
}
>;
export type ContactPoint = ComGithubGrafanaGrafanaAppsAlertingNotificationsPkgApisReceiverV0Alpha2Receiver;
@@ -1,4 +1,4 @@
import { countBy, isEmpty } from 'lodash';
import { isEmpty, mapValues } from 'lodash';
import { ContactPoint } from './types';
@@ -13,12 +13,15 @@ import { ContactPoint } from './types';
* @returns A string description of the ContactPoint's integrations
*/
export function getContactPointDescription(contactPoint: ContactPoint): string {
if (isEmpty(contactPoint.spec.integrations)) {
const integrations = contactPoint.spec.integrations;
const isEmptyContactPoint = Object.values(integrations).every(isEmpty);
if (isEmptyContactPoint) {
return '<empty contact point>';
}
// Count the occurrences of each integration type
const integrationCounts = countBy(contactPoint.spec.integrations, (integration) => integration.type);
const integrationCounts = mapValues(integrations, (integrations) => integrations?.length ?? 0);
const description = Object.entries(integrationCounts)
.map(([type, count]) => {
@@ -9,6 +9,8 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver/v0alpha2"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
)
@@ -22,7 +24,7 @@ type AccessControlService interface {
}
func Authorize(ctx context.Context, ac AccessControlService, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if attr.GetResource() != ResourceInfo.GroupResource().Resource {
if attr.GetResource() != v0alpha1.ResourceInfo.GroupResource().Resource && attr.GetResource() != v0alpha2.ResourceInfo.GroupResource().Resource {
return authorizer.DecisionNoOpinion, "", nil
}
user, err := identity.GetRequester(ctx)
@@ -1,4 +1,4 @@
package receiver
package v0alpha1
import (
"fmt"
@@ -1,4 +1,4 @@
package receiver
package v0alpha1
import (
"context"
@@ -76,9 +76,9 @@ func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOpti
q := ngmodels.GetReceiversQuery{
OrgID: orgId,
Decrypt: false,
//Names: ctx.QueryStrings("names"), // TODO: Query params.
//Limit: ctx.QueryInt("limit"),
//Offset: ctx.QueryInt("offset"),
// Names: ctx.QueryStrings("names"), // TODO: Query params.
// Limit: ctx.QueryInt("limit"),
// Offset: ctx.QueryInt("offset"),
}
user, err := identity.GetRequester(ctx)
@@ -1,4 +1,4 @@
package receiver
package v0alpha1
import (
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
@@ -1,4 +1,4 @@
package receiver
package v0alpha1
import (
"fmt"
@@ -0,0 +1,615 @@
package v0alpha2
import (
"errors"
"fmt"
"strings"
"unsafe"
"github.com/grafana/alerting/receivers"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
func convertToK8sResources(
orgID int64,
receivers []*ngmodels.Receiver,
accesses map[string]ngmodels.ReceiverPermissionSet,
metadatas map[string]ngmodels.ReceiverMetadata,
namespacer request.NamespaceMapper,
selector fields.Selector,
) (*model.ReceiverList, error) {
result := &model.ReceiverList{
Items: make([]model.Receiver, 0, len(receivers)),
}
for _, receiver := range receivers {
var access *ngmodels.ReceiverPermissionSet
if accesses != nil {
if a, ok := accesses[receiver.GetUID()]; ok {
access = &a
}
}
var metadata *ngmodels.ReceiverMetadata
if metadatas != nil {
if m, ok := metadatas[receiver.GetUID()]; ok {
metadata = &m
}
}
k8sResource, err := convertToK8sResource(orgID, receiver, access, metadata, namespacer, false)
if err != nil {
return nil, err
}
if selector != nil && !selector.Empty() && !selector.Matches(model.SelectableFields(k8sResource)) {
continue
}
result.Items = append(result.Items, *k8sResource)
}
return result, nil
}
func convertToK8sResource(
orgID int64,
receiver *ngmodels.Receiver,
access *ngmodels.ReceiverPermissionSet,
metadata *ngmodels.ReceiverMetadata,
namespacer request.NamespaceMapper,
keepSecrets bool,
) (*model.Receiver, error) {
spec, err := specFromDomainReceiver(receiver, keepSecrets)
if err != nil {
return nil, err
}
r := &model.Receiver{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID(receiver.GetUID()), // This is needed to make PATCH work
Name: receiver.GetUID(),
Namespace: namespacer(orgID),
ResourceVersion: receiver.Version,
},
Spec: spec,
}
r.SetProvenanceStatus(string(receiver.Provenance))
if access != nil {
for _, action := range ngmodels.ReceiverPermissions() {
mappedAction, ok := permissionMapper[action]
if !ok {
return nil, fmt.Errorf("unknown action %v", action)
}
if can, _ := access.Has(action); can {
r.SetAccessControl(mappedAction)
}
}
}
if metadata != nil {
rules := make([]string, 0, len(metadata.InUseByRules))
for _, rule := range metadata.InUseByRules {
rules = append(rules, rule.UID)
}
r.SetInUse(metadata.InUseByRoutes, rules)
}
r.UID = gapiutil.CalculateClusterWideUID(r)
return r, nil
}
var permissionMapper = map[ngmodels.ReceiverPermission]string{
ngmodels.ReceiverPermissionReadSecret: "canReadSecrets",
ngmodels.ReceiverPermissionAdmin: "canAdmin",
ngmodels.ReceiverPermissionWrite: "canWrite",
ngmodels.ReceiverPermissionDelete: "canDelete",
}
// ContactPointFromContactPointExport parses the database model of the contact point (group of integrations) where settings are represented in JSON,
// to strongly typed ContactPoint.
func specFromDomainReceiver(domain *ngmodels.Receiver, secrets bool) (model.Spec, error) {
j := jsoniter.ConfigCompatibleWithStandardLibrary
j.RegisterExtension(&contactPointsExtension{
KeepSecret: secrets,
})
result := model.Spec{
Title: domain.Name,
}
var errs []error
for _, rawIntegration := range domain.Integrations {
err := parseIntegration(j, &result, rawIntegration)
if err != nil {
// accumulate errors to report all at once.
errs = append(errs, fmt.Errorf("failed to parse %s integration (uid:%s): %w", rawIntegration.Config.Type, rawIntegration.UID, err))
}
}
return result, errors.Join(errs...)
}
// ContactPointToContactPointExport converts v0alpha2.Receiver to models.Receiver.
// It uses a special extension for json-iterator API that properly handles marshaling of some specific fields.
//
//nolint:gocyclo
func convertToDomainModel(apiModel *model.Receiver) (*ngmodels.Receiver, error) {
j := jsoniter.ConfigCompatibleWithStandardLibrary
// use json iterator with custom extension that has special codec for some field.
// This is needed to keep the API models clean and convert from a database model
j.RegisterExtension(&contactPointsExtension{})
cp := apiModel.Spec
integration := make([]*ngmodels.Integration, 0, apiModel.Spec.IntegrationsCount())
var errs []error
for _, i := range cp.Integrations.Alertmanager {
el, err := marshallIntegration(j, "prometheus-alertmanager", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Dingding {
el, err := marshallIntegration(j, "dingding", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Discord {
el, err := marshallIntegration(j, "discord", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Email {
el, err := marshallIntegration(j, "email", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Googlechat {
el, err := marshallIntegration(j, "googlechat", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Jira {
el, err := marshallIntegration(j, "jira", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Kafka {
el, err := marshallIntegration(j, "kafka", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Line {
el, err := marshallIntegration(j, "line", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Mqtt {
el, err := marshallIntegration(j, "mqtt", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Opsgenie {
el, err := marshallIntegration(j, "opsgenie", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Pagerduty {
el, err := marshallIntegration(j, "pagerduty", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Oncall {
el, err := marshallIntegration(j, "oncall", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Pushover {
el, err := marshallIntegration(j, "pushover", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Sensugo {
el, err := marshallIntegration(j, "sensugo", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Sns {
el, err := marshallIntegration(j, "sns", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Slack {
el, err := marshallIntegration(j, "slack", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Teams {
el, err := marshallIntegration(j, "teams", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Telegram {
el, err := marshallIntegration(j, "telegram", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Threema {
el, err := marshallIntegration(j, "threema", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Victorops {
el, err := marshallIntegration(j, "victorops", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Webhook {
el, err := marshallIntegration(j, "webhook", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Wecom {
el, err := marshallIntegration(j, "wecom", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Integrations.Webex {
el, err := marshallIntegration(j, "webex", i, i.DisableResolveMessage, i.Uid, cp.Title)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
result := &ngmodels.Receiver{
UID: apiModel.Name,
Name: apiModel.Spec.Title,
Integrations: integration,
Provenance: ngmodels.Provenance(apiModel.GetProvenanceStatus()),
Version: apiModel.ResourceVersion,
}
return result, nil
}
// marshallIntegration converts the API model integration to the storage model that contains settings in the JSON format.
// The secret fields are not encrypted.
func marshallIntegration(json jsoniter.API, integrationType string, integration interface{}, disableResolveMessage *bool, uid *string, name string) (*ngmodels.Integration, error) {
data, err := json.Marshal(integration)
if err != nil {
return nil, fmt.Errorf("failed to marshall integration '%s' to JSON: %w", integrationType, err)
}
settings := map[string]interface{}{}
err = json.Unmarshal(data, &settings)
delete(settings, "uid") // integration UID is part of the integration
if err != nil {
return nil, fmt.Errorf("failed to marshall integration '%s' to map: %w", integrationType, err)
}
config, err := ngmodels.IntegrationConfigFromType(integrationType)
if err != nil {
return nil, err
}
e := &ngmodels.Integration{
UID: "",
Name: name,
Config: config,
Settings: settings,
SecureSettings: nil,
}
if uid != nil {
e.UID = *uid
}
if disableResolveMessage != nil {
e.DisableResolveMessage = *disableResolveMessage
}
return e, nil
}
//nolint:gocyclo
func parseIntegration(json jsoniter.API, result *model.Spec, integration *ngmodels.Integration) error {
var err error
var disable *bool
if integration.DisableResolveMessage { // populate only if true
disable = util.Pointer(integration.DisableResolveMessage)
}
data, err := json.Marshal(integration.Settings)
if err != nil {
return fmt.Errorf("failed to marshall integration '%s' to JSON: %w", integration.Config.Type, err)
}
switch strings.ToLower(integration.Config.Type) {
case "prometheus-alertmanager":
integration := model.AlertmanagerIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Alertmanager = append(result.Integrations.Alertmanager, integration)
}
case "dingding":
integration := model.DingdingIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Dingding = append(result.Integrations.Dingding, integration)
}
case "discord":
integration := model.DiscordIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Discord = append(result.Integrations.Discord, integration)
}
case "email":
integration := model.EmailIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Email = append(result.Integrations.Email, integration)
}
case "googlechat":
integration := model.GooglechatIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Googlechat = append(result.Integrations.Googlechat, integration)
}
case "jira":
integration := model.JiraIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Jira = append(result.Integrations.Jira, integration)
}
case "kafka":
integration := model.KafkaIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Kafka = append(result.Integrations.Kafka, integration)
}
case "line":
integration := model.LineIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Line = append(result.Integrations.Line, integration)
}
case "mqtt":
integration := model.MqttIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Mqtt = append(result.Integrations.Mqtt, integration)
}
case "opsgenie":
integration := model.OpsgenieIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Opsgenie = append(result.Integrations.Opsgenie, integration)
}
case "pagerduty":
integration := model.PagerdutyIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Pagerduty = append(result.Integrations.Pagerduty, integration)
}
case "oncall":
integration := model.OnCallIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Oncall = append(result.Integrations.Oncall, integration)
}
case "pushover":
integration := model.PushoverIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Pushover = append(result.Integrations.Pushover, integration)
}
case "sensugo":
integration := model.SensugoIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Sensugo = append(result.Integrations.Sensugo, integration)
}
case "sns":
integration := model.SnsIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Sns = append(result.Integrations.Sns, integration)
}
case "slack":
integration := model.SlackIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Slack = append(result.Integrations.Slack, integration)
}
case "teams":
integration := model.TeamsIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Teams = append(result.Integrations.Teams, integration)
}
case "telegram":
integration := model.TelegramIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Telegram = append(result.Integrations.Telegram, integration)
}
case "threema":
integration := model.ThreemaIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Threema = append(result.Integrations.Threema, integration)
}
case "victorops":
integration := model.VictoropsIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Victorops = append(result.Integrations.Victorops, integration)
}
case "webhook":
integration := model.WebhookIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Webhook = append(result.Integrations.Webhook, integration)
}
case "wecom":
integration := model.WecomIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Wecom = append(result.Integrations.Wecom, integration)
}
case "webex":
integration := model.WebexIntegration{DisableResolveMessage: disable, Uid: util.Pointer(integration.UID)}
if err = json.Unmarshal(data, &integration); err == nil {
result.Integrations.Webex = append(result.Integrations.Webex, integration)
}
default:
err = fmt.Errorf("integration %s is not supported", integration.Config.Type)
}
return err
}
// contactPointsExtension extends jsoniter with special codecs for some integrations' fields that are encoded differently in the legacy configuration.
type contactPointsExtension struct {
jsoniter.DummyExtension
KeepSecret bool
}
func (c *contactPointsExtension) UpdateStructDescriptor(structDescriptor *jsoniter.StructDescriptor) {
if structDescriptor.Type == reflect2.TypeOf(model.EmailIntegration{}) {
bind := structDescriptor.GetField("Addresses")
codec := &emailAddressCodec{}
bind.Decoder = codec
bind.Encoder = codec
}
if structDescriptor.Type == reflect2.TypeOf(model.PushoverIntegration{}) {
codec := &numberAsStringCodec{}
for _, field := range []string{"Priority", "OkPriority"} {
desc := structDescriptor.GetField(field)
desc.Decoder = codec
desc.Encoder = codec
}
// the same logic is in the pushover.NewConfig in alerting module
codec = &numberAsStringCodec{ignoreError: true}
for _, field := range []string{"Retry", "Expire"} {
desc := structDescriptor.GetField(field)
desc.Decoder = codec
desc.Encoder = codec
}
}
if structDescriptor.Type == reflect2.TypeOf(model.WebhookIntegration{}) {
codec := &numberAsStringCodec{ignoreError: true}
desc := structDescriptor.GetField("MaxAlerts")
desc.Decoder = codec
desc.Encoder = codec
}
if structDescriptor.Type == reflect2.TypeOf(model.OnCallIntegration{}) {
codec := &numberAsStringCodec{ignoreError: true}
desc := structDescriptor.GetField("MaxAlerts")
desc.Decoder = codec
desc.Encoder = codec
}
if structDescriptor.Type == reflect2.TypeOf(model.MqttIntegration{}) {
codec := &numberAsStringCodec{ignoreError: true}
desc := structDescriptor.GetField("Qos")
desc.Decoder = codec
desc.Encoder = codec
}
}
type emailAddressCodec struct{}
func (d *emailAddressCodec) IsEmpty(ptr unsafe.Pointer) bool {
f := *(*[]string)(ptr)
return len(f) == 0
}
func (d *emailAddressCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
f := *(*[]string)(ptr)
addresses := strings.Join(f, ";")
stream.WriteString(addresses)
}
func (d *emailAddressCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
s := iter.ReadString()
emails := strings.FieldsFunc(strings.Trim(s, "\""), func(r rune) bool {
switch r {
case ',', ';', '\n':
return true
}
return false
})
*((*[]string)(ptr)) = emails
}
// converts a string representation of a number to *int64
type numberAsStringCodec struct {
ignoreError bool // if true, then ignores the error and keeps value nil
}
func (d *numberAsStringCodec) IsEmpty(ptr unsafe.Pointer) bool {
return *((*(*int))(ptr)) == nil
}
func (d *numberAsStringCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
val := *((*(*int))(ptr))
if val == nil {
stream.WriteNil()
return
}
stream.WriteInt(*val)
}
func (d *numberAsStringCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
valueType := iter.WhatIsNext()
var value int64
switch valueType {
case jsoniter.NumberValue:
value = iter.ReadInt64()
case jsoniter.StringValue:
var num receivers.OptionalNumber
err := num.UnmarshalJSON(iter.ReadStringAsSlice())
if err != nil {
iter.ReportError("numberAsStringCodec", fmt.Sprintf("failed to unmarshall string as OptionalNumber: %s", err.Error()))
}
if num.String() == "" {
return
}
value, err = num.Int64()
if err != nil {
if !d.ignoreError {
iter.ReportError("numberAsStringCodec", fmt.Sprintf("string does not represent an integer number: %s", err.Error()))
}
return
}
case jsoniter.NilValue:
iter.ReadNil()
return
default:
iter.ReportError("numberAsStringCodec", "not number or string")
}
*((*(*int64))(ptr)) = &value
}
@@ -0,0 +1,60 @@
package v0alpha2
import (
"encoding/json"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/grafana/alerting/notify"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
func TestConvertToK8sResource(t *testing.T) {
for integrationType, cfg := range notify.AllKnownConfigsForTesting {
t.Run(integrationType, func(t *testing.T) {
schema, err := models.IntegrationConfigFromType(integrationType)
require.NoError(t, err)
settingsMap := map[string]any{}
require.NoError(t, json.Unmarshal([]byte(cfg.Config), &settingsMap))
recCfg := &models.Receiver{
UID: fmt.Sprintf("uid-%s", integrationType),
Name: fmt.Sprintf("name-%s", integrationType),
Integrations: []*models.Integration{
{
UID: fmt.Sprintf("intuid-%s", integrationType),
Name: fmt.Sprintf("name-%s", integrationType),
Config: schema,
DisableResolveMessage: false,
Settings: settingsMap,
SecureSettings: nil,
},
},
Provenance: "api",
Version: "1234",
}
result, err := convertToK8sResource(1, recCfg, &models.ReceiverPermissionSet{}, &models.ReceiverMetadata{}, request.GetNamespaceMapper(nil), true)
require.NoError(t, err)
back, err := convertToDomainModel(result)
require.NoError(t, err)
switch integrationType {
case "webhook": // webhook has a different format for maxAlerts, in test config it's a string, in API model it's an int.
val := back.Integrations[0].Settings["maxAlerts"].(float64)
back.Integrations[0].Settings["maxAlerts"] = fmt.Sprintf("%d", int(val))
case "mqtt": // same for qos
val := back.Integrations[0].Settings["qos"].(float64)
back.Integrations[0].Settings["qos"] = fmt.Sprintf("%d", int(val))
}
diff := cmp.Diff(recCfg, back)
if len(diff) != 0 {
require.Failf(t, "The re-marshalled configuration does not match the expected one", diff)
}
})
}
}
@@ -0,0 +1,285 @@
package v0alpha2
import (
"context"
"errors"
"fmt"
apierrors "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"
model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2"
"github.com/grafana/grafana/pkg/apimachinery/identity"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
)
var (
_ grafanarest.Storage = (*legacyStorage)(nil)
)
type ReceiverService interface {
GetReceiver(ctx context.Context, q ngmodels.GetReceiverQuery, user identity.Requester) (*ngmodels.Receiver, error)
GetReceivers(ctx context.Context, q ngmodels.GetReceiversQuery, user identity.Requester) ([]*ngmodels.Receiver, error)
CreateReceiver(ctx context.Context, r *ngmodels.Receiver, orgID int64, user identity.Requester) (*ngmodels.Receiver, error)
UpdateReceiver(ctx context.Context, r *ngmodels.Receiver, storedSecureFields map[string][]string, orgID int64, user identity.Requester) (*ngmodels.Receiver, error)
DeleteReceiver(ctx context.Context, name string, provenance definitions.Provenance, version string, orgID int64, user identity.Requester) error
}
type MetadataService interface {
AccessControlMetadata(ctx context.Context, user identity.Requester, receivers ...*ngmodels.Receiver) (map[string]ngmodels.ReceiverPermissionSet, error)
InUseMetadata(ctx context.Context, orgID int64, receivers ...*ngmodels.Receiver) (map[string]ngmodels.ReceiverMetadata, error)
}
type legacyStorage struct {
service ReceiverService
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
metadata MetadataService
}
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
}
q := ngmodels.GetReceiversQuery{
OrgID: orgId,
Decrypt: true,
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
res, err := s.service.GetReceivers(ctx, q, user)
if err != nil {
// This API should not be returning a forbidden error when the user does not have access to any resources.
// This can be true for a contact point creator role, for example.
// This should eventually be changed downstream in the auth logic but provisioning API currently relies on this
// behaviour to return useful forbidden errors when exporting decrypted receivers.
if !errors.Is(err, alertingac.ErrAuthorizationBase) {
return nil, err
}
res = nil
}
accesses, err := s.metadata.AccessControlMetadata(ctx, user, res...)
if err != nil {
return nil, fmt.Errorf("failed to get access control metadata: %w", err)
}
inUses, err := s.metadata.InUseMetadata(ctx, orgId, res...)
if err != nil {
return nil, fmt.Errorf("failed to get in-use metadata: %w", err)
}
return convertToK8sResources(orgId, res, accesses, inUses, s.namespacer, opts.FieldSelector)
}
func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) {
return s.get(ctx, uid, false, true)
}
func (s *legacyStorage) get(ctx context.Context, uid string, keepSecrets bool, fillMetadata bool) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
name, err := legacy_storage.UidToName(uid)
if err != nil {
return nil, apierrors.NewNotFound(ResourceInfo.GroupResource(), uid)
}
q := ngmodels.GetReceiverQuery{
OrgID: info.OrgID,
Name: name,
Decrypt: true,
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
r, err := s.service.GetReceiver(ctx, q, user)
if err != nil {
return nil, err
}
var access *ngmodels.ReceiverPermissionSet
var inUse *ngmodels.ReceiverMetadata
if fillMetadata {
accesses, err := s.metadata.AccessControlMetadata(ctx, user, r)
if err == nil {
if a, ok := accesses[r.GetUID()]; ok {
access = &a
}
} else {
return nil, fmt.Errorf("failed to get access control metadata: %w", err)
}
inUses, err := s.metadata.InUseMetadata(ctx, info.OrgID, r)
if err == nil {
if a, ok := inUses[r.GetUID()]; ok {
inUse = &a
}
} else {
return nil, fmt.Errorf("failed to get access control metadata: %w", err)
}
}
return convertToK8sResource(info.OrgID, r, access, inUse, s.namespacer, keepSecrets)
}
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.(*model.Receiver)
if !ok {
return nil, fmt.Errorf("expected receiver but got %s", obj.GetObjectKind().GroupVersionKind())
}
if p.Name != "" { // TODO remove when metadata.name can be defined by user
return nil, apierrors.NewBadRequest("object's metadata.name should be empty")
}
model, err := convertToDomainModel(p)
if err != nil {
return nil, err
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
out, err := s.service.CreateReceiver(ctx, model, info.OrgID, user)
if err != nil {
return nil, err
}
return convertToK8sResource(info.OrgID, out, nil, nil, s.namespacer, false)
}
func (s *legacyStorage) Update(ctx context.Context,
uid 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
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, false, err
}
old, err := s.get(ctx, uid, true, false)
if err != nil {
return old, false, err
}
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.(*model.Receiver)
if !ok {
return nil, false, fmt.Errorf("expected receiver but got %s", obj.GetObjectKind().GroupVersionKind())
}
model, err := convertToDomainModel(p)
if err != nil {
return old, false, err
}
updated, err := s.service.UpdateReceiver(ctx, model, nil, info.OrgID, user)
if err != nil {
return nil, false, err
}
r, err := convertToK8sResource(info.OrgID, updated, nil, nil, s.namespacer, false)
return r, false, err
}
// GracefulDeleter
func (s *legacyStorage) Delete(ctx context.Context, uid string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, false, err
}
old, err := s.Get(ctx, uid, nil)
if err != nil {
return old, false, err
}
if deleteValidation != nil {
if err = deleteValidation(ctx, old); err != nil {
return nil, false, err
}
}
version := ""
if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil {
version = *options.Preconditions.ResourceVersion
}
err = s.service.DeleteReceiver(ctx, uid, definitions.Provenance(ngmodels.ProvenanceNone), version, info.OrgID, user) // TODO add support for dry-run option
return old, false, err // false - will be deleted async
}
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
return nil, apierrors.NewMethodNotSupported(ResourceInfo.GroupResource(), "deleteCollection")
}
@@ -0,0 +1,19 @@
package v0alpha2
import (
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
)
func NewStorage(
legacySvc ReceiverService,
namespacer request.NamespaceMapper,
metadata MetadataService,
) grafanarest.Storage {
return &legacyStorage{
service: legacySvc,
namespacer: namespacer,
tableConverter: ResourceInfo.TableConverter(),
metadata: metadata,
}
}
@@ -0,0 +1,38 @@
package v0alpha2
import (
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
modelv2 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/receiver/v0alpha2"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
var kindV2 = modelv2.Kind()
var ResourceInfo = utils.NewResourceInfo(kindV2.Group(), kindV2.Version(),
kindV2.GroupVersionResource().Resource, strings.ToLower(kindV2.Kind()), kindV2.Kind(),
func() runtime.Object { return kindV2.ZeroValue() },
func() runtime.Object { return kindV2.ZeroListValue() },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The receiver name"},
{Name: "Integrations", Type: "string", Format: "string", Description: "The integration types"},
},
Reader: func(obj any) ([]interface{}, error) {
r, ok := obj.(*modelv2.Receiver)
if ok {
return []interface{}{
r.Name,
r.Spec.Title,
strings.Join(r.Spec.GetIntegrationsTypes(), ","),
}, nil
}
return nil, fmt.Errorf("expected resource or info")
},
},
)
@@ -12,6 +12,8 @@ import (
notificationsApp "github.com/grafana/grafana/apps/alerting/notifications/pkg/app"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver"
receiverv1 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver/v0alpha1"
receiverv2 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver/v0alpha2"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/templategroup"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval"
@@ -55,7 +57,8 @@ func getAuthorizer(authz accesscontrol.AccessControl) authorizer.Authorizer {
return templategroup.Authorize(ctx, authz, a)
case timeinterval.ResourceInfo.GroupResource().Resource:
return timeinterval.Authorize(ctx, authz, a)
case receiver.ResourceInfo.GroupResource().Resource:
case receiverv1.ResourceInfo.GroupResource().Resource,
receiverv2.ResourceInfo.GroupResource().Resource:
return receiver.Authorize(ctx, ac.NewReceiverAccess[*ngmodels.Receiver](authz, false), a)
case routingtree.ResourceInfo.GroupResource().Resource:
return routingtree.Authorize(ctx, authz, a)
@@ -66,14 +69,16 @@ func getAuthorizer(authz accesscontrol.AccessControl) authorizer.Authorizer {
func getLegacyStorage(namespacer request.NamespaceMapper, ng *ngalert.AlertNG) runner.LegacyStorageGetter {
return func(gvr schema.GroupVersionResource) grafanarest.Storage {
if gvr == receiver.ResourceInfo.GroupVersionResource() {
return receiver.NewStorage(ng.Api.ReceiverService, namespacer, ng.Api.ReceiverService)
if gvr == receiverv1.ResourceInfo.GroupVersionResource() {
return receiverv1.NewStorage(ng.Api.ReceiverService, namespacer, ng.Api.ReceiverService)
} else if gvr == timeinterval.ResourceInfo.GroupVersionResource() {
return timeinterval.NewStorage(ng.Api.MuteTimings, namespacer)
} else if gvr == templategroup.ResourceInfo.GroupVersionResource() {
return templategroup.NewStorage(ng.Api.Templates, namespacer)
} else if gvr == routingtree.ResourceInfo.GroupVersionResource() {
return routingtree.NewStorage(ng.Api.Policies, namespacer)
} else if gvr == receiverv2.ResourceInfo.GroupVersionResource() {
return receiverv2.NewStorage(ng.Api.ReceiverService, namespacer, ng.Api.ReceiverService)
}
panic("unknown legacy storage requested: " + gvr.String())
}
@@ -71,12 +71,12 @@ func (b *appBuilder) InstallSchema(scheme *runtime.Scheme) error {
// Link this group to the internal representation.
// This is used for server-side-apply (PATCH), and avoids the error:
// "no kind is registered for the type"
gvInternal := schema.GroupVersion{
Group: gv.Group,
Version: runtime.APIVersionInternal,
}
scheme.AddKnownTypeWithName(gvInternal.WithKind(kind.Kind()), kind.ZeroValue())
scheme.AddKnownTypeWithName(gvInternal.WithKind(kind.Kind()+"List"), kind.ZeroListValue())
// gvInternal := schema.GroupVersion{
// Group: gv.Group,
// Version: runtime.APIVersionInternal,
// }
// scheme.AddKnownTypeWithName(gvInternal.WithKind(kind.Kind()), kind.ZeroValue())
// scheme.AddKnownTypeWithName(gvInternal.WithKind(kind.Kind()+"List"), kind.ZeroListValue())
if len(kind.SelectableFields()) == 0 {
continue
@@ -241,74 +241,6 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
return limitOffset(filtered, q.Offset, q.Limit), nil
}
// ListReceivers returns a list of receivers a user has access to.
// Receivers can be filtered by name.
// This offers an looser permissions compared to GetReceivers. When a user doesn't have read access it will check for list access instead of returning an empty list.
// If the users has list access, all receiver settings will be removed from the response. This option is for backwards compatibility with the v1/receivers endpoint
// and should be removed when FGAC is fully implemented.
func (rs *ReceiverService) ListReceivers(ctx context.Context, q models.ListReceiversQuery, user identity.Requester) ([]*models.Receiver, error) { // TODO: Remove this method with FGAC.
ctx, span := rs.tracer.Start(ctx, "alerting.receivers.list", trace.WithAttributes(
attribute.Int64("query_org_id", q.OrgID),
attribute.StringSlice("query_names", q.Names),
attribute.Int("query_limit", q.Limit),
attribute.Int("query_offset", q.Offset),
))
defer span.End()
listAccess, err := rs.authz.HasList(ctx, user)
if err != nil {
return nil, err
}
uids := make([]string, 0, len(q.Names))
for _, name := range q.Names {
uids = append(uids, legacy_storage.NameToUid(name))
}
revision, err := rs.cfgStore.Get(ctx, q.OrgID)
if err != nil {
return nil, err
}
postables := revision.GetReceivers(uids)
span.AddEvent("Loaded receivers", trace.WithAttributes(
attribute.String("concurrency_token", revision.ConcurrencyToken),
attribute.Int("count", len(postables)),
))
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
if err != nil {
return nil, err
}
receivers, err := PostableApiReceiversToReceivers(postables, storedProvenances)
if err != nil {
return nil, err
}
if !listAccess {
var err error
receivers, err = rs.authz.FilterRead(ctx, user, receivers...)
if err != nil {
return nil, err
}
span.AddEvent("Applied access control filter", trace.WithAttributes(
attribute.Int("count", len(receivers)),
))
}
// Remove settings.
for _, r := range receivers {
for _, integration := range r.Integrations {
integration.Settings = nil
integration.SecureSettings = nil
integration.DisableResolveMessage = false
}
}
return limitOffset(receivers, q.Offset, q.Limit), nil
}
// DeleteReceiver deletes a receiver by uid.
// UID field currently does not exist, we assume the uid is a particular hashed value of the receiver name.
func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, callerProvenance definitions.Provenance, version string, orgID int64, user identity.Requester) error {
@@ -1740,11 +1740,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "advisor.grafana.app",
"kind": "Check",
"version": "__internal"
},
{
"group": "advisor.grafana.app",
"kind": "Check",
@@ -1807,11 +1802,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "advisor.grafana.app",
"kind": "CheckList",
"version": "__internal"
},
{
"group": "advisor.grafana.app",
"kind": "CheckList",
@@ -1953,11 +1943,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "advisor.grafana.app",
"kind": "CheckType",
"version": "__internal"
},
{
"group": "advisor.grafana.app",
"kind": "CheckType",
@@ -2001,11 +1986,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "advisor.grafana.app",
"kind": "CheckTypeList",
"version": "__internal"
},
{
"group": "advisor.grafana.app",
"kind": "CheckTypeList",
@@ -1741,11 +1741,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "investigations.grafana.app",
"kind": "Investigation",
"version": "__internal"
},
{
"group": "investigations.grafana.app",
"kind": "Investigation",
@@ -1892,11 +1887,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "investigations.grafana.app",
"kind": "InvestigationIndex",
"version": "__internal"
},
{
"group": "investigations.grafana.app",
"kind": "InvestigationIndex",
@@ -2031,11 +2021,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "investigations.grafana.app",
"kind": "InvestigationIndexList",
"version": "__internal"
},
{
"group": "investigations.grafana.app",
"kind": "InvestigationIndexList",
@@ -2218,11 +2203,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "investigations.grafana.app",
"kind": "InvestigationList",
"version": "__internal"
},
{
"group": "investigations.grafana.app",
"kind": "InvestigationList",
@@ -3433,11 +3433,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "notifications.alerting.grafana.app",
"kind": "Receiver",
"version": "__internal"
},
{
"group": "notifications.alerting.grafana.app",
"kind": "Receiver",
@@ -3481,11 +3476,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "notifications.alerting.grafana.app",
"kind": "ReceiverList",
"version": "__internal"
},
{
"group": "notifications.alerting.grafana.app",
"kind": "ReceiverList",
@@ -3724,11 +3714,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "notifications.alerting.grafana.app",
"kind": "RoutingTree",
"version": "__internal"
},
{
"group": "notifications.alerting.grafana.app",
"kind": "RoutingTree",
@@ -3772,11 +3757,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "notifications.alerting.grafana.app",
"kind": "RoutingTreeList",
"version": "__internal"
},
{
"group": "notifications.alerting.grafana.app",
"kind": "RoutingTreeList",
@@ -3980,11 +3960,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "notifications.alerting.grafana.app",
"kind": "TemplateGroup",
"version": "__internal"
},
{
"group": "notifications.alerting.grafana.app",
"kind": "TemplateGroup",
@@ -4028,11 +4003,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "notifications.alerting.grafana.app",
"kind": "TemplateGroupList",
"version": "__internal"
},
{
"group": "notifications.alerting.grafana.app",
"kind": "TemplateGroupList",
@@ -4208,11 +4178,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "notifications.alerting.grafana.app",
"kind": "TimeInterval",
"version": "__internal"
},
{
"group": "notifications.alerting.grafana.app",
"kind": "TimeInterval",
@@ -4256,11 +4221,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "notifications.alerting.grafana.app",
"kind": "TimeIntervalList",
"version": "__internal"
},
{
"group": "notifications.alerting.grafana.app",
"kind": "TimeIntervalList",
@@ -910,11 +910,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "playlist.grafana.app",
"kind": "Playlist",
"version": "__internal"
},
{
"group": "playlist.grafana.app",
"kind": "Playlist",
@@ -977,11 +972,6 @@
}
},
"x-kubernetes-group-version-kind": [
{
"group": "playlist.grafana.app",
"kind": "PlaylistList",
"version": "__internal"
},
{
"group": "playlist.grafana.app",
"kind": "PlaylistList",
+3
View File
@@ -96,6 +96,9 @@ func TestIntegrationOpenAPIs(t *testing.T) {
}, {
Group: "notifications.alerting.grafana.app",
Version: "v0alpha1",
}, {
Group: "notifications.alerting.grafana.app",
Version: "v0alpha2",
}}
for _, gv := range groups {
VerifyOpenAPISnapshots(t, dir, gv, h)