Browse Source

Verify releases with a model comparison

This commit fills in the detail of verifying releases, by comparing
the model before with the model after to make sure only the intended
changes have been made.
Michael Bridgen 2 years ago
parent
commit
a782c4ce2b

+ 7
- 0
cluster/kubernetes/resource/cronjob.go View File

@@ -1,6 +1,7 @@
1 1
 package resource
2 2
 
3 3
 import (
4
+	"github.com/weaveworks/flux/image"
4 5
 	"github.com/weaveworks/flux/resource"
5 6
 )
6 7
 
@@ -20,3 +21,9 @@ type CronJobSpec struct {
20 21
 func (c CronJob) Containers() []resource.Container {
21 22
 	return c.Spec.JobTemplate.Spec.Template.Containers()
22 23
 }
24
+
25
+func (c CronJob) SetContainerImage(container string, ref image.Ref) error {
26
+	return c.Spec.JobTemplate.Spec.Template.SetContainerImage(container, ref)
27
+}
28
+
29
+var _ resource.Workload = CronJob{}

+ 10
- 5
cluster/kubernetes/resource/daemonset.go View File

@@ -1,18 +1,23 @@
1 1
 package resource
2 2
 
3 3
 import (
4
+	"github.com/weaveworks/flux/image"
4 5
 	"github.com/weaveworks/flux/resource"
5 6
 )
6 7
 
7 8
 type DaemonSet struct {
8 9
 	baseObject
9
-	Spec DaemonSetSpec
10
-}
11
-
12
-type DaemonSetSpec struct {
13
-	Template PodTemplate
10
+	Spec struct {
11
+		Template PodTemplate
12
+	}
14 13
 }
15 14
 
16 15
 func (ds DaemonSet) Containers() []resource.Container {
17 16
 	return ds.Spec.Template.Containers()
18 17
 }
18
+
19
+func (ds DaemonSet) SetContainerImage(container string, ref image.Ref) error {
20
+	return ds.Spec.Template.SetContainerImage(container, ref)
21
+}
22
+
23
+var _ resource.Workload = DaemonSet{}

+ 7
- 0
cluster/kubernetes/resource/deployment.go View File

@@ -1,6 +1,7 @@
1 1
 package resource
2 2
 
3 3
 import (
4
+	"github.com/weaveworks/flux/image"
4 5
 	"github.com/weaveworks/flux/resource"
5 6
 )
6 7
 
@@ -17,3 +18,9 @@ type DeploymentSpec struct {
17 18
 func (d Deployment) Containers() []resource.Container {
18 19
 	return d.Spec.Template.Containers()
19 20
 }
21
+
22
+func (d Deployment) SetContainerImage(container string, ref image.Ref) error {
23
+	return d.Spec.Template.SetContainerImage(container, ref)
24
+}
25
+
26
+var _ resource.Workload = Deployment{}

+ 12
- 0
cluster/kubernetes/resource/spec.go View File

@@ -1,6 +1,8 @@
1 1
 package resource
2 2
 
3 3
 import (
4
+	"fmt"
5
+
4 6
 	"github.com/weaveworks/flux/image"
5 7
 	"github.com/weaveworks/flux/resource"
6 8
 )
@@ -28,6 +30,16 @@ func (t PodTemplate) Containers() []resource.Container {
28 30
 	return result
29 31
 }
30 32
 
33
+func (t PodTemplate) SetContainerImage(container string, ref image.Ref) error {
34
+	for i, c := range t.Spec.Containers {
35
+		if c.Name == container {
36
+			t.Spec.Containers[i].Image = ref.String()
37
+			return nil
38
+		}
39
+	}
40
+	return fmt.Errorf("container %q not found in workload", container)
41
+}
42
+
31 43
 type PodSpec struct {
32 44
 	ImagePullSecrets []struct{ Name string }
33 45
 	Volumes          []Volume

+ 7
- 0
cluster/kubernetes/resource/statefulset.go View File

@@ -1,6 +1,7 @@
1 1
 package resource
2 2
 
3 3
 import (
4
+	"github.com/weaveworks/flux/image"
4 5
 	"github.com/weaveworks/flux/resource"
5 6
 )
6 7
 
@@ -17,3 +18,9 @@ type StatefulSetSpec struct {
17 18
 func (ss StatefulSet) Containers() []resource.Container {
18 19
 	return ss.Spec.Template.Containers()
19 20
 }
21
+
22
+func (ss StatefulSet) SetContainerImage(container string, ref image.Ref) error {
23
+	return ss.Spec.Template.SetContainerImage(container, ref)
24
+}
25
+
26
+var _ resource.Workload = StatefulSet{}

+ 30
- 0
release/errors.go View File

@@ -0,0 +1,30 @@
1
+package release
2
+
3
+import (
4
+	fluxerr "github.com/weaveworks/flux/errors"
5
+)
6
+
7
+func MakeReleaseError(err error) *fluxerr.Error {
8
+	return &fluxerr.Error{
9
+		Type: fluxerr.User,
10
+		Help: `The release process failed, with this message:
11
+
12
+    ` + err.Error() + `
13
+
14
+This may be because of a limitation in the formats of file Flux can
15
+deal with. See
16
+
17
+    https://github.com/weaveworks/flux/blob/master/site/requirements.md
18
+
19
+for those limitations.
20
+
21
+If your files appear to meet the requirements, it may simply be a bug
22
+in Flux. PLease report it at
23
+
24
+    https://github.com/weaveworks/flux/issues
25
+
26
+and try to include the problematic manifest, if it can be identified.
27
+`,
28
+		Err: err,
29
+	}
30
+}

+ 66
- 0
release/releaser.go View File

@@ -1,6 +1,8 @@
1 1
 package release
2 2
 
3 3
 import (
4
+	"fmt"
5
+	"strings"
4 6
 	"time"
5 7
 
6 8
 	"github.com/go-kit/kit/log"
@@ -42,6 +44,9 @@ func Release(rc *ReleaseContext, changes Changes, logger log.Logger) (results up
42 44
 			err = VerifyChanges(before, updates, after)
43 45
 		}
44 46
 	}
47
+	if err != nil {
48
+		err = MakeReleaseError(err)
49
+	}
45 50
 	return results, err
46 51
 }
47 52
 
@@ -58,6 +63,67 @@ func ApplyChanges(rc *ReleaseContext, updates []*update.ControllerUpdate, logger
58 63
 	return err
59 64
 }
60 65
 
66
+// VerifyChanges checks that the `after` resources are exactly the
67
+// `before` resources with the updates applied. It destructively
68
+// updates `before`.
61 69
 func VerifyChanges(before map[string]resource.Resource, updates []*update.ControllerUpdate, after map[string]resource.Resource) error {
70
+	timer := update.NewStageTimer("verify_changes")
71
+	defer timer.ObserveDuration()
72
+
73
+	for _, update := range updates {
74
+		res, ok := before[update.ResourceID.String()]
75
+		if !ok {
76
+			return fmt.Errorf("resource %q mentioned in update not found in resources", update.ResourceID.String())
77
+		}
78
+		wl, ok := res.(resource.Workload)
79
+		if !ok {
80
+			return fmt.Errorf("resource %q mentioned in update is not a workload", update.ResourceID.String())
81
+		}
82
+		for _, containerUpdate := range update.Updates {
83
+			wl.SetContainerImage(containerUpdate.Container, containerUpdate.Target)
84
+		}
85
+	}
86
+
87
+	for id, afterRes := range after {
88
+		beforeRes, ok := before[id]
89
+		if !ok {
90
+			return fmt.Errorf("resource %q is new after update")
91
+		}
92
+		delete(before, id)
93
+
94
+		beforeWorkload, ok := beforeRes.(resource.Workload)
95
+		if !ok {
96
+			// the resource in question isn't a workload, so ignore it.
97
+			continue
98
+		}
99
+		afterWorkload, ok := afterRes.(resource.Workload)
100
+		if !ok {
101
+			return fmt.Errorf("resource %q is no longer a workload (Deployment or DaemonSet, or similar) after update", id)
102
+		}
103
+
104
+		beforeContainers := beforeWorkload.Containers()
105
+		afterContainers := afterWorkload.Containers()
106
+		if len(beforeContainers) != len(afterContainers) {
107
+			return fmt.Errorf("resource %q has different set of containers after update", id)
108
+		}
109
+		for i := range afterContainers {
110
+			if beforeContainers[i].Name != afterContainers[i].Name {
111
+				return fmt.Errorf("Container [%d] has a different name after update: was %q, now %q", i, beforeContainers[i].Name, afterContainers[i].Name)
112
+			}
113
+			if beforeContainers[i].Image != afterContainers[i].Image {
114
+				return fmt.Errorf("The image for container %q in resource %q was changed (to %q) and should not have been", beforeContainers[i].Name, id, afterContainers[i].Image.String())
115
+			}
116
+		}
117
+
118
+	}
119
+
120
+	var disappeared []string
121
+	for id := range before {
122
+		disappeared = append(disappeared, fmt.Sprintf("%q", id))
123
+	}
124
+	if len(disappeared) > 0 {
125
+		return fmt.Errorf("resources {%s} present before update but not after", strings.Join(disappeared, ", "))
126
+	}
127
+
62 128
 	return nil
63 129
 }

+ 4
- 0
resource/resource.go View File

@@ -22,4 +22,8 @@ type Container struct {
22 22
 type Workload interface {
23 23
 	Resource
24 24
 	Containers() []Container
25
+	// SetContainerImage mutates this workload so that the container
26
+	// named has the image given. This is not expected to have an
27
+	// effect on any underlying file or cluster resource.
28
+	SetContainerImage(container string, ref image.Ref) error
25 29
 }

Loading…
Cancel
Save