Implement event recording

- emit Kubernetes events for reconciliation actions
- forward events to notification controller
- remove the Profile API/CRD
This commit is contained in:
stefanprodan 2020-07-01 20:39:48 +03:00
parent 570d2ea05e
commit 2ebd5b6450
20 changed files with 135 additions and 1092 deletions

View File

@ -22,7 +22,6 @@ RUN go mod download
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
COPY internal/ internal/
# build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o kustomize-controller main.go

View File

@ -22,7 +22,7 @@ Features:
* prunes the Kubernetes objects removed from source
* checks the health of the deployed workloads
* runs `Kustomizations` in a specific order, taking into account the depends-on relationship
* reports on Slack or Discord whenever a `Kustomization` status changes
* notifies whenever a `Kustomization` status changes
Specifications:
* [API](docs/spec/v1alpha1/README.md)
@ -262,34 +262,57 @@ set in the Git repository.
### Configure alerting
The kustomize controller can post message to Slack or Discord whenever a kustomization status changes.
The kustomize-controller emits Kubernetes events whenever a kustomization status changes.
Alerting can be configured by creating a profile that targets a list of kustomizations:
You can use the [notification-controller](https://github.com/fluxcd/notification-controller) to forward these events
to Slack, Microsoft Teams, Discord or Rocket chart.
Create a notification provider for Slack:
```yaml
apiVersion: kustomize.fluxcd.io/v1alpha1
kind: Profile
apiVersion: notification.fluxcd.io/v1alpha1
kind: Provider
metadata:
name: default
name: slack
namespace: kustomize-system
spec:
alert:
type: slack
verbosity: info
address: https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
username: kustomize-controller
channel: general
kustomizations:
- '*'
type: slack
channel: alerts
secretRef:
name: slack-url
---
apiVersion: v1
kind: Secret
metadata:
name: slack-url
namespace: kustomize-system
data:
address: <encoded-url>
```
The alert provider type can be: `slack` or `discord` and the verbosity can be set to `info` or `error`.
Create an alert for a list of GitRepositories and Kustomizations:
The `*` wildcard tells the controller to use this profile for all kustomizations that are present
in the same namespace as the profile.
Multiple profiles can be used to send alerts to different channels or Slack organizations.
```yaml
apiVersion: notification.fluxcd.io/v1alpha1
kind: Alert
metadata:
name: on-call
namespace: kustomize-system
spec:
providerRef:
name: slack
eventSeverity: info
eventSources:
- kind: GitRepository
name: podinfo-releases
- kind: Kustomization
name: podinfo-production
```
When the verbosity is set to `error`, the controller will alert on any error encountered during the
Multiple alerts can be used to send notifications to different channels or Slack organizations.
The event severity can be set to `info` or `error`.
When the severity is set to `error`, the controller will alert on any error encountered during the
reconciliation process. This includes kustomize build and validation errors, apply errors and
health check failures.

View File

@ -1,93 +0,0 @@
/*
Copyright 2020 The Flux CD contributors.
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ProfileSpec defines the desired state of Profile
type ProfileSpec struct {
// Alerting configuration of the kustomizations targeted by this profile.
// +optional
Alert *AlertProvider `json:"alert"`
// The list of kustomizations that this profile applies to.
// +required
Kustomizations []string `json:"kustomizations"`
}
// Alert is the configuration of alerting for a specific provider
type AlertProvider struct {
// HTTP(S) webhook address of this provider
// +required
Address string `json:"address"`
// Alert channel for this provider
// +required
Channel string `json:"channel"`
// Bot username for this provider
// +required
Username string `json:"username"`
// Filter alerts based on verbosity level, defaults to ('error').
// +kubebuilder:validation:Enum=info;error
// +optional
Verbosity string `json:"verbosity,omitempty"`
// Type of provider
// +kubebuilder:validation:Enum=slack;discord
// +required
Type string `json:"type"`
}
// ProfileStatus defines the observed state of Profile
type ProfileStatus struct {
// +optional
Conditions []Condition `json:"conditions,omitempty"`
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +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=""
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// Profile is the Schema for the profiles API
type Profile struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ProfileSpec `json:"spec,omitempty"`
Status ProfileStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// ProfileList contains a list of Profile
type ProfileList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Profile `json:"items"`
}
func init() {
SchemeBuilder.Register(&Profile{}, &ProfileList{})
}

View File

@ -25,21 +25,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AlertProvider) DeepCopyInto(out *AlertProvider) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertProvider.
func (in *AlertProvider) DeepCopy() *AlertProvider {
if in == nil {
return nil
}
out := new(AlertProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Condition) DeepCopyInto(out *Condition) {
*out = *in
@ -179,112 +164,6 @@ func (in *KustomizationStatus) DeepCopy() *KustomizationStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Profile) DeepCopyInto(out *Profile) {
*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 Profile.
func (in *Profile) DeepCopy() *Profile {
if in == nil {
return nil
}
out := new(Profile)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Profile) 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 *ProfileList) DeepCopyInto(out *ProfileList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Profile, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileList.
func (in *ProfileList) DeepCopy() *ProfileList {
if in == nil {
return nil
}
out := new(ProfileList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProfileList) 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 *ProfileSpec) DeepCopyInto(out *ProfileSpec) {
*out = *in
if in.Alert != nil {
in, out := &in.Alert, &out.Alert
*out = new(AlertProvider)
**out = **in
}
if in.Kustomizations != nil {
in, out := &in.Kustomizations, &out.Kustomizations
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileSpec.
func (in *ProfileSpec) DeepCopy() *ProfileSpec {
if in == nil {
return nil
}
out := new(ProfileSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProfileStatus) DeepCopyInto(out *ProfileStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileStatus.
func (in *ProfileStatus) DeepCopy() *ProfileStatus {
if in == nil {
return nil
}
out := new(ProfileStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceAccount) DeepCopyInto(out *ServiceAccount) {
*out = *in

View File

@ -1,133 +0,0 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.2.5
creationTimestamp: null
name: profiles.kustomize.fluxcd.io
spec:
additionalPrinterColumns:
- JSONPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- JSONPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
- JSONPath: .metadata.creationTimestamp
name: Age
type: date
group: kustomize.fluxcd.io
names:
kind: Profile
listKind: ProfileList
plural: profiles
singular: profile
scope: Namespaced
subresources:
status: {}
validation:
openAPIV3Schema:
description: Profile is the Schema for the profiles 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: ProfileSpec defines the desired state of Profile
properties:
alert:
description: Alerting configuration of the kustomizations targeted by
this profile.
properties:
address:
description: HTTP(S) webhook address of this provider
type: string
channel:
description: Alert channel for this provider
type: string
type:
description: Type of provider
enum:
- slack
- discord
type: string
username:
description: Bot username for this provider
type: string
verbosity:
description: Filter alerts based on verbosity level, defaults to
('error').
enum:
- info
- error
type: string
required:
- address
- channel
- type
- username
type: object
kustomizations:
description: The list of kustomizations that this profile applies to.
items:
type: string
type: array
required:
- kustomizations
type: object
status:
description: ProfileStatus defines the observed state of Profile
properties:
conditions:
items:
description: Condition contains condition information for a kustomization.
properties:
lastTransitionTime:
description: LastTransitionTime is the timestamp corresponding
to the last status change of this condition.
format: date-time
type: string
message:
description: Message is a human readable description of the details
of the last transition, complementing reason.
type: string
reason:
description: Reason is a brief machine readable explanation for
the condition's last transition.
type: string
status:
description: Status of the condition, one of ('True', 'False',
'Unknown').
type: string
type:
description: Type of the condition, currently ('Ready').
type: string
required:
- status
- type
type: object
type: array
type: object
type: object
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -2,6 +2,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- bases/kustomize.fluxcd.io_kustomizations.yaml
- bases/kustomize.fluxcd.io_profiles.yaml
# +kubebuilder:scaffold:crdkustomizeresource

View File

@ -1,24 +0,0 @@
# permissions for end users to edit profiles.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: profile-editor-role
rules:
- apiGroups:
- kustomize.fluxcd.io
resources:
- profiles
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kustomize.fluxcd.io
resources:
- profiles/status
verbs:
- get

View File

@ -1,20 +0,0 @@
# permissions for end users to view profiles.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: profile-viewer-role
rules:
- apiGroups:
- kustomize.fluxcd.io
resources:
- profiles
verbs:
- get
- list
- watch
- apiGroups:
- kustomize.fluxcd.io
resources:
- profiles/status
verbs:
- get

View File

@ -26,26 +26,6 @@ rules:
- get
- patch
- update
- apiGroups:
- kustomize.fluxcd.io
resources:
- profiles
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kustomize.fluxcd.io
resources:
- profiles/status
verbs:
- get
- patch
- update
- apiGroups:
- source.fluxcd.io
resources:

View File

@ -1,13 +0,0 @@
apiVersion: kustomize.fluxcd.io/v1alpha1
kind: Profile
metadata:
name: default
spec:
alert:
type: slack
address: https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
username: kustomize-controller
channel: general
verbosity: info
kustomizations:
- '*'

View File

@ -86,7 +86,7 @@ func (r *GitRepositoryWatcher) Reconcile(req ctrl.Request) (ctrl.Result, error)
func (r *GitRepositoryWatcher) SetupWithManager(mgr ctrl.Manager) error {
// create a kustomization index based on Git repository name
err := mgr.GetFieldIndexer().IndexField(&kustomizev1.Kustomization{}, kustomizev1.SourceIndexKey,
err := mgr.GetFieldIndexer().IndexField(context.TODO(), &kustomizev1.Kustomization{}, kustomizev1.SourceIndexKey,
func(rawObj runtime.Object) []string {
k := rawObj.(*kustomizev1.Kustomization)
if k.Spec.SourceRef.Kind == "GitRepository" {

View File

@ -33,23 +33,27 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kuberecorder "k8s.io/client-go/tools/record"
"k8s.io/client-go/tools/reference"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
kustypes "sigs.k8s.io/kustomize/api/types"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
"github.com/fluxcd/kustomize-controller/internal/alert"
"github.com/fluxcd/pkg/lockedfile"
"github.com/fluxcd/pkg/recorder"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
)
// KustomizationReconciler reconciles a Kustomization object
type KustomizationReconciler struct {
client.Client
requeueDependency time.Duration
Log logr.Logger
Scheme *runtime.Scheme
requeueDependency time.Duration
Log logr.Logger
Scheme *runtime.Scheme
EventRecorder kuberecorder.EventRecorder
ExternalEventRecorder *recorder.EventRecorder
}
// +kubebuilder:rbac:groups=kustomize.fluxcd.io,resources=kustomizations,verbs=get;list;watch;create;update;patch;delete
@ -130,7 +134,7 @@ func (r *KustomizationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro
// instead we requeue on a fix interval.
msg := fmt.Sprintf("Dependencies do not meet ready condition, retrying in %s", r.requeueDependency.String())
log.Error(err, msg)
r.alert(kustomization, msg, "info")
r.event(kustomization, msg, "info")
return ctrl.Result{RequeueAfter: r.requeueDependency}, nil
}
log.Info("All dependencies area ready, proceeding with apply")
@ -140,7 +144,7 @@ func (r *KustomizationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro
syncedKustomization, err := r.sync(*kustomization.DeepCopy(), source)
if err != nil {
log.Error(err, "Kustomization apply failed", "revision", source.GetArtifact().Revision)
r.alert(kustomization, err.Error(), "error")
r.event(kustomization, err.Error(), "error")
}
// update status
@ -488,7 +492,7 @@ func (r *KustomizationReconciler) apply(kustomization kustomizev1.Kustomization,
}
}
if diff {
r.alert(kustomization, string(output), "info")
r.event(kustomization, string(output), "info")
}
return nil
}
@ -570,7 +574,7 @@ func (r *KustomizationReconciler) checkHealth(kustomization kustomizev1.Kustomiz
}
if alerts != "" {
r.alert(kustomization, alerts, "info")
r.event(kustomization, alerts, "info")
}
return nil
}
@ -628,56 +632,24 @@ func (r *KustomizationReconciler) checkDependencies(kustomization kustomizev1.Ku
return nil
}
func (r *KustomizationReconciler) getProfiles(kustomization kustomizev1.Kustomization) ([]kustomizev1.Profile, error) {
list := make([]kustomizev1.Profile, 0)
var profiles kustomizev1.ProfileList
err := r.List(context.TODO(), &profiles, client.InNamespace(kustomization.GetNamespace()))
if err != nil {
return list, err
}
// filter profiles that match this kustomization taking into account '*' wildcard
for _, profile := range profiles.Items {
for _, name := range profile.Spec.Kustomizations {
if name == kustomization.GetName() || name == "*" {
list = append(list, profile)
break
}
}
}
return list, nil
}
func (r *KustomizationReconciler) alert(kustomization kustomizev1.Kustomization, msg string, verbosity string) {
profiles, err := r.getProfiles(kustomization)
func (r *KustomizationReconciler) event(kustomization kustomizev1.Kustomization, msg string, severity string) {
r.EventRecorder.Event(&kustomization, "Normal", severity, msg)
objRef, err := reference.GetReference(r.Scheme, &kustomization)
if err != nil {
r.Log.WithValues(
strings.ToLower(kustomization.Kind),
fmt.Sprintf("%s/%s", kustomization.GetNamespace(), kustomization.GetName()),
).Error(err, "unable to list profiles")
).Error(err, "unable to send event")
return
}
for _, profile := range profiles {
if settings := profile.Spec.Alert; settings != nil {
provider, err := alert.NewProvider(settings.Type, settings.Address, settings.Username, settings.Channel)
if err != nil {
r.Log.WithValues(
strings.ToLower(kustomization.Kind),
fmt.Sprintf("%s/%s", kustomization.GetNamespace(), kustomization.GetName()),
).Error(err, "unable to configure alert provider")
continue
}
if settings.Verbosity == verbosity || verbosity == "error" {
err = provider.Post(kustomization.GetName(), kustomization.GetNamespace(), msg, verbosity)
if err != nil {
r.Log.WithValues(
strings.ToLower(kustomization.Kind),
fmt.Sprintf("%s/%s", kustomization.GetNamespace(), kustomization.GetName()),
).Error(err, "unable to send alert")
}
}
if r.ExternalEventRecorder != nil {
if err := r.ExternalEventRecorder.Eventf(*objRef, nil, severity, severity, msg); err != nil {
r.Log.WithValues(
strings.ToLower(kustomization.Kind),
fmt.Sprintf("%s/%s", kustomization.GetNamespace(), kustomization.GetName()),
).Error(err, "unable to send event")
return
}
}
}

View File

@ -1,84 +0,0 @@
/*
Copyright 2020 The Flux CD contributors.
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"
"strings"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
)
// ProfileReconciler reconciles a Profile object
type ProfileReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=kustomize.fluxcd.io,resources=profiles,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=kustomize.fluxcd.io,resources=profiles/status,verbs=get;update;patch
func (r *ProfileReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
var profile kustomizev1.Profile
if err := r.Get(ctx, req.NamespacedName, &profile); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
log := r.Log.WithValues(strings.ToLower(profile.Kind), req.NamespacedName)
init := true
for _, condition := range profile.Status.Conditions {
if condition.Type == kustomizev1.ReadyCondition && condition.Status == corev1.ConditionTrue {
init = false
break
}
}
if init {
profile.Status.Conditions = []kustomizev1.Condition{
{
Type: kustomizev1.ReadyCondition,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: kustomizev1.InitializedReason,
Message: kustomizev1.InitializedReason,
},
}
if err := r.Status().Update(ctx, &profile); err != nil {
return ctrl.Result{Requeue: true}, err
}
log.Info("Profile initialised")
}
return ctrl.Result{}, nil
}
func (r *ProfileReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&kustomizev1.Profile{}).
Complete(r)
}

View File

@ -10,8 +10,6 @@
Resource Types:
<ul class="simple"><li>
<a href="#kustomize.fluxcd.io/v1alpha1.Kustomization">Kustomization</a>
</li><li>
<a href="#kustomize.fluxcd.io/v1alpha1.Profile">Profile</a>
</li></ul>
<h3 id="kustomize.fluxcd.io/v1alpha1.Kustomization">Kustomization
</h3>
@ -220,190 +218,11 @@ KustomizationStatus
</table>
</div>
</div>
<h3 id="kustomize.fluxcd.io/v1alpha1.Profile">Profile
</h3>
<p>Profile is the Schema for the profiles API</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>apiVersion</code><br>
string</td>
<td>
<code>kustomize.fluxcd.io/v1alpha1</code>
</td>
</tr>
<tr>
<td>
<code>kind</code><br>
string
</td>
<td>
<code>Profile</code>
</td>
</tr>
<tr>
<td>
<code>metadata</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta">
Kubernetes meta/v1.ObjectMeta
</a>
</em>
</td>
<td>
Refer to the Kubernetes API documentation for the fields of the
<code>metadata</code> field.
</td>
</tr>
<tr>
<td>
<code>spec</code><br>
<em>
<a href="#kustomize.fluxcd.io/v1alpha1.ProfileSpec">
ProfileSpec
</a>
</em>
</td>
<td>
<br/>
<br/>
<table>
<tr>
<td>
<code>alert</code><br>
<em>
<a href="#kustomize.fluxcd.io/v1alpha1.AlertProvider">
AlertProvider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Alerting configuration of the kustomizations targeted by this profile.</p>
</td>
</tr>
<tr>
<td>
<code>kustomizations</code><br>
<em>
[]string
</em>
</td>
<td>
<p>The list of kustomizations that this profile applies to.</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<code>status</code><br>
<em>
<a href="#kustomize.fluxcd.io/v1alpha1.ProfileStatus">
ProfileStatus
</a>
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="kustomize.fluxcd.io/v1alpha1.AlertProvider">AlertProvider
</h3>
<p>
(<em>Appears on:</em>
<a href="#kustomize.fluxcd.io/v1alpha1.ProfileSpec">ProfileSpec</a>)
</p>
<p>Alert is the configuration of alerting for a specific provider</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>address</code><br>
<em>
string
</em>
</td>
<td>
<p>HTTP(S) webhook address of this provider</p>
</td>
</tr>
<tr>
<td>
<code>channel</code><br>
<em>
string
</em>
</td>
<td>
<p>Alert channel for this provider</p>
</td>
</tr>
<tr>
<td>
<code>username</code><br>
<em>
string
</em>
</td>
<td>
<p>Bot username for this provider</p>
</td>
</tr>
<tr>
<td>
<code>verbosity</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Filter alerts based on verbosity level, defaults to (&lsquo;error&rsquo;).</p>
</td>
</tr>
<tr>
<td>
<code>type</code><br>
<em>
string
</em>
</td>
<td>
<p>Type of provider</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="kustomize.fluxcd.io/v1alpha1.Condition">Condition
</h3>
<p>
(<em>Appears on:</em>
<a href="#kustomize.fluxcd.io/v1alpha1.KustomizationStatus">KustomizationStatus</a>,
<a href="#kustomize.fluxcd.io/v1alpha1.ProfileStatus">ProfileStatus</a>)
<a href="#kustomize.fluxcd.io/v1alpha1.KustomizationStatus">KustomizationStatus</a>)
</p>
<p>Condition contains condition information for a kustomization.</p>
<div class="md-typeset__scrollwrap">
@ -696,86 +515,6 @@ Snapshot
</table>
</div>
</div>
<h3 id="kustomize.fluxcd.io/v1alpha1.ProfileSpec">ProfileSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#kustomize.fluxcd.io/v1alpha1.Profile">Profile</a>)
</p>
<p>ProfileSpec defines the desired state of Profile</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>alert</code><br>
<em>
<a href="#kustomize.fluxcd.io/v1alpha1.AlertProvider">
AlertProvider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Alerting configuration of the kustomizations targeted by this profile.</p>
</td>
</tr>
<tr>
<td>
<code>kustomizations</code><br>
<em>
[]string
</em>
</td>
<td>
<p>The list of kustomizations that this profile applies to.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="kustomize.fluxcd.io/v1alpha1.ProfileStatus">ProfileStatus
</h3>
<p>
(<em>Appears on:</em>
<a href="#kustomize.fluxcd.io/v1alpha1.Profile">Profile</a>)
</p>
<p>ProfileStatus defines the observed state of Profile</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>conditions</code><br>
<em>
<a href="#kustomize.fluxcd.io/v1alpha1.Condition">
[]Condition
</a>
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="kustomize.fluxcd.io/v1alpha1.ServiceAccount">ServiceAccount
</h3>
<p>

View File

@ -14,8 +14,6 @@ of Kubernetes objects generated with Kustomize.
+ [Kustomization dependencies](kustomization.md#kustomization-dependencies)
+ [Role-based access control](kustomization.md#role-based-access-control)
+ [Status](kustomization.md#status)
- [Profile CRD](profile.md)
+ [Alerting configuration](profile.md#alerting)
## Implementation

View File

@ -1,97 +0,0 @@
# Profile
The `Profile` API defines a common behavior for a group of [Kustomization](kustomization.md) objects.
## Specification
```go
type ProfileSpec struct {
// Alerting configuration of the kustomizations targeted by this profile.
// +optional
Alert *AlertProvider `json:"alert"`
// The list of kustomizations that this profile applies to.
// +required
Kustomizations []string `json:"kustomizations"`
}
```
Alerting configuration:
```go
type AlertProvider struct {
// HTTP(S) webhook address of this provider
// +required
Address string `json:"address"`
// Alert channel for this provider
// +required
Channel string `json:"channel"`
// Bot username for this provider
// +required
Username string `json:"username"`
// Filter alerts based on verbosity level, defaults to ('error').
// +kubebuilder:validation:Enum=info;error
// +optional
Verbosity string `json:"verbosity,omitempty"`
// Type of provider
// +kubebuilder:validation:Enum=slack;discord
// +required
Type string `json:"type"`
}
```
Status condition types:
```go
const (
// ReadyCondition represents the fact that a given Profile has been
// processed by the controller.
ReadyCondition string = "Ready"
)
```
Status condition reasons:
```go
const (
// InitializedReason represents the fact that a given resource has been initialized.
InitializedReason string = "Initialized"
)
```
## Alerting
Alerting can be configured by creating a profile that contains an alert definition:
```yaml
apiVersion: kustomize.fluxcd.io/v1alpha1
kind: Profile
metadata:
name: default
spec:
alert:
type: slack
verbosity: info
address: https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
username: kustomize-controller
channel: general
kustomizations:
- '*'
```
The alert provider type can be: `slack` or `discord` and the verbosity can be set to `info` or `error`.
The `*` wildcard tells the controller to use this profile for all kustomizations that are present
in the same namespace as the profile.
Multiple profiles can be used to send alerts to different channels or Slack organizations.
When the verbosity is set to `error`, the controller will alert on any error encountered during the
reconciliation process. This includes kustomize build and validation errors, apply errors and
health check failures.
When the verbosity is set to `info`, the controller will alert whenever a kustomization status changes.

10
go.mod
View File

@ -3,15 +3,15 @@ module github.com/fluxcd/kustomize-controller
go 1.13
require (
github.com/fluxcd/pkg v0.0.1
github.com/fluxcd/pkg v0.0.2
github.com/fluxcd/source-controller v0.0.1
github.com/go-logr/logr v0.1.0
github.com/onsi/ginkgo v1.11.0
github.com/onsi/gomega v1.8.1
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
sigs.k8s.io/controller-runtime v0.5.0
k8s.io/api v0.18.4
k8s.io/apimachinery v0.18.4
k8s.io/client-go v0.18.4
sigs.k8s.io/controller-runtime v0.6.0
sigs.k8s.io/kustomize/api v0.4.1
sigs.k8s.io/yaml v1.2.0
)

44
go.sum
View File

@ -124,6 +124,7 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e/go.mod h1:CgNC6SGbT+Xb8wGGvzilttZL1mc5sQ/5KkcxsZttMIk=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
@ -135,8 +136,8 @@ github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fluxcd/pkg v0.0.1 h1:yECp5SBjX7vUBOjd3KYBoVQwt22A0u1SZJjYV4PduAk=
github.com/fluxcd/pkg v0.0.1/go.mod h1:3DgEcVmkVYrA/BDb/fyDIJllxK++c/ovLCMPRlkAp9Y=
github.com/fluxcd/pkg v0.0.2 h1:e2ekyxBNZg0phh3adSxCsaGcozGyy/FQwhD2LtA+Y0Q=
github.com/fluxcd/pkg v0.0.2/go.mod h1:rtlppQU+9DNikyDZptLdOeTf+wBvQQiQQ/J113FPoeU=
github.com/fluxcd/source-controller v0.0.1 h1:17/b/Zcb3OUkUoo03W+L7TGwkCKG23K9HrgL+d5WMXE=
github.com/fluxcd/source-controller v0.0.1/go.mod h1:tmscNdCxEt7+Xt2g1+bI38hMPw2leYMFAaCn4UlMGuw=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
@ -278,6 +279,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
@ -287,6 +290,7 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk=
github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
@ -305,10 +309,14 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -453,6 +461,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@ -582,6 +592,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
@ -659,6 +670,7 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -729,6 +741,7 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@ -770,31 +783,51 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI=
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78=
k8s.io/api v0.18.4 h1:8x49nBRxuXGUlDlwlWd3RMY1SayZrzFfxea3UZSkFw4=
k8s.io/api v0.18.4/go.mod h1:lOIQAKYgai1+vz9J7YcDZwC26Z0zQewYOGWdyIPUUQ4=
k8s.io/apiextensions-apiserver v0.17.2 h1:cP579D2hSZNuO/rZj9XFRzwJNYb41DbNANJb6Kolpss=
k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs=
k8s.io/apiextensions-apiserver v0.18.2 h1:I4v3/jAuQC+89L3Z7dDgAiN4EOjN6sbm6iBqQwHTah8=
k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY=
k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=
k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
k8s.io/apimachinery v0.18.4 h1:ST2beySjhqwJoIFk6p7Hp5v5O0hYY6Gngq/gUYXTPIA=
k8s.io/apimachinery v0.18.4/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo=
k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw=
k8s.io/cli-runtime v0.17.2/go.mod h1:aa8t9ziyQdbkuizkNLAw3qe3srSyWh9zlSB7zTqRNPI=
k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k=
k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc=
k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI=
k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU=
k8s.io/client-go v0.18.4 h1:un55V1Q/B3JO3A76eS0kUSywgGK/WR3BQ8fHQjNa6Zc=
k8s.io/client-go v0.18.4/go.mod h1:f5sXwL4yAZRkAtzOxRWUhA/N8XzGCb+nPZI8PfobZ9g=
k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc=
k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs=
k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmDfUM=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY=
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/kubectl v0.17.2/go.mod h1:y4rfLV0n6aPmvbRCqZQjvOp3ezxsFgpqL+zF5jH/lxk=
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/metrics v0.17.2/go.mod h1:3TkNHET4ROd+NfzNxkjoVfQ0Ob4iZnaHmSEA4vYpwLw=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU=
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
@ -804,15 +837,22 @@ mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIa
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw=
rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0=
sigs.k8s.io/controller-runtime v0.5.0 h1:CbqIy5fbUX+4E9bpnBFd204YAzRYlM9SWW77BbrcDQo=
sigs.k8s.io/controller-runtime v0.5.0/go.mod h1:REiJzC7Y00U+2YkMbT8wxgrsX5USpXKGhb2sCtAXiT8=
sigs.k8s.io/controller-runtime v0.6.0 h1:Fzna3DY7c4BIP6KwfSlrfnj20DJ+SeMBK8HSFvOk9NM=
sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo=
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/kustomize/api v0.4.1 h1:Lwco6Rsxd3TcubJzx9wAV2k7roh0M95FjrS29n76TRo=
sigs.k8s.io/kustomize/api v0.4.1/go.mod h1:NqxqT+wbYHrD0P19Uu4dXiMsVwI1IwQs+MJHlLhmPqQ=
sigs.k8s.io/kustomize/kyaml v0.1.11/go.mod h1:72/rLkSi+L/pHM1oCjwrf3ClU+tH5kZQvvdLSqIHwWU=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU=
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=

View File

@ -1,128 +0,0 @@
package alert
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"strings"
"time"
)
// Provider holds the information needed to post alerts
type Provider struct {
URL string
Username string
Channel string
}
// Payload holds the channel and attachments
type Payload struct {
Channel string `json:"channel"`
Username string `json:"username"`
Text string `json:"text,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
}
// Attachment holds the markdown message body
type Attachment struct {
Color string `json:"color"`
AuthorName string `json:"author_name"`
Text string `json:"text"`
MrkdwnIn []string `json:"mrkdwn_in"`
}
// NewProvider validates the URL and returns a provider object
func NewProvider(providerType string, hookURL string, username string, channel string) (*Provider, error) {
hook, err := url.ParseRequestURI(hookURL)
if err != nil {
return nil, fmt.Errorf("invalid hook URL %s", hookURL)
}
if providerType == "discord" {
// https://birdie0.github.io/discord-webhooks-guide/other/slack_formatting.html
if !strings.HasSuffix(hookURL, "/slack") {
hook.Path = path.Join(hook.Path, "slack")
hookURL = hook.String()
}
}
if username == "" {
return nil, errors.New("empty username")
}
if channel == "" {
return nil, errors.New("empty channel")
}
return &Provider{
Channel: channel,
URL: hookURL,
Username: username,
}, nil
}
// Post message to the provider hook URL
func (s *Provider) Post(name string, namespace string, message string, severity string) error {
payload := Payload{
Channel: s.Channel,
Username: s.Username,
}
color := "good"
if severity == "error" {
color = "danger"
}
a := Attachment{
Color: color,
AuthorName: fmt.Sprintf("%s/%s", namespace, name),
Text: message,
MrkdwnIn: []string{"text"},
}
payload.Attachments = []Attachment{a}
err := postMessage(s.URL, payload)
if err != nil {
return fmt.Errorf("postMessage failed: %w", err)
}
return nil
}
func postMessage(address string, payload interface{}) error {
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshalling notification payload failed: %w", err)
}
b := bytes.NewBuffer(data)
req, err := http.NewRequest("POST", address, b)
if err != nil {
return fmt.Errorf("http.NewRequest failed: %w", err)
}
req.Header.Set("Content-type", "application/json")
ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
defer cancel()
res, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return fmt.Errorf("sending notification failed: %w", err)
}
defer res.Body.Close()
statusCode := res.StatusCode
if statusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("sending notification failed: %s", string(body))
}
return nil
}

36
main.go
View File

@ -21,15 +21,15 @@ import (
"os"
"time"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
"github.com/fluxcd/kustomize-controller/controllers"
"github.com/fluxcd/pkg/recorder"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
"github.com/fluxcd/kustomize-controller/controllers"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
// +kubebuilder:scaffold:imports
)
@ -49,6 +49,7 @@ func init() {
func main() {
var (
metricsAddr string
eventsAddr string
enableLeaderElection bool
concurrent int
requeueDependency time.Duration
@ -56,6 +57,7 @@ func main() {
)
flag.StringVar(&metricsAddr, "metrics-addr", ":8282", "The address the metric endpoint binds to.")
flag.StringVar(&eventsAddr, "events-addr", "", "The address of the events receiver.")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
@ -66,6 +68,16 @@ func main() {
ctrl.SetLogger(zap.New(zap.UseDevMode(!logJSON)))
var eventRecorder *recorder.EventRecorder
if eventsAddr != "" {
if er, err := recorder.NewEventRecorder(eventsAddr, "kustomize-controller"); err != nil {
setupLog.Error(err, "unable to create event recorder")
os.Exit(1)
} else {
eventRecorder = er
}
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
@ -88,9 +100,11 @@ func main() {
os.Exit(1)
}
if err = (&controllers.KustomizationReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Kustomization"),
Scheme: mgr.GetScheme(),
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Kustomization"),
Scheme: mgr.GetScheme(),
EventRecorder: mgr.GetEventRecorderFor("kustomize-controller"),
ExternalEventRecorder: eventRecorder,
}).SetupWithManager(mgr, controllers.KustomizationReconcilerOptions{
MaxConcurrentReconciles: concurrent,
DependencyRequeueInterval: requeueDependency,
@ -98,14 +112,6 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "Kustomization")
os.Exit(1)
}
if err = (&controllers.ProfileReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Profile"),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Profile")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
setupLog.Info("starting manager")