Refactor reconcilers and introduce v1beta2 API

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
Stefan Prodan 2022-10-27 15:41:08 +03:00 committed by Hidde Beydals
parent 188aad73da
commit 974a77da00
29 changed files with 2546 additions and 441 deletions

View File

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

View File

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

View File

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

117
api/v1beta2/alert_types.go Normal file
View File

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

View File

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

20
api/v1beta2/doc.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
},
},
}
// Record Prometheus metrics.
r.Metrics.RecordReadiness(ctx, obj)
r.Metrics.RecordDuration(ctx, obj, reconcileStart)
r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend)
if retErr == nil && (result.IsZero() || !result.Requeue) {
conditions.Delete(alert, meta.ReconcilingCondition)
patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{})
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
}

View File

@ -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 = &notifyv1.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 := &notifyv1.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())

View File

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

View File

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

View File

@ -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,104 +56,15 @@ type ReceiverReconcilerOptions struct {
RateLimiter ratelimiter.RateLimiter
}
// +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=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
// +kubebuilder:rbac:groups=image.fluxcd.io,resources=imagerepositories/status,verbs=get
// +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()
log := ctrl.LoggerFrom(ctx)
receiver := &v1beta1.Receiver{}
if err := r.Get(ctx, req.NamespacedName, receiver); 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 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)
}()
if !controllerutil.ContainsFinalizer(receiver, v1beta1.NotificationFinalizer) {
controllerutil.AddFinalizer(receiver, v1beta1.NotificationFinalizer)
result = ctrl.Result{Requeue: true}
return
}
if !receiver.ObjectMeta.DeletionTimestamp.IsZero() {
controllerutil.RemoveFinalizer(receiver, v1beta1.NotificationFinalizer)
result = ctrl.Result{}
return
}
return r.reconcile(ctx, receiver)
}
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(&v1beta1.Receiver{}).
For(&apiv1.Receiver{}, builder.WithPredicates(
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}),
)).
WithOptions(controller.Options{
MaxConcurrentReconciles: opts.MaxConcurrentReconciles,
RateLimiter: opts.RateLimiter,
@ -159,31 +73,139 @@ func (r *ReceiverReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts R
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
// +kubebuilder:rbac:groups=image.fluxcd.io,resources=imagerepositories/status,verbs=get
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
reconcileStart := time.Now()
log := ctrl.LoggerFrom(ctx)
obj := &apiv1.Receiver{}
if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Initialize the runtime patcher with the current version of the object.
patcher := patch.NewSerialPatcher(obj, r.Client)
defer func() {
// 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(obj, apiv1.NotificationFinalizer) {
controllerutil.AddFinalizer(obj, apiv1.NotificationFinalizer)
result = ctrl.Result{Requeue: true}
return
}
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
controllerutil.RemoveFinalizer(obj, apiv1.NotificationFinalizer)
result = ctrl.Result{}
return
}
// 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)
}
// 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(),

View File

@ -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 := &notifyv1.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())
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())
// 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())
g.Expect(resultR.Status.URL).To(BeIdenticalTo(address))
})
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())
}
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
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())
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())
})
}

View File

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

View File

@ -2,20 +2,20 @@
<p>Packages:</p>
<ul class="simple">
<li>
<a href="#notification.toolkit.fluxcd.io%2fv1beta1">notification.toolkit.fluxcd.io/v1beta1</a>
<a href="#notification.toolkit.fluxcd.io%2fv1beta2">notification.toolkit.fluxcd.io/v1beta2</a>
</li>
</ul>
<h2 id="notification.toolkit.fluxcd.io/v1beta1">notification.toolkit.fluxcd.io/v1beta1</h2>
<p>Package v1beta1 contains API Schema definitions for the notification v1beta1 API group</p>
<h2 id="notification.toolkit.fluxcd.io/v1beta2">notification.toolkit.fluxcd.io/v1beta2</h2>
<p>Package v1beta2 contains API Schema definitions for the notification v1beta2 API group</p>
Resource Types:
<ul class="simple"><li>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Alert">Alert</a>
<a href="#notification.toolkit.fluxcd.io/v1beta2.Alert">Alert</a>
</li><li>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Provider">Provider</a>
<a href="#notification.toolkit.fluxcd.io/v1beta2.Provider">Provider</a>
</li><li>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Receiver">Receiver</a>
<a href="#notification.toolkit.fluxcd.io/v1beta2.Receiver">Receiver</a>
</li></ul>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.Alert">Alert
<h3 id="notification.toolkit.fluxcd.io/v1beta2.Alert">Alert
</h3>
<p>Alert is the Schema for the alerts API</p>
<div class="md-typeset__scrollwrap">
@ -33,7 +33,7 @@ Resource Types:
<code>apiVersion</code><br>
string</td>
<td>
<code>notification.toolkit.fluxcd.io/v1beta1</code>
<code>notification.toolkit.fluxcd.io/v1beta2</code>
</td>
</tr>
<tr>
@ -63,7 +63,7 @@ Refer to the Kubernetes API documentation for the fields of the
<td>
<code>spec</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.AlertSpec">
<a href="#notification.toolkit.fluxcd.io/v1beta2.AlertSpec">
AlertSpec
</a>
</em>
@ -102,7 +102,7 @@ If set to &lsquo;info&rsquo; no events will be filtered.</p>
<td>
<code>eventSources</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.CrossNamespaceObjectReference">
<a href="#notification.toolkit.fluxcd.io/v1beta2.CrossNamespaceObjectReference">
[]CrossNamespaceObjectReference
</a>
</em>
@ -155,7 +155,7 @@ Defaults to false.</p>
<td>
<code>status</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.AlertStatus">
<a href="#notification.toolkit.fluxcd.io/v1beta2.AlertStatus">
AlertStatus
</a>
</em>
@ -167,7 +167,7 @@ AlertStatus
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.Provider">Provider
<h3 id="notification.toolkit.fluxcd.io/v1beta2.Provider">Provider
</h3>
<p>Provider is the Schema for the providers API</p>
<div class="md-typeset__scrollwrap">
@ -185,7 +185,7 @@ AlertStatus
<code>apiVersion</code><br>
string</td>
<td>
<code>notification.toolkit.fluxcd.io/v1beta1</code>
<code>notification.toolkit.fluxcd.io/v1beta2</code>
</td>
</tr>
<tr>
@ -215,7 +215,7 @@ Refer to the Kubernetes API documentation for the fields of the
<td>
<code>spec</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.ProviderSpec">
<a href="#notification.toolkit.fluxcd.io/v1beta2.ProviderSpec">
ProviderSpec
</a>
</em>
@ -347,7 +347,7 @@ Defaults to false.</p>
<td>
<code>status</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.ProviderStatus">
<a href="#notification.toolkit.fluxcd.io/v1beta2.ProviderStatus">
ProviderStatus
</a>
</em>
@ -359,7 +359,7 @@ ProviderStatus
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.Receiver">Receiver
<h3 id="notification.toolkit.fluxcd.io/v1beta2.Receiver">Receiver
</h3>
<p>Receiver is the Schema for the receivers API</p>
<div class="md-typeset__scrollwrap">
@ -377,7 +377,7 @@ ProviderStatus
<code>apiVersion</code><br>
string</td>
<td>
<code>notification.toolkit.fluxcd.io/v1beta1</code>
<code>notification.toolkit.fluxcd.io/v1beta2</code>
</td>
</tr>
<tr>
@ -407,7 +407,7 @@ Refer to the Kubernetes API documentation for the fields of the
<td>
<code>spec</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.ReceiverSpec">
<a href="#notification.toolkit.fluxcd.io/v1beta2.ReceiverSpec">
ReceiverSpec
</a>
</em>
@ -445,7 +445,7 @@ e.g. &lsquo;push&rsquo; for GitHub or &lsquo;Push Hook&rsquo; for GitLab.</p>
<td>
<code>resources</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.CrossNamespaceObjectReference">
<a href="#notification.toolkit.fluxcd.io/v1beta2.CrossNamespaceObjectReference">
[]CrossNamespaceObjectReference
</a>
</em>
@ -488,7 +488,7 @@ Defaults to false.</p>
<td>
<code>status</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.ReceiverStatus">
<a href="#notification.toolkit.fluxcd.io/v1beta2.ReceiverStatus">
ReceiverStatus
</a>
</em>
@ -500,11 +500,11 @@ ReceiverStatus
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.AlertSpec">AlertSpec
<h3 id="notification.toolkit.fluxcd.io/v1beta2.AlertSpec">AlertSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Alert">Alert</a>)
<a href="#notification.toolkit.fluxcd.io/v1beta2.Alert">Alert</a>)
</p>
<p>AlertSpec defines an alerting rule for events involving a list of objects</p>
<div class="md-typeset__scrollwrap">
@ -547,7 +547,7 @@ If set to &lsquo;info&rsquo; no events will be filtered.</p>
<td>
<code>eventSources</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.CrossNamespaceObjectReference">
<a href="#notification.toolkit.fluxcd.io/v1beta2.CrossNamespaceObjectReference">
[]CrossNamespaceObjectReference
</a>
</em>
@ -597,11 +597,11 @@ Defaults to false.</p>
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.AlertStatus">AlertStatus
<h3 id="notification.toolkit.fluxcd.io/v1beta2.AlertStatus">AlertStatus
</h3>
<p>
(<em>Appears on:</em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Alert">Alert</a>)
<a href="#notification.toolkit.fluxcd.io/v1beta2.Alert">Alert</a>)
</p>
<p>AlertStatus defines the observed state of Alert</p>
<div class="md-typeset__scrollwrap">
@ -616,6 +616,21 @@ Defaults to false.</p>
<tbody>
<tr>
<td>
<code>ReconcileRequestStatus</code><br>
<em>
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
</a>
</em>
</td>
<td>
<p>
(Members of <code>ReconcileRequestStatus</code> are embedded into this type.)
</p>
</td>
</tr>
<tr>
<td>
<code>conditions</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#condition-v1-meta">
@ -643,12 +658,12 @@ int64
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.CrossNamespaceObjectReference">CrossNamespaceObjectReference
<h3 id="notification.toolkit.fluxcd.io/v1beta2.CrossNamespaceObjectReference">CrossNamespaceObjectReference
</h3>
<p>
(<em>Appears on:</em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.AlertSpec">AlertSpec</a>,
<a href="#notification.toolkit.fluxcd.io/v1beta1.ReceiverSpec">ReceiverSpec</a>)
<a href="#notification.toolkit.fluxcd.io/v1beta2.AlertSpec">AlertSpec</a>,
<a href="#notification.toolkit.fluxcd.io/v1beta2.ReceiverSpec">ReceiverSpec</a>)
</p>
<p>CrossNamespaceObjectReference contains enough information to let you locate the
typed referenced object at cluster level</p>
@ -726,11 +741,11 @@ operator is &ldquo;In&rdquo;, and the values array contains only &ldquo;value&rd
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.ProviderSpec">ProviderSpec
<h3 id="notification.toolkit.fluxcd.io/v1beta2.ProviderSpec">ProviderSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Provider">Provider</a>)
<a href="#notification.toolkit.fluxcd.io/v1beta2.Provider">Provider</a>)
</p>
<p>ProviderSpec defines the desired state of Provider</p>
<div class="md-typeset__scrollwrap">
@ -863,11 +878,11 @@ Defaults to false.</p>
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.ProviderStatus">ProviderStatus
<h3 id="notification.toolkit.fluxcd.io/v1beta2.ProviderStatus">ProviderStatus
</h3>
<p>
(<em>Appears on:</em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Provider">Provider</a>)
<a href="#notification.toolkit.fluxcd.io/v1beta2.Provider">Provider</a>)
</p>
<p>ProviderStatus defines the observed state of Provider</p>
<div class="md-typeset__scrollwrap">
@ -882,14 +897,17 @@ Defaults to false.</p>
<tbody>
<tr>
<td>
<code>observedGeneration</code><br>
<code>ReconcileRequestStatus</code><br>
<em>
int64
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ObservedGeneration is the last reconciled generation.</p>
<p>
(Members of <code>ReconcileRequestStatus</code> are embedded into this type.)
</p>
</td>
</tr>
<tr>
@ -905,15 +923,27 @@ int64
<em>(Optional)</em>
</td>
</tr>
<tr>
<td>
<code>observedGeneration</code><br>
<em>
int64
</em>
</td>
<td>
<em>(Optional)</em>
<p>ObservedGeneration is the last reconciled generation.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.ReceiverSpec">ReceiverSpec
<h3 id="notification.toolkit.fluxcd.io/v1beta2.ReceiverSpec">ReceiverSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Receiver">Receiver</a>)
<a href="#notification.toolkit.fluxcd.io/v1beta2.Receiver">Receiver</a>)
</p>
<p>ReceiverSpec defines the desired state of Receiver</p>
<div class="md-typeset__scrollwrap">
@ -955,7 +985,7 @@ e.g. &lsquo;push&rsquo; for GitHub or &lsquo;Push Hook&rsquo; for GitLab.</p>
<td>
<code>resources</code><br>
<em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.CrossNamespaceObjectReference">
<a href="#notification.toolkit.fluxcd.io/v1beta2.CrossNamespaceObjectReference">
[]CrossNamespaceObjectReference
</a>
</em>
@ -995,11 +1025,11 @@ Defaults to false.</p>
</table>
</div>
</div>
<h3 id="notification.toolkit.fluxcd.io/v1beta1.ReceiverStatus">ReceiverStatus
<h3 id="notification.toolkit.fluxcd.io/v1beta2.ReceiverStatus">ReceiverStatus
</h3>
<p>
(<em>Appears on:</em>
<a href="#notification.toolkit.fluxcd.io/v1beta1.Receiver">Receiver</a>)
<a href="#notification.toolkit.fluxcd.io/v1beta2.Receiver">Receiver</a>)
</p>
<p>ReceiverStatus defines the observed state of Receiver</p>
<div class="md-typeset__scrollwrap">
@ -1014,6 +1044,21 @@ Defaults to false.</p>
<tbody>
<tr>
<td>
<code>ReconcileRequestStatus</code><br>
<em>
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#ReconcileRequestStatus">
github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
</a>
</em>
</td>
<td>
<p>
(Members of <code>ReconcileRequestStatus</code> are embedded into this type.)
</p>
</td>
</tr>
<tr>
<td>
<code>conditions</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#condition-v1-meta">

View File

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

View File

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

View File

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

View File

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

View File

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

39
main.go
View File

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