Skip to content

Commit 96ba030

Browse files
author
Alexander Matyushentsev
authored
feat: support notifications on rollout events using notifications-engine (argoproj#1175)
feat: support notifications on rollout events using notifications-engine (argoproj#1175) Signed-off-by: Alexander Matyushentsev <[email protected]>
1 parent e2f87a4 commit 96ba030

22 files changed

+924
-42
lines changed

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ plugin-darwin: ui/dist
197197
cp -r ui/dist/app/* server/static
198198
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${PLUGIN_CLI_NAME}-darwin-amd64 ./cmd/kubectl-argo-rollouts
199199

200-
.PHONY: plugin-docs
201-
plugin-docs:
202-
go run ./hack/gen-plugin-docs/main.go
200+
.PHONY: docs
201+
docs:
202+
go run ./hack/gen-docs/main.go
203203

204204
.PHONY: builder-image
205205
builder-image:
@@ -259,7 +259,7 @@ clean:
259259
precheckin: test lint
260260

261261
.PHONY: release-docs
262-
release-docs: plugin-docs
262+
release-docs: docs
263263
docker run --rm -it \
264264
-v ~/.ssh:/root/.ssh \
265265
-v ${CURRENT_DIR}:/docs \

cmd/rollouts-controller/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ func newCommand() *cobra.Command {
135135
clusterDynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, resyncDuration, metav1.NamespaceAll, instanceIDTweakListFunc)
136136
// 3. We finally need an istio dynamic informer factory which does not use a tweakListFunc.
137137
istioDynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, resyncDuration, namespace, nil)
138+
139+
controllerNamespaceInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(
140+
kubeClient,
141+
resyncDuration,
142+
kubeinformers.WithNamespace(defaults.Namespace()))
143+
configMapInformer := controllerNamespaceInformerFactory.Core().V1().ConfigMaps()
144+
secretInformer := controllerNamespaceInformerFactory.Core().V1().Secrets()
138145
cm := controller.NewManager(
139146
namespace,
140147
kubeClient,
@@ -153,6 +160,8 @@ func newCommand() *cobra.Command {
153160
tolerantinformer.NewTolerantClusterAnalysisTemplateInformer(clusterDynamicInformerFactory),
154161
istioDynamicInformerFactory.ForResource(istioutil.GetIstioVirtualServiceGVR()).Informer(),
155162
istioDynamicInformerFactory.ForResource(istioutil.GetIstioDestinationRuleGVR()).Informer(),
163+
configMapInformer,
164+
secretInformer,
156165
resyncDuration,
157166
instanceID,
158167
metricsPort,
@@ -166,6 +175,7 @@ func newCommand() *cobra.Command {
166175
clusterDynamicInformerFactory.Start(stopCh)
167176
}
168177
kubeInformerFactory.Start(stopCh)
178+
controllerNamespaceInformerFactory.Start(stopCh)
169179
jobInformerFactory.Start(stopCh)
170180

171181
// Check if Istio installed on cluster before starting dynamicInformerFactory

controller/controller.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package controller
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"time"
67

7-
"github.com/argoproj/argo-rollouts/utils/queue"
8-
8+
"github.com/argoproj/notifications-engine/pkg/api"
9+
"github.com/argoproj/notifications-engine/pkg/controller"
910
"github.com/pkg/errors"
1011
smiclientset "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned"
1112
log "github.com/sirupsen/logrus"
1213
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1315
"k8s.io/apimachinery/pkg/util/runtime"
1416
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
1517
"k8s.io/apimachinery/pkg/util/wait"
@@ -28,11 +30,14 @@ import (
2830
"github.com/argoproj/argo-rollouts/controller/metrics"
2931
"github.com/argoproj/argo-rollouts/experiments"
3032
"github.com/argoproj/argo-rollouts/ingress"
33+
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
3134
clientset "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned"
3235
rolloutscheme "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/scheme"
3336
informers "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/rollouts/v1alpha1"
3437
"github.com/argoproj/argo-rollouts/rollout"
3538
"github.com/argoproj/argo-rollouts/service"
39+
"github.com/argoproj/argo-rollouts/utils/defaults"
40+
"github.com/argoproj/argo-rollouts/utils/queue"
3641
"github.com/argoproj/argo-rollouts/utils/record"
3742
)
3843

@@ -61,12 +66,13 @@ const (
6166

6267
// Manager is the controller implementation for Argo-Rollout resources
6368
type Manager struct {
64-
metricsServer *metrics.MetricsServer
65-
rolloutController *rollout.Controller
66-
experimentController *experiments.Controller
67-
analysisController *analysis.Controller
68-
serviceController *service.Controller
69-
ingressController *ingress.Controller
69+
metricsServer *metrics.MetricsServer
70+
rolloutController *rollout.Controller
71+
experimentController *experiments.Controller
72+
analysisController *analysis.Controller
73+
serviceController *service.Controller
74+
ingressController *ingress.Controller
75+
notificationsController controller.NotificationController
7076

7177
rolloutSynced cache.InformerSynced
7278
experimentSynced cache.InformerSynced
@@ -77,6 +83,8 @@ type Manager struct {
7783
ingressSynced cache.InformerSynced
7884
jobSynced cache.InformerSynced
7985
replicasSetSynced cache.InformerSynced
86+
configMapSynced cache.InformerSynced
87+
secretSynced cache.InformerSynced
8088

8189
rolloutWorkqueue workqueue.RateLimitingInterface
8290
serviceWorkqueue workqueue.RateLimitingInterface
@@ -110,6 +118,8 @@ func NewManager(
110118
clusterAnalysisTemplateInformer informers.ClusterAnalysisTemplateInformer,
111119
istioVirtualServiceInformer cache.SharedIndexInformer,
112120
istioDestinationRuleInformer cache.SharedIndexInformer,
121+
configMapInformer coreinformers.ConfigMapInformer,
122+
secretInformer coreinformers.SecretInformer,
113123
resyncPeriod time.Duration,
114124
instanceID string,
115125
metricsPort int,
@@ -139,8 +149,22 @@ func NewManager(
139149
ingressWorkqueue := workqueue.NewNamedRateLimitingQueue(queue.DefaultArgoRolloutsRateLimiter(), "Ingresses")
140150

141151
refResolver := rollout.NewInformerBasedWorkloadRefResolver(namespace, dynamicclientset, discoveryClient, rolloutWorkqueue, rolloutsInformer.Informer())
142-
143-
recorder := record.NewEventRecorder(kubeclientset, metrics.MetricRolloutEventsTotal)
152+
apiFactory := api.NewFactory(record.NewAPIFactorySettings(), defaults.Namespace(), secretInformer.Informer(), configMapInformer.Informer())
153+
recorder := record.NewEventRecorder(kubeclientset, metrics.MetricRolloutEventsTotal, apiFactory)
154+
notificationsController := controller.NewController(dynamicclientset.Resource(v1alpha1.RolloutGVR), rolloutsInformer.Informer(), apiFactory,
155+
controller.WithToUnstructured(func(obj metav1.Object) (*unstructured.Unstructured, error) {
156+
data, err := json.Marshal(obj)
157+
if err != nil {
158+
return nil, err
159+
}
160+
res := &unstructured.Unstructured{}
161+
err = json.Unmarshal(data, res)
162+
if err != nil {
163+
return nil, err
164+
}
165+
return res, nil
166+
}),
167+
)
144168

145169
rolloutController := rollout.NewController(rollout.ControllerConfig{
146170
Namespace: namespace,
@@ -229,6 +253,8 @@ func NewManager(
229253
analysisTemplateSynced: analysisTemplateInformer.Informer().HasSynced,
230254
clusterAnalysisTemplateSynced: clusterAnalysisTemplateInformer.Informer().HasSynced,
231255
replicasSetSynced: replicaSetInformer.Informer().HasSynced,
256+
configMapSynced: configMapInformer.Informer().HasSynced,
257+
secretSynced: secretInformer.Informer().HasSynced,
232258
rolloutWorkqueue: rolloutWorkqueue,
233259
experimentWorkqueue: experimentWorkqueue,
234260
analysisRunWorkqueue: analysisRunWorkqueue,
@@ -239,6 +265,7 @@ func NewManager(
239265
ingressController: ingressController,
240266
experimentController: experimentController,
241267
analysisController: analysisController,
268+
notificationsController: notificationsController,
242269
dynamicClientSet: dynamicclientset,
243270
refResolver: refResolver,
244271
namespace: namespace,
@@ -260,7 +287,7 @@ func (c *Manager) Run(rolloutThreadiness, serviceThreadiness, ingressThreadiness
260287

261288
// Wait for the caches to be synced before starting workers
262289
log.Info("Waiting for controller's informer caches to sync")
263-
if ok := cache.WaitForCacheSync(stopCh, c.serviceSynced, c.ingressSynced, c.jobSynced, c.rolloutSynced, c.experimentSynced, c.analysisRunSynced, c.analysisTemplateSynced, c.replicasSetSynced); !ok {
290+
if ok := cache.WaitForCacheSync(stopCh, c.serviceSynced, c.ingressSynced, c.jobSynced, c.rolloutSynced, c.experimentSynced, c.analysisRunSynced, c.analysisTemplateSynced, c.replicasSetSynced, c.configMapSynced, c.secretSynced); !ok {
264291
return fmt.Errorf("failed to wait for caches to sync")
265292
}
266293
// only wait for cluster scoped informers to sync if we are running in cluster-wide mode
@@ -277,6 +304,8 @@ func (c *Manager) Run(rolloutThreadiness, serviceThreadiness, ingressThreadiness
277304
go wait.Until(func() { c.ingressController.Run(ingressThreadiness, stopCh) }, time.Second, stopCh)
278305
go wait.Until(func() { c.experimentController.Run(experimentThreadiness, stopCh) }, time.Second, stopCh)
279306
go wait.Until(func() { c.analysisController.Run(analysisThreadiness, stopCh) }, time.Second, stopCh)
307+
go wait.Until(func() { c.notificationsController.Run(rolloutThreadiness, stopCh) }, time.Second, stopCh)
308+
280309
log.Info("Started controller")
281310

282311
go func() {

docs/features/notifications.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Notifications
2+
3+
!!! important
4+
Available since v1.1
5+
6+
Argo Rollouts provides notifications powered by the [Notifications Engine](https://github.com/argoproj/notifications-engine).
7+
Controller administrators can leverage flexible systems of triggers and templates to configure notifications requested
8+
by the end users. The end-users can subscribe to the configured triggers by adding an annotation to the Rollout objects.
9+
10+
## Configuration
11+
12+
The trigger defines the condition when the notification should be sent as well as the notification content template.
13+
Default Argo Rollouts comes with a list of built-in triggers that cover the most important events of Argo Rollout live-cycle.
14+
Both triggers and templates are configured in the `argo-rollouts-notification-configmap` ConfigMap. In order to get
15+
started quickly, you can use pre-configured notification templates defined in [notifications-install.yaml](../../manifests/notifications-install.yaml).
16+
17+
If you are leveraging Kustomize it is recommended to include [notifications-install.yaml](../../manifests/notifications-install.yaml) as a remote
18+
resource into your `kustomization.yaml` file:
19+
20+
```yaml
21+
apiVersion: kustomize.config.k8s.io/v1beta1
22+
kind: Kustomization
23+
24+
resources:
25+
- https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
26+
- https://github.com/argoproj/argo-rollouts/releases/latest/download/notifications-install.yaml
27+
```
28+
29+
After including the `argo-rollouts-notification-configmap` ConfigMap the administrator needs to configure integration
30+
with the required notifications service such as Slack or MS Teams. An example below demonstrates Slack integration:
31+
32+
```yaml
33+
apiVersion: v1
34+
kind: ConfigMap
35+
metadata:
36+
name: argo-rollouts-notification-configmap
37+
data:
38+
service.slack: |
39+
token: $slack-token
40+
---
41+
apiVersion: v1
42+
kind: Secret
43+
metadata:
44+
name: argo-rollouts-notification-secret
45+
stringData:
46+
slack-token: <my-slack-token>
47+
```
48+
49+
Learn more about supported services and configuration settings in services [documentation](../generated/notification-services/overview.md).
50+
51+
## Subscriptions
52+
53+
The end-users can start leveraging notifications using `notifications.argoproj.io/subscribe.<trigger>.<service>: <recipient>` annotation.
54+
For example, the following annotation subscribes two Slack channels to notifications about canary rollout step completion:
55+
56+
```yaml
57+
---
58+
apiVersion: argoproj.io/v1alpha1
59+
kind: Rollout
60+
metadata:
61+
name: rollout-canary
62+
annotations:
63+
notifications.argoproj.io/subscribe.on-rollout-step-completed.slack: my-channel1;my-channel2
64+
65+
```
66+
67+
Annotation key consists of following parts:
68+
69+
* `on-rollout-step-completed` - trigger name
70+
* `slack` - notification service name
71+
* `my-channel1;my-channel2` - a semicolon separated list of recipients
72+
73+
## Customization
74+
75+
The Rollout administrator can customize the notifications by configuring notification templates and custom triggers
76+
in `argo-rollouts-notification-configmap` ConfigMap.
77+
78+
### Templates
79+
80+
The notification template is a stateless function that generates the notification content. The template is leveraging
81+
[html/template](https://golang.org/pkg/html/template/) golang package. It is meant to be reusable and can be referenced by multiple triggers.
82+
83+
An example below demonstrates a sample template:
84+
85+
```yaml
86+
apiVersion: v1
87+
kind: ConfigMap
88+
metadata:
89+
name: argo-rollouts-notification-configmap
90+
data:
91+
template.my-purple-template: |
92+
message: |
93+
Rollout {{.rollout.metadata.name}} has purple image
94+
slack:
95+
attachments: |
96+
[{
97+
"title": "{{ .rollout.metadata.name}}",
98+
"color": "#800080"
99+
}]
100+
```
101+
102+
Each template has access to the following fields:
103+
104+
- `rollout` holds the rollout object.
105+
- `recipient` holds the recipient name.
106+
107+
The `message` field of the template definition allows creating a basic notification for any notification service. You can
108+
leverage notification service-specific fields to create complex notifications. For example using service-specific you can
109+
add blocks and attachments for Slack, subject for Email or URL path, and body for Webhook. See corresponding service
110+
[documentation](../generated/notification-services/overview.md) for more information.
111+
112+
### Custom Triggers
113+
114+
In addition to custom notification template administrator and configure custom triggers. Custom trigger defines the
115+
condition when the notification should be sent. The definition includes name, condition and notification templates reference.
116+
The condition is a predicate expression that returns true if the notification should be sent. The trigger condition
117+
evaluation is powered by [antonmedv/expr](https://github.com/antonmedv/expr).
118+
The condition language syntax is described at [Language-Definition.md](https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md).
119+
120+
The trigger is configured in `argo-rollouts-notification-configmap` ConfigMap. For example the following trigger sends a notification
121+
when rollout pod spec uses `argoproj/rollouts-demo:purple` image:
122+
123+
```yaml
124+
apiVersion: v1
125+
kind: ConfigMap
126+
metadata:
127+
name: argocd-notifications-cm
128+
data:
129+
trigger.on-purple: |
130+
- send: [my-purple-template]
131+
when: rollout.spec.template.spec.containers[0].image == 'argoproj/rollouts-demo:purple'
132+
```
133+
134+
Each condition might use several templates. Typically each template is responsible for generating a service-specific notification part.

examples/notifications/configmap.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: argo-rollouts-notification-configmap
5+
data:
6+
service.slack: |
7+
token: $slack-token
8+
## Custom Trigger
9+
trigger.on-purple: |
10+
- send: [my-purple-template]
11+
when: rollout.spec.template.spec.containers[0].image == 'argoproj/rollouts-demo:purple'
12+
template.my-purple-template: |
13+
message: |
14+
Rollout {{.rollout.metadata.name}} has purple image
15+
slack:
16+
attachments: |
17+
[{
18+
"title": "{{ .rollout.metadata.name}}",
19+
"color": "#800080"
20+
}]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
4+
resources:
5+
- ../../manifests/notifications
6+
7+
patchesStrategicMerge:
8+
- configmap.yaml

0 commit comments

Comments
 (0)