Browse Source

Use kubeyaml to do image updates

This wipes out a bunch of regexp-(ab)using code, in favour of using a
self-contained binary from an image, which is expected to be on the
`$PATH`.

For the tests, we have to have a `kubectl` in the `$PATH`. A small
shell script shim suffices.

To make the binary available in a pod, the kubeyaml image can be
called as an initContainer, copying the files to a shared volume.
Michael Bridgen 1 year ago
parent
commit
86ffeab273
5 changed files with 29 additions and 164 deletions
  1. 1
    1
      Makefile
  2. 2
    0
      bin/kubeyaml
  3. 4
    0
      cluster/kubernetes/policies.go
  4. 22
    162
      cluster/kubernetes/update.go
  5. 0
    1
      http/daemon/upstream.go

+ 1
- 1
Makefile View File

@@ -37,7 +37,7 @@ realclean: clean
37 37
 	rm -rf ./cache
38 38
 
39 39
 test:
40
-	go test ${TEST_FLAGS} $(shell go list ./... | grep -v "^github.com/weaveworks/flux/vendor" | sort -u)
40
+	PATH=${PATH}:${PWD}/bin go test ${TEST_FLAGS} $(shell go list ./... | grep -v "^github.com/weaveworks/flux/vendor" | sort -u)
41 41
 
42 42
 build/.%.done: docker/Dockerfile.%
43 43
 	mkdir -p ./build/docker/$*

+ 2
- 0
bin/kubeyaml View File

@@ -0,0 +1,2 @@
1
+#!/bin/sh
2
+docker run --rm -i squaremo/kubeyaml:latest "$@"

+ 4
- 0
cluster/kubernetes/policies.go View File

@@ -153,3 +153,7 @@ func (m *Manifests) ServicesWithPolicies(root string) (policy.ResourceMap, error
153 153
 	}
154 154
 	return result, nil
155 155
 }
156
+
157
+func multilineRE(lines ...string) *regexp.Regexp {
158
+	return regexp.MustCompile(`(?m:^` + strings.Join(lines, "\n") + `$)`)
159
+}

+ 22
- 162
cluster/kubernetes/update.go View File

@@ -2,14 +2,13 @@ package kubernetes
2 2
 
3 3
 import (
4 4
 	"bytes"
5
-	"fmt"
6
-	"io"
7
-	"regexp"
5
+	"os/exec"
8 6
 	"strings"
9 7
 
8
+	"github.com/pkg/errors"
9
+
10 10
 	"github.com/weaveworks/flux"
11 11
 	"github.com/weaveworks/flux/image"
12
-	"github.com/weaveworks/flux/resource"
13 12
 )
14 13
 
15 14
 // updatePodController takes the body of a resource definition
@@ -20,165 +19,26 @@ import (
20 19
 // where all references to the old image have been replaced with the
21 20
 // new one.
22 21
 //
23
-// This function has some requirements of the YAML structure. Read the
24
-// source and comments below to learn about them.
25
-func updatePodController(def []byte, resourceID flux.ResourceID, container string, newImageID image.Ref) ([]byte, error) {
26
-	// Sanity check
27
-	obj, err := parseObj(def)
28
-	if err != nil {
29
-		return nil, err
30
-	}
31
-
32
-	if _, ok := resourceKinds[strings.ToLower(obj.Kind)]; !ok {
33
-		return nil, UpdateNotSupportedError(obj.Kind)
34
-	}
35
-
36
-	var buf bytes.Buffer
37
-	err = tryUpdate(def, resourceID, container, newImageID, &buf)
38
-	return buf.Bytes(), err
39
-}
40
-
41
-// Attempt to update an RC or Deployment config. This makes several assumptions
42
-// that are justified only with the phrase "because that's how we do it",
43
-// including:
44
-//
45
-//  * the file is a replication controller or deployment
46
-//  * the update is from one tag of an image to another tag of the
47
-//    same image; e.g., "weaveworks/helloworld:a00001" to
48
-//    "weaveworks/helloworld:a00002"
49
-//  * the container spec to update is the (first) one that uses the
50
-//    same image name (e.g., weaveworks/helloworld)
51
-//  * the name of the controller is updated to reflect the new tag
52
-//  * there's a label which must be updated in both the pod spec and the selector
53
-//  * the file uses canonical YAML syntax, that is, one line per item
54
-//  * ... other assumptions as encoded in the regular expressions used
55
-//
56
-// Here's an example of the assumed structure:
57
-//
58
-// ```
59
-// apiVersion: v1
60
-// kind: Deployment # not presently checked
61
-// metadata:                         # )
62
-//   ...                             # ) any number of equally-indented lines
63
-//   name: helloworld-master-a000001 # ) can precede the name
64
-// spec:
65
-//   replicas: 2
66
-//   selector:                 # )
67
-//     name: helloworld        # ) this use of labels is assumed
68
-//     version: master-a000001 # )
69
-//   template:
70
-//     metadata:
71
-//       labels:                   # )
72
-//         name: helloworld        # ) this structure is assumed, as for the selector
73
-//         version: master-a000001 # )
74
-//     spec:
75
-//       containers:
76
-//       # extra container specs are allowed here ...
77
-//       - name: helloworld                                    # )
78
-//         image: quay.io/weaveworks/helloworld:master-a000001 # ) these must be together
79
-//         args:
80
-//         - -msg=Ahoy
81
-//         ports:
82
-//         - containerPort: 80
83
-// ```
84
-func tryUpdate(def []byte, id flux.ResourceID, container string, newImage image.Ref, out io.Writer) error {
85
-	containers, err := extractContainers(def, id)
86
-
87
-	matchingContainers := map[int]resource.Container{}
88
-	for i, c := range containers {
89
-		if c.Name != container {
90
-			continue
91
-		}
92
-		currentImage := c.Image
93
-		if err != nil {
94
-			return fmt.Errorf("could not parse image %s", c.Image)
95
-		}
96
-		if currentImage.CanonicalName() == newImage.CanonicalName() {
97
-			matchingContainers[i] = c
98
-		}
99
-	}
100
-
101
-	if len(matchingContainers) == 0 {
102
-		return fmt.Errorf("could not find container using image: %s", newImage.Repository())
103
-	}
104
-
105
-	// Detect how indented the "containers" block is.
106
-	// TODO: delete all regular expressions which are used to modify YAML.
107
-	// See #1019. Modifying this is not recommended.
108
-	newDef := string(def)
109
-	matches := regexp.MustCompile(`( +)containers:.*`).FindStringSubmatch(newDef)
110
-	if len(matches) != 2 {
111
-		return fmt.Errorf("could not find container specs")
112
-	}
113
-	indent := matches[1]
114
-
115
-	// TODO: delete all regular expressions which are used to modify YAML.
116
-	// See #1019. Modifying this is not recommended.
117
-	optq := `["']?` // An optional single or double quote
118
-	// Replace the container images
119
-	// Parse out all the container blocks
120
-	containersRE := regexp.MustCompile(`(?m:^` + indent + `containers:\s*(?:#.*)*$(?:\n(?:` + indent + `[-\s#].*)?)*)`)
121
-	// Parse out an individual container blog
122
-	containerRE := regexp.MustCompile(`(?m:` + indent + `-.*(?:\n(?:` + indent + `\s+.*)?)*)`)
123
-	// Parse out the image ID
124
-	imageRE := regexp.MustCompile(`(` + indent + `[-\s]\s*` + optq + `image` + optq + `:\s*)` + optq + `(?:[\w\.\-/:]+\s*?)*` + optq + `([\t\f #]+.*)?`)
125
-	imageReplacement := fmt.Sprintf("${1}%s${2}", maybeQuote(newImage.String()))
126
-	// Find the block of container specs
127
-	newDef = containersRE.ReplaceAllStringFunc(newDef, func(containers string) string {
128
-		i := 0
129
-		// Find each container spec
130
-		return containerRE.ReplaceAllStringFunc(containers, func(spec string) string {
131
-			if _, ok := matchingContainers[i]; ok {
132
-				// container matches, let's replace the image
133
-				spec = imageRE.ReplaceAllString(spec, imageReplacement)
134
-				delete(matchingContainers, i)
135
-			}
136
-			i++
137
-			return spec
138
-		})
139
-	})
140
-
141
-	if len(matchingContainers) > 0 {
142
-		missed := []string{}
143
-		for _, c := range matchingContainers {
144
-			missed = append(missed, c.Name)
145
-		}
146
-		return fmt.Errorf("did not update expected containers: %s", strings.Join(missed, ", "))
22
+// This function has many additional requirements that are likely in flux. Read
23
+// the source to learn about them.
24
+func updatePodController(file []byte, resource flux.ResourceID, container string, newImageID image.Ref) ([]byte, error) {
25
+	namespace, kind, name := resource.Components()
26
+	if _, ok := resourceKinds[strings.ToLower(kind)]; !ok {
27
+		return nil, UpdateNotSupportedError(kind)
147 28
 	}
148 29
 
149
-	// TODO: delete all regular expressions which are used to modify YAML.
150
-	// See #1019. Modifying this is not recommended.
151
-	// Replacing labels: these are in two places, the container template and the selector
152
-	// TODO: This doesn't handle # comments
153
-	// TODO: This encodes an expectation of map keys being ordered (i.e. name *then* version)
154
-	// TODO: This assumes that these are indented by exactly 2 spaces (which may not be true)
155
-	replaceLabelsRE := multilineRE(
156
-		`((?:  selector|      labels):.*)`,
157
-		`((?:  ){2,4}name:.*)`,
158
-		`((?:  ){2,4}version:\s*) (?:`+optq+`[-\w]+`+optq+`)(\s.*)`,
159
-	)
160
-	replaceLabels := fmt.Sprintf("$1\n$2\n$3 %s$4", maybeQuote(newImage.Tag))
161
-	newDef = replaceLabelsRE.ReplaceAllString(newDef, replaceLabels)
162
-
163
-	fmt.Fprint(out, newDef)
164
-	return nil
165
-}
166
-
167
-func multilineRE(lines ...string) *regexp.Regexp {
168
-	return regexp.MustCompile(`(?m:^` + strings.Join(lines, "\n") + `$)`)
169
-}
170
-
171
-// TODO: delete all regular expressions which are used to modify YAML.
172
-// See #1019. Modifying this is not recommended.
173
-var looksLikeNumber *regexp.Regexp = regexp.MustCompile("^(" + strings.Join([]string{
174
-	`(-?[1-9](\.[0-9]*[1-9])?(e[-+][1-9][0-9]*)?)`,
175
-	`(-?(0|[1-9][0-9]*))`,
176
-	`(0|(\.inf)|(-\.inf)|(\.nan))`},
177
-	"|") + ")$")
178
-
179
-func maybeQuote(scalar string) string {
180
-	if looksLikeNumber.MatchString(scalar) {
181
-		return `"` + scalar + `"`
30
+	args := []string{"--namespace", namespace, "--kind", kind, "--name", name}
31
+	args = append(args, "--container", container, "--image", newImageID.String())
32
+
33
+	println("TRACE:", "kubeyaml", strings.Join(args, " "))
34
+	cmd := exec.Command("kubeyaml", args...)
35
+	out := &bytes.Buffer{}
36
+	errOut := &bytes.Buffer{}
37
+	cmd.Stdin = bytes.NewBuffer(file)
38
+	cmd.Stdout = out
39
+	cmd.Stderr = errOut
40
+	if err := cmd.Run(); err != nil {
41
+		return nil, errors.Wrap(err, errOut.String())
182 42
 	}
183
-	return scalar
43
+	return out.Bytes(), nil
184 44
 }

+ 0
- 1
http/daemon/upstream.go View File

@@ -175,7 +175,6 @@ func (a *Upstream) setConnectionDuration(duration float64) {
175 175
 }
176 176
 
177 177
 func (a *Upstream) LogEvent(event event.Event) error {
178
-	// Instance ID is set via token here, so we can leave it blank.
179 178
 	return a.apiClient.LogEvent(context.TODO(), event)
180 179
 }
181 180
 

Loading…
Cancel
Save