From eb1d46940818c70f988b6469eb9568549a0cec4c Mon Sep 17 00:00:00 2001 From: David Sauer Date: Sat, 14 Nov 2020 15:34:33 +0100 Subject: [PATCH] extract image digest from buildkit logs --- hack/sniffer/main.go | 8 ++-- pkg/build.go | 57 ++++++++++++++----------- pkg/build_test.go | 78 +++++++++++++++++++++++++++++++++++ pkg/kubernetes.go | 2 - pkg/mock.go | 52 +++++++++++++++++++++++ pkg/pull.go | 44 ++------------------ pkg/push.go | 15 ++----- pkg/tag.go | 2 +- tests/Tiltfile | 8 ++-- tests/docker-build.yaml | 2 +- tests/tilt-ci.yaml | 14 +++++++ tests/tilt/Dockerfile | 3 -- tests/tilt/Tiltfile | 8 ++-- tests/tilt/image-a/Dockerfile | 3 ++ tests/tilt/image-b/Dockerfile | 3 ++ tests/tilt/kubernetes.yaml | 52 +++++++++++++++++++---- 16 files changed, 250 insertions(+), 101 deletions(-) create mode 100644 pkg/build_test.go delete mode 100644 tests/tilt/Dockerfile create mode 100644 tests/tilt/image-a/Dockerfile create mode 100644 tests/tilt/image-b/Dockerfile diff --git a/hack/sniffer/main.go b/hack/sniffer/main.go index cb2dda93..d59f086d 100644 --- a/hack/sniffer/main.go +++ b/hack/sniffer/main.go @@ -16,6 +16,8 @@ import ( func main() { http.HandleFunc("/", sniffer) + log.Println("proxy http traffic from :23765 to http://127.0.0.1:2376") + if err := http.ListenAndServe(":23765", nil); err != nil { log.Fatalf("listen and serve: %v", err) } @@ -53,7 +55,7 @@ func serveReverseProxy(target string, w http.ResponseWriter, r *http.Request) er return printRespose(os.Stdout, w) } - proxy.ServeHTTP(r, w) + proxy.ServeHTTP(w, r) return nil } @@ -66,7 +68,7 @@ func printRequest(w io.Writer, r *http.Request) (*bytes.Reader, error) { return nil, fmt.Errorf("reading body: %v", err) } - w.Write([]byte(fmt.Sprintf("req: %v\n\n", req))) + w.Write([]byte(fmt.Sprintf("req: %v\n\n", r))) r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) reader := bytes.NewReader(body) @@ -75,7 +77,7 @@ func printRequest(w io.Writer, r *http.Request) (*bytes.Reader, error) { } func printRespose(w io.Writer, resp *http.Response) error { - b, err := ioutil.ReadAll(wp.Body) + b, err := ioutil.ReadAll(resp.Body) if err != nil { return err } diff --git a/pkg/build.go b/pkg/build.go index 5272d93d..dca6e291 100644 --- a/pkg/build.go +++ b/pkg/build.go @@ -1,6 +1,7 @@ package wedding import ( + "bytes" "context" "encoding/json" "fmt" @@ -9,6 +10,7 @@ import ( "log" "net/http" "os" + "regexp" "strconv" "time" @@ -67,13 +69,9 @@ func (s Service) build(w http.ResponseWriter, r *http.Request) { err = s.executeBuild(ctx, cfg, w) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("execute build: %v", err))) log.Printf("execute build: %v", err) return } - - w.Write([]byte(`{"aux":{"ID":"sha256:42341736246f8e99122d49e4c0e414f0a3e5f69a024e72a2ac1a39a2093d483f"}}`)) } func buildParameters(r *http.Request) (*buildConfig, error) { @@ -316,9 +314,9 @@ func (s Service) executeBuild(ctx context.Context, cfg *buildConfig, w http.Resp imageNames += fmt.Sprintf("wedding-registry:5000/images/%s", tag) } - output := "--output type=image,push=true,name=wedding-registry:5000/digests" + destination := "--output type=image,push=true,name=wedding-registry:5000/digests" if imageNames != "" { - output = fmt.Sprintf("--output type=image,push=true,\"name=%s\"", imageNames) + destination = fmt.Sprintf("--output type=image,push=true,\"name=%s\"", imageNames) } // TODO add timeout for script @@ -340,7 +338,7 @@ buildctl-daemonless.sh \ %s \ --export-cache=type=registry,ref=wedding-registry:5000/cache-repo,mode=max \ --import-cache=type=registry,ref=wedding-registry:5000/cache-repo -`, presignedContextURL, output) +`, presignedContextURL, destination) pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -392,9 +390,15 @@ buildctl-daemonless.sh \ }, } - streamer := streamer{w: w} + o := &output{w: w} + d := &digestParser{w: o} + err = s.executePod(ctx, pod, d) + if err != nil { + o.Errorf("execute build: %v", err) + return err + } - err = s.executePod(ctx, pod, streamer) + err = d.publish(w) if err != nil { return err } @@ -402,28 +406,35 @@ buildctl-daemonless.sh \ return nil } -type streamer struct { - w io.Writer +type digestParser struct { + buf bytes.Buffer + w io.Writer } -func (s streamer) Write(b []byte) (int, error) { - i := len(b) +func (d *digestParser) publish(w io.Writer) error { + patterns := regexp. + MustCompile(`exporting manifest (sha256:[0-9a-f]+)`). + FindStringSubmatch(d.buf.String()) - b, err := json.Marshal(string(b)) - if err != nil { - panic(err) // encode a string to json should not fail + if len(patterns) != 2 || patterns[1] == "" { + return fmt.Errorf("digest not found") } - _, err = s.w.Write([]byte(fmt.Sprintf(`{"stream": %s}`, b))) + log.Printf("found digest: %s", patterns[1]) + + _, err := w.Write([]byte(fmt.Sprintf(`{"aux":{"ID":"%s"}}`, patterns[1]))) if err != nil { - return 0, err + return err } - if f, ok := s.w.(http.Flusher); ok { - f.Flush() - } else { - return 0, fmt.Errorf("stream can not be flushed") + return nil +} + +func (d *digestParser) Write(bb []byte) (int, error) { + _, err := d.buf.Write(bb) + if err != nil { + return 0, err } - return i, nil + return d.w.Write(bb) } diff --git a/pkg/build_test.go b/pkg/build_test.go new file mode 100644 index 00000000..2797e912 --- /dev/null +++ b/pkg/build_test.go @@ -0,0 +1,78 @@ +package wedding + +import ( + "bytes" + "io" + "io/ioutil" + "testing" +) + +func Test_digestParser_publish(t *testing.T) { + type fields struct { + buf bytes.Buffer + w io.Writer + } + tests := []struct { + name string + fields fields + input string + wantW string + wantErr bool + }{ + { + name: "found", + fields: fields{ + buf: bytes.Buffer{}, + w: ioutil.Discard, + }, + input: `#5 [2/2] RUN sleep 1 +#5 DONE 1.2s + +#7 exporting to image +#7 exporting layers +#7 exporting layers 0.4s done +#7 exporting manifest sha256:d8438874a02b14e2ad7be50f7505ec3d9fe645964e6987101179ef42f8bed5b6 0.0s done +#7 exporting config sha256:0d3cc5d5b92a708fbabb79b63b59839ca87012742a1b5e741cf51fd1ad14b804 0.0s done +#7 pushing layers +#7 pushing layers 0.2s done +#7 pushing manifest for wedding-registry:5000/digests:latest +#7 pushing manifest for wedding-registry:5000/digests:latest 0.1s done +#7 DONE 0.7s + +#8 exporting cache +#8 preparing build cache for export 0.1s done +#8 writing layer sha256:166a2418f7e86fa48d87bf6807b4e5b35f078acb2ad1cbf10444a7025913c24f +#8 writing layer sha256:166a2418f7e86fa48d87bf6807b4e5b35f078acb2ad1cbf10444a7025913c24f done +#8 writing layer sha256:1966ea362d2394e7c5c508ebf3695f039dd3825bd1e7a07449ae530aea3c4cd1 done +#8 writing layer sha256:5a9f1c0027a73bc0e66a469f90e47a59e23ab3472126ed28e6a4e7b1a98d1eb5 done +#8 writing layer sha256:b5c20b2b484f5ca9bc9d98dc79f8f1381ee0c063111ea0ddf42d1ae5ea942d50 done +#8 writing layer sha256:bb79b6b2107fea8e8a47133a660b78e3a546998fcf0427be39ac9a0af4a97e90 done +#8 writing layer sha256:e2eabaeb95d9574853154f705dc7ebce6184a95cd153e3ff87108e200267aa0a 0.1s done +#8 writing config sha256:d7e07a2c74d972a2a645ea9ba4d4970a71b2795611236ff61810cb30b60d7725 0.1s done +#8 writing manifest sha256:3acb5d16e32a8cf7094e195a5d24ca15d4fbe8a433a8bd5cc2365040739eb2dc`, + wantW: `{"aux":{"ID":"sha256:d8438874a02b14e2ad7be50f7505ec3d9fe645964e6987101179ef42f8bed5b6"}}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := digestParser{ + buf: tt.fields.buf, + w: tt.fields.w, + } + + if _, err := d.Write([]byte(tt.input)); err != nil { + t.Errorf("digestParser.Write() error = %v", err) + return + } + + w := &bytes.Buffer{} + if err := d.publish(w); (err != nil) != tt.wantErr { + t.Errorf("digestParser.publish() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("digestParser.publish() = %v, want %v", gotW, tt.wantW) + } + }) + } +} diff --git a/pkg/kubernetes.go b/pkg/kubernetes.go index 47fddc5a..19b4b211 100644 --- a/pkg/kubernetes.go +++ b/pkg/kubernetes.go @@ -33,8 +33,6 @@ func (s Service) executePod(ctx context.Context, pod *corev1.Pod, w io.Writer) e return } - return - w.Write([]byte("Deleting pod.\n")) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) diff --git a/pkg/mock.go b/pkg/mock.go index bc02c9a1..11838372 100644 --- a/pkg/mock.go +++ b/pkg/mock.go @@ -1,10 +1,62 @@ package wedding import ( + "encoding/json" "fmt" + "io" "net/http" ) +type output struct { + w io.Writer +} + +func (o output) Write(b []byte) (int, error) { + i := len(b) + + b, err := json.Marshal(string(b)) + if err != nil { + return 0, err + } + + _, err = o.w.Write([]byte(fmt.Sprintf(`{"stream": %s}`, b))) + if err != nil { + return 0, err + } + + if f, ok := o.w.(http.Flusher); ok { + f.Flush() + } else { + return 0, fmt.Errorf("stream can not be flushed") + } + + return i, nil +} + +func (o output) Errorf(e string, args ...interface{}) error { + return o.Error(fmt.Sprintf(e, args...)) +} + +func (o output) Error(e string) error { + b, err := json.Marshal(string(e)) + if err != nil { + return err + } + + _, err = o.w.Write([]byte(fmt.Sprintf(`{"errorDetail": {"code": %d, "message": %s}, "error": %s}`, 1, b, b))) + if err != nil { + return err + } + + if f, ok := o.w.(http.Flusher); ok { + f.Flush() + } else { + return fmt.Errorf("stream can not be flushed") + } + + return nil +} + func ping(w http.ResponseWriter, r *http.Request) { w.Header().Set("Api-Version", apiVersion) w.Header().Set("Docker-Experimental", "false") diff --git a/pkg/pull.go b/pkg/pull.go index dbd4285e..88a84cae 100644 --- a/pkg/pull.go +++ b/pkg/pull.go @@ -1,11 +1,8 @@ package wedding import ( - "bytes" "context" - "encoding/json" "fmt" - "io" "log" "net/http" "time" @@ -139,43 +136,10 @@ skopeo copy --dest-tls-verify=false docker://%s docker://%s }, } - b := &bytes.Buffer{} - messanger := streamer{w: w} - err = s.executePod(r.Context(), pod, b) + o := &output{w: w} + err = s.executePod(r.Context(), pod, o) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - io.Copy(w, b) - w.Write([]byte(fmt.Sprintf("execute push: %v", err))) - log.Printf("execute push: %v", err) - return - } - - w.WriteHeader(http.StatusOK) - io.Copy(messanger, b) -} - -type messanger struct { - w io.Writer -} - -func (m messanger) Write(b []byte) (int, error) { - i := len(b) - - b, err := json.Marshal(string(b)) - if err != nil { - panic(err) // encode a string to json should not fail - } - - _, err = m.w.Write([]byte(fmt.Sprintf(`{"message": %s}`, b))) - if err != nil { - return 0, err - } - - if f, ok := m.w.(http.Flusher); ok { - f.Flush() - } else { - return 0, fmt.Errorf("stream can not be flushed") + log.Printf("execute pull: %v", err) + o.Errorf("execute pull: %v", err) } - - return i, nil } diff --git a/pkg/push.go b/pkg/push.go index 6caa6836..ec7eb740 100644 --- a/pkg/push.go +++ b/pkg/push.go @@ -1,10 +1,8 @@ package wedding import ( - "bytes" "context" "fmt" - "io" "log" "net/http" "time" @@ -101,17 +99,10 @@ skopeo copy --src-tls-verify=false --dest-tls-verify=false docker://%s docker:// }, } - b := &bytes.Buffer{} - messanger := streamer{w: w} - err = s.executePod(r.Context(), pod, b) + o := &output{w: w} + err = s.executePod(r.Context(), pod, o) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - io.Copy(w, b) - w.Write([]byte(fmt.Sprintf("execute push: %v", err))) log.Printf("execute push: %v", err) - return + o.Errorf("execute push: %v", err) } - - w.WriteHeader(http.StatusOK) - io.Copy(messanger, b) } diff --git a/pkg/tag.go b/pkg/tag.go index fa922d1d..a8461d50 100644 --- a/pkg/tag.go +++ b/pkg/tag.go @@ -64,10 +64,10 @@ skopeo copy --src-tls-verify=false --dest-tls-verify=false docker://%s docker:// b := &bytes.Buffer{} err := s.executePod(r.Context(), pod, b) if err != nil { + log.Printf("execute tagging: %v", err) w.WriteHeader(http.StatusInternalServerError) io.Copy(w, b) w.Write([]byte(fmt.Sprintf("execute tagging: %v", err))) - log.Printf("execute tagging: %v", err) return } diff --git a/tests/Tiltfile b/tests/Tiltfile index 952691de..9e985c16 100644 --- a/tests/Tiltfile +++ b/tests/Tiltfile @@ -2,11 +2,11 @@ k8s_yaml('docker-build.yaml') k8s_resource('test-docker-build', resource_deps=['wedding']) -#k8s_yaml('docker-pull-tag-push.yaml') -#k8s_resource('test-docker-pull-tag-push', resource_deps=['wedding']) +k8s_yaml('docker-pull-tag-push.yaml') +k8s_resource('test-docker-pull-tag-push', resource_deps=['wedding']) -#k8s_yaml('tilt-ci.yaml') -#k8s_resource('test-tilt-ci', resource_deps=['wedding']) +k8s_yaml('tilt-ci.yaml') +k8s_resource('test-tilt-ci', resource_deps=['wedding']) docker_build( 'testing-image', diff --git a/tests/docker-build.yaml b/tests/docker-build.yaml index 97f63d7f..8a2b4670 100644 --- a/tests/docker-build.yaml +++ b/tests/docker-build.yaml @@ -18,7 +18,7 @@ spec: set -euxo pipefail docker build ./docker - # if docker build ./docker-broken; then echo "this should fail"; false; else echo "exit code propagated"; fi + if docker build ./docker-broken; then echo "this should fail"; false; else echo "exit code propagated"; fi echo "done" env: diff --git a/tests/tilt-ci.yaml b/tests/tilt-ci.yaml index 25986a8c..83e4ad26 100644 --- a/tests/tilt-ci.yaml +++ b/tests/tilt-ci.yaml @@ -26,9 +26,23 @@ spec: cd tilt tilt ci + + sleep 10 + + curl --fail -o a http://service-a/ + curl --fail -o b http://service-b/ + + echo a > a_ + echo b > b_ + + diff a a_ + diff b b_ + tilt down echo "done" + + sleep 120 env: - name: "DOCKER_HOST" value: "tcp://wedding:2376" diff --git a/tests/tilt/Dockerfile b/tests/tilt/Dockerfile deleted file mode 100644 index 6c010e7b..00000000 --- a/tests/tilt/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM nginx - -RUN sleep 1 diff --git a/tests/tilt/Tiltfile b/tests/tilt/Tiltfile index 3e127dab..f1828510 100644 --- a/tests/tilt/Tiltfile +++ b/tests/tilt/Tiltfile @@ -6,9 +6,7 @@ min_tilt_version('0.15.0') # includes fix for auto_init+False with tilt ci k8s_yaml('kubernetes.yaml') -docker_build('use-case-3-image', '.') +default_registry('registry.registry.svc:5000') -k8s_resource( - 'use-case-3', - port_forwards=['8080:80'], -) +docker_build('service-a-image', './image-a') +docker_build('service-b-image', './image-b') diff --git a/tests/tilt/image-a/Dockerfile b/tests/tilt/image-a/Dockerfile new file mode 100644 index 00000000..6e9fa641 --- /dev/null +++ b/tests/tilt/image-a/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx + +RUN echo "a" > /usr/share/nginx/html/index.html diff --git a/tests/tilt/image-b/Dockerfile b/tests/tilt/image-b/Dockerfile new file mode 100644 index 00000000..24ce391f --- /dev/null +++ b/tests/tilt/image-b/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx + +RUN echo "b" > /usr/share/nginx/html/index.html diff --git a/tests/tilt/kubernetes.yaml b/tests/tilt/kubernetes.yaml index 108065fe..8b72a350 100644 --- a/tests/tilt/kubernetes.yaml +++ b/tests/tilt/kubernetes.yaml @@ -1,32 +1,69 @@ apiVersion: v1 kind: Service metadata: - name: use-case-3 + name: service-a spec: selector: - app: use-case-3 + app: service-a ports: - name: nginx port: 80 --- +apiVersion: v1 +kind: Service +metadata: + name: service-b +spec: + selector: + app: service-b + ports: + - name: nginx + port: 80 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: service-a + labels: + app: service-a +spec: + selector: + matchLabels: + app: service-a + template: + metadata: + labels: + app: service-a + spec: + containers: + - name: nginx + image: service-a-image + ports: + - name: nginx + containerPort: 80 + readinessProbe: + httpGet: + path: / + port: 80 +--- apiVersion: apps/v1 kind: Deployment metadata: - name: use-case-3 + name: service-b labels: - app: use-case-3 + app: service-b spec: selector: matchLabels: - app: use-case-3 + app: service-b template: metadata: labels: - app: use-case-3 + app: service-b spec: containers: - name: nginx - image: use-case-3-image + image: service-b-image ports: - name: nginx containerPort: 80 @@ -34,3 +71,4 @@ spec: httpGet: path: / port: 80 +