From fbe3e058016f9dd6c2f7fe7a737aaae1ec609088 Mon Sep 17 00:00:00 2001 From: Christian Jantz Date: Mon, 14 May 2018 23:28:24 +0200 Subject: [PATCH] WIP: Feature/healthcheck signal (#177) * added basic healthcheck implementation * updated go-containerregistry version * added build args parameter to healthcheck execute * added go-containerregistry HealthCheck passing * dereferenced health for conversion --- Gopkg.lock | 2 +- pkg/commands/commands.go | 2 + pkg/commands/healthcheck.go | 52 +++++++++++++++ .../google/go-containerregistry/name/ref.go | 1 + .../google/go-containerregistry/v1/config.go | 22 +++++++ .../go-containerregistry/v1/mutate/mutate.go | 9 ++- .../go-containerregistry/v1/random/image.go | 12 +++- .../go-containerregistry/v1/remote/image.go | 2 - .../go-containerregistry/v1/remote/list.go | 64 +++++++++++++++++++ .../go-containerregistry/v1/tarball/write.go | 30 +++++---- 10 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 pkg/commands/healthcheck.go create mode 100644 vendor/github.com/google/go-containerregistry/v1/remote/list.go diff --git a/Gopkg.lock b/Gopkg.lock index 188d26165..d8c5ddd48 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -393,7 +393,7 @@ "v1/types", "v1/v1util" ] - revision = "71d387b23c8c5488e8a70a39b0b214af1ac01f30" + revision = "ee5a6c257df843b47a2666ff0fff3d31d484ebda" [[projects]] name = "github.com/googleapis/gax-go" diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index e9084715e..09bb19d0b 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -68,6 +68,8 @@ func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, e return &ArgCommand{cmd: c}, nil case *instructions.ShellCommand: return &ShellCommand{cmd: c}, nil + case *instructions.HealthCheckCommand: + return &HealthCheckCommand{cmd: c}, nil case *instructions.MaintainerCommand: logrus.Warnf("%s is deprecated, skipping", cmd.Name()) return nil, nil diff --git a/pkg/commands/healthcheck.go b/pkg/commands/healthcheck.go new file mode 100644 index 000000000..e5f4924a3 --- /dev/null +++ b/pkg/commands/healthcheck.go @@ -0,0 +1,52 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "strings" + + "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" + "github.com/docker/docker/builder/dockerfile/instructions" + "github.com/google/go-containerregistry/v1" + "github.com/sirupsen/logrus" +) + +type HealthCheckCommand struct { + cmd *instructions.HealthCheckCommand +} + +// ExecuteCommand handles command processing similar to CMD and RUN, +func (h *HealthCheckCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + logrus.Info("cmd: HEALTHCHECK") + + check := v1.HealthConfig(*h.cmd.Health) + config.Healthcheck = &check + + return nil +} + +// FilesToSnapshot returns an empty array since this is a metadata command +func (h *HealthCheckCommand) FilesToSnapshot() []string { + return []string{} +} + +// CreatedBy returns some information about the command for the image config history +func (h *HealthCheckCommand) CreatedBy() string { + entrypoint := []string{"HEALTHCHECK"} + + return strings.Join(append(entrypoint, strings.Join(h.cmd.Health.Test, " ")), " ") +} diff --git a/vendor/github.com/google/go-containerregistry/name/ref.go b/vendor/github.com/google/go-containerregistry/name/ref.go index 356d79eb6..58775daa3 100644 --- a/vendor/github.com/google/go-containerregistry/name/ref.go +++ b/vendor/github.com/google/go-containerregistry/name/ref.go @@ -45,5 +45,6 @@ func ParseReference(s string, strict Strictness) (Reference, error) { if d, err := NewDigest(s, strict); err == nil { return d, nil } + // TODO: Combine above errors into something more useful? return nil, errors.New("could not parse reference") } diff --git a/vendor/github.com/google/go-containerregistry/v1/config.go b/vendor/github.com/google/go-containerregistry/v1/config.go index 17fa0d20b..d1d809d91 100644 --- a/vendor/github.com/google/go-containerregistry/v1/config.go +++ b/vendor/github.com/google/go-containerregistry/v1/config.go @@ -64,6 +64,27 @@ type RootFS struct { DiffIDs []Hash `json:"diff_ids"` } +// HealthConfig holds configuration settings for the HEALTHCHECK feature. +type HealthConfig struct { + // Test is the test to perform to check that the container is healthy. + // An empty slice means to inherit the default. + // The options are: + // {} : inherit healthcheck + // {"NONE"} : disable healthcheck + // {"CMD", args...} : exec arguments directly + // {"CMD-SHELL", command} : run command with system's default shell + Test []string `json:",omitempty"` + + // Zero means to inherit. Durations are expressed as integer nanoseconds. + Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks. + Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung. + StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down. + + // Retries is the number of consecutive failures needed to consider a container as unhealthy. + // Zero means inherit. + Retries int `json:",omitempty"` +} + // Config is a submessage of the config file described as: // The execution parameters which SHOULD be used as a base when running // a container using the image. @@ -77,6 +98,7 @@ type Config struct { AttachStdin bool AttachStdout bool Cmd []string + Healthcheck *HealthConfig Domainname string Entrypoint []string Env []string diff --git a/vendor/github.com/google/go-containerregistry/v1/mutate/mutate.go b/vendor/github.com/google/go-containerregistry/v1/mutate/mutate.go index 46f4f5843..55e480590 100644 --- a/vendor/github.com/google/go-containerregistry/v1/mutate/mutate.go +++ b/vendor/github.com/google/go-containerregistry/v1/mutate/mutate.go @@ -300,7 +300,14 @@ func extract(img v1.Image, w io.Writer) error { } // check if we have seen value before - name := filepath.Join(dirname, basename) + // if we're checking a directory, don't filepath.Join names + var name string + if header.Typeflag == tar.TypeDir { + name = header.Name + } else { + name = filepath.Join(dirname, basename) + } + if _, ok := fileMap[name]; ok { continue } diff --git a/vendor/github.com/google/go-containerregistry/v1/random/image.go b/vendor/github.com/google/go-containerregistry/v1/random/image.go index 09047558b..5f5bc903e 100644 --- a/vendor/github.com/google/go-containerregistry/v1/random/image.go +++ b/vendor/github.com/google/go-containerregistry/v1/random/image.go @@ -15,6 +15,7 @@ package random import ( + "archive/tar" "bytes" "crypto/rand" "fmt" @@ -50,8 +51,15 @@ var _ partial.UncompressedLayer = (*uncompressedLayer)(nil) func Image(byteSize, layers int64) (v1.Image, error) { layerz := make(map[v1.Hash]partial.UncompressedLayer) for i := int64(0); i < layers; i++ { - b := bytes.NewBuffer(nil) - if _, err := io.CopyN(b, rand.Reader, byteSize); err != nil { + var b bytes.Buffer + tw := tar.NewWriter(&b) + if err := tw.WriteHeader(&tar.Header{ + Name: fmt.Sprintf("random_file_%d.txt", i), + Size: byteSize, + }); err != nil { + return nil, err + } + if _, err := io.CopyN(tw, rand.Reader, byteSize); err != nil { return nil, err } bts := b.Bytes() diff --git a/vendor/github.com/google/go-containerregistry/v1/remote/image.go b/vendor/github.com/google/go-containerregistry/v1/remote/image.go index 42801afe7..d4c6d74ab 100644 --- a/vendor/github.com/google/go-containerregistry/v1/remote/image.go +++ b/vendor/github.com/google/go-containerregistry/v1/remote/image.go @@ -19,7 +19,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "net/http" "net/url" "sync" @@ -115,7 +114,6 @@ func (r *remoteImage) RawManifest() ([]byte, error) { err := fmt.Errorf("manifest digest: %q does not match Docker-Content-Digest: %q for %q", digest, checksum, r.ref) if r.ref.Context().RegistryStr() == name.DefaultRegistry { // TODO(docker/distribution#2395): Remove this check. - log.Println(err) } else { // When pulling by tag, we can only validate that the digest matches what the registry told us it should be. return nil, err diff --git a/vendor/github.com/google/go-containerregistry/v1/remote/list.go b/vendor/github.com/google/go-containerregistry/v1/remote/list.go new file mode 100644 index 000000000..f1e206ddd --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/v1/remote/list.go @@ -0,0 +1,64 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/google/go-containerregistry/authn" + "github.com/google/go-containerregistry/name" + "github.com/google/go-containerregistry/v1/remote/transport" +) + +type Tags struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} + +// TODO(jonjohnsonjr): return []name.Tag? +func List(repo name.Repository, auth authn.Authenticator, t http.RoundTripper) ([]string, error) { + scopes := []string{repo.Scope(transport.PullScope)} + tr, err := transport.New(repo.Registry, auth, t, scopes) + if err != nil { + return nil, err + } + + uri := url.URL{ + Scheme: transport.Scheme(repo.Registry), + Host: repo.Registry.RegistryStr(), + Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()), + } + + client := http.Client{Transport: tr} + resp, err := client.Get(uri.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := checkError(resp, http.StatusOK); err != nil { + return nil, err + } + + tags := Tags{} + if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil { + return nil, err + } + + return tags.Tags, nil +} diff --git a/vendor/github.com/google/go-containerregistry/v1/tarball/write.go b/vendor/github.com/google/go-containerregistry/v1/tarball/write.go index bebf3018d..73a3f73d3 100644 --- a/vendor/github.com/google/go-containerregistry/v1/tarball/write.go +++ b/vendor/github.com/google/go-containerregistry/v1/tarball/write.go @@ -32,20 +32,24 @@ type WriteOptions struct { // TODO(mattmoor): Whether to store things compressed? } -// Write saves the image as the given tag in a tarball at the given path. -func Write(p string, tag name.Tag, img v1.Image, wo *WriteOptions) error { - // Write in the compressed format. - // This is a tarball, on-disk, with: - // One manifest.json file at the top level containing information about several images. - // One file for each layer, named after the layer's SHA. - // One file for the config blob, named after its SHA. - +// WriteToFile writes in the compressed format to a tarball, on disk. +// This is just syntactic sugar wrapping tarball.Write with a new file. +func WriteToFile(p string, tag name.Tag, img v1.Image, wo *WriteOptions) error { w, err := os.Create(p) if err != nil { return err } defer w.Close() + return Write(tag, img, wo, w) +} + +// Write the contents of the image to the provided reader, in the compressed format. +// The contents are written in the following format: +// One manifest.json file at the top level containing information about several images. +// One file for each layer, named after the layer's SHA. +// One file for the config blob, named after its SHA. +func Write(tag name.Tag, img v1.Image, wo *WriteOptions, w io.Writer) error { tf := tar.NewWriter(w) defer tf.Close() @@ -58,7 +62,7 @@ func Write(p string, tag name.Tag, img v1.Image, wo *WriteOptions) error { if err != nil { return err } - if err := writeFile(tf, cfgName.String(), bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil { + if err := writeTarEntry(tf, cfgName.String(), bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil { return err } @@ -94,10 +98,9 @@ func Write(p string, tag name.Tag, img v1.Image, wo *WriteOptions) error { return err } - if err := writeFile(tf, layerFiles[i], r, blobSize); err != nil { + if err := writeTarEntry(tf, layerFiles[i], r, blobSize); err != nil { return err } - } // Generate the tar descriptor and write it. @@ -112,10 +115,11 @@ func Write(p string, tag name.Tag, img v1.Image, wo *WriteOptions) error { if err != nil { return err } - return writeFile(tf, "manifest.json", bytes.NewReader(tdBytes), int64(len(tdBytes))) + return writeTarEntry(tf, "manifest.json", bytes.NewReader(tdBytes), int64(len(tdBytes))) } -func writeFile(tf *tar.Writer, path string, r io.Reader, size int64) error { +// write a file to the provided writer with a corresponding tar header +func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error { hdr := &tar.Header{ Mode: 0644, Typeflag: tar.TypeReg,