From 974a77da0071d4f5da8e90fb2a671ddb1a5dc155 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 27 Oct 2022 15:41:08 +0300 Subject: [PATCH 01/26] Refactor reconcilers and introduce v1beta2 API Signed-off-by: Stefan Prodan --- Makefile | 4 +- PROJECT | 9 + api/v1beta1/zz_generated.deepcopy.go | 2 +- api/v1beta2/alert_types.go | 117 ++++++ api/v1beta2/condition_types.go | 35 ++ api/v1beta2/doc.go | 20 + api/v1beta2/groupversion_info.go | 33 ++ api/v1beta2/provider_types.go | 170 ++++++++ api/v1beta2/receiver_types.go | 131 ++++++ api/v1beta2/reference_types.go | 49 +++ api/v1beta2/zz_generated.deepcopy.go | 382 ++++++++++++++++++ ...notification.toolkit.fluxcd.io_alerts.yaml | 199 +++++++++ ...ification.toolkit.fluxcd.io_providers.yaml | 188 +++++++++ ...ification.toolkit.fluxcd.io_receivers.yaml | 207 ++++++++++ config/rbac/role.yaml | 16 + controllers/alert_controller.go | 161 ++++---- ...dling_test.go => alert_controller_test.go} | 139 ++++++- controllers/provider_controller.go | 151 ++++--- controllers/provider_controller_test.go | 148 +++++++ controllers/receiver_controller.go | 192 +++++---- controllers/receiver_controller_test.go | 235 +++++++++-- controllers/suite_test.go | 47 ++- docs/api/notification.md | 127 ++++-- hack/boilerplate.go.txt | 2 +- internal/notifier/factory.go | 44 +- internal/server/event_handlers.go | 2 +- internal/server/receiver_handler_test.go | 57 +-- internal/server/receiver_handlers.go | 39 +- main.go | 39 +- 29 files changed, 2525 insertions(+), 420 deletions(-) create mode 100644 api/v1beta2/alert_types.go create mode 100644 api/v1beta2/condition_types.go create mode 100644 api/v1beta2/doc.go create mode 100644 api/v1beta2/groupversion_info.go create mode 100644 api/v1beta2/provider_types.go create mode 100644 api/v1beta2/receiver_types.go create mode 100644 api/v1beta2/reference_types.go create mode 100644 api/v1beta2/zz_generated.deepcopy.go rename controllers/{event_handling_test.go => alert_controller_test.go} (61%) create mode 100644 controllers/provider_controller_test.go diff --git a/Makefile b/Makefile index 73a2877dc..4f3274ac5 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ IMG ?= fluxcd/notification-controller:latest # Produce CRDs that work back to Kubernetes 1.16 CRD_OPTIONS ?= crd:crdVersions=v1 -SOURCE_VER ?= v0.24.0 +SOURCE_VER ?= v0.31.0 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -76,7 +76,7 @@ manifests: controller-gen # Generate API reference documentation api-docs: gen-crd-api-reference-docs - $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta1 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/notification.md + $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta2 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/notification.md # Run go mod tidy tidy: diff --git a/PROJECT b/PROJECT index f3217e44c..c53381c21 100644 --- a/PROJECT +++ b/PROJECT @@ -10,4 +10,13 @@ resources: - group: notification kind: Receiver version: v1beta1 +- group: notification + kind: Provider + version: v1beta2 +- group: notification + kind: Alert + version: v1beta2 +- group: notification + kind: Receiver + version: v1beta2 version: "2" diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index c20ff16bc..a7f1bece8 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -2,7 +2,7 @@ // +build !ignore_autogenerated /* -Copyright 2020 The Flux authors +Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/v1beta2/alert_types.go b/api/v1beta2/alert_types.go new file mode 100644 index 000000000..494685595 --- /dev/null +++ b/api/v1beta2/alert_types.go @@ -0,0 +1,117 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + "github.com/fluxcd/pkg/apis/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + AlertKind string = "Alert" +) + +// AlertSpec defines an alerting rule for events involving a list of objects +type AlertSpec struct { + // Send events using this provider. + // +required + ProviderRef meta.LocalObjectReference `json:"providerRef"` + + // Filter events based on severity, defaults to ('info'). + // If set to 'info' no events will be filtered. + // +kubebuilder:validation:Enum=info;error + // +kubebuilder:default:=info + // +optional + EventSeverity string `json:"eventSeverity,omitempty"` + + // Filter events based on the involved objects. + // +required + EventSources []CrossNamespaceObjectReference `json:"eventSources"` + + // A list of Golang regular expressions to be used for excluding messages. + // +optional + ExclusionList []string `json:"exclusionList,omitempty"` + + // Short description of the impact and affected cluster. + // +optional + Summary string `json:"summary,omitempty"` + + // This flag tells the controller to suspend subsequent events dispatching. + // Defaults to false. + // +optional + Suspend bool `json:"suspend,omitempty"` +} + +// AlertStatus defines the observed state of Alert +type AlertStatus struct { + meta.ReconcileRequestStatus `json:",inline"` + + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ObservedGeneration is the last observed generation. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +genclient +// +genclient:Namespaced +// +kubebuilder:storageversion +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" + +// Alert is the Schema for the alerts API +type Alert struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AlertSpec `json:"spec,omitempty"` + // +kubebuilder:default:={"observedGeneration":-1} + Status AlertStatus `json:"status,omitempty"` +} + +// GetStatusConditions returns a pointer to the Status.Conditions slice +// Deprecated: use GetConditions instead. +func (in *Alert) GetStatusConditions() *[]metav1.Condition { + return &in.Status.Conditions +} + +// GetConditions returns the status conditions of the object. +func (in *Alert) GetConditions() []metav1.Condition { + return in.Status.Conditions +} + +// SetConditions sets the status conditions on the object. +func (in *Alert) SetConditions(conditions []metav1.Condition) { + in.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true + +// AlertList contains a list of Alert +type AlertList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Alert `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Alert{}, &AlertList{}) +} diff --git a/api/v1beta2/condition_types.go b/api/v1beta2/condition_types.go new file mode 100644 index 000000000..da0c51f87 --- /dev/null +++ b/api/v1beta2/condition_types.go @@ -0,0 +1,35 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +const NotificationFinalizer = "finalizers.fluxcd.io" + +const ( + // InitializedReason represents the fact that a given resource has been initialized. + InitializedReason string = "Initialized" + + // ValidationFailedReason represents the fact that some part of the spec of a given resource + // couldn't be validated. + ValidationFailedReason string = "ValidationFailed" + + // TokenNotFound represents the fact that receiver token can't be found. + TokenNotFoundReason string = "TokenNotFound" + + // ProgressingWithRetryReason represents the fact that + // the reconciliation encountered an error that will be retried. + ProgressingWithRetryReason string = "ProgressingWithRetry" +) diff --git a/api/v1beta2/doc.go b/api/v1beta2/doc.go new file mode 100644 index 000000000..6b1836747 --- /dev/null +++ b/api/v1beta2/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta2 contains API Schema definitions for the notification v1beta2 API group +// +kubebuilder:object:generate=true +// +groupName=notification.toolkit.fluxcd.io +package v1beta2 diff --git a/api/v1beta2/groupversion_info.go b/api/v1beta2/groupversion_info.go new file mode 100644 index 000000000..8cdf68dcc --- /dev/null +++ b/api/v1beta2/groupversion_info.go @@ -0,0 +1,33 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "notification.toolkit.fluxcd.io", Version: "v1beta2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1beta2/provider_types.go b/api/v1beta2/provider_types.go new file mode 100644 index 000000000..58fc5af2e --- /dev/null +++ b/api/v1beta2/provider_types.go @@ -0,0 +1,170 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + "time" + + "github.com/fluxcd/pkg/apis/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + ProviderKind string = "Provider" +) + +// ProviderSpec defines the desired state of Provider +type ProviderSpec struct { + // Type of provider + // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;generic-hmac;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie;alertmanager;grafana;githubdispatch; + // +required + Type string `json:"type"` + + // Alert channel for this provider + // +optional + Channel string `json:"channel,omitempty"` + + // Bot username for this provider + // +optional + Username string `json:"username,omitempty"` + + // HTTP/S webhook address of this provider + // +kubebuilder:validation:Pattern="^(http|https)://" + // +kubebuilder:validation:Optional + // +optional + Address string `json:"address,omitempty"` + + // Timeout for sending alerts to the provider. + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m))+$" + // +optional + Timeout *metav1.Duration `json:"timeout,omitempty"` + + // HTTP/S address of the proxy + // +kubebuilder:validation:Pattern="^(http|https)://" + // +kubebuilder:validation:Optional + // +optional + Proxy string `json:"proxy,omitempty"` + + // Secret reference containing the provider webhook URL + // using "address" as data key + // +optional + SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` + + // CertSecretRef can be given the name of a secret containing + // a PEM-encoded CA certificate (`caFile`) + // +optional + CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"` + + // This flag tells the controller to suspend subsequent events handling. + // Defaults to false. + // +optional + Suspend bool `json:"suspend,omitempty"` +} + +const ( + GenericProvider string = "generic" + GenericHMACProvider string = "generic-hmac" + SlackProvider string = "slack" + GrafanaProvider string = "grafana" + DiscordProvider string = "discord" + MSTeamsProvider string = "msteams" + RocketProvider string = "rocket" + GitHubDispatchProvider string = "githubdispatch" + GitHubProvider string = "github" + GitLabProvider string = "gitlab" + BitbucketProvider string = "bitbucket" + AzureDevOpsProvider string = "azuredevops" + GoogleChatProvider string = "googlechat" + WebexProvider string = "webex" + SentryProvider string = "sentry" + AzureEventHubProvider string = "azureeventhub" + TelegramProvider string = "telegram" + LarkProvider string = "lark" + Matrix string = "matrix" + OpsgenieProvider string = "opsgenie" + AlertManagerProvider string = "alertmanager" +) + +// ProviderStatus defines the observed state of Provider +type ProviderStatus struct { + meta.ReconcileRequestStatus `json:",inline"` + + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ObservedGeneration is the last reconciled generation. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +genclient +// +genclient:Namespaced +// +kubebuilder:storageversion +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" + +// Provider is the Schema for the providers API +type Provider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ProviderSpec `json:"spec,omitempty"` + // +kubebuilder:default:={"observedGeneration":-1} + Status ProviderStatus `json:"status,omitempty"` +} + +// GetStatusConditions returns a pointer to the Status.Conditions slice +// Deprecated: use GetConditions instead. +func (in *Provider) GetStatusConditions() *[]metav1.Condition { + return &in.Status.Conditions +} + +// GetConditions returns the status conditions of the object. +func (in *Provider) GetConditions() []metav1.Condition { + return in.Status.Conditions +} + +// SetConditions sets the status conditions on the object. +func (in *Provider) SetConditions(conditions []metav1.Condition) { + in.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true + +// ProviderList contains a list of Provider +type ProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Provider `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Provider{}, &ProviderList{}) +} + +func (in *Provider) GetTimeout() time.Duration { + duration := 15 * time.Second + if in.Spec.Timeout != nil { + duration = in.Spec.Timeout.Duration + } + + return duration +} diff --git a/api/v1beta2/receiver_types.go b/api/v1beta2/receiver_types.go new file mode 100644 index 000000000..a9c027df5 --- /dev/null +++ b/api/v1beta2/receiver_types.go @@ -0,0 +1,131 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/fluxcd/pkg/apis/meta" +) + +// ReceiverSpec defines the desired state of Receiver +type ReceiverSpec struct { + // Type of webhook sender, used to determine + // the validation procedure and payload deserialization. + // +kubebuilder:validation:Enum=generic;generic-hmac;github;gitlab;bitbucket;harbor;dockerhub;quay;gcr;nexus;acr + // +required + Type string `json:"type"` + + // A list of events to handle, + // e.g. 'push' for GitHub or 'Push Hook' for GitLab. + // +optional + Events []string `json:"events"` + + // A list of resources to be notified about changes. + // +required + Resources []CrossNamespaceObjectReference `json:"resources"` + + // Secret reference containing the token used + // to validate the payload authenticity + // +required + SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"` + + // This flag tells the controller to suspend subsequent events handling. + // Defaults to false. + // +optional + Suspend bool `json:"suspend,omitempty"` +} + +// ReceiverStatus defines the observed state of Receiver +type ReceiverStatus struct { + meta.ReconcileRequestStatus `json:",inline"` + + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // Generated webhook URL in the format + // of '/hook/sha256sum(token+name+namespace)'. + // +optional + URL string `json:"url,omitempty"` + + // ObservedGeneration is the last observed generation. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +const ( + GenericReceiver string = "generic" + GenericHMACReceiver string = "generic-hmac" + GitHubReceiver string = "github" + GitLabReceiver string = "gitlab" + BitbucketReceiver string = "bitbucket" + HarborReceiver string = "harbor" + DockerHubReceiver string = "dockerhub" + QuayReceiver string = "quay" + GCRReceiver string = "gcr" + NexusReceiver string = "nexus" + ReceiverKind string = "Receiver" + ACRReceiver string = "acr" +) + +// GetStatusConditions returns a pointer to the Status.Conditions slice +// Deprecated: use GetConditions instead. +func (in *Receiver) GetStatusConditions() *[]metav1.Condition { + return &in.Status.Conditions +} + +// GetConditions returns the status conditions of the object. +func (in *Receiver) GetConditions() []metav1.Condition { + return in.Status.Conditions +} + +// SetConditions sets the status conditions on the object. +func (in *Receiver) SetConditions(conditions []metav1.Condition) { + in.Status.Conditions = conditions +} + +// +genclient +// +genclient:Namespaced +// +kubebuilder:storageversion +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" + +// Receiver is the Schema for the receivers API +type Receiver struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ReceiverSpec `json:"spec,omitempty"` + // +kubebuilder:default:={"observedGeneration":-1} + Status ReceiverStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ReceiverList contains a list of Receiver +type ReceiverList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Receiver `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Receiver{}, &ReceiverList{}) +} diff --git a/api/v1beta2/reference_types.go b/api/v1beta2/reference_types.go new file mode 100644 index 000000000..b051890ac --- /dev/null +++ b/api/v1beta2/reference_types.go @@ -0,0 +1,49 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +// CrossNamespaceObjectReference contains enough information to let you locate the +// typed referenced object at cluster level +type CrossNamespaceObjectReference struct { + // API version of the referent + // +optional + APIVersion string `json:"apiVersion,omitempty"` + + // Kind of the referent + // +kubebuilder:validation:Enum=Bucket;GitRepository;Kustomization;HelmRelease;HelmChart;HelmRepository;ImageRepository;ImagePolicy;ImageUpdateAutomation;OCIRepository + // +required + Kind string `json:"kind,omitempty"` + + // Name of the referent + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=53 + // +required + Name string `json:"name"` + + // Namespace of the referent + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=53 + // +kubebuilder:validation:Optional + // +optional + Namespace string `json:"namespace,omitempty"` + + // MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + // map is equivalent to an element of matchExpressions, whose key field is "key", the + // operator is "In", and the values array contains only "value". The requirements are ANDed. + // +optional + MatchLabels map[string]string `json:"matchLabels,omitempty"` +} diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go new file mode 100644 index 000000000..4d8542c9c --- /dev/null +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -0,0 +1,382 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta2 + +import ( + "github.com/fluxcd/pkg/apis/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Alert) DeepCopyInto(out *Alert) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Alert. +func (in *Alert) DeepCopy() *Alert { + if in == nil { + return nil + } + out := new(Alert) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Alert) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertList) DeepCopyInto(out *AlertList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Alert, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertList. +func (in *AlertList) DeepCopy() *AlertList { + if in == nil { + return nil + } + out := new(AlertList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AlertList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertSpec) DeepCopyInto(out *AlertSpec) { + *out = *in + out.ProviderRef = in.ProviderRef + if in.EventSources != nil { + in, out := &in.EventSources, &out.EventSources + *out = make([]CrossNamespaceObjectReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ExclusionList != nil { + in, out := &in.ExclusionList, &out.ExclusionList + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertSpec. +func (in *AlertSpec) DeepCopy() *AlertSpec { + if in == nil { + return nil + } + out := new(AlertSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertStatus) DeepCopyInto(out *AlertStatus) { + *out = *in + out.ReconcileRequestStatus = in.ReconcileRequestStatus + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertStatus. +func (in *AlertStatus) DeepCopy() *AlertStatus { + if in == nil { + return nil + } + out := new(AlertStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CrossNamespaceObjectReference) DeepCopyInto(out *CrossNamespaceObjectReference) { + *out = *in + if in.MatchLabels != nil { + in, out := &in.MatchLabels, &out.MatchLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceObjectReference. +func (in *CrossNamespaceObjectReference) DeepCopy() *CrossNamespaceObjectReference { + if in == nil { + return nil + } + out := new(CrossNamespaceObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Provider) DeepCopyInto(out *Provider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Provider. +func (in *Provider) DeepCopy() *Provider { + if in == nil { + return nil + } + out := new(Provider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Provider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderList) DeepCopyInto(out *ProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Provider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderList. +func (in *ProviderList) DeepCopy() *ProviderList { + if in == nil { + return nil + } + out := new(ProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { + *out = *in + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(v1.Duration) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(meta.LocalObjectReference) + **out = **in + } + if in.CertSecretRef != nil { + in, out := &in.CertSecretRef, &out.CertSecretRef + *out = new(meta.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec. +func (in *ProviderSpec) DeepCopy() *ProviderSpec { + if in == nil { + return nil + } + out := new(ProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) { + *out = *in + out.ReconcileRequestStatus = in.ReconcileRequestStatus + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderStatus. +func (in *ProviderStatus) DeepCopy() *ProviderStatus { + if in == nil { + return nil + } + out := new(ProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Receiver) DeepCopyInto(out *Receiver) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Receiver. +func (in *Receiver) DeepCopy() *Receiver { + if in == nil { + return nil + } + out := new(Receiver) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Receiver) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReceiverList) DeepCopyInto(out *ReceiverList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Receiver, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReceiverList. +func (in *ReceiverList) DeepCopy() *ReceiverList { + if in == nil { + return nil + } + out := new(ReceiverList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ReceiverList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReceiverSpec) DeepCopyInto(out *ReceiverSpec) { + *out = *in + if in.Events != nil { + in, out := &in.Events, &out.Events + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]CrossNamespaceObjectReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReceiverSpec. +func (in *ReceiverSpec) DeepCopy() *ReceiverSpec { + if in == nil { + return nil + } + out := new(ReceiverSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReceiverStatus) DeepCopyInto(out *ReceiverStatus) { + *out = *in + out.ReconcileRequestStatus = in.ReconcileRequestStatus + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReceiverStatus. +func (in *ReceiverStatus) DeepCopy() *ReceiverStatus { + if in == nil { + return nil + } + out := new(ReceiverStatus) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml index 39707e0d4..0efe67a96 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml @@ -206,6 +206,205 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: Alert is the Schema for the alerts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AlertSpec defines an alerting rule for events involving a + list of objects + properties: + eventSeverity: + default: info + description: Filter events based on severity, defaults to ('info'). + If set to 'info' no events will be filtered. + enum: + - info + - error + type: string + eventSources: + description: Filter events based on the involved objects. + items: + description: CrossNamespaceObjectReference contains enough information + to let you locate the typed referenced object at cluster level + properties: + apiVersion: + description: API version of the referent + type: string + kind: + description: Kind of the referent + enum: + - Bucket + - GitRepository + - Kustomization + - HelmRelease + - HelmChart + - HelmRepository + - ImageRepository + - ImagePolicy + - ImageUpdateAutomation + - OCIRepository + type: string + matchLabels: + additionalProperties: + type: string + description: MatchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + name: + description: Name of the referent + maxLength: 53 + minLength: 1 + type: string + namespace: + description: Namespace of the referent + maxLength: 53 + minLength: 1 + type: string + required: + - name + type: object + type: array + exclusionList: + description: A list of Golang regular expressions to be used for excluding + messages. + items: + type: string + type: array + providerRef: + description: Send events using this provider. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + summary: + description: Short description of the impact and affected cluster. + type: string + suspend: + description: This flag tells the controller to suspend subsequent + events dispatching. Defaults to false. + type: boolean + required: + - eventSources + - providerRef + type: object + status: + default: + observedGeneration: -1 + description: AlertStatus defines the observed state of Alert + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value can + be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index f80113523..2ce93e81f 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -195,6 +195,194 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: Provider is the Schema for the providers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ProviderSpec defines the desired state of Provider + properties: + address: + description: HTTP/S webhook address of this provider + pattern: ^(http|https):// + type: string + certSecretRef: + description: CertSecretRef can be given the name of a secret containing + a PEM-encoded CA certificate (`caFile`) + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + channel: + description: Alert channel for this provider + type: string + proxy: + description: HTTP/S address of the proxy + pattern: ^(http|https):// + type: string + secretRef: + description: Secret reference containing the provider webhook URL + using "address" as data key + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + suspend: + description: This flag tells the controller to suspend subsequent + events handling. Defaults to false. + type: boolean + timeout: + description: Timeout for sending alerts to the provider. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + type: + description: Type of provider + enum: + - slack + - discord + - msteams + - rocket + - generic + - generic-hmac + - github + - gitlab + - bitbucket + - azuredevops + - googlechat + - webex + - sentry + - azureeventhub + - telegram + - lark + - matrix + - opsgenie + - alertmanager + - grafana + - githubdispatch + type: string + username: + description: Bot username for this provider + type: string + required: + - type + type: object + status: + default: + observedGeneration: -1 + description: ProviderStatus defines the observed state of Provider + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value can + be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last reconciled generation. + format: int64 + type: integer + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml index d2d1df8bb..590d26c2f 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml @@ -214,6 +214,213 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: Receiver is the Schema for the receivers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ReceiverSpec defines the desired state of Receiver + properties: + events: + description: A list of events to handle, e.g. 'push' for GitHub or + 'Push Hook' for GitLab. + items: + type: string + type: array + resources: + description: A list of resources to be notified about changes. + items: + description: CrossNamespaceObjectReference contains enough information + to let you locate the typed referenced object at cluster level + properties: + apiVersion: + description: API version of the referent + type: string + kind: + description: Kind of the referent + enum: + - Bucket + - GitRepository + - Kustomization + - HelmRelease + - HelmChart + - HelmRepository + - ImageRepository + - ImagePolicy + - ImageUpdateAutomation + - OCIRepository + type: string + matchLabels: + additionalProperties: + type: string + description: MatchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + name: + description: Name of the referent + maxLength: 53 + minLength: 1 + type: string + namespace: + description: Namespace of the referent + maxLength: 53 + minLength: 1 + type: string + required: + - name + type: object + type: array + secretRef: + description: Secret reference containing the token used to validate + the payload authenticity + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + suspend: + description: This flag tells the controller to suspend subsequent + events handling. Defaults to false. + type: boolean + type: + description: Type of webhook sender, used to determine the validation + procedure and payload deserialization. + enum: + - generic + - generic-hmac + - github + - gitlab + - bitbucket + - harbor + - dockerhub + - quay + - gcr + - nexus + - acr + type: string + required: + - resources + - type + type: object + status: + default: + observedGeneration: -1 + description: ReceiverStatus defines the observed state of Receiver + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value can + be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + url: + description: Generated webhook URL in the format of '/hook/sha256sum(token+name+namespace)'. + type: string + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 85f4270ef..29a8b7077 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -137,3 +137,19 @@ rules: - helmrepositories/status verbs: - get +- apiGroups: + - source.fluxcd.io + resources: + - ocirepositories + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - source.fluxcd.io + resources: + - ocirepositories/status + verbs: + - get diff --git a/controllers/alert_controller.go b/controllers/alert_controller.go index 73be26647..0396f94f3 100644 --- a/controllers/alert_controller.go +++ b/controllers/alert_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,8 +21,7 @@ import ( "fmt" "time" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" ctrl "sigs.k8s.io/controller-runtime" @@ -43,7 +42,7 @@ import ( "github.com/fluxcd/pkg/runtime/predicates" kuberecorder "k8s.io/client-go/tools/record" - "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" ) var ( @@ -56,7 +55,7 @@ type AlertReconciler struct { helper.Metrics kuberecorder.EventRecorder - Scheme *runtime.Scheme + ControllerName string } type AlertReconcilerOptions struct { @@ -69,9 +68,9 @@ func (r *AlertReconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *AlertReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts AlertReconcilerOptions) error { - if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &v1beta1.Alert{}, ProviderIndexKey, + if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &apiv1.Alert{}, ProviderIndexKey, func(o client.Object) []string { - alert := o.(*v1beta1.Alert) + alert := o.(*apiv1.Alert) return []string{ fmt.Sprintf("%s/%s", alert.GetNamespace(), alert.Spec.ProviderRef.Name), } @@ -80,10 +79,11 @@ func (r *AlertReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts Aler } return ctrl.NewControllerManagedBy(mgr). - For(&v1beta1.Alert{}). - WithEventFilter(predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{})). + For(&apiv1.Alert{}, builder.WithPredicates( + predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}), + )). Watches( - &source.Kind{Type: &v1beta1.Provider{}}, + &source.Kind{Type: &apiv1.Provider{}}, handler.EnqueueRequestsFromMapFunc(r.requestsForProviderChange), builder.WithPredicates(predicate.GenerationChangedPredicate{}), ). @@ -99,95 +99,66 @@ func (r *AlertReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts Aler // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=alerts/status,verbs=get;update;patch func (r *AlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { - start := time.Now() + reconcileStart := time.Now() log := ctrl.LoggerFrom(ctx) - alert := &v1beta1.Alert{} - if err := r.Get(ctx, req.NamespacedName, alert); err != nil { + obj := &apiv1.Alert{} + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - // record suspension metrics - r.RecordSuspend(ctx, alert, alert.Spec.Suspend) - - if alert.Spec.Suspend { - log.Info("Reconciliation is suspended for this object") - return ctrl.Result{}, nil - } - - patchHelper, err := patch.NewHelper(alert, r.Client) - if err != nil { - return ctrl.Result{}, err - } + // Initialize the runtime patcher with the current version of the object. + patcher := patch.NewSerialPatcher(obj, r.Client) defer func() { - patchOpts := []patch.Option{ - patch.WithOwnedConditions{ - Conditions: []string{ - meta.ReadyCondition, - meta.ReconcilingCondition, - meta.StalledCondition, - }, - }, - } - - if retErr == nil && (result.IsZero() || !result.Requeue) { - conditions.Delete(alert, meta.ReconcilingCondition) - - patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{}) + // Record Prometheus metrics. + r.Metrics.RecordReadiness(ctx, obj) + r.Metrics.RecordDuration(ctx, obj, reconcileStart) + r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) - readyCondition := conditions.Get(alert, meta.ReadyCondition) - switch readyCondition.Status { - case metav1.ConditionFalse: - // As we are no longer reconciling and the end-state is not ready, the reconciliation has stalled - conditions.MarkStalled(alert, readyCondition.Reason, readyCondition.Message) - case metav1.ConditionTrue: - // As we are no longer reconciling and the end-state is ready, the reconciliation is no longer stalled - conditions.Delete(alert, meta.StalledCondition) - } - } - - if err := patchHelper.Patch(ctx, alert, patchOpts...); err != nil { - retErr = kerrors.NewAggregate([]error{retErr, err}) - } - - r.Metrics.RecordReadiness(ctx, alert) - r.Metrics.RecordDuration(ctx, alert, start) + // Patch finalizers, status and conditions. + retErr = r.patch(ctx, obj, patcher) }() - if !controllerutil.ContainsFinalizer(alert, v1beta1.NotificationFinalizer) { - controllerutil.AddFinalizer(alert, v1beta1.NotificationFinalizer) + if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { + controllerutil.AddFinalizer(obj, apiv1.NotificationFinalizer) result = ctrl.Result{Requeue: true} return } - if !alert.ObjectMeta.DeletionTimestamp.IsZero() { - controllerutil.RemoveFinalizer(alert, v1beta1.NotificationFinalizer) + if !obj.ObjectMeta.DeletionTimestamp.IsZero() { + controllerutil.RemoveFinalizer(obj, apiv1.NotificationFinalizer) result = ctrl.Result{} return } - return r.reconcile(ctx, alert) + // Return early if the object is suspended. + if obj.Spec.Suspend { + log.Info("Reconciliation is suspended for this object") + return ctrl.Result{}, nil + } + + return r.reconcile(ctx, obj) } -func (r *AlertReconciler) reconcile(ctx context.Context, alert *v1beta1.Alert) (ctrl.Result, error) { +func (r *AlertReconciler) reconcile(ctx context.Context, alert *apiv1.Alert) (ctrl.Result, error) { // Mark the resource as under reconciliation - conditions.MarkReconciling(alert, meta.ProgressingReason, "") + conditions.MarkReconciling(alert, meta.ProgressingReason, "Reconciliation in progress") // validate alert spec and provider if err := r.validate(ctx, alert); err != nil { - conditions.MarkFalse(alert, meta.ReadyCondition, v1beta1.ValidationFailedReason, err.Error()) - return ctrl.Result{}, client.IgnoreNotFound(err) + conditions.MarkFalse(alert, meta.ReadyCondition, apiv1.ValidationFailedReason, err.Error()) + return ctrl.Result{Requeue: true}, client.IgnoreNotFound(err) } - conditions.MarkTrue(alert, meta.ReadyCondition, meta.SucceededReason, v1beta1.InitializedReason) + conditions.MarkTrue(alert, meta.ReadyCondition, meta.SucceededReason, apiv1.InitializedReason) ctrl.LoggerFrom(ctx).Info("Alert initialized") return ctrl.Result{}, nil } -func (r *AlertReconciler) validate(ctx context.Context, alert *v1beta1.Alert) error { - provider := &v1beta1.Provider{} +func (r *AlertReconciler) validate(ctx context.Context, alert *apiv1.Alert) error { + provider := &apiv1.Provider{} providerName := types.NamespacedName{Namespace: alert.Namespace, Name: alert.Spec.ProviderRef.Name} if err := r.Get(ctx, providerName, provider); err != nil { // log not found errors since they get filtered out @@ -203,13 +174,13 @@ func (r *AlertReconciler) validate(ctx context.Context, alert *v1beta1.Alert) er } func (r *AlertReconciler) requestsForProviderChange(o client.Object) []reconcile.Request { - provider, ok := o.(*v1beta1.Provider) + provider, ok := o.(*apiv1.Provider) if !ok { panic(fmt.Errorf("expected a provider, got %T", o)) } ctx := context.Background() - var list v1beta1.AlertList + var list apiv1.AlertList if err := r.List(ctx, &list, client.MatchingFields{ ProviderIndexKey: client.ObjectKeyFromObject(provider).String(), }); err != nil { @@ -223,3 +194,53 @@ func (r *AlertReconciler) requestsForProviderChange(o client.Object) []reconcile return reqs } + +// patch updates the object status, conditions and finalizers. +func (r *AlertReconciler) patch(ctx context.Context, obj *apiv1.Alert, patcher *patch.SerialPatcher) (retErr error) { + // Configure the runtime patcher. + patchOpts := []patch.Option{} + ownedConditions := []string{ + meta.ReadyCondition, + meta.ReconcilingCondition, + meta.StalledCondition, + } + patchOpts = append(patchOpts, + patch.WithOwnedConditions{Conditions: ownedConditions}, + patch.WithForceOverwriteConditions{}, + patch.WithFieldOwner(r.ControllerName), + ) + + // Set the value of the reconciliation request in status. + if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok { + obj.Status.LastHandledReconcileAt = v + } + + // Remove the Reconciling condition and update the observed generation + // if the reconciliation was successful. + if conditions.IsTrue(obj, meta.ReadyCondition) { + conditions.Delete(obj, meta.ReconcilingCondition) + obj.Status.ObservedGeneration = obj.Generation + } + + // Set the Reconciling reason to ProgressingWithRetry if the + // reconciliation has failed. + if conditions.IsFalse(obj, meta.ReadyCondition) && + conditions.Has(obj, meta.ReconcilingCondition) { + rc := conditions.Get(obj, meta.ReconcilingCondition) + rc.Reason = apiv1.ProgressingWithRetryReason + conditions.Set(obj, rc) + } + + // Patch the object status, conditions and finalizers. + if err := patcher.Patch(ctx, obj, patchOpts...); err != nil { + if !obj.GetDeletionTimestamp().IsZero() { + err = kerrors.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) }) + } + retErr = kerrors.NewAggregate([]error{retErr, err}) + if retErr != nil { + return retErr + } + } + + return nil +} diff --git a/controllers/event_handling_test.go b/controllers/alert_controller_test.go similarity index 61% rename from controllers/event_handling_test.go rename to controllers/alert_controller_test.go index 8ec6beb5a..33ee2532f 100644 --- a/controllers/event_handling_test.go +++ b/controllers/alert_controller_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package controllers import ( @@ -16,26 +32,127 @@ import ( prommetrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logf "sigs.k8s.io/controller-runtime/pkg/log" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" - notifyv1 "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" "github.com/fluxcd/notification-controller/internal/server" ) -func TestEventHandler(t *testing.T) { - // randomize var? create http server here? +func TestAlertReconciler_Reconcile(t *testing.T) { + g := NewWithT(t) + timeout := 5 * time.Second + resultA := &apiv1.Alert{} + namespaceName := "alert-" + randStringRunes(5) + providerName := "provider-" + randStringRunes(5) + + g.Expect(createNamespace(namespaceName)).NotTo(HaveOccurred(), "failed to create test namespace") + + provider := &apiv1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerName, + Namespace: namespaceName, + }, + Spec: apiv1.ProviderSpec{ + Type: "generic", + Address: "https://webhook.internal", + }, + } + + alert := &apiv1.Alert{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("alert-%s", randStringRunes(5)), + Namespace: namespaceName, + }, + Spec: apiv1.AlertSpec{ + ProviderRef: meta.LocalObjectReference{ + Name: providerName, + }, + EventSeverity: "info", + EventSources: []apiv1.CrossNamespaceObjectReference{ + { + Kind: "Bucket", + Name: "*", + }, + }, + }, + } + g.Expect(k8sClient.Create(context.Background(), alert)).To(Succeed()) + + t.Run("fails with provider not found error", func(t *testing.T) { + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA) + return conditions.Has(resultA, meta.ReadyCondition) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.IsReady(resultA)).To(BeFalse()) + g.Expect(conditions.GetReason(resultA, meta.ReadyCondition)).To(BeIdenticalTo(apiv1.ValidationFailedReason)) + g.Expect(conditions.GetMessage(resultA, meta.ReadyCondition)).To(ContainSubstring(providerName)) + + g.Expect(conditions.Has(resultA, meta.ReconcilingCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(resultA, meta.ReconcilingCondition)).To(BeIdenticalTo(apiv1.ProgressingWithRetryReason)) + g.Expect(conditions.GetObservedGeneration(resultA, meta.ReconcilingCondition)).To(BeIdenticalTo(resultA.Generation)) + g.Expect(controllerutil.ContainsFinalizer(resultA, apiv1.NotificationFinalizer)).To(BeTrue()) + }) + + t.Run("recovers when provider exists", func(t *testing.T) { + g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA) + return conditions.IsReady(resultA) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.GetObservedGeneration(resultA, meta.ReadyCondition)).To(BeIdenticalTo(resultA.Generation)) + g.Expect(resultA.Status.ObservedGeneration).To(BeIdenticalTo(resultA.Generation)) + g.Expect(conditions.Has(resultA, meta.ReconcilingCondition)).To(BeFalse()) + }) + + t.Run("handles reconcileAt", func(t *testing.T) { + reconcileRequestAt := metav1.Now().String() + resultA.SetAnnotations(map[string]string{ + meta.ReconcileRequestAnnotation: reconcileRequestAt, + }) + g.Expect(k8sClient.Update(context.Background(), resultA)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA) + return resultA.Status.LastHandledReconcileAt == reconcileRequestAt + }, timeout, time.Second).Should(BeTrue()) + }) + + t.Run("finalizes suspended object", func(t *testing.T) { + resultA.Spec.Suspend = true + g.Expect(k8sClient.Update(context.Background(), resultA)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA) + return resultA.Spec.Suspend == true + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(k8sClient.Delete(context.Background(), resultA)).To(Succeed()) + + g.Eventually(func() bool { + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA) + return apierrors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) + }) +} + +func TestAlertReconciler_EventHandler(t *testing.T) { g := NewWithT(t) var ( namespace = "events-" + randStringRunes(5) req *http.Request - provider *notifyv1.Provider + provider *apiv1.Provider ) g.Expect(createNamespace(namespace)).NotTo(HaveOccurred(), "failed to create test namespace") @@ -67,19 +184,19 @@ func TestEventHandler(t *testing.T) { Name: fmt.Sprintf("provider-%s", randStringRunes(5)), Namespace: namespace, } - provider = ¬ifyv1.Provider{ + provider = &apiv1.Provider{ ObjectMeta: metav1.ObjectMeta{ Name: providerKey.Name, Namespace: providerKey.Namespace, }, - Spec: notifyv1.ProviderSpec{ + Spec: apiv1.ProviderSpec{ Type: "generic", Address: rcvServer.URL, }, } g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed()) g.Eventually(func() bool { - var obj notifyv1.Provider + var obj apiv1.Provider g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), &obj)) return conditions.IsReady(&obj) }, 30*time.Second, time.Second).Should(BeTrue()) @@ -105,17 +222,17 @@ func TestEventHandler(t *testing.T) { Namespace: namespace, } - alert := ¬ifyv1.Alert{ + alert := &apiv1.Alert{ ObjectMeta: metav1.ObjectMeta{ Name: alertKey.Name, Namespace: alertKey.Namespace, }, - Spec: notifyv1.AlertSpec{ + Spec: apiv1.AlertSpec{ ProviderRef: meta.LocalObjectReference{ Name: providerKey.Name, }, EventSeverity: "info", - EventSources: []notifyv1.CrossNamespaceObjectReference{ + EventSources: []apiv1.CrossNamespaceObjectReference{ { Kind: "Bucket", Name: "hyacinth", @@ -149,7 +266,7 @@ func TestEventHandler(t *testing.T) { // wait for controller to mark the alert as ready g.Eventually(func() bool { - var obj notifyv1.Alert + var obj apiv1.Alert g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), &obj)) return conditions.IsReady(&obj) }, 30*time.Second, time.Second).Should(BeTrue()) diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index f6c6c28fa..7390a38ec 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,11 +23,11 @@ import ( "time" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -41,7 +41,7 @@ import ( "github.com/fluxcd/pkg/runtime/patch" "github.com/fluxcd/pkg/runtime/predicates" - "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" "github.com/fluxcd/notification-controller/internal/notifier" ) @@ -50,7 +50,7 @@ type ProviderReconciler struct { client.Client helper.Metrics - Scheme *runtime.Scheme + ControllerName string } type ProviderReconcilerOptions struct { @@ -64,8 +64,9 @@ func (r *ProviderReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *ProviderReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts ProviderReconcilerOptions) error { return ctrl.NewControllerManagedBy(mgr). - For(&v1beta1.Provider{}). - WithEventFilter(predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{})). + For(&apiv1.Provider{}, builder.WithPredicates( + predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}), + )). WithOptions(controller.Options{ MaxConcurrentReconciles: opts.MaxConcurrentReconciles, RateLimiter: opts.RateLimiter, @@ -76,96 +77,68 @@ func (r *ProviderReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts P // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=providers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=providers/status,verbs=get;update;patch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { - start := time.Now() + reconcileStart := time.Now() log := ctrl.LoggerFrom(ctx) - provider := &v1beta1.Provider{} - if err := r.Get(ctx, req.NamespacedName, provider); err != nil { + obj := &apiv1.Provider{} + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - r.RecordSuspend(ctx, provider, provider.Spec.Suspend) - // return early if the object is suspended - if provider.Spec.Suspend { - log.Info("Reconciliation is suspended for this object") - return ctrl.Result{}, nil - } - - patchHelper, err := patch.NewHelper(provider, r.Client) - if err != nil { - return ctrl.Result{}, err - } + // Initialize the runtime patcher with the current version of the object. + patcher := patch.NewSerialPatcher(obj, r.Client) defer func() { - patchOpts := []patch.Option{ - patch.WithOwnedConditions{ - Conditions: []string{ - meta.ReadyCondition, - meta.ReconcilingCondition, - meta.StalledCondition, - }, - }, - } - - if retErr == nil && (result.IsZero() || !result.Requeue) { - conditions.Delete(provider, meta.ReconcilingCondition) - - patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{}) - - readyCondition := conditions.Get(provider, meta.ReadyCondition) - switch readyCondition.Status { - case metav1.ConditionFalse: - // As we are no longer reconciling and the end-state is not ready, the reconciliation has stalled - conditions.MarkStalled(provider, readyCondition.Reason, readyCondition.Message) - case metav1.ConditionTrue: - // As we are no longer reconciling and the end-state is ready, the reconciliation is no longer stalled - conditions.Delete(provider, meta.StalledCondition) - } - } - - if err := patchHelper.Patch(ctx, provider, patchOpts...); err != nil { - retErr = kerrors.NewAggregate([]error{retErr, err}) - } - - r.Metrics.RecordReadiness(ctx, provider) - r.Metrics.RecordDuration(ctx, provider, start) + // Record Prometheus metrics. + r.Metrics.RecordReadiness(ctx, obj) + r.Metrics.RecordDuration(ctx, obj, reconcileStart) + r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) + // Patch finalizers, status and conditions. + retErr = r.patch(ctx, obj, patcher) }() - if !controllerutil.ContainsFinalizer(provider, v1beta1.NotificationFinalizer) { - controllerutil.AddFinalizer(provider, v1beta1.NotificationFinalizer) + if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { + controllerutil.AddFinalizer(obj, apiv1.NotificationFinalizer) result = ctrl.Result{Requeue: true} return } - if !provider.ObjectMeta.DeletionTimestamp.IsZero() { - controllerutil.RemoveFinalizer(provider, v1beta1.NotificationFinalizer) + if !obj.ObjectMeta.DeletionTimestamp.IsZero() { + controllerutil.RemoveFinalizer(obj, apiv1.NotificationFinalizer) result = ctrl.Result{} return } - return r.reconcile(ctx, provider) + // Return early if the object is suspended. + if obj.Spec.Suspend { + log.Info("Reconciliation is suspended for this object") + return ctrl.Result{}, nil + } + + return r.reconcile(ctx, obj) } -func (r *ProviderReconciler) reconcile(ctx context.Context, obj *v1beta1.Provider) (ctrl.Result, error) { +func (r *ProviderReconciler) reconcile(ctx context.Context, obj *apiv1.Provider) (ctrl.Result, error) { // Mark the resource as under reconciliation - conditions.MarkReconciling(obj, meta.ProgressingReason, "") + conditions.MarkReconciling(obj, meta.ProgressingReason, "Reconciliation in progress") // validate provider spec and credentials if err := r.validate(ctx, obj); err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, v1beta1.ValidationFailedReason, err.Error()) - return ctrl.Result{}, err + conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.ValidationFailedReason, err.Error()) + return ctrl.Result{Requeue: true}, err } - conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, v1beta1.InitializedReason) + conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, apiv1.InitializedReason) ctrl.LoggerFrom(ctx).Info("Provider initialized") return ctrl.Result{}, nil } -func (r *ProviderReconciler) validate(ctx context.Context, provider *v1beta1.Provider) error { +func (r *ProviderReconciler) validate(ctx context.Context, provider *apiv1.Provider) error { address := provider.Spec.Address proxy := provider.Spec.Proxy username := provider.Spec.Username @@ -240,3 +213,53 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider *v1beta1.Pro return nil } + +// patch updates the object status, conditions and finalizers. +func (r *ProviderReconciler) patch(ctx context.Context, obj *apiv1.Provider, patcher *patch.SerialPatcher) (retErr error) { + // Configure the runtime patcher. + patchOpts := []patch.Option{} + ownedConditions := []string{ + meta.ReadyCondition, + meta.ReconcilingCondition, + meta.StalledCondition, + } + patchOpts = append(patchOpts, + patch.WithOwnedConditions{Conditions: ownedConditions}, + patch.WithForceOverwriteConditions{}, + patch.WithFieldOwner(r.ControllerName), + ) + + // Set the value of the reconciliation request in status. + if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok { + obj.Status.LastHandledReconcileAt = v + } + + // Remove the Reconciling condition and update the observed generation + // if the reconciliation was successful. + if conditions.IsTrue(obj, meta.ReadyCondition) { + conditions.Delete(obj, meta.ReconcilingCondition) + obj.Status.ObservedGeneration = obj.Generation + } + + // Set the Reconciling reason to ProgressingWithRetry if the + // reconciliation has failed. + if conditions.IsFalse(obj, meta.ReadyCondition) && + conditions.Has(obj, meta.ReconcilingCondition) { + rc := conditions.Get(obj, meta.ReconcilingCondition) + rc.Reason = apiv1.ProgressingWithRetryReason + conditions.Set(obj, rc) + } + + // Patch the object status, conditions and finalizers. + if err := patcher.Patch(ctx, obj, patchOpts...); err != nil { + if !obj.GetDeletionTimestamp().IsZero() { + err = kerrors.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) }) + } + retErr = kerrors.NewAggregate([]error{retErr, err}) + if retErr != nil { + return retErr + } + } + + return nil +} diff --git a/controllers/provider_controller_test.go b/controllers/provider_controller_test.go new file mode 100644 index 000000000..ded30ca8d --- /dev/null +++ b/controllers/provider_controller_test.go @@ -0,0 +1,148 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "testing" + "time" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/conditions" + + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" +) + +func TestProviderReconciler_Reconcile(t *testing.T) { + g := NewWithT(t) + timeout := 5 * time.Second + resultP := &apiv1.Provider{} + namespaceName := "provider-" + randStringRunes(5) + secretName := "secret-" + randStringRunes(5) + + g.Expect(createNamespace(namespaceName)).NotTo(HaveOccurred(), "failed to create test namespace") + + providerKey := types.NamespacedName{ + Name: fmt.Sprintf("provider-%s", randStringRunes(5)), + Namespace: namespaceName, + } + provider := &apiv1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerKey.Name, + Namespace: providerKey.Namespace, + }, + Spec: apiv1.ProviderSpec{ + Type: "generic", + Address: "https://webhook.internal", + }, + } + g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed()) + + t.Run("reports ready status", func(t *testing.T) { + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) + return resultP.Status.ObservedGeneration == resultP.Generation + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.IsReady(resultP)).To(BeTrue()) + g.Expect(conditions.GetReason(resultP, meta.ReadyCondition)).To(BeIdenticalTo(meta.SucceededReason)) + + g.Expect(conditions.Has(resultP, meta.ReconcilingCondition)).To(BeFalse()) + g.Expect(controllerutil.ContainsFinalizer(resultP, apiv1.NotificationFinalizer)).To(BeTrue()) + }) + + t.Run("fails with secret not found error", func(t *testing.T) { + resultP.Spec.SecretRef = &meta.LocalObjectReference{ + Name: secretName, + } + g.Expect(k8sClient.Update(context.Background(), resultP)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) + return !conditions.IsReady(resultP) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.GetReason(resultP, meta.ReadyCondition)).To(BeIdenticalTo(apiv1.ValidationFailedReason)) + g.Expect(conditions.GetMessage(resultP, meta.ReadyCondition)).To(ContainSubstring(secretName)) + + g.Expect(conditions.Has(resultP, meta.ReconcilingCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(resultP, meta.ReconcilingCondition)).To(BeIdenticalTo(apiv1.ProgressingWithRetryReason)) + g.Expect(conditions.GetObservedGeneration(resultP, meta.ReconcilingCondition)).To(BeIdenticalTo(resultP.Generation)) + g.Expect(resultP.Status.ObservedGeneration).To(BeIdenticalTo(resultP.Generation - 1)) + }) + + t.Run("recovers when secret exists", func(t *testing.T) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespaceName, + }, + StringData: map[string]string{ + "token": "test", + }, + } + g.Expect(k8sClient.Create(context.Background(), secret)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) + return conditions.IsReady(resultP) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.GetObservedGeneration(resultP, meta.ReadyCondition)).To(BeIdenticalTo(resultP.Generation)) + g.Expect(resultP.Status.ObservedGeneration).To(BeIdenticalTo(resultP.Generation)) + g.Expect(conditions.Has(resultP, meta.ReconcilingCondition)).To(BeFalse()) + }) + + t.Run("handles reconcileAt", func(t *testing.T) { + reconcileRequestAt := metav1.Now().String() + resultP.SetAnnotations(map[string]string{ + meta.ReconcileRequestAnnotation: reconcileRequestAt, + }) + g.Expect(k8sClient.Update(context.Background(), resultP)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) + return resultP.Status.LastHandledReconcileAt == reconcileRequestAt + }, timeout, time.Second).Should(BeTrue()) + }) + + t.Run("finalizes suspended object", func(t *testing.T) { + resultP.Spec.Suspend = true + g.Expect(k8sClient.Update(context.Background(), resultP)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) + return resultP.Spec.Suspend == true + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(k8sClient.Delete(context.Background(), resultP)).To(Succeed()) + + g.Eventually(func() bool { + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) + return apierrors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) + }) +} diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index 28586892a..732d44cb8 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,29 +23,32 @@ import ( "time" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/errors" + kerrors "k8s.io/apimachinery/pkg/util/errors" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/ratelimiter" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" helper "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/patch" + "github.com/fluxcd/pkg/runtime/predicates" - "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" ) // ReceiverReconciler reconciles a Receiver object type ReceiverReconciler struct { client.Client helper.Metrics - Scheme *runtime.Scheme + + ControllerName string } type ReceiverReconcilerOptions struct { @@ -53,12 +56,31 @@ type ReceiverReconcilerOptions struct { RateLimiter ratelimiter.RateLimiter } +func (r *ReceiverReconciler) SetupWithManager(mgr ctrl.Manager) error { + return r.SetupWithManagerAndOptions(mgr, ReceiverReconcilerOptions{}) +} + +func (r *ReceiverReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts ReceiverReconcilerOptions) error { + return ctrl.NewControllerManagedBy(mgr). + For(&apiv1.Receiver{}, builder.WithPredicates( + predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}), + )). + WithOptions(controller.Options{ + MaxConcurrentReconciles: opts.MaxConcurrentReconciles, + RateLimiter: opts.RateLimiter, + RecoverPanic: true, + }). + Complete(r) +} + // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=receivers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=receivers/status,verbs=get;update;patch // +kubebuilder:rbac:groups=source.fluxcd.io,resources=buckets,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=source.fluxcd.io,resources=buckets/status,verbs=get // +kubebuilder:rbac:groups=source.fluxcd.io,resources=gitrepositories,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=source.fluxcd.io,resources=gitrepositories/status,verbs=get +// +kubebuilder:rbac:groups=source.fluxcd.io,resources=ocirepositories,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=source.fluxcd.io,resources=ocirepositories/status,verbs=get // +kubebuilder:rbac:groups=source.fluxcd.io,resources=helmrepositories,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=source.fluxcd.io,resources=helmrepositories/status,verbs=get // +kubebuilder:rbac:groups=image.fluxcd.io,resources=imagerepositories,verbs=get;list;watch;update;patch @@ -66,124 +88,124 @@ type ReceiverReconcilerOptions struct { // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { - start := time.Now() + reconcileStart := time.Now() log := ctrl.LoggerFrom(ctx) - receiver := &v1beta1.Receiver{} - if err := r.Get(ctx, req.NamespacedName, receiver); err != nil { + obj := &apiv1.Receiver{} + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - // Record suspension metrics - defer r.RecordSuspend(ctx, receiver, receiver.Spec.Suspend) - // Return early if the object is suspended - if receiver.Spec.Suspend { - log.Info("Reconciliation is suspended for this object") - return ctrl.Result{}, nil - } + // Initialize the runtime patcher with the current version of the object. + patcher := patch.NewSerialPatcher(obj, r.Client) - // Initialize the patch helper - patchHelper, err := patch.NewHelper(receiver, r.Client) - if err != nil { - return ctrl.Result{}, err - } defer func() { - // Patch the object, ignoring conflicts on the conditions owned by this controller - patchOpts := []patch.Option{ - patch.WithOwnedConditions{ - Conditions: []string{ - meta.ReadyCondition, - meta.ReconcilingCondition, - meta.StalledCondition, - }, - }, - } - - // Determine if the resource is still being reconciled, or if it has stalled, and record this observation - if retErr == nil && (result.IsZero() || !result.Requeue) { - // We are no longer reconciling - conditions.Delete(receiver, meta.ReconcilingCondition) - - // We have now observed this generation - patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{}) - - readyCondition := conditions.Get(receiver, meta.ReadyCondition) - switch readyCondition.Status { - case metav1.ConditionFalse: - // As we are no longer reconciling and the end-state is not ready, the reconciliation has stalled - conditions.MarkStalled(receiver, readyCondition.Reason, readyCondition.Message) - case metav1.ConditionTrue: - // As we are no longer reconciling and the end-state is ready, the reconciliation is no longer stalled - conditions.Delete(receiver, meta.StalledCondition) - } - } - - // Finally, patch the resource - if err := patchHelper.Patch(ctx, receiver, patchOpts...); err != nil { - retErr = errors.NewAggregate([]error{retErr, err}) - } - - // Always record readiness and duration metrics - r.Metrics.RecordReadiness(ctx, receiver) - r.Metrics.RecordDuration(ctx, receiver, start) + // Record Prometheus metrics. + r.Metrics.RecordReadiness(ctx, obj) + r.Metrics.RecordDuration(ctx, obj, reconcileStart) + r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) + // Patch finalizers, status and conditions. + retErr = r.patch(ctx, obj, patcher) }() - if !controllerutil.ContainsFinalizer(receiver, v1beta1.NotificationFinalizer) { - controllerutil.AddFinalizer(receiver, v1beta1.NotificationFinalizer) + if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { + controllerutil.AddFinalizer(obj, apiv1.NotificationFinalizer) result = ctrl.Result{Requeue: true} return } - if !receiver.ObjectMeta.DeletionTimestamp.IsZero() { - controllerutil.RemoveFinalizer(receiver, v1beta1.NotificationFinalizer) + if !obj.ObjectMeta.DeletionTimestamp.IsZero() { + controllerutil.RemoveFinalizer(obj, apiv1.NotificationFinalizer) result = ctrl.Result{} return } - return r.reconcile(ctx, receiver) -} - -func (r *ReceiverReconciler) SetupWithManager(mgr ctrl.Manager) error { - return r.SetupWithManagerAndOptions(mgr, ReceiverReconcilerOptions{}) -} + // Return early if the object is suspended. + if obj.Spec.Suspend { + log.Info("Reconciliation is suspended for this object") + return ctrl.Result{}, nil + } -func (r *ReceiverReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts ReceiverReconcilerOptions) error { - return ctrl.NewControllerManagedBy(mgr). - For(&v1beta1.Receiver{}). - WithOptions(controller.Options{ - MaxConcurrentReconciles: opts.MaxConcurrentReconciles, - RateLimiter: opts.RateLimiter, - RecoverPanic: true, - }). - Complete(r) + return r.reconcile(ctx, obj) } // reconcile steps through the actual reconciliation tasks for the object, it returns early on the first step that // produces an error. -func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *v1beta1.Receiver) (ctrl.Result, error) { +func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *apiv1.Receiver) (ctrl.Result, error) { // Mark the resource as under reconciliation - conditions.MarkReconciling(obj, meta.ProgressingReason, "") + conditions.MarkReconciling(obj, meta.ProgressingReason, "Reconciliation in progress") token, err := r.token(ctx, obj) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, v1beta1.TokenNotFoundReason, err.Error()) - return ctrl.Result{}, err + conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.TokenNotFoundReason, err.Error()) + return ctrl.Result{Requeue: true}, err } receiverURL := fmt.Sprintf("/hook/%s", sha256sum(token+obj.Name+obj.Namespace)) + msg := fmt.Sprintf("Receiver initialized with URL: %s", receiverURL) // Mark the resource as ready and set the URL - conditions.MarkTrue(obj, meta.ReadyCondition, v1beta1.InitializedReason, "Receiver initialized with URL: %s", receiverURL) + conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, msg) obj.Status.URL = receiverURL - ctrl.LoggerFrom(ctx).Info("Receiver initialized") + ctrl.LoggerFrom(ctx).Info(msg) return ctrl.Result{}, nil } +// patch updates the object status, conditions and finalizers. +func (r *ReceiverReconciler) patch(ctx context.Context, obj *apiv1.Receiver, patcher *patch.SerialPatcher) (retErr error) { + // Configure the runtime patcher. + patchOpts := []patch.Option{} + ownedConditions := []string{ + meta.ReadyCondition, + meta.ReconcilingCondition, + meta.StalledCondition, + } + patchOpts = append(patchOpts, + patch.WithOwnedConditions{Conditions: ownedConditions}, + patch.WithForceOverwriteConditions{}, + patch.WithFieldOwner(r.ControllerName), + ) + + // Set the value of the reconciliation request in status. + if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok { + obj.Status.LastHandledReconcileAt = v + } + + // Remove the Reconciling condition and update the observed generation + // if the reconciliation was successful. + if conditions.IsTrue(obj, meta.ReadyCondition) { + conditions.Delete(obj, meta.ReconcilingCondition) + obj.Status.ObservedGeneration = obj.Generation + } + + // Set the Reconciling reason to ProgressingWithRetry if the + // reconciliation has failed. + if conditions.IsFalse(obj, meta.ReadyCondition) && + conditions.Has(obj, meta.ReconcilingCondition) { + rc := conditions.Get(obj, meta.ReconcilingCondition) + rc.Reason = apiv1.ProgressingWithRetryReason + conditions.Set(obj, rc) + } + + // Patch the object status, conditions and finalizers. + if err := patcher.Patch(ctx, obj, patchOpts...); err != nil { + if !obj.GetDeletionTimestamp().IsZero() { + err = kerrors.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) }) + } + retErr = kerrors.NewAggregate([]error{retErr, err}) + if retErr != nil { + return retErr + } + } + + return nil +} + // token extract the token value from the secret object -func (r *ReceiverReconciler) token(ctx context.Context, receiver *v1beta1.Receiver) (string, error) { +func (r *ReceiverReconciler) token(ctx context.Context, receiver *apiv1.Receiver) (string, error) { token := "" secretName := types.NamespacedName{ Namespace: receiver.GetNamespace(), diff --git a/controllers/receiver_controller_test.go b/controllers/receiver_controller_test.go index eacba5f50..33d7fc8c3 100644 --- a/controllers/receiver_controller_test.go +++ b/controllers/receiver_controller_test.go @@ -1,11 +1,25 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package controllers import ( "context" "fmt" "net/http" - "os" - "strings" "testing" "time" @@ -13,21 +27,159 @@ import ( prommetrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logf "sigs.k8s.io/controller-runtime/pkg/log" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/ssa" - notifyv1 "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" "github.com/fluxcd/notification-controller/internal/server" ) -func TestReceiverHandler(t *testing.T) { +func TestReceiverReconciler_Reconcile(t *testing.T) { + g := NewWithT(t) + timeout := 5 * time.Second + resultR := &apiv1.Receiver{} + namespaceName := "receiver-" + randStringRunes(5) + secretName := "secret-" + randStringRunes(5) + + g.Expect(createNamespace(namespaceName)).NotTo(HaveOccurred(), "failed to create test namespace") + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespaceName, + }, + StringData: map[string]string{ + "token": "test", + }, + } + g.Expect(k8sClient.Create(context.Background(), secret)).To(Succeed()) + + receiverKey := types.NamespacedName{ + Name: fmt.Sprintf("receiver-%s", randStringRunes(5)), + Namespace: namespaceName, + } + receiver := &apiv1.Receiver{ + ObjectMeta: metav1.ObjectMeta{ + Name: receiverKey.Name, + Namespace: receiverKey.Namespace, + }, + Spec: apiv1.ReceiverSpec{ + Type: "generic", + Events: []string{"push"}, + Resources: []apiv1.CrossNamespaceObjectReference{ + { + Name: "podinfo", + Kind: "GitRepository", + }, + }, + SecretRef: meta.LocalObjectReference{ + Name: secretName, + }, + }, + } + g.Expect(k8sClient.Create(context.Background(), receiver)).To(Succeed()) + + t.Run("reports ready status", func(t *testing.T) { + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) + return resultR.Status.ObservedGeneration == resultR.Generation + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.IsReady(resultR)).To(BeTrue()) + g.Expect(conditions.GetReason(resultR, meta.ReadyCondition)).To(BeIdenticalTo(meta.SucceededReason)) + + g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeFalse()) + g.Expect(controllerutil.ContainsFinalizer(resultR, apiv1.NotificationFinalizer)).To(BeTrue()) + }) + + t.Run("fails with secret not found error", func(t *testing.T) { + g.Expect(k8sClient.Delete(context.Background(), secret)).To(Succeed()) + + reconcileRequestAt := metav1.Now().String() + resultR.SetAnnotations(map[string]string{ + meta.ReconcileRequestAnnotation: reconcileRequestAt, + }) + g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) + return !conditions.IsReady(resultR) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.GetReason(resultR, meta.ReadyCondition)).To(BeIdenticalTo(apiv1.TokenNotFoundReason)) + g.Expect(conditions.GetMessage(resultR, meta.ReadyCondition)).To(ContainSubstring(secretName)) + + g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(resultR, meta.ReconcilingCondition)).To(BeIdenticalTo(apiv1.ProgressingWithRetryReason)) + g.Expect(conditions.GetObservedGeneration(resultR, meta.ReconcilingCondition)).To(BeIdenticalTo(resultR.Generation)) + }) + + t.Run("recovers when secret exists", func(t *testing.T) { + newSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespaceName, + }, + StringData: map[string]string{ + "token": "test", + }, + } + g.Expect(k8sClient.Create(context.Background(), newSecret)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) + return conditions.IsReady(resultR) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.GetObservedGeneration(resultR, meta.ReadyCondition)).To(BeIdenticalTo(resultR.Generation)) + g.Expect(resultR.Status.ObservedGeneration).To(BeIdenticalTo(resultR.Generation)) + g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeFalse()) + }) + + t.Run("handles reconcileAt", func(t *testing.T) { + reconcileRequestAt := metav1.Now().String() + resultR.SetAnnotations(map[string]string{ + meta.ReconcileRequestAnnotation: reconcileRequestAt, + }) + g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) + return resultR.Status.LastHandledReconcileAt == reconcileRequestAt + }, timeout, time.Second).Should(BeTrue()) + }) + + t.Run("finalizes suspended object", func(t *testing.T) { + resultR.Spec.Suspend = true + g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) + return resultR.Spec.Suspend == true + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(k8sClient.Delete(context.Background(), resultR)).To(Succeed()) + + g.Eventually(func() bool { + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) + return apierrors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) + }) +} + +func TestReceiverReconciler_EventHandler(t *testing.T) { g := NewWithT(t) + timeout := 30 * time.Second + resultR := &apiv1.Receiver{} receiverServer := server.NewReceiverServer("127.0.0.1:56788", logf.Log, k8sClient) receiverMdlw := middleware.New(middleware.Config{ @@ -77,15 +229,15 @@ func TestReceiverHandler(t *testing.T) { Name: fmt.Sprintf("test-receiver-%s", randStringRunes(5)), } - receiver := ¬ifyv1.Receiver{ + receiver := &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: receiverKey.Name, Namespace: receiverKey.Namespace, }, - Spec: notifyv1.ReceiverSpec{ + Spec: apiv1.ReceiverSpec{ Type: "generic", Events: []string{"pull"}, - Resources: []notifyv1.CrossNamespaceObjectReference{ + Resources: []apiv1.CrossNamespaceObjectReference{ { Name: "podinfo", Kind: "GitRepository", @@ -101,44 +253,39 @@ func TestReceiverHandler(t *testing.T) { address := fmt.Sprintf("/hook/%s", sha256sum(token+receiverKey.Name+receiverKey.Namespace)) - var rcvrObj notifyv1.Receiver - g.Eventually(func() bool { - g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), &rcvrObj)) - return rcvrObj.Status.URL == address - }, 30*time.Second, time.Second).Should(BeTrue()) - - // Update receiver and check that url doesn't change - rcvrObj.Spec.Events = []string{"ping", "push"} - g.Expect(k8sClient.Update(context.Background(), &rcvrObj)).To(Succeed()) - g.Consistently(func() bool { - var obj notifyv1.Receiver - g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), &obj)).To(Succeed()) - return obj.Status.URL == address - }, 30*time.Second, time.Second).Should(BeTrue()) - - res, err := http.Post("http://localhost:56788/"+address, "application/json", nil) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(res.StatusCode).To(Equal(http.StatusOK)) - g.Eventually(func() bool { - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(object.GroupVersionKind()) - g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(object), obj)).To(Succeed()) - v, ok := obj.GetAnnotations()[meta.ReconcileRequestAnnotation] - return ok && v != "" - }, 30*time.Second, time.Second).Should(BeTrue()) -} + t.Run("generates URL when ready", func(t *testing.T) { + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) + return conditions.IsReady(resultR) + }, timeout, time.Second).Should(BeTrue()) -func readManifest(manifest, namespace string) (*unstructured.Unstructured, error) { - data, err := os.ReadFile(manifest) - if err != nil { - return nil, err - } - yml := fmt.Sprintf(string(data), namespace) + g.Expect(resultR.Status.URL).To(BeIdenticalTo(address)) + }) - object, err := ssa.ReadObject(strings.NewReader(yml)) - if err != nil { - return nil, err - } + t.Run("doesn't update the URL on spec updates", func(t *testing.T) { + resultR.Spec.Events = []string{"ping", "push"} + g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) + return resultR.Status.ObservedGeneration == resultR.Generation + }, timeout, time.Second).Should(BeTrue()) - return object, nil + g.Expect(conditions.IsReady(resultR)) + g.Expect(resultR.Status.URL).To(BeIdenticalTo(address)) + }) + + t.Run("handles event", func(t *testing.T) { + res, err := http.Post("http://localhost:56788/"+address, "application/json", nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(res.StatusCode).To(Equal(http.StatusOK)) + + g.Eventually(func() bool { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(object.GroupVersionKind()) + g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(object), obj)).To(Succeed()) + v, ok := obj.GetAnnotations()[meta.ReconcileRequestAnnotation] + return ok && v != "" + }, timeout, time.Second).Should(BeTrue()) + }) } diff --git a/controllers/suite_test.go b/controllers/suite_test.go index e022146f3..90f2ad042 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2020, 2021 The Flux authors +Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import ( "math/rand" "os" "path/filepath" + "strings" "testing" "github.com/fluxcd/pkg/runtime/controller" @@ -29,6 +30,7 @@ import ( "github.com/fluxcd/pkg/ssa" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/cli-utils/pkg/kstatus/polling" @@ -36,7 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - notifyv1 "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" // +kubebuilder:scaffold:imports ) @@ -49,7 +51,7 @@ var ( func TestMain(m *testing.M) { var err error - utilruntime.Must(notifyv1.AddToScheme(scheme.Scheme)) + utilruntime.Must(apiv1.AddToScheme(scheme.Scheme)) //utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) testEnv = testenv.New(testenv.WithCRDPath( @@ -61,26 +63,32 @@ func TestMain(m *testing.M) { panic(fmt.Sprintf("failed to create k8s client: %v", err)) } + controllerName := "notification-controller" testMetricsH := controller.MustMakeMetrics(testEnv) - //controllerName := "notification-controller" + reconciler := AlertReconciler{ - Client: testEnv, - Metrics: testMetricsH, + Client: testEnv, + Metrics: testMetricsH, + ControllerName: controllerName, } if err := (reconciler).SetupWithManager(testEnv); err != nil { panic(fmt.Sprintf("Failed to start AlerReconciler: %v", err)) } if err := (&ProviderReconciler{ - Client: testEnv, + Client: testEnv, + Metrics: testMetricsH, + ControllerName: controllerName, }).SetupWithManager(testEnv); err != nil { - panic(fmt.Sprintf("Failed to start PRoviderReconciler: %v", err)) + panic(fmt.Sprintf("Failed to start ProviderReconciler: %v", err)) } if err := (&ReceiverReconciler{ - Client: testEnv, + Client: testEnv, + Metrics: testMetricsH, + ControllerName: controllerName, }).SetupWithManager(testEnv); err != nil { - panic(fmt.Sprintf("Failed to start PRoviderReconciler: %v", err)) + panic(fmt.Sprintf("Failed to start ReceiverReconciler: %v", err)) } go func() { @@ -98,8 +106,8 @@ func TestMain(m *testing.M) { poller := polling.NewStatusPoller(k8sClient, restMapper, polling.Options{}) owner := ssa.Owner{ - Field: "notification-controller", - Group: "notification-controller", + Field: controllerName, + Group: controllerName, } manager = ssa.NewResourceManager(k8sClient, poller, owner) @@ -131,3 +139,18 @@ func createNamespace(name string) error { } return k8sClient.Create(context.Background(), namespace) } + +func readManifest(manifest, namespace string) (*unstructured.Unstructured, error) { + data, err := os.ReadFile(manifest) + if err != nil { + return nil, err + } + yml := fmt.Sprintf(string(data), namespace) + + object, err := ssa.ReadObject(strings.NewReader(yml)) + if err != nil { + return nil, err + } + + return object, nil +} diff --git a/docs/api/notification.md b/docs/api/notification.md index 1033671a8..7df6e6029 100644 --- a/docs/api/notification.md +++ b/docs/api/notification.md @@ -2,20 +2,20 @@

Packages:

-

notification.toolkit.fluxcd.io/v1beta1

-

Package v1beta1 contains API Schema definitions for the notification v1beta1 API group

+

notification.toolkit.fluxcd.io/v1beta2

+

Package v1beta2 contains API Schema definitions for the notification v1beta2 API group

Resource Types: -

Alert +

Alert

Alert is the Schema for the alerts API

@@ -33,7 +33,7 @@ Resource Types: apiVersion
string -notification.toolkit.fluxcd.io/v1beta1 +notification.toolkit.fluxcd.io/v1beta2 @@ -63,7 +63,7 @@ Refer to the Kubernetes API documentation for the fields of the spec
- + AlertSpec @@ -102,7 +102,7 @@ If set to ‘info’ no events will be filtered.

eventSources
- + []CrossNamespaceObjectReference @@ -155,7 +155,7 @@ Defaults to false.

status
- + AlertStatus @@ -167,7 +167,7 @@ AlertStatus
-

Provider +

Provider

Provider is the Schema for the providers API

@@ -185,7 +185,7 @@ AlertStatus apiVersion
string -notification.toolkit.fluxcd.io/v1beta1 +notification.toolkit.fluxcd.io/v1beta2 @@ -215,7 +215,7 @@ Refer to the Kubernetes API documentation for the fields of the spec
- + ProviderSpec @@ -347,7 +347,7 @@ Defaults to false.

status
- + ProviderStatus @@ -359,7 +359,7 @@ ProviderStatus
-

Receiver +

Receiver

Receiver is the Schema for the receivers API

@@ -377,7 +377,7 @@ ProviderStatus apiVersion
string -notification.toolkit.fluxcd.io/v1beta1 +notification.toolkit.fluxcd.io/v1beta2 @@ -407,7 +407,7 @@ Refer to the Kubernetes API documentation for the fields of the spec
- + ReceiverSpec @@ -445,7 +445,7 @@ e.g. ‘push’ for GitHub or ‘Push Hook’ for GitLab.

resources
- + []CrossNamespaceObjectReference @@ -488,7 +488,7 @@ Defaults to false.

status
- + ReceiverStatus @@ -500,11 +500,11 @@ ReceiverStatus
-

AlertSpec +

AlertSpec

(Appears on: -Alert) +Alert)

AlertSpec defines an alerting rule for events involving a list of objects

@@ -547,7 +547,7 @@ If set to ‘info’ no events will be filtered.

eventSources
- + []CrossNamespaceObjectReference @@ -597,11 +597,11 @@ Defaults to false.

-

AlertStatus +

AlertStatus

(Appears on: -Alert) +Alert)

AlertStatus defines the observed state of Alert

@@ -616,6 +616,21 @@ Defaults to false.

+ReconcileRequestStatus
+ + +github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus + + + + +

+(Members of ReconcileRequestStatus are embedded into this type.) +

+ + + + conditions
@@ -643,12 +658,12 @@ int64
-

CrossNamespaceObjectReference +

CrossNamespaceObjectReference

(Appears on: -AlertSpec, -ReceiverSpec) +AlertSpec, +ReceiverSpec)

CrossNamespaceObjectReference contains enough information to let you locate the typed referenced object at cluster level

@@ -726,11 +741,11 @@ operator is “In”, and the values array contains only “value&rd -

ProviderSpec +

ProviderSpec

(Appears on: -Provider) +Provider)

ProviderSpec defines the desired state of Provider

@@ -863,11 +878,11 @@ Defaults to false.

-

ProviderStatus +

ProviderStatus

(Appears on: -Provider) +Provider)

ProviderStatus defines the observed state of Provider

@@ -882,14 +897,17 @@ Defaults to false.

-observedGeneration
+ReconcileRequestStatus
-int64 + +github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus + -(Optional) -

ObservedGeneration is the last reconciled generation.

+

+(Members of ReconcileRequestStatus are embedded into this type.) +

@@ -905,15 +923,27 @@ int64 (Optional) + + +observedGeneration
+ +int64 + + + +(Optional) +

ObservedGeneration is the last reconciled generation.

+ +
-

ReceiverSpec +

ReceiverSpec

(Appears on: -Receiver) +Receiver)

ReceiverSpec defines the desired state of Receiver

@@ -955,7 +985,7 @@ e.g. ‘push’ for GitHub or ‘Push Hook’ for GitLab.

resources
- + []CrossNamespaceObjectReference @@ -995,11 +1025,11 @@ Defaults to false.

-

ReceiverStatus +

ReceiverStatus

(Appears on: -Receiver) +Receiver)

ReceiverStatus defines the observed state of Receiver

@@ -1014,6 +1044,21 @@ Defaults to false.

+ReconcileRequestStatus
+ + +github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus + + + + +

+(Members of ReconcileRequestStatus are embedded into this type.) +

+ + + + conditions
diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index ae5141121..74dbebc30 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go index 91a52d90c..52d8eabb4 100644 --- a/internal/notifier/factory.go +++ b/internal/notifier/factory.go @@ -20,7 +20,7 @@ import ( "crypto/x509" "fmt" - "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" ) type Factory struct { @@ -55,47 +55,47 @@ func (f Factory) Notifier(provider string) (Interface, error) { var n Interface var err error switch provider { - case v1beta1.GenericProvider: + case apiv1.GenericProvider: n, err = NewForwarder(f.URL, f.ProxyURL, f.Headers, f.CertPool, nil) - case v1beta1.GenericHMACProvider: + case apiv1.GenericHMACProvider: n, err = NewForwarder(f.URL, f.ProxyURL, f.Headers, f.CertPool, []byte(f.Token)) - case v1beta1.SlackProvider: + case apiv1.SlackProvider: n, err = NewSlack(f.URL, f.ProxyURL, f.Token, f.CertPool, f.Username, f.Channel) - case v1beta1.DiscordProvider: + case apiv1.DiscordProvider: n, err = NewDiscord(f.URL, f.ProxyURL, f.Username, f.Channel) - case v1beta1.RocketProvider: + case apiv1.RocketProvider: n, err = NewRocket(f.URL, f.ProxyURL, f.CertPool, f.Username, f.Channel) - case v1beta1.MSTeamsProvider: + case apiv1.MSTeamsProvider: n, err = NewMSTeams(f.URL, f.ProxyURL, f.CertPool) - case v1beta1.GitHubProvider: + case apiv1.GitHubProvider: n, err = NewGitHub(f.URL, f.Token, f.CertPool) - case v1beta1.GitHubDispatchProvider: + case apiv1.GitHubDispatchProvider: n, err = NewGitHubDispatch(f.URL, f.Token, f.CertPool) - case v1beta1.GitLabProvider: + case apiv1.GitLabProvider: n, err = NewGitLab(f.URL, f.Token, f.CertPool) - case v1beta1.BitbucketProvider: + case apiv1.BitbucketProvider: n, err = NewBitbucket(f.URL, f.Token, f.CertPool) - case v1beta1.AzureDevOpsProvider: + case apiv1.AzureDevOpsProvider: n, err = NewAzureDevOps(f.URL, f.Token, f.CertPool) - case v1beta1.GoogleChatProvider: + case apiv1.GoogleChatProvider: n, err = NewGoogleChat(f.URL, f.ProxyURL) - case v1beta1.WebexProvider: + case apiv1.WebexProvider: n, err = NewWebex(f.URL, f.ProxyURL, f.CertPool, f.Channel, f.Token) - case v1beta1.SentryProvider: + case apiv1.SentryProvider: n, err = NewSentry(f.CertPool, f.URL, f.Channel) - case v1beta1.AzureEventHubProvider: + case apiv1.AzureEventHubProvider: n, err = NewAzureEventHub(f.URL, f.Token, f.Channel) - case v1beta1.TelegramProvider: + case apiv1.TelegramProvider: n, err = NewTelegram(f.Channel, f.Token) - case v1beta1.LarkProvider: + case apiv1.LarkProvider: n, err = NewLark(f.URL) - case v1beta1.Matrix: + case apiv1.Matrix: n, err = NewMatrix(f.URL, f.Token, f.Channel, f.CertPool) - case v1beta1.OpsgenieProvider: + case apiv1.OpsgenieProvider: n, err = NewOpsgenie(f.URL, f.ProxyURL, f.CertPool, f.Token) - case v1beta1.AlertManagerProvider: + case apiv1.AlertManagerProvider: n, err = NewAlertmanager(f.URL, f.ProxyURL, f.CertPool) - case v1beta1.GrafanaProvider: + case apiv1.GrafanaProvider: n, err = NewGrafana(f.URL, f.ProxyURL, f.Token, f.CertPool, f.Username, f.Password) default: err = fmt.Errorf("provider %s not supported", provider) diff --git a/internal/server/event_handlers.go b/internal/server/event_handlers.go index b4b923ec3..465dd7457 100644 --- a/internal/server/event_handlers.go +++ b/internal/server/event_handlers.go @@ -38,7 +38,7 @@ import ( eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" "github.com/fluxcd/pkg/masktoken" - apiv1 "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" "github.com/fluxcd/notification-controller/internal/notifier" ) diff --git a/internal/server/receiver_handler_test.go b/internal/server/receiver_handler_test.go index 00dbb6548..c28436e40 100644 --- a/internal/server/receiver_handler_test.go +++ b/internal/server/receiver_handler_test.go @@ -23,18 +23,19 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" - "github.com/google/go-github/v41/github" "net/http/httptest" "testing" + "github.com/google/go-github/v41/github" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "github.com/fluxcd/notification-controller/api/v1beta1" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/logger" + + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" ) func Test_validate(t *testing.T) { @@ -48,19 +49,19 @@ func Test_validate(t *testing.T) { hashOpts hashOpts headers map[string]string payload map[string]interface{} - receiver *v1beta1.Receiver + receiver *apiv1.Receiver receiverType string secret *corev1.Secret expectedErr bool }{ { name: "Generic receiver", - receiver: &v1beta1.Receiver{ + receiver: &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: "test-receiver", }, - Spec: v1beta1.ReceiverSpec{ - Type: v1beta1.GenericReceiver, + Spec: apiv1.ReceiverSpec{ + Type: apiv1.GenericReceiver, SecretRef: meta.LocalObjectReference{ Name: "token", }, @@ -78,12 +79,12 @@ func Test_validate(t *testing.T) { }, { name: "gitlab receiver", - receiver: &v1beta1.Receiver{ + receiver: &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: "gitlab-receiver", }, - Spec: v1beta1.ReceiverSpec{ - Type: v1beta1.GitLabReceiver, + Spec: apiv1.ReceiverSpec{ + Type: apiv1.GitLabReceiver, SecretRef: meta.LocalObjectReference{ Name: "token", }, @@ -104,12 +105,12 @@ func Test_validate(t *testing.T) { }, { name: "github receiver", - receiver: &v1beta1.Receiver{ + receiver: &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: "test-receiver", }, - Spec: v1beta1.ReceiverSpec{ - Type: v1beta1.GitHubReceiver, + Spec: apiv1.ReceiverSpec{ + Type: apiv1.GitHubReceiver, SecretRef: meta.LocalObjectReference{ Name: "token", }, @@ -137,12 +138,12 @@ func Test_validate(t *testing.T) { }, { name: "generic hmac receiver", - receiver: &v1beta1.Receiver{ + receiver: &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: "generic-hmac-receiver", }, - Spec: v1beta1.ReceiverSpec{ - Type: v1beta1.GenericHMACReceiver, + Spec: apiv1.ReceiverSpec{ + Type: apiv1.GenericHMACReceiver, SecretRef: meta.LocalObjectReference{ Name: "token", }, @@ -167,12 +168,12 @@ func Test_validate(t *testing.T) { }, { name: "bitbucket receiver", - receiver: &v1beta1.Receiver{ + receiver: &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: "bitbucket-receiver", }, - Spec: v1beta1.ReceiverSpec{ - Type: v1beta1.BitbucketReceiver, + Spec: apiv1.ReceiverSpec{ + Type: apiv1.BitbucketReceiver, Events: []string{"push"}, SecretRef: meta.LocalObjectReference{ Name: "token", @@ -199,12 +200,12 @@ func Test_validate(t *testing.T) { }, { name: "quay receiver", - receiver: &v1beta1.Receiver{ + receiver: &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: "quay-receiver", }, - Spec: v1beta1.ReceiverSpec{ - Type: v1beta1.QuayReceiver, + Spec: apiv1.ReceiverSpec{ + Type: apiv1.QuayReceiver, SecretRef: meta.LocalObjectReference{ Name: "token", }, @@ -228,12 +229,12 @@ func Test_validate(t *testing.T) { }, { name: "harbor receiver", - receiver: &v1beta1.Receiver{ + receiver: &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: "harbor-receiver", }, - Spec: v1beta1.ReceiverSpec{ - Type: v1beta1.HarborReceiver, + Spec: apiv1.ReceiverSpec{ + Type: apiv1.HarborReceiver, SecretRef: meta.LocalObjectReference{ Name: "token", }, @@ -254,12 +255,12 @@ func Test_validate(t *testing.T) { }, { name: "missing secret", - receiver: &v1beta1.Receiver{ + receiver: &apiv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: "missing-secret", }, - Spec: v1beta1.ReceiverSpec{ - Type: v1beta1.GenericReceiver, + Spec: apiv1.ReceiverSpec{ + Type: apiv1.GenericReceiver, SecretRef: meta.LocalObjectReference{ Name: "non-existing", }, @@ -270,7 +271,7 @@ func Test_validate(t *testing.T) { } scheme := runtime.NewScheme() - v1beta1.AddToScheme(scheme) + apiv1.AddToScheme(scheme) corev1.AddToScheme(scheme) for _, tt := range tests { diff --git a/internal/server/receiver_handlers.go b/internal/server/receiver_handlers.go index cbd5a4df0..b713be13b 100644 --- a/internal/server/receiver_handlers.go +++ b/internal/server/receiver_handlers.go @@ -39,7 +39,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/notification-controller/api/v1beta1" + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" ) func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Request) { @@ -49,7 +49,7 @@ func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Req s.logger.Info(fmt.Sprintf("handling request: %s", digest)) - var allReceivers v1beta1.ReceiverList + var allReceivers apiv1.ReceiverList err := s.kubeClient.List(ctx, &allReceivers) if err != nil { s.logger.Error(err, "unable to list receivers") @@ -57,7 +57,7 @@ func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Req return } - receivers := make([]v1beta1.Receiver, 0) + receivers := make([]apiv1.Receiver, 0) for _, receiver := range allReceivers.Items { if !receiver.Spec.Suspend && conditions.IsReady(&receiver) && @@ -74,7 +74,7 @@ func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Req withErrors := false for _, receiver := range receivers { logger := s.logger.WithValues( - "reconciler kind", v1beta1.ReceiverKind, + "reconciler kind", apiv1.ReceiverKind, "name", receiver.Name, "namespace", receiver.Namespace) @@ -104,21 +104,21 @@ func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Req } } -func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver, r *http.Request) error { +func (s *ReceiverServer) validate(ctx context.Context, receiver apiv1.Receiver, r *http.Request) error { token, err := s.token(ctx, receiver) if err != nil { return fmt.Errorf("unable to read token, error: %w", err) } logger := s.logger.WithValues( - "reconciler kind", v1beta1.ReceiverKind, + "reconciler kind", apiv1.ReceiverKind, "name", receiver.Name, "namespace", receiver.Namespace) switch receiver.Spec.Type { - case v1beta1.GenericReceiver: + case apiv1.GenericReceiver: return nil - case v1beta1.GenericHMACReceiver: + case apiv1.GenericHMACReceiver: b, err := io.ReadAll(r.Body) if err != nil { return fmt.Errorf("unable to read request body: %s", err) @@ -129,7 +129,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver return fmt.Errorf("unable to validate HMAC signature: %s", err) } return nil - case v1beta1.GitHubReceiver: + case apiv1.GitHubReceiver: _, err := github.ValidatePayload(r, []byte(token)) if err != nil { return fmt.Errorf("the GitHub signature header is invalid, err: %w", err) @@ -151,7 +151,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver logger.Info(fmt.Sprintf("handling GitHub event: %s", event)) return nil - case v1beta1.GitLabReceiver: + case apiv1.GitLabReceiver: if r.Header.Get("X-Gitlab-Token") != token { return fmt.Errorf("the X-Gitlab-Token header value does not match the receiver token") } @@ -172,7 +172,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver logger.Info(fmt.Sprintf("handling GitLab event: %s", event)) return nil - case v1beta1.BitbucketReceiver: + case apiv1.BitbucketReceiver: _, err := github.ValidatePayload(r, []byte(token)) if err != nil { return fmt.Errorf("the Bitbucket server signature header is invalid, err: %w", err) @@ -194,7 +194,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver logger.Info(fmt.Sprintf("handling Bitbucket server event: %s", event)) return nil - case v1beta1.QuayReceiver: + case apiv1.QuayReceiver: type payload struct { DockerUrl string `json:"docker_url"` UpdatedTags []string `json:"updated_tags"` @@ -207,14 +207,14 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver logger.Info(fmt.Sprintf("handling Quay event from %s", p.DockerUrl)) return nil - case v1beta1.HarborReceiver: + case apiv1.HarborReceiver: if r.Header.Get("Authorization") != token { return fmt.Errorf("the Harbor Authorization header value does not match the receiver token") } logger.Info("handling Harbor event") return nil - case v1beta1.DockerHubReceiver: + case apiv1.DockerHubReceiver: type payload struct { PushData struct { Tag string `json:"tag"` @@ -230,7 +230,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver logger.Info(fmt.Sprintf("handling DockerHub event from %s for tag %s", p.Repository.URL, p.PushData.Tag)) return nil - case v1beta1.GCRReceiver: + case apiv1.GCRReceiver: const ( insert = "insert" tokenIndex = len("Bearer ") @@ -271,7 +271,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver logger.Info(fmt.Sprintf("handling GCR event from %s for tag %s", d.Digest, d.Tag)) return nil - case v1beta1.NexusReceiver: + case apiv1.NexusReceiver: signature := r.Header.Get("X-Nexus-Webhook-Signature") if len(signature) == 0 { return fmt.Errorf("Nexus signature is missing from header") @@ -296,7 +296,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver logger.Info(fmt.Sprintf("handling Nexus event from %s", p.RepositoryName)) return nil - case v1beta1.ACRReceiver: + case apiv1.ACRReceiver: type target struct { Repository string `json:"repository"` Tag string `json:"tag"` @@ -319,7 +319,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver v1beta1.Receiver return fmt.Errorf("recevier type '%s' not supported", receiver.Spec.Type) } -func (s *ReceiverServer) token(ctx context.Context, receiver v1beta1.Receiver) (string, error) { +func (s *ReceiverServer) token(ctx context.Context, receiver apiv1.Receiver) (string, error) { token := "" secretName := types.NamespacedName{ Namespace: receiver.GetNamespace(), @@ -341,7 +341,7 @@ func (s *ReceiverServer) token(ctx context.Context, receiver v1beta1.Receiver) ( return token, nil } -func (s *ReceiverServer) annotate(ctx context.Context, resource v1beta1.CrossNamespaceObjectReference, defaultNamespace string) error { +func (s *ReceiverServer) annotate(ctx context.Context, resource apiv1.CrossNamespaceObjectReference, defaultNamespace string) error { namespace := defaultNamespace if resource.Namespace != "" { namespace = resource.Namespace @@ -355,6 +355,7 @@ func (s *ReceiverServer) annotate(ctx context.Context, resource v1beta1.CrossNam "Bucket": "source.toolkit.fluxcd.io/v1beta2", "HelmRepository": "source.toolkit.fluxcd.io/v1beta2", "GitRepository": "source.toolkit.fluxcd.io/v1beta2", + "OCIRepository": "source.toolkit.fluxcd.io/v1beta2", "ImageRepository": "image.toolkit.fluxcd.io/v1beta1", } diff --git a/main.go b/main.go index 666309416..25d0fc9d8 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,13 +21,6 @@ import ( "os" "time" - "github.com/fluxcd/pkg/runtime/acl" - "github.com/fluxcd/pkg/runtime/client" - helper "github.com/fluxcd/pkg/runtime/controller" - "github.com/fluxcd/pkg/runtime/leaderelection" - "github.com/fluxcd/pkg/runtime/logger" - "github.com/fluxcd/pkg/runtime/pprof" - "github.com/fluxcd/pkg/runtime/probes" "github.com/sethvargo/go-limiter/memorystore" prommetrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" @@ -38,7 +31,15 @@ import ( ctrl "sigs.k8s.io/controller-runtime" crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" - "github.com/fluxcd/notification-controller/api/v1beta1" + "github.com/fluxcd/pkg/runtime/acl" + "github.com/fluxcd/pkg/runtime/client" + helper "github.com/fluxcd/pkg/runtime/controller" + "github.com/fluxcd/pkg/runtime/leaderelection" + "github.com/fluxcd/pkg/runtime/logger" + "github.com/fluxcd/pkg/runtime/pprof" + "github.com/fluxcd/pkg/runtime/probes" + + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" "github.com/fluxcd/notification-controller/controllers" "github.com/fluxcd/notification-controller/internal/server" // +kubebuilder:scaffold:imports @@ -54,7 +55,7 @@ var ( func init() { _ = clientgoscheme.AddToScheme(scheme) - _ = v1beta1.AddToScheme(scheme) + _ = apiv1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } @@ -123,9 +124,9 @@ func main() { metricsH := helper.MustMakeMetrics(mgr) if err = (&controllers.ProviderReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Metrics: metricsH, + Client: mgr.GetClient(), + ControllerName: controllerName, + Metrics: metricsH, }).SetupWithManagerAndOptions(mgr, controllers.ProviderReconcilerOptions{ MaxConcurrentReconciles: concurrent, RateLimiter: helper.GetRateLimiter(rateLimiterOptions), @@ -134,9 +135,9 @@ func main() { os.Exit(1) } if err = (&controllers.AlertReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Metrics: metricsH, + Client: mgr.GetClient(), + ControllerName: controllerName, + Metrics: metricsH, }).SetupWithManagerAndOptions(mgr, controllers.AlertReconcilerOptions{ MaxConcurrentReconciles: concurrent, RateLimiter: helper.GetRateLimiter(rateLimiterOptions), @@ -145,9 +146,9 @@ func main() { os.Exit(1) } if err = (&controllers.ReceiverReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Metrics: metricsH, + Client: mgr.GetClient(), + ControllerName: controllerName, + Metrics: metricsH, }).SetupWithManagerAndOptions(mgr, controllers.ReceiverReconcilerOptions{ MaxConcurrentReconciles: concurrent, RateLimiter: helper.GetRateLimiter(rateLimiterOptions), From 9f2d0e1a6c1b4ce17216484223d07a3461394799 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 27 Oct 2022 17:48:46 +0300 Subject: [PATCH 02/26] Generate unique commit status updates Use the Provider cluster assigned UID to compose a unique commit status ID to avoid name collisions when multiple clusters write to the same repository. Signed-off-by: Stefan Prodan --- controllers/provider_controller.go | 2 +- internal/notifier/azure_devops.go | 21 +++++---- internal/notifier/azure_devops_fuzz_test.go | 13 +++--- internal/notifier/azure_devops_test.go | 6 +-- internal/notifier/bitbucket_fuzz_test.go | 10 ++--- internal/notifier/factory.go | 50 ++++++++++++--------- internal/notifier/github.go | 21 +++++---- internal/notifier/github_fuzz_test.go | 17 ++++--- internal/notifier/github_test.go | 8 ++-- internal/notifier/gitlab.go | 19 ++++---- internal/notifier/gitlab_fuzz_test.go | 13 +++--- internal/notifier/gitlab_test.go | 8 ++-- internal/notifier/util.go | 4 +- internal/notifier/util_test.go | 4 +- internal/server/event_handlers.go | 2 +- 15 files changed, 106 insertions(+), 92 deletions(-) diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index 7390a38ec..087c21c56 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -206,7 +206,7 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider *apiv1.Provi } } - factory := notifier.NewFactory(address, proxy, username, provider.Spec.Channel, token, headers, certPool, password) + factory := notifier.NewFactory(address, proxy, username, provider.Spec.Channel, token, headers, certPool, password, string(provider.UID)) if _, err := factory.Notifier(provider.Spec.Type); err != nil { return fmt.Errorf("failed to initialize provider, error: %w", err) } diff --git a/internal/notifier/azure_devops.go b/internal/notifier/azure_devops.go index 85ee85991..e42217600 100644 --- a/internal/notifier/azure_devops.go +++ b/internal/notifier/azure_devops.go @@ -35,13 +35,14 @@ const genre string = "fluxcd" // AzureDevOps is a Azure DevOps notifier. type AzureDevOps struct { - Project string - Repo string - Client git.Client + Project string + Repo string + ProviderUID string + Client git.Client } // NewAzureDevOps creates and returns a new AzureDevOps notifier. -func NewAzureDevOps(addr string, token string, certPool *x509.CertPool) (*AzureDevOps, error) { +func NewAzureDevOps(providerUID string, addr string, token string, certPool *x509.CertPool) (*AzureDevOps, error) { if len(token) == 0 { return nil, errors.New("azure devops token cannot be empty") } @@ -71,9 +72,10 @@ func NewAzureDevOps(addr string, token string, certPool *x509.CertPool) (*AzureD Client: *client, } return &AzureDevOps{ - Project: proj, - Repo: repo, - Client: gitClient, + Project: proj, + Repo: repo, + ProviderUID: providerUID, + Client: gitClient, }, nil } @@ -99,7 +101,8 @@ func (a AzureDevOps) Post(ctx context.Context, event eventv1.Event) error { // Check if the exact status is already set g := genre - name, desc := formatNameAndDescription(event) + _, desc := formatNameAndDescription(event) + id := generateCommitStatusID(a.ProviderUID, event) createArgs := git.CreateCommitStatusArgs{ Project: &a.Project, RepositoryId: &a.Repo, @@ -109,7 +112,7 @@ func (a AzureDevOps) Post(ctx context.Context, event eventv1.Event) error { State: &state, Context: &git.GitStatusContext{ Genre: &g, - Name: &name, + Name: &id, }, }, } diff --git a/internal/notifier/azure_devops_fuzz_test.go b/internal/notifier/azure_devops_fuzz_test.go index 25c07c72e..1aaafcce7 100644 --- a/internal/notifier/azure_devops_fuzz_test.go +++ b/internal/notifier/azure_devops_fuzz_test.go @@ -33,13 +33,12 @@ import ( const apiLocations = `{"count":0,"value":[{"area":"","id":"428dd4fb-fda5-4722-af02-9313b80305da","routeTemplate":"","resourceName":"","maxVersion":"6.0","minVersion":"5.0","releasedVersion":"6.0"}]}` func Fuzz_AzureDevOps(f *testing.F) { - f.Add("alakazam", "org/proj/_git/repo", "revision/dsa123a", "error", "", []byte{}, []byte(`{"count":1,"value":[{"state":"error","description":"","context":{"genre":"fluxcd","name":"/"}}]}`)) - f.Add("alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":1,"value":[{"state":"info","description":"","context":{"genre":"fluxcd","name":"/"}}]}`)) - f.Add("alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":0,"value":[]}`)) - f.Add("alakazam", "org/proj/_git/repo", "", "", "Progressing", []byte{}, []byte{}) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "error", "", []byte{}, []byte(`{"count":1,"value":[{"state":"error","description":"","context":{"genre":"fluxcd","name":"/"}}]}`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":1,"value":[{"state":"info","description":"","context":{"genre":"fluxcd","name":"/"}}]}`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":0,"value":[]}`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "alakazam", "org/proj/_git/repo", "", "", "Progressing", []byte{}, []byte{}) - f.Fuzz(func(t *testing.T, - token, urlSuffix, revision, severity, reason string, seed, response []byte) { + f.Fuzz(func(t *testing.T, uuid, token, urlSuffix, revision, severity, reason string, seed, response []byte) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "_apis") { w.Write([]byte(apiLocations)) @@ -55,7 +54,7 @@ func Fuzz_AzureDevOps(f *testing.F) { var cert x509.CertPool _ = fuzz.NewConsumer(seed).GenerateStruct(&cert) - azureDevOps, err := NewAzureDevOps(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert) + azureDevOps, err := NewAzureDevOps(uuid, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert) if err != nil { return } diff --git a/internal/notifier/azure_devops_test.go b/internal/notifier/azure_devops_test.go index 75c49f0e3..0e5ebca24 100644 --- a/internal/notifier/azure_devops_test.go +++ b/internal/notifier/azure_devops_test.go @@ -24,19 +24,19 @@ import ( ) func TestNewAzureDevOpsBasic(t *testing.T) { - a, err := NewAzureDevOps("https://dev.azure.com/foo/bar/_git/baz", "foo", nil) + a, err := NewAzureDevOps("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://dev.azure.com/foo/bar/_git/baz", "foo", nil) assert.Nil(t, err) assert.Equal(t, a.Project, "bar") assert.Equal(t, a.Repo, "baz") } func TestNewAzureDevOpsInvalidUrl(t *testing.T) { - _, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "foo", nil) + _, err := NewAzureDevOps("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://dev.azure.com/foo/bar/baz", "foo", nil) assert.NotNil(t, err) } func TestNewAzureDevOpsMissingToken(t *testing.T) { - _, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "", nil) + _, err := NewAzureDevOps("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://dev.azure.com/foo/bar/baz", "", nil) assert.NotNil(t, err) } diff --git a/internal/notifier/bitbucket_fuzz_test.go b/internal/notifier/bitbucket_fuzz_test.go index e92bf2ce3..d525fc377 100644 --- a/internal/notifier/bitbucket_fuzz_test.go +++ b/internal/notifier/bitbucket_fuzz_test.go @@ -31,12 +31,10 @@ import ( ) func Fuzz_Bitbucket(f *testing.F) { - f.Add("user:pass", "org/repo", "revision/dsa123a", "info", []byte{}, []byte(`{"state":"SUCCESSFUL","description":"","key":"","name":"","url":""}`)) - f.Add("user:pass", "org/repo", "revision/dsa123a", "error", []byte{}, []byte(`{}`)) - - f.Fuzz(func(t *testing.T, - token, urlSuffix, revision, severity string, seed, response []byte) { + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "user:pass", "org/repo", "revision/dsa123a", "info", []byte{}, []byte(`{"state":"SUCCESSFUL","description":"","key":"","name":"","url":""}`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "user:pass", "org/repo", "revision/dsa123a", "error", []byte{}, []byte(`{}`)) + f.Fuzz(func(t *testing.T, uuid, token, urlSuffix, revision, severity string, seed, response []byte) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.Copy(io.Discard, r.Body) w.Write(response) @@ -47,7 +45,7 @@ func Fuzz_Bitbucket(f *testing.F) { var cert x509.CertPool _ = fuzz.NewConsumer(seed).GenerateStruct(&cert) - bitbucket, err := NewBitbucket(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert) + bitbucket, err := NewBitbucket(uuid, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert) if err != nil { return } diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go index 52d8eabb4..971921bba 100644 --- a/internal/notifier/factory.go +++ b/internal/notifier/factory.go @@ -24,26 +24,36 @@ import ( ) type Factory struct { - URL string - ProxyURL string - Username string - Channel string - Token string - Headers map[string]string - CertPool *x509.CertPool - Password string + URL string + ProxyURL string + Username string + Channel string + Token string + Headers map[string]string + CertPool *x509.CertPool + Password string + ProviderUID string } -func NewFactory(url string, proxy string, username string, channel string, token string, headers map[string]string, certPool *x509.CertPool, password string) *Factory { +func NewFactory(url string, + proxy string, + username string, + channel string, + token string, + headers map[string]string, + certPool *x509.CertPool, + password string, + providerUID string) *Factory { return &Factory{ - URL: url, - ProxyURL: proxy, - Channel: channel, - Username: username, - Token: token, - Headers: headers, - CertPool: certPool, - Password: password, + URL: url, + ProxyURL: proxy, + Channel: channel, + Username: username, + Token: token, + Headers: headers, + CertPool: certPool, + Password: password, + ProviderUID: providerUID, } } @@ -68,15 +78,15 @@ func (f Factory) Notifier(provider string) (Interface, error) { case apiv1.MSTeamsProvider: n, err = NewMSTeams(f.URL, f.ProxyURL, f.CertPool) case apiv1.GitHubProvider: - n, err = NewGitHub(f.URL, f.Token, f.CertPool) + n, err = NewGitHub(f.ProviderUID, f.URL, f.Token, f.CertPool) case apiv1.GitHubDispatchProvider: n, err = NewGitHubDispatch(f.URL, f.Token, f.CertPool) case apiv1.GitLabProvider: - n, err = NewGitLab(f.URL, f.Token, f.CertPool) + n, err = NewGitLab(f.ProviderUID, f.URL, f.Token, f.CertPool) case apiv1.BitbucketProvider: n, err = NewBitbucket(f.URL, f.Token, f.CertPool) case apiv1.AzureDevOpsProvider: - n, err = NewAzureDevOps(f.URL, f.Token, f.CertPool) + n, err = NewAzureDevOps(f.ProviderUID, f.URL, f.Token, f.CertPool) case apiv1.GoogleChatProvider: n, err = NewGoogleChat(f.URL, f.ProxyURL) case apiv1.WebexProvider: diff --git a/internal/notifier/github.go b/internal/notifier/github.go index 580dbb81a..b83e2b661 100644 --- a/internal/notifier/github.go +++ b/internal/notifier/github.go @@ -34,12 +34,13 @@ import ( ) type GitHub struct { - Owner string - Repo string - Client *github.Client + Owner string + Repo string + ProviderUID string + Client *github.Client } -func NewGitHub(addr string, token string, certPool *x509.CertPool) (*GitHub, error) { +func NewGitHub(providerUID string, addr string, token string, certPool *x509.CertPool) (*GitHub, error) { if len(token) == 0 { return nil, errors.New("github token cannot be empty") } @@ -81,9 +82,10 @@ func NewGitHub(addr string, token string, certPool *x509.CertPool) (*GitHub, err } return &GitHub{ - Owner: comp[0], - Repo: comp[1], - Client: client, + Owner: comp[0], + Repo: comp[1], + ProviderUID: providerUID, + Client: client, }, nil } @@ -106,11 +108,12 @@ func (g *GitHub) Post(ctx context.Context, event eventv1.Event) error { if err != nil { return err } - name, desc := formatNameAndDescription(event) + _, desc := formatNameAndDescription(event) + id := generateCommitStatusID(g.ProviderUID, event) status := &github.RepoStatus{ State: &state, - Context: &name, + Context: &id, Description: &desc, } diff --git a/internal/notifier/github_fuzz_test.go b/internal/notifier/github_fuzz_test.go index 9f615b669..651860430 100644 --- a/internal/notifier/github_fuzz_test.go +++ b/internal/notifier/github_fuzz_test.go @@ -30,15 +30,14 @@ import ( ) func Fuzz_GitHub(f *testing.F) { - f.Add("token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`)) - f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"success","description":""}]`)) - f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`)) - f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/"}]`)) - f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{}]`)) - f.Add("token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{}) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"success","description":""}]`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/"}]`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{}]`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{}) - f.Fuzz(func(t *testing.T, - token, urlSuffix, revision, severity, reason string, seed, response []byte) { + f.Fuzz(func(t *testing.T, uuid, token, urlSuffix, revision, severity, reason string, seed, response []byte) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(response) io.Copy(io.Discard, r.Body) @@ -49,7 +48,7 @@ func Fuzz_GitHub(f *testing.F) { var cert x509.CertPool _ = fuzz.NewConsumer(seed).GenerateStruct(&cert) - github, err := NewGitHub(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert) + github, err := NewGitHub(uuid, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert) if err != nil { return } diff --git a/internal/notifier/github_test.go b/internal/notifier/github_test.go index c320df17d..76532ad0f 100644 --- a/internal/notifier/github_test.go +++ b/internal/notifier/github_test.go @@ -24,7 +24,7 @@ import ( ) func TestNewGitHubBasic(t *testing.T) { - g, err := NewGitHub("https://github.com/foo/bar", "foobar", nil) + g, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://github.com/foo/bar", "foobar", nil) assert.Nil(t, err) assert.Equal(t, g.Owner, "foo") assert.Equal(t, g.Repo, "bar") @@ -32,7 +32,7 @@ func TestNewGitHubBasic(t *testing.T) { } func TestNewEmterpriseGitHubBasic(t *testing.T) { - g, err := NewGitHub("https://foobar.com/foo/bar", "foobar", nil) + g, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://foobar.com/foo/bar", "foobar", nil) assert.Nil(t, err) assert.Equal(t, g.Owner, "foo") assert.Equal(t, g.Repo, "bar") @@ -40,12 +40,12 @@ func TestNewEmterpriseGitHubBasic(t *testing.T) { } func TestNewGitHubInvalidUrl(t *testing.T) { - _, err := NewGitHub("https://github.com/foo/bar/baz", "foobar", nil) + _, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://github.com/foo/bar/baz", "foobar", nil) assert.NotNil(t, err) } func TestNewGitHubEmptyToken(t *testing.T) { - _, err := NewGitHub("https://github.com/foo/bar", "", nil) + _, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://github.com/foo/bar", "", nil) assert.NotNil(t, err) } diff --git a/internal/notifier/gitlab.go b/internal/notifier/gitlab.go index 13c6a69a3..9e976bd66 100644 --- a/internal/notifier/gitlab.go +++ b/internal/notifier/gitlab.go @@ -31,11 +31,12 @@ import ( ) type GitLab struct { - Id string - Client *gitlab.Client + Id string + ProviderUID string + Client *gitlab.Client } -func NewGitLab(addr string, token string, certPool *x509.CertPool) (*GitLab, error) { +func NewGitLab(providerUID string, addr string, token string, certPool *x509.CertPool) (*GitLab, error) { if len(token) == 0 { return nil, errors.New("gitlab token cannot be empty") } @@ -61,8 +62,9 @@ func NewGitLab(addr string, token string, certPool *x509.CertPool) (*GitLab, err } gitlab := &GitLab{ - Id: id, - Client: client, + Id: id, + ProviderUID: providerUID, + Client: client, } return gitlab, nil @@ -88,9 +90,10 @@ func (g *GitLab) Post(ctx context.Context, event eventv1.Event) error { return err } - name, desc := formatNameAndDescription(event) + _, desc := formatNameAndDescription(event) + id := generateCommitStatusID(g.ProviderUID, event) status := &gitlab.CommitStatus{ - Name: name, + Name: id, SHA: rev, Status: string(state), Description: desc, @@ -106,7 +109,7 @@ func (g *GitLab) Post(ctx context.Context, event eventv1.Event) error { } setOpt := &gitlab.SetCommitStatusOptions{ - Name: &name, + Name: &id, Description: &desc, State: state, } diff --git a/internal/notifier/gitlab_fuzz_test.go b/internal/notifier/gitlab_fuzz_test.go index a6d2b4920..fc1d33078 100644 --- a/internal/notifier/gitlab_fuzz_test.go +++ b/internal/notifier/gitlab_fuzz_test.go @@ -30,13 +30,12 @@ import ( ) func Fuzz_GitLab(f *testing.F) { - f.Add("token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`)) - f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`)) - f.Add("token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{}) - f.Add("token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[]`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`)) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{}) + f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[]`)) - f.Fuzz(func(t *testing.T, - token, urlSuffix, revision, severity, reason string, seed, response []byte) { + f.Fuzz(func(t *testing.T, uuid, token, urlSuffix, revision, severity, reason string, seed, response []byte) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(response) io.Copy(io.Discard, r.Body) @@ -47,7 +46,7 @@ func Fuzz_GitLab(f *testing.F) { var cert x509.CertPool _ = fuzz.NewConsumer(seed).GenerateStruct(&cert) - gitLab, err := NewGitLab(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert) + gitLab, err := NewGitLab(uuid, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert) if err != nil { return } diff --git a/internal/notifier/gitlab_test.go b/internal/notifier/gitlab_test.go index 32a50bdf9..753e37cca 100644 --- a/internal/notifier/gitlab_test.go +++ b/internal/notifier/gitlab_test.go @@ -23,25 +23,25 @@ import ( ) func TestNewGitLabBasic(t *testing.T) { - g, err := NewGitLab("https://gitlab.com/foo/bar", "foobar", nil) + g, err := NewGitLab("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://gitlab.com/foo/bar", "foobar", nil) assert.Nil(t, err) assert.Equal(t, g.Id, "foo/bar") } func TestNewGitLabSubgroups(t *testing.T) { - g, err := NewGitLab("https://gitlab.com/foo/bar/baz", "foobar", nil) + g, err := NewGitLab("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://gitlab.com/foo/bar/baz", "foobar", nil) assert.Nil(t, err) assert.Equal(t, g.Id, "foo/bar/baz") } func TestNewGitLabSelfHosted(t *testing.T) { - g, err := NewGitLab("https://example.com/foo/bar", "foo:bar", nil) + g, err := NewGitLab("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com/foo/bar", "foo:bar", nil) assert.Nil(t, err) assert.Equal(t, g.Id, "foo/bar") assert.Equal(t, g.Client.BaseURL().Host, "example.com") } func TestNewGitLabEmptyToken(t *testing.T) { - _, err := NewGitLab("https://gitlab.com/foo/bar", "", nil) + _, err := NewGitLab("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://gitlab.com/foo/bar", "", nil) assert.NotNil(t, err) } diff --git a/internal/notifier/util.go b/internal/notifier/util.go index 4da9324d3..2034948d1 100644 --- a/internal/notifier/util.go +++ b/internal/notifier/util.go @@ -109,11 +109,11 @@ func splitCamelcase(src string) (entries []string) { func parseRevision(rev string) (string, error) { comp := strings.Split(rev, "/") if len(comp) < 2 { - return "", fmt.Errorf("Revision string format incorrect: %v", rev) + return "", fmt.Errorf("revision string format incorrect: %v", rev) } sha := comp[len(comp)-1] if sha == "" { - return "", fmt.Errorf("Commit Sha cannot be empty: %v", rev) + return "", fmt.Errorf("commit SHA cannot be empty: %v", rev) } return sha, nil } diff --git a/internal/notifier/util_test.go b/internal/notifier/util_test.go index 062000e89..4051e562c 100644 --- a/internal/notifier/util_test.go +++ b/internal/notifier/util_test.go @@ -54,13 +54,13 @@ func TestUtil_ParseRevisionNestedBranch(t *testing.T) { func TestUtil_ParseRevisionOneComponents(t *testing.T) { revString := "master" _, err := parseRevision(revString) - require.EqualError(t, err, "Revision string format incorrect: master") + require.EqualError(t, err, "revision string format incorrect: master") } func TestUtil_ParseRevisionTooFewComponents(t *testing.T) { revString := "master/" _, err := parseRevision(revString) - require.EqualError(t, err, "Commit Sha cannot be empty: master/") + require.EqualError(t, err, "commit SHA cannot be empty: master/") } func TestUtil_ParseGitHttps(t *testing.T) { diff --git a/internal/server/event_handlers.go b/internal/server/event_handlers.go index 465dd7457..e7f9c3cc8 100644 --- a/internal/server/event_handlers.go +++ b/internal/server/event_handlers.go @@ -243,7 +243,7 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request) continue } - factory := notifier.NewFactory(webhook, proxy, username, provider.Spec.Channel, token, headers, certPool, password) + factory := notifier.NewFactory(webhook, proxy, username, provider.Spec.Channel, token, headers, certPool, password, string(provider.UID)) sender, err := factory.Notifier(provider.Spec.Type) if err != nil { s.logger.Error(err, "failed to initialize provider", From 8dadcac1e2784a41bc83f5110eeb6682d9f7d823 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 27 Oct 2022 18:34:33 +0300 Subject: [PATCH 03/26] api: Add validation to URLs (max 2048) and Summary (max 255) Signed-off-by: Stefan Prodan --- api/v1beta2/alert_types.go | 1 + api/v1beta2/condition_types.go | 2 +- api/v1beta2/provider_types.go | 4 ++++ config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml | 1 + .../crd/bases/notification.toolkit.fluxcd.io_providers.yaml | 4 ++++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/api/v1beta2/alert_types.go b/api/v1beta2/alert_types.go index 494685595..9c367b1ee 100644 --- a/api/v1beta2/alert_types.go +++ b/api/v1beta2/alert_types.go @@ -47,6 +47,7 @@ type AlertSpec struct { ExclusionList []string `json:"exclusionList,omitempty"` // Short description of the impact and affected cluster. + // +kubebuilder:validation:MaxLength:=255 // +optional Summary string `json:"summary,omitempty"` diff --git a/api/v1beta2/condition_types.go b/api/v1beta2/condition_types.go index da0c51f87..119345797 100644 --- a/api/v1beta2/condition_types.go +++ b/api/v1beta2/condition_types.go @@ -26,7 +26,7 @@ const ( // couldn't be validated. ValidationFailedReason string = "ValidationFailed" - // TokenNotFound represents the fact that receiver token can't be found. + // TokenNotFoundReason represents the fact that receiver token can't be found. TokenNotFoundReason string = "TokenNotFound" // ProgressingWithRetryReason represents the fact that diff --git a/api/v1beta2/provider_types.go b/api/v1beta2/provider_types.go index 58fc5af2e..90c9b2734 100644 --- a/api/v1beta2/provider_types.go +++ b/api/v1beta2/provider_types.go @@ -35,15 +35,18 @@ type ProviderSpec struct { Type string `json:"type"` // Alert channel for this provider + // +kubebuilder:validation:MaxLength:=2048 // +optional Channel string `json:"channel,omitempty"` // Bot username for this provider + // +kubebuilder:validation:MaxLength:=2048 // +optional Username string `json:"username,omitempty"` // HTTP/S webhook address of this provider // +kubebuilder:validation:Pattern="^(http|https)://" + // +kubebuilder:validation:MaxLength:=2048 // +kubebuilder:validation:Optional // +optional Address string `json:"address,omitempty"` @@ -56,6 +59,7 @@ type ProviderSpec struct { // HTTP/S address of the proxy // +kubebuilder:validation:Pattern="^(http|https)://" + // +kubebuilder:validation:MaxLength:=2048 // +kubebuilder:validation:Optional // +optional Proxy string `json:"proxy,omitempty"` diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml index 0efe67a96..57983be78 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml @@ -311,6 +311,7 @@ spec: type: object summary: description: Short description of the impact and affected cluster. + maxLength: 255 type: string suspend: description: This flag tells the controller to suspend subsequent diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index 2ce93e81f..f7a04d2e5 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -230,6 +230,7 @@ spec: properties: address: description: HTTP/S webhook address of this provider + maxLength: 2048 pattern: ^(http|https):// type: string certSecretRef: @@ -244,9 +245,11 @@ spec: type: object channel: description: Alert channel for this provider + maxLength: 2048 type: string proxy: description: HTTP/S address of the proxy + maxLength: 2048 pattern: ^(http|https):// type: string secretRef: @@ -294,6 +297,7 @@ spec: type: string username: description: Bot username for this provider + maxLength: 2048 type: string required: - type From a9ac01a2aab2dd362db6e932b1a95b018f2df070 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 27 Oct 2022 19:07:05 +0300 Subject: [PATCH 04/26] Avoid key collision for BitBucket status updates Signed-off-by: Stefan Prodan --- internal/notifier/bitbucket.go | 19 +++++++++++-------- internal/notifier/bitbucket_test.go | 6 +++--- internal/notifier/factory.go | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/internal/notifier/bitbucket.go b/internal/notifier/bitbucket.go index 65dfe9c81..393c7fca2 100644 --- a/internal/notifier/bitbucket.go +++ b/internal/notifier/bitbucket.go @@ -34,13 +34,14 @@ import ( // Bitbucket is a Bitbucket Server notifier. type Bitbucket struct { - Owner string - Repo string - Client *bitbucket.Client + Owner string + Repo string + ProviderUID string + Client *bitbucket.Client } // NewBitbucket creates and returns a new Bitbucket notifier. -func NewBitbucket(addr string, token string, certPool *x509.CertPool) (*Bitbucket, error) { +func NewBitbucket(providerUID string, addr string, token string, certPool *x509.CertPool) (*Bitbucket, error) { if len(token) == 0 { return nil, errors.New("bitbucket token cannot be empty") } @@ -76,9 +77,10 @@ func NewBitbucket(addr string, token string, certPool *x509.CertPool) (*Bitbucke } return &Bitbucket{ - Owner: owner, - Repo: repo, - Client: client, + Owner: owner, + Repo: repo, + ProviderUID: providerUID, + Client: client, }, nil } @@ -103,8 +105,9 @@ func (b Bitbucket) Post(ctx context.Context, event eventv1.Event) error { } name, desc := formatNameAndDescription(event) + id := generateCommitStatusID(b.ProviderUID, event) // key has a limitation of 40 characters in bitbucket api - key := sha1String(name) + key := sha1String(id) cmo := &bitbucket.CommitsOptions{ Owner: b.Owner, diff --git a/internal/notifier/bitbucket_test.go b/internal/notifier/bitbucket_test.go index 519506eed..7dcd42d8d 100644 --- a/internal/notifier/bitbucket_test.go +++ b/internal/notifier/bitbucket_test.go @@ -23,18 +23,18 @@ import ( ) func TestNewBitbucketBasic(t *testing.T) { - b, err := NewBitbucket("https://bitbucket.org/foo/bar", "foo:bar", nil) + b, err := NewBitbucket("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://bitbucket.org/foo/bar", "foo:bar", nil) assert.Nil(t, err) assert.Equal(t, b.Owner, "foo") assert.Equal(t, b.Repo, "bar") } func TestNewBitbucketInvalidUrl(t *testing.T) { - _, err := NewBitbucket("https://bitbucket.org/foo/bar/baz", "foo:bar", nil) + _, err := NewBitbucket("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://bitbucket.org/foo/bar/baz", "foo:bar", nil) assert.NotNil(t, err) } func TestNewBitbucketInvalidToken(t *testing.T) { - _, err := NewBitbucket("https://bitbucket.org/foo/bar", "bar", nil) + _, err := NewBitbucket("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://bitbucket.org/foo/bar", "bar", nil) assert.NotNil(t, err) } diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go index 971921bba..98cf9e63c 100644 --- a/internal/notifier/factory.go +++ b/internal/notifier/factory.go @@ -84,7 +84,7 @@ func (f Factory) Notifier(provider string) (Interface, error) { case apiv1.GitLabProvider: n, err = NewGitLab(f.ProviderUID, f.URL, f.Token, f.CertPool) case apiv1.BitbucketProvider: - n, err = NewBitbucket(f.URL, f.Token, f.CertPool) + n, err = NewBitbucket(f.ProviderUID, f.URL, f.Token, f.CertPool) case apiv1.AzureDevOpsProvider: n, err = NewAzureDevOps(f.ProviderUID, f.URL, f.Token, f.CertPool) case apiv1.GoogleChatProvider: From ab92536f404ed8129e64e9d192e2f6abb0b4e2e5 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 27 Oct 2022 22:12:25 +0300 Subject: [PATCH 05/26] docs: Add the API spec for Alerts v1beta2 Signed-off-by: Stefan Prodan --- docs/spec/v1beta2/alerts.md | 168 ++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/spec/v1beta2/alerts.md diff --git a/docs/spec/v1beta2/alerts.md b/docs/spec/v1beta2/alerts.md new file mode 100644 index 000000000..5f022fdc9 --- /dev/null +++ b/docs/spec/v1beta2/alerts.md @@ -0,0 +1,168 @@ +# Alerts + +The `Alert` API defines how events are filtered by severity and involved object, and what provider to use for dispatching. + +## Example + +The following is an example of how to send alerts to Slack when Flux fails to reconcile the `flux-system` namespace. + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: slack-bot + namespace: flux-system +spec: + type: slack + address: https://slack.com/api/chat.postMessage + secretRef: + name: slack-bot-token +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Alert +metadata: + name: slack + namespace: flux-system +spec: + summary: "Cluster addons impacted in us-east-2" + providerRef: + name: slack-bot + eventSeverity: error + eventSources: + - kind: GitRepository + name: '*' + - kind: Kustomization + name: '*' +``` + +In the above example: + +- A Provider named `slack-bot` is created, indicated by the + `Provider.metadata.name` field. +- An Alert named `slack` is created, indicated by the + `Alert.metadata.name` field. +- The Alert references the `slack-bot` provider, indicated by the + `Alert.spec.providerRef` field. +- The notification-controller starts listening for events sent for + all GitRepositories and Kustomizations in the `flux-system` namespace. +- When an event with severity `error` is received, the controller posts + a message on Slack containing the `summary` text and the reconciliation error. + +You can run this example by saving the manifests into `slack-alerts.yaml`. + +1. First create a secret with the Slack bot token: + + ```sh + kubectl -n flux-system create secret generic slack-bot-token --from-literal=token=xoxb-YOUR-TOKEN + ``` + +2. Apply the resources on the cluster: + + ```sh + kubectl -n flux-system apply --server-side -f slack-alerts.yaml + ``` + +## Writing an Alert spec + +As with all other Kubernetes config, an Alert needs `apiVersion`, +`kind`, and `metadata` fields. The name of an Alert object must be a +valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). + +An Alert also needs a +[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). + +### Provider reference + +`.spec.providerRef.name` is a required field to specify a name reference to a +Provider in the same namespace as the Alert. + +### Event sources + +`.spec.eventSources` is a required field to specify a list of references to +Flux objects for which events are forwarded to the alert provider API. + +To select events issued by Flux objects, each entry in the `.spec.eventSources` list +must contain the following fields: + +- `kind` is the Flux Custom Resource Kind such as GitRepository, HelmRelease, Kustomization, etc. +- `name` is the Flux Custom Resource `.metadata.name`, or it can be set to the `*` wildcard. +- `namespace` is the Flux Custom Resource `.metadata.namespace`. + When not specified, the Alert `.metadata.namespace` is used instead. + +#### Select objects by name + +To select events issued by a single Flux object, set the `kind`, `name` and `namespace`: + +```yaml +eventSources: + - kind: GitRepository + name: webapp + namespace: apps +``` + +#### Select all objects in a namespace + +The `*` wildcard can be used to select events issued by all Flux objects of a particular `kind` in a `namespace`: + +```yaml +eventSources: + - kind: HelmRelease + name: '*' + namespace: apps +``` + +#### Select objects by label + +To select events issued by all Flux objects of a particular `kind` with specific `labels`: + +```yaml +eventSources: + - kind: HelmRelease + name: '*' + namespace: apps + matchLabels: + team: app-dev +``` + +#### Disable cross-namespace selectors + +**Note:** On multi-tenant clusters, platform admins can disable cross-namespace references with the +`--no-cross-namespace-refs=true` flag. When this flag is set, alerts can only refer to event sources +in the same namespace as the alert object, preventing tenants from subscribing to another tenant's events. + +### Event severity + +`.spec.eventSeverity` is an optional field to filter events based on severity. When not specified, or +when the value is set to `info`, all events are forwarded to the alert provider API, including errors. +To receive alerts only on errors, set the field value to `error`. + +### Event exclusion + +`.spec.exclusionList` is an optional field to specify a list of regex expressions to filter +events based on message content. + +### Example + +Skip alerting if the message matches a [Go regex](https://golang.org/pkg/regexp/syntax) +from the exclusion list: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Alert +metadata: + name: +spec: + eventSources: + - kind: GitRepository + name: '*' + exclusionList: + - "waiting.*socket" +``` + +The above definition will not send alerts for transient Git clone errors like: + +```text +unable to clone 'ssh://git@ssh.dev.azure.com/v3/...', error: SSH could not read data: Error waiting on socket +``` From ab5cdc8dc1da6534a5775e55ce1787cb0e7dd883 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 28 Oct 2022 17:05:06 +0300 Subject: [PATCH 06/26] docs: Add the API spec for Events and Providers v1beta2 Signed-off-by: Stefan Prodan --- docs/spec/v1beta2/README.md | 14 + docs/spec/v1beta2/alerts.md | 21 +- docs/spec/v1beta2/events.md | 62 ++ docs/spec/v1beta2/providers.md | 1020 ++++++++++++++++++++++++++++ docs/spec/v1beta2/statusupdates.md | 138 ++++ 5 files changed, 1252 insertions(+), 3 deletions(-) create mode 100644 docs/spec/v1beta2/README.md create mode 100644 docs/spec/v1beta2/events.md create mode 100644 docs/spec/v1beta2/providers.md create mode 100644 docs/spec/v1beta2/statusupdates.md diff --git a/docs/spec/v1beta2/README.md b/docs/spec/v1beta2/README.md new file mode 100644 index 000000000..f853575ef --- /dev/null +++ b/docs/spec/v1beta2/README.md @@ -0,0 +1,14 @@ +# notification.toolkit.fluxcd.io/v1beta2 + +This is the v1beta2 API specification for defining events handling and dispatching. + +## Specification + +* [Alerts](alerts.md) +* [Events](events.md) +* [Providers](providers.md) +* [Receivers](receivers.md) + +## Go Client + +* [github.com/fluxcd/pkg/recorder](https://github.com/fluxcd/pkg/tree/main/recorder) diff --git a/docs/spec/v1beta2/alerts.md b/docs/spec/v1beta2/alerts.md index 5f022fdc9..adce0df9f 100644 --- a/docs/spec/v1beta2/alerts.md +++ b/docs/spec/v1beta2/alerts.md @@ -15,6 +15,7 @@ metadata: namespace: flux-system spec: type: slack + channel: general address: https://slack.com/api/chat.postMessage secretRef: name: slack-bot-token @@ -47,7 +48,8 @@ In the above example: - The notification-controller starts listening for events sent for all GitRepositories and Kustomizations in the `flux-system` namespace. - When an event with severity `error` is received, the controller posts - a message on Slack containing the `summary` text and the reconciliation error. + a message on Slack channel from `.spec.channel`, + containing the `summary` text and the reconciliation error. You can run this example by saving the manifests into `slack-alerts.yaml`. @@ -72,10 +74,17 @@ valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working- An Alert also needs a [`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). +### Summary + +`.spec.summary` is an optional field to specify a short description of the +impact and affected cluster. + +The summary max length can't be greater than 255 characters. + ### Provider reference `.spec.providerRef.name` is a required field to specify a name reference to a -Provider in the same namespace as the Alert. +[Provider](providers.md) in the same namespace as the Alert. ### Event sources @@ -142,7 +151,7 @@ To receive alerts only on errors, set the field value to `error`. `.spec.exclusionList` is an optional field to specify a list of regex expressions to filter events based on message content. -### Example +#### Example Skip alerting if the message matches a [Go regex](https://golang.org/pkg/regexp/syntax) from the exclusion list: @@ -166,3 +175,9 @@ The above definition will not send alerts for transient Git clone errors like: ```text unable to clone 'ssh://git@ssh.dev.azure.com/v3/...', error: SSH could not read data: Error waiting on socket ``` + +### Suspend + +`.spec.suspend` is an optional field to suspend the altering. +When set to `true`, the controller will stop processing events. +When the field is set to `false` or removed, it will resume. diff --git a/docs/spec/v1beta2/events.md b/docs/spec/v1beta2/events.md new file mode 100644 index 000000000..b2fd85d98 --- /dev/null +++ b/docs/spec/v1beta2/events.md @@ -0,0 +1,62 @@ +# Events + +The `Event` API defines the structure of the events issued by Flux controllers. + +Flux controllers use the [fluxcd/pkg/runtime/events](https://github.com/fluxcd/pkg/tree/main/runtime/events) +package to push events to the notification-controller API. + +## Example + +The following is an example of an event sent by kustomize-controller to report a reconciliation error. + +```json +{ + "involvedObject": { + "apiVersion": "kustomize.toolkit.fluxcd.io/v1beta2", + "kind": "Kustomization", + "name": "webapp", + "namespace": "apps", + "uid": "7d0cdc51-ddcf-4743-b223-83ca5c699632" + }, + "metadata": { + "kustomize.toolkit.fluxcd.io/revision": "main/731f7eaddfb6af01cb2173e18f0f75b0ba780ef1" + }, + "severity":"error", + "reason": "ValidationFailed", + "message":"service/apps/webapp validation error: spec.type: Unsupported value: Ingress", + "reportingController":"kustomize-controller", + "timestamp":"2022-10-28T07:26:19Z" +} +``` + +In the above example: + +- An event is issued by kustomize-controller for a specific object, indicated in the + `involvedObject` field. +- The notification-controller receives the event and finds the [alerts](alerts.md) + that match the `involvedObject` and `severity` values. +- For all matching alerts, the controller posts the `message` and the source revision + extracted from `metadata` to the alert provider API. + +## Event structure + +The Go type that defines the event structure can be found in the +[fluxcd/pkg/runtime/events](https://github.com/fluxcd/pkg/blob/main/runtime/events/event.go) +package. + +## Rate limiting + +Events received by notification-controller are subject to rate limiting to reduce the +amount of duplicate alerts sent to external systems like Slack, Sentry, etc. + +Events are rate limited based on `involvedObject.name`, `involvedObject.namespace`, +`involvedObject.kind`, `message`, and `metadata`. +The interval of the rate limit is set by default to `5m` but can be configured +with the `--rate-limit-interval` controller flag. + +The event server exposes HTTP request metrics to track the amount of rate limited events. +The following promql will get the rate at which requests are rate limited: + +``` +rate(gotk_event_http_request_duration_seconds_count{code="429"}[30s]) +``` diff --git a/docs/spec/v1beta2/providers.md b/docs/spec/v1beta2/providers.md new file mode 100644 index 000000000..e30c0337b --- /dev/null +++ b/docs/spec/v1beta2/providers.md @@ -0,0 +1,1020 @@ +# Providers + +The `Provider` API defines how events are encoded and where to send them. + +## Example + +The following is an example of how to send alerts to Slack when Flux fails to install or upgrade Flagger. + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: slack-bot + namespace: flagger-system +spec: + type: slack + channel: general + address: https://slack.com/api/chat.postMessage + secretRef: + name: slack-bot-token +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Alert +metadata: + name: slack + namespace: flagger-system +spec: + summary: "Flagger impacted in us-east-2" + providerRef: + name: slack-bot + eventSeverity: error + eventSources: + - kind: HelmRepository + name: '*' + - kind: HelmRelease + name: '*' +``` + +In the above example: + +- A Provider named `slack-bot` is created, indicated by the + `Provider.metadata.name` field. +- An Alert named `slack` is created, indicated by the + `Alert.metadata.name` field. +- The Alert references the `slack-bot` provider, indicated by the + `Alert.spec.providerRef` field. +- The notification-controller starts listening for events sent for + all HelmRepositories and HelmReleases in the `flagger-system` namespace. +- When an event with severity `error` is received, the controller posts + a message on Slack containing the `summary` text and the Helm install or upgrade error. +- The controller uses the Slack Bot token from the secret indicated by the + `Provider.spec.secretRef.name` to authenticate with the Slack API. + +You can run this example by saving the manifests into `slack-alerts.yaml`. + +1. First create a secret with the Slack bot token: + + ```sh + kubectl -n flagger-system create secret generic slack-bot-token --from-literal=token=xoxb-YOUR-TOKEN + ``` + +2. Apply the resources on the cluster: + + ```sh + kubectl -n flagger-system apply --server-side -f slack-alerts.yaml + ``` + +## Writing a provider spec + +As with all other Kubernetes config, a Provider needs `apiVersion`, +`kind`, and `metadata` fields. The name of an Alert object must be a +valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). +A Provider also needs a +[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). + +### Type + +`.spec.type` is a required field that specifies which SaaS API to use. + +The supported providers are: + +| Provider | Type | +|---------------------------------------------------------|------------------| +| [Prometheus Alertmanager](#prometheus-alertmanager) | `alertmanager` | +| [Azure Event Hub](#azure-event-hub) | `azureeventhub` | +| [Discord](#discord) | `discord` | +| [Generic webhook](#generic-webhook) | `generic` | +| [Generic webhook with HMAC](#generic-webhook-with-hmac) | `generic-hmac` | +| [GitHub dispatch](#github-dispatch) | `githubdispatch` | +| [Google Chat](#google-chat) | `googlechat` | +| [Grafana](#grafana) | `grafana` | +| [Lark](#lark) | `lark` | +| [Matrix](#matrix) | `matrix` | +| [Microsoft Teams](#microsoft-teams) | `msteams` | +| [Opsgenie](#opsgenie) | `opsgenie` | +| [Rocket](#rocket) | `rocket` | +| [Sentry](#sentry) | `sentry` | +| [Slack](#slack) | `slack` | +| [Telegram](#telegram) | `telegram` | +| [WebEx](#webex) | `webex` | + +### Address + +`.spec.address` is an optional field that specifies the URL where the events are posted. + +If the address contains sensitive information such as tokens or passwords, it is +recommended to store the address in the Kubernetes secret referenced by `.spec.secretRef.name`. + +### Channel + +`.spec.channel` is an optional field that specifies the channel where the events are posted. + +### Secret reference + +`.spec.secretRef.name` is an optional field to specify a name reference to a +Secret in the same namespace as the Provider, containing the authentication +credentials for the provider API. + +The Kubernetes secret can have any of the following keys: + +- `address` - overrides `.spec.address` +- `proxy` - overrides `.spec.proxy` +- `token` - used for authentication +- `headers` - HTTP headers values included in the POST request + +#### Address example + +For providers which emblem tokens or other sensitive information in the URL, +the incoming webhooks address can be stored in the secret using the `address` key: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-provider-url + namespace: default +stringData: + address: "https://webhook.example.com/token" +``` + +#### Token example + +For providers which require token based authentication, the API token +can be stored in the secret using the `token` key: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-provider-auth + namespace: default +stringData: + token: "my-api-token" +``` + +#### HTTP headers example + +For providers which require specific HTTP headers to be attached to the POST request, +the headers can be set in the secret using the `headers` key: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-provider-headers + namespace: default +stringData: + headers: | + Authorization: my-api-token + X-Forwarded-Proto: https +``` + +#### Proxy auth example + +Some networks need to use an authenticated proxy to access external services. +Therefore, the proxy address can be stored as a secret to hide parameters like the username and password: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-provider-proxy + namespace: default +stringData: + proxy: "http://username:password@proxy_url:proxy_port" +``` + +### TLS certificates + +`.spec.certSecretRef` is an optional field to specify a name reference to a +Secret in the same namespace as the Provider, containing the TLS CA certificate. + +#### Example + +To enable notification-controller to communicate with a provider API over HTTPS +using a self-signed TLS certificate, set the `caFile` like so: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: my-webhook + namespace: flagger-system +spec: + type: generic + address: https://my-webhook.internal + certSecretRef: + name: my-ca-crt +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-ca-crt + namespace: default +stringData: + caFile: | + <--- CA Key ---> +``` + +### HTTP/S proxy + +`.spec.proxy` is an optional field to specify an HTTP/S proxy address. + +### Suspend + +`.spec.suspend` is an optional field to suspend the provider. +When set to `true`, the controller will stop sending events to this provider. +When the field is set to `false` or removed, it will resume. + +## Provider guids + +### Generic webhook + +The `generic` webhook triggers an HTTP POST request to the provided endpoint. + +The `Gotk-Component` header identifies which component this event is coming +from, e.g. `source-controller`, `kustomize-controller`. + +``` +POST / HTTP/1.1 +Host: example.com +Accept-Encoding: gzip +Content-Length: 452 +Content-Type: application/json +Gotk-Component: source-controller +User-Agent: Go-http-client/1.1 +``` + +The body of the request looks like this: + +```json +{ + "involvedObject": { + "apiVersion": "kustomize.toolkit.fluxcd.io/v1beta2", + "kind": "Kustomization", + "name": "webapp", + "namespace": "apps", + "uid": "7d0cdc51-ddcf-4743-b223-83ca5c699632" + }, + "metadata": { + "kustomize.toolkit.fluxcd.io/revision": "main/731f7eaddfb6af01cb2173e18f0f75b0ba780ef1" + }, + "severity":"error", + "reason": "ValidationFailed", + "message":"service/apps/webapp validation error: spec.type: Unsupported value: Ingress", + "reportingController":"kustomize-controller", + "reportingInstance":"kustomize-controller-7c7b47f5f-8bhrp", + "timestamp":"2022-10-28T07:26:19Z" +} +``` + +The `involvedObject` key contains the object that triggered the event. + +You can add additional headers to the POST request by providing a `headers` field to the secret +referenced by the provider. An example is given below: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: generic + namespace: default +spec: + type: generic + address: https://api.github.com/repos/owner/repo/dispatches + secretRef: + name: generic-secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: generic-secret + namespace: default +stringData: + headers: | + Authorization: token + X-Forwarded-Proto: https +``` + +### Generic webhook with HMAC + +If you set the `.spec.type` of a `Provider` resource to `generic-hmac` then the HTTP request +sent to the webhook will include the `X-Signature` HTTP header carrying the HMAC of the request body. +This allows the webhook server to authenticate the request. +The key used for the HMAC must be supplied in the `token` field of the Secret resource referenced in `.spec.secretRef`. +The HTTP header value has the following format: + +``` +X-Signature: HASH_FUNC=HASH +``` + +`HASH_FUNC` denotes the Hash function used to generate the HMAC and currently defaults +to `sha256` but may change in the future. You must make sure to take this value into +account when verifying the HMAC. `HASH` is the hex-encoded HMAC value. +The following Go code illustrates how the header is parsed and verified: + +```go +func verifySignature(sig string, payload, key []byte) error { + sigHdr := strings.Split(sig, "=") + if len(shgHdr) != 2 { + return fmt.Errorf("invalid signature value") + } + var newF func() hash.Hash + switch sigHdr[0] { + case "sha224": + newF = sha256.New224 + case "sha256": + newF = sha256.New + case "sha384": + newF = sha512.New384 + case "sha512": + newF = sha512.New + default: + return fmt.Errorf("unsupported signature algorithm %q", sigHdr[0]) + } + mac := hmac.New(newF, key) + if _, err := mac.Write(payload); err != nil { + return fmt.Errorf("error MAC'ing payload: %w", err) + } + sum := fmt.Sprintf("%x", mac.Sum(nil)) + if sum != sigHdr[1] { + return fmt.Errorf("HMACs don't match: %#v != %#v", sum, sigHdr[1]) + } + return nil +} +[...] +key := []byte("b1fad212fb1b87a56c79e5da48018650b85ab7cf") +if len(r.Header["X-Signature"]) > 0 { + if err := verifySignature(r.Header["X-Signature"][0], body, key); err != nil { + // handle signature verification failure here + } +} +``` + +### Slack + +To send alerts to Slack, we recommend using a Slack Bot App token. +To obtain a token, please follow [Slack's guide on bot users](https://api.slack.com/bot-users). + +Once you have a Slack bot token (starts with `xoxb-`), create a secret for it with: + +```shell +kubectl create secret generic slack-token --from-literal=token=BOT-TOKEN +``` + +Create a provider of type `slack`, with the address set to `https://slack.com/api/chat.postMessage` +and reference the `slack-token` secret: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: slack + namespace: default +spec: + type: slack + channel: general + address: https://slack.com/api/chat.postMessage + secretRef: + name: slack-token +``` + +Slack legacy webhooks are also supported, the webhook URL can be set in the `address` field or in the secret: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: slack + namespace: default +spec: + type: slack + secretRef: + name: slack-webhook +--- +apiVersion: v1 +kind: Secret +metadata: + name: slack-webhook + namespace: default +stringData: + address: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK" +``` + +### Microsoft Teams + +To send alerts to Teams, first create an +[incoming webhook](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) +on the Microsoft Teams UI: + +1. Open the settings of the channel you want the notifications to be sent to. +2. Click on `Connectors`. +3. Click on the `Add` button for `Incoming Webhook`. +4. Click on `Configure` and copy the webhook URL given. + +Once you have the webhook URL, create a secret for it with: + +```shell +kubectl create secret generic teams-webhook \ +--from-literal=address= +``` + +Create a provider of type `msteam` and reference the `teams-webhook` secret: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: msteams + namespace: default +spec: + type: msteams + secretRef: + name: slack-webhook +``` + +### Discord + +To send events to Discord, first [create a webhook](https://discord.com/developers/docs/resources/webhook#create-webhook). + +Once you have the webhook URL, create a secret for it with: + +```shell +kubectl create secret generic discord-webhook \ +--from-literal=address= +``` + +Create a provider of type `discord` and reference the `discord-webhook` secret: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: discord + namespace: default +spec: + type: discord + secretRef: + name: discord-webhook +``` + +### Sentry + +To send events to Sentry, create a provider of type `sentry` and a secret with the Sentry URL: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: sentry + namespace: default +spec: + type: sentry + channel: staging-env + secretRef: + name: sentry-webhook +--- +apiVersion: v1 +kind: Secret +metadata: + name: sentry-webhook + namespace: default +stringData: + address: "https://....@sentry.io/12341234" +``` + +Note that the `.spec.channel` field can be used to specify which environment the messages are sent for. + +The sentry provider also sends traces for events with the severity `info`. +This can be disabled by setting, the `Alert.spec.eventSeverity` field to `error`. + +### Telegram + +For telegram, You can get the token from [the botfather](https://core.telegram.org/bots#6-botfather) +and use `https://api.telegram.org/` as the address. + +Once you have a Telegram token, create a secret for it with: + +```shell +kubectl create secret generic telegram-token \ +--from-literal=token=BOT-TOKEN +``` + +Create a provider of type `telegram` and reference the `telegram-token` secret: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: telegram + namespace: default +spec: + type: telegram + address: https://api.telegram.org + channel: "@fluxtest" # or "-1557265138" (channel id) + secretRef: + name: telegram-token +``` + +Note that `.spec.channel` can be a unique identifier for the target chat +or the username of the target channel (in the format `@channelusername`). + +### Matrix + +For Matrix, the address is the homeserver URL and the token is the access token +returned by a call to `/login` or `/register`. + +Once you have a Matrix token, create a secret for it with: + +```shell +kubectl create secret generic matrix-token \ +--from-literal=token=MY-TOKEN +``` + +Create a provider of type `matrix` and reference the `matrix-token` secret: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: matrix + namespace: default +spec: + type: matrix + address: https://matrix.org + channel: "!jezptmDwEeLapMLjOc:matrix.org" + secretRef: + name: matrix-token +``` + +Note that `.spec.channel` holds the room ID. + +### Lark + +For sending notifications to Lark, you will have to +[add a bot to the group](https://www.larksuite.com/hc/en-US/articles/360048487736-Bot-Use-bots-in-groups#III.%20How%20to%20configure%20custom%20bots%20in%20a%20group%C2%A0) +and set up a webhook for that bot account. This serves as the address field in the secret: + +```shell +kubectl create secret generic lark-webhook \ +--from-literal=address= +``` + +Create a provider of type `lark` and reference the `lark-webhook` secret: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: lark + namespace: default +spec: + type: lark + secretRef: + name: lark-webhook +``` + +### Rocket + +To send events to Rocket chat, first [create an incoming webhook](https://docs.rocket.chat/guides/administration/admin-panel/integrations#create-a-new-incoming-webhook). + +Once you have the webhook URL, create a secret for it with: + +```shell +kubectl create secret generic rocket-webhook \ +--from-literal=address= +``` + +Create a provider of type `rocket` and reference the `rocket-webhook` secret: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: rocket + namespace: default +spec: + type: rocket + secretRef: + name: rocket-webhook +``` + +### Google Chat + +To send notifications to Google chat, first [create an incoming webhook](https://developers.google.com/chat/how-tos/webhooks#create_a_webhook). + +Once you have the webhook URL, create a secret for it with: + +```shell +kubectl create secret generic google-webhook \ +--from-literal=address= +``` + +Create a provider of type `googlechat` and reference the `google-webhook` secret: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: google + namespace: default +spec: + type: googlechat + secretRef: + name: google-webhook +``` + +### Opsgenie + +To send notifications to Opsgenie, first +[add a REST API integration](https://support.atlassian.com/opsgenie/docs/create-a-default-api-integration/). + +Once you have a Opsgenie API key, create a secret for it with: + +```shell +kubectl create secret generic opsgenie-token \ +--from-literal=token= +``` + +Create a provider of type `opsgenie` and reference the `opsgenie-token` secret: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: opsgenie + namespace: default +spec: + type: opsgenie + address: https://api.opsgenie.com/v2/alerts + secretRef: + name: opsgenie-token +``` + +### Prometheus Alertmanager + +To send events to the Prometheus [Alertmanager v2 API](https://github.com/prometheus/alertmanager/blob/main/api/v2/openapi.yaml), +create a provider of type `alertmanager` and set the `address` field to the `api/v2/alerts` endpoint: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: alertmanager + namespace: default +spec: + type: alertmanager + # webhook address (ignored if secretRef is specified) + address: https://....@/api/v2/alerts/" +``` + +If Alertmanager has basic authentication configured, it is recommended to use +`.spec.secretRef` and include the `username:password` in the address string inside the secret. + +When an event is received, the controller will send a single alert with at least one annotation +which is the `message` found for the event. +If an `Alert.spec.summary` is provided, an additional "summary" annotation will be added. + +The provider will send the following labels for the event: + + +| Label | Description | +|-----------|------------------------------------------------------------------------------------------------------| +| alertname | The string Flux followed by the Kind and the reason for the event e.g `FluxKustomizationProgressing` | +| severity | The severity of the event (`error` or `info`) | +| timestamp | The timestamp of the event | +| reason | The machine readable reason for the objects transition into the current status | +| kind | The kind of the involved object associated with the event | +| name | The name of the involved object associated with the event | +| namespace | The namespace of the involved object associated with the event | + +### Webex + +General steps on how to send notifications to a Webex space: + +From the Webex App UI: + +- create a Webex space where you want notifications to be sent +- after creating a Webex bot (described in next section), add the bot email address to the Webex space ("People | Add people") + +Register to https://developer.webex.com/, after signing in: + +- Create a bot for forwarding Flux notifications to a Webex Space + (User profile icon | MyWebexApps | Create a New App | Create a Bot). +- Make a note of the bot email address, this email needs to be added to the Webex space from the Webex App. +- Generate a bot access token, this is the ID to use in the kubernetes Secret "token" field. +- Find the room ID associated to the webex space using https://developer.webex.com/docs/api/v1/rooms/list-rooms + (select GET, click on "Try It" and search the GET results for the matching Webex space entry), + this is the ID to use in the webex Provider manifest "channel" field. + +Example: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: webex + namespace: default +spec: + type: webex + address: https://webexapis.com/v1/messages + channel: + secretRef: + name: webex-token +--- +apiVersion: v1 +kind: Secret +metadata: + name: webex-token + namespace: default +stringData: + token: +``` + +Notes: + +- `.spec.address` should always be set to the same global Webex API gateway `https://webexapis.com/v1/messages` +- `.spec.channel` should contain the Webex space room ID as obtained from `https://developer.webex.com/` (long alphanumeric string copied as is). + +If you do not see any notifications in the targeted Webex space, check that you have added the bot +email address to the Webex space, if the bot email address is not added to the space, +the notification-controller will log a 404 room not found error every time a notification is sent out. + +### Grafana + +To send notifications to [Grafana annotations API](https://grafana.com/docs/grafana/latest/http_api/annotations/), +enable the annotations on a Dashboard like so: + +- Annotations > Query > Enable Match any +- Annotations > Query > Tags (Add Tag: `flux`) + +If Grafana has authentication configured, create a Kubernetes Secret with the API token: + +```shell +kubectl create secret generic grafana-token \ +--from-literal=token= \ +``` + +Grafana can also use basic authorization to authenticate the requests, if both the token and +the username/password are set in the secret, then token takes precedence over`basic auth: + +```shell +kubectl create secret generic grafana-token \ +--from-literal=username= \ +--from-literal=password= +``` + +Create a provider of type `grafana` and reference the `grafana-token` secret: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: grafana + namespace: default +spec: + type: grafana + address: https:///api/annotations + secretRef: + name: grafana-token +``` + +### GitHub dispatch + +The `githubdispatch` provider generates GitHub events of type +[`repository_dispatch`](https://docs.github.com/en/rest/reference/repos#create-a-repository-dispatch-event) +for the selected repository. The `repository_dispatch` events can be used to trigger GitHub Actions workflow. + +The request includes the `event_type` and `client_payload` fields: + +- `event_type` is generated from the involved object in the format `{Kind}/{Name}.{Namespace}`. +- `client_payload` contains the [Flux event](events.md). + +### Setting up the GitHub dispatch provider + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: github-dispatch + namespace: flux-system +spec: + type: githubdispatch + address: https://github.com/stefanprodan/podinfo + secretRef: + name: api-token +``` + +The `address` is the address of your repository where you want to send webhooks to trigger GitHub workflows. + +GitHub uses personal access tokens for authentication with its API: + +* [GitHub personal access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token) + +The provider requires a secret in the same format, with the personal access token as the value for the token key: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: api-token + namespace: default +data: + token: +``` + +#### Setting up a GitHub workflow + +To trigger a GitHub Actions workflow when a Flux Kustomization finishes reconciling, +you need to set the event type for the repository_dispatch trigger to match the Flux object ID: + +```yaml +name: test-github-dispatch-provider +on: + repository_dispatch: + types: [Kustomization/podinfo.flux-system] +``` + +Assuming that we deploy all Flux kustomization resources in the same namespace, +it will be useful to have a unique kustomization resource name for each application. +This will allow you to use only `event_type` to trigger tests for the exact application. + +Let's say we have following folder structure for applications kustomization manifests: + +```bash +apps/ +├── app1 +│   └── overlays +│   ├── production +│   └── staging +└── app2 + └── overlays + ├── production + └── staging +``` + +You can then create a flux kustomization resource for the app to have unique `event_type` per app. +The kustomization manifest for app1/staging: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: app1 + namespace: flux-system +spec: + path: "./app1/staging" +``` + +You would also like to know from the notification which cluster is being used for deployment. +You can add the `spec.summary` field to the Flux alert configuration to mention the relevant cluster: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Alert +metadata: + name: github-dispatch + namespace: flux-system +spec: + summary: "staging (us-west-2)" + providerRef: + name: github-dispatch + eventSeverity: info + eventSources: + - kind: Kustomization + name: 'podinfo' +``` + +Now you can the trigger tests in the GitHub workflow for app1 in a staging cluster when +the app1 resources defined in `./app1/staging/` are reconciled by Flux: + +```yaml +name: test-github-dispatch-provider +on: + repository_dispatch: + types: [Kustomization/podinfo.flux-system] +jobs: + run-tests-staging: + if: github.event.client_payload.metadata.summary == 'staging (us-west-2)' + runs-on: ubuntu-18.04 + steps: + - name: Run tests + run: echo "running tests.." +``` + +### Azure Event Hub + +The Azure Event Hub supports two authentication methods, [JWT](https://docs.microsoft.com/en-us/azure/event-hubs/authenticate-application) +and [SAS](https://docs.microsoft.com/en-us/azure/event-hubs/authorize-access-shared-access-signature) based. + +#### JWT based auth + +In JWT we use 3 input values. Channel, token and address. +We perform the following translation to match we the data we need to communicate with Azure Event Hub. + +- channel = Azure Event Hub namespace +- address = Azure Event Hub name +- token = JWT + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: azure + namespace: default +spec: + type: azureeventhub + address: + channel: + secretRef: + name: azure-token +--- +apiVersion: v1 +kind: Secret +metadata: + name: azure-token + namespace: default +stringData: + token: +``` + +The controller doesn't take any responsibility for the JWT token to be updated. +You need to use a secondary tool to make sure that the token in the secret is renewed. + +If you want to make a easy test assuming that you have setup a Azure Enterprise application and you called it +event-hub you can follow most of the bellow commands. You will need to provide the `client_secret` that you got +when generating the Azure Enterprise Application. + +```shell +export AZURE_CLIENT=$(az ad app list --filter "startswith(displayName,'event-hub')" --query '[].appId' |jq -r '.[0]') +export AZURE_SECRET='secret-client-secret-generated-at-creation' +export AZURE_TENANT=$(az account show -o tsv --query tenantId) + +curl -X GET --data 'grant_type=client_credentials' --data "client_id=$AZURE_CLIENT" --data "client_secret=$AZURE_SECRET" --data 'resource=https://eventhubs.azure.net' -H 'Content-Type: application/x-www-form-urlencoded' https://login.microsoftonline.com/$AZURE_TENANT/oauth2/token |jq .access_token +``` + +Use the output you got from `curl` and add it to your secret like bellow: + +```shell +kubectl create secret generic azure-token \ +--from-literal=token='A-valid-JWT-token' +``` + +#### SAS based auth + +When using SAS auth, we only use the `address` field in the secret. + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: azure + namespace: default +spec: + type: azureeventhub + secretRef: + name: azure-webhook +--- +apiVersion: v1 +kind: Secret +metadata: + name: azure-webhook + namespace: default +stringData: + address: +``` + +Assuming that you have created Azure event hub and namespace you should be able to use a similar command to get your +connection string. This will give you the default Root SAS, it's NOT supposed to be used in production. + +```shell +az eventhubs namespace authorization-rule keys list --resource-group --namespace-name --name RootManageSharedAccessKey -o tsv --query primaryConnectionString +# The output should look something like this: +Endpoint=sb://fluxv2.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=yoursaskeygeneatedbyazure +``` + +To create the needed secret: + +```shell +kubectl create secret generic azure-webhook \ +--from-literal=address="Endpoint=sb://fluxv2.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=yoursaskeygeneatedbyazure" +``` diff --git a/docs/spec/v1beta2/statusupdates.md b/docs/spec/v1beta2/statusupdates.md new file mode 100644 index 000000000..520c98e1b --- /dev/null +++ b/docs/spec/v1beta2/statusupdates.md @@ -0,0 +1,138 @@ +# Git Commit Status Updates + +The notification-controller can mark Git commits as reconciled by posting +Flux `Kustomization` events to the origin repository using Git SaaS providers APIs. + +## Example + +The following is an example of how to update the Git commit status for the GitHub repository where +Flux was bootstrapped with `flux bootstrap github --owner=my-gh-org --repository=my-gh-repo`. + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: github-status + namespace: flux-system +spec: + type: github + address: https://github.com/my-gh-org/my-gh-repo + secretRef: + name: github-token +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Alert +metadata: + name: github-status + namespace: flux-system +spec: + providerRef: + name: github-status + eventSources: + - kind: Kustomization + name: flux-system +``` + +In the above example: + +- A Provider named `github-status` is created, indicated by the + `Provider.metadata.name` field. +- An Alert named `github-status` is created, indicated by the + `Alert.metadata.name` field. +- The Alert references the GitHub provider, indicated by the + `Alert.spec.providerRef` field. +- The notification-controller starts listening for events sent by + the `flux-system` Kustomization. +- When an event is received, the controller extracts the Git commit SHA + from the [event](events.md) payload. +- The controller uses the GitHub PAT from the secret indicated by the + `Provider.spec.secretRef.name` to authenticate with the GitHub API. +- The controller sets the commit status to `kustomization/flux-system/` + followed by the success or error message from the [event](events.md) body. + + +## Writing a Git commit status provider spec + +As with all other Kubernetes config, a Provider needs `apiVersion`, +`kind`, and `metadata` fields. The name of an Alert object must be a +valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). +A Provider also needs a +[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). + +### Type + +`.spec.type` is a required field that specifies which Git SaaS vendor hosts the repository. + +The supported types are: `github`, `gitlab`, `bitbucket` and `azuredevops`. + +### Address + +`.spec.address` is a required field that specifies the HTTPS URL of the Git repository +where the commits originate from. + +### Secret reference + +`.spec.secretRef.name` is a required field to specify a name reference to a +Secret in the same namespace as the Provider, containing the authentication +credentials for the Git SaaS API. + +#### GitHub authentication + +When `.spec.type` is set to `github`, the referenced secret must contain a key called `token` with the value set to a +[GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). + +The token must has permissions to update the commit status for the GitHub repository specified in `.spec.address`. + +You can create the secret with `kubectl` like this: + +```shell +kubectl create secret generic github-token --from-literal=token= +``` + +#### GitLab authentication + +When `.spec.type` is set to `gitlab`, the referenced secret must contain a key called `token` with the value set to a +[GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html). + +The token must has permissions to update the commit status for the GitLab repository specified in `.spec.address`. + +You can create the secret with `kubectl` like this: + +```shell +kubectl create secret generic gitlab-token --from-literal=token= +``` + +#### BitBucket authentication + +When `.spec.type` is set to `bitbucket`, the referenced secret must contain a key called `token` with the value +set to a BitBucket username and an +[app password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/#Create-an-app-password) +in the format `:`. + +The app password must have `Repositories (Read/Write)` permission for +the BitBucket repository specified in `.spec.address`. + +You can create the secret with `kubectl` like this: + +```shell +kubectl create secret generic gitlab-token --from-literal=token=: +``` + +#### Azure DevOps authentication + +When `.spec.type` is set to `azuredevops`, the referenced secret must contain a key called `token` with the value set to a +[Azure DevOps personal access token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page). + +The token must has permissions to update the commit status for the Azure DevOps repository specified in `.spec.address`. + +You can create the secret with `kubectl` like this: + +```shell +kubectl create secret generic github-token --from-literal=token= +``` + +### Suspend + +`.spec.suspend` is an optional field to suspend the Git commit status updates. +When set to `true`, the controller will stop processing events for this provider. +When the field is set to `false` or removed, it will resume the commit status updates. From 613111c51db7c54c984e0c12838608452d798492 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 31 Oct 2022 12:59:57 +0200 Subject: [PATCH 07/26] docs: Add the API spec for Receivers v1beta2 Signed-off-by: Stefan Prodan --- docs/spec/README.md | 79 +----- docs/spec/v1beta2/providers.md | 2 +- docs/spec/v1beta2/receivers.md | 447 +++++++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+), 76 deletions(-) create mode 100644 docs/spec/v1beta2/receivers.md diff --git a/docs/spec/README.md b/docs/spec/README.md index 4b32684f8..5f1903063 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -34,9 +34,9 @@ events are processed and where to dispatch them. Notification API: -* [Provider](v1beta1/provider.md) -* [Alert](v1beta1/alert.md) -* [Event](v1beta1/event.md) +* [Alerts](v1beta2/alerts.md) +* [Providers](v1beta2/providers.md) +* [Git Commit Status Updates](v1beta2/statusupdates.md) The alert delivery method is **at-most once** with a timeout of 15 seconds. The controller performs automatic retries for connection errors and 500-range response code. @@ -52,7 +52,7 @@ to be accessed by GitHub, GitLab, Bitbucket, Harbor, DockerHub, Jenkins, Quay, e Receiver API: -* [Receiver](v1beta1/receiver.md) +* [Receivers](v1beta2/receivers.md) When a `Receiver` is created, the controller sets the `Receiver` status to Ready and generates a URL in the format `/hook/sha256sum(token+name+namespace)`. @@ -64,74 +64,3 @@ When the controller receives a POST request: * validates the signature using the `token` secret * extract the event type from the payload * triggers a reconciliation for `spec.resources` if the event type matches one of the `spec.events` items - -## Example - -After installing notification-controller, we can configure alerting for events issued -by source-controller and kustomize-controller. - -Create a notification provider for Slack: - -```yaml -apiVersion: notification.toolkit.fluxcd.io/v1beta1 -kind: Provider -metadata: - name: slack -spec: - type: slack - channel: prod-alerts - secretRef: - name: slack-url ---- -apiVersion: v1 -kind: Secret -metadata: - name: slack-url -data: - address: -``` - -Create an alert for a list of GitRepositories and Kustomizations: - -```yaml -apiVersion: notification.toolkit.fluxcd.io/v1beta1 -kind: Alert -metadata: - name: on-call-webapp -spec: - providerRef: - name: slack - eventSeverity: info - eventSources: - - kind: GitRepository - name: '*' - - kind: Kustomization - name: webapp-frontend - - kind: Kustomization - name: webapp-backend -``` - -Based on the above configuration, the controller will post messages on Slack every time there is an event -issued for the webapp Git repository and Kustomizations. - -Kustomization apply event example: - -```json -{ - "severity": "info", - "ts": "2020-09-17T07:27:11.921Z", - "reportingController": "kustomize-controller", - "reason": "ApplySucceed", - "message": "Kustomization applied in 1.4s, revision: master/a1afe267b54f38b46b487f6e938a6fd508278c07", - "involvedObject": { - "kind": "Kustomization", - "name": "webapp-backend", - "namespace": "default" - }, - "metadata": { - "service/backend": "created", - "deployment.apps/backend": "created", - "horizontalpodautoscaler.autoscaling/backend": "created" - } -} -``` diff --git a/docs/spec/v1beta2/providers.md b/docs/spec/v1beta2/providers.md index e30c0337b..f6f7a1853 100644 --- a/docs/spec/v1beta2/providers.md +++ b/docs/spec/v1beta2/providers.md @@ -233,7 +233,7 @@ stringData: When set to `true`, the controller will stop sending events to this provider. When the field is set to `false` or removed, it will resume. -## Provider guids +## Provider guides ### Generic webhook diff --git a/docs/spec/v1beta2/receivers.md b/docs/spec/v1beta2/receivers.md new file mode 100644 index 000000000..dec2be907 --- /dev/null +++ b/docs/spec/v1beta2/receivers.md @@ -0,0 +1,447 @@ +# Incoming Webhook Receivers + +The `Receiver` API defines an incoming webhook receiver that triggers +the reconciliation for a group of Flux Custom Resources. + +## Example + +The following is an example of how to configure an incoming webhook for the GitHub repository where +Flux was bootstrapped with `flux bootstrap github`. After a Git push, GitHub will send a push event to +notification-controller, which in turn tells Flux to pull and apply the latest changes from upstream. + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: github-receiver + namespace: flux-system +spec: + type: github + events: + - "ping" + - "push" + secretRef: + name: receiver-token + resources: + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: GitRepository + name: flux-system +``` + +In the above example: + +- A Receiver named `github-receiver` is created, indicated by the + `.metadata.name` field. +- The notification-controller generates a unique URL using the Receiver + name, namespace and the token from the referenced `.spec.secretRef.name` secret. +- The URL is reported in the `.status.url` field. +- When a GitHub push event is received, the controller verifies the that the + request is legitimate using HMAC and the `X-Hub-Signature` HTTP header. +- If the event type matches `.spec.events` and the payload is verified, then the controller + triggers a reconciliation for the `flux-system` GitRepository which is listed under `.spec.resouces`. + +You can run this example by saving the manifest into `github-receiver.yaml`. + +1. Generate a random string and create a secret with a `token` field: + + ```sh + TOKEN=$(head -c 12 /dev/urandom | shasum | cut -d ' ' -f1) + + kubectl -n flux-system create secret generic receiver-token \ + --from-literal=token=$TOKEN + ``` + +2. Apply the resource on the cluster: + + ```sh + kubectl -n flux-system apply -f github-receiver.yaml + ``` + +3. Run `kubectl -n flux-system get receivers` to see the generated URL: + + ```console + NAME READY STATUS + github-receiver True Receiver initialised with URL: /hook/bed6d00b5555b1603e1f59b94d7fdbca58089cb5663633fb83f2815dc626d92b + ``` + +4. On GitHub, navigate to your repository and click on the "Add webhook" button under "Settings/Webhooks". + Fill the form with: + - **Payload URL**: compose the address using the receiver LB and the generated URL `http:///`. + - **Secret**: use the token string + +## Writing a Receiver spec + +As with all other Kubernetes config, a Receiver needs `apiVersion`, +`kind`, and `metadata` fields. The name of a Receiver object must be a +valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). +A Receiver also needs a +[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). + +### Type + +`.spec.type` is a required field that specifies which SaaS API to use. + +The supported receiver types are: + +| Receiver | Type | +|-----------------------------------------------------|----------------| +| [Generic webhook](#generic-receiver) | `generic` | +| [Generic webhook with HMAC](#generic-hmac-receiver) | `generic-hmac` | +| [GitHub](#github-receiver) | `github` | +| [Gitea](#github-receiver) | `github` | +| [GitLab](#gitlab-receiver) | `gitlab` | +| [Bitbucket server](#bitbucket-server-receiver) | `bitbucket` | +| [Harbor](#harbor-receiver) | `harbor` | +| [DockerHub](#dockerhub-receiver) | `dockerhub` | +| [Quay](#quay-receiver) | `quay` | +| [Nexus](#nexus-receiver) | `nexus` | +| [Azure Container Registry](#acr-receiver) | `acr` | +| [Google Container Registry](#gcr-receiver) | `gcr` | + +### Events filtering + +`.spec.events` in an optional field to specify a list of event types +that this Receiver should handle. If left empty, all events are handled. + +### Resources + +`.spec.resources` is a required field to specify which Flux Custom Resources +should be reconciled when an event is received. + +A resource entry must contain the following fields: +- `apiVersion` is the Flux Custom Resource API group and version such as `source.toolkit.fluxcd.io/v1beta2`. +- `kind` is the Flux Custom Resource `.kind` such as GitRepository, OCIRepository, HelmRepository, etc. +- `name` is the Flux Custom Resource `.metadata.name`. +- `namespace` is the Flux Custom Resource `.metadata.namespace`. + When not specified, the Receiver `.metadata.namespace` is used instead. + +#### Disable cross-namespace selectors + +**Note:** On multi-tenant clusters, platform admins can disable cross-namespace references with the +`--no-cross-namespace-refs=true` flag. When this flag is set, Receivers can only refer to resources +in the same namespace as the alert object, preventing tenants from triggering reconciliations +to another tenant's resources. + +### Secret reference + +`.spec.secretRef.name` is a required field to specify a name reference to a +Secret in the same namespace as the Receiver, containing the secret token. + +### Suspend + +`.spec.suspend` is an optional field to suspend the receiver. +When set to `true`, the controller will stop processing events for this receiver. +When the field is set to `false` or removed, it will resume. + +## Public ingress considerations + +Considerations should be made when exposing the controller's `webhook-receiver` Kubernetes Service +to the public internet. Each request to the receiver endpoint will result in request to the Kubernetes +API as the controller needs to fetch information about the receiver. The receiver endpoint may be +protected with a token, but it does not defend against a situation where a legitimate webhook source +starts sending large amounts of requests, or the token is somehow leaked. +This may result in unwanted consequences for the controller, +as it may get rate limited by the Kubernetes API, degrading its functionality. + +It is therefore a good idea to set rate limits on the ingress resource which exposes the receiver. +If you are using ingress-nginx that can be done by +[adding annotations](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#rate-limiting). + +## Provider guides + +### Generic receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: generic-receiver + namespace: default +spec: + type: generic + secretRef: + name: webhook-token + resources: + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: GitRepository + name: webapp + namespace: default + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + name: webapp + namespace: default + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: Bucket + name: webapp + namespace: default + - apiVersion: image.toolkit.fluxcd.io/v1beta1 + kind: ImageRepository + name: webapp + namespace: default +``` + +When the receiver type is set to `generic`, the controller will not perform token validation nor event filtering. + +### Generic HMAC receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: generic-hmac-receiver + namespace: default +spec: + type: generic-hmac + secretRef: + name: webhook-token + resources: + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: GitRepository + name: webapp + namespace: default +``` + +This generic receiver verifies that the request is legitimate using HMAC. +The controller uses the `X-Signature` header to get the hash signature. +The signature should be prefixed with the hash function(`sha1`, `sha256`, or `sha512`) like this: +`=`. + +1. Generate hash signature using OpenSSL: + +```sh +printf '' | openssl dgst -sha1 -r -hmac "" | awk '{print $1}' +``` + +You can use the flag `sha256` or `sha512` if you want a different hash function. + +2. Send a HTTP POST request to the webhook URL: + +```sh +curl -X POST -H "X-Signature: sha1=" -d '' +``` + +Generate hash signature using Go: + +```go +func sign(payload, key string) string { + h := hmac.New(sha1.New, []byte(key)) + h.Write([]byte(payload)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// set headers +req.Header.Set("X-Signature", fmt.Sprintf("sha1=%s", sign(payload, key))) +``` + +### GitHub receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: github-receiver + namespace: default +spec: + type: github + events: + - "ping" + - "push" + secretRef: + name: webhook-token + resources: + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: GitRepository + name: webapp + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + name: webapp +``` + +Note that you have to set the generated token as the GitHub webhook secret value. +The controller uses the `X-Hub-Signature` HTTP header to verify that the request is legitimate. + +### Gitea receiver + +The Gitea webhook works with the [Github receiver](#github-receiver). You can use the same example +given for the Github receiver. + +### GitLab receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: gitlab-receiver + namespace: default +spec: + type: gitlab + events: + - "Push Hook" + - "Tag Push Hook" + secretRef: + name: webhook-token + resources: + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: GitRepository + name: webapp-frontend + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: GitRepository + name: webapp-backend +``` + +Note that you have to configure the GitLab webhook with the generated token. +The controller uses the `X-Gitlab-Token` HTTP header to verify that the request is legitimate. + +### Bitbucket server receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: bitbucket-receiver + namespace: default +spec: + type: bitbucket + events: + - "repo:refs_changed" + secretRef: + name: webhook-token + resources: + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: GitRepository + name: webapp +``` + +Note that you have to set the generated token as the Bitbucket server webhook secret value. +The controller uses the `X-Hub-Signature` HTTP header to verify that the request is legitimate. + +Also note, the *Bitbucket cloud* service does not yet provide any support for signing webhook requests. +([1](https://jira.atlassian.com/browse/BCLOUD-14683), [2](https://jira.atlassian.com/browse/BCLOUD-12195)). +If your repositories are on Bitbucket cloud, you will need to use the Generic receiver instead. + +### Harbor receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: harbor-receiver + namespace: default +spec: + type: harbor + secretRef: + name: webhook-token + resources: + - apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + name: webapp + - apiVersion: image.toolkit.fluxcd.io/v1beta1 + kind: ImageRepository + name: webapp +``` + +Note that you have to set the generated token as the Harbor webhook authentication header. +The controller uses the `Authentication` HTTP header to verify that the request is legitimate. + +### DockerHub receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: dockerhub-receiver + namespace: default +spec: + type: dockerhub + secretRef: + name: webhook-token + resources: + - apiVersion: image.toolkit.fluxcd.io/v1beta1 + kind: ImageRepository + name: webapp +``` + +### Quay receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: quay-receiver + namespace: default +spec: + type: quay + secretRef: + name: webhook-token + resources: + - apiVersion: image.toolkit.fluxcd.io/vbeta1 + kind: ImageRepository + name: webapp +``` + +### Nexus receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: nexus-receiver + namespace: default +spec: + type: nexus + secretRef: + name: webhook-token + resources: + - apiVersion: image.toolkit.fluxcd.io/v1beta1 + kind: ImageRepository + name: webapp +``` + +Note that you have to fill in the generated token as the secret key when creating the Nexus Webhook Capability. +See [Nexus Webhook Capability](https://help.sonatype.com/repomanager3/webhooks/enabling-a-repository-webhook-capability) +The controller uses the `X-Nexus-Webhook-Signature` HTTP header to verify that the request is legitimate. + +### GCR receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: gcr-receiver + namespace: default +spec: + type: gcr + secretRef: + name: webhook-token + resources: + - apiVersion: image.toolkit.fluxcd.io/v1beta1 + kind: ImageRepository + name: webapp + namespace: default +``` + +Note that the controller decodes the JWT from the authorization +header of the push request and verifies it against the GCP API. +For more information, take a look at this +[documentation](https://cloud.google.com/pubsub/docs/push?&_ga=2.123897930.-1945316571.1602156486#authentication_and_authorization). + +### ACR receiver + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: acr-receiver + namespace: default +spec: + type: acr + secretRef: + name: webhook-token + resources: + - kind: ImageRepository + name: webapp +``` + +Note that the controller doesn't verify the authenticity of the request as Azure doesn't provide any mechanism for verification. +You can take a look at the [Azure Container webhook reference](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-webhook-reference). From 5fd17e622011e934d11ec8e517be6c682ea29ef2 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 4 Nov 2022 15:48:18 +0200 Subject: [PATCH 08/26] Refactor the Receiver URL generation Signed-off-by: Stefan Prodan --- api/v1beta2/receiver_types.go | 40 +++++++++++++++++----------- controllers/receiver_controller.go | 8 +----- controllers/suite_test.go | 6 +++++ internal/server/receiver_handlers.go | 9 +++---- internal/server/receiver_server.go | 17 +++--------- 5 files changed, 39 insertions(+), 41 deletions(-) diff --git a/api/v1beta2/receiver_types.go b/api/v1beta2/receiver_types.go index a9c027df5..3082ea6e7 100644 --- a/api/v1beta2/receiver_types.go +++ b/api/v1beta2/receiver_types.go @@ -17,11 +17,30 @@ limitations under the License. package v1beta2 import ( + "crypto/sha256" + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/fluxcd/pkg/apis/meta" ) +const ( + ReceiverKind string = "Receiver" + ReceiverWebhookPath string = "/hook/" + GenericReceiver string = "generic" + GenericHMACReceiver string = "generic-hmac" + GitHubReceiver string = "github" + GitLabReceiver string = "gitlab" + BitbucketReceiver string = "bitbucket" + HarborReceiver string = "harbor" + DockerHubReceiver string = "dockerhub" + QuayReceiver string = "quay" + GCRReceiver string = "gcr" + NexusReceiver string = "nexus" + ACRReceiver string = "acr" +) + // ReceiverSpec defines the desired state of Receiver type ReceiverSpec struct { // Type of webhook sender, used to determine @@ -67,21 +86,6 @@ type ReceiverStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` } -const ( - GenericReceiver string = "generic" - GenericHMACReceiver string = "generic-hmac" - GitHubReceiver string = "github" - GitLabReceiver string = "gitlab" - BitbucketReceiver string = "bitbucket" - HarborReceiver string = "harbor" - DockerHubReceiver string = "dockerhub" - QuayReceiver string = "quay" - GCRReceiver string = "gcr" - NexusReceiver string = "nexus" - ReceiverKind string = "Receiver" - ACRReceiver string = "acr" -) - // GetStatusConditions returns a pointer to the Status.Conditions slice // Deprecated: use GetConditions instead. func (in *Receiver) GetStatusConditions() *[]metav1.Condition { @@ -98,6 +102,12 @@ func (in *Receiver) SetConditions(conditions []metav1.Condition) { in.Status.Conditions = conditions } +// GetWebhookURL returns the incoming webhook URL for the given token. +func (in *Receiver) GetWebhookURL(token string) string { + digest := sha256.Sum256([]byte(token + in.GetName() + in.GetNamespace())) + return fmt.Sprintf("%s%x", ReceiverWebhookPath, digest) +} + // +genclient // +genclient:Namespaced // +kubebuilder:storageversion diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index 732d44cb8..5013b9173 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -18,7 +18,6 @@ package controllers import ( "context" - "crypto/sha256" "fmt" "time" @@ -142,7 +141,7 @@ func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *apiv1.Receiver) return ctrl.Result{Requeue: true}, err } - receiverURL := fmt.Sprintf("/hook/%s", sha256sum(token+obj.Name+obj.Namespace)) + receiverURL := obj.GetWebhookURL(token) msg := fmt.Sprintf("Receiver initialized with URL: %s", receiverURL) // Mark the resource as ready and set the URL @@ -226,8 +225,3 @@ func (r *ReceiverReconciler) token(ctx context.Context, receiver *apiv1.Receiver return token, nil } - -func sha256sum(val string) string { - digest := sha256.Sum256([]byte(val)) - return fmt.Sprintf("%x", digest) -} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 90f2ad042..7896fa6ab 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "crypto/sha256" "fmt" "math/rand" "os" @@ -154,3 +155,8 @@ func readManifest(manifest, namespace string) (*unstructured.Unstructured, error return object, nil } + +func sha256sum(val string) string { + digest := sha256.Sum256([]byte(val)) + return fmt.Sprintf("%x", digest) +} diff --git a/internal/server/receiver_handlers.go b/internal/server/receiver_handlers.go index b713be13b..e8eda551a 100644 --- a/internal/server/receiver_handlers.go +++ b/internal/server/receiver_handlers.go @@ -45,7 +45,7 @@ import ( func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { ctx := context.Background() - digest := url.PathEscape(strings.TrimLeft(r.RequestURI, "/hook/")) + digest := url.PathEscape(strings.TrimLeft(r.RequestURI, apiv1.ReceiverWebhookPath)) s.logger.Info(fmt.Sprintf("handling request: %s", digest)) @@ -61,7 +61,7 @@ func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Req for _, receiver := range allReceivers.Items { if !receiver.Spec.Suspend && conditions.IsReady(&receiver) && - receiver.Status.URL == fmt.Sprintf("/hook/%s", digest) { + receiver.Status.URL == fmt.Sprintf("%s%s", apiv1.ReceiverWebhookPath, digest) { receivers = append(receivers, receiver) } } @@ -231,10 +231,7 @@ func (s *ReceiverServer) validate(ctx context.Context, receiver apiv1.Receiver, logger.Info(fmt.Sprintf("handling DockerHub event from %s for tag %s", p.Repository.URL, p.PushData.Tag)) return nil case apiv1.GCRReceiver: - const ( - insert = "insert" - tokenIndex = len("Bearer ") - ) + const tokenIndex = len("Bearer ") type data struct { Action string `json:"action"` diff --git a/internal/server/receiver_server.go b/internal/server/receiver_server.go index 0f601710a..0ea4ccbd9 100644 --- a/internal/server/receiver_server.go +++ b/internal/server/receiver_server.go @@ -18,18 +18,16 @@ package server import ( "context" - "crypto/sha256" - "fmt" "net/http" - "net/url" "os" - "strings" "time" "github.com/go-logr/logr" "github.com/slok/go-http-metrics/middleware" "github.com/slok/go-http-metrics/middleware/std" "sigs.k8s.io/controller-runtime/pkg/client" + + apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" ) // ReceiverServer handles webhook POST requests @@ -39,7 +37,7 @@ type ReceiverServer struct { kubeClient client.Client } -// NewEventServer returns an HTTP server that handles webhooks +// NewReceiverServer returns an HTTP server that handles webhooks func NewReceiverServer(port string, logger logr.Logger, kubeClient client.Client) *ReceiverServer { return &ReceiverServer{ port: port, @@ -51,7 +49,7 @@ func NewReceiverServer(port string, logger logr.Logger, kubeClient client.Client // ListenAndServe starts the HTTP server on the specified port func (s *ReceiverServer) ListenAndServe(stopCh <-chan struct{}, mdlw middleware.Middleware) { mux := http.NewServeMux() - mux.Handle("/hook/", http.HandlerFunc(s.handlePayload())) + mux.Handle(apiv1.ReceiverWebhookPath, http.HandlerFunc(s.handlePayload())) h := std.Handler("", mdlw, mux) srv := &http.Server{ Addr: s.port, @@ -76,10 +74,3 @@ func (s *ReceiverServer) ListenAndServe(stopCh <-chan struct{}, mdlw middleware. s.logger.Info("Receiver server stopped") } } - -func receiverKeyFunc(r *http.Request) (string, error) { - id := url.PathEscape(strings.TrimLeft(r.RequestURI, "/hook/")) - val := strings.Join([]string{"receiver", id}, "/") - digest := sha256.Sum256([]byte(val)) - return fmt.Sprintf("%x", digest), nil -} From 2b35ef5845ea4861691c35ef5acf6beb33e27136 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 4 Nov 2022 17:40:02 +0200 Subject: [PATCH 09/26] API: Consolidate the documentation style for v1beta2 Signed-off-by: Stefan Prodan --- api/v1beta2/alert_types.go | 29 ++-- api/v1beta2/doc.go | 2 +- api/v1beta2/groupversion_info.go | 4 +- api/v1beta2/provider_types.go | 87 ++++++------ api/v1beta2/receiver_types.go | 29 ++-- api/v1beta2/reference_types.go | 8 +- ...notification.toolkit.fluxcd.io_alerts.yaml | 34 +++-- ...ification.toolkit.fluxcd.io_providers.yaml | 33 +++-- ...ification.toolkit.fluxcd.io_receivers.yaml | 33 +++-- docs/api/notification.md | 133 +++++++++--------- 10 files changed, 197 insertions(+), 195 deletions(-) diff --git a/api/v1beta2/alert_types.go b/api/v1beta2/alert_types.go index 9c367b1ee..88a008389 100644 --- a/api/v1beta2/alert_types.go +++ b/api/v1beta2/alert_types.go @@ -25,42 +25,45 @@ const ( AlertKind string = "Alert" ) -// AlertSpec defines an alerting rule for events involving a list of objects +// AlertSpec defines an alerting rule for events involving a list of objects. type AlertSpec struct { - // Send events using this provider. + // ProviderRef specifies which Provider this Alert should use. // +required ProviderRef meta.LocalObjectReference `json:"providerRef"` - // Filter events based on severity, defaults to ('info'). + // EventSeverity specifies how to filter events based on severity. // If set to 'info' no events will be filtered. // +kubebuilder:validation:Enum=info;error // +kubebuilder:default:=info // +optional EventSeverity string `json:"eventSeverity,omitempty"` - // Filter events based on the involved objects. + // EventSources specifies how to filter events based + // on the involved object kind, name and namespace. // +required EventSources []CrossNamespaceObjectReference `json:"eventSources"` - // A list of Golang regular expressions to be used for excluding messages. + // ExclusionList specifies a list of Golang regular expressions + // to be used for excluding messages. // +optional ExclusionList []string `json:"exclusionList,omitempty"` - // Short description of the impact and affected cluster. + // Summary holds a short description of the impact and affected cluster. // +kubebuilder:validation:MaxLength:=255 // +optional Summary string `json:"summary,omitempty"` - // This flag tells the controller to suspend subsequent events dispatching. - // Defaults to false. + // Suspend tells the controller to suspend subsequent + // events handling for this Alert. // +optional Suspend bool `json:"suspend,omitempty"` } -// AlertStatus defines the observed state of Alert +// AlertStatus defines the observed state of the Alert. type AlertStatus struct { meta.ReconcileRequestStatus `json:",inline"` + // Conditions holds the conditions for the Alert. // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` @@ -88,12 +91,6 @@ type Alert struct { Status AlertStatus `json:"status,omitempty"` } -// GetStatusConditions returns a pointer to the Status.Conditions slice -// Deprecated: use GetConditions instead. -func (in *Alert) GetStatusConditions() *[]metav1.Condition { - return &in.Status.Conditions -} - // GetConditions returns the status conditions of the object. func (in *Alert) GetConditions() []metav1.Condition { return in.Status.Conditions @@ -106,7 +103,7 @@ func (in *Alert) SetConditions(conditions []metav1.Condition) { // +kubebuilder:object:root=true -// AlertList contains a list of Alert +// AlertList contains a list of Alerts. type AlertList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` diff --git a/api/v1beta2/doc.go b/api/v1beta2/doc.go index 6b1836747..c1a08a328 100644 --- a/api/v1beta2/doc.go +++ b/api/v1beta2/doc.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1beta2 contains API Schema definitions for the notification v1beta2 API group +// Package v1beta2 contains API Schema definitions for the notification v1beta2 API group. // +kubebuilder:object:generate=true // +groupName=notification.toolkit.fluxcd.io package v1beta2 diff --git a/api/v1beta2/groupversion_info.go b/api/v1beta2/groupversion_info.go index 8cdf68dcc..35598eb69 100644 --- a/api/v1beta2/groupversion_info.go +++ b/api/v1beta2/groupversion_info.go @@ -22,10 +22,10 @@ import ( ) var ( - // GroupVersion is group version used to register these objects + // GroupVersion is group version used to register these objects. GroupVersion = schema.GroupVersion{Group: "notification.toolkit.fluxcd.io", Version: "v1beta2"} - // SchemeBuilder is used to add go types to the GroupVersionKind scheme + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} // AddToScheme adds the types in this group-version to the given scheme. diff --git a/api/v1beta2/provider_types.go b/api/v1beta2/provider_types.go index 90c9b2734..b2cc29a73 100644 --- a/api/v1beta2/provider_types.go +++ b/api/v1beta2/provider_types.go @@ -24,90 +24,88 @@ import ( ) const ( - ProviderKind string = "Provider" + ProviderKind string = "Provider" + GenericProvider string = "generic" + GenericHMACProvider string = "generic-hmac" + SlackProvider string = "slack" + GrafanaProvider string = "grafana" + DiscordProvider string = "discord" + MSTeamsProvider string = "msteams" + RocketProvider string = "rocket" + GitHubDispatchProvider string = "githubdispatch" + GitHubProvider string = "github" + GitLabProvider string = "gitlab" + BitbucketProvider string = "bitbucket" + AzureDevOpsProvider string = "azuredevops" + GoogleChatProvider string = "googlechat" + WebexProvider string = "webex" + SentryProvider string = "sentry" + AzureEventHubProvider string = "azureeventhub" + TelegramProvider string = "telegram" + LarkProvider string = "lark" + Matrix string = "matrix" + OpsgenieProvider string = "opsgenie" + AlertManagerProvider string = "alertmanager" ) -// ProviderSpec defines the desired state of Provider +// ProviderSpec defines the desired state of the Provider. type ProviderSpec struct { - // Type of provider + // Type specifies which Provider implementation to use. // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;generic-hmac;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie;alertmanager;grafana;githubdispatch; // +required Type string `json:"type"` - // Alert channel for this provider + // Channel specifies the destination channel where events should be posted. // +kubebuilder:validation:MaxLength:=2048 // +optional Channel string `json:"channel,omitempty"` - // Bot username for this provider + // Username specifies the name under which events are posted. // +kubebuilder:validation:MaxLength:=2048 // +optional Username string `json:"username,omitempty"` - // HTTP/S webhook address of this provider + // Address specifies the HTTP/S incoming webhook address of this Provider. // +kubebuilder:validation:Pattern="^(http|https)://" // +kubebuilder:validation:MaxLength:=2048 // +kubebuilder:validation:Optional // +optional Address string `json:"address,omitempty"` - // Timeout for sending alerts to the provider. + // Timeout for sending alerts to the Provider. // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m))+$" // +optional Timeout *metav1.Duration `json:"timeout,omitempty"` - // HTTP/S address of the proxy + // Proxy the HTTP/S address of the proxy server. // +kubebuilder:validation:Pattern="^(http|https)://" // +kubebuilder:validation:MaxLength:=2048 // +kubebuilder:validation:Optional // +optional Proxy string `json:"proxy,omitempty"` - // Secret reference containing the provider webhook URL - // using "address" as data key + // SecretRef specifies the Secret containing the authentication + // credentials for this Provider. // +optional SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` - // CertSecretRef can be given the name of a secret containing - // a PEM-encoded CA certificate (`caFile`) + // CertSecretRef specifies the Secret containing + // a PEM-encoded CA certificate (`caFile`). // +optional CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"` - // This flag tells the controller to suspend subsequent events handling. - // Defaults to false. + // Suspend tells the controller to suspend subsequent + // events handling for this Provider. // +optional Suspend bool `json:"suspend,omitempty"` } -const ( - GenericProvider string = "generic" - GenericHMACProvider string = "generic-hmac" - SlackProvider string = "slack" - GrafanaProvider string = "grafana" - DiscordProvider string = "discord" - MSTeamsProvider string = "msteams" - RocketProvider string = "rocket" - GitHubDispatchProvider string = "githubdispatch" - GitHubProvider string = "github" - GitLabProvider string = "gitlab" - BitbucketProvider string = "bitbucket" - AzureDevOpsProvider string = "azuredevops" - GoogleChatProvider string = "googlechat" - WebexProvider string = "webex" - SentryProvider string = "sentry" - AzureEventHubProvider string = "azureeventhub" - TelegramProvider string = "telegram" - LarkProvider string = "lark" - Matrix string = "matrix" - OpsgenieProvider string = "opsgenie" - AlertManagerProvider string = "alertmanager" -) - -// ProviderStatus defines the observed state of Provider +// ProviderStatus defines the observed state of the Provider. type ProviderStatus struct { meta.ReconcileRequestStatus `json:",inline"` + // Conditions holds the conditions for the Provider. // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` @@ -125,7 +123,7 @@ type ProviderStatus struct { // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" -// Provider is the Schema for the providers API +// Provider is the Schema for the providers API. type Provider struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -135,12 +133,6 @@ type Provider struct { Status ProviderStatus `json:"status,omitempty"` } -// GetStatusConditions returns a pointer to the Status.Conditions slice -// Deprecated: use GetConditions instead. -func (in *Provider) GetStatusConditions() *[]metav1.Condition { - return &in.Status.Conditions -} - // GetConditions returns the status conditions of the object. func (in *Provider) GetConditions() []metav1.Condition { return in.Status.Conditions @@ -153,7 +145,7 @@ func (in *Provider) SetConditions(conditions []metav1.Condition) { // +kubebuilder:object:root=true -// ProviderList contains a list of Provider +// ProviderList contains a list of Providers. type ProviderList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` @@ -164,6 +156,7 @@ func init() { SchemeBuilder.Register(&Provider{}, &ProviderList{}) } +// GetTimeout returns the timeout value with a default of 15s for this Provider. func (in *Provider) GetTimeout() time.Duration { duration := 15 * time.Second if in.Spec.Timeout != nil { diff --git a/api/v1beta2/receiver_types.go b/api/v1beta2/receiver_types.go index 3082ea6e7..7465d4810 100644 --- a/api/v1beta2/receiver_types.go +++ b/api/v1beta2/receiver_types.go @@ -41,7 +41,7 @@ const ( ACRReceiver string = "acr" ) -// ReceiverSpec defines the desired state of Receiver +// ReceiverSpec defines the desired state of the Receiver. type ReceiverSpec struct { // Type of webhook sender, used to determine // the validation procedure and payload deserialization. @@ -49,7 +49,7 @@ type ReceiverSpec struct { // +required Type string `json:"type"` - // A list of events to handle, + // Events specifies the list of event types to handle, // e.g. 'push' for GitHub or 'Push Hook' for GitLab. // +optional Events []string `json:"events"` @@ -58,40 +58,35 @@ type ReceiverSpec struct { // +required Resources []CrossNamespaceObjectReference `json:"resources"` - // Secret reference containing the token used - // to validate the payload authenticity + // SecretRef specifies the Secret containing the token used + // to validate the payload authenticity. // +required SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"` - // This flag tells the controller to suspend subsequent events handling. - // Defaults to false. + // Suspend tells the controller to suspend subsequent + // events handling for this receiver. // +optional Suspend bool `json:"suspend,omitempty"` } -// ReceiverStatus defines the observed state of Receiver +// ReceiverStatus defines the observed state of the Receiver. type ReceiverStatus struct { meta.ReconcileRequestStatus `json:",inline"` + // Conditions holds the conditions for the Receiver. // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` - // Generated webhook URL in the format + // URL is the generated incoming webhook address in the format // of '/hook/sha256sum(token+name+namespace)'. // +optional URL string `json:"url,omitempty"` - // ObservedGeneration is the last observed generation. + // ObservedGeneration is the last observed generation of the Receiver object. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` } -// GetStatusConditions returns a pointer to the Status.Conditions slice -// Deprecated: use GetConditions instead. -func (in *Receiver) GetStatusConditions() *[]metav1.Condition { - return &in.Status.Conditions -} - // GetConditions returns the status conditions of the object. func (in *Receiver) GetConditions() []metav1.Condition { return in.Status.Conditions @@ -117,7 +112,7 @@ func (in *Receiver) GetWebhookURL(token string) string { // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" -// Receiver is the Schema for the receivers API +// Receiver is the Schema for the receivers API. type Receiver struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -129,7 +124,7 @@ type Receiver struct { // +kubebuilder:object:root=true -// ReceiverList contains a list of Receiver +// ReceiverList contains a list of Receivers. type ReceiverList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` diff --git a/api/v1beta2/reference_types.go b/api/v1beta2/reference_types.go index b051890ac..50594f3fd 100644 --- a/api/v1beta2/reference_types.go +++ b/api/v1beta2/reference_types.go @@ -19,22 +19,22 @@ package v1beta2 // CrossNamespaceObjectReference contains enough information to let you locate the // typed referenced object at cluster level type CrossNamespaceObjectReference struct { - // API version of the referent + // API version of the referent. // +optional APIVersion string `json:"apiVersion,omitempty"` - // Kind of the referent + // Kind of the referent. // +kubebuilder:validation:Enum=Bucket;GitRepository;Kustomization;HelmRelease;HelmChart;HelmRepository;ImageRepository;ImagePolicy;ImageUpdateAutomation;OCIRepository // +required Kind string `json:"kind,omitempty"` - // Name of the referent + // Name of the referent. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=53 // +required Name string `json:"name"` - // Namespace of the referent + // Namespace of the referent. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=53 // +kubebuilder:validation:Optional diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml index 57983be78..a9d7465d1 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml @@ -238,27 +238,28 @@ spec: type: object spec: description: AlertSpec defines an alerting rule for events involving a - list of objects + list of objects. properties: eventSeverity: default: info - description: Filter events based on severity, defaults to ('info'). - If set to 'info' no events will be filtered. + description: EventSeverity specifies how to filter events based on + severity. If set to 'info' no events will be filtered. enum: - info - error type: string eventSources: - description: Filter events based on the involved objects. + description: EventSources specifies how to filter events based on + the involved object kind, name and namespace. items: description: CrossNamespaceObjectReference contains enough information to let you locate the typed referenced object at cluster level properties: apiVersion: - description: API version of the referent + description: API version of the referent. type: string kind: - description: Kind of the referent + description: Kind of the referent. enum: - Bucket - GitRepository @@ -281,12 +282,12 @@ spec: are ANDed. type: object name: - description: Name of the referent + description: Name of the referent. maxLength: 53 minLength: 1 type: string namespace: - description: Namespace of the referent + description: Namespace of the referent. maxLength: 53 minLength: 1 type: string @@ -295,13 +296,14 @@ spec: type: object type: array exclusionList: - description: A list of Golang regular expressions to be used for excluding - messages. + description: ExclusionList specifies a list of Golang regular expressions + to be used for excluding messages. items: type: string type: array providerRef: - description: Send events using this provider. + description: ProviderRef specifies which Provider this Alert should + use. properties: name: description: Name of the referent. @@ -310,12 +312,13 @@ spec: - name type: object summary: - description: Short description of the impact and affected cluster. + description: Summary holds a short description of the impact and affected + cluster. maxLength: 255 type: string suspend: - description: This flag tells the controller to suspend subsequent - events dispatching. Defaults to false. + description: Suspend tells the controller to suspend subsequent events + handling for this Alert. type: boolean required: - eventSources @@ -324,9 +327,10 @@ spec: status: default: observedGeneration: -1 - description: AlertStatus defines the observed state of Alert + description: AlertStatus defines the observed state of the Alert. properties: conditions: + description: Conditions holds the conditions for the Alert. items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index f7a04d2e5..7ce1517fd 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -211,7 +211,7 @@ spec: name: v1beta2 schema: openAPIV3Schema: - description: Provider is the Schema for the providers API + description: Provider is the Schema for the providers API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -226,16 +226,17 @@ spec: metadata: type: object spec: - description: ProviderSpec defines the desired state of Provider + description: ProviderSpec defines the desired state of the Provider. properties: address: - description: HTTP/S webhook address of this provider + description: Address specifies the HTTP/S incoming webhook address + of this Provider. maxLength: 2048 pattern: ^(http|https):// type: string certSecretRef: - description: CertSecretRef can be given the name of a secret containing - a PEM-encoded CA certificate (`caFile`) + description: CertSecretRef specifies the Secret containing a PEM-encoded + CA certificate (`caFile`). properties: name: description: Name of the referent. @@ -244,17 +245,18 @@ spec: - name type: object channel: - description: Alert channel for this provider + description: Channel specifies the destination channel where events + should be posted. maxLength: 2048 type: string proxy: - description: HTTP/S address of the proxy + description: Proxy the HTTP/S address of the proxy server. maxLength: 2048 pattern: ^(http|https):// type: string secretRef: - description: Secret reference containing the provider webhook URL - using "address" as data key + description: SecretRef specifies the Secret containing the authentication + credentials for this Provider. properties: name: description: Name of the referent. @@ -263,15 +265,15 @@ spec: - name type: object suspend: - description: This flag tells the controller to suspend subsequent - events handling. Defaults to false. + description: Suspend tells the controller to suspend subsequent events + handling for this Provider. type: boolean timeout: - description: Timeout for sending alerts to the provider. + description: Timeout for sending alerts to the Provider. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ type: string type: - description: Type of provider + description: Type specifies which Provider implementation to use. enum: - slack - discord @@ -296,7 +298,7 @@ spec: - githubdispatch type: string username: - description: Bot username for this provider + description: Username specifies the name under which events are posted. maxLength: 2048 type: string required: @@ -305,9 +307,10 @@ spec: status: default: observedGeneration: -1 - description: ProviderStatus defines the observed state of Provider + description: ProviderStatus defines the observed state of the Provider. properties: conditions: + description: Conditions holds the conditions for the Provider. items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml index 590d26c2f..d723f6ea8 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml @@ -230,7 +230,7 @@ spec: name: v1beta2 schema: openAPIV3Schema: - description: Receiver is the Schema for the receivers API + description: Receiver is the Schema for the receivers API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -245,11 +245,11 @@ spec: metadata: type: object spec: - description: ReceiverSpec defines the desired state of Receiver + description: ReceiverSpec defines the desired state of the Receiver. properties: events: - description: A list of events to handle, e.g. 'push' for GitHub or - 'Push Hook' for GitLab. + description: Events specifies the list of event types to handle, e.g. + 'push' for GitHub or 'Push Hook' for GitLab. items: type: string type: array @@ -260,10 +260,10 @@ spec: to let you locate the typed referenced object at cluster level properties: apiVersion: - description: API version of the referent + description: API version of the referent. type: string kind: - description: Kind of the referent + description: Kind of the referent. enum: - Bucket - GitRepository @@ -286,12 +286,12 @@ spec: are ANDed. type: object name: - description: Name of the referent + description: Name of the referent. maxLength: 53 minLength: 1 type: string namespace: - description: Namespace of the referent + description: Namespace of the referent. maxLength: 53 minLength: 1 type: string @@ -300,8 +300,8 @@ spec: type: object type: array secretRef: - description: Secret reference containing the token used to validate - the payload authenticity + description: SecretRef specifies the Secret containing the token used + to validate the payload authenticity. properties: name: description: Name of the referent. @@ -310,8 +310,8 @@ spec: - name type: object suspend: - description: This flag tells the controller to suspend subsequent - events handling. Defaults to false. + description: Suspend tells the controller to suspend subsequent events + handling for this receiver. type: boolean type: description: Type of webhook sender, used to determine the validation @@ -336,9 +336,10 @@ spec: status: default: observedGeneration: -1 - description: ReceiverStatus defines the observed state of Receiver + description: ReceiverStatus defines the observed state of the Receiver. properties: conditions: + description: Conditions holds the conditions for the Receiver. items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct @@ -412,11 +413,13 @@ spec: be detected. type: string observedGeneration: - description: ObservedGeneration is the last observed generation. + description: ObservedGeneration is the last observed generation of + the Receiver object. format: int64 type: integer url: - description: Generated webhook URL in the format of '/hook/sha256sum(token+name+namespace)'. + description: URL is the generated incoming webhook address in the + format of '/hook/sha256sum(token+name+namespace)'. type: string type: object type: object diff --git a/docs/api/notification.md b/docs/api/notification.md index 7df6e6029..d7b81e8b2 100644 --- a/docs/api/notification.md +++ b/docs/api/notification.md @@ -6,7 +6,7 @@

notification.toolkit.fluxcd.io/v1beta2

-

Package v1beta2 contains API Schema definitions for the notification v1beta2 API group

+

Package v1beta2 contains API Schema definitions for the notification v1beta2 API group.

Resource Types:
  • Alert @@ -82,7 +82,7 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference -

    Send events using this provider.

    +

    ProviderRef specifies which Provider this Alert should use.

    @@ -94,7 +94,7 @@ string (Optional) -

    Filter events based on severity, defaults to (‘info’). +

    EventSeverity specifies how to filter events based on severity. If set to ‘info’ no events will be filtered.

    @@ -108,7 +108,8 @@ If set to ‘info’ no events will be filtered.

    -

    Filter events based on the involved objects.

    +

    EventSources specifies how to filter events based +on the involved object kind, name and namespace.

    @@ -120,7 +121,8 @@ If set to ‘info’ no events will be filtered.

    (Optional) -

    A list of Golang regular expressions to be used for excluding messages.

    +

    ExclusionList specifies a list of Golang regular expressions +to be used for excluding messages.

    @@ -132,7 +134,7 @@ string (Optional) -

    Short description of the impact and affected cluster.

    +

    Summary holds a short description of the impact and affected cluster.

    @@ -144,8 +146,8 @@ bool (Optional) -

    This flag tells the controller to suspend subsequent events dispatching. -Defaults to false.

    +

    Suspend tells the controller to suspend subsequent +events handling for this Alert.

    @@ -169,7 +171,7 @@ AlertStatus

Provider

-

Provider is the Schema for the providers API

+

Provider is the Schema for the providers API.

@@ -232,7 +234,7 @@ string @@ -244,7 +246,7 @@ string @@ -256,7 +258,7 @@ string @@ -268,7 +270,7 @@ string @@ -282,7 +284,7 @@ Kubernetes meta/v1.Duration @@ -294,7 +296,7 @@ string @@ -308,8 +310,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference @@ -323,8 +325,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference @@ -336,8 +338,8 @@ bool
-

Type of provider

+

Type specifies which Provider implementation to use.

(Optional) -

Alert channel for this provider

+

Channel specifies the destination channel where events should be posted.

(Optional) -

Bot username for this provider

+

Username specifies the name under which events are posted.

(Optional) -

HTTP/S webhook address of this provider

+

Address specifies the HTTP/S incoming webhook address of this Provider.

(Optional) -

Timeout for sending alerts to the provider.

+

Timeout for sending alerts to the Provider.

(Optional) -

HTTP/S address of the proxy

+

Proxy the HTTP/S address of the proxy server.

(Optional) -

Secret reference containing the provider webhook URL -using “address” as data key

+

SecretRef specifies the Secret containing the authentication +credentials for this Provider.

(Optional) -

CertSecretRef can be given the name of a secret containing -a PEM-encoded CA certificate (caFile)

+

CertSecretRef specifies the Secret containing +a PEM-encoded CA certificate (caFile).

(Optional) -

This flag tells the controller to suspend subsequent events handling. -Defaults to false.

+

Suspend tells the controller to suspend subsequent +events handling for this Provider.

@@ -361,7 +363,7 @@ ProviderStatus

Receiver

-

Receiver is the Schema for the receivers API

+

Receiver is the Schema for the receivers API.

@@ -437,7 +439,7 @@ the validation procedure and payload deserialization.

@@ -464,8 +466,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference @@ -477,8 +479,8 @@ bool
(Optional) -

A list of events to handle, +

Events specifies the list of event types to handle, e.g. ‘push’ for GitHub or ‘Push Hook’ for GitLab.

-

Secret reference containing the token used -to validate the payload authenticity

+

SecretRef specifies the Secret containing the token used +to validate the payload authenticity.

(Optional) -

This flag tells the controller to suspend subsequent events handling. -Defaults to false.

+

Suspend tells the controller to suspend subsequent +events handling for this receiver.

@@ -506,7 +508,7 @@ ReceiverStatus (Appears on: Alert)

-

AlertSpec defines an alerting rule for events involving a list of objects

+

AlertSpec defines an alerting rule for events involving a list of objects.

@@ -527,7 +529,7 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference @@ -539,7 +541,7 @@ string @@ -553,7 +555,8 @@ If set to ‘info’ no events will be filtered.

@@ -565,7 +568,8 @@ If set to ‘info’ no events will be filtered.

@@ -577,7 +581,7 @@ string @@ -589,8 +593,8 @@ bool @@ -603,7 +607,7 @@ Defaults to false.

(Appears on:Alert)

-

AlertStatus defines the observed state of Alert

+

AlertStatus defines the observed state of the Alert.

-

Send events using this provider.

+

ProviderRef specifies which Provider this Alert should use.

(Optional) -

Filter events based on severity, defaults to (‘info’). +

EventSeverity specifies how to filter events based on severity. If set to ‘info’ no events will be filtered.

-

Filter events based on the involved objects.

+

EventSources specifies how to filter events based +on the involved object kind, name and namespace.

(Optional) -

A list of Golang regular expressions to be used for excluding messages.

+

ExclusionList specifies a list of Golang regular expressions +to be used for excluding messages.

(Optional) -

Short description of the impact and affected cluster.

+

Summary holds a short description of the impact and affected cluster.

(Optional) -

This flag tells the controller to suspend subsequent events dispatching. -Defaults to false.

+

Suspend tells the controller to suspend subsequent +events handling for this Alert.

@@ -640,6 +644,7 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus @@ -686,7 +691,7 @@ string @@ -697,7 +702,7 @@ string @@ -708,7 +713,7 @@ string @@ -720,7 +725,7 @@ string @@ -747,7 +752,7 @@ operator is “In”, and the values array contains only “value&rd (Appears on:Provider)

-

ProviderSpec defines the desired state of Provider

+

ProviderSpec defines the desired state of the Provider.

(Optional) +

Conditions holds the conditions for the Alert.

(Optional) -

API version of the referent

+

API version of the referent.

-

Kind of the referent

+

Kind of the referent.

-

Name of the referent

+

Name of the referent.

(Optional) -

Namespace of the referent

+

Namespace of the referent.

@@ -766,7 +771,7 @@ string @@ -778,7 +783,7 @@ string @@ -790,7 +795,7 @@ string @@ -802,7 +807,7 @@ string @@ -816,7 +821,7 @@ Kubernetes meta/v1.Duration @@ -828,7 +833,7 @@ string @@ -842,8 +847,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference @@ -857,8 +862,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference @@ -870,8 +875,8 @@ bool @@ -884,7 +889,7 @@ Defaults to false.

(Appears on:Provider)

-

ProviderStatus defines the observed state of Provider

+

ProviderStatus defines the observed state of the Provider.

-

Type of provider

+

Type specifies which Provider implementation to use.

(Optional) -

Alert channel for this provider

+

Channel specifies the destination channel where events should be posted.

(Optional) -

Bot username for this provider

+

Username specifies the name under which events are posted.

(Optional) -

HTTP/S webhook address of this provider

+

Address specifies the HTTP/S incoming webhook address of this Provider.

(Optional) -

Timeout for sending alerts to the provider.

+

Timeout for sending alerts to the Provider.

(Optional) -

HTTP/S address of the proxy

+

Proxy the HTTP/S address of the proxy server.

(Optional) -

Secret reference containing the provider webhook URL -using “address” as data key

+

SecretRef specifies the Secret containing the authentication +credentials for this Provider.

(Optional) -

CertSecretRef can be given the name of a secret containing -a PEM-encoded CA certificate (caFile)

+

CertSecretRef specifies the Secret containing +a PEM-encoded CA certificate (caFile).

(Optional) -

This flag tells the controller to suspend subsequent events handling. -Defaults to false.

+

Suspend tells the controller to suspend subsequent +events handling for this Provider.

@@ -921,6 +926,7 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus @@ -945,7 +951,7 @@ int64 (Appears on:Receiver)

-

ReceiverSpec defines the desired state of Receiver

+

ReceiverSpec defines the desired state of the Receiver.

(Optional) +

Conditions holds the conditions for the Provider.

@@ -977,7 +983,7 @@ the validation procedure and payload deserialization.

@@ -1004,8 +1010,8 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference @@ -1017,8 +1023,8 @@ bool @@ -1031,7 +1037,7 @@ Defaults to false.

(Appears on:Receiver)

-

ReceiverStatus defines the observed state of Receiver

+

ReceiverStatus defines the observed state of the Receiver.

(Optional) -

A list of events to handle, +

Events specifies the list of event types to handle, e.g. ‘push’ for GitHub or ‘Push Hook’ for GitLab.

-

Secret reference containing the token used -to validate the payload authenticity

+

SecretRef specifies the Secret containing the token used +to validate the payload authenticity.

(Optional) -

This flag tells the controller to suspend subsequent events handling. -Defaults to false.

+

Suspend tells the controller to suspend subsequent +events handling for this receiver.

@@ -1068,6 +1074,7 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus @@ -1079,7 +1086,7 @@ string @@ -1092,7 +1099,7 @@ int64 From ae65712eb64fc3ade90c21a4f5ded337e5bd818d Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 7 Nov 2022 16:59:27 +0200 Subject: [PATCH 10/26] Add reconciliation interval to providers and receivers Periodically reconcile providers and receivers with their Secret references to surface config errors after initialisation. Signed-off-by: Stefan Prodan --- api/v1beta2/provider_types.go | 7 +++ api/v1beta2/receiver_types.go | 7 +++ api/v1beta2/zz_generated.deepcopy.go | 2 + ...ification.toolkit.fluxcd.io_providers.yaml | 7 +++ ...ification.toolkit.fluxcd.io_receivers.yaml | 7 +++ controllers/alert_controller_test.go | 4 ++ controllers/provider_controller.go | 2 +- controllers/provider_controller_test.go | 5 ++ controllers/receiver_controller.go | 2 +- controllers/receiver_controller_test.go | 8 +++ docs/api/notification.md | 52 +++++++++++++++++++ docs/spec/v1beta2/providers.md | 6 +++ docs/spec/v1beta2/receivers.md | 6 +++ 13 files changed, 113 insertions(+), 2 deletions(-) diff --git a/api/v1beta2/provider_types.go b/api/v1beta2/provider_types.go index b2cc29a73..57a30ee17 100644 --- a/api/v1beta2/provider_types.go +++ b/api/v1beta2/provider_types.go @@ -55,6 +55,13 @@ type ProviderSpec struct { // +required Type string `json:"type"` + // Interval at which to reconcile the Provider with its Secret references. + // +kubebuilder:default="10m" + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" + // +required + Interval metav1.Duration `json:"interval"` + // Channel specifies the destination channel where events should be posted. // +kubebuilder:validation:MaxLength:=2048 // +optional diff --git a/api/v1beta2/receiver_types.go b/api/v1beta2/receiver_types.go index 7465d4810..cdaaaef92 100644 --- a/api/v1beta2/receiver_types.go +++ b/api/v1beta2/receiver_types.go @@ -49,6 +49,13 @@ type ReceiverSpec struct { // +required Type string `json:"type"` + // Interval at which to reconcile the Receiver with its Secret references. + // +kubebuilder:default="10m" + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" + // +required + Interval metav1.Duration `json:"interval"` + // Events specifies the list of event types to handle, // e.g. 'push' for GitHub or 'Push Hook' for GitLab. // +optional diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 4d8542c9c..6d9527e4f 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -221,6 +221,7 @@ func (in *ProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { *out = *in + out.Interval = in.Interval if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout *out = new(v1.Duration) @@ -333,6 +334,7 @@ func (in *ReceiverList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ReceiverSpec) DeepCopyInto(out *ReceiverSpec) { *out = *in + out.Interval = in.Interval if in.Events != nil { in, out := &in.Events, &out.Events *out = make([]string, len(*in)) diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index 7ce1517fd..b4863f4cc 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -249,6 +249,12 @@ spec: should be posted. maxLength: 2048 type: string + interval: + default: 10m + description: Interval at which to reconcile the Provider with its + Secret references. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string proxy: description: Proxy the HTTP/S address of the proxy server. maxLength: 2048 @@ -302,6 +308,7 @@ spec: maxLength: 2048 type: string required: + - interval - type type: object status: diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml index d723f6ea8..5a09643ec 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml @@ -253,6 +253,12 @@ spec: items: type: string type: array + interval: + default: 10m + description: Interval at which to reconcile the Receiver with its + Secret references. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string resources: description: A list of resources to be notified about changes. items: @@ -330,6 +336,7 @@ spec: - acr type: string required: + - interval - resources - type type: object diff --git a/controllers/alert_controller_test.go b/controllers/alert_controller_test.go index 33ee2532f..43817dc59 100644 --- a/controllers/alert_controller_test.go +++ b/controllers/alert_controller_test.go @@ -88,6 +88,7 @@ func TestAlertReconciler_Reconcile(t *testing.T) { g.Expect(k8sClient.Create(context.Background(), alert)).To(Succeed()) t.Run("fails with provider not found error", func(t *testing.T) { + g := NewWithT(t) g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA) return conditions.Has(resultA, meta.ReadyCondition) @@ -104,6 +105,7 @@ func TestAlertReconciler_Reconcile(t *testing.T) { }) t.Run("recovers when provider exists", func(t *testing.T) { + g := NewWithT(t) g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed()) g.Eventually(func() bool { @@ -117,6 +119,7 @@ func TestAlertReconciler_Reconcile(t *testing.T) { }) t.Run("handles reconcileAt", func(t *testing.T) { + g := NewWithT(t) reconcileRequestAt := metav1.Now().String() resultA.SetAnnotations(map[string]string{ meta.ReconcileRequestAnnotation: reconcileRequestAt, @@ -130,6 +133,7 @@ func TestAlertReconciler_Reconcile(t *testing.T) { }) t.Run("finalizes suspended object", func(t *testing.T) { + g := NewWithT(t) resultA.Spec.Suspend = true g.Expect(k8sClient.Update(context.Background(), resultA)).To(Succeed()) diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index 087c21c56..d358a2026 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -135,7 +135,7 @@ func (r *ProviderReconciler) reconcile(ctx context.Context, obj *apiv1.Provider) conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, apiv1.InitializedReason) ctrl.LoggerFrom(ctx).Info("Provider initialized") - return ctrl.Result{}, nil + return ctrl.Result{RequeueAfter: obj.Spec.Interval.Duration}, nil } func (r *ProviderReconciler) validate(ctx context.Context, provider *apiv1.Provider) error { diff --git a/controllers/provider_controller_test.go b/controllers/provider_controller_test.go index ded30ca8d..f6cac063b 100644 --- a/controllers/provider_controller_test.go +++ b/controllers/provider_controller_test.go @@ -62,6 +62,7 @@ func TestProviderReconciler_Reconcile(t *testing.T) { g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed()) t.Run("reports ready status", func(t *testing.T) { + g := NewWithT(t) g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) return resultP.Status.ObservedGeneration == resultP.Generation @@ -75,6 +76,7 @@ func TestProviderReconciler_Reconcile(t *testing.T) { }) t.Run("fails with secret not found error", func(t *testing.T) { + g := NewWithT(t) resultP.Spec.SecretRef = &meta.LocalObjectReference{ Name: secretName, } @@ -95,6 +97,7 @@ func TestProviderReconciler_Reconcile(t *testing.T) { }) t.Run("recovers when secret exists", func(t *testing.T) { + g := NewWithT(t) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, @@ -117,6 +120,7 @@ func TestProviderReconciler_Reconcile(t *testing.T) { }) t.Run("handles reconcileAt", func(t *testing.T) { + g := NewWithT(t) reconcileRequestAt := metav1.Now().String() resultP.SetAnnotations(map[string]string{ meta.ReconcileRequestAnnotation: reconcileRequestAt, @@ -130,6 +134,7 @@ func TestProviderReconciler_Reconcile(t *testing.T) { }) t.Run("finalizes suspended object", func(t *testing.T) { + g := NewWithT(t) resultP.Spec.Suspend = true g.Expect(k8sClient.Update(context.Background(), resultP)).To(Succeed()) diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index 5013b9173..c7d1dd9fd 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -150,7 +150,7 @@ func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *apiv1.Receiver) ctrl.LoggerFrom(ctx).Info(msg) - return ctrl.Result{}, nil + return ctrl.Result{RequeueAfter: obj.Spec.Interval.Duration}, nil } // patch updates the object status, conditions and finalizers. diff --git a/controllers/receiver_controller_test.go b/controllers/receiver_controller_test.go index 33d7fc8c3..c1e0e8de4 100644 --- a/controllers/receiver_controller_test.go +++ b/controllers/receiver_controller_test.go @@ -89,6 +89,7 @@ func TestReceiverReconciler_Reconcile(t *testing.T) { g.Expect(k8sClient.Create(context.Background(), receiver)).To(Succeed()) t.Run("reports ready status", func(t *testing.T) { + g := NewWithT(t) g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) return resultR.Status.ObservedGeneration == resultR.Generation @@ -102,6 +103,7 @@ func TestReceiverReconciler_Reconcile(t *testing.T) { }) t.Run("fails with secret not found error", func(t *testing.T) { + g := NewWithT(t) g.Expect(k8sClient.Delete(context.Background(), secret)).To(Succeed()) reconcileRequestAt := metav1.Now().String() @@ -124,6 +126,7 @@ func TestReceiverReconciler_Reconcile(t *testing.T) { }) t.Run("recovers when secret exists", func(t *testing.T) { + g := NewWithT(t) newSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, @@ -146,6 +149,7 @@ func TestReceiverReconciler_Reconcile(t *testing.T) { }) t.Run("handles reconcileAt", func(t *testing.T) { + g := NewWithT(t) reconcileRequestAt := metav1.Now().String() resultR.SetAnnotations(map[string]string{ meta.ReconcileRequestAnnotation: reconcileRequestAt, @@ -159,6 +163,7 @@ func TestReceiverReconciler_Reconcile(t *testing.T) { }) t.Run("finalizes suspended object", func(t *testing.T) { + g := NewWithT(t) resultR.Spec.Suspend = true g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed()) @@ -254,6 +259,7 @@ func TestReceiverReconciler_EventHandler(t *testing.T) { address := fmt.Sprintf("/hook/%s", sha256sum(token+receiverKey.Name+receiverKey.Namespace)) t.Run("generates URL when ready", func(t *testing.T) { + g := NewWithT(t) g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR) return conditions.IsReady(resultR) @@ -263,6 +269,7 @@ func TestReceiverReconciler_EventHandler(t *testing.T) { }) t.Run("doesn't update the URL on spec updates", func(t *testing.T) { + g := NewWithT(t) resultR.Spec.Events = []string{"ping", "push"} g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed()) @@ -276,6 +283,7 @@ func TestReceiverReconciler_EventHandler(t *testing.T) { }) t.Run("handles event", func(t *testing.T) { + g := NewWithT(t) res, err := http.Post("http://localhost:56788/"+address, "application/json", nil) g.Expect(err).ToNot(HaveOccurred()) g.Expect(res.StatusCode).To(Equal(http.StatusOK)) diff --git a/docs/api/notification.md b/docs/api/notification.md index d7b81e8b2..95891a664 100644 --- a/docs/api/notification.md +++ b/docs/api/notification.md @@ -239,6 +239,19 @@ string + + + + + + + + + + + + + + + + @@ -453,6 +454,7 @@ Kubernetes meta/v1.Duration @@ -810,6 +812,7 @@ Kubernetes meta/v1.Duration @@ -1023,6 +1026,7 @@ Kubernetes meta/v1.Duration From 1a4a4a8d4fc99f16ae24cfd299f32dbdf1080246 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 14 Nov 2022 15:48:26 +0200 Subject: [PATCH 15/26] Validate the provider inline address and proxy Signed-off-by: Stefan Prodan --- controllers/provider_controller.go | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index 94556f314..ec9ae0bf4 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -20,6 +20,7 @@ import ( "context" "crypto/x509" "fmt" + "net/url" "time" corev1 "k8s.io/api/core/v1" @@ -143,8 +144,14 @@ func (r *ProviderReconciler) reconcile(ctx context.Context, obj *apiv1.Provider) // Mark the resource as under reconciliation conditions.MarkReconciling(obj, meta.ProgressingReason, "Reconciliation in progress") - // validate provider spec and credentials - if err := r.validate(ctx, obj); err != nil { + // Validate the provider inline address and proxy. + if err := r.validateURLs(obj); err != nil { + conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.ValidationFailedReason, err.Error()) + return ctrl.Result{Requeue: true}, err + } + + // Validate the provider credentials. + if err := r.validateCredentials(ctx, obj); err != nil { conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.ValidationFailedReason, err.Error()) return ctrl.Result{Requeue: true}, err } @@ -154,7 +161,22 @@ func (r *ProviderReconciler) reconcile(ctx context.Context, obj *apiv1.Provider) return ctrl.Result{RequeueAfter: obj.Spec.Interval.Duration}, nil } -func (r *ProviderReconciler) validate(ctx context.Context, provider *apiv1.Provider) error { +func (r *ProviderReconciler) validateURLs(provider *apiv1.Provider) error { + address := provider.Spec.Address + proxy := provider.Spec.Proxy + + if provider.Spec.SecretRef == nil { + if _, err := url.ParseRequestURI(address); err != nil { + return fmt.Errorf("invalid address %s: %w", address, err) + } + if _, err := url.ParseRequestURI(proxy); proxy != "" && err != nil { + return fmt.Errorf("invalid proxy %s: %w", proxy, err) + } + } + return nil +} + +func (r *ProviderReconciler) validateCredentials(ctx context.Context, provider *apiv1.Provider) error { address := provider.Spec.Address proxy := provider.Spec.Proxy username := provider.Spec.Username From c9a774fe85db4a5f0154e5074d4135608b652ae8 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 14 Nov 2022 17:38:22 +0200 Subject: [PATCH 16/26] Compact success events Signed-off-by: Stefan Prodan --- api/v1beta2/receiver_types.go | 4 ++-- controllers/alert_controller.go | 13 ++++++------- controllers/alert_controller_test.go | 2 +- controllers/provider_controller.go | 4 ++-- controllers/receiver_controller.go | 5 ++--- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/api/v1beta2/receiver_types.go b/api/v1beta2/receiver_types.go index def029674..8fa8f1737 100644 --- a/api/v1beta2/receiver_types.go +++ b/api/v1beta2/receiver_types.go @@ -104,8 +104,8 @@ func (in *Receiver) SetConditions(conditions []metav1.Condition) { in.Status.Conditions = conditions } -// GetWebhookURL returns the incoming webhook URL for the given token. -func (in *Receiver) GetWebhookURL(token string) string { +// GetWebhookPath returns the incoming webhook path for the given token. +func (in *Receiver) GetWebhookPath(token string) string { digest := sha256.Sum256([]byte(token + in.GetName() + in.GetNamespace())) return fmt.Sprintf("%s%x", ReceiverWebhookPath, digest) } diff --git a/controllers/alert_controller.go b/controllers/alert_controller.go index 5aec2c476..4bf82b34a 100644 --- a/controllers/alert_controller.go +++ b/controllers/alert_controller.go @@ -129,8 +129,7 @@ func (r *AlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu // Log and emit success event. if retErr == nil && conditions.IsReady(obj) { - msg := fmt.Sprintf("Reconciliation finished in %s", - time.Since(reconcileStart).String()) + msg := "Reconciliation finished" log.Info(msg) r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) } @@ -158,12 +157,12 @@ func (r *AlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu } func (r *AlertReconciler) reconcile(ctx context.Context, alert *apiv1.Alert) (ctrl.Result, error) { - // Mark the resource as under reconciliation + // Mark the resource as under reconciliation. conditions.MarkReconciling(alert, meta.ProgressingReason, "Reconciliation in progress") - // validate alert spec and provider - if err := r.validate(ctx, alert); err != nil { - conditions.MarkFalse(alert, meta.ReadyCondition, apiv1.ValidationFailedReason, err.Error()) + // Check if the provider exist and is ready. + if err := r.isProviderReady(ctx, alert); err != nil { + conditions.MarkFalse(alert, meta.ReadyCondition, meta.FailedReason, err.Error()) return ctrl.Result{Requeue: true}, client.IgnoreNotFound(err) } @@ -172,7 +171,7 @@ func (r *AlertReconciler) reconcile(ctx context.Context, alert *apiv1.Alert) (ct return ctrl.Result{}, nil } -func (r *AlertReconciler) validate(ctx context.Context, alert *apiv1.Alert) error { +func (r *AlertReconciler) isProviderReady(ctx context.Context, alert *apiv1.Alert) error { provider := &apiv1.Provider{} providerName := types.NamespacedName{Namespace: alert.Namespace, Name: alert.Spec.ProviderRef.Name} if err := r.Get(ctx, providerName, provider); err != nil { diff --git a/controllers/alert_controller_test.go b/controllers/alert_controller_test.go index 43817dc59..a6d42888d 100644 --- a/controllers/alert_controller_test.go +++ b/controllers/alert_controller_test.go @@ -95,7 +95,7 @@ func TestAlertReconciler_Reconcile(t *testing.T) { }, timeout, time.Second).Should(BeTrue()) g.Expect(conditions.IsReady(resultA)).To(BeFalse()) - g.Expect(conditions.GetReason(resultA, meta.ReadyCondition)).To(BeIdenticalTo(apiv1.ValidationFailedReason)) + g.Expect(conditions.GetReason(resultA, meta.ReadyCondition)).To(BeIdenticalTo(meta.FailedReason)) g.Expect(conditions.GetMessage(resultA, meta.ReadyCondition)).To(ContainSubstring(providerName)) g.Expect(conditions.Has(resultA, meta.ReconcilingCondition)).To(BeTrue()) diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index ec9ae0bf4..fdeb6185e 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -112,8 +112,8 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r // Log and emit success event. if retErr == nil && conditions.IsReady(obj) { - msg := fmt.Sprintf("Reconciliation finished in %s, next run in %s", - time.Since(reconcileStart).String(), obj.Spec.Interval.Duration.String()) + msg := fmt.Sprintf("Reconciliation finished, next run in %s", + obj.Spec.Interval.Duration.String()) log.Info(msg) r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) } diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index f2b20ade1..e4fe6a3df 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -118,8 +118,7 @@ func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r // Log and emit success event. if retErr == nil && conditions.IsReady(obj) { - msg := fmt.Sprintf("Reconciliation finished in %s, next run in %s", - time.Since(reconcileStart).String(), obj.Spec.Interval.Duration.String()) + msg := fmt.Sprintf("Reconciliation finished, next run in %s", obj.Spec.Interval.Duration.String()) log.Info(msg) r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) } @@ -159,7 +158,7 @@ func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *apiv1.Receiver) return ctrl.Result{Requeue: true}, err } - receiverURL := obj.GetWebhookURL(token) + receiverURL := obj.GetWebhookPath(token) msg := fmt.Sprintf("Receiver initialized with URL: %s", receiverURL) // Mark the resource as ready and set the URL From d16588a1938c3320fcd6345e7a19db4a2dd576f5 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 15 Nov 2022 11:29:49 +0200 Subject: [PATCH 17/26] Deprecate `Receiver.status.url` in favor of `.status.webhookPath` Signed-off-by: Stefan Prodan --- api/v1beta2/receiver_types.go | 6 ++++++ .../notification.toolkit.fluxcd.io_receivers.yaml | 9 +++++++-- controllers/receiver_controller.go | 14 ++++++++------ controllers/receiver_controller_test.go | 3 +++ docs/api/notification.md | 14 ++++++++++++++ docs/spec/v1beta2/receivers.md | 14 +++++++------- internal/server/receiver_handlers.go | 2 +- 7 files changed, 46 insertions(+), 16 deletions(-) diff --git a/api/v1beta2/receiver_types.go b/api/v1beta2/receiver_types.go index 8fa8f1737..12319b374 100644 --- a/api/v1beta2/receiver_types.go +++ b/api/v1beta2/receiver_types.go @@ -86,9 +86,15 @@ type ReceiverStatus struct { // URL is the generated incoming webhook address in the format // of '/hook/sha256sum(token+name+namespace)'. + // Deprecated: Replaced by WebhookPath. // +optional URL string `json:"url,omitempty"` + // WebhookPath is the generated incoming webhook address in the format + // of '/hook/sha256sum(token+name+namespace)'. + // +optional + WebhookPath string `json:"webhookPath,omitempty"` + // ObservedGeneration is the last observed generation of the Receiver object. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml index db5956c42..6ca6dad53 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml @@ -424,8 +424,13 @@ spec: format: int64 type: integer url: - description: URL is the generated incoming webhook address in the - format of '/hook/sha256sum(token+name+namespace)'. + description: 'URL is the generated incoming webhook address in the + format of ''/hook/sha256sum(token+name+namespace)''. Deprecated: + Replaced by WebhookPath.' + type: string + webhookPath: + description: WebhookPath is the generated incoming webhook address + in the format of '/hook/sha256sum(token+name+namespace)'. type: string type: object type: object diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index e4fe6a3df..ce2550f81 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -148,24 +148,26 @@ func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r // reconcile steps through the actual reconciliation tasks for the object, it returns early on the first step that // produces an error. func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *apiv1.Receiver) (ctrl.Result, error) { - // Mark the resource as under reconciliation + // Mark the resource as under reconciliation. conditions.MarkReconciling(obj, meta.ProgressingReason, "Reconciliation in progress") token, err := r.token(ctx, obj) if err != nil { conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.TokenNotFoundReason, err.Error()) obj.Status.URL = "" + obj.Status.WebhookPath = "" return ctrl.Result{Requeue: true}, err } - receiverURL := obj.GetWebhookPath(token) - msg := fmt.Sprintf("Receiver initialized with URL: %s", receiverURL) + webhookPath := obj.GetWebhookPath(token) + msg := fmt.Sprintf("Receiver initialized for path: %s", webhookPath) - // Mark the resource as ready and set the URL + // Mark the resource as ready and set the webhook path in status. conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, msg) - if obj.Status.URL != receiverURL { - obj.Status.URL = receiverURL + if obj.Status.WebhookPath != webhookPath { + obj.Status.URL = webhookPath + obj.Status.WebhookPath = webhookPath ctrl.LoggerFrom(ctx).Info(msg) } diff --git a/controllers/receiver_controller_test.go b/controllers/receiver_controller_test.go index c1e0e8de4..26a012695 100644 --- a/controllers/receiver_controller_test.go +++ b/controllers/receiver_controller_test.go @@ -266,6 +266,8 @@ func TestReceiverReconciler_EventHandler(t *testing.T) { }, timeout, time.Second).Should(BeTrue()) g.Expect(resultR.Status.URL).To(BeIdenticalTo(address)) + g.Expect(resultR.Status.WebhookPath).To(BeIdenticalTo(address)) + g.Expect(conditions.GetMessage(resultR, meta.ReadyCondition)).To(ContainSubstring(address)) }) t.Run("doesn't update the URL on spec updates", func(t *testing.T) { @@ -280,6 +282,7 @@ func TestReceiverReconciler_EventHandler(t *testing.T) { g.Expect(conditions.IsReady(resultR)) g.Expect(resultR.Status.URL).To(BeIdenticalTo(address)) + g.Expect(resultR.Status.WebhookPath).To(BeIdenticalTo(address)) }) t.Run("handles event", func(t *testing.T) { diff --git a/docs/api/notification.md b/docs/api/notification.md index 082fa17e8..cc2837b5e 100644 --- a/docs/api/notification.md +++ b/docs/api/notification.md @@ -1143,6 +1143,20 @@ string + + + + diff --git a/docs/spec/v1beta2/receivers.md b/docs/spec/v1beta2/receivers.md index fe6b12a1d..e305e3337 100644 --- a/docs/spec/v1beta2/receivers.md +++ b/docs/spec/v1beta2/receivers.md @@ -33,9 +33,9 @@ In the above example: - A Receiver named `github-receiver` is created, indicated by the `.metadata.name` field. -- The notification-controller generates a unique URL using the Receiver +- The notification-controller generates a unique webhook path using the Receiver name, namespace and the token from the referenced `.spec.secretRef.name` secret. -- The URL is reported in the `.status.url` field. +- The incoming webhook path is reported in the `.status.webhookPath` field. - When a GitHub push event is received, the controller verifies the that the request is legitimate using HMAC and the `X-Hub-Signature` HTTP header. - If the event type matches `.spec.events` and the payload is verified, then the controller @@ -62,12 +62,12 @@ You can run this example by saving the manifest into `github-receiver.yaml`. ```console NAME READY STATUS - github-receiver True Receiver initialised with URL: /hook/bed6d00b5555b1603e1f59b94d7fdbca58089cb5663633fb83f2815dc626d92b + github-receiver True Receiver initialised for path: /hook/bed6d00b5555b1603e1f59b94d7fdbca58089cb5663633fb83f2815dc626d92b ``` 4. On GitHub, navigate to your repository and click on the "Add webhook" button under "Settings/Webhooks". Fill the form with: - - **Payload URL**: compose the address using the receiver LB and the generated URL `http:///`. + - **Payload URL**: compose the address using the receiver ingress hostname and the generated path `https:///`. - **Secret**: use the token string ## Writing a Receiver spec @@ -269,7 +269,7 @@ The controller uses the `X-Hub-Signature` HTTP header to verify that the request ### Gitea receiver -The Gitea webhook works with the [Github receiver](#github-receiver). You can use the same example +The Gitea webhook works with the [GitHub receiver](#github-receiver). You can use the same example given for the Github receiver. ### GitLab receiver @@ -381,7 +381,7 @@ spec: secretRef: name: webhook-token resources: - - apiVersion: image.toolkit.fluxcd.io/vbeta1 + - apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImageRepository name: webapp ``` @@ -449,5 +449,5 @@ spec: name: webapp ``` -Note that the controller doesn't verify the authenticity of the request as Azure doesn't provide any mechanism for verification. +Note that the controller doesn't verify the authenticity of the request as Azure does not provide any mechanism for verification. You can take a look at the [Azure Container webhook reference](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-webhook-reference). diff --git a/internal/server/receiver_handlers.go b/internal/server/receiver_handlers.go index e8eda551a..492bc8e7f 100644 --- a/internal/server/receiver_handlers.go +++ b/internal/server/receiver_handlers.go @@ -61,7 +61,7 @@ func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Req for _, receiver := range allReceivers.Items { if !receiver.Spec.Suspend && conditions.IsReady(&receiver) && - receiver.Status.URL == fmt.Sprintf("%s%s", apiv1.ReceiverWebhookPath, digest) { + receiver.Status.WebhookPath == fmt.Sprintf("%s%s", apiv1.ReceiverWebhookPath, digest) { receivers = append(receivers, receiver) } } From 5a0d6dde0a267d0fa1c5a618c9c0497dc667096e Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 15 Nov 2022 17:16:08 +0200 Subject: [PATCH 18/26] Use `ProgressingWithRetry` from `fluxcd/pkg/apis/meta` Signed-off-by: Stefan Prodan --- api/v1beta2/condition_types.go | 4 ---- controllers/alert_controller.go | 2 +- controllers/alert_controller_test.go | 2 +- controllers/provider_controller.go | 4 ++-- controllers/provider_controller_test.go | 2 +- controllers/receiver_controller.go | 2 +- controllers/receiver_controller_test.go | 2 +- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/api/v1beta2/condition_types.go b/api/v1beta2/condition_types.go index 119345797..9ad629ec0 100644 --- a/api/v1beta2/condition_types.go +++ b/api/v1beta2/condition_types.go @@ -28,8 +28,4 @@ const ( // TokenNotFoundReason represents the fact that receiver token can't be found. TokenNotFoundReason string = "TokenNotFound" - - // ProgressingWithRetryReason represents the fact that - // the reconciliation encountered an error that will be retried. - ProgressingWithRetryReason string = "ProgressingWithRetry" ) diff --git a/controllers/alert_controller.go b/controllers/alert_controller.go index 4bf82b34a..5e3dd6259 100644 --- a/controllers/alert_controller.go +++ b/controllers/alert_controller.go @@ -241,7 +241,7 @@ func (r *AlertReconciler) patch(ctx context.Context, obj *apiv1.Alert, patcher * if conditions.IsFalse(obj, meta.ReadyCondition) && conditions.Has(obj, meta.ReconcilingCondition) { rc := conditions.Get(obj, meta.ReconcilingCondition) - rc.Reason = apiv1.ProgressingWithRetryReason + rc.Reason = meta.ProgressingWithRetryReason conditions.Set(obj, rc) } diff --git a/controllers/alert_controller_test.go b/controllers/alert_controller_test.go index a6d42888d..0c50f1308 100644 --- a/controllers/alert_controller_test.go +++ b/controllers/alert_controller_test.go @@ -99,7 +99,7 @@ func TestAlertReconciler_Reconcile(t *testing.T) { g.Expect(conditions.GetMessage(resultA, meta.ReadyCondition)).To(ContainSubstring(providerName)) g.Expect(conditions.Has(resultA, meta.ReconcilingCondition)).To(BeTrue()) - g.Expect(conditions.GetReason(resultA, meta.ReconcilingCondition)).To(BeIdenticalTo(apiv1.ProgressingWithRetryReason)) + g.Expect(conditions.GetReason(resultA, meta.ReconcilingCondition)).To(BeIdenticalTo(meta.ProgressingWithRetryReason)) g.Expect(conditions.GetObservedGeneration(resultA, meta.ReconcilingCondition)).To(BeIdenticalTo(resultA.Generation)) g.Expect(controllerutil.ContainsFinalizer(resultA, apiv1.NotificationFinalizer)).To(BeTrue()) }) diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index fdeb6185e..dc16419cd 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -146,7 +146,7 @@ func (r *ProviderReconciler) reconcile(ctx context.Context, obj *apiv1.Provider) // Validate the provider inline address and proxy. if err := r.validateURLs(obj); err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.ValidationFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.InvalidURLReason, err.Error()) return ctrl.Result{Requeue: true}, err } @@ -284,7 +284,7 @@ func (r *ProviderReconciler) patch(ctx context.Context, obj *apiv1.Provider, pat if conditions.IsFalse(obj, meta.ReadyCondition) && conditions.Has(obj, meta.ReconcilingCondition) { rc := conditions.Get(obj, meta.ReconcilingCondition) - rc.Reason = apiv1.ProgressingWithRetryReason + rc.Reason = meta.ProgressingWithRetryReason conditions.Set(obj, rc) } diff --git a/controllers/provider_controller_test.go b/controllers/provider_controller_test.go index f6cac063b..0210058d7 100644 --- a/controllers/provider_controller_test.go +++ b/controllers/provider_controller_test.go @@ -91,7 +91,7 @@ func TestProviderReconciler_Reconcile(t *testing.T) { g.Expect(conditions.GetMessage(resultP, meta.ReadyCondition)).To(ContainSubstring(secretName)) g.Expect(conditions.Has(resultP, meta.ReconcilingCondition)).To(BeTrue()) - g.Expect(conditions.GetReason(resultP, meta.ReconcilingCondition)).To(BeIdenticalTo(apiv1.ProgressingWithRetryReason)) + g.Expect(conditions.GetReason(resultP, meta.ReconcilingCondition)).To(BeIdenticalTo(meta.ProgressingWithRetryReason)) g.Expect(conditions.GetObservedGeneration(resultP, meta.ReconcilingCondition)).To(BeIdenticalTo(resultP.Generation)) g.Expect(resultP.Status.ObservedGeneration).To(BeIdenticalTo(resultP.Generation - 1)) }) diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index ce2550f81..4253de50b 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -206,7 +206,7 @@ func (r *ReceiverReconciler) patch(ctx context.Context, obj *apiv1.Receiver, pat if conditions.IsFalse(obj, meta.ReadyCondition) && conditions.Has(obj, meta.ReconcilingCondition) { rc := conditions.Get(obj, meta.ReconcilingCondition) - rc.Reason = apiv1.ProgressingWithRetryReason + rc.Reason = meta.ProgressingWithRetryReason conditions.Set(obj, rc) } diff --git a/controllers/receiver_controller_test.go b/controllers/receiver_controller_test.go index 26a012695..b74f9140e 100644 --- a/controllers/receiver_controller_test.go +++ b/controllers/receiver_controller_test.go @@ -121,7 +121,7 @@ func TestReceiverReconciler_Reconcile(t *testing.T) { g.Expect(conditions.GetMessage(resultR, meta.ReadyCondition)).To(ContainSubstring(secretName)) g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeTrue()) - g.Expect(conditions.GetReason(resultR, meta.ReconcilingCondition)).To(BeIdenticalTo(apiv1.ProgressingWithRetryReason)) + g.Expect(conditions.GetReason(resultR, meta.ReconcilingCondition)).To(BeIdenticalTo(meta.ProgressingWithRetryReason)) g.Expect(conditions.GetObservedGeneration(resultR, meta.ReconcilingCondition)).To(BeIdenticalTo(resultR.Generation)) }) From 4aad301aa229fb31d73c01e38caa63eb3867ae8a Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 16 Nov 2022 11:05:36 +0200 Subject: [PATCH 19/26] Stall providers with invalid spec Signed-off-by: Stefan Prodan --- api/v1beta2/provider_types.go | 14 +++++--- api/v1beta2/receiver_types.go | 4 +-- api/v1beta2/zz_generated.deepcopy.go | 12 +++++-- ...ification.toolkit.fluxcd.io_providers.yaml | 6 ++-- ...ification.toolkit.fluxcd.io_receivers.yaml | 2 +- controllers/provider_controller.go | 22 +++++++++++-- controllers/provider_controller_test.go | 33 +++++++++++++++++++ controllers/receiver_controller_test.go | 1 + 8 files changed, 79 insertions(+), 15 deletions(-) diff --git a/api/v1beta2/provider_types.go b/api/v1beta2/provider_types.go index fe4eca20c..bd0e5cc2c 100644 --- a/api/v1beta2/provider_types.go +++ b/api/v1beta2/provider_types.go @@ -56,11 +56,11 @@ type ProviderSpec struct { Type string `json:"type"` // Interval at which to reconcile the Provider with its Secret references. - // +kubebuilder:default="10m" + // +kubebuilder:default="600s" // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" // +optional - Interval metav1.Duration `json:"interval"` + Interval *metav1.Duration `json:"interval,omitempty"` // Channel specifies the destination channel where events should be posted. // +kubebuilder:validation:MaxLength:=2048 @@ -73,7 +73,7 @@ type ProviderSpec struct { Username string `json:"username,omitempty"` // Address specifies the HTTP/S incoming webhook address of this Provider. - // +kubebuilder:validation:Pattern="^(http|https)://" + // +kubebuilder:validation:Pattern="^(http|https)://.*$" // +kubebuilder:validation:MaxLength:=2048 // +kubebuilder:validation:Optional // +optional @@ -86,7 +86,7 @@ type ProviderSpec struct { Timeout *metav1.Duration `json:"timeout,omitempty"` // Proxy the HTTP/S address of the proxy server. - // +kubebuilder:validation:Pattern="^(http|https)://" + // +kubebuilder:validation:Pattern="^(http|https)://.*$" // +kubebuilder:validation:MaxLength:=2048 // +kubebuilder:validation:Optional // +optional @@ -172,3 +172,9 @@ func (in *Provider) GetTimeout() time.Duration { return duration } + +// GetRequeueAfter returns the duration after which the Provider must be +// reconciled again. +func (in *Provider) GetRequeueAfter() time.Duration { + return in.Spec.Interval.Duration +} diff --git a/api/v1beta2/receiver_types.go b/api/v1beta2/receiver_types.go index 12319b374..b0cdd2a51 100644 --- a/api/v1beta2/receiver_types.go +++ b/api/v1beta2/receiver_types.go @@ -50,11 +50,11 @@ type ReceiverSpec struct { Type string `json:"type"` // Interval at which to reconcile the Receiver with its Secret references. - // +kubebuilder:default="10m" + // +kubebuilder:default="600s" // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" // +optional - Interval metav1.Duration `json:"interval"` + Interval *metav1.Duration `json:"interval,omitempty"` // Events specifies the list of event types to handle, // e.g. 'push' for GitHub or 'Push Hook' for GitLab. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 6d9527e4f..431aac8ba 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -221,7 +221,11 @@ func (in *ProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { *out = *in - out.Interval = in.Interval + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout *out = new(v1.Duration) @@ -334,7 +338,11 @@ func (in *ReceiverList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ReceiverSpec) DeepCopyInto(out *ReceiverSpec) { *out = *in - out.Interval = in.Interval + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } if in.Events != nil { in, out := &in.Events, &out.Events *out = make([]string, len(*in)) diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index 55d1c3d4e..c9c1d220a 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -232,7 +232,7 @@ spec: description: Address specifies the HTTP/S incoming webhook address of this Provider. maxLength: 2048 - pattern: ^(http|https):// + pattern: ^(http|https)://.*$ type: string certSecretRef: description: CertSecretRef specifies the Secret containing a PEM-encoded @@ -250,7 +250,7 @@ spec: maxLength: 2048 type: string interval: - default: 10m + default: 600s description: Interval at which to reconcile the Provider with its Secret references. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ @@ -258,7 +258,7 @@ spec: proxy: description: Proxy the HTTP/S address of the proxy server. maxLength: 2048 - pattern: ^(http|https):// + pattern: ^(http|https)://.*$ type: string secretRef: description: SecretRef specifies the Secret containing the authentication diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml index 6ca6dad53..3837d819f 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml @@ -254,7 +254,7 @@ spec: type: string type: array interval: - default: 10m + default: 600s description: Interval at which to reconcile the Receiver with its Secret references. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index dc16419cd..4db44bff6 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -110,6 +110,14 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r r.Event(obj, corev1.EventTypeWarning, meta.FailedReason, retErr.Error()) } + // Log the staleness error and pause reconciliation until spec changes. + if conditions.IsStalled(obj) { + result = ctrl.Result{Requeue: false} + log.Error(retErr, "Reconciliation has stalled") + retErr = nil + return + } + // Log and emit success event. if retErr == nil && conditions.IsReady(obj) { msg := fmt.Sprintf("Reconciliation finished, next run in %s", @@ -141,12 +149,14 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r } func (r *ProviderReconciler) reconcile(ctx context.Context, obj *apiv1.Provider) (ctrl.Result, error) { - // Mark the resource as under reconciliation + // Mark the resource as under reconciliation. conditions.MarkReconciling(obj, meta.ProgressingReason, "Reconciliation in progress") + conditions.Delete(obj, meta.StalledCondition) - // Validate the provider inline address and proxy. + // Mark the reconciliation as stalled if the inline URL and/or proxy are invalid. if err := r.validateURLs(obj); err != nil { conditions.MarkFalse(obj, meta.ReadyCondition, meta.InvalidURLReason, err.Error()) + conditions.MarkTrue(obj, meta.StalledCondition, meta.InvalidURLReason, err.Error()) return ctrl.Result{Requeue: true}, err } @@ -272,10 +282,11 @@ func (r *ProviderReconciler) patch(ctx context.Context, obj *apiv1.Provider, pat obj.Status.LastHandledReconcileAt = v } - // Remove the Reconciling condition and update the observed generation + // Remove the Reconciling/Stalled condition and update the observed generation // if the reconciliation was successful. if conditions.IsTrue(obj, meta.ReadyCondition) { conditions.Delete(obj, meta.ReconcilingCondition) + conditions.Delete(obj, meta.StalledCondition) obj.Status.ObservedGeneration = obj.Generation } @@ -288,6 +299,11 @@ func (r *ProviderReconciler) patch(ctx context.Context, obj *apiv1.Provider, pat conditions.Set(obj, rc) } + // Remove the Reconciling condition if the reconciliation has stalled. + if conditions.Has(obj, meta.StalledCondition) { + conditions.Delete(obj, meta.ReconcilingCondition) + } + // Patch the object status, conditions and finalizers. if err := patcher.Patch(ctx, obj, patchOpts...); err != nil { if !obj.GetDeletionTimestamp().IsZero() { diff --git a/controllers/provider_controller_test.go b/controllers/provider_controller_test.go index 0210058d7..2315daed4 100644 --- a/controllers/provider_controller_test.go +++ b/controllers/provider_controller_test.go @@ -73,6 +73,7 @@ func TestProviderReconciler_Reconcile(t *testing.T) { g.Expect(conditions.Has(resultP, meta.ReconcilingCondition)).To(BeFalse()) g.Expect(controllerutil.ContainsFinalizer(resultP, apiv1.NotificationFinalizer)).To(BeTrue()) + g.Expect(resultP.Spec.Interval.Duration).To(BeIdenticalTo(10 * time.Minute)) }) t.Run("fails with secret not found error", func(t *testing.T) { @@ -133,6 +134,38 @@ func TestProviderReconciler_Reconcile(t *testing.T) { }, timeout, time.Second).Should(BeTrue()) }) + t.Run("becomes stalled on invalid proxy", func(t *testing.T) { + g := NewWithT(t) + resultP.Spec.SecretRef = nil + resultP.Spec.Proxy = "https://proxy.internal|" + g.Expect(k8sClient.Update(context.Background(), resultP)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) + return !conditions.IsReady(resultP) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.Has(resultP, meta.ReconcilingCondition)).To(BeFalse()) + g.Expect(conditions.Has(resultP, meta.StalledCondition)).To(BeTrue()) + g.Expect(conditions.GetObservedGeneration(resultP, meta.StalledCondition)).To(BeIdenticalTo(resultP.Generation)) + g.Expect(conditions.GetReason(resultP, meta.StalledCondition)).To(BeIdenticalTo(meta.InvalidURLReason)) + g.Expect(conditions.GetReason(resultP, meta.ReadyCondition)).To(BeIdenticalTo(meta.InvalidURLReason)) + }) + + t.Run("recovers from staleness", func(t *testing.T) { + g := NewWithT(t) + resultP.Spec.Proxy = "https://proxy.internal" + g.Expect(k8sClient.Update(context.Background(), resultP)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), resultP) + return conditions.IsReady(resultP) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.Has(resultP, meta.ReconcilingCondition)).To(BeFalse()) + g.Expect(conditions.Has(resultP, meta.StalledCondition)).To(BeFalse()) + }) + t.Run("finalizes suspended object", func(t *testing.T) { g := NewWithT(t) resultP.Spec.Suspend = true diff --git a/controllers/receiver_controller_test.go b/controllers/receiver_controller_test.go index b74f9140e..07c33efa5 100644 --- a/controllers/receiver_controller_test.go +++ b/controllers/receiver_controller_test.go @@ -100,6 +100,7 @@ func TestReceiverReconciler_Reconcile(t *testing.T) { g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeFalse()) g.Expect(controllerutil.ContainsFinalizer(resultR, apiv1.NotificationFinalizer)).To(BeTrue()) + g.Expect(resultR.Spec.Interval.Duration).To(BeIdenticalTo(10 * time.Minute)) }) t.Run("fails with secret not found error", func(t *testing.T) { From 831785a2957d291e03d9a5c39a8f89dda10f4902 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 16 Nov 2022 13:24:06 +0200 Subject: [PATCH 20/26] docs: Add status and events to examples Signed-off-by: Stefan Prodan --- docs/spec/v1beta2/alerts.md | 26 +++++++++++++++++++++++--- docs/spec/v1beta2/providers.md | 28 ++++++++++++++++++++++++++-- docs/spec/v1beta2/receivers.md | 26 +++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/docs/spec/v1beta2/alerts.md b/docs/spec/v1beta2/alerts.md index adce0df9f..5cdec363f 100644 --- a/docs/spec/v1beta2/alerts.md +++ b/docs/spec/v1beta2/alerts.md @@ -65,6 +65,25 @@ You can run this example by saving the manifests into `slack-alerts.yaml`. kubectl -n flux-system apply --server-side -f slack-alerts.yaml ``` +3. Run `kubectl -n flux-system describe alert slack` to see its status: + + ```console + ... + Status: + Conditions: + Last Transition Time: 2022-11-16T23:43:38Z + Message: Initialized + Observed Generation: 1 + Reason: Succeeded + Status: True + Type: Ready + Observed Generation: 1 + Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Succeeded 82s notification-controller Initialized + ``` + ## Writing an Alert spec As with all other Kubernetes config, an Alert needs `apiVersion`, @@ -136,9 +155,10 @@ eventSources: #### Disable cross-namespace selectors -**Note:** On multi-tenant clusters, platform admins can disable cross-namespace references with the -`--no-cross-namespace-refs=true` flag. When this flag is set, alerts can only refer to event sources -in the same namespace as the alert object, preventing tenants from subscribing to another tenant's events. +**Note:** On multi-tenant clusters, platform admins can disable cross-namespace references by +starting the controller with the `--no-cross-namespace-refs=true` flag. +When this flag is set, alerts can only refer to event sources in the same namespace as the alert object, +preventing tenants from subscribing to another tenant's events. ### Event severity diff --git a/docs/spec/v1beta2/providers.md b/docs/spec/v1beta2/providers.md index 34c33f434..6360fb349 100644 --- a/docs/spec/v1beta2/providers.md +++ b/docs/spec/v1beta2/providers.md @@ -66,6 +66,25 @@ You can run this example by saving the manifests into `slack-alerts.yaml`. kubectl -n flagger-system apply --server-side -f slack-alerts.yaml ``` +3. Run `kubectl -n flagger-system describe provider slack-bot` to see its status: + + ```console + ... + Status: + Conditions: + Last Transition Time: 2022-11-16T23:43:38Z + Message: Initialized + Observed Generation: 1 + Reason: Succeeded + Status: True + Type: Ready + Observed Generation: 1 + Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Succeeded 82s notification-controller Reconciliation finished, next run in 10m + ``` + ## Writing a provider spec As with all other Kubernetes config, a Provider needs `apiVersion`, @@ -106,6 +125,7 @@ The supported providers are: If the address contains sensitive information such as tokens or passwords, it is recommended to store the address in the Kubernetes secret referenced by `.spec.secretRef.name`. +When the referenced Secret contains an `address` key, the `.spec.address` value is ignored. ### Channel @@ -126,7 +146,7 @@ The Kubernetes secret can have any of the following keys: #### Address example -For providers which emblem tokens or other sensitive information in the URL, +For providers which embed tokens or other sensitive information in the URL, the incoming webhooks address can be stored in the secret using the `address` key: ```yaml @@ -227,6 +247,10 @@ stringData: `.spec.proxy` is an optional field to specify an HTTP/S proxy address. +If the proxy address contains sensitive information such as basic auth credentials, it is +recommended to store the proxy in the Kubernetes secret referenced by `.spec.secretRef.name`. +When the referenced Secret contains a `proxy` key, the `.spec.proxy` value is ignored. + ### Interval `.spec.interval` is a required field with a default of ten minutes that specifies @@ -239,7 +263,7 @@ references. When set to `true`, the controller will stop sending events to this provider. When the field is set to `false` or removed, it will resume. -## Provider guides +## Working with Providers ### Generic webhook diff --git a/docs/spec/v1beta2/receivers.md b/docs/spec/v1beta2/receivers.md index e305e3337..2a84a509b 100644 --- a/docs/spec/v1beta2/receivers.md +++ b/docs/spec/v1beta2/receivers.md @@ -58,14 +58,34 @@ You can run this example by saving the manifest into `github-receiver.yaml`. kubectl -n flux-system apply -f github-receiver.yaml ``` -3. Run `kubectl -n flux-system get receivers` to see the generated URL: +3. Run `kubectl -n flux-system describe receiver github-receiver` to see its status: + + ```console + ... + Status: + Conditions: + Last Transition Time: 2022-11-16T23:43:38Z + Message: Receiver initialised for path: /hook/bed6d00b5555b1603e1f59b94d7fdbca58089cb5663633fb83f2815dc626d92b + Observed Generation: 1 + Reason: Succeeded + Status: True + Type: Ready + Observed Generation: 1 + Webhook Path: /hook/bed6d00b5555b1603e1f59b94d7fdbca58089cb5663633fb83f2815dc626d92b + Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Succeeded 82s notification-controller Reconciliation finished, next run in 10m + ``` + +4. Run `kubectl -n flux-system get receivers` to see the generated webhook path: ```console NAME READY STATUS github-receiver True Receiver initialised for path: /hook/bed6d00b5555b1603e1f59b94d7fdbca58089cb5663633fb83f2815dc626d92b ``` -4. On GitHub, navigate to your repository and click on the "Add webhook" button under "Settings/Webhooks". +5. On GitHub, navigate to your repository and click on the "Add webhook" button under "Settings/Webhooks". Fill the form with: - **Payload URL**: compose the address using the receiver ingress hostname and the generated path `https:///`. - **Secret**: use the token string @@ -154,7 +174,7 @@ It is therefore a good idea to set rate limits on the ingress resource which exp If you are using ingress-nginx that can be done by [adding annotations](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#rate-limiting). -## Provider guides +## Working with Receivers ### Generic receiver From 04f52885704feabebb665d51ff56db3cd600483b Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 16 Nov 2022 13:53:20 +0200 Subject: [PATCH 21/26] docs: Add alert status specification Signed-off-by: Stefan Prodan --- docs/spec/v1beta2/alerts.md | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/spec/v1beta2/alerts.md b/docs/spec/v1beta2/alerts.md index 5cdec363f..83b9709dd 100644 --- a/docs/spec/v1beta2/alerts.md +++ b/docs/spec/v1beta2/alerts.md @@ -201,3 +201,57 @@ unable to clone 'ssh://git@ssh.dev.azure.com/v3/...', error: SSH could not read `.spec.suspend` is an optional field to suspend the altering. When set to `true`, the controller will stop processing events. When the field is set to `false` or removed, it will resume. + +## Alert Status + +### Conditions + +An Alert enters various states during its lifecycle, reflected as +[Kubernetes Conditions][typical-status-properties]. +It can be [ready](#ready-alert), or it can [fail during +reconciliation](#failed-alert). + +The Alert API is compatible with the [kstatus specification][kstatus-spec], +and reports the `Reconciling` condition where applicable. + +#### Ready Alert + +The notification-controller marks an Alert as _ready_ when it has the following +characteristics: + +- The Alert's Provider referenced in `.spec.providerRef.name` is found on the cluster. +- The Alert's Provider `Ready` status condition is set to `True`. + +When the Alert is "ready", the controller sets a Condition with the following +attributes in the Alert's `.status.conditions`: + +- `type: Ready` +- `status: "True"` +- `reason: Succeeded` + +#### Failed Alert + +The notification-controller may get stuck trying to reconcile an Alert if its Provider +can't be found or if the Provider is not ready. + +When this happens, the controller sets the `Ready` Condition status to `False`, +and adds a Condition with the following attributes: + +- `type: Reconciling` +- `status: "True"` +- `reason: ProgressingWithRetry` + +### Observed Generation + +The notification-controller reports an +[observed generation][typical-status-properties] +in the Alert's `.status.observedGeneration`. The observed generation is the +latest `.metadata.generation` which resulted in a [ready state](#ready-alert). + +### Last Handled Reconcile At + +The notification-controller reports the last `reconcile.fluxcd.io/requestedAt` +annotation value it acted on in the `.status.lastHandledReconcileAt` field. + +[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties +[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus From 78fe519f5d26f31b0151a2cb303bd8df4a2373ba Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 16 Nov 2022 14:15:07 +0200 Subject: [PATCH 22/26] docs: Add commit status updates to providers spec Signed-off-by: Stefan Prodan --- docs/spec/v1beta2/providers.md | 101 ++++++++++++++++++++- docs/spec/v1beta2/statusupdates.md | 138 ----------------------------- 2 files changed, 100 insertions(+), 139 deletions(-) delete mode 100644 docs/spec/v1beta2/statusupdates.md diff --git a/docs/spec/v1beta2/providers.md b/docs/spec/v1beta2/providers.md index 6360fb349..af7c656fd 100644 --- a/docs/spec/v1beta2/providers.md +++ b/docs/spec/v1beta2/providers.md @@ -97,7 +97,7 @@ A Provider also needs a `.spec.type` is a required field that specifies which SaaS API to use. -The supported providers are: +The supported alerting providers are: | Provider | Type | |---------------------------------------------------------|------------------| @@ -119,6 +119,15 @@ The supported providers are: | [Telegram](#telegram) | `telegram` | | [WebEx](#webex) | `webex` | +The supported providers for [Git commit status updates](#git-commit-status-updates) are: + +| Provider | Type | +|-------------------------------|---------------| +| [Azure DevOps](#azure-devops) | `azuredevops` | +| [Bitbucket](#bitbucket) | `bitbucket` | +| [GitHub](#github) | `github` | +| [GitLab](#gitlab) | `gitlab` | + ### Address `.spec.address` is an optional field that specifies the URL where the events are posted. @@ -1048,3 +1057,93 @@ To create the needed secret: kubectl create secret generic azure-webhook \ --from-literal=address="Endpoint=sb://fluxv2.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=yoursaskeygeneatedbyazure" ``` + +### Git Commit Status Updates + +The notification-controller can mark Git commits as reconciled by posting +Flux `Kustomization` events to the origin repository using Git SaaS providers APIs. + +#### Example + +The following is an example of how to update the Git commit status for the GitHub repository where +Flux was bootstrapped with `flux bootstrap github --owner=my-gh-org --repository=my-gh-repo`. + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: github-status + namespace: flux-system +spec: + type: github + address: https://github.com/my-gh-org/my-gh-repo + secretRef: + name: github-token +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Alert +metadata: + name: github-status + namespace: flux-system +spec: + providerRef: + name: github-status + eventSources: + - kind: Kustomization + name: flux-system +``` + +#### GitHub + +When `.spec.type` is set to `github`, the referenced secret must contain a key called `token` with the value set to a +[GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). + +The token must has permissions to update the commit status for the GitHub repository specified in `.spec.address`. + +You can create the secret with `kubectl` like this: + +```shell +kubectl create secret generic github-token --from-literal=token= +``` + +#### GitLab + +When `.spec.type` is set to `gitlab`, the referenced secret must contain a key called `token` with the value set to a +[GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html). + +The token must has permissions to update the commit status for the GitLab repository specified in `.spec.address`. + +You can create the secret with `kubectl` like this: + +```shell +kubectl create secret generic gitlab-token --from-literal=token= +``` + +#### BitBucket + +When `.spec.type` is set to `bitbucket`, the referenced secret must contain a key called `token` with the value +set to a BitBucket username and an +[app password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/#Create-an-app-password) +in the format `:`. + +The app password must have `Repositories (Read/Write)` permission for +the BitBucket repository specified in `.spec.address`. + +You can create the secret with `kubectl` like this: + +```shell +kubectl create secret generic gitlab-token --from-literal=token=: +``` + +#### Azure DevOps + +When `.spec.type` is set to `azuredevops`, the referenced secret must contain a key called `token` with the value set to a +[Azure DevOps personal access token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page). + +The token must has permissions to update the commit status for the Azure DevOps repository specified in `.spec.address`. + +You can create the secret with `kubectl` like this: + +```shell +kubectl create secret generic github-token --from-literal=token= +``` diff --git a/docs/spec/v1beta2/statusupdates.md b/docs/spec/v1beta2/statusupdates.md deleted file mode 100644 index 520c98e1b..000000000 --- a/docs/spec/v1beta2/statusupdates.md +++ /dev/null @@ -1,138 +0,0 @@ -# Git Commit Status Updates - -The notification-controller can mark Git commits as reconciled by posting -Flux `Kustomization` events to the origin repository using Git SaaS providers APIs. - -## Example - -The following is an example of how to update the Git commit status for the GitHub repository where -Flux was bootstrapped with `flux bootstrap github --owner=my-gh-org --repository=my-gh-repo`. - -```yaml -apiVersion: notification.toolkit.fluxcd.io/v1beta2 -kind: Provider -metadata: - name: github-status - namespace: flux-system -spec: - type: github - address: https://github.com/my-gh-org/my-gh-repo - secretRef: - name: github-token ---- -apiVersion: notification.toolkit.fluxcd.io/v1beta2 -kind: Alert -metadata: - name: github-status - namespace: flux-system -spec: - providerRef: - name: github-status - eventSources: - - kind: Kustomization - name: flux-system -``` - -In the above example: - -- A Provider named `github-status` is created, indicated by the - `Provider.metadata.name` field. -- An Alert named `github-status` is created, indicated by the - `Alert.metadata.name` field. -- The Alert references the GitHub provider, indicated by the - `Alert.spec.providerRef` field. -- The notification-controller starts listening for events sent by - the `flux-system` Kustomization. -- When an event is received, the controller extracts the Git commit SHA - from the [event](events.md) payload. -- The controller uses the GitHub PAT from the secret indicated by the - `Provider.spec.secretRef.name` to authenticate with the GitHub API. -- The controller sets the commit status to `kustomization/flux-system/` - followed by the success or error message from the [event](events.md) body. - - -## Writing a Git commit status provider spec - -As with all other Kubernetes config, a Provider needs `apiVersion`, -`kind`, and `metadata` fields. The name of an Alert object must be a -valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). -A Provider also needs a -[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). - -### Type - -`.spec.type` is a required field that specifies which Git SaaS vendor hosts the repository. - -The supported types are: `github`, `gitlab`, `bitbucket` and `azuredevops`. - -### Address - -`.spec.address` is a required field that specifies the HTTPS URL of the Git repository -where the commits originate from. - -### Secret reference - -`.spec.secretRef.name` is a required field to specify a name reference to a -Secret in the same namespace as the Provider, containing the authentication -credentials for the Git SaaS API. - -#### GitHub authentication - -When `.spec.type` is set to `github`, the referenced secret must contain a key called `token` with the value set to a -[GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). - -The token must has permissions to update the commit status for the GitHub repository specified in `.spec.address`. - -You can create the secret with `kubectl` like this: - -```shell -kubectl create secret generic github-token --from-literal=token= -``` - -#### GitLab authentication - -When `.spec.type` is set to `gitlab`, the referenced secret must contain a key called `token` with the value set to a -[GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html). - -The token must has permissions to update the commit status for the GitLab repository specified in `.spec.address`. - -You can create the secret with `kubectl` like this: - -```shell -kubectl create secret generic gitlab-token --from-literal=token= -``` - -#### BitBucket authentication - -When `.spec.type` is set to `bitbucket`, the referenced secret must contain a key called `token` with the value -set to a BitBucket username and an -[app password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/#Create-an-app-password) -in the format `:`. - -The app password must have `Repositories (Read/Write)` permission for -the BitBucket repository specified in `.spec.address`. - -You can create the secret with `kubectl` like this: - -```shell -kubectl create secret generic gitlab-token --from-literal=token=: -``` - -#### Azure DevOps authentication - -When `.spec.type` is set to `azuredevops`, the referenced secret must contain a key called `token` with the value set to a -[Azure DevOps personal access token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page). - -The token must has permissions to update the commit status for the Azure DevOps repository specified in `.spec.address`. - -You can create the secret with `kubectl` like this: - -```shell -kubectl create secret generic github-token --from-literal=token= -``` - -### Suspend - -`.spec.suspend` is an optional field to suspend the Git commit status updates. -When set to `true`, the controller will stop processing events for this provider. -When the field is set to `false` or removed, it will resume the commit status updates. From 20fa1a008c8c4db1126b3edec63942f208bf7f51 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 16 Nov 2022 16:04:44 +0200 Subject: [PATCH 23/26] docs: Add Provider and Receiver status spec Signed-off-by: Stefan Prodan --- docs/spec/v1beta2/providers.md | 86 ++++++++++++++++++++++++++++++++++ docs/spec/v1beta2/receivers.md | 62 +++++++++++++++++++++++- 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/docs/spec/v1beta2/providers.md b/docs/spec/v1beta2/providers.md index af7c656fd..e23699050 100644 --- a/docs/spec/v1beta2/providers.md +++ b/docs/spec/v1beta2/providers.md @@ -1147,3 +1147,89 @@ You can create the secret with `kubectl` like this: ```shell kubectl create secret generic github-token --from-literal=token= ``` + +## Provider Status + +### Conditions + +An Provider enters various states during its lifecycle, reflected as +[Kubernetes Conditions][typical-status-properties]. +It can be [ready](#ready-provider), [stalled](#stalled-provider), or it can [fail during +reconciliation](#failed-provider). + +The Provider API is compatible with the [kstatus specification][kstatus-spec], +and reports `Reconciling` and `Stalled` conditions where applicable to +provide better (timeout) support to solutions polling the Provider to become +`Ready`. + +#### Ready Provider + +The notification-controller marks a Provider as _ready_ when it has the following +characteristics: + +- The Provider's address and proxy are well-formatted URLs. +- The Provider's referenced Secrets are found on the cluster. +- The Provider's referenced Secrets contain valid keys and values. + +When the Provider is "ready", the controller sets a Condition with the following +attributes in the Provider's `.status.conditions`: + +- `type: Ready` +- `status: "True"` +- `reason: Succeeded` + +#### Stalled Provider + +The notification-controller may not be able to reconcile a Provider due to miss-configuration. +This can occur due to some of the following factors: + +- The specified address and/or proxy is not a valid URL. +- The specified proxy is not a valid URL. + +When this happens, the controller sets the `Ready` Condition status to `False`, +and adds a Condition with the following attributes: + +- `type: Stalling` +- `status: "True"` +- `reason: InvalidURL` + +This condition has a ["negative polarity"][typical-status-properties], +and is only present on the Provider while the status value is `"True"`. + +#### Failed Provider + +The notification-controller may get stuck trying to reconcile a Provider. +This can occur due to some of the following factors: + +- The [Secret reference](#secret-reference) contains a reference to a + non-existing Secret. +- The credentials in the referenced Secret are invalid. +- The [TLS Secret reference](#tls-certificates) contains a reference to a + non-existing Secret. +- The TLS certs in the referenced Secret are invalid. + +When this happens, the controller sets the `Ready` Condition status to `False`, +and adds a Condition with the following attributes: + +- `type: Reconciling` +- `status: "True"` +- `reason: ProgressingWithRetry` + +While the Provider has this Condition, the controller will continue to attempt +to reconcile it with an exponential backoff, until +it succeeds and the Provider is marked as [ready](#ready-provider). + +### Observed Generation + +The notification-controller reports an +[observed generation][typical-status-properties] +in the Provider's `.status.observedGeneration`. The observed generation is the +latest `.metadata.generation` which resulted in a [ready state](#ready-provider). + +### Last Handled Reconcile At + +The notification-controller reports the last `reconcile.fluxcd.io/requestedAt` +annotation value it acted on in the `.status.lastHandledReconcileAt` field. + +[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties +[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus diff --git a/docs/spec/v1beta2/receivers.md b/docs/spec/v1beta2/receivers.md index 2a84a509b..5faff6b97 100644 --- a/docs/spec/v1beta2/receivers.md +++ b/docs/spec/v1beta2/receivers.md @@ -131,7 +131,7 @@ should be reconciled when an event is received. A resource entry must contain the following fields: - `apiVersion` is the Flux Custom Resource API group and version such as `source.toolkit.fluxcd.io/v1beta2`. -- `kind` is the Flux Custom Resource `.kind` such as GitRepository, OCIRepository, HelmRepository, etc. +- `kind` is the Flux Custom Resource `.kind` such as GitRepository, OCIRepository, HelmRepository and Bucket. - `name` is the Flux Custom Resource `.metadata.name`. - `namespace` is the Flux Custom Resource `.metadata.namespace`. When not specified, the Receiver `.metadata.namespace` is used instead. @@ -471,3 +471,63 @@ spec: Note that the controller doesn't verify the authenticity of the request as Azure does not provide any mechanism for verification. You can take a look at the [Azure Container webhook reference](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-webhook-reference). + +## Receiver Status + +### Conditions + +An Receiver enters various states during its lifecycle, reflected as +[Kubernetes Conditions][typical-status-properties]. +It can be [ready](#ready-receiver), or it can [fail during +reconciliation](#failed-receiver). + +The Receiver API is compatible with the [kstatus specification][kstatus-spec], +and reports the `Reconciling` condition where applicable. + +#### Ready Receiver + +The notification-controller marks a Receiver as _ready_ when it has the following +characteristics: + +- The Receiver's Secret referenced in `.spec.secretRef.name` is found on the cluster. +- The Receiver's Secret contains a `token` key. + +When the Receiver is "ready", the controller sets a Condition with the following +attributes in the Alert's `.status.conditions`: + +- `type: Ready` +- `status: "True"` +- `reason: Succeeded` + +#### Failed Receiver + +The notification-controller may get stuck trying to reconcile a Receiver if its secret token +can't be found. + +When this happens, the controller sets the `Ready` Condition status to `False`, +and adds a Condition with the following attributes: + +- `type: Reconciling` +- `status: "True"` +- `reason: ProgressingWithRetry` + +### Observed Generation + +The notification-controller reports an +[observed generation][typical-status-properties] +in the Receiver's `.status.observedGeneration`. The observed generation is the +latest `.metadata.generation` which resulted in a [ready state](#ready-receiver). + +### Last Handled Reconcile At + +The notification-controller reports the last `reconcile.fluxcd.io/requestedAt` +annotation value it acted on in the `.status.lastHandledReconcileAt` field. + +### Webhook Path + +When a Receiver becomes [ready](#ready-receiver), the controller reports the generated +incoming webhook path under `.status.webhookPath`. +The path format is `/hook/sha256sum(token+name+namespace)`. + +[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties +[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus From ef94247cfeaa9c1b1b4f4ed87dfb8bdebf66fb73 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 18 Nov 2022 19:36:23 +0000 Subject: [PATCH 24/26] docs: improve Receiver spec - Extensively document the various Receiver types, their validation mechanics and their caveats. - Move various things around a bit to ensure we follow the same flow as the rewritten source-controller specs. - Explicitly document the expected format of the Secret. - Various other small rewordings and fixes. Signed-off-by: Hidde Beydals --- docs/spec/v1beta2/receivers.md | 717 +++++++++++++++++++++++++-------- 1 file changed, 549 insertions(+), 168 deletions(-) diff --git a/docs/spec/v1beta2/receivers.md b/docs/spec/v1beta2/receivers.md index 5faff6b97..026c141d3 100644 --- a/docs/spec/v1beta2/receivers.md +++ b/docs/spec/v1beta2/receivers.md @@ -1,13 +1,18 @@ # Incoming Webhook Receivers -The `Receiver` API defines an incoming webhook receiver that triggers -the reconciliation for a group of Flux Custom Resources. +The `Receiver` API defines an incoming webhook receiver that triggers the +reconciliation for a group of Flux Custom Resources. ## Example -The following is an example of how to configure an incoming webhook for the GitHub repository where -Flux was bootstrapped with `flux bootstrap github`. After a Git push, GitHub will send a push event to -notification-controller, which in turn tells Flux to pull and apply the latest changes from upstream. +The following is an example of how to configure an incoming webhook for the +GitHub repository where Flux was bootstrapped with `flux bootstrap github`. +After a Git push, GitHub will send a push event to notification-controller, +which in turn tells Flux to pull and apply the latest changes from upstream. + +**Note:** The following assumes an Ingress exposes the controller's +`webhook-receiver` Kubernetes Service. How to configure the Ingress is out of +scope for this example. ```yaml --- @@ -33,17 +38,20 @@ In the above example: - A Receiver named `github-receiver` is created, indicated by the `.metadata.name` field. -- The notification-controller generates a unique webhook path using the Receiver - name, namespace and the token from the referenced `.spec.secretRef.name` secret. +- The notification-controller generates a unique webhook path using the + Receiver name, namespace and the token from the referenced + `.spec.secretRef.name` secret. - The incoming webhook path is reported in the `.status.webhookPath` field. -- When a GitHub push event is received, the controller verifies the that the - request is legitimate using HMAC and the `X-Hub-Signature` HTTP header. -- If the event type matches `.spec.events` and the payload is verified, then the controller - triggers a reconciliation for the `flux-system` GitRepository which is listed under `.spec.resouces`. +- When a GitHub push event is received, the controller verifies the payload's + integrity and authenticity, using [HMAC][] and the `X-Hub-Signature` HTTP + header. +- If the event type matches `.spec.events` and the payload is verified, then + the controller triggers a reconciliation for the `flux-system` GitRepository + which is listed under `.spec.resources`. You can run this example by saving the manifest into `github-receiver.yaml`. -1. Generate a random string and create a secret with a `token` field: +1. Generate a random string and create a Secret with a `token` field: ```sh TOKEN=$(head -c 12 /dev/urandom | shasum | cut -d ' ' -f1) @@ -85,100 +93,59 @@ You can run this example by saving the manifest into `github-receiver.yaml`. github-receiver True Receiver initialised for path: /hook/bed6d00b5555b1603e1f59b94d7fdbca58089cb5663633fb83f2815dc626d92b ``` -5. On GitHub, navigate to your repository and click on the "Add webhook" button under "Settings/Webhooks". - Fill the form with: - - **Payload URL**: compose the address using the receiver ingress hostname and the generated path `https:///`. - - **Secret**: use the token string +5. On GitHub, navigate to your repository and click on the "Add webhook" button + under "Settings/Webhooks". Fill the form with: + + - **Payload URL**: The composed address, consisting of the Ingress' hostname + exposing the controller's `webhook-receiver` Kubernetes Service, and the + generated path for the Receiver. For this example: + `https:///hook/bed6d00b5555b1603e1f59b94d7fdbca58089cb5663633fb83f2815dc626d92b` + - **Secret**: The `token` string generated in step 1. ## Writing a Receiver spec As with all other Kubernetes config, a Receiver needs `apiVersion`, `kind`, and `metadata` fields. The name of a Receiver object must be a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). + A Receiver also needs a [`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). ### Type -`.spec.type` is a required field that specifies which SaaS API to use. - -The supported receiver types are: - -| Receiver | Type | -|-----------------------------------------------------|----------------| -| [Generic webhook](#generic-receiver) | `generic` | -| [Generic webhook with HMAC](#generic-hmac-receiver) | `generic-hmac` | -| [GitHub](#github-receiver) | `github` | -| [Gitea](#github-receiver) | `github` | -| [GitLab](#gitlab-receiver) | `gitlab` | -| [Bitbucket server](#bitbucket-server-receiver) | `bitbucket` | -| [Harbor](#harbor-receiver) | `harbor` | -| [DockerHub](#dockerhub-receiver) | `dockerhub` | -| [Quay](#quay-receiver) | `quay` | -| [Nexus](#nexus-receiver) | `nexus` | -| [Azure Container Registry](#acr-receiver) | `acr` | -| [Google Container Registry](#gcr-receiver) | `gcr` | - -### Events filtering - -`.spec.events` in an optional field to specify a list of event types -that this Receiver should handle. If left empty, all events are handled. - -### Resources - -`.spec.resources` is a required field to specify which Flux Custom Resources -should be reconciled when an event is received. - -A resource entry must contain the following fields: -- `apiVersion` is the Flux Custom Resource API group and version such as `source.toolkit.fluxcd.io/v1beta2`. -- `kind` is the Flux Custom Resource `.kind` such as GitRepository, OCIRepository, HelmRepository and Bucket. -- `name` is the Flux Custom Resource `.metadata.name`. -- `namespace` is the Flux Custom Resource `.metadata.namespace`. - When not specified, the Receiver `.metadata.namespace` is used instead. - -#### Disable cross-namespace selectors +`.spec.type` is a required field that specifies how the controller should +handle the incoming webhook request. -**Note:** On multi-tenant clusters, platform admins can disable cross-namespace references with the -`--no-cross-namespace-refs=true` flag. When this flag is set, Receivers can only refer to resources -in the same namespace as the alert object, preventing tenants from triggering reconciliations -to another tenant's resources. - -### Secret reference - -`.spec.secretRef.name` is a required field to specify a name reference to a -Secret in the same namespace as the Receiver, containing the secret token. - -### Interval - -`.spec.interval` is a required field with a default of ten minutes that specifies -the time interval at which the controller reconciles the provider with its Secret -references. - -### Suspend - -`.spec.suspend` is an optional field to suspend the receiver. -When set to `true`, the controller will stop processing events for this receiver. -When the field is set to `false` or removed, it will resume. +#### Supported Receiver types -## Public ingress considerations +| Receiver | Type | Supports filtering using [Events](#events) | +|--------------------------------------------|----------------|--------------------------------------------| +| [Generic webhook](#generic) | `generic` | ❌ | +| [Generic webhook with HMAC](#generic-hmac) | `generic-hmac` | ❌ | +| [GitHub](#github) | `github` | ✅ | +| [Gitea](#github) | `github` | ✅ | +| [GitLab](#gitlab) | `gitlab` | ✅ | +| [Bitbucket server](#bitbucket-server) | `bitbucket` | ✅ | +| [Harbor](#harbor) | `harbor` | ❌ | +| [DockerHub](#dockerhub) | `dockerhub` | ❌ | +| [Quay](#quay) | `quay` | ❌ | +| [Nexus](#nexus) | `nexus` | ❌ | +| [Azure Container Registry](#acr) | `acr` | ❌ | +| [Google Container Registry](#gcr) | `gcr` | ❌ | -Considerations should be made when exposing the controller's `webhook-receiver` Kubernetes Service -to the public internet. Each request to the receiver endpoint will result in request to the Kubernetes -API as the controller needs to fetch information about the receiver. The receiver endpoint may be -protected with a token, but it does not defend against a situation where a legitimate webhook source -starts sending large amounts of requests, or the token is somehow leaked. -This may result in unwanted consequences for the controller, -as it may get rate limited by the Kubernetes API, degrading its functionality. +#### Generic -It is therefore a good idea to set rate limits on the ingress resource which exposes the receiver. -If you are using ingress-nginx that can be done by -[adding annotations](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#rate-limiting). +When a Receiver's `.spec.type` is set to `generic`, the controller will respond +to any HTTP request to the generated [`.status.webhookURL` path](#webhook-path), +and request a reconciliation for all listed [Resources](#resources). -## Working with Receivers +**Note:** This type of Receiver does not perform any validation on the incoming +request, and it does not support filtering using [Events](#events). -### Generic receiver +##### Generic example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -193,25 +160,34 @@ spec: kind: GitRepository name: webapp namespace: default - - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - name: webapp - namespace: default - - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: Bucket - name: webapp - namespace: default - - apiVersion: image.toolkit.fluxcd.io/v1beta1 - kind: ImageRepository - name: webapp - namespace: default ``` -When the receiver type is set to `generic`, the controller will not perform token validation nor event filtering. +#### Generic HMAC -### Generic HMAC receiver +When a Receiver's `.spec.type` is set to `generic-hmac`, the controller will +respond to any HTTP request to the generated [`.status.webhookURL` path](#webhook-path), +while verifying the request's payload integrity and authenticity using [HMAC][]. + +The controller uses the `X-Signature` header to get the hash signature. This +signature should be prefixed with the hash function (`sha1`, `sha256` or +`sha512`) used to generate the signature, in the following format: +`=`. + +To validate the HMAC signature, the controller will use the `token` string +from the [Secret reference](#secret-reference) to generate a hash signature +using the same hash function as the one specified in the `X-Signature` header. + +If the generated hash signature matches the one specified in the `X-Signature` +header, the controller will request a reconciliation for all listed +[Resources](#resources). + +**Note:** This type of Receiver does not support filtering using +[Events](#events). + +##### Generic HMAC example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -228,41 +204,50 @@ spec: namespace: default ``` -This generic receiver verifies that the request is legitimate using HMAC. -The controller uses the `X-Signature` header to get the hash signature. -The signature should be prefixed with the hash function(`sha1`, `sha256`, or `sha512`) like this: -`=`. +##### HMAC signature generation example -1. Generate hash signature using OpenSSL: +1. Generate the HMAC hash for the request body using OpenSSL: -```sh -printf '' | openssl dgst -sha1 -r -hmac "" | awk '{print $1}' -``` + ```sh + printf '' | openssl dgst -sha1 -r -hmac "" | awk '{print $1}' + ``` -You can use the flag `sha256` or `sha512` if you want a different hash function. + You can replace the `-sha1` flag with `-sha256` or `-sha512` to use a + different hash function. -2. Send a HTTP POST request to the webhook URL: +2. Send an HTTP POST request with the body and the HMAC hash to the webhook URL: -```sh -curl -X POST -H "X-Signature: sha1=" -d '' -``` + ```sh + curl -X POST -H "X-Signature: =" -d '' + ``` -Generate hash signature using Go: +#### GitHub -```go -func sign(payload, key string) string { - h := hmac.New(sha1.New, []byte(key)) - h.Write([]byte(payload)) - return fmt.Sprintf("%x", h.Sum(nil)) -} +When a Receiver's `.spec.type` is set to `github`, the controller will respond +to an [HTTP webhook event payload](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads) +from GitHub to the generated [`.status.webhookURL` path](#webhook-path), +while verifying the payload using [HMAC][]. -// set headers -req.Header.Set("X-Signature", fmt.Sprintf("sha1=%s", sign(payload, key))) -``` +The controller uses the [`X-Hub-Signature` header](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#delivery-headers) +from the request made by GitHub to get the hash signature. To enable the +inclusion of this header, the `token` string from the [Secret reference](#secret-reference) +must be configured as the [secret token for the +webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks#setting-your-secret-token). + +The controller will calculate the HMAC hash signature for the received request +payload using the same `token` string, and compare it with the one specified in +the header. If the two signatures match, the controller will request a +reconciliation for all listed [Resources](#resources). + +This type of Receiver offers the ability to filter incoming events by comparing +the `X-GitHub-Event` header to the list of [Events](#events). +For a list of available events, see the [GitHub +documentation](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads). -### GitHub receiver +##### GitHub example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -279,22 +264,49 @@ spec: - apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: GitRepository name: webapp - - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - name: webapp ``` -Note that you have to set the generated token as the GitHub webhook secret value. -The controller uses the `X-Hub-Signature` HTTP header to verify that the request is legitimate. +The above example makes use of the [`.spec.events` field](#events) to filter +incoming events from GitHub, instructing the controller to only respond to +[`ping`](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#ping) +and [`push`](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push) +events. + +#### Gitea + +For Gitea, the `.spec.type` field can be set to `github` as it produces [GitHub +type](#github) compatible [webhook event payloads](https://docs.gitea.io/en-us/webhooks/). + +**Note:** While the payloads are compatible with the GitHub type, the number of +available events may be limited and/or different from the ones available in +GitHub. Refer to the [Gitea source code](https://github.com/go-gitea/gitea/blob/main/models/webhook/hooktask.go#L28) +to see the list of available [events](#events). + +#### GitLab + +When a Receiver's `.spec.type` is set to `gitlab`, the controller will respond +to an [HTTP webhook event payload](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#events) +from GitLab to the generated [`.status.webhookURL` path](#webhook-path). + +The controller validates the payload's authenticity by comparing the +[`X-Gitlab-Token` header](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#validate-payloads-by-using-a-secret-token) +from the request made by GitLab to the `token` string from the [Secret +reference](#secret-reference). To enable the inclusion of this header, the +`token` string must be configured as the "Secret token" while [configuring a +webhook in GitLab](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#configure-a-webhook-in-gitlab). -### Gitea receiver +If the two tokens match, the controller will request a reconciliation for all +listed [Resources](#resources). -The Gitea webhook works with the [GitHub receiver](#github-receiver). You can use the same example -given for the Github receiver. +This type of Receiver offers the ability to filter incoming events by comparing +the `X-Gitlab-Event` header to the list of [Events](#events). For a list of +available webhook types, refer to the [GitLab +documentation](https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html). -### GitLab receiver +##### GitLab example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -316,12 +328,45 @@ spec: name: webapp-backend ``` -Note that you have to configure the GitLab webhook with the generated token. -The controller uses the `X-Gitlab-Token` HTTP header to verify that the request is legitimate. +The above example makes use of the [`.spec.events` field](#events) to filter +incoming events from GitLab, instructing the controller to only respond to +[`Push Hook`](https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#push-events) +and [`Tag Push Hook`](https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#tag-events) +events. + +#### Bitbucket Server + +When a Receiver's `.spec.type` is set to `bitbucket`, the controller will +respond to an [HTTP webhook event payload](https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html) +from Bitbucket Server to the generated [`.status.webhookURL` path](#webhook-path), +while verifying the payload's integrity and authenticity using [HMAC][]. + +The controller uses the [`X-Hub-Signature` header](https://confluence.atlassian.com/bitbucketserver/manage-webhooks-938025878.html#Managewebhooks-webhooksecrets) +from the request made by BitBucket Server to get the hash signature. To enable +the inclusion of this header, the `token` string from the [Secret +reference](#secret-reference) must be configured as the "Secret" while creating +a webhook in Bitbucket Server. + +The controller will calculate the HMAC hash signature for the received request +payload using the same `token` string, and compare it with the one specified in +the header. If the two signatures match, the controller will request a +reconciliation for all listed [Resources](#resources). + +This type of Receiver offers the ability to filter incoming events by comparing +the `X-Event-Key` header to the list of [Events](#events). For a list of +available event keys, refer to the [Bitbucket Server +documentation](https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html#Eventpayload-Repositoryevents). + +**Note:** Bitbucket Cloud does not support signing webhook requests +([BCLOUD-14683](https://jira.atlassian.com/browse/BCLOUD-14683), +[BCLOUD-12195](https://jira.atlassian.com/browse/BCLOUD-12195)). If your +repositories are on Bitbucket Cloud, you will need to use a [Generic +Receiver](#generic) instead. -### Bitbucket server receiver +##### Bitbucket Server example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -339,16 +384,35 @@ spec: name: webapp ``` -Note that you have to set the generated token as the Bitbucket server webhook secret value. -The controller uses the `X-Hub-Signature` HTTP header to verify that the request is legitimate. +The above example makes use of the [`.spec.events` field](#events) to filter +incoming events from Bitbucket Server, instructing the controller to only +respond to [`repo:refs_changed` (Push)](https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html#Eventpayload-Push) +events. + +#### Harbor + +When a Receiver's `.spec.type` is set to `harbor`, the controller will respond +to an [HTTP webhook event payload](https://goharbor.io/docs/latest/working-with-projects/project-configuration/configure-webhooks/#payload-format) +from Harbor to the generated [`.status.webhookURL` path](#webhook-path). -Also note, the *Bitbucket cloud* service does not yet provide any support for signing webhook requests. -([1](https://jira.atlassian.com/browse/BCLOUD-14683), [2](https://jira.atlassian.com/browse/BCLOUD-12195)). -If your repositories are on Bitbucket cloud, you will need to use the Generic receiver instead. +The controller validates the payload's authenticity by comparing the +`Authorization` header from the request made by Harbor to the `token` string +from the [Secret reference](#secret-reference). To enable the inclusion of this +header, the `token` string must be configured as the "Auth Header" while +[configuring a webhook in +Harbor](https://goharbor.io/docs/latest/working-with-projects/project-configuration/configure-webhooks/#configure-webhooks). -### Harbor receiver +If the two tokens match, the controller will request a reconciliation for all +listed [Resources](#resources). + +**Note:** This type of Receiver does not support filtering using +[Events](#events). However, Harbor does support configuring event types for +which a webhook will be triggered. + +##### Harbor example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -359,20 +423,29 @@ spec: secretRef: name: webhook-token resources: - - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - name: webapp - apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImageRepository name: webapp ``` -Note that you have to set the generated token as the Harbor webhook authentication header. -The controller uses the `Authentication` HTTP header to verify that the request is legitimate. +#### DockerHub + +When a Receiver's `.spec.type` is set to `dockerhub`, the controller will +respond to an [HTTP webhook event payload](https://docs.docker.com/docker-hub/webhooks/) +from DockerHub to the generated [`.status.webhookURL` path](#webhook-path). + +The controller performs minimal validation of the payload by attempting to +unmarshal the [JSON request body](https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload). +If the unmarshalling is successful, the controller will request a reconciliation +for all listed [Resources](#resources). -### DockerHub receiver +**Note:** This type of Receiver does not support filtering using +[Events](#events). + +##### DockerHub example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -388,9 +461,25 @@ spec: name: webapp ``` -### Quay receiver +#### Quay + +When a Receiver's `.spec.type` is set to `quay`, the controller will respond to +an HTTP [Repository Push Notification payload](https://docs.quay.io/guides/notifications.html#repository-push) +from Quay to the generated [`.status.webhookURL` path](#webhook-path). + +The controller performs minimal validation of the payload by attempting to +unmarshal the JSON request body to the expected format. If the unmarshalling is +successful, the controller will request a reconciliation for all listed +[Resources](#resources). + +**Note:** This type of Receiver does not support filtering using +[Events](#events). In addition, it does not support any "Repository +Notification" other than "Repository Push". + +##### Quay example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -406,9 +495,35 @@ spec: name: webapp ``` -### Nexus receiver +#### Nexus + +When a Receiver's `.spec.type` is set to `nexus`, the controller will respond +to an [HTTP webhook event payload](https://help.sonatype.com/repomanager3/integrations/webhooks/example-headers-and-payloads) +from Nexus Repository Manager 3 to the generated [`.status.webhookURL` +path](#webhook-path), while verifying the payload's integrity and +authenticity using [HMAC][]. + +The controller validates the payload by comparing the +[`X-Nexus-Webhook-Signature` header](https://help.sonatype.com/repomanager3/integrations/webhooks/working-with-hmac-payloads) +from the request made by Nexus to the `token` string from the [Secret +reference](#secret-reference). To enable the inclusion of this header, the +`token` string must be configured as the "Secret Key" while [enabling a +repository webhook capability](https://help.sonatype.com/repomanager3/integrations/webhooks/enabling-a-repository-webhook-capability). + +The controller will calculate the HMAC hash signature for the received request +payload using the same `token` string, and compare it with the one specified in +the header. If the two signatures match, the controller will attempt to +unmarshal the request body to the expected format. If the unmarshalling is +successful, the controller will request a reconciliation for all listed +[Resources](#resources). + +**Note:** This type of Receiver does not support filtering using +[Events](#events). + +##### Nexus example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -424,13 +539,30 @@ spec: name: webapp ``` -Note that you have to fill in the generated token as the secret key when creating the Nexus Webhook Capability. -See [Nexus Webhook Capability](https://help.sonatype.com/repomanager3/webhooks/enabling-a-repository-webhook-capability) -The controller uses the `X-Nexus-Webhook-Signature` HTTP header to verify that the request is legitimate. +#### GCR + +When a Receiver's `.spec.type` is set to `gcr`, the controller will respond to +an [HTTP webhook event payload](https://cloud.google.com/container-registry/docs/configuring-notifications#notification_examples) +from Google Cloud Registry to the generated [`.status.webhookURL`](#webhook-path), +while verifying the payload is legitimate using [JWT](https://cloud.google.com/pubsub/docs/push#authentication). + +The controller verifies the request originates from Google by validating the +token from the [`Authorization` header](https://cloud.google.com/pubsub/docs/push#validate_tokens). +For this to work, authentication must be enabled for the Pub/Sub subscription, +refer to the [Google Cloud documentation](https://cloud.google.com/pubsub/docs/push#configure_for_push_authentication) +for more information. -### GCR receiver +When the verification succeeds, the request payload is unmarshalled to the +expected format. If this is successful, the controller will request a +reconciliation for all listed [Resources](#resources). + +**Note:** This type of Receiver does not support filtering using +[Events](#events). + +##### GCR example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -447,14 +579,24 @@ spec: namespace: default ``` -Note that the controller decodes the JWT from the authorization -header of the push request and verifies it against the GCP API. -For more information, take a look at this -[documentation](https://cloud.google.com/pubsub/docs/push?&_ga=2.123897930.-1945316571.1602156486#authentication_and_authorization). +#### ACR + +When a Receiver's `.spec.type` is set to `acr`, the controller will respond to +an [HTTP webhook event payload](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-webhook-reference), +from Azure Container Registry to the generated [`.status.webhookURL`](#webhook-path). + +The controller performs minimal validation of the payload by attempting to +unmarshal the JSON request body. If the unmarshalling is successful, the +controller will request a reconciliation for all listed [Resources](#resources). + +**Note:** This type of Receiver does not support filtering using +[Events](#events). However, Azure Container Registry does [support configuring +webhooks to only send events for specific actions](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-webhook#create-webhook---azure-portal). -### ACR receiver +##### ACR example ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Receiver metadata: @@ -469,14 +611,252 @@ spec: name: webapp ``` -Note that the controller doesn't verify the authenticity of the request as Azure does not provide any mechanism for verification. -You can take a look at the [Azure Container webhook reference](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-webhook-reference). +### Events + +`.spec.events` in an optional field to specify a list of webhook payload event +types this Receiver should act on. If left empty, no filtering is applied and +any (valid) payload is handled. + +**Note:** Support for this field, and the entries in it, is dependent on the +Receiver type. See the [supported Receiver types](#supported-receiver-types) +section for more information. + +### Resources + +`.spec.resources` is a required field to specify which Flux Custom Resources +should be reconciled when the Receiver's [webhook path](#webhook-path) is +called. + +A resource entry contains the following fields: + +- `apiVersion`: The Flux Custom Resource API group and version, such as + `source.toolkit.fluxcd.io/v1beta2`. +- `kind`: The Flux Custom Resource kind, supported values are `Bucket`, + `GitRepository`, `Kustomization`, `HelmRelease`, `HelmChart`, + `HelmRepository`, `ImageRepository`, `ImagePolicy`, `ImageUpdateAutomation` + and `OCIRepository`. +- `name`: The Flux Custom Resource `.metadata.name`. This field may contain + a wildcard `*` to match all resources of the `kind`. +- `namespace` (Optional): The Flux Custom Resource `.metadata.namespace`. + When not specified, the Receiver's `.metadata.namespace` is used instead. + + +**Note:** Cross-namespace references [can be disabled for security +reasons](#disabling-cross-namespace-selectors). + +### Secret reference + +`.spec.secretRef.name` is a required field to specify a name reference to a +Secret in the same namespace as the Receiver. The Secret must contain a `token` +key, whose value is a string containing a (random) secret token. + +This token is used to salt the generated [webhook path](#webhook-path), and +depending on the Receiver [type](#supported-receiver-types), to verify the +authenticity of a request. + +#### Secret example + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: webhook-token + namespace: default +type: Opaque +stringData: + token: +```` + +### Interval + +`.spec.interval` is a required field with a default of ten minutes that specifies +the time interval at which the controller reconciles the provider with its Secret +reference. + +### Suspend + +`.spec.suspend` is an optional field to suspend the Receiver. +When set to `true`, the controller will stop processing events for this Receiver. +When the field is set to `false` or removed, it will resume. + +## Working with Receivers + +### Disabling cross-namespace selectors + +On multi-tenant clusters, platform admins can disable cross-namespace +references with the `--no-cross-namespace-refs=true` flag. When this flag is +set, Receivers can only refer to [Resources](#resources) in the same namespace +as the [Alert](alerts.md) object, preventing tenants from triggering +reconciliations to another tenant's resources. + +### Public Ingress considerations + +Considerations should be made when exposing the controller's `webhook-receiver` +Kubernetes Service to the public internet. Each request to a Receiver [webhook +path](#webhook-path) will result in request to the Kubernetes API, as the +controller needs to fetch information about the resource. This endpoint may be +protected with a token, but this does not defend against a situation where a +legitimate webhook caller starts sending large amounts of requests, or the +token is somehow leaked. This may result in unwanted consequences for the +controller, as it may get rate limited by the Kubernetes API, degrading its +functionality. + +It is therefore a good idea to set rate limits on the Ingress which exposes +the Kubernetes Service. If you are using ingress-nginx, this can be done by +[adding annotations](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#rate-limiting). + +### Triggering a reconcile + +To manually tell the notification-controller to reconcile a Receiver outside +of the [specified interval window](#interval), a Receiver can be annotated with +`reconcile.fluxcd.io/requestedAt: `. Annotating the resource +queues the Receiver for reconciliation if the `` differs from +the last value the controller acted on, as reported in +[`.status.lastHandledReconcileAt`](#last-handled-reconcile-at). + +Using `kubectl`: + +```sh +kubectl annotate --field-manager=flux-client-side-apply --overwrite receiver/ reconcile.fluxcd.io/requestedAt="$(date +%s)" +``` + +Using `flux`: + +```sh +flux reconcile source receiver +``` + +### Waiting for `Ready` + +When a change is applied, it is possible to wait for the Receiver to reach a +[ready state](#ready-receiver) using `kubectl`: + +```sh +kubectl wait receiver/ --for=condition=ready --timeout=1m +``` + +### Suspending and resuming + +When you find yourself in a situation where you temporarily want to pause the +reconciliation of a Receiver and the handling of requests, you can suspend it +using the [`.spec.suspend` field](#suspend). + +#### Suspend a Receiver + +In your YAML declaration: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: +spec: + suspend: true +``` + +Using `kubectl`: + +```sh +kubectl patch receiver --field-manager=flux-client-side-apply -p '{\"spec\": {\"suspend\" : true }}' +``` + +Using `flux`: + +```sh +flux suspend receiver +``` + +#### Resume a Receiver + +In your YAML declaration, comment out (or remove) the field: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Receiver +metadata: + name: +spec: + # suspend: true +``` + +**Note:** Setting the field value to `false` has the same effect as removing +it, but does not allow for "hot patching" using e.g. `kubectl` while practicing +GitOps; as the manually applied patch would be overwritten by the declared +state in Git. + +Using `kubectl`: + +```sh +kubectl patch receiver --field-manager=flux-client-side-apply -p '{\"spec\" : {\"suspend\" : false }}' +``` + +Using `flux`: + +```sh +flux resume receiver +``` + +### Debugging a Receiver + +There are several ways to gather information about a Receiver for debugging +purposes. + +#### Describe the Receiver + +Describing a Receiver using `kubectl describe receiver ` displays +the latest recorded information for the resource in the Status and Events +sections: + +```console +... +Status: +... +Status: + Conditions: + Last Transition Time: 2022-11-21T12:41:48Z + Message: Reconciliation in progress + Observed Generation: 1 + Reason: ProgressingWithRetry + Status: True + Type: Reconciling + Last Transition Time: 2022-11-21T12:41:48Z + Message: unable to read token from secret 'default/webhook-token' error: Secret "webhook-token" not found + Observed Generation: 1 + Reason: TokenNotFound + Status: False + Type: Ready + Observed Generation: -1 +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Warning Failed 5s (x4 over 16s) notification-controller unable to read token from secret 'default/webhook-token' error: Secret "webhook-token" not found +``` + +#### Trace emitted Events + +To view events for specific Receiver(s), `kubectl get events` can be used in +combination with `--field-sector` to list the Events for specific objects. +For example, running + +```sh +kubectl get events --field-selector involvedObject.kind=Receiver,involvedObject.name= +``` + +lists + +```console +LAST SEEN TYPE REASON OBJECT MESSAGE +3m44s Warning Failed receiver/ unable to read token from secret 'default/webhook-token' error: Secret "webhook-token" not found +``` ## Receiver Status ### Conditions -An Receiver enters various states during its lifecycle, reflected as +A Receiver enters various states during its lifecycle, reflected as [Kubernetes Conditions][typical-status-properties]. It can be [ready](#ready-receiver), or it can [fail during reconciliation](#failed-receiver). @@ -501,8 +881,8 @@ attributes in the Alert's `.status.conditions`: #### Failed Receiver -The notification-controller may get stuck trying to reconcile a Receiver if its secret token -can't be found. +The notification-controller may get stuck trying to reconcile a Receiver if its +secret token can not be found. When this happens, the controller sets the `Ready` Condition status to `False`, and adds a Condition with the following attributes: @@ -525,9 +905,10 @@ annotation value it acted on in the `.status.lastHandledReconcileAt` field. ### Webhook Path -When a Receiver becomes [ready](#ready-receiver), the controller reports the generated -incoming webhook path under `.status.webhookPath`. -The path format is `/hook/sha256sum(token+name+namespace)`. +When a Receiver becomes [ready](#ready-receiver), the controller reports the +generated incoming webhook path under `.status.webhookPath`. The path format is +`/hook/sha256sum(token+name+namespace)`. [typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties [kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus +[HMAC]: https://en.wikipedia.org/wiki/HMAC From b0c02d7c327d877752ebaade093cf61b02a995d0 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Mon, 21 Nov 2022 12:40:32 +0000 Subject: [PATCH 25/26] config: ensure event create/patch is registered Signed-off-by: Hidde Beydals --- config/rbac/role.yaml | 7 +++++++ controllers/alert_controller.go | 1 + controllers/provider_controller.go | 1 + controllers/receiver_controller.go | 1 + 4 files changed, 10 insertions(+) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 29a8b7077..16025e8ee 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -5,6 +5,13 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - "" resources: diff --git a/controllers/alert_controller.go b/controllers/alert_controller.go index 5e3dd6259..1f2714e85 100644 --- a/controllers/alert_controller.go +++ b/controllers/alert_controller.go @@ -98,6 +98,7 @@ func (r *AlertReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts Aler // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=alerts,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=alerts/status,verbs=get;update;patch +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *AlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { reconcileStart := time.Now() diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index 4db44bff6..42f12bc2f 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -81,6 +81,7 @@ func (r *ProviderReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts P // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=providers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=providers/status,verbs=get;update;patch // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { reconcileStart := time.Now() diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index 4253de50b..c403560e5 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -86,6 +86,7 @@ func (r *ReceiverReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts R // +kubebuilder:rbac:groups=source.fluxcd.io,resources=helmrepositories/status,verbs=get // +kubebuilder:rbac:groups=image.fluxcd.io,resources=imagerepositories,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=image.fluxcd.io,resources=imagerepositories/status,verbs=get +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { From 16012cefa26760e3da00bf9798fa73a495dc3cfa Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 25 Nov 2022 17:01:33 +0000 Subject: [PATCH 26/26] docs: Rewrite portion of Provider spec Plus addressing of a couple of nits. The following must still be picked up at a later moment: - More sections need to be moved from "working with" to "writing a ..." - Documentation flow can likely be improved once the sections have been moved. - Technical description of the behavior of the Provider types could be improved, this should be easier to do when everything has the same format. Signed-off-by: Hidde Beydals --- docs/spec/README.md | 2 +- docs/spec/v1beta2/providers.md | 876 ++++++++++++++++++++------------- docs/spec/v1beta2/receivers.md | 29 +- 3 files changed, 539 insertions(+), 368 deletions(-) diff --git a/docs/spec/README.md b/docs/spec/README.md index 5f1903063..0b8c0cb98 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -36,7 +36,7 @@ Notification API: * [Alerts](v1beta2/alerts.md) * [Providers](v1beta2/providers.md) -* [Git Commit Status Updates](v1beta2/statusupdates.md) +* [Events](v1beta2/events.md) The alert delivery method is **at-most once** with a timeout of 15 seconds. The controller performs automatic retries for connection errors and 500-range response code. diff --git a/docs/spec/v1beta2/providers.md b/docs/spec/v1beta2/providers.md index e23699050..37a5da45e 100644 --- a/docs/spec/v1beta2/providers.md +++ b/docs/spec/v1beta2/providers.md @@ -4,7 +4,8 @@ The `Provider` API defines how events are encoded and where to send them. ## Example -The following is an example of how to send alerts to Slack when Flux fails to install or upgrade Flagger. +The following is an example of how to send alerts to Slack when Flux fails to +install or upgrade [Flagger](https://github.com/fluxcd/flagger). ```yaml --- @@ -48,7 +49,8 @@ In the above example: - The notification-controller starts listening for events sent for all HelmRepositories and HelmReleases in the `flagger-system` namespace. - When an event with severity `error` is received, the controller posts - a message on Slack containing the `summary` text and the Helm install or upgrade error. + a message on Slack containing the `summary` text and the Helm install or + upgrade error. - The controller uses the Slack Bot token from the secret indicated by the `Provider.spec.secretRef.name` to authenticate with the Slack API. @@ -90,6 +92,7 @@ You can run this example by saving the manifests into `slack-alerts.yaml`. As with all other Kubernetes config, a Provider needs `apiVersion`, `kind`, and `metadata` fields. The name of an Alert object must be a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). + A Provider also needs a [`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). @@ -101,11 +104,10 @@ The supported alerting providers are: | Provider | Type | |---------------------------------------------------------|------------------| -| [Prometheus Alertmanager](#prometheus-alertmanager) | `alertmanager` | -| [Azure Event Hub](#azure-event-hub) | `azureeventhub` | -| [Discord](#discord) | `discord` | | [Generic webhook](#generic-webhook) | `generic` | | [Generic webhook with HMAC](#generic-webhook-with-hmac) | `generic-hmac` | +| [Azure Event Hub](#azure-event-hub) | `azureeventhub` | +| [Discord](#discord) | `discord` | | [GitHub dispatch](#github-dispatch) | `githubdispatch` | | [Google Chat](#google-chat) | `googlechat` | | [Grafana](#grafana) | `grafana` | @@ -113,6 +115,7 @@ The supported alerting providers are: | [Matrix](#matrix) | `matrix` | | [Microsoft Teams](#microsoft-teams) | `msteams` | | [Opsgenie](#opsgenie) | `opsgenie` | +| [Prometheus Alertmanager](#prometheus-alertmanager) | `alertmanager` | | [Rocket](#rocket) | `rocket` | | [Sentry](#sentry) | `sentry` | | [Slack](#slack) | `slack` | @@ -128,170 +131,15 @@ The supported providers for [Git commit status updates](#git-commit-status-updat | [GitHub](#github) | `github` | | [GitLab](#gitlab) | `gitlab` | -### Address - -`.spec.address` is an optional field that specifies the URL where the events are posted. - -If the address contains sensitive information such as tokens or passwords, it is -recommended to store the address in the Kubernetes secret referenced by `.spec.secretRef.name`. -When the referenced Secret contains an `address` key, the `.spec.address` value is ignored. - -### Channel - -`.spec.channel` is an optional field that specifies the channel where the events are posted. - -### Secret reference - -`.spec.secretRef.name` is an optional field to specify a name reference to a -Secret in the same namespace as the Provider, containing the authentication -credentials for the provider API. - -The Kubernetes secret can have any of the following keys: - -- `address` - overrides `.spec.address` -- `proxy` - overrides `.spec.proxy` -- `token` - used for authentication -- `headers` - HTTP headers values included in the POST request - -#### Address example - -For providers which embed tokens or other sensitive information in the URL, -the incoming webhooks address can be stored in the secret using the `address` key: - -```yaml ---- -apiVersion: v1 -kind: Secret -metadata: - name: my-provider-url - namespace: default -stringData: - address: "https://webhook.example.com/token" -``` - -#### Token example - -For providers which require token based authentication, the API token -can be stored in the secret using the `token` key: - -```yaml ---- -apiVersion: v1 -kind: Secret -metadata: - name: my-provider-auth - namespace: default -stringData: - token: "my-api-token" -``` - -#### HTTP headers example - -For providers which require specific HTTP headers to be attached to the POST request, -the headers can be set in the secret using the `headers` key: - -```yaml ---- -apiVersion: v1 -kind: Secret -metadata: - name: my-provider-headers - namespace: default -stringData: - headers: | - Authorization: my-api-token - X-Forwarded-Proto: https -``` - -#### Proxy auth example - -Some networks need to use an authenticated proxy to access external services. -Therefore, the proxy address can be stored as a secret to hide parameters like the username and password: - -```yaml ---- -apiVersion: v1 -kind: Secret -metadata: - name: my-provider-proxy - namespace: default -stringData: - proxy: "http://username:password@proxy_url:proxy_port" -``` - -### TLS certificates - -`.spec.certSecretRef` is an optional field to specify a name reference to a -Secret in the same namespace as the Provider, containing the TLS CA certificate. - -#### Example - -To enable notification-controller to communicate with a provider API over HTTPS -using a self-signed TLS certificate, set the `caFile` like so: - -```yaml ---- -apiVersion: notification.toolkit.fluxcd.io/v1beta2 -kind: Provider -metadata: - name: my-webhook - namespace: flagger-system -spec: - type: generic - address: https://my-webhook.internal - certSecretRef: - name: my-ca-crt ---- -apiVersion: v1 -kind: Secret -metadata: - name: my-ca-crt - namespace: default -stringData: - caFile: | - <--- CA Key ---> -``` - -### HTTP/S proxy +#### Alerting -`.spec.proxy` is an optional field to specify an HTTP/S proxy address. - -If the proxy address contains sensitive information such as basic auth credentials, it is -recommended to store the proxy in the Kubernetes secret referenced by `.spec.secretRef.name`. -When the referenced Secret contains a `proxy` key, the `.spec.proxy` value is ignored. +##### Generic webhook -### Interval +When `.spec.type` is set to `generic`, the controller will send an HTTP POST +request to the provided [Address](#address). -`.spec.interval` is a required field with a default of ten minutes that specifies -the time interval at which the controller reconciles the provider with its Secret -references. - -### Suspend - -`.spec.suspend` is an optional field to suspend the provider. -When set to `true`, the controller will stop sending events to this provider. -When the field is set to `false` or removed, it will resume. - -## Working with Providers - -### Generic webhook - -The `generic` webhook triggers an HTTP POST request to the provided endpoint. - -The `Gotk-Component` header identifies which component this event is coming -from, e.g. `source-controller`, `kustomize-controller`. - -``` -POST / HTTP/1.1 -Host: example.com -Accept-Encoding: gzip -Content-Length: 452 -Content-Type: application/json -Gotk-Component: source-controller -User-Agent: Go-http-client/1.1 -``` - -The body of the request looks like this: +The body of the request is a [JSON `Event` object](events.md#event-structure), +for example: ```json { @@ -314,59 +162,66 @@ The body of the request looks like this: } ``` -The `involvedObject` key contains the object that triggered the event. +Where the `involvedObject` key contains the metadata from the object triggering +the event. -You can add additional headers to the POST request by providing a `headers` field to the secret -referenced by the provider. An example is given below: +The controller includes a `Gotk-Component` header in the request, which can be +used to identify the component which sent the event, e.g. `source-controller` +or `notification-controller`. -```yaml -apiVersion: notification.toolkit.fluxcd.io/v1beta2 -kind: Provider -metadata: - name: generic - namespace: default -spec: - type: generic - address: https://api.github.com/repos/owner/repo/dispatches - secretRef: - name: generic-secret ---- -apiVersion: v1 -kind: Secret -metadata: - name: generic-secret - namespace: default -stringData: - headers: | - Authorization: token - X-Forwarded-Proto: https ``` +POST / HTTP/1.1 +Host: example.com +Accept-Encoding: gzip +Content-Length: 452 +Content-Type: application/json +Gotk-Component: kustomize-controller +User-Agent: Go-http-client/1.1 +``` + +You can add additional headers to the POST request using a [`headers` key in the +referenced Secret](#http-headers-example). -### Generic webhook with HMAC +##### Generic webhook with HMAC -If you set the `.spec.type` of a `Provider` resource to `generic-hmac` then the HTTP request -sent to the webhook will include the `X-Signature` HTTP header carrying the HMAC of the request body. -This allows the webhook server to authenticate the request. -The key used for the HMAC must be supplied in the `token` field of the Secret resource referenced in `.spec.secretRef`. -The HTTP header value has the following format: +When `.spec.type` is set to `generic-hmac`, the controller will send an HTTP +POST request to the provided [Address](#address) for an [Event](events.md#event-structure), +while including an `X-Signature` HTTP header carrying the HMAC of the request +body. The inclusion of the header allows the receiver to verify the +authenticity and integrity of the request. + +The `X-Signature` header is calculated by generating an HMAC of the request +body using the [`token` key from the referenced Secret](#token-example). The +HTTP header value has the following format: ``` -X-Signature: HASH_FUNC=HASH +X-Signature: = ``` -`HASH_FUNC` denotes the Hash function used to generate the HMAC and currently defaults -to `sha256` but may change in the future. You must make sure to take this value into -account when verifying the HMAC. `HASH` is the hex-encoded HMAC value. -The following Go code illustrates how the header is parsed and verified: +`` denotes the hash function used to generate the HMAC and +currently defaults to `sha256`, which may change in the future. `` is the +HMAC of the request body, encoded as a hexadecimal string. + +while `` is the hex-encoded HMAC value. + +The body of the request is a [JSON `Event` object](events.md#event-structure), +as described in the [Generic webhook](#generic-webhook) section. + +###### HMAC verification example + +The following example in Go shows how to verify the authenticity and integrity +of a request by using the X-Signature header. ```go -func verifySignature(sig string, payload, key []byte) error { - sigHdr := strings.Split(sig, "=") - if len(shgHdr) != 2 { +func verifySignature(signature string, payload, key []byte) error { + sig := strings.Split(signature, "=") + + if len(sig) != 2 { return fmt.Errorf("invalid signature value") } + var newF func() hash.Hash - switch sigHdr[0] { + switch sig[0] { case "sha224": newF = sha256.New224 case "sha256": @@ -378,38 +233,80 @@ func verifySignature(sig string, payload, key []byte) error { default: return fmt.Errorf("unsupported signature algorithm %q", sigHdr[0]) } + mac := hmac.New(newF, key) if _, err := mac.Write(payload); err != nil { - return fmt.Errorf("error MAC'ing payload: %w", err) + return fmt.Errorf("failed to write payload to HMAC encoder: %w", err) } + sum := fmt.Sprintf("%x", mac.Sum(nil)) - if sum != sigHdr[1] { - return fmt.Errorf("HMACs don't match: %#v != %#v", sum, sigHdr[1]) + if sum != sig[0] { + return fmt.Errorf("HMACs do not match: %#v != %#v", sum, sigHdr[0]) } return nil } -[...] -key := []byte("b1fad212fb1b87a56c79e5da48018650b85ab7cf") -if len(r.Header["X-Signature"]) > 0 { - if err := verifySignature(r.Header["X-Signature"][0], body, key); err != nil { - // handle signature verification failure here + +func handleRequest(w http.ResponseWriter, r *http.Request) { + // Require a X-Signature header + if len(r.Header["X-Signature"])) == 0 { + http.Error(w, "missing X-Signature header", http.StatusBadRequest) + return + } + + // Read the request body with a limit of 1MB + lr := io.LimitReader(r.Body, 1<<20) + body, err := ioutil.ReadAll(lr) + if err != nil { + http.Error(w, "failed to read request body", http.StatusBadRequest) + return } + + // Verify signature using the same token as the Secret referenced in + // Provider + key := "" + if err := verifySignature(r.Header.Get("X-Signature"), body, key); err != nil { + http.Error(w, fmt.Sprintf("failed to verify HMAC signature: %s", err.String()), http.StatusBadRequest) + return + } + + // Do something with the verified request body + // ... } ``` -### Slack +##### Slack -To send alerts to Slack, we recommend using a Slack Bot App token. -To obtain a token, please follow [Slack's guide on bot users](https://api.slack.com/bot-users). +When `.spec.type` is set to `slack`, the controller will send a message for an +[Event](events.md#event-structure) to the provided Slack API [Address](#address). -Once you have a Slack bot token (starts with `xoxb-`), create a secret for it with: +The Event will be formatted into a Slack message using an [Attachment](https://api.slack.com/reference/messaging/attachments), +with the metadata attached as fields, and the involved object as author. +The severity of the Event is used to set the color of the attachment. -```shell -kubectl create secret generic slack-token --from-literal=token=BOT-TOKEN -``` +When a [Channel](#channel) is provided, it will be added as a [`channel` +field](https://api.slack.com/methods/chat.postMessage#arg_channel) to the API +payload. Otherwise, the further configuration of the [Address](#address) will +determine the channel. + +When [Username](#username) is set, this will be added as a [`username` +field](https://api.slack.com/methods/chat.postMessage#arg_username) to the +payload, defaulting to the name of the reporting controller. + +This Provider type supports the configuration of a [proxy URL](#https-proxy) +and/or [TLS certificates](#tls-certificates). -Create a provider of type `slack`, with the address set to `https://slack.com/api/chat.postMessage` -and reference the `slack-token` secret: +###### Slack example + +To configure a Provider for Slack, we recommend using a Slack Bot App token which is +not attached to a specific Slack account. To obtain a token, please follow +[Slack's guide on creating an app](https://api.slack.com/authentication/basics#creating). + +Once you have obtained a token, [create a Secret containing the `token` +key](#token-example) and a `slack` Provider with the `address` set to +`https://slack.com/api/chat.postMessage`. + +Using this API endpoint, it is possible to send messages to multiple channels +by adding the integration to each channel. ```yaml --- @@ -424,9 +321,21 @@ spec: address: https://slack.com/api/chat.postMessage secretRef: name: slack-token +--- +apiVersion: v1 +kind: Secret +metadata: + name: slack-token + namespace: default +stringData: + token: xoxb-1234567890-1234567890-1234567890-1234567890 ``` -Slack legacy webhooks are also supported, the webhook URL can be set in the `address` field or in the secret: +###### Slack (legacy) example + +To configure a Provider for Slack using the [legacy incoming webhook API](https://api.slack.com/messaging/webhooks), +create a Secret with the `address` set to `https://hooks.slack.com/services/...`, +and a `slack` Provider with a [Secret reference](#address-example). ```yaml --- @@ -449,25 +358,26 @@ stringData: address: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK" ``` -### Microsoft Teams +##### Microsoft Teams -To send alerts to Teams, first create an -[incoming webhook](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) -on the Microsoft Teams UI: +When `.spec.type` is set to `msteams`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Microsoft Teams [Address](#address). -1. Open the settings of the channel you want the notifications to be sent to. -2. Click on `Connectors`. -3. Click on the `Add` button for `Incoming Webhook`. -4. Click on `Configure` and copy the webhook URL given. +The Event will be formatted into a Microsoft Teams +[connector message](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#example-of-connector-message), +with the metadata attached as facts, and the involved object as summary. +The severity of the Event is used to set the color of the message. -Once you have the webhook URL, create a secret for it with: +This Provider type supports the configuration of a [proxy URL](#https-proxy) +and/or [TLS certificates](#tls-certificates), but lacks support for +configuring a [Channel](#channel). This can be configured during the +creation of the incoming webhook in Microsoft Teams. -```shell -kubectl create secret generic teams-webhook \ ---from-literal=address= -``` +###### Microsoft Teams example -Create a provider of type `msteam` and reference the `teams-webhook` secret: +To configure a Provider for Microsoft Teams, create a Secret with [the +`address`](#address-example) set to the [webhook URL](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook#create-incoming-webhooks-1), +and a `msteams` Provider with a [Secret reference](#address-example). ```yaml --- @@ -480,20 +390,34 @@ spec: type: msteams secretRef: name: slack-webhook +--- +apiVersion: v1 +kind: Secret +metadata: + name: msteams-webhook + namespace: default +stringData: + address: "https://xxx.webhook.office.com/..." ``` -### Discord +##### Discord -To send events to Discord, first [create a webhook](https://discord.com/developers/docs/resources/webhook#create-webhook). +When `.spec.type` is set to `discord`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Discord [Address](#address). -Once you have the webhook URL, create a secret for it with: +The Event will be formatted into a [Slack message](#slack) and send to the +`/slack` endpoint of the provided Discord [Address](#address). -```shell -kubectl create secret generic discord-webhook \ ---from-literal=address= -``` +This Provider type supports the configuration of a [proxy URL](#https-proxy) +and/or [TLS certificates](#tls-certificates), but lacks support for +configuring a [Channel](#channel). This can be configured [during the creation +of the address](https://discord.com/developers/docs/resources/webhook#create-webhook) + +###### Discord example -Create a provider of type `discord` and reference the `discord-webhook` secret: +To configure a Provider for Discord, create a Secret with [the `address`](#address-example) +set to the [webhook URL](https://discord.com/developers/docs/resources/webhook#create-webhook), +and a `discord` Provider with a [Secret reference](#secret-reference). ```yaml --- @@ -506,11 +430,40 @@ spec: type: discord secretRef: name: discord-webhook +--- +apiVersion: v1 +kind: Secret +metadata: + name: discord-webhook + namespace: default +stringData: + address: "https://discord.com/api/webhooks/..." ``` -### Sentry -To send events to Sentry, create a provider of type `sentry` and a secret with the Sentry URL: +##### Sentry + +When `.spec.type` is set to `sentry`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Sentry [Address](#address). + +Depending on the `severity` of the Event, the controller will capture a [Sentry +Event](https://develop.sentry.dev/sdk/event-payloads/)for `error`, or [Sentry +Transaction Event](https://develop.sentry.dev/sdk/event-payloads/transaction/) +with a [Span](https://develop.sentry.dev/sdk/event-payloads/span/) for `info`. +The metadata of the Event is included as [`extra` data](https://develop.sentry.dev/sdk/event-payloads/#optional-attributes) +in the Sentry Event, or as [Span `tags`](https://develop.sentry.dev/sdk/event-payloads/span/#attributes). + +The Provider's [Channel](#channel) is used to set the `environment` on the +Sentry client. + +This Provider type supports the configuration of +[TLS certificates](#tls-certificates). + +###### Sentry example + +To configure a Provider for Sentry, create a Secret with [the `address`](#address-example) +set to a [Sentry DSN](https://docs.sentry.io/product/sentry-basics/dsn-explainer/), +and a `sentry` Provider with a [Secret reference](#secret-reference). ```yaml --- @@ -534,24 +487,31 @@ stringData: address: "https://....@sentry.io/12341234" ``` -Note that the `.spec.channel` field can be used to specify which environment the messages are sent for. +**Note:** The `sentry` Provider also sends traces for events with the severity +`info`. This can be disabled by setting the `spec.eventSeverity` field to +`error` on an `Alert`. -The sentry provider also sends traces for events with the severity `info`. -This can be disabled by setting, the `Alert.spec.eventSeverity` field to `error`. +##### Telegram -### Telegram +When `.spec.type` is set to `telegram`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Telegram [Address](#address). -For telegram, You can get the token from [the botfather](https://core.telegram.org/bots#6-botfather) -and use `https://api.telegram.org/` as the address. +The Event will be formatted into a message string, with the metadata attached +as a list of key-value pairs. -Once you have a Telegram token, create a secret for it with: +The Provider's [Channel](#channel) is used to set the receiver of the message. +This can be a unique identifier (`-1234567890`) for the target chat, or +the username (`@username`) of the target channel. -```shell -kubectl create secret generic telegram-token \ ---from-literal=token=BOT-TOKEN -``` +This Provider type does not support the configuration of a [proxy URL](#https-proxy) +or [TLS certificates](#tls-certificates). + +###### Telegram example -Create a provider of type `telegram` and reference the `telegram-token` secret: +To configure a Provider for Telegram, create a Secret with [the `token`](#token-example) +obtained from [the BotFather](https://core.telegram.org/bots#how-do-i-create-a-bot), +and a `telegram` Provider with a [Secret reference](#secret-reference), and the +`address` set to `https://api.telegram.org`. ```yaml --- @@ -563,27 +523,31 @@ metadata: spec: type: telegram address: https://api.telegram.org - channel: "@fluxtest" # or "-1557265138" (channel id) + channel: "@fluxcd" # or "-1557265138" (channel id) secretRef: name: telegram-token ``` -Note that `.spec.channel` can be a unique identifier for the target chat -or the username of the target channel (in the format `@channelusername`). +##### Matrix -### Matrix +When `.spec.type` is set to `matrix`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Matrix [Address](#address). -For Matrix, the address is the homeserver URL and the token is the access token -returned by a call to `/login` or `/register`. +The Event will be formatted into a message string, with the metadata attached +as a list of key-value pairs, and send as a [`m.room.message` text event](https://spec.matrix.org/v1.3/client-server-api/#mroommessage) +to the provided Matrix [Address](#address). -Once you have a Matrix token, create a secret for it with: +The Provider's [Channel](#channel) is used to set the receiver of the message +using a room identifier (`!1234567890:example.org`). -```shell -kubectl create secret generic matrix-token \ ---from-literal=token=MY-TOKEN -``` +This provider type does support the configuration of [TLS +certificates](#tls-certificates). -Create a provider of type `matrix` and reference the `matrix-token` secret: +###### Matrix example + +To configure a Provider for Matrix, create a Secret with [the `token`](#token-example) +obtained from [the Matrix endpoint](https://matrix.org/docs/guides/client-server-api#registration), +and a `matrix` Provider with a [Secret reference](#secret-reference). ```yaml apiVersion: notification.toolkit.fluxcd.io/v1beta2 @@ -599,20 +563,22 @@ spec: name: matrix-token ``` -Note that `.spec.channel` holds the room ID. +##### Lark -### Lark +When `.spec.type` is set to `lark`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Lark [Address](#address). -For sending notifications to Lark, you will have to -[add a bot to the group](https://www.larksuite.com/hc/en-US/articles/360048487736-Bot-Use-bots-in-groups#III.%20How%20to%20configure%20custom%20bots%20in%20a%20group%C2%A0) -and set up a webhook for that bot account. This serves as the address field in the secret: +The Event will be formatted into a [Lark Message card](https://open.larksuite.com/document/ukTMukTMukTM/uczM3QjL3MzN04yNzcDN), +with the metadata written to the message string. -```shell -kubectl create secret generic lark-webhook \ ---from-literal=address= -``` +This Provider type does not support the configuration of a [proxy URL](#https-proxy) +or [TLS certificates](#tls-certificates). + +###### Lark example -Create a provider of type `lark` and reference the `lark-webhook` secret: +To configure a Provider for Lark, create a Secret with [the `address`](#address-example) +obtained from [adding a bot to a group](https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/bot-v3/use-custom-bots-in-a-group#57181e84), +and a `lark` Provider with a [Secret reference](#secret-reference). ```yaml apiVersion: notification.toolkit.fluxcd.io/v1beta2 @@ -624,20 +590,32 @@ spec: type: lark secretRef: name: lark-webhook +--- +apiVersion: v1 +kind: Secret +metadata: + name: lark-webhook + namespace: default +stringData: + address: "https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxxx" ``` -### Rocket +##### Rocket -To send events to Rocket chat, first [create an incoming webhook](https://docs.rocket.chat/guides/administration/admin-panel/integrations#create-a-new-incoming-webhook). +When `.spec.type` is set to `rocket`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Rocket [Address](#address). -Once you have the webhook URL, create a secret for it with: +The Event will be formatted into a [Slack message](#slack) and send as a +payload the provided Rocket [Address](#address). -```shell -kubectl create secret generic rocket-webhook \ ---from-literal=address= -``` +This Provider type does support the configuration of a [proxy URL](#https-proxy) +and [TLS certificates](#tls-certificates). -Create a provider of type `rocket` and reference the `rocket-webhook` secret: +###### Rocket example + +To configure a Provider for Rocket, create a Secret with [the `address`](#address-example) +set to the Rocket [webhook URL](https://docs.rocket.chat/guides/administration/admin-panel/integrations#incoming-webhook-script), +and a `rocket` Provider with a [Secret reference](#secret-reference). ```yaml --- @@ -652,18 +630,22 @@ spec: name: rocket-webhook ``` -### Google Chat +##### Google Chat -To send notifications to Google chat, first [create an incoming webhook](https://developers.google.com/chat/how-tos/webhooks#create_a_webhook). +When `.spec.type` is set to `googlechat`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Google Chat [Address](#address). -Once you have the webhook URL, create a secret for it with: +The Event will be formatted into a [Google Chat card message](https://developers.google.com/chat/api/reference/rest/v1/cards-v1), +with the metadata added as a list of [key-value pairs](https://developers.google.com/chat/api/reference/rest/v1/cards-v1#keyvalue) +in a [widget](https://developers.google.com/chat/api/reference/rest/v1/cards-v1#widgetmarkup). -```shell -kubectl create secret generic google-webhook \ ---from-literal=address= -``` +This Provider type does support the configuration of a [proxy URL](#https-proxy). + +###### Google Chat example -Create a provider of type `googlechat` and reference the `google-webhook` secret: +To configure a Provider for Google Chat, create a Secret with [the `address`](#address-example) +set to the Google Chat [webhook URL](https://developers.google.com/chat/how-tos/webhooks#create_a_webhook), +and a `googlechat` Provider with a [Secret reference](#secret-reference). ```yaml --- @@ -676,23 +658,37 @@ spec: type: googlechat secretRef: name: google-webhook +--- +apiVersion: v1 +kind: Secret +metadata: + name: google-webhook + namespace: default +stringData: + address: https://chat.googleapis.com/v1/spaces/... ``` -### Opsgenie +##### Opsgenie -To send notifications to Opsgenie, first -[add a REST API integration](https://support.atlassian.com/opsgenie/docs/create-a-default-api-integration/). +When `.spec.type` is set to `opsgenie`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Opsgenie [Address](#address). -Once you have a Opsgenie API key, create a secret for it with: +The Event will be formatted into a [Opsgenie alert](https://docs.opsgenie.com/docs/alert-api#section-create-alert-request), +with the metadata added to the [`details` field](https://docs.opsgenie.com/docs/alert-api#create-alert) +as a list of key-value pairs. -```shell -kubectl create secret generic opsgenie-token \ ---from-literal=token= -``` +This Provider type does support the configuration of a [proxy URL](#https-proxy) +and [TLS certificates](#tls-certificates). + +###### Opsgenie example -Create a provider of type `opsgenie` and reference the `opsgenie-token` secret: +To configure a Provider for Opsgenie, create a Secret with [the `token`](#token-example) +set to the Opsgenie [API token](https://support.atlassian.com/opsgenie/docs/create-a-default-api-integration/), +and a `opsgenie` Provider with a [Secret reference](#secret-reference) and the +`address` set to `https://api.opsgenie.com/v2/alerts`. ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Provider metadata: @@ -703,34 +699,28 @@ spec: address: https://api.opsgenie.com/v2/alerts secretRef: name: opsgenie-token -``` - -### Prometheus Alertmanager - -To send events to the Prometheus [Alertmanager v2 API](https://github.com/prometheus/alertmanager/blob/main/api/v2/openapi.yaml), -create a provider of type `alertmanager` and set the `address` field to the `api/v2/alerts` endpoint: - -```yaml -apiVersion: notification.toolkit.fluxcd.io/v1beta2 -kind: Provider +--- +apiVersion: v1 +kind: Secret metadata: - name: alertmanager + name: opsgenie-token namespace: default -spec: - type: alertmanager - # webhook address (ignored if secretRef is specified) - address: https://....@/api/v2/alerts/" +stringData: + token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` -If Alertmanager has basic authentication configured, it is recommended to use -`.spec.secretRef` and include the `username:password` in the address string inside the secret. +##### Prometheus Alertmanager -When an event is received, the controller will send a single alert with at least one annotation -which is the `message` found for the event. -If an `Alert.spec.summary` is provided, an additional "summary" annotation will be added. +When `.spec.type` is set to `alertmanager`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Prometheus Alertmanager +[Address](#address). -The provider will send the following labels for the event: +The Event will be formatted into a `firing` [Prometheus Alertmanager +alert](https://prometheus.io/docs/alerting/latest/notifications/#alert), +with the metadata added to the `labels` fields, and the `message` (and optional +`.metadata.summary`) added as annotations. +In addition to the metadata from the Event, the following labels will be added: | Label | Description | |-----------|------------------------------------------------------------------------------------------------------| @@ -742,28 +732,65 @@ The provider will send the following labels for the event: | name | The name of the involved object associated with the event | | namespace | The namespace of the involved object associated with the event | -### Webex +This Provider type does support the configuration of a [proxy URL](#https-proxy) +and [TLS certificates](#tls-certificates). -General steps on how to send notifications to a Webex space: +###### Prometheus Alertmanager example -From the Webex App UI: +To configure a Provider for Prometheus Alertmanager, create a Secret with [the +`address`](#address-example) set to the Prometheus Alertmanager [HTTP API +URL](https://prometheus.io/docs/alerting/latest/https/#http-traffic) +including Basic Auth credentials, and a `alertmanager` Provider with a [Secret +reference](#secret-reference). -- create a Webex space where you want notifications to be sent -- after creating a Webex bot (described in next section), add the bot email address to the Webex space ("People | Add people") +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: alertmanager + namespace: default +spec: + type: alertmanager + secretRef: + name: alertmanager-address +--- +apiVersion: v1 +kind: Secret +metadata: + name: alertmanager-address + namespace: default +stringData: + address: https://username:password@/api/v2/alerts/" +``` + +##### Webex + +When `.spec.type` is set to `webex`, the controller will send a payload for +an [Event](events.md#event-structure) to the provided Webex [Address](#address). + +The Event will be formatted into a message string, with the metadata attached +as a list of key-value pairs, and send as a [Webex message](https://developer.webex.com/docs/api/v1/messages/create-a-message). + +The [Channel](#channel) is used to set the ID of the room to send the message +to. -Register to https://developer.webex.com/, after signing in: +This Provider type does support the configuration of a [proxy URL](#https-proxy) +and [TLS certificates](#tls-certificates). -- Create a bot for forwarding Flux notifications to a Webex Space - (User profile icon | MyWebexApps | Create a New App | Create a Bot). -- Make a note of the bot email address, this email needs to be added to the Webex space from the Webex App. -- Generate a bot access token, this is the ID to use in the kubernetes Secret "token" field. -- Find the room ID associated to the webex space using https://developer.webex.com/docs/api/v1/rooms/list-rooms - (select GET, click on "Try It" and search the GET results for the matching Webex space entry), - this is the ID to use in the webex Provider manifest "channel" field. +###### Webex example -Example: +To configure a Provider for Webex, create a Secret with [the `token`](#token-example) +set to the Webex [access token](https://developer.webex.com/docs/api/getting-started#authentication), +and a `webex` Provider with a [Secret reference](#secret-reference) and the +`address` set to `https://webexapis.com/v1/messages`. + +**Note:** To be able to send messages to a Webex room, the bot needs to be +added to the room. Failing to do so will result in 404 errors, logged by the +controller. ```yaml +--- apiVersion: notification.toolkit.fluxcd.io/v1beta2 kind: Provider metadata: @@ -785,14 +812,158 @@ stringData: token: ``` -Notes: +### Address + +`.spec.address` is an optional field that specifies the URL where the events are posted. + +If the address contains sensitive information such as tokens or passwords, it is +recommended to store the address in the Kubernetes secret referenced by `.spec.secretRef.name`. +When the referenced Secret contains an `address` key, the `.spec.address` value is ignored. + +### Channel + +`.spec.channel` is an optional field that specifies the channel where the events are posted. + +### Username + +`.spec.username` is an optional field that specifies the username used to post +the events. Can be overwritten with a [Secret reference](#secret-reference). + +### Secret reference + +`.spec.secretRef.name` is an optional field to specify a name reference to a +Secret in the same namespace as the Provider, containing the authentication +credentials for the provider API. + +The Kubernetes secret can have any of the following keys: + +- `address` - overrides `.spec.address` +- `proxy` - overrides `.spec.proxy` +- `token` - used for authentication +- `username` - overrides `.spec.username` +- `headers` - HTTP headers values included in the POST request + +#### Address example + +For providers which embed tokens or other sensitive information in the URL, +the incoming webhooks address can be stored in the secret using the `address` key: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-provider-url + namespace: default +stringData: + address: "https://webhook.example.com/token" +``` + +#### Token example -- `.spec.address` should always be set to the same global Webex API gateway `https://webexapis.com/v1/messages` -- `.spec.channel` should contain the Webex space room ID as obtained from `https://developer.webex.com/` (long alphanumeric string copied as is). +For providers which require token based authentication, the API token +can be stored in the secret using the `token` key: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-provider-auth + namespace: default +stringData: + token: "my-api-token" +``` + +#### HTTP headers example + +For providers which require specific HTTP headers to be attached to the POST request, +the headers can be set in the secret using the `headers` key: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-provider-headers + namespace: default +stringData: + headers: | + Authorization: my-api-token + X-Forwarded-Proto: https +``` + +#### Proxy auth example + +Some networks need to use an authenticated proxy to access external services. +Therefore, the proxy address can be stored as a secret to hide parameters like the username and password: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-provider-proxy + namespace: default +stringData: + proxy: "http://username:password@proxy_url:proxy_port" +``` + +### TLS certificates + +`.spec.certSecretRef` is an optional field to specify a name reference to a +Secret in the same namespace as the Provider, containing the TLS CA certificate. + +#### Example + +To enable notification-controller to communicate with a provider API over HTTPS +using a self-signed TLS certificate, set the `caFile` like so: + +```yaml +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: my-webhook + namespace: flagger-system +spec: + type: generic + address: https://my-webhook.internal + certSecretRef: + name: my-ca-crt +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-ca-crt + namespace: default +stringData: + caFile: | + <--- CA Key ---> +``` + +### HTTP/S proxy + +`.spec.proxy` is an optional field to specify an HTTP/S proxy address. + +If the proxy address contains sensitive information such as basic auth credentials, it is +recommended to store the proxy in the Kubernetes secret referenced by `.spec.secretRef.name`. +When the referenced Secret contains a `proxy` key, the `.spec.proxy` value is ignored. + +### Interval + +`.spec.interval` is a required field with a default of ten minutes that specifies +the time interval at which the controller reconciles the provider with its Secret +references. + +### Suspend + +`.spec.suspend` is an optional field to suspend the provider. +When set to `true`, the controller will stop sending events to this provider. +When the field is set to `false` or removed, it will resume. + +## Working with Providers -If you do not see any notifications in the targeted Webex space, check that you have added the bot -email address to the Webex space, if the bot email address is not added to the space, -the notification-controller will log a 404 room not found error every time a notification is sent out. ### Grafana @@ -1042,8 +1213,9 @@ stringData: address: ``` -Assuming that you have created Azure event hub and namespace you should be able to use a similar command to get your -connection string. This will give you the default Root SAS, it's NOT supposed to be used in production. +Assuming that you have created the Azure event hub and namespace you should be +able to use a similar command to get your connection string. This will give +you the default Root SAS, which is NOT supposed to be used in production. ```shell az eventhubs namespace authorization-rule keys list --resource-group --namespace-name --name RootManageSharedAccessKey -o tsv --query primaryConnectionString @@ -1098,7 +1270,7 @@ spec: When `.spec.type` is set to `github`, the referenced secret must contain a key called `token` with the value set to a [GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). -The token must has permissions to update the commit status for the GitHub repository specified in `.spec.address`. +The token must have permissions to update the commit status for the GitHub repository specified in `.spec.address`. You can create the secret with `kubectl` like this: @@ -1111,7 +1283,7 @@ kubectl create secret generic github-token --from-literal=token= When `.spec.type` is set to `gitlab`, the referenced secret must contain a key called `token` with the value set to a [GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html). -The token must has permissions to update the commit status for the GitLab repository specified in `.spec.address`. +The token must have permissions to update the commit status for the GitLab repository specified in `.spec.address`. You can create the secret with `kubectl` like this: @@ -1140,7 +1312,7 @@ kubectl create secret generic gitlab-token --from-literal=token=: -```` +``` ### Interval @@ -698,9 +698,8 @@ path](#webhook-path) will result in request to the Kubernetes API, as the controller needs to fetch information about the resource. This endpoint may be protected with a token, but this does not defend against a situation where a legitimate webhook caller starts sending large amounts of requests, or the -token is somehow leaked. This may result in unwanted consequences for the -controller, as it may get rate limited by the Kubernetes API, degrading its -functionality. +token is somehow leaked. This may result in the controller, as it may get rate +limited by the Kubernetes API, degrading its functionality. It is therefore a good idea to set rate limits on the Ingress which exposes the Kubernetes Service. If you are using ingress-nginx, this can be done by
(Optional) +

Conditions holds the conditions for the Receiver.

(Optional) -

Generated webhook URL in the format +

URL is the generated incoming webhook address in the format of ‘/hook/sha256sum(token+name+namespace)’.

(Optional) -

ObservedGeneration is the last observed generation.

+

ObservedGeneration is the last observed generation of the Receiver object.

+interval
+ + +Kubernetes meta/v1.Duration + + +
+

Interval at which to reconcile the Provider with its Secret references.

+
channel
string @@ -432,6 +445,19 @@ the validation procedure and payload deserialization.

+interval
+ + +Kubernetes meta/v1.Duration + + +
+

Interval at which to reconcile the Receiver with its Secret references.

+
events
[]string @@ -776,6 +802,19 @@ string
+interval
+ + +Kubernetes meta/v1.Duration + + +
+

Interval at which to reconcile the Provider with its Secret references.

+
channel
string @@ -976,6 +1015,19 @@ the validation procedure and payload deserialization.

+interval
+ + +Kubernetes meta/v1.Duration + + +
+

Interval at which to reconcile the Receiver with its Secret references.

+
events
[]string diff --git a/docs/spec/v1beta2/providers.md b/docs/spec/v1beta2/providers.md index f6f7a1853..34c33f434 100644 --- a/docs/spec/v1beta2/providers.md +++ b/docs/spec/v1beta2/providers.md @@ -227,6 +227,12 @@ stringData: `.spec.proxy` is an optional field to specify an HTTP/S proxy address. +### Interval + +`.spec.interval` is a required field with a default of ten minutes that specifies +the time interval at which the controller reconciles the provider with its Secret +references. + ### Suspend `.spec.suspend` is an optional field to suspend the provider. diff --git a/docs/spec/v1beta2/receivers.md b/docs/spec/v1beta2/receivers.md index dec2be907..fe6b12a1d 100644 --- a/docs/spec/v1beta2/receivers.md +++ b/docs/spec/v1beta2/receivers.md @@ -128,6 +128,12 @@ to another tenant's resources. `.spec.secretRef.name` is a required field to specify a name reference to a Secret in the same namespace as the Receiver, containing the secret token. +### Interval + +`.spec.interval` is a required field with a default of ten minutes that specifies +the time interval at which the controller reconciles the provider with its Secret +references. + ### Suspend `.spec.suspend` is an optional field to suspend the receiver. From ee2600a07c53a8c5382b860a5e809796b33bee39 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 8 Nov 2022 11:59:04 +0200 Subject: [PATCH 11/26] Issue warning events on reconciliation errors Signed-off-by: Stefan Prodan --- controllers/alert_controller.go | 6 ++++++ controllers/provider_controller.go | 7 +++++++ controllers/receiver_controller.go | 7 +++++++ controllers/suite_test.go | 8 +++++--- main.go | 3 +++ 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/controllers/alert_controller.go b/controllers/alert_controller.go index 0396f94f3..e7905f422 100644 --- a/controllers/alert_controller.go +++ b/controllers/alert_controller.go @@ -21,6 +21,7 @@ import ( "fmt" "time" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" @@ -116,6 +117,11 @@ func (r *AlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu r.Metrics.RecordDuration(ctx, obj, reconcileStart) r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) + // Issue warning event if the reconciliation failed. + if retErr != nil { + r.Event(obj, corev1.EventTypeWarning, meta.FailedReason, retErr.Error()) + } + // Patch finalizers, status and conditions. retErr = r.patch(ctx, obj, patcher) }() diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index d358a2026..68592c073 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -26,6 +26,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" + kuberecorder "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -49,6 +50,7 @@ import ( type ProviderReconciler struct { client.Client helper.Metrics + kuberecorder.EventRecorder ControllerName string } @@ -97,6 +99,11 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r r.Metrics.RecordDuration(ctx, obj, reconcileStart) r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) + // Issue warning event if the reconciliation failed. + if retErr != nil { + r.Event(obj, corev1.EventTypeWarning, meta.FailedReason, retErr.Error()) + } + // Patch finalizers, status and conditions. retErr = r.patch(ctx, obj, patcher) }() diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index c7d1dd9fd..9fdf9cd89 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -25,6 +25,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" + kuberecorder "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -46,6 +47,7 @@ import ( type ReceiverReconciler struct { client.Client helper.Metrics + kuberecorder.EventRecorder ControllerName string } @@ -104,6 +106,11 @@ func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r r.Metrics.RecordDuration(ctx, obj, reconcileStart) r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) + // Issue warning event if the reconciliation failed. + if retErr != nil { + r.Event(obj, corev1.EventTypeWarning, meta.FailedReason, retErr.Error()) + } + // Patch finalizers, status and conditions. retErr = r.patch(ctx, obj, patcher) }() diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 7896fa6ab..7f7458a7d 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -67,12 +67,12 @@ func TestMain(m *testing.M) { controllerName := "notification-controller" testMetricsH := controller.MustMakeMetrics(testEnv) - reconciler := AlertReconciler{ + if err := (&AlertReconciler{ Client: testEnv, Metrics: testMetricsH, ControllerName: controllerName, - } - if err := (reconciler).SetupWithManager(testEnv); err != nil { + EventRecorder: testEnv.GetEventRecorderFor(controllerName), + }).SetupWithManager(testEnv); err != nil { panic(fmt.Sprintf("Failed to start AlerReconciler: %v", err)) } @@ -80,6 +80,7 @@ func TestMain(m *testing.M) { Client: testEnv, Metrics: testMetricsH, ControllerName: controllerName, + EventRecorder: testEnv.GetEventRecorderFor(controllerName), }).SetupWithManager(testEnv); err != nil { panic(fmt.Sprintf("Failed to start ProviderReconciler: %v", err)) } @@ -88,6 +89,7 @@ func TestMain(m *testing.M) { Client: testEnv, Metrics: testMetricsH, ControllerName: controllerName, + EventRecorder: testEnv.GetEventRecorderFor(controllerName), }).SetupWithManager(testEnv); err != nil { panic(fmt.Sprintf("Failed to start ReceiverReconciler: %v", err)) } diff --git a/main.go b/main.go index 25d0fc9d8..85627d7ab 100644 --- a/main.go +++ b/main.go @@ -127,6 +127,7 @@ func main() { Client: mgr.GetClient(), ControllerName: controllerName, Metrics: metricsH, + EventRecorder: mgr.GetEventRecorderFor(controllerName), }).SetupWithManagerAndOptions(mgr, controllers.ProviderReconcilerOptions{ MaxConcurrentReconciles: concurrent, RateLimiter: helper.GetRateLimiter(rateLimiterOptions), @@ -138,6 +139,7 @@ func main() { Client: mgr.GetClient(), ControllerName: controllerName, Metrics: metricsH, + EventRecorder: mgr.GetEventRecorderFor(controllerName), }).SetupWithManagerAndOptions(mgr, controllers.AlertReconcilerOptions{ MaxConcurrentReconciles: concurrent, RateLimiter: helper.GetRateLimiter(rateLimiterOptions), @@ -149,6 +151,7 @@ func main() { Client: mgr.GetClient(), ControllerName: controllerName, Metrics: metricsH, + EventRecorder: mgr.GetEventRecorderFor(controllerName), }).SetupWithManagerAndOptions(mgr, controllers.ReceiverReconcilerOptions{ MaxConcurrentReconciles: concurrent, RateLimiter: helper.GetRateLimiter(rateLimiterOptions), From caec764cccdb9d4ad589fd1d7e4e418cac648bad Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 8 Nov 2022 15:44:53 +0200 Subject: [PATCH 12/26] Refactor notifies to use Flux Event API v1beta1 Signed-off-by: Stefan Prodan --- docs/spec/v1beta2/events.md | 2 +- internal/notifier/forwarder_test.go | 2 +- internal/notifier/github_dispatch_test.go | 4 +++- internal/notifier/slack_test.go | 4 +++- internal/notifier/util_test.go | 7 ++++--- internal/notifier/webex_test.go | 4 +++- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/spec/v1beta2/events.md b/docs/spec/v1beta2/events.md index b2fd85d98..286fe3a98 100644 --- a/docs/spec/v1beta2/events.md +++ b/docs/spec/v1beta2/events.md @@ -41,7 +41,7 @@ In the above example: ## Event structure The Go type that defines the event structure can be found in the -[fluxcd/pkg/runtime/events](https://github.com/fluxcd/pkg/blob/main/runtime/events/event.go) +[fluxcd/pkg/apis/event/v1beta1](https://github.com/fluxcd/pkg/blob/main/apis/event/v1beta1/event.go) package. ## Rate limiting diff --git a/internal/notifier/forwarder_test.go b/internal/notifier/forwarder_test.go index d0f6815b4..dfc53b814 100644 --- a/internal/notifier/forwarder_test.go +++ b/internal/notifier/forwarder_test.go @@ -25,10 +25,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" - "github.com/stretchr/testify/require" ) func TestForwarder_New(t *testing.T) { diff --git a/internal/notifier/github_dispatch_test.go b/internal/notifier/github_dispatch_test.go index 0841f7d16..370fb0580 100644 --- a/internal/notifier/github_dispatch_test.go +++ b/internal/notifier/github_dispatch_test.go @@ -22,6 +22,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" ) func TestNewGitHubDispatchBasic(t *testing.T) { @@ -55,7 +57,7 @@ func TestGitHubDispatch_PostUpdate(t *testing.T) { require.NoError(t, err) event := testEvent() - event.Metadata["commit_status"] = "update" + event.Metadata[eventv1.MetaCommitStatusKey] = eventv1.MetaCommitStatusUpdateValue err = githubDispatch.Post(context.TODO(), event) require.NoError(t, err) } diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index e9bcc5a72..9722cfcbf 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -25,6 +25,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" ) func TestSlack_Post(t *testing.T) { @@ -52,7 +54,7 @@ func TestSlack_PostUpdate(t *testing.T) { require.NoError(t, err) event := testEvent() - event.Metadata["commit_status"] = "update" + event.Metadata[eventv1.MetaCommitStatusKey] = eventv1.MetaCommitStatusUpdateValue err = slack.Post(context.TODO(), event) require.NoError(t, err) } diff --git a/internal/notifier/util_test.go b/internal/notifier/util_test.go index 4051e562c..83572b6d2 100644 --- a/internal/notifier/util_test.go +++ b/internal/notifier/util_test.go @@ -19,14 +19,15 @@ package notifier import ( "testing" - eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" ) func TestUtil_NameAndDescription(t *testing.T) { event := eventv1.Event{ - InvolvedObject: v1.ObjectReference{ + InvolvedObject: corev1.ObjectReference{ Kind: "Kustomization", Name: "gitops-system", }, diff --git a/internal/notifier/webex_test.go b/internal/notifier/webex_test.go index e8991e2d5..5a806b657 100644 --- a/internal/notifier/webex_test.go +++ b/internal/notifier/webex_test.go @@ -25,6 +25,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" ) func TestWebex_Post(t *testing.T) { @@ -50,7 +52,7 @@ func TestWebex_PostUpdate(t *testing.T) { require.NoError(t, err) event := testEvent() - event.Metadata["commit_status"] = "update" + event.Metadata[eventv1.MetaCommitStatusKey] = eventv1.MetaCommitStatusUpdateValue err = webex.Post(context.TODO(), event) require.NoError(t, err) } From 012faa2a08363bf9d26791629f917117067fdc4b Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 10 Nov 2022 10:20:48 +0200 Subject: [PATCH 13/26] Log and emit events on successful reconciliation Signed-off-by: Stefan Prodan --- controllers/alert_controller.go | 15 ++++++++++++--- controllers/provider_controller.go | 15 ++++++++++++--- controllers/receiver_controller.go | 21 +++++++++++++++++---- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/controllers/alert_controller.go b/controllers/alert_controller.go index e7905f422..327960c62 100644 --- a/controllers/alert_controller.go +++ b/controllers/alert_controller.go @@ -117,13 +117,23 @@ func (r *AlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu r.Metrics.RecordDuration(ctx, obj, reconcileStart) r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) - // Issue warning event if the reconciliation failed. + // Emit warning event if the reconciliation failed. if retErr != nil { r.Event(obj, corev1.EventTypeWarning, meta.FailedReason, retErr.Error()) } + // Log and emit success event. + if retErr == nil && conditions.IsReady(obj) { + msg := fmt.Sprintf("Reconciliation finished in %s", + time.Since(reconcileStart).String()) + log.Info(msg) + r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) + } + // Patch finalizers, status and conditions. - retErr = r.patch(ctx, obj, patcher) + if err := r.patch(ctx, obj, patcher); err != nil { + retErr = kerrors.NewAggregate([]error{retErr, err}) + } }() if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { @@ -158,7 +168,6 @@ func (r *AlertReconciler) reconcile(ctx context.Context, alert *apiv1.Alert) (ct } conditions.MarkTrue(alert, meta.ReadyCondition, meta.SucceededReason, apiv1.InitializedReason) - ctrl.LoggerFrom(ctx).Info("Alert initialized") return ctrl.Result{}, nil } diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index 68592c073..a553fb94a 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -99,13 +99,23 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r r.Metrics.RecordDuration(ctx, obj, reconcileStart) r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) - // Issue warning event if the reconciliation failed. + // Emit warning event if the reconciliation failed. if retErr != nil { r.Event(obj, corev1.EventTypeWarning, meta.FailedReason, retErr.Error()) } + // Log and emit success event. + if retErr == nil && conditions.IsReady(obj) { + msg := fmt.Sprintf("Reconciliation finished in %s, next run in %s", + time.Since(reconcileStart).String(), obj.Spec.Interval.Duration.String()) + log.Info(msg) + r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) + } + // Patch finalizers, status and conditions. - retErr = r.patch(ctx, obj, patcher) + if err := r.patch(ctx, obj, patcher); err != nil { + retErr = kerrors.NewAggregate([]error{retErr, err}) + } }() if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { @@ -140,7 +150,6 @@ func (r *ProviderReconciler) reconcile(ctx context.Context, obj *apiv1.Provider) } conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, apiv1.InitializedReason) - ctrl.LoggerFrom(ctx).Info("Provider initialized") return ctrl.Result{RequeueAfter: obj.Spec.Interval.Duration}, nil } diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index 9fdf9cd89..4fad788ce 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -106,13 +106,23 @@ func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r r.Metrics.RecordDuration(ctx, obj, reconcileStart) r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) - // Issue warning event if the reconciliation failed. + // Emit warning event if the reconciliation failed. if retErr != nil { r.Event(obj, corev1.EventTypeWarning, meta.FailedReason, retErr.Error()) } + // Log and emit success event. + if retErr == nil && conditions.IsReady(obj) { + msg := fmt.Sprintf("Reconciliation finished in %s, next run in %s", + time.Since(reconcileStart).String(), obj.Spec.Interval.Duration.String()) + log.Info(msg) + r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) + } + // Patch finalizers, status and conditions. - retErr = r.patch(ctx, obj, patcher) + if err := r.patch(ctx, obj, patcher); err != nil { + retErr = kerrors.NewAggregate([]error{retErr, err}) + } }() if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { @@ -145,6 +155,7 @@ func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *apiv1.Receiver) token, err := r.token(ctx, obj) if err != nil { conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.TokenNotFoundReason, err.Error()) + obj.Status.URL = "" return ctrl.Result{Requeue: true}, err } @@ -153,9 +164,11 @@ func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *apiv1.Receiver) // Mark the resource as ready and set the URL conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, msg) - obj.Status.URL = receiverURL - ctrl.LoggerFrom(ctx).Info(msg) + if obj.Status.URL != receiverURL { + obj.Status.URL = receiverURL + ctrl.LoggerFrom(ctx).Info(msg) + } return ctrl.Result{RequeueAfter: obj.Spec.Interval.Duration}, nil } From 70c678f691b02f894ddd27377bd2057f5b487da5 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 11 Nov 2022 15:45:53 +0200 Subject: [PATCH 14/26] Make interval optional Signed-off-by: Stefan Prodan --- api/v1beta2/provider_types.go | 2 +- api/v1beta2/receiver_types.go | 2 +- .../notification.toolkit.fluxcd.io_providers.yaml | 1 - .../notification.toolkit.fluxcd.io_receivers.yaml | 1 - controllers/alert_controller.go | 10 +++++----- controllers/provider_controller.go | 10 +++++----- controllers/receiver_controller.go | 10 +++++----- docs/api/notification.md | 4 ++++ 8 files changed, 21 insertions(+), 19 deletions(-) diff --git a/api/v1beta2/provider_types.go b/api/v1beta2/provider_types.go index 57a30ee17..fe4eca20c 100644 --- a/api/v1beta2/provider_types.go +++ b/api/v1beta2/provider_types.go @@ -59,7 +59,7 @@ type ProviderSpec struct { // +kubebuilder:default="10m" // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" - // +required + // +optional Interval metav1.Duration `json:"interval"` // Channel specifies the destination channel where events should be posted. diff --git a/api/v1beta2/receiver_types.go b/api/v1beta2/receiver_types.go index cdaaaef92..def029674 100644 --- a/api/v1beta2/receiver_types.go +++ b/api/v1beta2/receiver_types.go @@ -53,7 +53,7 @@ type ReceiverSpec struct { // +kubebuilder:default="10m" // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" - // +required + // +optional Interval metav1.Duration `json:"interval"` // Events specifies the list of event types to handle, diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index b4863f4cc..55d1c3d4e 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -308,7 +308,6 @@ spec: maxLength: 2048 type: string required: - - interval - type type: object status: diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml index 5a09643ec..db5956c42 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml @@ -336,7 +336,6 @@ spec: - acr type: string required: - - interval - resources - type type: object diff --git a/controllers/alert_controller.go b/controllers/alert_controller.go index 327960c62..5aec2c476 100644 --- a/controllers/alert_controller.go +++ b/controllers/alert_controller.go @@ -112,6 +112,11 @@ func (r *AlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu patcher := patch.NewSerialPatcher(obj, r.Client) defer func() { + // Patch finalizers, status and conditions. + if err := r.patch(ctx, obj, patcher); err != nil { + retErr = kerrors.NewAggregate([]error{retErr, err}) + } + // Record Prometheus metrics. r.Metrics.RecordReadiness(ctx, obj) r.Metrics.RecordDuration(ctx, obj, reconcileStart) @@ -129,11 +134,6 @@ func (r *AlertReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu log.Info(msg) r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) } - - // Patch finalizers, status and conditions. - if err := r.patch(ctx, obj, patcher); err != nil { - retErr = kerrors.NewAggregate([]error{retErr, err}) - } }() if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index a553fb94a..94556f314 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -94,6 +94,11 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r patcher := patch.NewSerialPatcher(obj, r.Client) defer func() { + // Patch finalizers, status and conditions. + if err := r.patch(ctx, obj, patcher); err != nil { + retErr = kerrors.NewAggregate([]error{retErr, err}) + } + // Record Prometheus metrics. r.Metrics.RecordReadiness(ctx, obj) r.Metrics.RecordDuration(ctx, obj, reconcileStart) @@ -111,11 +116,6 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r log.Info(msg) r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) } - - // Patch finalizers, status and conditions. - if err := r.patch(ctx, obj, patcher); err != nil { - retErr = kerrors.NewAggregate([]error{retErr, err}) - } }() if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { diff --git a/controllers/receiver_controller.go b/controllers/receiver_controller.go index 4fad788ce..f2b20ade1 100644 --- a/controllers/receiver_controller.go +++ b/controllers/receiver_controller.go @@ -101,6 +101,11 @@ func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r patcher := patch.NewSerialPatcher(obj, r.Client) defer func() { + // Patch finalizers, status and conditions. + if err := r.patch(ctx, obj, patcher); err != nil { + retErr = kerrors.NewAggregate([]error{retErr, err}) + } + // Record Prometheus metrics. r.Metrics.RecordReadiness(ctx, obj) r.Metrics.RecordDuration(ctx, obj, reconcileStart) @@ -118,11 +123,6 @@ func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r log.Info(msg) r.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg) } - - // Patch finalizers, status and conditions. - if err := r.patch(ctx, obj, patcher); err != nil { - retErr = kerrors.NewAggregate([]error{retErr, err}) - } }() if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) { diff --git a/docs/api/notification.md b/docs/api/notification.md index 95891a664..082fa17e8 100644 --- a/docs/api/notification.md +++ b/docs/api/notification.md @@ -247,6 +247,7 @@ Kubernetes meta/v1.Duration
+(Optional)

Interval at which to reconcile the Provider with its Secret references.

+(Optional)

Interval at which to reconcile the Receiver with its Secret references.

+(Optional)

Interval at which to reconcile the Provider with its Secret references.

+(Optional)

Interval at which to reconcile the Receiver with its Secret references.

(Optional)

URL is the generated incoming webhook address in the format +of ‘/hook/sha256sum(token+name+namespace)’. +Deprecated: Replaced by WebhookPath.

+
+webhookPath
+ +string + +
+(Optional) +

WebhookPath is the generated incoming webhook address in the format of ‘/hook/sha256sum(token+name+namespace)’.