Browse Source

Distinguish automated release events

Previously when we ran automation the changes to be made were
collected into a set of releases of individual images. This was
largely to fit with the existing release mechanism.

The recent tag filtering implementation changed how releases could be
specified, allowing you to supply an implementation of
`update.Changes` which would then be used to calculate the changes to
manifests.

However, the commit notes and events still accommodate only the
original kind of release spec. The upshot: fields in commit notes were
empty, and notifications missing.

One quick fix would be to finesse automated release specs to look like
the original kind. However, since the former can mention multiple
images, it is easier said than done. Instead I decided to bite the
bullet and create a new kind of event (and hook it into notification).

Also removes the custom Slack template, which we didn't use.

Fixes #641.
Michael Bridgen 3 years ago
parent
commit
b499ece555

+ 1
- 1
daemon/images.go View File

@@ -58,7 +58,7 @@ func (d *Daemon) PollImages(logger log.Logger) {
58 58
 		}
59 59
 	}
60 60
 
61
-	d.UpdateManifests(update.Spec{Type: update.Images, Spec: changes})
61
+	d.UpdateManifests(update.Spec{Type: update.Auto, Spec: changes})
62 62
 }
63 63
 
64 64
 func getTagPattern(services policy.ServiceMap, service flux.ServiceID, container string) string {

+ 28
- 6
daemon/loop.go View File

@@ -164,7 +164,8 @@ func (d *Daemon) pullAndSync(logger log.Logger) {
164 164
 
165 165
 			// If any of the commit notes has a release event, send
166 166
 			// that to the service
167
-			if n.Spec.Type == update.Images {
167
+			switch n.Spec.Type {
168
+			case update.Images:
168 169
 				// Map new note.Spec into ReleaseSpec
169 170
 				spec := n.Spec.Spec.(update.ReleaseSpec)
170 171
 				// And create a release event
@@ -176,11 +177,32 @@ func (d *Daemon) pullAndSync(logger log.Logger) {
176 177
 					EndedAt:    time.Now().UTC(),
177 178
 					LogLevel:   history.LogLevelInfo,
178 179
 					Metadata: &history.ReleaseEventMetadata{
179
-						Revision: revisions[i],
180
-						Spec:     spec,
181
-						Cause:    n.Spec.Cause,
182
-						Result:   n.Result,
183
-						Error:    n.Result.Error(),
180
+						ReleaseEventCommon: history.ReleaseEventCommon{
181
+							Revision: revisions[i],
182
+							Result:   n.Result,
183
+							Error:    n.Result.Error(),
184
+						},
185
+						Spec:  spec,
186
+						Cause: n.Spec.Cause,
187
+					},
188
+				}); err != nil {
189
+					logger.Log("err", err)
190
+				}
191
+			case update.Auto:
192
+				spec := n.Spec.Spec.(update.Automated)
193
+				if err := d.LogEvent(history.Event{
194
+					ServiceIDs: serviceIDs.ToSlice(),
195
+					Type:       history.EventAutoRelease,
196
+					StartedAt:  started,
197
+					EndedAt:    time.Now().UTC(),
198
+					LogLevel:   history.LogLevelInfo,
199
+					Metadata: &history.AutoReleaseEventMetadata{
200
+						ReleaseEventCommon: history.ReleaseEventCommon{
201
+							Revision: revisions[i],
202
+							Result:   n.Result,
203
+							Error:    n.Result.Error(),
204
+						},
205
+						Spec: spec,
184 206
 					},
185 207
 				}); err != nil {
186 208
 					logger.Log("err", err)

+ 38
- 15
history/event.go View File

@@ -15,13 +15,14 @@ import (
15 15
 
16 16
 // These are all the types of events.
17 17
 const (
18
-	EventCommit     = "commit"
19
-	EventSync       = "sync"
20
-	EventRelease    = "release"
21
-	EventAutomate   = "automate"
22
-	EventDeautomate = "deautomate"
23
-	EventLock       = "lock"
24
-	EventUnlock     = "unlock"
18
+	EventCommit      = "commit"
19
+	EventSync        = "sync"
20
+	EventRelease     = "release"
21
+	EventAutoRelease = "autorelease"
22
+	EventAutomate    = "automate"
23
+	EventDeautomate  = "deautomate"
24
+	EventLock        = "lock"
25
+	EventUnlock      = "unlock"
25 26
 
26 27
 	LogLevelDebug = "debug"
27 28
 	LogLevelInfo  = "info"
@@ -172,16 +173,27 @@ type SyncEventMetadata struct {
172 173
 	Revisions []string `json:"revisions,omitempty"`
173 174
 }
174 175
 
175
-// ReleaseEventMetadata is the metadata for when service(s) are released
176
-type ReleaseEventMetadata struct {
177
-	Revision string             // the revision which has the changes for the release
178
-	Spec     update.ReleaseSpec `json:"spec"`
179
-	Cause    update.Cause       `json:"cause"`
180
-	Result   update.Result      `json:"result"`
176
+type ReleaseEventCommon struct {
177
+	Revision string        // the revision which has the changes for the release
178
+	Result   update.Result `json:"result"`
181 179
 	// Message of the error if there was one.
182 180
 	Error string `json:"error,omitempty"`
183 181
 }
184 182
 
183
+// ReleaseEventMetadata is the metadata for when service(s) are released
184
+type ReleaseEventMetadata struct {
185
+	ReleaseEventCommon
186
+	Spec  update.ReleaseSpec `json:"spec"`
187
+	Cause update.Cause       `json:"cause"`
188
+}
189
+
190
+// AutoReleaseEventMetadata is for when service(s) are released
191
+// automatically because there's a new image or images
192
+type AutoReleaseEventMetadata struct {
193
+	ReleaseEventCommon
194
+	Spec update.Automated `json:"spec"`
195
+}
196
+
185 197
 type UnknownEventMetadata map[string]interface{}
186 198
 
187 199
 func (e *Event) UnmarshalJSON(in []byte) error {
@@ -200,8 +212,8 @@ func (e *Event) UnmarshalJSON(in []byte) error {
200 212
 		return errors.New("Event type is empty")
201 213
 	}
202 214
 
203
-	// If we have a type which we want to convert, overwrite the
204
-	// standard Event with the new unmarshalled type
215
+	// The cases correspond to kinds of event that we care about
216
+	// processing e.g., for notifications.
205 217
 	switch wireEvent.Type {
206 218
 	case EventRelease:
207 219
 		var metadata ReleaseEventMetadata
@@ -210,6 +222,13 @@ func (e *Event) UnmarshalJSON(in []byte) error {
210 222
 		}
211 223
 		e.Metadata = &metadata
212 224
 		break
225
+	case EventAutoRelease:
226
+		var metadata AutoReleaseEventMetadata
227
+		if err := json.Unmarshal(wireEvent.MetadataBytes, &metadata); err != nil {
228
+			return err
229
+		}
230
+		e.Metadata = &metadata
231
+		break
213 232
 	default:
214 233
 		if len(wireEvent.MetadataBytes) > 0 {
215 234
 			var metadata UnknownEventMetadata
@@ -245,6 +264,10 @@ func (rem *ReleaseEventMetadata) Type() string {
245 264
 	return EventRelease
246 265
 }
247 266
 
267
+func (rem *AutoReleaseEventMetadata) Type() string {
268
+	return EventAutoRelease
269
+}
270
+
248 271
 // Special exception from pointer receiver rule, as UnknownEventMetadata is a
249 272
 // type alias for a map
250 273
 func (uem UnknownEventMetadata) Type() string {

+ 11
- 12
notifications/notifications.go View File

@@ -3,20 +3,19 @@ package notifications
3 3
 import (
4 4
 	"github.com/weaveworks/flux/history"
5 5
 	"github.com/weaveworks/flux/instance"
6
-	"github.com/weaveworks/flux/update"
7 6
 )
8 7
 
9
-// Release performs post-release notifications for an instance
10
-func Release(cfg instance.Config, r *history.ReleaseEventMetadata, releaseError string) error {
11
-	if r.Spec.Kind != update.ReleaseKindExecute {
12
-		return nil
13
-	}
14
-
15
-	// TODO: Use a config settings format which allows multiple notifiers to be
16
-	// configured.
17
-	var err error
8
+func Event(cfg instance.Config, e history.Event) error {
9
+	// If this is a release
18 10
 	if cfg.Settings.Slack.HookURL != "" {
19
-		err = slackNotifyRelease(cfg.Settings.Slack, r, releaseError)
11
+		switch e.Type {
12
+		case history.EventRelease:
13
+			r := e.Metadata.(*history.ReleaseEventMetadata)
14
+			return slackNotifyRelease(cfg.Settings.Slack, r, r.Error)
15
+		case history.EventAutoRelease:
16
+			r := e.Metadata.(*history.AutoReleaseEventMetadata)
17
+			return slackNotifyAutoRelease(cfg.Settings.Slack, r, r.Error)
18
+		}
20 19
 	}
21
-	return err
20
+	return nil
22 21
 }

+ 14
- 11
notifications/notifications_test.go View File

@@ -26,15 +26,17 @@ func exampleRelease(t *testing.T) *history.ReleaseEventMetadata {
26 26
 			Kind:         update.ReleaseKindExecute,
27 27
 			Excludes:     nil,
28 28
 		},
29
-		Result: update.Result{
30
-			flux.ServiceID("default/helloworld"): {
31
-				Status: update.ReleaseStatusFailed,
32
-				Error:  "overall-release-error",
33
-				PerContainer: []update.ContainerUpdate{
34
-					update.ContainerUpdate{
35
-						Container: "container1",
36
-						Current:   img1a1,
37
-						Target:    img1a2,
29
+		ReleaseEventCommon: history.ReleaseEventCommon{
30
+			Result: update.Result{
31
+				flux.ServiceID("default/helloworld"): {
32
+					Status: update.ReleaseStatusFailed,
33
+					Error:  "overall-release-error",
34
+					PerContainer: []update.ContainerUpdate{
35
+						update.ContainerUpdate{
36
+							Container: "container1",
37
+							Current:   img1a1,
38
+							Target:    img1a2,
39
+						},
38 40
 					},
39 41
 				},
40 42
 			},
@@ -50,14 +52,15 @@ func TestRelease_DryRun(t *testing.T) {
50 52
 
51 53
 	// It should send releases to slack
52 54
 	r := exampleRelease(t)
55
+	ev := history.Event{Metadata: r}
53 56
 	r.Spec.Kind = update.ReleaseKindPlan
54
-	if err := Release(instance.Config{
57
+	if err := Event(instance.Config{
55 58
 		Settings: flux.UnsafeInstanceConfig{
56 59
 			Slack: flux.NotifierConfig{
57 60
 				HookURL: server.URL,
58 61
 			},
59 62
 		},
60
-	}, r, ""); err != nil {
63
+	}, ev); err != nil {
61 64
 		t.Fatal(err)
62 65
 	}
63 66
 }

+ 26
- 9
notifications/slack.go View File

@@ -19,7 +19,9 @@ import (
19 19
 )
20 20
 
21 21
 const (
22
-	defaultReleaseTemplate = `Release {{with .Release.Cause}}({{if .User}}{{.User}}{{else}}unknown{{end}}{{if .Message}}: {{.Message}}{{end}}){{end}} {{trim (print .Release.Spec.ImageSpec) "<>"}} to {{with .Release.Spec.ServiceSpecs}}{{range $index, $spec := .}}{{if not (eq $index 0)}}, {{if last $index $.Release.Spec.ServiceSpecs}}and {{end}}{{end}}{{trim (print .) "<>"}}{{end}}{{end}}. {{with .Error}}{{.}}. failed{{else}}done{{end}}`
22
+	ReleaseTemplate = `Release {{with .Release.Cause}}({{if .User}}{{.User}}{{else}}unknown{{end}}{{if .Message}}: {{.Message}}{{end}}){{end}} {{trim (print .Release.Spec.ImageSpec) "<>"}} to {{with .Release.Spec.ServiceSpecs}}{{range $index, $spec := .}}{{if not (eq $index 0)}}, {{if last $index $.Release.Spec.ServiceSpecs}}and {{end}}{{end}}{{trim (print .) "<>"}}{{end}}{{end}}.{{with .Error}} Failed: {{.}}{{end}}`
23
+
24
+	AutoReleaseTemplate = `Automated release of new image{{if not (last 0 $.Images)}}s{{end}} {{with .Images}}{{range $index, $image := .}}{{if not (eq $index 0)}}, {{if last $index $.Images}}and {{end}}{{end}}{{.}}{{end}}{{end}}.{{with .Error}} Failed: {{.}}{{end}}`
23 25
 )
24 26
 
25 27
 var (
@@ -27,20 +29,16 @@ var (
27 29
 )
28 30
 
29 31
 func slackNotifyRelease(config flux.NotifierConfig, release *history.ReleaseEventMetadata, releaseError string) error {
30
-	if release.Spec.Kind == update.ReleaseKindPlan {
32
+	// sanity check: we shouldn't get any other kind, but you
33
+	// never know.
34
+	if release.Spec.Kind != update.ReleaseKindExecute {
31 35
 		return nil
32 36
 	}
33
-
34
-	template := defaultReleaseTemplate
35
-	if config.ReleaseTemplate != "" {
36
-		template = config.ReleaseTemplate
37
-	}
38
-
39 37
 	errorMessage := ""
40 38
 	if releaseError != "" {
41 39
 		errorMessage = releaseError
42 40
 	}
43
-	text, err := instantiateTemplate("release", template, struct {
41
+	text, err := instantiateTemplate("release", ReleaseTemplate, struct {
44 42
 		Release *history.ReleaseEventMetadata
45 43
 		Error   string
46 44
 	}{
@@ -54,6 +52,25 @@ func slackNotifyRelease(config flux.NotifierConfig, release *history.ReleaseEven
54 52
 	return notify(config, text)
55 53
 }
56 54
 
55
+func slackNotifyAutoRelease(config flux.NotifierConfig, release *history.AutoReleaseEventMetadata, releaseError string) error {
56
+	errorMessage := ""
57
+	if releaseError != "" {
58
+		errorMessage = releaseError
59
+	}
60
+	text, err := instantiateTemplate("auto-release", AutoReleaseTemplate, struct {
61
+		Images []flux.ImageID
62
+		Error  string
63
+	}{
64
+		Images: release.Spec.Images(),
65
+		Error:  errorMessage,
66
+	})
67
+	if err != nil {
68
+		return err
69
+	}
70
+
71
+	return notify(config, text)
72
+}
73
+
57 74
 func notify(config flux.NotifierConfig, text string) error {
58 75
 	buf := &bytes.Buffer{}
59 76
 	if err := json.NewEncoder(buf).Encode(map[string]string{

+ 1
- 40
notifications/slack_test.go View File

@@ -44,7 +44,7 @@ func TestSlackNotifier(t *testing.T) {
44 44
 	}
45 45
 	for k, expectedV := range map[string]string{
46 46
 		"username": "user1",
47
-		"text":     "Release (test-user: this was to test notifications) all latest to default/helloworld. test-error. failed",
47
+		"text":     "Release (test-user: this was to test notifications) all latest to default/helloworld. Failed: test-error",
48 48
 	} {
49 49
 		if v, ok := body[k]; !ok || v != expectedV {
50 50
 			t.Errorf("Expected %s to have been set to %q, but got: %q", k, expectedV, v)
@@ -66,45 +66,6 @@ func TestSlackNotifierDryRun(t *testing.T) {
66 66
 	}
67 67
 }
68 68
 
69
-func TestSlackNotifierCustomTemplate(t *testing.T) {
70
-	var gotReq *http.Request
71
-	var bodyBuffer bytes.Buffer
72
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
73
-		gotReq = r
74
-		io.Copy(&bodyBuffer, r.Body)
75
-		w.WriteHeader(200)
76
-	}))
77
-	defer server.Close()
78
-
79
-	// It should send releases to slack
80
-	if err := slackNotifyRelease(flux.NotifierConfig{
81
-		HookURL:         server.URL,
82
-		ReleaseTemplate: "My custom template here",
83
-	}, exampleRelease(t), "test-error"); err != nil {
84
-		t.Fatal(err)
85
-	}
86
-	if gotReq == nil {
87
-		t.Fatal("Expected a request to slack to have been made")
88
-	}
89
-
90
-	// Req should be a post
91
-	if gotReq.Method != "POST" {
92
-		t.Errorf("Expected request method to be POST, but got %q", gotReq.Method)
93
-	}
94
-
95
-	body := map[string]string{}
96
-	if err := json.NewDecoder(&bodyBuffer).Decode(&body); err != nil {
97
-		t.Fatal(err)
98
-	}
99
-	for k, expectedV := range map[string]string{
100
-		"text": "My custom template here",
101
-	} {
102
-		if v, ok := body[k]; !ok || v != expectedV {
103
-			t.Errorf("Expected %s to have been set to %q, but got: %q", k, expectedV, v)
104
-		}
105
-	}
106
-}
107
-
108 69
 func TestSlackNotifierErrorHandling(t *testing.T) {
109 70
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
110 71
 		w.WriteHeader(500)

+ 7
- 11
server/server.go View File

@@ -143,17 +143,13 @@ func (s *Server) LogEvent(instID flux.InstanceID, e history.Event) error {
143 143
 		return errors.Wrapf(err, "logging event")
144 144
 	}
145 145
 
146
-	// If this is a release
147
-	if e.Type == history.EventRelease {
148
-		cfg, err := helper.Config.Get()
149
-		if err != nil {
150
-			return errors.Wrapf(err, "getting config")
151
-		}
152
-		releaseMeta := e.Metadata.(*history.ReleaseEventMetadata)
153
-		err = notifications.Release(cfg, releaseMeta, releaseMeta.Error)
154
-		if err != nil {
155
-			return errors.Wrapf(err, "notifying slack")
156
-		}
146
+	cfg, err := helper.Config.Get()
147
+	if err != nil {
148
+		return errors.Wrapf(err, "getting config")
149
+	}
150
+	err = notifications.Event(cfg, e)
151
+	if err != nil {
152
+		return errors.Wrapf(err, "notifying slack")
157 153
 	}
158 154
 	return nil
159 155
 }

+ 17
- 17
update/automated.go View File

@@ -10,17 +10,17 @@ import (
10 10
 )
11 11
 
12 12
 type Automated struct {
13
-	changes []change
13
+	Changes []Change
14 14
 }
15 15
 
16
-type change struct {
17
-	service   flux.ServiceID
18
-	container cluster.Container
19
-	imageID   flux.ImageID
16
+type Change struct {
17
+	ServiceID flux.ServiceID
18
+	Container cluster.Container
19
+	ImageID   flux.ImageID
20 20
 }
21 21
 
22 22
 func (a *Automated) Add(service flux.ServiceID, container cluster.Container, image flux.ImageID) {
23
-	a.changes = append(a.changes, change{service, container, image})
23
+	a.Changes = append(a.Changes, Change{service, container, image})
24 24
 }
25 25
 
26 26
 func (a *Automated) CalculateRelease(rc ReleaseContext, logger log.Logger) ([]*ServiceUpdate, Result, error) {
@@ -54,16 +54,16 @@ func (a *Automated) ReleaseKind() ReleaseKind {
54 54
 
55 55
 func (a *Automated) CommitMessage() string {
56 56
 	var images []string
57
-	for _, image := range a.images() {
57
+	for _, image := range a.Images() {
58 58
 		images = append(images, image.String())
59 59
 	}
60 60
 	return fmt.Sprintf("Release %s to automated", strings.Join(images, ", "))
61 61
 }
62 62
 
63
-func (a *Automated) images() []flux.ImageID {
63
+func (a *Automated) Images() []flux.ImageID {
64 64
 	imageMap := map[flux.ImageID]struct{}{}
65
-	for _, change := range a.changes {
66
-		imageMap[change.imageID] = struct{}{}
65
+	for _, change := range a.Changes {
66
+		imageMap[change.ImageID] = struct{}{}
67 67
 	}
68 68
 	var images []flux.ImageID
69 69
 	for image, _ := range imageMap {
@@ -112,11 +112,11 @@ func (a *Automated) calculateImageUpdates(rc ReleaseContext, candidates []*Servi
112 112
 			}
113 113
 
114 114
 			for _, change := range changes {
115
-				if change.container.Name != container.Name {
115
+				if change.Container.Name != container.Name {
116 116
 					continue
117 117
 				}
118 118
 
119
-				u.ManifestBytes, err = rc.Manifests().UpdateDefinition(u.ManifestBytes, container.Name, change.imageID)
119
+				u.ManifestBytes, err = rc.Manifests().UpdateDefinition(u.ManifestBytes, container.Name, change.ImageID)
120 120
 				if err != nil {
121 121
 					return nil, err
122 122
 				}
@@ -124,7 +124,7 @@ func (a *Automated) calculateImageUpdates(rc ReleaseContext, candidates []*Servi
124 124
 				containerUpdates = append(containerUpdates, ContainerUpdate{
125 125
 					Container: container.Name,
126 126
 					Current:   currentImageID,
127
-					Target:    change.imageID,
127
+					Target:    change.ImageID,
128 128
 				})
129 129
 			}
130 130
 		}
@@ -147,10 +147,10 @@ func (a *Automated) calculateImageUpdates(rc ReleaseContext, candidates []*Servi
147 147
 	return updates, nil
148 148
 }
149 149
 
150
-func (a *Automated) serviceMap() map[flux.ServiceID][]change {
151
-	set := map[flux.ServiceID][]change{}
152
-	for _, change := range a.changes {
153
-		set[change.service] = append(set[change.service], change)
150
+func (a *Automated) serviceMap() map[flux.ServiceID][]Change {
151
+	set := map[flux.ServiceID][]Change{}
152
+	for _, change := range a.Changes {
153
+		set[change.ServiceID] = append(set[change.ServiceID], change)
154 154
 	}
155 155
 	return set
156 156
 }

+ 7
- 0
update/spec.go View File

@@ -10,6 +10,7 @@ import (
10 10
 const (
11 11
 	Images = "image"
12 12
 	Policy = "policy"
13
+	Auto   = "auto"
13 14
 )
14 15
 
15 16
 // How did this update get triggered?
@@ -51,6 +52,12 @@ func (spec *Spec) UnmarshalJSON(in []byte) error {
51 52
 			return err
52 53
 		}
53 54
 		spec.Spec = update
55
+	case Auto:
56
+		var update Automated
57
+		if err := json.Unmarshal(wire.SpecBytes, &update); err != nil {
58
+			return err
59
+		}
60
+		spec.Spec = update
54 61
 	default:
55 62
 		return errors.New("unknown spec type: " + wire.Type)
56 63
 	}

Loading…
Cancel
Save