Refactor reconcilers and introduce v1beta2 API
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
parent
188aad73da
commit
974a77da00
4
Makefile
4
Makefile
|
|
@ -2,7 +2,7 @@
|
|||
IMG ?= fluxcd/notification-controller:latest
|
||||
# Produce CRDs that work back to Kubernetes 1.16
|
||||
CRD_OPTIONS ?= crd:crdVersions=v1
|
||||
SOURCE_VER ?= v0.24.0
|
||||
SOURCE_VER ?= v0.31.0
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
|
|
@ -76,7 +76,7 @@ manifests: controller-gen
|
|||
|
||||
# Generate API reference documentation
|
||||
api-docs: gen-crd-api-reference-docs
|
||||
$(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta1 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/notification.md
|
||||
$(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta2 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/notification.md
|
||||
|
||||
# Run go mod tidy
|
||||
tidy:
|
||||
|
|
|
|||
9
PROJECT
9
PROJECT
|
|
@ -10,4 +10,13 @@ resources:
|
|||
- group: notification
|
||||
kind: Receiver
|
||||
version: v1beta1
|
||||
- group: notification
|
||||
kind: Provider
|
||||
version: v1beta2
|
||||
- group: notification
|
||||
kind: Alert
|
||||
version: v1beta2
|
||||
- group: notification
|
||||
kind: Receiver
|
||||
version: v1beta2
|
||||
version: "2"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
|
|
@ -16,26 +32,127 @@ import (
|
|||
prommetrics "github.com/slok/go-http-metrics/metrics/prometheus"
|
||||
"github.com/slok/go-http-metrics/middleware"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/runtime/conditions"
|
||||
|
||||
notifyv1 "github.com/fluxcd/notification-controller/api/v1beta1"
|
||||
apiv1 "github.com/fluxcd/notification-controller/api/v1beta2"
|
||||
"github.com/fluxcd/notification-controller/internal/server"
|
||||
)
|
||||
|
||||
func TestEventHandler(t *testing.T) {
|
||||
// randomize var? create http server here?
|
||||
func TestAlertReconciler_Reconcile(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
timeout := 5 * time.Second
|
||||
resultA := &apiv1.Alert{}
|
||||
namespaceName := "alert-" + randStringRunes(5)
|
||||
providerName := "provider-" + randStringRunes(5)
|
||||
|
||||
g.Expect(createNamespace(namespaceName)).NotTo(HaveOccurred(), "failed to create test namespace")
|
||||
|
||||
provider := &apiv1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: providerName,
|
||||
Namespace: namespaceName,
|
||||
},
|
||||
Spec: apiv1.ProviderSpec{
|
||||
Type: "generic",
|
||||
Address: "https://webhook.internal",
|
||||
},
|
||||
}
|
||||
|
||||
alert := &apiv1.Alert{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("alert-%s", randStringRunes(5)),
|
||||
Namespace: namespaceName,
|
||||
},
|
||||
Spec: apiv1.AlertSpec{
|
||||
ProviderRef: meta.LocalObjectReference{
|
||||
Name: providerName,
|
||||
},
|
||||
EventSeverity: "info",
|
||||
EventSources: []apiv1.CrossNamespaceObjectReference{
|
||||
{
|
||||
Kind: "Bucket",
|
||||
Name: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
g.Expect(k8sClient.Create(context.Background(), alert)).To(Succeed())
|
||||
|
||||
t.Run("fails with provider not found error", func(t *testing.T) {
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA)
|
||||
return conditions.Has(resultA, meta.ReadyCondition)
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
|
||||
g.Expect(conditions.IsReady(resultA)).To(BeFalse())
|
||||
g.Expect(conditions.GetReason(resultA, meta.ReadyCondition)).To(BeIdenticalTo(apiv1.ValidationFailedReason))
|
||||
g.Expect(conditions.GetMessage(resultA, meta.ReadyCondition)).To(ContainSubstring(providerName))
|
||||
|
||||
g.Expect(conditions.Has(resultA, meta.ReconcilingCondition)).To(BeTrue())
|
||||
g.Expect(conditions.GetReason(resultA, meta.ReconcilingCondition)).To(BeIdenticalTo(apiv1.ProgressingWithRetryReason))
|
||||
g.Expect(conditions.GetObservedGeneration(resultA, meta.ReconcilingCondition)).To(BeIdenticalTo(resultA.Generation))
|
||||
g.Expect(controllerutil.ContainsFinalizer(resultA, apiv1.NotificationFinalizer)).To(BeTrue())
|
||||
})
|
||||
|
||||
t.Run("recovers when provider exists", func(t *testing.T) {
|
||||
g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA)
|
||||
return conditions.IsReady(resultA)
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
|
||||
g.Expect(conditions.GetObservedGeneration(resultA, meta.ReadyCondition)).To(BeIdenticalTo(resultA.Generation))
|
||||
g.Expect(resultA.Status.ObservedGeneration).To(BeIdenticalTo(resultA.Generation))
|
||||
g.Expect(conditions.Has(resultA, meta.ReconcilingCondition)).To(BeFalse())
|
||||
})
|
||||
|
||||
t.Run("handles reconcileAt", func(t *testing.T) {
|
||||
reconcileRequestAt := metav1.Now().String()
|
||||
resultA.SetAnnotations(map[string]string{
|
||||
meta.ReconcileRequestAnnotation: reconcileRequestAt,
|
||||
})
|
||||
g.Expect(k8sClient.Update(context.Background(), resultA)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA)
|
||||
return resultA.Status.LastHandledReconcileAt == reconcileRequestAt
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
})
|
||||
|
||||
t.Run("finalizes suspended object", func(t *testing.T) {
|
||||
resultA.Spec.Suspend = true
|
||||
g.Expect(k8sClient.Update(context.Background(), resultA)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA)
|
||||
return resultA.Spec.Suspend == true
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
|
||||
g.Expect(k8sClient.Delete(context.Background(), resultA)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), resultA)
|
||||
return apierrors.IsNotFound(err)
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlertReconciler_EventHandler(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
var (
|
||||
namespace = "events-" + randStringRunes(5)
|
||||
req *http.Request
|
||||
provider *notifyv1.Provider
|
||||
provider *apiv1.Provider
|
||||
)
|
||||
g.Expect(createNamespace(namespace)).NotTo(HaveOccurred(), "failed to create test namespace")
|
||||
|
||||
|
|
@ -67,19 +184,19 @@ func TestEventHandler(t *testing.T) {
|
|||
Name: fmt.Sprintf("provider-%s", randStringRunes(5)),
|
||||
Namespace: namespace,
|
||||
}
|
||||
provider = ¬ifyv1.Provider{
|
||||
provider = &apiv1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: providerKey.Name,
|
||||
Namespace: providerKey.Namespace,
|
||||
},
|
||||
Spec: notifyv1.ProviderSpec{
|
||||
Spec: apiv1.ProviderSpec{
|
||||
Type: "generic",
|
||||
Address: rcvServer.URL,
|
||||
},
|
||||
}
|
||||
g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed())
|
||||
g.Eventually(func() bool {
|
||||
var obj notifyv1.Provider
|
||||
var obj apiv1.Provider
|
||||
g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), &obj))
|
||||
return conditions.IsReady(&obj)
|
||||
}, 30*time.Second, time.Second).Should(BeTrue())
|
||||
|
|
@ -105,17 +222,17 @@ func TestEventHandler(t *testing.T) {
|
|||
Namespace: namespace,
|
||||
}
|
||||
|
||||
alert := ¬ifyv1.Alert{
|
||||
alert := &apiv1.Alert{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: alertKey.Name,
|
||||
Namespace: alertKey.Namespace,
|
||||
},
|
||||
Spec: notifyv1.AlertSpec{
|
||||
Spec: apiv1.AlertSpec{
|
||||
ProviderRef: meta.LocalObjectReference{
|
||||
Name: providerKey.Name,
|
||||
},
|
||||
EventSeverity: "info",
|
||||
EventSources: []notifyv1.CrossNamespaceObjectReference{
|
||||
EventSources: []apiv1.CrossNamespaceObjectReference{
|
||||
{
|
||||
Kind: "Bucket",
|
||||
Name: "hyacinth",
|
||||
|
|
@ -149,7 +266,7 @@ func TestEventHandler(t *testing.T) {
|
|||
|
||||
// wait for controller to mark the alert as ready
|
||||
g.Eventually(func() bool {
|
||||
var obj notifyv1.Alert
|
||||
var obj apiv1.Alert
|
||||
g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(alert), &obj))
|
||||
return conditions.IsReady(&obj)
|
||||
}, 30*time.Second, time.Second).Should(BeTrue())
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,25 @@
|
|||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -13,21 +27,159 @@ import (
|
|||
prommetrics "github.com/slok/go-http-metrics/metrics/prometheus"
|
||||
"github.com/slok/go-http-metrics/middleware"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/runtime/conditions"
|
||||
"github.com/fluxcd/pkg/ssa"
|
||||
|
||||
notifyv1 "github.com/fluxcd/notification-controller/api/v1beta1"
|
||||
apiv1 "github.com/fluxcd/notification-controller/api/v1beta2"
|
||||
"github.com/fluxcd/notification-controller/internal/server"
|
||||
)
|
||||
|
||||
func TestReceiverHandler(t *testing.T) {
|
||||
func TestReceiverReconciler_Reconcile(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
timeout := 5 * time.Second
|
||||
resultR := &apiv1.Receiver{}
|
||||
namespaceName := "receiver-" + randStringRunes(5)
|
||||
secretName := "secret-" + randStringRunes(5)
|
||||
|
||||
g.Expect(createNamespace(namespaceName)).NotTo(HaveOccurred(), "failed to create test namespace")
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: namespaceName,
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"token": "test",
|
||||
},
|
||||
}
|
||||
g.Expect(k8sClient.Create(context.Background(), secret)).To(Succeed())
|
||||
|
||||
receiverKey := types.NamespacedName{
|
||||
Name: fmt.Sprintf("receiver-%s", randStringRunes(5)),
|
||||
Namespace: namespaceName,
|
||||
}
|
||||
receiver := &apiv1.Receiver{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: receiverKey.Name,
|
||||
Namespace: receiverKey.Namespace,
|
||||
},
|
||||
Spec: apiv1.ReceiverSpec{
|
||||
Type: "generic",
|
||||
Events: []string{"push"},
|
||||
Resources: []apiv1.CrossNamespaceObjectReference{
|
||||
{
|
||||
Name: "podinfo",
|
||||
Kind: "GitRepository",
|
||||
},
|
||||
},
|
||||
SecretRef: meta.LocalObjectReference{
|
||||
Name: secretName,
|
||||
},
|
||||
},
|
||||
}
|
||||
g.Expect(k8sClient.Create(context.Background(), receiver)).To(Succeed())
|
||||
|
||||
t.Run("reports ready status", func(t *testing.T) {
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)
|
||||
return resultR.Status.ObservedGeneration == resultR.Generation
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
|
||||
g.Expect(conditions.IsReady(resultR)).To(BeTrue())
|
||||
g.Expect(conditions.GetReason(resultR, meta.ReadyCondition)).To(BeIdenticalTo(meta.SucceededReason))
|
||||
|
||||
g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeFalse())
|
||||
g.Expect(controllerutil.ContainsFinalizer(resultR, apiv1.NotificationFinalizer)).To(BeTrue())
|
||||
})
|
||||
|
||||
t.Run("fails with secret not found error", func(t *testing.T) {
|
||||
g.Expect(k8sClient.Delete(context.Background(), secret)).To(Succeed())
|
||||
|
||||
reconcileRequestAt := metav1.Now().String()
|
||||
resultR.SetAnnotations(map[string]string{
|
||||
meta.ReconcileRequestAnnotation: reconcileRequestAt,
|
||||
})
|
||||
g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)
|
||||
return !conditions.IsReady(resultR)
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
|
||||
g.Expect(conditions.GetReason(resultR, meta.ReadyCondition)).To(BeIdenticalTo(apiv1.TokenNotFoundReason))
|
||||
g.Expect(conditions.GetMessage(resultR, meta.ReadyCondition)).To(ContainSubstring(secretName))
|
||||
|
||||
g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeTrue())
|
||||
g.Expect(conditions.GetReason(resultR, meta.ReconcilingCondition)).To(BeIdenticalTo(apiv1.ProgressingWithRetryReason))
|
||||
g.Expect(conditions.GetObservedGeneration(resultR, meta.ReconcilingCondition)).To(BeIdenticalTo(resultR.Generation))
|
||||
})
|
||||
|
||||
t.Run("recovers when secret exists", func(t *testing.T) {
|
||||
newSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: namespaceName,
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"token": "test",
|
||||
},
|
||||
}
|
||||
g.Expect(k8sClient.Create(context.Background(), newSecret)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)
|
||||
return conditions.IsReady(resultR)
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
|
||||
g.Expect(conditions.GetObservedGeneration(resultR, meta.ReadyCondition)).To(BeIdenticalTo(resultR.Generation))
|
||||
g.Expect(resultR.Status.ObservedGeneration).To(BeIdenticalTo(resultR.Generation))
|
||||
g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeFalse())
|
||||
})
|
||||
|
||||
t.Run("handles reconcileAt", func(t *testing.T) {
|
||||
reconcileRequestAt := metav1.Now().String()
|
||||
resultR.SetAnnotations(map[string]string{
|
||||
meta.ReconcileRequestAnnotation: reconcileRequestAt,
|
||||
})
|
||||
g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)
|
||||
return resultR.Status.LastHandledReconcileAt == reconcileRequestAt
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
})
|
||||
|
||||
t.Run("finalizes suspended object", func(t *testing.T) {
|
||||
resultR.Spec.Suspend = true
|
||||
g.Expect(k8sClient.Update(context.Background(), resultR)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)
|
||||
return resultR.Spec.Suspend == true
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
|
||||
g.Expect(k8sClient.Delete(context.Background(), resultR)).To(Succeed())
|
||||
|
||||
g.Eventually(func() bool {
|
||||
err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)
|
||||
return apierrors.IsNotFound(err)
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
})
|
||||
}
|
||||
|
||||
func TestReceiverReconciler_EventHandler(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
timeout := 30 * time.Second
|
||||
resultR := &apiv1.Receiver{}
|
||||
|
||||
receiverServer := server.NewReceiverServer("127.0.0.1:56788", logf.Log, k8sClient)
|
||||
receiverMdlw := middleware.New(middleware.Config{
|
||||
|
|
@ -77,15 +229,15 @@ func TestReceiverHandler(t *testing.T) {
|
|||
Name: fmt.Sprintf("test-receiver-%s", randStringRunes(5)),
|
||||
}
|
||||
|
||||
receiver := ¬ifyv1.Receiver{
|
||||
receiver := &apiv1.Receiver{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: receiverKey.Name,
|
||||
Namespace: receiverKey.Namespace,
|
||||
},
|
||||
Spec: notifyv1.ReceiverSpec{
|
||||
Spec: apiv1.ReceiverSpec{
|
||||
Type: "generic",
|
||||
Events: []string{"pull"},
|
||||
Resources: []notifyv1.CrossNamespaceObjectReference{
|
||||
Resources: []apiv1.CrossNamespaceObjectReference{
|
||||
{
|
||||
Name: "podinfo",
|
||||
Kind: "GitRepository",
|
||||
|
|
@ -101,44 +253,39 @@ func TestReceiverHandler(t *testing.T) {
|
|||
|
||||
address := fmt.Sprintf("/hook/%s", sha256sum(token+receiverKey.Name+receiverKey.Namespace))
|
||||
|
||||
var rcvrObj notifyv1.Receiver
|
||||
g.Eventually(func() bool {
|
||||
g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), &rcvrObj))
|
||||
return rcvrObj.Status.URL == address
|
||||
}, 30*time.Second, time.Second).Should(BeTrue())
|
||||
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())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ‘info’ 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. ‘push’ for GitHub or ‘Push Hook’ 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 ‘info’ 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 “In”, and the values array contains only “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. ‘push’ for GitHub or ‘Push Hook’ 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">
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
39
main.go
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Flux authors
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
@ -21,13 +21,6 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/pkg/runtime/acl"
|
||||
"github.com/fluxcd/pkg/runtime/client"
|
||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||
"github.com/fluxcd/pkg/runtime/leaderelection"
|
||||
"github.com/fluxcd/pkg/runtime/logger"
|
||||
"github.com/fluxcd/pkg/runtime/pprof"
|
||||
"github.com/fluxcd/pkg/runtime/probes"
|
||||
"github.com/sethvargo/go-limiter/memorystore"
|
||||
prommetrics "github.com/slok/go-http-metrics/metrics/prometheus"
|
||||
"github.com/slok/go-http-metrics/middleware"
|
||||
|
|
@ -38,7 +31,15 @@ import (
|
|||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
|
||||
|
||||
"github.com/fluxcd/notification-controller/api/v1beta1"
|
||||
"github.com/fluxcd/pkg/runtime/acl"
|
||||
"github.com/fluxcd/pkg/runtime/client"
|
||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||
"github.com/fluxcd/pkg/runtime/leaderelection"
|
||||
"github.com/fluxcd/pkg/runtime/logger"
|
||||
"github.com/fluxcd/pkg/runtime/pprof"
|
||||
"github.com/fluxcd/pkg/runtime/probes"
|
||||
|
||||
apiv1 "github.com/fluxcd/notification-controller/api/v1beta2"
|
||||
"github.com/fluxcd/notification-controller/controllers"
|
||||
"github.com/fluxcd/notification-controller/internal/server"
|
||||
// +kubebuilder:scaffold:imports
|
||||
|
|
@ -54,7 +55,7 @@ var (
|
|||
func init() {
|
||||
_ = clientgoscheme.AddToScheme(scheme)
|
||||
|
||||
_ = v1beta1.AddToScheme(scheme)
|
||||
_ = apiv1.AddToScheme(scheme)
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
|
|
@ -123,9 +124,9 @@ func main() {
|
|||
metricsH := helper.MustMakeMetrics(mgr)
|
||||
|
||||
if err = (&controllers.ProviderReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Metrics: metricsH,
|
||||
Client: mgr.GetClient(),
|
||||
ControllerName: controllerName,
|
||||
Metrics: metricsH,
|
||||
}).SetupWithManagerAndOptions(mgr, controllers.ProviderReconcilerOptions{
|
||||
MaxConcurrentReconciles: concurrent,
|
||||
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
|
||||
|
|
@ -134,9 +135,9 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
if err = (&controllers.AlertReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Metrics: metricsH,
|
||||
Client: mgr.GetClient(),
|
||||
ControllerName: controllerName,
|
||||
Metrics: metricsH,
|
||||
}).SetupWithManagerAndOptions(mgr, controllers.AlertReconcilerOptions{
|
||||
MaxConcurrentReconciles: concurrent,
|
||||
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
|
||||
|
|
@ -145,9 +146,9 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
if err = (&controllers.ReceiverReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Metrics: metricsH,
|
||||
Client: mgr.GetClient(),
|
||||
ControllerName: controllerName,
|
||||
Metrics: metricsH,
|
||||
}).SetupWithManagerAndOptions(mgr, controllers.ReceiverReconcilerOptions{
|
||||
MaxConcurrentReconciles: concurrent,
|
||||
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
|
||||
|
|
|
|||
Loading…
Reference in New Issue