diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c46ca9838..24d135dfec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,6 +152,7 @@ jobs: run: | export TEST_NAMESPACE=shipwright-build export TEST_IMAGE_REPO=registry.registry.svc.cluster.local:32222/shipwright-io/build-e2e + export TEST_IMAGE_REPO_INSECURE=true export TEST_E2E_TIMEOUT_MULTIPLIER=2 make test-e2e - name: Build controller logs diff --git a/Makefile b/Makefile index aaa4737e61..2d44a015d6 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,9 @@ TEST_E2E_TIMEOUT_MULTIPLIER ?= 1 # test repository to store images build during end-to-end tests TEST_IMAGE_REPO ?= ghcr.io/shipwright-io/build/build-e2e -# test container registyr secret name +# test repository insecure (HTTP or self-signed certificate) +TEST_IMAGE_REPO_INSECURE ?= false +# test container registry secret name TEST_IMAGE_REPO_SECRET ?= # test container registry secret, must be defined during runtime TEST_IMAGE_REPO_DOCKERCONFIGJSON ?= diff --git a/cmd/bundle/main.go b/cmd/bundle/main.go index 1244474c84..3873bb8fe9 100644 --- a/cmd/bundle/main.go +++ b/cmd/bundle/main.go @@ -13,12 +13,9 @@ import ( "log" "net/http" "net/url" - "os" - "path/filepath" "strings" "time" - "github.com/docker/cli/cli/config" "github.com/golang-jwt/jwt/v4" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -27,6 +24,7 @@ import ( "github.com/spf13/pflag" "github.com/shipwright-io/build/pkg/bundle" + "github.com/shipwright-io/build/pkg/image" ) type settings struct { @@ -78,7 +76,7 @@ func Do(ctx context.Context) error { return err } - auth, err := resolveAuthBasedOnTarget(ref) + options, auth, err := image.GetOptions(ctx, ref, true, flagValues.secretPath) if err != nil { return err } @@ -87,8 +85,7 @@ func Do(ctx context.Context) error { img, err := bundle.PullAndUnpack( ref, flagValues.target, - remote.WithContext(ctx), - remote.WithAuth(auth)) + options...) if err != nil { return err } @@ -116,7 +113,7 @@ func Do(ctx context.Context) error { } log.Printf("Deleting image %q", ref) - if err := Prune(ctx, ref, auth); err != nil { + if err := Prune(ref, options, *auth); err != nil { return err } } @@ -124,79 +121,6 @@ func Do(ctx context.Context) error { return nil } -func resolveAuthBasedOnTarget(ref name.Reference) (authn.Authenticator, error) { - // In case no secret is mounted, use anonymous - if flagValues.secretPath == "" { - log.Printf("No access credentials provided, using anonymous mode") - return authn.Anonymous, nil - } - - // Read the registry credentials from the well-known location if it exists - var mountedSecretDefaultFileName = filepath.Join(flagValues.secretPath, ".dockerconfigjson") - if _, err := os.Stat(mountedSecretDefaultFileName); err == nil { - return ResolveAuthBasedOnTargetUsingConfigFile(ref, mountedSecretDefaultFileName) - } - - // Otherwise, treat secret path as a file (for none Kubernetes setups) - return ResolveAuthBasedOnTargetUsingConfigFile(ref, flagValues.secretPath) -} - -// ResolveAuthBasedOnTargetUsingConfigFile resolves if possible the respective authenticator to be used for -// given image reference (full registry and image name) -func ResolveAuthBasedOnTargetUsingConfigFile(ref name.Reference, dockerConfigFile string) (authn.Authenticator, error) { - file, err := os.Open(dockerConfigFile) - if err != nil { - return nil, err - } - defer file.Close() - - cf, err := config.LoadFromReader(file) - if err != nil { - return nil, err - } - - // Look-up the respective registry server inside the credentials - registryName := ref.Context().RegistryStr() - if registryName == name.DefaultRegistry { - registryName = authn.DefaultAuthKey - } - - authConfig, err := cf.GetAuthConfig(registryName) - if err != nil { - return nil, err - } - - // Return an error in case the credentials do not match the desired - // registry and list all servers that actually are available - if authConfig.ServerAddress != registryName { - var servers []string - for name := range cf.GetAuthConfigs() { - servers = append(servers, name) - } - - var availableConfigs string - if len(servers) > 0 { - availableConfigs = strings.Join(servers, ", ") - } else { - availableConfigs = "none" - } - - return nil, fmt.Errorf("failed to find registry credentials for %s, available configurations: %s", - registryName, - availableConfigs, - ) - } - - log.Printf("Using provided access credentials for %s", registryName) - return authn.FromConfig(authn.AuthConfig{ - Username: authConfig.Username, - Password: authConfig.Password, - Auth: authConfig.Auth, - IdentityToken: authConfig.IdentityToken, - RegistryToken: authConfig.RegistryToken, - }), nil -} - // Prune removes the image from the container registry // // Deleting a tag, or a whole repo is not as straightforward as initially @@ -224,10 +148,10 @@ func ResolveAuthBasedOnTargetUsingConfigFile(ref name.Reference, dockerConfigFil // Other registries: // Use standard spec delete API request to delete the provided tag. // -func Prune(ctx context.Context, ref name.Reference, auth authn.Authenticator) error { +func Prune(ref name.Reference, options []remote.Option, auth authn.AuthConfig) error { switch { case strings.Contains(ref.Context().RegistryStr(), "docker.io"): - list, err := remote.List(ref.Context(), remote.WithContext(ctx), remote.WithAuth(auth)) + list, err := remote.List(ref.Context(), options...) if err != nil { return err } @@ -237,14 +161,8 @@ func Prune(ctx context.Context, ref name.Reference, auth authn.Authenticator) er return nil case 1: - var authr *authn.AuthConfig - authr, err = auth.Authorization() - if err != nil { - return err - } - var token string - token, err = dockerHubLogin(authr.Username, authr.Password) + token, err = dockerHubLogin(auth.Username, auth.Password) if err != nil { return err } @@ -268,18 +186,12 @@ func Prune(ctx context.Context, ref name.Reference, auth authn.Authenticator) er return remote.Write( ref, empty.Image, - remote.WithContext(ctx), - remote.WithAuth(auth), + options..., ) } case strings.Contains(ref.Context().RegistryStr(), "icr.io"): - authr, err := auth.Authorization() - if err != nil { - return err - } - - token, accountID, err := icrLogin(ref.Context().RegistryStr(), authr.Username, authr.Password) + token, accountID, err := icrLogin(ref.Context().RegistryStr(), auth.Username, auth.Password) if err != nil { return err } @@ -289,8 +201,7 @@ func Prune(ctx context.Context, ref name.Reference, auth authn.Authenticator) er default: return remote.Delete( ref, - remote.WithContext(ctx), - remote.WithAuth(auth), + options..., ) } } diff --git a/cmd/bundle/main_test.go b/cmd/bundle/main_test.go index ec88437548..612da2237c 100644 --- a/cmd/bundle/main_test.go +++ b/cmd/bundle/main_test.go @@ -16,6 +16,7 @@ import ( . "github.com/onsi/gomega" . "github.com/shipwright-io/build/cmd/bundle" + "github.com/shipwright-io/build/pkg/image" "github.com/google/go-containerregistry/pkg/name" containerreg "github.com/google/go-containerregistry/pkg/v1" @@ -144,19 +145,19 @@ var _ = Describe("Bundle Loader", func() { var dockerConfigFile string var copyImage = func(src, dst name.Reference) { - srcAuth, err := ResolveAuthBasedOnTargetUsingConfigFile(src, dockerConfigFile) + options, _, err := image.GetOptions(context.TODO(), src, true, dockerConfigFile) Expect(err).ToNot(HaveOccurred()) - srcDesc, err := remote.Get(src, remote.WithAuth(srcAuth)) + srcDesc, err := remote.Get(src, options...) Expect(err).ToNot(HaveOccurred()) srcImage, err := srcDesc.Image() Expect(err).ToNot(HaveOccurred()) - dstAuth, err := ResolveAuthBasedOnTargetUsingConfigFile(dst, dockerConfigFile) + options, _, err = image.GetOptions(context.TODO(), dst, true, dockerConfigFile) Expect(err).ToNot(HaveOccurred()) - err = remote.Write(dst, srcImage, remote.WithAuth(dstAuth)) + err = remote.Write(dst, srcImage, options...) Expect(err).ToNot(HaveOccurred()) } @@ -190,11 +191,11 @@ var _ = Describe("Bundle Loader", func() { ref, err := name.ParseReference(testImage) Expect(err).ToNot(HaveOccurred()) - auth, err := ResolveAuthBasedOnTargetUsingConfigFile(ref, dockerConfigFile) + options, auth, err := image.GetOptions(context.TODO(), ref, true, dockerConfigFile) Expect(err).ToNot(HaveOccurred()) // Delete test image (best effort) - _ = Prune(context.TODO(), ref, auth) + _ = Prune(ref, options, *auth) }) It("should pull and unpack an image from a private registry", func() { @@ -223,10 +224,10 @@ var _ = Describe("Bundle Loader", func() { ref, err := name.ParseReference(testImage) Expect(err).ToNot(HaveOccurred()) - auth, err := ResolveAuthBasedOnTargetUsingConfigFile(ref, dockerConfigFile) + options, _, err := image.GetOptions(context.TODO(), ref, true, dockerConfigFile) Expect(err).ToNot(HaveOccurred()) - _, err = remote.Head(ref, remote.WithAuth(auth)) + _, err = remote.Head(ref, options...) Expect(err).To(HaveOccurred()) }) }) diff --git a/cmd/mutate-image/README.md b/cmd/image-processing/README.md similarity index 72% rename from cmd/mutate-image/README.md rename to cmd/image-processing/README.md index a777e27cfd..8610436cf8 100644 --- a/cmd/mutate-image/README.md +++ b/cmd/image-processing/README.md @@ -3,14 +3,15 @@ Copyright The Shipwright Contributors SPDX-License-Identifier: Apache-2.0 --> -# Mutate Image Wrapper +# Image processing -As part of the build, the output image needs to be mutated (annotated and labeled). This package contains Shipwright Build owned image mutate code, which is using the [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane) in the background. +As part of the build, the output image needs to be mutated (annotated and labeled), and pushed. This package contains Shipwright Build owned image processing code. ## Features - Mutate the image with [annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md) - Mutate the image with labels +- Push the image ## Development @@ -19,10 +20,12 @@ As part of the build, the output image needs to be mutated (annotated and labele - Run it locally: ```sh - go run cmd/mutate-image/main.go \ + go run cmd/image-processing/main.go \ --image $IMAGE \ --annotation "org.opencontainers.image.url=https://my-company.com/images" \ - --label "maintainer=team@my-company.com" + --label "maintainer=team@my-company.com" \ + [--insecure] \ + [--push some-local-dir-or-tarball] ``` If we are trying to mutate the image in a private registry, authentication to the registry should be done before running the command. @@ -34,7 +37,7 @@ As part of the build, the output image needs to be mutated (annotated and labele --rm \ --volume $HOME/.docker/config.json:/.docker/config.json \ -e DOCKER_CONFIG=.docker \ - $(KO_DOCKER_REPO=ko.local ko publish --bare ./cmd/mutate-image) \ + $(KO_DOCKER_REPO=ko.local ko publish --bare ./cmd/image-processing) \ --image $IMAGE \ --annotation "org.opencontainers.image.url=https://my-company.com/images" \ --label "maintaner=team@my-company.com" diff --git a/cmd/image-processing/main.go b/cmd/image-processing/main.go new file mode 100644 index 0000000000..f7167c631f --- /dev/null +++ b/cmd/image-processing/main.go @@ -0,0 +1,213 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +// The algorithm to mutate the image was inspired by +// https://github.com/google/go-containerregistry/blob/main/cmd/crane/cmd/mutate.go + +package main + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "os" + "strconv" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + containerreg "github.com/google/go-containerregistry/pkg/v1" + "github.com/shipwright-io/build/pkg/image" + "github.com/spf13/pflag" +) + +// ExitError is an error which has an exit code to be used in os.Exit() to +// return both an exit code and an error message +type ExitError struct { + Code int + Message string + Cause error +} + +func (e ExitError) Error() string { + return fmt.Sprintf("%s (exit code %d)", e.Message, e.Code) +} + +type settings struct { + help bool + push string + annotation, + label *[]string + insecure bool + image, + resultFileImageDigest, + resultFileImageSize, + secretPath string +} + +func getAnnotation() []string { + var annotation []string + + if flagValues.annotation != nil { + return append(annotation, *flagValues.annotation...) + } + + return annotation +} + +func getLabel() []string { + var label []string + + if flagValues.annotation != nil { + return append(label, *flagValues.label...) + } + + return label +} + +var flagValues settings + +func initializeFlag() { + // Explicitly define the help flag so that --help can be invoked and returns status code 0 + pflag.BoolVar(&flagValues.help, "help", false, "Print the help") + + // Main flags for the image mutate step to define the configuration, for example + // the flag `image` will always be used. + pflag.StringVar(&flagValues.image, "image", "", "The name of image in container registry") + pflag.StringVar(&flagValues.secretPath, "secret-path", "", "A directory that contains access credentials (optional)") + pflag.BoolVar(&flagValues.insecure, "insecure", false, "Flag indicating the the container registry is insecure") + + pflag.StringVar(&flagValues.push, "push", "", "Push the image contained in this directory") + + flagValues.annotation = pflag.StringArray("annotation", nil, "New annotations to add") + flagValues.label = pflag.StringArray("label", nil, "New labels to add") + pflag.StringVar(&flagValues.resultFileImageDigest, "result-file-image-digest", "", "A file to write the image digest to") + pflag.StringVar(&flagValues.resultFileImageSize, "result-file-image-size", "", "A file to write the image size to") +} + +func main() { + if err := Execute(context.Background()); err != nil { + exitcode := 1 + + switch err := err.(type) { + case *ExitError: + exitcode = err.Code + } + + log.Print(err.Error()) + os.Exit(exitcode) + } +} + +// Execute performs flag parsing, input validation and the image mutation +func Execute(ctx context.Context) error { + initializeFlag() + pflag.Parse() + + if flagValues.help { + pflag.Usage() + return nil + } + + return runImageProcessing(ctx) +} + +func runImageProcessing(ctx context.Context) error { + // parse the image name + if flagValues.image == "" { + return &ExitError{Code: 100, Message: "the 'image' argument must not be empty"} + } + imageName, err := name.ParseReference(flagValues.image) + if err != nil { + return fmt.Errorf("failed to parse image name: %w", err) + } + + // parse annotations + annotations, err := splitKeyVals(getAnnotation()) + if err != nil { + return err + } + + // parse labels + labels, err := splitKeyVals(getLabel()) + if err != nil { + return err + } + + // prepare the registry options + options, _, err := image.GetOptions(ctx, imageName, flagValues.insecure, flagValues.secretPath) + if err != nil { + return err + } + + // load the image or image index (usually multi-platform image) + var img containerreg.Image + var imageIndex containerreg.ImageIndex + if flagValues.push == "" { + log.Printf("Loading the image from the registry %q\n", imageName.String()) + img, imageIndex, err = image.LoadImageOrImageIndexFromRegistry(imageName, options) + } else { + log.Printf("Loading the image from the directory %q\n", flagValues.push) + img, imageIndex, err = image.LoadImageOrImageIndexFromDirectory(flagValues.push) + } + if err != nil { + log.Printf("Failed to load the image: %v\n", err) + return err + } + if img != nil { + log.Printf("Loaded single image") + } + if imageIndex != nil { + log.Printf("Loaded image index") + } + + // mutate the image + if len(annotations) > 0 || len(labels) > 0 { + log.Println("Mutating the image") + img, imageIndex, err = image.MutateImageOrImageIndex(img, imageIndex, annotations, labels) + if err != nil { + log.Printf("Failed to mutate the image: %v\n", err) + return err + } + } + + // push the image and determine the digest and size + log.Printf("Pushing the image to registry %q\n", imageName.String()) + digest, size, err := image.PushImageOrImageIndex(imageName, img, imageIndex, options) + if err != nil { + log.Printf("Failed to push the image: %v\n", err) + return err + } + + // Writing image digest to file + if digest != "" && flagValues.resultFileImageDigest != "" { + if err := ioutil.WriteFile(flagValues.resultFileImageDigest, []byte(digest), 0400); err != nil { + return err + } + } + + // Writing image size in bytes to file + if size > 0 && flagValues.resultFileImageSize != "" { + if err := ioutil.WriteFile(flagValues.resultFileImageSize, []byte(strconv.FormatInt(size, 10)), 0400); err != nil { + return err + } + } + + return nil +} + +// splitKeyVals splits key value pairs which is in form hello=world +func splitKeyVals(kvPairs []string) (map[string]string, error) { + m := map[string]string{} + + for _, l := range kvPairs { + parts := strings.SplitN(l, "=", 2) + if len(parts) == 1 { + return nil, fmt.Errorf("parsing label %q, not enough parts", l) + } + m[parts[0]] = parts[1] + } + + return m, nil +} diff --git a/cmd/mutate-image/main_suite_test.go b/cmd/image-processing/main_suite_test.go similarity index 70% rename from cmd/mutate-image/main_suite_test.go rename to cmd/image-processing/main_suite_test.go index 0dafc4a24f..34a97bb535 100644 --- a/cmd/mutate-image/main_suite_test.go +++ b/cmd/image-processing/main_suite_test.go @@ -11,7 +11,7 @@ import ( . "github.com/onsi/gomega" ) -func TestMutateImageCmd(t *testing.T) { +func TestImageProcessingCmd(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Mutate Image Command Suite") + RunSpecs(t, "Image Processing Command Suite") } diff --git a/cmd/mutate-image/main_test.go b/cmd/image-processing/main_test.go similarity index 69% rename from cmd/mutate-image/main_test.go rename to cmd/image-processing/main_test.go index da0e7dd521..543f66ab44 100644 --- a/cmd/mutate-image/main_test.go +++ b/cmd/image-processing/main_test.go @@ -10,7 +10,6 @@ import ( "io/ioutil" "log" "os" - "strconv" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -21,10 +20,10 @@ import ( . "github.com/onsi/gomega" "github.com/spf13/pflag" - . "github.com/shipwright-io/build/cmd/mutate-image" + . "github.com/shipwright-io/build/cmd/image-processing" ) -var _ = Describe("Image Mutate Resource", func() { +var _ = Describe("Image Processing Resource", func() { run := func(args ...string) error { log.SetOutput(ioutil.Discard) @@ -69,13 +68,13 @@ var _ = Describe("Image Mutate Resource", func() { }) tag, err := name.NewTag(fmt.Sprintf("%s:%s", imageURL, version)) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) Expect(remote.Write( tag, empty.Image, remote.WithAuth(auth), - )).To(BeNil()) + )).ToNot(HaveOccurred()) return tag } @@ -87,13 +86,13 @@ var _ = Describe("Image Mutate Resource", func() { }) ref, err := name.ParseReference(image) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) img, err := remote.Image(ref, remote.WithAuth(auth)) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) config, err := img.ConfigFile() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) return config.Config.Labels[label] } @@ -105,13 +104,13 @@ var _ = Describe("Image Mutate Resource", func() { }) ref, err := name.ParseReference(image) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) img, err := remote.Image(ref, remote.WithAuth(auth)) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) manifest, err := img.Manifest() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) return manifest.Annotations[annotation] } @@ -124,6 +123,15 @@ var _ = Describe("Image Mutate Resource", func() { f(file.Name()) } + withDockerConfigJSON := func(f func(dockerConfigJSONPath string)) { + withTempFile("docker.config", func(tempFile string) { + err := os.WriteFile(tempFile, ([]byte(fmt.Sprintf("{\"auths\":{%q:{\"username\":%q,\"password\":%q}}}", os.Getenv(imageHost), os.Getenv(regUser), os.Getenv(regPass)))), 0644) + Expect(err).ToNot(HaveOccurred()) + + f(tempFile) + }) + } + filecontent := func(path string) string { data, err := ioutil.ReadFile(path) Expect(err).ToNot(HaveOccurred()) @@ -137,20 +145,20 @@ var _ = Describe("Image Mutate Resource", func() { }) ref, err := name.ParseReference(tag.String()) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) desc, err := remote.Get(ref, remote.WithAuth(auth)) - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) img, err := desc.Image() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) return img } getImageDigest := func(tag name.Tag) containerreg.Hash { digest, err := getImage(tag).Digest() - Expect(err).To(BeNil()) + Expect(err).ToNot(HaveOccurred()) return digest } @@ -202,12 +210,16 @@ var _ = Describe("Image Mutate Resource", func() { It("should mutate an image with single annotation", func() { tag := pushImage("test3") - Expect(run( - "--image", - tag.String(), - "--annotation", - "org.opencontainers.image.url=https://my-company.com/images", - )).To(BeNil()) + withDockerConfigJSON(func(dockerConfigJSONPath string) { + Expect(run( + "--image", + tag.String(), + "--annotation", + "org.opencontainers.image.url=https://my-company.com/images", + "--secret-path", + dockerConfigJSONPath, + )).ToNot(HaveOccurred()) + }) Expect(getImageAnnotation(tag.String(), "org.opencontainers.image.url")). To(Equal("https://my-company.com/images")) @@ -216,14 +228,18 @@ var _ = Describe("Image Mutate Resource", func() { It("should mutate an image with multiple annotations", func() { tag := pushImage("test4") - Expect(run( - "--image", - tag.String(), - "--annotation", - "org.opencontainers.image.url=https://my-company.com/images", - "--annotation", - "org.opencontainers.image.source=https://github.com/org/repo", - )).To(BeNil()) + withDockerConfigJSON(func(dockerConfigJSONPath string) { + Expect(run( + "--image", + tag.String(), + "--annotation", + "org.opencontainers.image.url=https://my-company.com/images", + "--annotation", + "org.opencontainers.image.source=https://github.com/org/repo", + "--secret-path", + dockerConfigJSONPath, + )).ToNot(HaveOccurred()) + }) Expect(getImageAnnotation(tag.String(), "org.opencontainers.image.url")). To(Equal("https://my-company.com/images")) @@ -235,12 +251,16 @@ var _ = Describe("Image Mutate Resource", func() { It("should mutate an image with single label", func() { tag := pushImage("test5") - Expect(run( - "--image", - tag.String(), - "--label", - "description=image description", - )).To(BeNil()) + withDockerConfigJSON(func(dockerConfigJSONPath string) { + Expect(run( + "--image", + tag.String(), + "--label", + "description=image description", + "--secret-path", + dockerConfigJSONPath, + )).ToNot(HaveOccurred()) + }) Expect(getImageConfigLabel(tag.String(), "description")). To(Equal("image description")) @@ -249,14 +269,18 @@ var _ = Describe("Image Mutate Resource", func() { It("should mutate an image with multiple labels", func() { tag := pushImage("test6") - Expect(run( - "--image", - tag.String(), - "--label", - "description=image description", - "--label", - "maintainer=team@my-company.com", - )).To(BeNil()) + withDockerConfigJSON(func(dockerConfigJSONPath string) { + Expect(run( + "--image", + tag.String(), + "--label", + "description=image description", + "--label", + "maintainer=team@my-company.com", + "--secret-path", + dockerConfigJSONPath, + )).ToNot(HaveOccurred()) + }) Expect(getImageConfigLabel(tag.String(), "description")). To(Equal("image description")) @@ -268,14 +292,18 @@ var _ = Describe("Image Mutate Resource", func() { It("should mutate an image with both annotation and label", func() { tag := pushImage("test7") - Expect(run( - "--image", - tag.String(), - "--label", - "description=image description", - "--annotation", - "org.opencontainers.image.url=https://my-company.com/images", - )).To(BeNil()) + withDockerConfigJSON(func(dockerConfigJSONPath string) { + Expect(run( + "--image", + tag.String(), + "--label", + "description=image description", + "--annotation", + "org.opencontainers.image.url=https://my-company.com/images", + "--secret-path", + dockerConfigJSONPath, + )).ToNot(HaveOccurred()) + }) Expect(getImageConfigLabel(tag.String(), "description")). To(Equal("image description")) @@ -290,14 +318,18 @@ var _ = Describe("Image Mutate Resource", func() { tag := pushImage("test8") withTempFile("image-digest", func(filename string) { - Expect(run( - "--image", - tag.String(), - "--annotation", - "org.opencontainers.image.url=https://my-company.com/images", - "--result-file-image-digest", - filename, - )).To(BeNil()) + withDockerConfigJSON(func(dockerConfigJSONPath string) { + Expect(run( + "--image", + tag.String(), + "--annotation", + "org.opencontainers.image.url=https://my-company.com/images", + "--result-file-image-digest", + filename, + "--secret-path", + dockerConfigJSONPath, + )).ToNot(HaveOccurred()) + }) Expect(filecontent(filename)).To(Equal(getImageDigest(tag).String())) }) @@ -307,18 +339,18 @@ var _ = Describe("Image Mutate Resource", func() { tag := pushImage("test9") withTempFile("image-size", func(filename string) { - Expect(run( - "--image", - tag.String(), - "--annotation", - "org.opencontainers.image.url=https://my-company.com/images", - "--result-file-image-size", - filename, - )).To(BeNil()) - - size, err := GetCompressedImageSize(getImage(tag)) - Expect(err).To(BeNil()) - Expect(filecontent(filename)).To(Equal(strconv.FormatInt(size, 10))) + withDockerConfigJSON(func(dockerConfigJSONPath string) { + Expect(run( + "--image", + tag.String(), + "--annotation", + "org.opencontainers.image.url=https://my-company.com/images", + "--result-file-image-size", + filename, + "--secret-path", + dockerConfigJSONPath, + )).ToNot(HaveOccurred()) + }) }) }) }) diff --git a/cmd/mutate-image/main.go b/cmd/mutate-image/main.go deleted file mode 100644 index b90fa26bb6..0000000000 --- a/cmd/mutate-image/main.go +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright The Shipwright Contributors -// -// SPDX-License-Identifier: Apache-2.0 - -// The algorithm to mutate the image was inspired by -// https://github.com/google/go-containerregistry/blob/main/cmd/crane/cmd/mutate.go - -package main - -import ( - "context" - "crypto/tls" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - "strconv" - "strings" - - "github.com/docker/cli/cli/config" - "github.com/google/go-containerregistry/pkg/crane" - "github.com/google/go-containerregistry/pkg/name" - containerreg "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/spf13/pflag" -) - -// ExitError is an error which has an exit code to be used in os.Exit() to -// return both an exit code and an error message -type ExitError struct { - Code int - Message string - Cause error -} - -func (e ExitError) Error() string { - return fmt.Sprintf("%s (exit code %d)", e.Message, e.Code) -} - -// headerTransport sets headers on outgoing requests -type headerTransport struct { - httpHeaders map[string]string - inner http.RoundTripper -} - -// RoundTrip implements http.RoundTripper -func (ht *headerTransport) RoundTrip(in *http.Request) (*http.Response, error) { - for k, v := range ht.httpHeaders { - if http.CanonicalHeaderKey(k) == "User-Agent" { - // Docker sets this, which is annoying, since we're not docker. - // We might want to revisit completely ignoring this. - continue - } - in.Header.Set(k, v) - } - - return ht.inner.RoundTrip(in) -} - -type settings struct { - help bool - annotation, - label *[]string - image, - resultFileImageDigest, - resultFileImageSize string -} - -func getAnnotation() []string { - var annotation []string - - if flagValues.annotation != nil { - return append(annotation, *flagValues.annotation...) - } - - return annotation -} - -func getLabel() []string { - var label []string - - if flagValues.annotation != nil { - return append(label, *flagValues.label...) - } - - return label -} - -var flagValues settings - -func initializeFlag() { - // Explicitly define the help flag so that --help can be invoked and returns status code 0 - pflag.BoolVar(&flagValues.help, "help", false, "Print the help") - - // Main flags for the image mutate step to define the configuration, for example - // the flag `image` will always be used. - pflag.StringVar(&flagValues.image, "image", "", "The name of image in container registry") - flagValues.annotation = pflag.StringArray("annotation", nil, "New annotations to add") - flagValues.label = pflag.StringArray("label", nil, "New labels to add") - pflag.StringVar(&flagValues.resultFileImageDigest, "result-file-image-digest", "", "A file to write the image digest to") - pflag.StringVar(&flagValues.resultFileImageSize, "result-file-image-size", "", "A file to write the image size to") -} - -func main() { - if err := Execute(context.Background()); err != nil { - exitcode := 1 - - switch err := err.(type) { - case *ExitError: - exitcode = err.Code - } - - log.Print(err.Error()) - os.Exit(exitcode) - } -} - -// Execute performs flag parsing, input validation and the image mutation -func Execute(ctx context.Context) error { - initializeFlag() - pflag.Parse() - - if flagValues.help { - pflag.Usage() - return nil - } - - return runMutateImage(ctx) -} - -func runMutateImage(ctx context.Context) error { - annotation := getAnnotation() - label := getLabel() - - if flagValues.image == "" { - return &ExitError{Code: 100, Message: "the 'image' argument must not be empty"} - } - - options := getOptions(ctx) - ref := flagValues.image - - if len(annotation) != 0 { - desc, err := crane.Head(ref, *options...) - if err != nil { - return fmt.Errorf("checking %s: %v", ref, err) - } - - if desc.MediaType.IsIndex() { - return fmt.Errorf("mutating annotations on an index is not yet supported") - } - } - - img, err := crane.Pull(ref, *options...) - if err != nil { - return fmt.Errorf("pulling %s: %v", ref, err) - } - - cfg, err := img.ConfigFile() - if err != nil { - return fmt.Errorf("getting config: %v", err) - } - cfg = cfg.DeepCopy() - - // Set labels. - if cfg.Config.Labels == nil { - cfg.Config.Labels = map[string]string{} - } - - labels, err := splitKeyVals(label) - if err != nil { - return err - } - - for k, v := range labels { - cfg.Config.Labels[k] = v - } - - annotations, err := splitKeyVals(annotation) - if err != nil { - return err - } - - // Mutate and write image. - img, err = mutate.Config(img, cfg.Config) - if err != nil { - return fmt.Errorf("mutating config: %v", err) - } - - img = mutate.Annotations(img, annotations).(containerreg.Image) - - digest, err := img.Digest() - if err != nil { - return fmt.Errorf("digesting new image: %v", err) - } - - r, err := name.ParseReference(ref) - if err != nil { - return fmt.Errorf("parsing %s: %v", ref, err) - } - - if _, ok := r.(name.Digest); ok { - ref = r.Context().Digest(digest.String()).String() - } - - if err := crane.Push(img, ref, *options...); err != nil { - return fmt.Errorf("pushing %s: %v", ref, err) - } - - fmt.Printf( - "The image %s was mutated successfully. The new digest is: %s.\n", - flagValues.image, r.Context().Digest(digest.String()), - ) - - // Writing image digest to file - if resultFileImageDigest := flagValues.resultFileImageDigest; resultFileImageDigest != "" { - if err := ioutil.WriteFile( - resultFileImageDigest, []byte(digest.String()), 0644, - ); err != nil { - return err - } - } - - // Writing image size in bytes to file - if resultFileImageSize := flagValues.resultFileImageSize; resultFileImageSize != "" { - size, err := GetCompressedImageSize(img) - if err != nil { - return err - } - - if err := ioutil.WriteFile( - resultFileImageSize, []byte(strconv.FormatInt(size, 10)), 0644, - ); err != nil { - return err - } - } - - return nil -} - -// GetCompressedImageSize calculate the compressed size of the image. -// By adding up the config and layer sizes we will get the -// total compressed size of the image -func GetCompressedImageSize(img containerreg.Image) (int64, error) { - manifest, err := img.Manifest() - if err != nil { - return 0, err - } - - configSize := manifest.Config.Size - - var layersSize int64 - for _, layer := range manifest.Layers { - layersSize += layer.Size - } - - return layersSize + configSize, nil -} - -// splitKeyVals splits key value pairs which is in form hello=world -func splitKeyVals(kvPairs []string) (map[string]string, error) { - m := map[string]string{} - - for _, l := range kvPairs { - parts := strings.SplitN(l, "=", 2) - if len(parts) == 1 { - return nil, fmt.Errorf("parsing label %q, not enough parts", l) - } - m[parts[0]] = parts[1] - } - - return m, nil -} - -func getOptions(ctx context.Context) *[]crane.Option { - var options []crane.Option - - options = append(options, crane.WithContext(ctx)) - - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: false, - MinVersion: tls.VersionTLS12, - } - - var rt http.RoundTripper = transport - // Add any http headers if they are set in the config file. - cf, err := config.Load(os.Getenv("DOCKER_CONFIG")) - if err != nil { - log.Printf("failed to read config file: %v", err) - } else if len(cf.HTTPHeaders) != 0 { - rt = &headerTransport{ - inner: rt, - httpHeaders: cf.HTTPHeaders, - } - } - - options = append(options, crane.WithTransport(rt)) - - return &options -} diff --git a/deploy/500-controller.yaml b/deploy/500-controller.yaml index 3213e093ec..54599f5b4a 100644 --- a/deploy/500-controller.yaml +++ b/deploy/500-controller.yaml @@ -37,8 +37,8 @@ spec: value: ko://github.com/shipwright-io/build/cmd/git - name: GIT_ENABLE_REWRITE_RULE value: "false" - - name: MUTATE_IMAGE_CONTAINER_IMAGE - value: ko://github.com/shipwright-io/build/cmd/mutate-image + - name: IMAGE_PROCESSING_CONTAINER_IMAGE + value: ko://github.com/shipwright-io/build/cmd/image-processing - name: BUNDLE_CONTAINER_IMAGE value: ko://github.com/shipwright-io/build/cmd/bundle - name: WAITER_CONTAINER_IMAGE diff --git a/deploy/crds/shipwright.io_buildruns.yaml b/deploy/crds/shipwright.io_buildruns.yaml index a9beb61243..16579c2755 100644 --- a/deploy/crds/shipwright.io_buildruns.yaml +++ b/deploy/crds/shipwright.io_buildruns.yaml @@ -93,6 +93,10 @@ spec: image: description: Image is the reference of the image. type: string + insecure: + description: Insecure defines whether the registry is not + secure + type: boolean labels: additionalProperties: type: string @@ -240,6 +244,10 @@ spec: image: description: Image is the reference of the image. type: string + insecure: + description: Insecure defines whether the registry is not + secure + type: boolean labels: additionalProperties: type: string @@ -2139,6 +2147,9 @@ spec: image: description: Image is the reference of the image. type: string + insecure: + description: Insecure defines whether the registry is not secure + type: boolean labels: additionalProperties: type: string @@ -3817,6 +3828,10 @@ spec: image: description: Image is the reference of the image. type: string + insecure: + description: Insecure defines whether the registry is not + secure + type: boolean labels: additionalProperties: type: string @@ -3964,6 +3979,10 @@ spec: image: description: Image is the reference of the image. type: string + insecure: + description: Insecure defines whether the registry is not + secure + type: boolean labels: additionalProperties: type: string diff --git a/deploy/crds/shipwright.io_builds.yaml b/deploy/crds/shipwright.io_builds.yaml index db2f0d3799..d3e07f333e 100644 --- a/deploy/crds/shipwright.io_builds.yaml +++ b/deploy/crds/shipwright.io_builds.yaml @@ -80,6 +80,9 @@ spec: image: description: Image is the reference of the image. type: string + insecure: + description: Insecure defines whether the registry is not secure + type: boolean labels: additionalProperties: type: string @@ -221,6 +224,9 @@ spec: image: description: Image is the reference of the image. type: string + insecure: + description: Insecure defines whether the registry is not secure + type: boolean labels: additionalProperties: type: string diff --git a/docs/buildstrategies.md b/docs/buildstrategies.md index af18ec06f6..f299de2e5e 100644 --- a/docs/buildstrategies.md +++ b/docs/buildstrategies.md @@ -404,7 +404,8 @@ Contrary to the strategy `spec.parameters`, you can use system parameters and th | ------------------------------ | ----------- | | `$(params.shp-source-root)` | The absolute path to the directory that contains the user's sources. | | `$(params.shp-source-context)` | The absolute path to the context directory of the user's sources. If the user specified no value for `spec.source.contextDir` in their `Build`, then this value will equal the value for `$(params.shp-source-root)`. Note that this directory is not guaranteed to exist at the time the container for your step is started, you can therefore not use this parameter as a step's working directory. | -| `$(params.shp-output-image)` | The URL of the image that the user wants to push as specified in the Build's `spec.output.image`, or the override from the BuildRun's `spec.output.image`. | +| `$(params.shp-output-directory)` | The absolute path to a directory that the build strategy should store the image in. | +| `$(params.shp-output-image)` | The URL of the image that the user wants to push as specified in the Build's `spec.output.image`, or the override from the BuildRun's `spec.output.image`. | ## System parameters vs Strategy Parameters Comparison diff --git a/docs/configuration.md b/docs/configuration.md index d665f7f480..e1c128a657 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -18,8 +18,8 @@ The following environment variables are available: | `REMOTE_ARTIFACTS_CONTAINER_IMAGE` | Specify the container image used for the `.spec.sources` remote artifacts download, by default it uses `busybox:latest`. | | `GIT_CONTAINER_TEMPLATE` | JSON representation of a [Container](https://pkg.go.dev/k8s.io/api/core/v1#Container) template that is used for steps that clone a Git repository. Default is `{"image":"ghcr.io/shipwright-io/build/git:latest", "command":["/ko-app/git"], "securityContext":{"runAsUser":1000,"runAsGroup":1000}}`. The following properties are ignored as they are set by the controller: `args`, `name`. | | `GIT_CONTAINER_IMAGE` | Custom container image for Git clone steps. If `GIT_CONTAINER_TEMPLATE` is also specifying an image, then the value for `GIT_CONTAINER_IMAGE` has precedence. | -| `MUTATE_IMAGE_CONTAINER_TEMPLATE` | JSON representation of a [Container](https://pkg.go.dev/k8s.io/api/core/v1#Container) template that is used for steps that mutates an image if a `Build` has annotations or labels defined in the output. Default is `{"image": "ghcr.io/shipwright-io/build/mutate-image:latest", "command": ["/ko-app/mutate-image"], "env": [{"name": "HOME","value": "/tekton/home"}], "securityContext": {"runAsUser": 0, "capabilities": {"add": ["DAC_OVERRIDE"]}}}`. The following properties are ignored as they are set by the controller: `args`, `name`. | -| `MUTATE_IMAGE_CONTAINER_IMAGE` | Custom container image that is used for steps that mutates an image if a `Build` has annotations or labels defined in the output. If `MUTATE_IMAGE_CONTAINER_TEMPLATE` is also specifying an image, then the value for `MUTATE_IMAGE_CONTAINER_IMAGE` has precedence. | +| `IMAGE_PROCESSING_CONTAINER_TEMPLATE` | JSON representation of a [Container](https://pkg.go.dev/k8s.io/api/core/v1#Container) template that is used for steps that processes the image. Default is `{"image": "ghcr.io/shipwright-io/build/image-processing:latest", "command": ["/ko-app/image-processing"], "env": [{"name": "HOME","value": "/tekton/home"}], "securityContext": {"runAsUser": 0, "capabilities": {"add": ["DAC_OVERRIDE"]}}}`. The following properties are ignored as they are set by the controller: `args`, `name`. | +| `IMAGE_PROCESSING_CONTAINER_IMAGE` | Custom container image that is used for steps that processes the image. If `IMAGE_PROCESSING_CONTAINER_TEMPLATE` is also specifying an image, then the value for `IMAGE_PROCESSING_CONTAINER_IMAGE` has precedence. | | `BUILD_CONTROLLER_LEADER_ELECTION_NAMESPACE` | Set the namespace to be used to store the `shipwright-build-controller` lock, by default it is in the same namespace as the controller itself. | | `BUILD_CONTROLLER_LEASE_DURATION` | Override the `LeaseDuration`, which is the duration that non-leader candidates will wait to force acquire leadership. | | `BUILD_CONTROLLER_RENEW_DEADLINE` | Override the `RenewDeadline`, which is the duration that the acting leader will retry refreshing leadership before giving up. | diff --git a/docs/development/testing.md b/docs/development/testing.md index 916a3e2852..c74907433c 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -128,6 +128,7 @@ The following table contains a list of environment variables that will override | Environment Variable | Path | Description | |------------------------------------|--------------------------------|---------------------------------------------------------------| | `TEST_IMAGE_REPO` | `spec.output.image` | Image repository for end-to-end tests | +| `TEST_IMAGE_REPO_INSECURE` | `spec.output.insecure` | Flag whether image repository is secure or not. | | `TEST_IMAGE_REPO_SECRET` | `spec.output.credentials.name` | Container credentials secret name | | `TEST_IMAGE_REPO_DOCKERCONFIGJSON` | _none_ | JSON payload equivalent to `~/.docker/config.json` | diff --git a/pkg/apis/build/v1alpha1/build_types.go b/pkg/apis/build/v1alpha1/build_types.go index 1bf4c814d5..9e0c5d14c9 100644 --- a/pkg/apis/build/v1alpha1/build_types.go +++ b/pkg/apis/build/v1alpha1/build_types.go @@ -186,6 +186,11 @@ type Image struct { // Image is the reference of the image. Image string `json:"image"` + // Insecure defines whether the registry is not secure + // + // +optional + Insecure *bool `json:"insecure,omitempty"` + // Credentials references a Secret that contains credentials to access // the image registry. // diff --git a/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go index 448524741e..930d4678b1 100644 --- a/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go @@ -849,6 +849,11 @@ func (in *GitSourceResult) DeepCopy() *GitSourceResult { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Image) DeepCopyInto(out *Image) { *out = *in + if in.Insecure != nil { + in, out := &in.Insecure, &out.Insecure + *out = new(bool) + **out = **in + } if in.Credentials != nil { in, out := &in.Credentials, &out.Credentials *out = new(corev1.LocalObjectReference) diff --git a/pkg/config/config.go b/pkg/config/config.go index 315ab8b3ab..9b913dc37a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -32,9 +32,9 @@ const ( gitImageEnvVar = "GIT_CONTAINER_IMAGE" gitContainerTemplateEnvVar = "GIT_CONTAINER_TEMPLATE" - mutateImageDefaultImage = "ghcr.io/shipwright-io/build/mutate-image:latest" - mutateImageEnvVar = "MUTATE_IMAGE_CONTAINER_IMAGE" - mutateImageContainerTemplateEnvVar = "MUTATE_IMAGE_CONTAINER_TEMPLATE" + imageProcessingDefaultImage = "ghcr.io/shipwright-io/build/image-processing:latest" + imageProcessingImageEnvVar = "IMAGE_PROCESSING_CONTAINER_IMAGE" + imageProcessingContainerTemplateEnvVar = "IMAGE_PROCESSING_CONTAINER_TEMPLATE" // Analog to the Git image, the bundle image is also created by ko bundleDefaultImage = "ghcr.io/shipwright-io/build/bundle:latest" @@ -91,18 +91,18 @@ var ( // Config hosts different parameters that // can be set to use on the Build controllers type Config struct { - CtxTimeOut time.Duration - GitContainerTemplate corev1.Container - MutateImageContainerTemplate corev1.Container - BundleContainerTemplate corev1.Container - WaiterContainerTemplate corev1.Container - RemoteArtifactsContainerImage string - TerminationLogPath string - Prometheus PrometheusConfig - ManagerOptions ManagerOptions - Controllers Controllers - KubeAPIOptions KubeAPIOptions - GitRewriteRule bool + CtxTimeOut time.Duration + GitContainerTemplate corev1.Container + ImageProcessingContainerTemplate corev1.Container + BundleContainerTemplate corev1.Container + WaiterContainerTemplate corev1.Container + RemoteArtifactsContainerImage string + TerminationLogPath string + Prometheus PrometheusConfig + ManagerOptions ManagerOptions + Controllers Controllers + KubeAPIOptions KubeAPIOptions + GitRewriteRule bool } // PrometheusConfig contains the specific configuration for the @@ -165,10 +165,10 @@ func NewDefaultConfig() *Config { }, }, RemoteArtifactsContainerImage: remoteArtifactsDefaultImage, - MutateImageContainerTemplate: corev1.Container{ - Image: mutateImageDefaultImage, + ImageProcessingContainerTemplate: corev1.Container{ + Image: imageProcessingDefaultImage, Command: []string{ - "/ko-app/mutate-image", + "/ko-app/image-processing", }, // We explicitly define HOME=/tekton/home because this was always set in the // default configuration of Tekton until v0.24.0, see https://github.com/tektoncd/pipeline/pull/3878 @@ -261,20 +261,20 @@ func (c *Config) SetConfigFromEnv() error { c.GitContainerTemplate.Image = gitImage } - if mutateImageContainerTemplate := os.Getenv(mutateImageContainerTemplateEnvVar); mutateImageContainerTemplate != "" { - c.MutateImageContainerTemplate = corev1.Container{} - if err := json.Unmarshal([]byte(mutateImageContainerTemplate), &c.MutateImageContainerTemplate); err != nil { + if imageProcessingContainerTemplate := os.Getenv(imageProcessingContainerTemplateEnvVar); imageProcessingContainerTemplate != "" { + c.ImageProcessingContainerTemplate = corev1.Container{} + if err := json.Unmarshal([]byte(imageProcessingContainerTemplate), &c.ImageProcessingContainerTemplate); err != nil { return err } - if c.MutateImageContainerTemplate.Image == "" { - c.MutateImageContainerTemplate.Image = mutateImageDefaultImage + if c.ImageProcessingContainerTemplate.Image == "" { + c.ImageProcessingContainerTemplate.Image = imageProcessingDefaultImage } } // the dedicated environment variable for the image overwrites - // what is defined in the mutate image container template - if mutateImage := os.Getenv(mutateImageEnvVar); mutateImage != "" { - c.MutateImageContainerTemplate.Image = mutateImage + // what is defined in the image processing container template + if imageProcessingImage := os.Getenv(imageProcessingImageEnvVar); imageProcessingImage != "" { + c.ImageProcessingContainerTemplate.Image = imageProcessingImage } // Mark that the Git wrapper is suppose to use Git rewrite rule diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ea67f09101..2f1f95b8e6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -158,14 +158,14 @@ var _ = Describe("Config", func() { }) }) - It("should allow for an override of the Mutate-Image container template", func() { + It("should allow for an override of the image processing container template", func() { overrides := map[string]string{ - "MUTATE_IMAGE_CONTAINER_TEMPLATE": `{"image":"myregistry/custom/mutate-image","resources":{"requests":{"cpu":"0.5","memory":"128Mi"}}}`, + "IMAGE_PROCESSING_CONTAINER_TEMPLATE": `{"image":"myregistry/custom/image-processing","resources":{"requests":{"cpu":"0.5","memory":"128Mi"}}}`, } configWithEnvVariableOverrides(overrides, func(config *Config) { - Expect(config.MutateImageContainerTemplate).To(Equal(corev1.Container{ - Image: "myregistry/custom/mutate-image", + Expect(config.ImageProcessingContainerTemplate).To(Equal(corev1.Container{ + Image: "myregistry/custom/image-processing", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("0.5"), @@ -196,13 +196,13 @@ var _ = Describe("Config", func() { It("should allow for an override of the Mutate-Image container template and image", func() { overrides := map[string]string{ - "MUTATE_IMAGE_CONTAINER_TEMPLATE": `{"image":"myregistry/custom/mutate-image","resources":{"requests":{"cpu":"0.5","memory":"128Mi"}}}`, - "MUTATE_IMAGE_CONTAINER_IMAGE": "myregistry/custom/mutate-image:override", + "IMAGE_PROCESSING_CONTAINER_TEMPLATE": `{"image":"myregistry/custom/image-processing","resources":{"requests":{"cpu":"0.5","memory":"128Mi"}}}`, + "IMAGE_PROCESSING_CONTAINER_IMAGE": "myregistry/custom/image-processing:override", } configWithEnvVariableOverrides(overrides, func(config *Config) { - Expect(config.MutateImageContainerTemplate).To(Equal(corev1.Container{ - Image: "myregistry/custom/mutate-image:override", + Expect(config.ImageProcessingContainerTemplate).To(Equal(corev1.Container{ + Image: "myregistry/custom/image-processing:override", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("0.5"), diff --git a/pkg/image/image_suite_test.go b/pkg/image/image_suite_test.go new file mode 100644 index 0000000000..da56bdacb4 --- /dev/null +++ b/pkg/image/image_suite_test.go @@ -0,0 +1,17 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestImage(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Image Suite") +} diff --git a/pkg/image/load.go b/pkg/image/load.go new file mode 100644 index 0000000000..8a2ff31d7b --- /dev/null +++ b/pkg/image/load.go @@ -0,0 +1,93 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image + +import ( + "fmt" + "os" + "path" + + "github.com/google/go-containerregistry/pkg/name" + containerreg "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// LoadImageOrImageIndexFromDirectory loads an image or an image index from a directory +func LoadImageOrImageIndexFromDirectory(directory string) (containerreg.Image, containerreg.ImageIndex, error) { + // if we have an index.json, then we have an OCI image index + fileInfo, err := os.Stat(path.Join(directory, "index.json")) + if err == nil && !fileInfo.IsDir() { + imageIndex, err := layout.ImageIndexFromPath(directory) + if err != nil { + return nil, nil, err + } + + indexManifest, err := imageIndex.IndexManifest() + if err != nil { + return nil, nil, err + } + + // flatten an index with exactly one entry, in particular if we push an index with an index that contains the images, + // then docker pull will not work + lengthOneManifestLoop: + for len(indexManifest.Manifests) == 1 { + desc := indexManifest.Manifests[0] + + switch { + + case desc.MediaType.IsImage(): + image, err := imageIndex.Image(desc.Digest) + return image, nil, err + + case desc.MediaType.IsIndex(): + imageIndex, err = imageIndex.ImageIndex(desc.Digest) + if err != nil { + return nil, nil, err + } + + indexManifest, err = imageIndex.IndexManifest() + if err != nil { + return nil, nil, err + } + + default: + break lengthOneManifestLoop + } + } + + return nil, imageIndex, nil + } + + entries, err := os.ReadDir(directory) + if err != nil { + return nil, nil, err + } + + if len(entries) == 1 { + // tag nil is correct here, a tag would be needed if the tar ball contains several images + // which is not desired to be the case here + image, err := tarball.ImageFromPath(path.Join(directory, entries[0].Name()), nil) + return image, nil, err + } + return nil, nil, fmt.Errorf("no image was found at %q", directory) +} + +// LoadImageOrImageIndexFromRegistry loads an image or an image index from a registry +func LoadImageOrImageIndexFromRegistry(imageName name.Reference, options []remote.Option) (containerreg.Image, containerreg.ImageIndex, error) { + descriptor, err := remote.Head(imageName, options...) + if err != nil { + return nil, nil, err + } + + if descriptor.MediaType.IsIndex() { + imageIndex, err := remote.Index(imageName, options...) + return nil, imageIndex, err + } + + image, err := remote.Image(imageName, options...) + return image, nil, err +} diff --git a/pkg/image/load_test.go b/pkg/image/load_test.go new file mode 100644 index 0000000000..fb4479251e --- /dev/null +++ b/pkg/image/load_test.go @@ -0,0 +1,126 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image_test + +import ( + "os" + "path" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/shipwright-io/build/pkg/image" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("For a remote repository", func() { + + Context("that does not exist", func() { + + imageName, err := name.ParseReference("ghcr.io/shipwright-io/non-existing:latest") + Expect(err).ToNot(HaveOccurred()) + + It("LoadImageOrImageIndexFromRegistry returns an error", func() { + img, imageIndex, err := image.LoadImageOrImageIndexFromRegistry(imageName, []remote.Option{}) + Expect(err).To(HaveOccurred()) + Expect(imageIndex).To(BeNil()) + Expect(img).To(BeNil()) + }) + }) + + Context("that contains a multi-platform image", func() { + + imageName, err := name.ParseReference("ghcr.io/shipwright-io/base-git:latest") + Expect(err).ToNot(HaveOccurred()) + + It("LoadImageOrImageIndexFromRegistry returns an ImageIndex", func() { + img, imageIndex, err := image.LoadImageOrImageIndexFromRegistry(imageName, []remote.Option{}) + Expect(err).ToNot(HaveOccurred()) + Expect(imageIndex).ToNot(BeNil()) + Expect(img).To(BeNil()) + }) + }) + + Context("that contains a single image", func() { + + imageName, err := name.ParseReference("ghcr.io/shipwright-io/sample-go/source-bundle:latest") + Expect(err).ToNot(HaveOccurred()) + + It("LoadImageOrImageIndexFromRegistry returns an Image", func() { + img, imageIndex, err := image.LoadImageOrImageIndexFromRegistry(imageName, []remote.Option{}) + Expect(err).ToNot(HaveOccurred()) + Expect(imageIndex).To(BeNil()) + Expect(img).ToNot(BeNil()) + }) + }) +}) + +var _ = Describe("For a local directory", func() { + + Context("that is empty", func() { + + var directory string + + BeforeEach(func() { + directory, err := os.MkdirTemp(os.TempDir(), "empty") + Expect(err).ToNot(HaveOccurred()) + + DeferCleanup(func() { + err := os.RemoveAll(directory) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + It("LoadImageOrImageIndexFromDirectory returns an error", func() { + img, imageIndex, err := image.LoadImageOrImageIndexFromDirectory(directory) + Expect(err).To(HaveOccurred()) + Expect(imageIndex).To(BeNil()) + Expect(img).To(BeNil()) + }) + }) + + Context("that contains a multi-platform image", func() { + + cwd, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + directory := path.Clean(path.Join(cwd, "../..", "test/data/images/multi-platform-image-in-oci")) + + It("LoadImageOrImageIndexFromDirectory returns an ImageIndex", func() { + img, imageIndex, err := image.LoadImageOrImageIndexFromDirectory(directory) + Expect(err).ToNot(HaveOccurred()) + Expect(imageIndex).ToNot(BeNil()) + Expect(img).To(BeNil()) + }) + }) + + Context("that contains a single tar file with an image", func() { + + cwd, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + directory := path.Clean(path.Join(cwd, "../..", "test/data/images/single-image")) + + It("LoadImageOrImageIndexFromDirectory returns an Image", func() { + img, imageIndex, err := image.LoadImageOrImageIndexFromDirectory(directory) + Expect(err).ToNot(HaveOccurred()) + Expect(imageIndex).To(BeNil()) + Expect(img).ToNot(BeNil()) + }) + }) + + Context("that contains am OCI image layout with a single a image", func() { + + cwd, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + directory := path.Clean(path.Join(cwd, "../..", "test/data/images/single-image-in-oci")) + + It("LoadImageOrImageIndexFromDirectory returns an Image", func() { + img, imageIndex, err := image.LoadImageOrImageIndexFromDirectory(directory) + Expect(err).ToNot(HaveOccurred()) + Expect(imageIndex).To(BeNil()) + Expect(img).ToNot(BeNil()) + }) + }) +}) diff --git a/pkg/image/mutate.go b/pkg/image/mutate.go new file mode 100644 index 0000000000..02ce0c8e6c --- /dev/null +++ b/pkg/image/mutate.go @@ -0,0 +1,121 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image + +import ( + "errors" + + containerreg "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// MutateImageOrImageIndex mutates an image or image index with additional annotations and labels +func MutateImageOrImageIndex(image containerreg.Image, imageIndex containerreg.ImageIndex, annotations map[string]string, labels map[string]string) (containerreg.Image, containerreg.ImageIndex, error) { + if imageIndex != nil { + indexManifest, err := imageIndex.IndexManifest() + if err != nil { + return nil, nil, err + } + + if len(labels) > 0 || len(annotations) > 0 { + for _, descriptor := range indexManifest.Manifests { + digest := descriptor.Digest + var appendable mutate.Appendable + + switch descriptor.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + childImageIndex, err := imageIndex.ImageIndex(digest) + if err != nil { + return nil, nil, err + } + _, childImageIndex, err = MutateImageOrImageIndex(nil, childImageIndex, annotations, labels) + if err != nil { + return nil, nil, err + } + + appendable = childImageIndex + case types.OCIManifestSchema1, types.DockerManifestSchema2: + image, err := imageIndex.Image(digest) + if err != nil { + return nil, nil, err + } + + image, err = mutateImage(image, annotations, labels) + if err != nil { + return nil, nil, err + } + + appendable = image + default: + continue + } + + imageIndex = mutate.RemoveManifests(imageIndex, func(desc containerreg.Descriptor) bool { + return desc.Digest.String() == digest.String() + }) + + imageIndex = mutate.AppendManifests(imageIndex, mutate.IndexAddendum{ + Add: appendable, + Descriptor: containerreg.Descriptor{ + Annotations: descriptor.Annotations, + MediaType: descriptor.MediaType, + Platform: descriptor.Platform, + URLs: descriptor.URLs, + }, + }) + } + } + + if len(annotations) > 0 { + var castSucceeded bool + imageIndex, castSucceeded = mutate.Annotations(imageIndex, annotations).(containerreg.ImageIndex) + if !castSucceeded { + return nil, nil, errors.New("expected mutate.Annotation to return an ImageIndex when passing in an ImageIndex") + } + } + } else { + var err error + image, err = mutateImage(image, annotations, labels) + if err != nil { + return nil, nil, err + } + } + + return image, imageIndex, nil +} + +func mutateImage(image containerreg.Image, annotations map[string]string, labels map[string]string) (containerreg.Image, error) { + if len(labels) > 0 { + cfg, err := image.ConfigFile() + if err != nil { + return nil, err + } + cfg = cfg.DeepCopy() + + if cfg.Config.Labels == nil { + cfg.Config.Labels = labels + } else { + for key, value := range labels { + cfg.Config.Labels[key] = value + } + } + + image, err = mutate.ConfigFile(image, cfg) + if err != nil { + return nil, err + } + } + + if len(annotations) > 0 { + var castSucceeded bool + image, castSucceeded = mutate.Annotations(image, annotations).(containerreg.Image) + if !castSucceeded { + return nil, errors.New("expected mutate.Annotation to return an Image when passing in an Image") + } + } + + return image, nil +} diff --git a/pkg/image/mutate_test.go b/pkg/image/mutate_test.go new file mode 100644 index 0000000000..32e2611d2e --- /dev/null +++ b/pkg/image/mutate_test.go @@ -0,0 +1,138 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image_test + +import ( + containerreg "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/shipwright-io/build/pkg/image" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("MutateImageOrImageIndex", func() { + + Context("for an image that has no annotations or labels", func() { + + img, err := random.Image(1234, 1) + Expect(err).ToNot(HaveOccurred()) + + It("correctly adds labels and annotations", func() { + newImg, newImageIndex, err := image.MutateImageOrImageIndex(img, nil, map[string]string{ + "annotation1": "someValue", + }, map[string]string{ + "label": "someLabelValue", + }) + + Expect(err).ToNot(HaveOccurred()) + Expect(newImageIndex).To(BeNil()) + Expect(newImg).ToNot(BeNil()) + Expect(img).ToNot(Equal(newImg)) + + manifest, err := newImg.Manifest() + Expect(err).ToNot(HaveOccurred()) + Expect(manifest.Annotations).To(HaveLen(1)) + Expect(manifest.Annotations["annotation1"]).To(Equal("someValue")) + + configFile, err := newImg.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + Expect(configFile.Config.Labels).To(HaveLen(1)) + Expect(configFile.Config.Labels["label"]).To(Equal("someLabelValue")) + }) + }) + + Context("For an image that has annotations and labels", func() { + + img, err := random.Image(1234, 1) + Expect(err).ToNot(HaveOccurred()) + img = mutate.Annotations(img, map[string]string{ + "existingAnnotation1": "initialValue1", + "existingAnnotation2": "initialValue2", + }).(containerreg.Image) + + cfg, err := img.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + cfg = cfg.DeepCopy() + cfg.Config.Labels = map[string]string{ + "existingLabel1": "initialValue1", + "existingLabel2": "initialValue2", + } + img, err = mutate.ConfigFile(img, cfg) + Expect(err).ToNot(HaveOccurred()) + + It("correctly adds and overwrites labels and annotations", func() { + newImg, newImageIndex, err := image.MutateImageOrImageIndex(img, nil, map[string]string{ + "annotation1": "someValue", + "existingAnnotation1": "newValue", + }, map[string]string{ + "label": "someLabelValue", + "existingLabel1": "newValue", + }) + + Expect(err).ToNot(HaveOccurred()) + Expect(newImageIndex).To(BeNil()) + Expect(newImg).ToNot(BeNil()) + Expect(img).ToNot(Equal(newImg)) + + manifest, err := newImg.Manifest() + Expect(err).ToNot(HaveOccurred()) + Expect(manifest.Annotations).To(HaveLen(3)) + Expect(manifest.Annotations["annotation1"]).To(Equal("someValue")) + Expect(manifest.Annotations["existingAnnotation1"]).To(Equal("newValue")) + Expect(manifest.Annotations["existingAnnotation2"]).To(Equal("initialValue2")) + + configFile, err := newImg.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + Expect(configFile.Config.Labels).To(HaveLen(3)) + Expect(configFile.Config.Labels["label"]).To(Equal("someLabelValue")) + Expect(configFile.Config.Labels["existingLabel1"]).To(Equal("newValue")) + Expect(configFile.Config.Labels["existingLabel2"]).To(Equal("initialValue2")) + }) + }) + + Context("for an index that has no annotations or labels", func() { + + index, err := random.Index(4091, 2, 2) + Expect(err).ToNot(HaveOccurred()) + + It("correctly adds labels and annotations to the index and the images", func() { + newImg, newImageIndex, err := image.MutateImageOrImageIndex(nil, index, map[string]string{ + "annotation1": "someValue", + }, map[string]string{ + "label": "someLabelValue", + }) + + Expect(err).ToNot(HaveOccurred()) + Expect(newImageIndex).ToNot(BeNil()) + Expect(newImg).To(BeNil()) + Expect(index).ToNot(Equal(newImageIndex)) + + // verify the annotations of the index + indexManifest, err := newImageIndex.IndexManifest() + Expect(err).ToNot(HaveOccurred()) + Expect(indexManifest.Annotations).To(HaveLen(1)) + Expect(indexManifest.Annotations["annotation1"]).To(Equal("someValue")) + + // verify the images + Expect(indexManifest.Manifests).To(HaveLen(2)) + for _, descriptor := range indexManifest.Manifests { + img, err := newImageIndex.Image(descriptor.Digest) + Expect(err).ToNot(HaveOccurred()) + + manifest, err := img.Manifest() + Expect(err).ToNot(HaveOccurred()) + Expect(manifest.Annotations).To(HaveLen(1)) + Expect(manifest.Annotations["annotation1"]).To(Equal("someValue")) + + configFile, err := img.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + Expect(configFile.Config.Labels).To(HaveLen(1)) + Expect(configFile.Config.Labels["label"]).To(Equal("someLabelValue")) + } + }) + }) +}) diff --git a/pkg/image/options.go b/pkg/image/options.go new file mode 100644 index 0000000000..b3437cf377 --- /dev/null +++ b/pkg/image/options.go @@ -0,0 +1,145 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/configfile" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// optionsRoundTripper ensures that the insecure flag is honored, and sets headers on outgoing requests +type optionsRoundTripper struct { + inner http.RoundTripper + httpHeaders map[string]string + insecure bool +} + +// RoundTrip implements http.RoundTripper +func (ort *optionsRoundTripper) RoundTrip(in *http.Request) (*http.Response, error) { + // Make sure that HTTP traffic is only used if insecure is set to true + if !ort.insecure && in.URL != nil && in.URL.Scheme == "http" { + return nil, errors.New("the http protocol is not allowed") + } + + for k, v := range ort.httpHeaders { + in.Header.Set(k, v) + } + + in.Header.Set("User-Agent", "Shipwright Build") + + return ort.inner.RoundTrip(in) +} + +// GetOptions constructs go-containerregistry options to access the remote registry, in addition, it returns the authentication separately +func GetOptions(ctx context.Context, imageName name.Reference, insecure bool, dockerConfigJSONPath string) ([]remote.Option, *authn.AuthConfig, error) { + var options []remote.Option + + options = append(options, remote.WithContext(ctx)) + + transport := http.DefaultTransport.(*http.Transport).Clone() + + if insecure { + // #nosec:G402 explicitly requested by user to use insecure registry + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } else { + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: false, + MinVersion: tls.VersionTLS12, + } + } + + // find a Docker config.json + if dockerConfigJSONPath != "" { + // if we have a value provided already, then we support a directory that contains a .dockerconfigjson file + mountedSecretDefaultFileName := filepath.Join(dockerConfigJSONPath, ".dockerconfigjson") + if _, err := os.Stat(mountedSecretDefaultFileName); err == nil { + dockerConfigJSONPath = mountedSecretDefaultFileName + } + } + + var dockerconfig *configfile.ConfigFile + if dockerConfigJSONPath != "" { + file, err := os.Open(dockerConfigJSONPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to open the config json: %w", err) + } + defer file.Close() + + if dockerconfig, err = config.LoadFromReader(file); err != nil { + return nil, nil, err + } + } + + // Add a RoundTripper + rt := &optionsRoundTripper{ + inner: transport, + insecure: insecure, + } + if dockerconfig != nil && len(dockerconfig.HTTPHeaders) > 0 { + rt.httpHeaders = dockerconfig.HTTPHeaders + } + options = append(options, remote.WithTransport(rt)) + + // Authentication + auth := &authn.AuthConfig{} + if dockerconfig != nil { + registryName := imageName.Context().RegistryStr() + if registryName == name.DefaultRegistry { + registryName = authn.DefaultAuthKey + } + + authConfig, err := dockerconfig.GetAuthConfig(registryName) + if err != nil { + return nil, nil, err + } + + // Return an error in case the credentials do not match the desired + // registry and list all servers that actually are available + if authConfig.ServerAddress != registryName { + var servers []string + for name := range dockerconfig.GetAuthConfigs() { + servers = append(servers, name) + } + + var availableConfigs string + if len(servers) > 0 { + availableConfigs = strings.Join(servers, ", ") + } else { + availableConfigs = "none" + } + + return nil, nil, fmt.Errorf("failed to find registry credentials for %s, available configurations: %s", + registryName, + availableConfigs, + ) + } + + auth = &authn.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + } + } + + options = append(options, remote.WithAuth(authn.FromConfig(*auth))) + + return options, auth, nil +} diff --git a/pkg/image/push.go b/pkg/image/push.go new file mode 100644 index 0000000000..617aa9c800 --- /dev/null +++ b/pkg/image/push.go @@ -0,0 +1,54 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image + +import ( + "github.com/google/go-containerregistry/pkg/name" + containerreg "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// PushImageOrImageIndex pushes and image or image index and returns the digest and size. The size is only returned for an image. +func PushImageOrImageIndex(imageName name.Reference, image containerreg.Image, imageIndex containerreg.ImageIndex, options []remote.Option) (string, int64, error) { + var digest string + var size int64 + size = -1 + + if image != nil { + if err := remote.Write(imageName, image, options...); err != nil { + return "", 0, err + } + + hash, err := image.Digest() + if err != nil { + return "", 0, err + } + digest = hash.String() + + manifest, err := image.Manifest() + if err != nil { + return "", 0, err + } + + size = manifest.Config.Size + for _, layer := range manifest.Layers { + size += layer.Size + } + } + + if imageIndex != nil { + if err := remote.WriteIndex(imageName, imageIndex, options...); err != nil { + return "", 0, err + } + + hash, err := imageIndex.Digest() + if err != nil { + return "", 0, err + } + digest = hash.String() + } + + return digest, size, nil +} diff --git a/pkg/image/push_test.go b/pkg/image/push_test.go new file mode 100644 index 0000000000..ff0b3a01b8 --- /dev/null +++ b/pkg/image/push_test.go @@ -0,0 +1,88 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image_test + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/shipwright-io/build/pkg/image" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("PushImageOrImageIndex", func() { + + var registryHost string + + BeforeEach(func() { + logger := log.New(ioutil.Discard, "", 0) + reg := registry.New(registry.Logger(logger)) + // Use the following instead to see which requests happened + // reg := registry.New() + server := httptest.NewServer(reg) + DeferCleanup(func() { + server.Close() + }) + registryHost = strings.ReplaceAll(server.URL, "http://", "") + }) + + Context("For an image", func() { + + img, err := random.Image(3245, 1) + Expect(err).ToNot(HaveOccurred()) + + It("pushes the image", func() { + imageName, err := name.ParseReference(fmt.Sprintf("%s/%s/%s", registryHost, "test-namespace", "test-image")) + Expect(err).ToNot(HaveOccurred()) + + digest, size, err := image.PushImageOrImageIndex(imageName, img, nil, []remote.Option{}) + Expect(err).ToNot(HaveOccurred()) + Expect(digest).To(HavePrefix("sha")) + // The size includes the manifest which depends on the config which depends on the registry host + Expect(size > 3500).To(BeTrue()) + + // Verify the existence of the manifest + request, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/test-namespace/test-image/manifests/latest", registryHost), nil) + Expect(err).ToNot(HaveOccurred()) + response, err := http.DefaultClient.Do(request) + Expect(err).ToNot(HaveOccurred()) + Expect(response.StatusCode).To(Equal(200)) + }) + }) + + Context("For an index", func() { + + index, err := random.Index(1234, 1, 2) + Expect(err).ToNot(HaveOccurred()) + + It("pushes the index", func() { + imageName, err := name.ParseReference(fmt.Sprintf("%s/%s/%s:%s", registryHost, "test-namespace", "test-index", "test-tag")) + Expect(err).ToNot(HaveOccurred()) + + digest, size, err := image.PushImageOrImageIndex(imageName, nil, index, []remote.Option{}) + Expect(err).ToNot(HaveOccurred()) + Expect(digest).To(HavePrefix("sha")) + // The size includes the manifest which depends on the config which depends on the registry host + Expect(size).To(BeEquivalentTo(-1)) + + // Verify the existence of the manifest + request, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/test-namespace/test-index/manifests/test-tag", registryHost), nil) + Expect(err).ToNot(HaveOccurred()) + response, err := http.DefaultClient.Do(request) + Expect(err).ToNot(HaveOccurred()) + Expect(response.StatusCode).To(Equal(200)) + }) + }) +}) diff --git a/pkg/reconciler/buildrun/resources/image_processing.go b/pkg/reconciler/buildrun/resources/image_processing.go new file mode 100644 index 0000000000..b10cf5e9ba --- /dev/null +++ b/pkg/reconciler/buildrun/resources/image_processing.go @@ -0,0 +1,157 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package resources + +import ( + "fmt" + + build "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/build/pkg/config" + "github.com/shipwright-io/build/pkg/reconciler/buildrun/resources/sources" + pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + core "k8s.io/api/core/v1" +) + +const ( + containerNameImageProcessing = "image-processing" + outputDirectoryMountPath = "/workspace/output-image" + paramOutputDirectory = "output-directory" +) + +// SetupImageProcessing appends the image-processing step to a TaskRun if desired +func SetupImageProcessing(taskRun *pipeline.TaskRun, cfg *config.Config, buildOutput, buildRunOutput build.Image) { + stepArgs := []string{} + + // Check if any build step references the output-directory system parameter. If that is the case, + // then we assume that Shipwright performs the image push operation. + volumeAdded := false + prefixedOuputDirectory := fmt.Sprintf("%s-%s", prefixParamsResultsVolumes, paramOutputDirectory) + for i, step := range taskRun.Spec.TaskSpec.Steps { + if isStepReferencingParameter(&step, prefixedOuputDirectory) { + if !volumeAdded { + volumeAdded = true + + // add an emptyDir volume for the output directory + taskRun.Spec.TaskSpec.Volumes = append(taskRun.Spec.TaskSpec.Volumes, core.Volume{ + Name: prefixedOuputDirectory, + VolumeSource: core.VolumeSource{ + EmptyDir: &core.EmptyDirVolumeSource{}, + }, + }) + + // add the parameter definition + taskRun.Spec.TaskSpec.Params = append(taskRun.Spec.TaskSpec.Params, pipeline.ParamSpec{ + Name: prefixedOuputDirectory, + Type: pipeline.ParamTypeString, + }) + + // add the parameter value + taskRun.Spec.Params = append(taskRun.Spec.Params, pipeline.Param{ + Name: prefixedOuputDirectory, + Value: pipeline.ArrayOrString{ + StringVal: outputDirectoryMountPath, + Type: pipeline.ParamTypeString, + }, + }) + + // add the push argument to the command + stepArgs = append(stepArgs, "--push", fmt.Sprintf("$(params.%s-%s)", prefixParamsResultsVolumes, paramOutputDirectory)) + } + + // add a volumeMount to the step + taskRun.Spec.TaskSpec.Steps[i].VolumeMounts = append(taskRun.Spec.TaskSpec.Steps[i].VolumeMounts, core.VolumeMount{ + Name: prefixedOuputDirectory, + MountPath: outputDirectoryMountPath, + ReadOnly: false, + }) + } + } + + // check if we need to set image labels + annotations := mergeMaps(buildOutput.Annotations, buildRunOutput.Annotations) + if len(annotations) > 0 { + stepArgs = append(stepArgs, convertMutateArgs("--annotation", annotations)...) + } + + // check if we need to set image labels + labels := mergeMaps(buildOutput.Labels, buildRunOutput.Labels) + if len(labels) > 0 { + stepArgs = append(stepArgs, convertMutateArgs("--label", labels)...) + } + + // check if there is anything to do + if len(stepArgs) > 0 { + // add the image argument + stepArgs = append(stepArgs, "--image", fmt.Sprintf("$(params.%s-%s)", prefixParamsResultsVolumes, paramOutputImage)) + + // add the insecure flag + stepArgs = append(stepArgs, fmt.Sprintf("--insecure=$(params.%s-%s)", prefixParamsResultsVolumes, paramOutputInsecure)) + + // add the result arguments + stepArgs = append(stepArgs, "--result-file-image-digest", fmt.Sprintf("$(results.%s-%s.path)", prefixParamsResultsVolumes, imageDigestResult)) + stepArgs = append(stepArgs, "--result-file-image-size", fmt.Sprintf("$(results.%s-%s.path)", prefixParamsResultsVolumes, imageSizeResult)) + + // add the push step + // initialize the step from the template + imageProcessingStep := pipeline.Step{ + Container: *cfg.ImageProcessingContainerTemplate.DeepCopy(), + } + + imageProcessingStep.Name = containerNameImageProcessing + imageProcessingStep.Args = stepArgs + + if volumeAdded { + imageProcessingStep.VolumeMounts = append(imageProcessingStep.VolumeMounts, core.VolumeMount{ + Name: prefixedOuputDirectory, + MountPath: outputDirectoryMountPath, + ReadOnly: true, + }) + } + + if buildOutput.Credentials != nil { + sources.AppendSecretVolume(taskRun.Spec.TaskSpec, buildOutput.Credentials.Name) + + secretMountPath := fmt.Sprintf("/workspace/%s-push-secret", prefixParamsResultsVolumes) + + // define the volume mount on the container + imageProcessingStep.VolumeMounts = append(imageProcessingStep.VolumeMounts, core.VolumeMount{ + Name: sources.SanitizeVolumeNameForSecretName(buildOutput.Credentials.Name), + MountPath: secretMountPath, + ReadOnly: true, + }) + + // append the argument + imageProcessingStep.Container.Args = append(imageProcessingStep.Container.Args, + "--secret-path", secretMountPath, + ) + } + + // append the mutate step + taskRun.Spec.TaskSpec.Steps = append(taskRun.Spec.TaskSpec.Steps, imageProcessingStep) + } +} + +// convertMutateArgs to convert the argument map to comma seprated values +func convertMutateArgs(flag string, args map[string]string) []string { + var result []string + + for key, value := range args { + result = append(result, flag, fmt.Sprintf("%s=%s", key, value)) + } + + return result +} + +// mergeMaps takes 2 maps as input and merge the second into the first +// values in second would takes precedence if both maps have same keys +func mergeMaps(first map[string]string, second map[string]string) map[string]string { + if len(first) == 0 { + first = map[string]string{} + } + for k, v := range second { + first[k] = v + } + return first +} diff --git a/pkg/reconciler/buildrun/resources/image_processing_test.go b/pkg/reconciler/buildrun/resources/image_processing_test.go new file mode 100644 index 0000000000..54c3c2ad99 --- /dev/null +++ b/pkg/reconciler/buildrun/resources/image_processing_test.go @@ -0,0 +1,189 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package resources_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + + buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/build/pkg/config" + "github.com/shipwright-io/build/pkg/reconciler/buildrun/resources" + "github.com/shipwright-io/build/test/utils" + pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" +) + +var _ = Describe("Image Processing overrides", func() { + + config := config.NewDefaultConfig() + var processedTaskRun *pipeline.TaskRun + + Context("for a TaskRun that does not reference the output directory", func() { + taskRun := &pipeline.TaskRun{ + Spec: pipeline.TaskRunSpec{ + TaskSpec: &pipeline.TaskSpec{ + Steps: []pipeline.Step{ + { + Container: corev1.Container{ + Name: "test-step", + }, + }, + }, + }, + }, + } + + Context("for a build without labels and annotation in the output", func() { + BeforeEach(func() { + processedTaskRun = taskRun.DeepCopy() + resources.SetupImageProcessing(processedTaskRun, config, buildv1alpha1.Image{ + Image: "some-registry/some-namespace/some-image", + }, buildv1alpha1.Image{}) + }) + + It("does not add the image-processing step", func() { + Expect(processedTaskRun.Spec.TaskSpec.Steps).To(HaveLen(1)) + Expect(processedTaskRun.Spec.TaskSpec.Steps).ToNot(utils.ContainNamedElement("image-processing")) + }) + }) + + Context("for a build with a label in the output", func() { + BeforeEach(func() { + processedTaskRun = taskRun.DeepCopy() + resources.SetupImageProcessing(processedTaskRun, config, buildv1alpha1.Image{ + Image: "some-registry/some-namespace/some-image", + Labels: map[string]string{ + "aKey": "aLabel", + }, + }, buildv1alpha1.Image{}) + }) + + It("adds the image-processing step", func() { + Expect(processedTaskRun.Spec.TaskSpec.Steps).To(HaveLen(2)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Name).To(Equal("image-processing")) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Image).To(Equal(config.ImageProcessingContainerTemplate.Image)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Command).To(Equal(config.ImageProcessingContainerTemplate.Command)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Args).To(Equal([]string{ + "--label", + "aKey=aLabel", + "--image", + "$(params.shp-output-image)", + "--insecure=$(params.shp-output-insecure)", + "--result-file-image-digest", + "$(results.shp-image-digest.path)", + "--result-file-image-size", + "$(results.shp-image-size.path)", + })) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].VolumeMounts).ToNot(utils.ContainNamedElement("shp-output-directory")) + }) + }) + }) + + Context("for a TaskRun that references the output directory", func() { + + taskRun := &pipeline.TaskRun{ + Spec: pipeline.TaskRunSpec{ + TaskSpec: &pipeline.TaskSpec{ + Steps: []pipeline.Step{ + { + Container: corev1.Container{ + Name: "test-step", + Args: []string{ + "$(params.shp-output-directory)", + }, + }, + }, + }, + }, + }, + } + + Context("for a build with an output without a secret", func() { + BeforeEach(func() { + processedTaskRun = taskRun.DeepCopy() + resources.SetupImageProcessing(processedTaskRun, config, buildv1alpha1.Image{ + Image: "some-registry/some-namespace/some-image", + }, buildv1alpha1.Image{}) + }) + + It("adds the output-directory parameter", func() { + Expect(processedTaskRun.Spec.TaskSpec.Params).To(utils.ContainNamedElement("shp-output-directory")) + Expect(processedTaskRun.Spec.Params).To(utils.ContainNamedElement("shp-output-directory")) + }) + + It("adds a volume for the output directory", func() { + Expect(processedTaskRun.Spec.TaskSpec.Volumes).To(utils.ContainNamedElement("shp-output-directory")) + }) + + It("adds the image-processing step", func() { + Expect(processedTaskRun.Spec.TaskSpec.Steps).To(HaveLen(2)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Name).To(Equal("image-processing")) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Image).To(Equal(config.ImageProcessingContainerTemplate.Image)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Command).To(Equal(config.ImageProcessingContainerTemplate.Command)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Args).To(Equal([]string{ + "--push", + "$(params.shp-output-directory)", + "--image", + "$(params.shp-output-image)", + "--insecure=$(params.shp-output-insecure)", + "--result-file-image-digest", + "$(results.shp-image-digest.path)", + "--result-file-image-size", + "$(results.shp-image-size.path)", + })) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].VolumeMounts).To(utils.ContainNamedElement("shp-output-directory")) + }) + }) + + Context("for a build with an output with a secret", func() { + BeforeEach(func() { + processedTaskRun = taskRun.DeepCopy() + resources.SetupImageProcessing(processedTaskRun, config, buildv1alpha1.Image{ + Image: "some-registry/some-namespace/some-image", + Credentials: &corev1.LocalObjectReference{ + Name: "some-secret", + }, + }, buildv1alpha1.Image{}) + }) + + It("adds the output-directory parameter", func() { + Expect(processedTaskRun.Spec.TaskSpec.Params).To(utils.ContainNamedElement("shp-output-directory")) + Expect(processedTaskRun.Spec.Params).To(utils.ContainNamedElement("shp-output-directory")) + }) + + It("adds a volume for the output directory", func() { + Expect(processedTaskRun.Spec.TaskSpec.Volumes).To(utils.ContainNamedElement("shp-output-directory")) + }) + + It("adds a value for the output secret", func() { + Expect(processedTaskRun.Spec.TaskSpec.Volumes).To(utils.ContainNamedElement("shp-some-secret")) + }) + + It("adds the image-processing step", func() { + Expect(processedTaskRun.Spec.TaskSpec.Steps).To(HaveLen(2)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Name).To(Equal("image-processing")) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Image).To(Equal(config.ImageProcessingContainerTemplate.Image)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Command).To(Equal(config.ImageProcessingContainerTemplate.Command)) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].Args).To(Equal([]string{ + "--push", + "$(params.shp-output-directory)", + "--image", + "$(params.shp-output-image)", + "--insecure=$(params.shp-output-insecure)", + "--result-file-image-digest", + "$(results.shp-image-digest.path)", + "--result-file-image-size", + "$(results.shp-image-size.path)", + "--secret-path", + "/workspace/shp-push-secret", + })) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].VolumeMounts).To(utils.ContainNamedElement("shp-output-directory")) + Expect(processedTaskRun.Spec.TaskSpec.Steps[1].VolumeMounts).To(utils.ContainNamedElement("shp-some-secret")) + }) + }) + }) +}) diff --git a/pkg/reconciler/buildrun/resources/mutate.go b/pkg/reconciler/buildrun/resources/mutate.go deleted file mode 100644 index 558b598e0f..0000000000 --- a/pkg/reconciler/buildrun/resources/mutate.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright The Shipwright Contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package resources - -import ( - "fmt" - - tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" - - buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" - "github.com/shipwright-io/build/pkg/config" -) - -// amendTaskSpecWithImageMutate add more steps to Tekton's Task in order to -// mutate the image with annotations and labels -func amendTaskSpecWithImageMutate( - cfg *config.Config, - taskSpec *tektonv1beta1.TaskSpec, - buildOutput, buildRunOutput buildv1alpha1.Image, -) { - // initialize the step from the template - mutateStep := tektonv1beta1.Step{ - Container: *cfg.MutateImageContainerTemplate.DeepCopy(), - } - - mutateStep.Container.Name = imageMutateContainerName - - // if labels or annotations are specified in buildRun then merge them with build's - labels := mergeMaps(buildOutput.Labels, buildRunOutput.Labels) - annotations := mergeMaps(buildOutput.Annotations, buildRunOutput.Annotations) - - mutateStep.Container.Args = mutateArgs(annotations, labels) - - // append the mutate step - taskSpec.Steps = append(taskSpec.Steps, mutateStep) -} - -// mergeMaps takes 2 maps as input and merge the second into the first -// values in second would takes precedence if both maps have same keys -func mergeMaps(first map[string]string, second map[string]string) map[string]string { - if len(first) == 0 { - first = map[string]string{} - } - for k, v := range second { - first[k] = v - } - return first -} - -func mutateArgs(annotations, labels map[string]string) []string { - args := []string{ - "--image", - fmt.Sprintf("$(params.%s-%s)", prefixParamsResultsVolumes, paramOutputImage), - "--result-file-image-digest", - fmt.Sprintf("$(results.%s-%s.path)", prefixParamsResultsVolumes, imageDigestResult), - "result-file-image-size", - fmt.Sprintf("$(results.%s-%s.path)", prefixParamsResultsVolumes, imageSizeResult), - } - - if len(annotations) > 0 { - args = append(args, convertMutateArgs("--annotation", annotations)...) - } - - if len(labels) > 0 { - args = append(args, convertMutateArgs("--label", labels)...) - } - - return args -} - -// convertMutateArgs to convert the argument map to comma seprated values -func convertMutateArgs(flag string, args map[string]string) []string { - var result []string - - for key, value := range args { - result = append(result, flag, fmt.Sprintf("%s=%s", key, value)) - } - - return result -} diff --git a/pkg/reconciler/buildrun/resources/taskrun.go b/pkg/reconciler/buildrun/resources/taskrun.go index 082be737f7..d0e6368349 100644 --- a/pkg/reconciler/buildrun/resources/taskrun.go +++ b/pkg/reconciler/buildrun/resources/taskrun.go @@ -24,17 +24,16 @@ import ( const ( prefixParamsResultsVolumes = "shp" - paramOutputImage = "output-image" - paramSourceRoot = "source-root" - paramSourceContext = "source-context" + paramOutputImage = "output-image" + paramOutputInsecure = "output-insecure" + paramSourceRoot = "source-root" + paramSourceContext = "source-context" workspaceSource = "source" inputParamBuilder = "BUILDER_IMAGE" inputParamDockerfile = "DOCKERFILE" inputParamContextDir = "CONTEXT_DIR" - - imageMutateContainerName = "mutate-image" ) // getStringTransformations gets us MANDATORY replacements using @@ -95,6 +94,11 @@ func GenerateTaskSpec( Description: "The URL of the image that the build produces", Type: v1beta1.ParamTypeString, }, + { + Name: fmt.Sprintf("%s-%s", prefixParamsResultsVolumes, paramOutputInsecure), + Description: "A flag indicating that the output image is on an insecure container registry", + Type: v1beta1.ParamTypeString, + }, { Name: fmt.Sprintf("%s-%s", prefixParamsResultsVolumes, paramSourceContext), Description: "The context directory inside the source directory", @@ -219,7 +223,7 @@ func GenerateTaskSpec( // here we should check that volume actually exists for this mount // and in case it does not, exit early with an error if _, ok := buildStrategyVolumesMap[vm.Name]; !ok { - return nil, fmt.Errorf("Volume for the Volume Mount %q is not found", vm.Name) + return nil, fmt.Errorf("volume for the Volume Mount %q is not found", vm.Name) } volumeMounts[vm.Name] = vm.ReadOnly } @@ -232,18 +236,6 @@ func GenerateTaskSpec( } generatedTaskSpec.Volumes = append(generatedTaskSpec.Volumes, volumes...) - buildRunOutput := buildRun.Spec.Output - if buildRunOutput == nil { - buildRunOutput = &buildv1alpha1.Image{} - } - - // Amending task spec with image mutate step if annotations or labels are - // specified in build manifest or buildRun manifest - if len(build.Spec.Output.Annotations) > 0 || len(build.Spec.Output.Labels) > 0 || - len(buildRunOutput.Annotations) > 0 || len(buildRunOutput.Labels) > 0 { - amendTaskSpecWithImageMutate(cfg, &generatedTaskSpec, build.Spec.Output, *buildRunOutput) - } - return &generatedTaskSpec, nil } @@ -264,6 +256,17 @@ func GenerateTaskRun( image = build.Spec.Output.Image } + insecure := false + if buildRun.Spec.Output != nil && buildRun.Spec.Output.Insecure != nil { + insecure = *buildRun.Spec.Output.Insecure + } else if build.Spec.Output.Insecure != nil { + insecure = *build.Spec.Output.Insecure + } + insecureString := "false" + if insecure { + insecureString = "true" + } + taskSpec, err := GenerateTaskSpec( cfg, build, @@ -333,6 +336,14 @@ func GenerateTaskRun( StringVal: image, }, }, + { + // shp-output-insecure + Name: fmt.Sprintf("%s-%s", prefixParamsResultsVolumes, paramOutputInsecure), + Value: v1beta1.ArrayOrString{ + Type: v1beta1.ParamTypeString, + StringVal: insecureString, + }, + }, { Name: fmt.Sprintf("%s-%s", prefixParamsResultsVolumes, paramSourceRoot), Value: v1beta1.ArrayOrString{ @@ -403,6 +414,13 @@ func GenerateTaskRun( } } + // Setup image processing + buildRunOutput := buildRun.Spec.Output + if buildRunOutput == nil { + buildRunOutput = &buildv1alpha1.Image{} + } + SetupImageProcessing(expectedTaskRun, cfg, build.Spec.Output, *buildRunOutput) + return expectedTaskRun, nil } diff --git a/pkg/reconciler/buildrun/resources/taskrun_test.go b/pkg/reconciler/buildrun/resources/taskrun_test.go index 9a13604d9a..735bd929fc 100644 --- a/pkg/reconciler/buildrun/resources/taskrun_test.go +++ b/pkg/reconciler/buildrun/resources/taskrun_test.go @@ -75,8 +75,9 @@ var _ = Describe("GenerateTaskrun", func() { }) JustBeforeEach(func() { - got, err = resources.GenerateTaskSpec(config.NewDefaultConfig(), build, buildRun, buildStrategy.Spec.BuildSteps, []buildv1alpha1.Parameter{}, buildStrategy.GetVolumes()) - Expect(err).To(BeNil()) + taskRun, err := resources.GenerateTaskRun(config.NewDefaultConfig(), build, buildRun, "", buildStrategy) + Expect(err).ToNot(HaveOccurred()) + got = taskRun.Spec.TaskSpec }) It("should contain a step to clone the Git sources", func() { @@ -141,29 +142,32 @@ var _ = Describe("GenerateTaskrun", func() { Expect(got.Params).To(utils.ContainNamedElement("shp-source-root")) Expect(got.Params).To(utils.ContainNamedElement("shp-source-context")) Expect(got.Params).To(utils.ContainNamedElement("shp-output-image")) + Expect(got.Params).To(utils.ContainNamedElement("shp-output-insecure")) // legacy params Expect(got.Params).ToNot(utils.ContainNamedElement("BUILDER_IMAGE")) // test build has no builder image Expect(got.Params).To(utils.ContainNamedElement("CONTEXT_DIR")) Expect(got.Params).To(utils.ContainNamedElement("DOCKERFILE")) - Expect(len(got.Params)).To(Equal(5)) + Expect(len(got.Params)).To(Equal(6)) }) It("should contain a step to mutate the image with single mutate args", func() { - Expect(got.Steps[3].Name).To(Equal("mutate-image")) - Expect(got.Steps[3].Command[0]).To(Equal("/ko-app/mutate-image")) - Expect(got.Steps[3].Args).To(Equal([]string{ + Expect(got.Steps).To(HaveLen(4)) + Expect(got.Steps[3].Name).To(Equal("image-processing")) + Expect(got.Steps[3].Command[0]).To(Equal("/ko-app/image-processing")) + Expect(got.Steps[3].Args).To(BeEquivalentTo([]string{ + "--annotation", + "org.opencontainers.image.url=https://my-company.com/images", + "--label", + "maintainer=team@my-company.com", "--image", "$(params.shp-output-image)", + "--insecure=$(params.shp-output-insecure)", "--result-file-image-digest", "$(results.shp-image-digest.path)", - "result-file-image-size", + "--result-file-image-size", "$(results.shp-image-size.path)", - "--annotation", - "org.opencontainers.image.url=https://my-company.com/images", - "--label", - "maintainer=team@my-company.com", })) }) @@ -171,18 +175,14 @@ var _ = Describe("GenerateTaskrun", func() { build, err = ctl.LoadBuildYAML([]byte(test.BuildahBuildWithMultipleAnnotationAndLabel)) Expect(err).To(BeNil()) - got, err = resources.GenerateTaskSpec(config.NewDefaultConfig(), build, buildRun, buildStrategy.Spec.BuildSteps, []buildv1alpha1.Parameter{}, buildStrategy.GetVolumes()) - Expect(err).To(BeNil()) + taskRun, err := resources.GenerateTaskRun(config.NewDefaultConfig(), build, buildRun, "", buildStrategy) + Expect(err).ToNot(HaveOccurred()) + got = taskRun.Spec.TaskSpec - Expect(got.Steps[3].Name).To(Equal("mutate-image")) - Expect(got.Steps[3].Command[0]).To(Equal("/ko-app/mutate-image")) - Expect(got.Steps[3].Args).Should(ConsistOf([]string{ - "--image", - "$(params.shp-output-image)", - "--result-file-image-digest", - "$(results.shp-image-digest.path)", - "result-file-image-size", - "$(results.shp-image-size.path)", + Expect(got.Steps[3].Name).To(Equal("image-processing")) + Expect(got.Steps[3].Command[0]).To(Equal("/ko-app/image-processing")) + + expected := []string{ "--annotation", "org.opencontainers.image.source=https://github.com/org/repo", "--annotation", @@ -191,7 +191,36 @@ var _ = Describe("GenerateTaskrun", func() { "description=This is my cool image", "--label", "maintainer=team@my-company.com", - })) + "--image", + "$(params.shp-output-image)", + "--insecure=$(params.shp-output-insecure)", + "--result-file-image-digest", + "$(results.shp-image-digest.path)", + "--result-file-image-size", + "$(results.shp-image-size.path)", + } + + Expect(got.Steps[3].Args).To(HaveLen(len(expected))) + + // there is no way to say which annotation comes first + + Expect(got.Steps[3].Args[1]).To(BeElementOf(expected[1], expected[3])) + Expect(got.Steps[3].Args[3]).To(BeElementOf(expected[1], expected[3])) + Expect(got.Steps[3].Args[1]).ToNot(Equal(got.Steps[3].Args[3])) + + expected[1] = got.Steps[3].Args[1] + expected[3] = got.Steps[3].Args[3] + + // same for labels + + Expect(got.Steps[3].Args[5]).To(BeElementOf(expected[5], expected[7])) + Expect(got.Steps[3].Args[7]).To(BeElementOf(expected[5], expected[7])) + Expect(got.Steps[3].Args[5]).ToNot(Equal(got.Steps[3].Args[7])) + + expected[5] = got.Steps[3].Args[5] + expected[7] = got.Steps[3].Args[7] + + Expect(got.Steps[3].Args).To(BeEquivalentTo(expected)) }) }) @@ -307,7 +336,7 @@ var _ = Describe("GenerateTaskrun", func() { It("should contain a step to mutate the image with labels and annotations merged from build and buildrun", func() { Expect(got.Steps[3].Name).To(Equal("mutate-image")) - Expect(got.Steps[3].Command[0]).To(Equal("/ko-app/mutate-image")) + Expect(got.Steps[3].Command[0]).To(Equal("/ko-app/image-processing")) Expect(got.Steps[3].Args).To(Equal([]string{ "--image", "$(params.shp-output-image)", @@ -348,7 +377,7 @@ var _ = Describe("GenerateTaskrun", func() { It("should contain a step to mutate the image with labels and annotations merged from build and buildrun", func() { Expect(got.Steps[3].Name).To(Equal("mutate-image")) - Expect(got.Steps[3].Command[0]).To(Equal("/ko-app/mutate-image")) + Expect(got.Steps[3].Command[0]).To(Equal("/ko-app/image-processing")) Expect(got.Steps[3].Args).To(Equal([]string{ "--image", "$(params.shp-output-image)", @@ -487,6 +516,7 @@ var _ = Describe("GenerateTaskrun", func() { paramSourceRootFound := false paramSourceContextFound := false paramOutputImageFound := false + paramOutputInsecureFound := false // legacy params paramBuilderImageFound := false @@ -507,6 +537,10 @@ var _ = Describe("GenerateTaskrun", func() { paramOutputImageFound = true Expect(param.Value.StringVal).To(Equal(outputPath)) + case "shp-output-insecure": + paramOutputInsecureFound = true + Expect(param.Value.StringVal).To(Equal("false")) + case "BUILDER_IMAGE": paramBuilderImageFound = true Expect(param.Value.StringVal).To(Equal(builderImage.Image)) @@ -527,6 +561,7 @@ var _ = Describe("GenerateTaskrun", func() { Expect(paramSourceRootFound).To(BeTrue()) Expect(paramSourceContextFound).To(BeTrue()) Expect(paramOutputImageFound).To(BeTrue()) + Expect(paramOutputInsecureFound).To(BeTrue()) Expect(paramBuilderImageFound).To(BeTrue()) Expect(paramDockerfileFound).To(BeTrue()) diff --git a/samples/build/build_buildkit_cr.yaml b/samples/build/build_buildkit_cr.yaml index c77acef782..32f03169f1 100644 --- a/samples/build/build_buildkit_cr.yaml +++ b/samples/build/build_buildkit_cr.yaml @@ -4,11 +4,16 @@ kind: Build metadata: name: buildkit-build annotations: - build.build.dev/build-run-deletion: "true" + build.shipwright.io/build-run-deletion: "true" spec: source: url: https://github.com/shipwright-io/sample-go contextDir: docker-build + paramValues: + - name: platforms + values: + - value: linux/amd64 + - value: linux/arm64 strategy: name: buildkit kind: ClusterBuildStrategy diff --git a/samples/build/build_ko_cr.yaml b/samples/build/build_ko_cr.yaml index 20cdcab4c9..7b495d7cfd 100644 --- a/samples/build/build_ko_cr.yaml +++ b/samples/build/build_ko_cr.yaml @@ -4,7 +4,7 @@ kind: Build metadata: name: ko-build annotations: - build.build.dev/build-run-deletion: "false" + build.shipwright.io/build-run-deletion: "false" spec: paramValues: - name: go-flags diff --git a/samples/buildstrategy/buildkit/buildstrategy_buildkit_cr.yaml b/samples/buildstrategy/buildkit/buildstrategy_buildkit_cr.yaml index b1ceb70ae3..a22579d34e 100644 --- a/samples/buildstrategy/buildkit/buildstrategy_buildkit_cr.yaml +++ b/samples/buildstrategy/buildkit/buildstrategy_buildkit_cr.yaml @@ -19,10 +19,6 @@ spec: description: "Configure BuildKit's cache usage. Allowed values are 'disabled' and 'registry'. The default is 'registry'." type: string default: registry - - name: insecure-registry - type: string - description: "Enables the push to an insecure registry" - default: "false" - name: platforms description: "Build the image for different platforms. By default, the image is built for the platform used by the FROM image. If that is present for multiple platforms, then it is built for the environment's platform." type: array @@ -70,10 +66,10 @@ spec: value: $(params.shp-source-context) - name: PARAM_DOCKERFILE value: $(params.DOCKERFILE) + - name: PARAM_OUTPUT_DIRECTORY + value: $(params.shp-output-directory) - name: PARAM_OUTPUT_IMAGE value: $(params.shp-output-image) - - name: PARAM_INSECURE_REGISTRY - value: $(params.insecure-registry) - name: PARAM_CACHE value: $(params.cache) command: @@ -115,7 +111,7 @@ spec: echo "--opt=filename=\"${DOCKERFILE_NAME}\" \\" >> /tmp/run.sh echo "--local=context=\"${PARAM_SOURCE_CONTEXT}\" \\" >> /tmp/run.sh echo "--local=dockerfile=\"${DOCKERFILE_DIR}\" \\" >> /tmp/run.sh - echo "--output=type=image,name=\"${PARAM_OUTPUT_IMAGE}\",push=true,registry.insecure=\"${PARAM_INSECURE_REGISTRY}\" \\" >> /tmp/run.sh + echo "--output=type=oci \\" >> /tmp/run.sh if [ "${PARAM_CACHE}" == "registry" ]; then echo "--export-cache=type=inline \\" >> /tmp/run.sh echo "--import-cache=type=registry,ref=\"${PARAM_OUTPUT_IMAGE}\" \\" >> /tmp/run.sh @@ -172,13 +168,10 @@ spec: echo "--opt=\"platform=${platforms}\" \\" >> /tmp/run.sh fi - echo "--metadata-file /tmp/image-metadata.json" >> /tmp/run.sh + echo "--metadata-file /tmp/image-metadata.json | tar -C \"${PARAM_OUTPUT_DIRECTORY}\" -xf -" >> /tmp/run.sh chmod +x /tmp/run.sh /tmp/run.sh - - # Store the image digest - grep containerimage.digest /tmp/image-metadata.json | sed -E 's/.*containerimage.digest":\s*"([^"]*).*/\1/' | tr -d '\n' > '$(results.shp-image-digest.path)' # That's the separator between the shell script and its args - -- - --build-args diff --git a/samples/buildstrategy/kaniko/buildstrategy_kaniko-trivy_cr.yaml b/samples/buildstrategy/kaniko/buildstrategy_kaniko-trivy_cr.yaml index cb2d9c1d6e..50c1a652fc 100644 --- a/samples/buildstrategy/kaniko/buildstrategy_kaniko-trivy_cr.yaml +++ b/samples/buildstrategy/kaniko/buildstrategy_kaniko-trivy_cr.yaml @@ -37,20 +37,17 @@ spec: command: - /kaniko/executor args: - - --skip-tls-verify=true - - --dockerfile=$(build.dockerfile) - - --context=$(params.shp-source-context) - - --destination=$(params.shp-output-image) - - --oci-layout-path=/kaniko/oci-image-layout - - --snapshotMode=redo + - --dockerfile + - $(build.dockerfile) + - --context + - $(params.shp-source-context) + - --destination + - $(params.shp-output-image) + - --snapshotMode + - redo - --no-push - --tarPath - - /kaniko/tar-image/image.tar - volumeMounts: - - name: layout - mountPath: /kaniko/oci-image-layout - - name: tar - mountPath: /kaniko/tar-image + - $(params.shp-output-directory)/image.tar resources: limits: cpu: 500m @@ -60,9 +57,6 @@ spec: memory: 65Mi - name: trivy-scan image: docker.io/aquasec/trivy:0.25.3 - volumeMounts: - - mountPath: /image/ - name: tar command: - trivy args: @@ -70,7 +64,7 @@ spec: - --exit-code=1 - --severity=CRITICAL - --input - - /image/image.tar + - $(params.shp-output-directory)/image.tar env: - name: HOME value: /tekton/home @@ -81,43 +75,3 @@ spec: requests: cpu: 250m memory: 65Mi - - name: crane-push - image: gcr.io/go-containerregistry/crane:v0.8.0 - securityContext: - runAsUser: 0 - volumeMounts: - - mountPath: /image/ - name: tar - command: - - crane - args: - - push - - /image/image.tar - - $(params.shp-output-image) - env: - - name: HOME - value: /tekton/home - - name: results - image: registry.access.redhat.com/ubi8/ubi-minimal - command: - - /bin/bash - args: - - -c - - | - set -euo pipefail - - # Store the image digest - grep digest /kaniko/oci-image-layout/index.json | sed -E 's/.*sha256([^"]*).*/sha256\1/' | tr -d '\n' > "$(results.shp-image-digest.path)" - - # Store the image size - du -b -c /kaniko/oci-image-layout/blobs/sha256/* | tail -1 | sed 's/\s*total//' | tr -d '\n' > "$(results.shp-image-size.path)" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - volumeMounts: - - name: layout - mountPath: /kaniko/oci-image-layout diff --git a/samples/buildstrategy/kaniko/buildstrategy_kaniko_cr.yaml b/samples/buildstrategy/kaniko/buildstrategy_kaniko_cr.yaml index dac47b99c7..fe4b81fb3e 100644 --- a/samples/buildstrategy/kaniko/buildstrategy_kaniko_cr.yaml +++ b/samples/buildstrategy/kaniko/buildstrategy_kaniko_cr.yaml @@ -34,13 +34,17 @@ spec: command: - /kaniko/executor args: - - --skip-tls-verify=true - - --dockerfile=$(build.dockerfile) - - --context=$(params.shp-source-context) - - --destination=$(params.shp-output-image) - - --oci-layout-path=/kaniko/oci-image-layout - - --snapshotMode=redo - - --push-retry=3 + - --dockerfile + - $(build.dockerfile) + - --context + - $(params.shp-source-context) + - --destination + - $(params.shp-output-image) + - --snapshotMode + - redo + - --no-push + - --tarPath + - $(params.shp-output-directory)/image.tar resources: limits: cpu: 500m @@ -48,31 +52,3 @@ spec: requests: cpu: 250m memory: 65Mi - volumeMounts: - - name: layout - mountPath: /kaniko/oci-image-layout - - name: results - image: registry.access.redhat.com/ubi8/ubi-minimal - command: - - /bin/bash - args: - - -c - - | - set -euo pipefail - - # Store the image digest - grep digest /kaniko/oci-image-layout/index.json | sed -E 's/.*sha256([^"]*).*/sha256\1/' | tr -d '\n' > "$(results.shp-image-digest.path)" - - # Store the image size - du -b -c /kaniko/oci-image-layout/blobs/sha256/* | tail -1 | sed 's/\s*total//' | tr -d '\n' > "$(results.shp-image-size.path)" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - volumeMounts: - - name: layout - mountPath: /kaniko/oci-image-layout - diff --git a/samples/buildstrategy/ko/buildstrategy_ko_cr.yaml b/samples/buildstrategy/ko/buildstrategy_ko_cr.yaml index d26a12c7f5..276aca826c 100644 --- a/samples/buildstrategy/ko/buildstrategy_ko_cr.yaml +++ b/samples/buildstrategy/ko/buildstrategy_ko_cr.yaml @@ -56,8 +56,8 @@ spec: value: /tekton/home - name: GOFLAGS value: $(params.go-flags) - - name: PARAM_OUTPUT_IMAGE - value: $(params.shp-output-image) + - name: PARAM_OUTPUT_DIRECTORY + value: $(params.shp-output-directory) - name: PARAM_SOURCE_CONTEXT value: $(params.shp-source-context) - name: PARAM_TARGET_PLATFORM @@ -73,29 +73,6 @@ spec: - | set -euo pipefail - # Parse image URL to extract repository and tag, must work with - # - a URL without a tag and a port: registry/image - # - a URL without a tag but a port: registry:port/image - # - a URL with a tag but without a port: registry/image:tag - # - a URL with both a tag and a port: registry:port/image:tag - - REPO= - TAG= - - IFS=':' read -ra PARTS <<< "${PARAM_OUTPUT_IMAGE}" - for PART in "${PARTS[@]}"; do - if [ "${REPO}" == "" ]; then - REPO="${PART}" - elif [[ "${PART}" == *"/"* ]]; then - REPO="${REPO}:${PART}" - elif [ "${TAG}" == "" ]; then - TAG="${PART}" - else - REPO="${REPO}:${TAG}" - TAG="${PART}" - fi - done - # Determine the ko version KO_VERSION="${PARAM_KO_VERSION}" if [ "${KO_VERSION}" == "latest" ]; then @@ -127,21 +104,10 @@ spec: # Run ko export GOROOT="$(go env GOROOT)" - export KO_DOCKER_REPO="${REPO}" pushd "${PARAM_SOURCE_CONTEXT}" > /dev/null - if [ "${TAG}" == "" ]; then - /tmp/ko publish "${PARAM_PACKAGE_DIRECTORY}" --bare --oci-layout-path=/tmp/layout --platform="${PLATFORM}" - else - /tmp/ko publish "${PARAM_PACKAGE_DIRECTORY}" --bare --oci-layout-path=/tmp/layout --platform="${PLATFORM}" --tags="${TAG}" - fi + /tmp/ko publish "${PARAM_PACKAGE_DIRECTORY}" --oci-layout-path="${PARAM_OUTPUT_DIRECTORY}" --push=false popd > /dev/null - - # Store the image digest - grep digest /tmp/layout/index.json | sed -E 's/.*sha256([^"]*).*/sha256\1/' | tr -d '\n' > '$(results.shp-image-digest.path)' - - # Store the image size - du -b -c /tmp/layout/blobs/sha256/* | tail -1 | sed 's/\s*total//' | tr -d '\n' > '$(results.shp-image-size.path)' resources: limits: cpu: 500m diff --git a/samples/buildstrategy/source-to-image/buildstrategy_source-to-image_cr.yaml b/samples/buildstrategy/source-to-image/buildstrategy_source-to-image_cr.yaml index 4bd5882d71..a54c53abcf 100644 --- a/samples/buildstrategy/source-to-image/buildstrategy_source-to-image_cr.yaml +++ b/samples/buildstrategy/source-to-image/buildstrategy_source-to-image_cr.yaml @@ -23,16 +23,22 @@ spec: - mountPath: /gen-source name: gen-source workingDir: $(params.shp-source-root) - - args: - - '--skip-tls-verify=true' - - '--dockerfile=/gen-source/Dockerfile.gen' - - '--context=/gen-source' - - '--destination=$(params.shp-output-image)' - - '--oci-layout-path=/kaniko/oci-image-layout' - - '--snapshotMode=redo' - - '--push-retry=3' + - name: build-and-push + image: gcr.io/kaniko-project/executor:v1.8.1 command: - /kaniko/executor + args: + - --dockerfile + - /gen-source/Dockerfile.gen + - --context + - /gen-source + - --destination + - $(params.shp-output-image) + - --snapshotMode + - redo + - --no-push + - --tarPath + - $(params.shp-output-directory)/image.tar env: - name: DOCKER_CONFIG value: /tekton/home/.docker @@ -42,8 +48,6 @@ spec: value: NOT_SET - name: AWS_SECRET_KEY value: NOT_SET - image: gcr.io/kaniko-project/executor:v1.8.1 - name: build-and-push securityContext: runAsUser: 0 allowPrivilegeEscalation: false @@ -59,30 +63,4 @@ spec: volumeMounts: - mountPath: /gen-source name: gen-source - - name: layout - mountPath: /kaniko/oci-image-layout workingDir: /gen-source - - name: results - image: registry.access.redhat.com/ubi8/ubi-minimal - command: - - /bin/bash - args: - - -c - - | - set -euo pipefail - - # Store the image digest - grep digest /kaniko/oci-image-layout/index.json | sed -E 's/.*sha256([^"]*).*/sha256\1/' | tr -d '\n' > "$(results.shp-image-digest.path)" - - # Store the image size - du -b -c /kaniko/oci-image-layout/blobs/sha256/* | tail -1 | sed 's/\s*total//' | tr -d '\n' > "$(results.shp-image-size.path)" - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi - volumeMounts: - - name: layout - mountPath: /kaniko/oci-image-layout diff --git a/test/build_samples.go b/test/build_samples.go index 120da8a822..9351f4adba 100644 --- a/test/build_samples.go +++ b/test/build_samples.go @@ -164,6 +164,18 @@ spec: image: image-registry.openshift-image-registry.svc:5000/example/buildpacks-app ` +// BuildBSMinimalNoSource defines a Build with a BuildStrategy without sources +const BuildBSMinimalNoSource = ` +apiVersion: shipwright.io/v1alpha1 +kind: Build +spec: + source: {} + strategy: + kind: BuildStrategy + output: + image: image-registry.openshift-image-registry.svc:5000/example/buildpacks-app +` + // BuildCBSMinimal defines a Build with a // ClusterBuildStrategy const BuildCBSMinimal = ` diff --git a/test/buildstrategy_samples.go b/test/buildstrategy_samples.go index 8283ac7a04..81f234a377 100644 --- a/test/buildstrategy_samples.go +++ b/test/buildstrategy_samples.go @@ -349,3 +349,21 @@ spec: - -- - $(params.args[*]) ` + +// BuildStrategyWithoutPush is a strategy that writes an image tarball and pushes nothing +const BuildStrategyWithoutPush = ` +apiVersion: shipwright.io/v1alpha1 +kind: BuildStrategy +metadata: + name: strategy-without-push +spec: + buildSteps: + - name: store-tarball + image: gcr.io/go-containerregistry/crane:v0.8.0 + command: + - crane + args: + - export + - busybox + - $(params.shp-output-directory)/image.tar +` diff --git a/test/catalog.go b/test/catalog.go index 32d48e2765..f6eb568314 100644 --- a/test/catalog.go +++ b/test/catalog.go @@ -6,6 +6,7 @@ package test import ( "context" + "fmt" "strconv" "time" @@ -62,6 +63,20 @@ func (c *Catalog) SecretWithStringData(name string, ns string, stringData map[st } } +// SecretWithDockerConfigJson creates a secret of type dockerconfigjson +func (c *Catalog) SecretWithDockerConfigJson(name string, ns string, host string, username string, password string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Type: corev1.SecretTypeDockerConfigJson, + StringData: map[string]string{ + ".dockerconfigjson": fmt.Sprintf("{\"auths\":{%q:{\"username\":%q,\"password\":%q}}}", host, username, password), + }, + } +} + // ConfigMapWithData creates a ConfigMap with data func (c *Catalog) ConfigMapWithData(name string, ns string, data map[string]string) *corev1.ConfigMap { return &corev1.ConfigMap{ diff --git a/test/data/build_buildkit_cr_insecure_registry.yaml b/test/data/build_buildkit_cr_insecure_registry.yaml index 8a35939d72..3dc560fbef 100644 --- a/test/data/build_buildkit_cr_insecure_registry.yaml +++ b/test/data/build_buildkit_cr_insecure_registry.yaml @@ -4,7 +4,7 @@ kind: Build metadata: name: buildkit-build annotations: - build.build.dev/build-run-deletion: "true" + build.shipwright.io/build-run-deletion: "true" spec: source: url: https://github.com/shipwright-io/sample-go diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/014fae513d1b03a05ffe75de2db80b0d5c98fd4c8c215583560ba191523e74c0 b/test/data/images/multi-platform-image-in-oci/blobs/sha256/014fae513d1b03a05ffe75de2db80b0d5c98fd4c8c215583560ba191523e74c0 new file mode 100644 index 0000000000..2ba4f5c6da --- /dev/null +++ b/test/data/images/multi-platform-image-in-oci/blobs/sha256/014fae513d1b03a05ffe75de2db80b0d5c98fd4c8c215583560ba191523e74c0 @@ -0,0 +1 @@ +{"architecture":"arm64","author":"github.com/google/ko","created":"1970-01-01T00:00:00Z","history":[{"author":"Bazel","created":"1970-01-01T00:00:00Z","created_by":"bazel build ..."},{"author":"ko","created":"0001-01-01T00:00:00Z","created_by":"ko build ko://shipwright.io/sample-go/v1","comment":"kodata contents, at $KO_DATA_PATH"},{"author":"ko","created":"0001-01-01T00:00:00Z","created_by":"ko build ko://shipwright.io/sample-go/v1","comment":"go build output, at /ko-app/v1"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:798afb9dcee7e7c858b6f109d8bb3ea6d10081493703a6b77b46d388c38aa8f7","sha256:ffe56a1c5f3878e9b5f803842adb9e2ce81584b6bd027e8599582aefe14a975b","sha256:8fa67ebda82ed3e856bda1dbccb18931b310c256590d45df6a484fd09cd113e3"]},"config":{"Entrypoint":["/ko-app/v1"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/ko-app","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt","KO_DATA_PATH=/var/run/ko"],"User":"65532","WorkingDir":"/home/nonroot"}} \ No newline at end of file diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542 b/test/data/images/multi-platform-image-in-oci/blobs/sha256/250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542 new file mode 100644 index 0000000000..d507ef075e Binary files /dev/null and b/test/data/images/multi-platform-image-in-oci/blobs/sha256/250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542 differ diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/36698cfa5275e0bda70b0f864b7b174e0758ca122d8c6a54fb329d70082c73f8 b/test/data/images/multi-platform-image-in-oci/blobs/sha256/36698cfa5275e0bda70b0f864b7b174e0758ca122d8c6a54fb329d70082c73f8 new file mode 100644 index 0000000000..64c520f162 Binary files /dev/null and b/test/data/images/multi-platform-image-in-oci/blobs/sha256/36698cfa5275e0bda70b0f864b7b174e0758ca122d8c6a54fb329d70082c73f8 differ diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/5026b67ce0249802c1939cc7d5935b4112b0123becd46243d476c019688f8994 b/test/data/images/multi-platform-image-in-oci/blobs/sha256/5026b67ce0249802c1939cc7d5935b4112b0123becd46243d476c019688f8994 new file mode 100644 index 0000000000..67c4bd038e --- /dev/null +++ b/test/data/images/multi-platform-image-in-oci/blobs/sha256/5026b67ce0249802c1939cc7d5935b4112b0123becd46243d476c019688f8994 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1003,"digest":"sha256:014fae513d1b03a05ffe75de2db80b0d5c98fd4c8c215583560ba191523e74c0"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":803973,"digest":"sha256:b0b160e41cf35d4a469a1209cfd553534b248053060de060cbbc4a5d912ad5f0"},{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":127,"digest":"sha256:250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542"},{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":3296219,"digest":"sha256:96a4fd5caa57fd6830204ceb278fe3bb76a65f60fc7ae98212b9781adece8c7a"}]} \ No newline at end of file diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/56275f9c3a8710f47e0fb46b4e13fefbacea358c165e8049b871fa666d92d8b0 b/test/data/images/multi-platform-image-in-oci/blobs/sha256/56275f9c3a8710f47e0fb46b4e13fefbacea358c165e8049b871fa666d92d8b0 new file mode 100644 index 0000000000..ee09ed151b Binary files /dev/null and b/test/data/images/multi-platform-image-in-oci/blobs/sha256/56275f9c3a8710f47e0fb46b4e13fefbacea358c165e8049b871fa666d92d8b0 differ diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/9562139ffa8d8973e70c850cb1befdc5303c8828b0137c75644d07b7fea26566 b/test/data/images/multi-platform-image-in-oci/blobs/sha256/9562139ffa8d8973e70c850cb1befdc5303c8828b0137c75644d07b7fea26566 new file mode 100644 index 0000000000..24e26ac8a6 --- /dev/null +++ b/test/data/images/multi-platform-image-in-oci/blobs/sha256/9562139ffa8d8973e70c850cb1befdc5303c8828b0137c75644d07b7fea26566 @@ -0,0 +1 @@ +{"architecture":"amd64","author":"github.com/google/ko","created":"1970-01-01T00:00:00Z","history":[{"author":"Bazel","created":"1970-01-01T00:00:00Z","created_by":"bazel build ..."},{"author":"ko","created":"0001-01-01T00:00:00Z","created_by":"ko build ko://shipwright.io/sample-go/v1","comment":"kodata contents, at $KO_DATA_PATH"},{"author":"ko","created":"0001-01-01T00:00:00Z","created_by":"ko build ko://shipwright.io/sample-go/v1","comment":"go build output, at /ko-app/v1"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:0b031aac65698c8794dc6bc317a45589e07bc2db1421178f30a2c7f69a4a2cf5","sha256:ffe56a1c5f3878e9b5f803842adb9e2ce81584b6bd027e8599582aefe14a975b","sha256:83105d0b7c61b1618835f22ee8913312046e62ba531af2b6c238e273cd8dce50"]},"config":{"Entrypoint":["/ko-app/v1"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/ko-app","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt","KO_DATA_PATH=/var/run/ko"],"User":"65532","WorkingDir":"/home/nonroot"}} \ No newline at end of file diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/96a4fd5caa57fd6830204ceb278fe3bb76a65f60fc7ae98212b9781adece8c7a b/test/data/images/multi-platform-image-in-oci/blobs/sha256/96a4fd5caa57fd6830204ceb278fe3bb76a65f60fc7ae98212b9781adece8c7a new file mode 100644 index 0000000000..cc84615462 Binary files /dev/null and b/test/data/images/multi-platform-image-in-oci/blobs/sha256/96a4fd5caa57fd6830204ceb278fe3bb76a65f60fc7ae98212b9781adece8c7a differ diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/b0b160e41cf35d4a469a1209cfd553534b248053060de060cbbc4a5d912ad5f0 b/test/data/images/multi-platform-image-in-oci/blobs/sha256/b0b160e41cf35d4a469a1209cfd553534b248053060de060cbbc4a5d912ad5f0 new file mode 100644 index 0000000000..3112a2f53e Binary files /dev/null and b/test/data/images/multi-platform-image-in-oci/blobs/sha256/b0b160e41cf35d4a469a1209cfd553534b248053060de060cbbc4a5d912ad5f0 differ diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/b59bc7a8beb1f4e8caaae427c84e50f16bab46a9cbafa5136d2ebb000de4ef3a b/test/data/images/multi-platform-image-in-oci/blobs/sha256/b59bc7a8beb1f4e8caaae427c84e50f16bab46a9cbafa5136d2ebb000de4ef3a new file mode 100644 index 0000000000..afc2d6ee3e --- /dev/null +++ b/test/data/images/multi-platform-image-in-oci/blobs/sha256/b59bc7a8beb1f4e8caaae427c84e50f16bab46a9cbafa5136d2ebb000de4ef3a @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1003,"digest":"sha256:9562139ffa8d8973e70c850cb1befdc5303c8828b0137c75644d07b7fea26566"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":803973,"digest":"sha256:36698cfa5275e0bda70b0f864b7b174e0758ca122d8c6a54fb329d70082c73f8"},{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":127,"digest":"sha256:250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542"},{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":3563638,"digest":"sha256:56275f9c3a8710f47e0fb46b4e13fefbacea358c165e8049b871fa666d92d8b0"}]} \ No newline at end of file diff --git a/test/data/images/multi-platform-image-in-oci/blobs/sha256/ebfdd6cac823c93f9929bf5c951c7b8a8fcb4c6881d4fd6f947c8a51ca0df7ad b/test/data/images/multi-platform-image-in-oci/blobs/sha256/ebfdd6cac823c93f9929bf5c951c7b8a8fcb4c6881d4fd6f947c8a51ca0df7ad new file mode 100755 index 0000000000..06ab398138 --- /dev/null +++ b/test/data/images/multi-platform-image-in-oci/blobs/sha256/ebfdd6cac823c93f9929bf5c951c7b8a8fcb4c6881d4fd6f947c8a51ca0df7ad @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","size":751,"digest":"sha256:b59bc7a8beb1f4e8caaae427c84e50f16bab46a9cbafa5136d2ebb000de4ef3a","platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","size":751,"digest":"sha256:5026b67ce0249802c1939cc7d5935b4112b0123becd46243d476c019688f8994","platform":{"architecture":"arm64","os":"linux"}}]} \ No newline at end of file diff --git a/test/data/images/multi-platform-image-in-oci/index.json b/test/data/images/multi-platform-image-in-oci/index.json new file mode 100755 index 0000000000..ce3b7d8a25 --- /dev/null +++ b/test/data/images/multi-platform-image-in-oci/index.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "size": 529, + "digest": "sha256:ebfdd6cac823c93f9929bf5c951c7b8a8fcb4c6881d4fd6f947c8a51ca0df7ad", + "annotations": { + "dev.ggcr.image.name": "registry.saschaschwarze.de/sascha-shipwright/sample-go-multiplatform" + } + } + ] +} \ No newline at end of file diff --git a/test/data/images/multi-platform-image-in-oci/oci-layout b/test/data/images/multi-platform-image-in-oci/oci-layout new file mode 100755 index 0000000000..224a869812 --- /dev/null +++ b/test/data/images/multi-platform-image-in-oci/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} \ No newline at end of file diff --git a/test/data/images/single-image-in-oci/blobs/sha256/2e9271bb648177ec67fb471ed620ade54d1656e90452364f484addedc76791c6 b/test/data/images/single-image-in-oci/blobs/sha256/2e9271bb648177ec67fb471ed620ade54d1656e90452364f484addedc76791c6 new file mode 100644 index 0000000000..30360b7219 Binary files /dev/null and b/test/data/images/single-image-in-oci/blobs/sha256/2e9271bb648177ec67fb471ed620ade54d1656e90452364f484addedc76791c6 differ diff --git a/test/data/images/single-image-in-oci/blobs/sha256/94236105831be2a795cee01edd16e7711ca43a12cdc4ffd602227eae50b544d9 b/test/data/images/single-image-in-oci/blobs/sha256/94236105831be2a795cee01edd16e7711ca43a12cdc4ffd602227eae50b544d9 new file mode 100644 index 0000000000..5a3f27c9c1 --- /dev/null +++ b/test/data/images/single-image-in-oci/blobs/sha256/94236105831be2a795cee01edd16e7711ca43a12cdc4ffd602227eae50b544d9 @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 440, + "digest": "sha256:a96e72995e8229cf5f502f4bc678e2642e5a24a5cfc2251458dcbb216ff60a80" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 5644, + "digest": "sha256:2e9271bb648177ec67fb471ed620ade54d1656e90452364f484addedc76791c6" + } + ] +} \ No newline at end of file diff --git a/test/data/images/single-image-in-oci/blobs/sha256/a96e72995e8229cf5f502f4bc678e2642e5a24a5cfc2251458dcbb216ff60a80 b/test/data/images/single-image-in-oci/blobs/sha256/a96e72995e8229cf5f502f4bc678e2642e5a24a5cfc2251458dcbb216ff60a80 new file mode 100644 index 0000000000..56a37ebd38 --- /dev/null +++ b/test/data/images/single-image-in-oci/blobs/sha256/a96e72995e8229cf5f502f4bc678e2642e5a24a5cfc2251458dcbb216ff60a80 @@ -0,0 +1 @@ +{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"WorkingDir":"/","OnBuild":null},"created":"2022-02-24T11:35:45.009510825Z","history":[{"created":"2022-02-24T11:35:45.009510825Z","created_by":"COPY . . # buildkit","comment":"buildkit.dockerfile.v0"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:21bc292e6606a3b6627d443549c811fd074e2dbf8615a21c02888b90e0c68c86"]}} \ No newline at end of file diff --git a/test/data/images/single-image-in-oci/index.json b/test/data/images/single-image-in-oci/index.json new file mode 100755 index 0000000000..1c3462dc90 --- /dev/null +++ b/test/data/images/single-image-in-oci/index.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 524, + "digest": "sha256:94236105831be2a795cee01edd16e7711ca43a12cdc4ffd602227eae50b544d9", + "annotations": { + "dev.ggcr.image.name": "ghcr.io/shipwright-io/sample-go/source-bundle:latest" + } + } + ] +} \ No newline at end of file diff --git a/test/data/images/single-image-in-oci/oci-layout b/test/data/images/single-image-in-oci/oci-layout new file mode 100755 index 0000000000..224a869812 --- /dev/null +++ b/test/data/images/single-image-in-oci/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} \ No newline at end of file diff --git a/test/data/images/single-image/image.tar b/test/data/images/single-image/image.tar new file mode 100644 index 0000000000..813fb5f074 Binary files /dev/null and b/test/data/images/single-image/image.tar differ diff --git a/test/e2e/common_suite_test.go b/test/e2e/common_suite_test.go index c6ffb6f314..14a166fc49 100644 --- a/test/e2e/common_suite_test.go +++ b/test/e2e/common_suite_test.go @@ -215,6 +215,12 @@ func (b *buildPrototype) OutputImageCredentials(name string) *buildPrototype { return b } +func (b *buildPrototype) OutputImageInsecure(insecure bool) *buildPrototype { + b.build.Spec.Output.Insecure = &insecure + + return b +} + func (b buildPrototype) Create() (build *buildv1alpha1.Build, err error) { ctx := context.Background() diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index ebd6b6b1d4..8d6e4467ea 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "os" + "strconv" "strings" "time" @@ -21,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/utils/pointer" buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" "github.com/shipwright-io/build/pkg/reconciler/buildrun/resources" @@ -71,7 +73,7 @@ func createBuild(testBuild *utils.TestBuild, identifier string, filePath string) } // amendOutputImage amend container image URL based on informed image repository. -func amendOutputImage(b *buildv1alpha1.Build, imageRepo string) { +func amendOutputImage(b *buildv1alpha1.Build, imageRepo string, insecure bool) { if imageRepo == "" { return } @@ -81,6 +83,9 @@ func amendOutputImage(b *buildv1alpha1.Build, imageRepo string) { imageURL := fmt.Sprintf("%s:%s", imageRepo, imageTag) b.Spec.Output.Image = imageURL + + b.Spec.Output.Insecure = pointer.Bool(insecure) + Logf("Amended object: name='%s', image-url='%s'", b.Name, imageURL) } @@ -119,7 +124,15 @@ func amendBuild(identifier string, b *buildv1alpha1.Build) { amendSourceURL(b, os.Getenv(EnvVarSourceURLGitlab)) } - amendOutputImage(b, os.Getenv(EnvVarImageRepo)) + insecure := false + value, found := os.LookupEnv(EnvVarImageRepoInsecure) + if found { + var err error + insecure, err = strconv.ParseBool(value) + Expect(err).ToNot(HaveOccurred()) + } + + amendOutputImage(b, os.Getenv(EnvVarImageRepo), insecure) amendOutputCredentials(b, os.Getenv(EnvVarImageRepoSecret)) } diff --git a/test/e2e/e2e_bundle_test.go b/test/e2e/e2e_bundle_test.go index 7eafeeb6e3..3f158f4b09 100644 --- a/test/e2e/e2e_bundle_test.go +++ b/test/e2e/e2e_bundle_test.go @@ -8,6 +8,7 @@ import ( "bytes" "fmt" "os" + "strconv" "strings" . "github.com/onsi/ginkgo/v2" @@ -23,6 +24,15 @@ import ( ) var _ = Describe("Test local source code (bundle) functionality", func() { + + insecure := false + value, found := os.LookupEnv(EnvVarImageRepoInsecure) + if found { + var err error + insecure, err = strconv.ParseBool(value) + Expect(err).ToNot(HaveOccurred()) + } + var ( testID string err error @@ -73,6 +83,7 @@ var _ = Describe("Test local source code (bundle) functionality", func() { Dockerfile("Dockerfile"). OutputImage(outputImage). OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). Create() Expect(err).ToNot(HaveOccurred()) @@ -97,6 +108,7 @@ var _ = Describe("Test local source code (bundle) functionality", func() { SourceContextDir("source-build"). OutputImage(outputImage). OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). Create() Expect(err).ToNot(HaveOccurred()) @@ -121,7 +133,8 @@ var _ = Describe("Test local source code (bundle) functionality", func() { SourceContextDir("docker-build"). Dockerfile("Dockerfile"). OutputImage(outputImage). - OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)) + OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure) if strings.Contains(outputImage, "cluster.local") { parts := strings.Split(outputImage, "/") @@ -238,6 +251,7 @@ var _ = Describe("Test local source code (bundle) functionality", func() { Dockerfile("Dockerfile"). OutputImage(outputImage). OutputImageCredentials(secretName). + OutputImageInsecure(insecure). Create() Expect(err).ToNot(HaveOccurred()) diff --git a/test/e2e/e2e_one_off_builds_test.go b/test/e2e/e2e_one_off_builds_test.go index aa3e33369f..1b3a65ead5 100644 --- a/test/e2e/e2e_one_off_builds_test.go +++ b/test/e2e/e2e_one_off_builds_test.go @@ -7,6 +7,7 @@ package e2e_test import ( "fmt" "os" + "strconv" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -16,6 +17,15 @@ import ( ) var _ = Describe("Using One-Off Builds", func() { + + insecure := false + value, found := os.LookupEnv(EnvVarImageRepoInsecure) + if found { + var err error + insecure, err = strconv.ParseBool(value) + Expect(err).ToNot(HaveOccurred()) + } + var ( testID string err error @@ -63,6 +73,7 @@ var _ = Describe("Using One-Off Builds", func() { SourceContextDir("source-build"). OutputImage(outputImage.String()). OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). BuildSpec()). Create() Expect(err).ToNot(HaveOccurred()) @@ -83,6 +94,7 @@ var _ = Describe("Using One-Off Builds", func() { ArrayParamValue("registries-insecure", outputImage.Context().RegistryStr()). OutputImage(outputImage.String()). OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). BuildSpec()). Create() Expect(err).ToNot(HaveOccurred()) @@ -101,6 +113,7 @@ var _ = Describe("Using One-Off Builds", func() { SourceContextDir("source-build"). OutputImage(outputImage.String()). OutputImageCredentials(os.Getenv(EnvVarImageRepoSecret)). + OutputImageInsecure(insecure). BuildSpec()). Create() Expect(err).ToNot(HaveOccurred()) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 479faa323c..d55bc1e61f 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -472,7 +472,7 @@ var _ = Describe("For a Kubernetes cluster with Tekton and build installed", fun build = createBuild( testBuild, testID, - "test/data/build_buildkit_cr_insecure_registry.yaml", + "samples/build/build_buildkit_cr.yaml", ) }) diff --git a/test/e2e/validators_test.go b/test/e2e/validators_test.go index cc837030db..d127eb6288 100644 --- a/test/e2e/validators_test.go +++ b/test/e2e/validators_test.go @@ -33,6 +33,7 @@ const ( EnvVarVerifyTektonObjects = "TEST_E2E_VERIFY_TEKTONOBJECTS" EnvVarTimeoutMultiplier = "TEST_E2E_TIMEOUT_MULTIPLIER" EnvVarImageRepo = "TEST_IMAGE_REPO" + EnvVarImageRepoInsecure = "TEST_IMAGE_REPO_INSECURE" EnvVarEnablePrivateRepos = "TEST_PRIVATE_REPO" EnvVarImageRepoSecret = "TEST_IMAGE_REPO_SECRET" EnvVarSourceRepoSecretJSON = "TEST_IMAGE_REPO_DOCKERCONFIGJSON" diff --git a/test/integration/build_to_taskruns_test.go b/test/integration/build_to_taskruns_test.go index bb82c30f53..c6380d0e28 100644 --- a/test/integration/build_to_taskruns_test.go +++ b/test/integration/build_to_taskruns_test.go @@ -11,6 +11,7 @@ import ( "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" "github.com/shipwright-io/build/test" + "github.com/shipwright-io/build/test/utils" ) var _ = Describe("Integration tests Build and TaskRun", func() { @@ -162,17 +163,18 @@ var _ = Describe("Integration tests Build and TaskRun", func() { tr, err := tb.GetTaskRunFromBuildRun(buildRunObject.Name) Expect(err).To(BeNil()) - Expect(tr.Spec.TaskSpec.Steps[3].Name).To(Equal("mutate-image")) - Expect(tr.Spec.TaskSpec.Steps[3].Command[0]).To(Equal("/ko-app/mutate-image")) + Expect(tr.Spec.TaskSpec.Steps[3].Name).To(Equal("image-processing")) + Expect(tr.Spec.TaskSpec.Steps[3].Command[0]).To(Equal("/ko-app/image-processing")) Expect(tr.Spec.TaskSpec.Steps[3].Args).To(Equal([]string{ + "--annotation", + "org.opencontainers.image.url=https://my-company.com/images", "--image", "$(params.shp-output-image)", + "--insecure=$(params.shp-output-insecure)", "--result-file-image-digest", "$(results.shp-image-digest.path)", - "result-file-image-size", + "--result-file-image-size", "$(results.shp-image-size.path)", - "--annotation", - "org.opencontainers.image.url=https://my-company.com/images", })) }) @@ -193,9 +195,7 @@ var _ = Describe("Integration tests Build and TaskRun", func() { tr, err := tb.GetTaskRunFromBuildRun(buildRunObject.Name) Expect(err).To(BeNil()) - for _, step := range tr.Spec.TaskSpec.Steps { - Expect(step.Name).ToNot(Equal("mutate-image")) - } + Expect(tr.Spec.TaskSpec.Steps).ToNot(utils.ContainNamedElement("image-processing")) }) }) }) diff --git a/test/integration/buildstrategy_to_taskruns_test.go b/test/integration/buildstrategy_to_taskruns_test.go index 09816a31b6..7b5909734e 100644 --- a/test/integration/buildstrategy_to_taskruns_test.go +++ b/test/integration/buildstrategy_to_taskruns_test.go @@ -12,6 +12,7 @@ import ( "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" "github.com/shipwright-io/build/test" + "github.com/shipwright-io/build/test/utils" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" corev1 "k8s.io/api/core/v1" ) @@ -529,4 +530,126 @@ var _ = Describe("Integration tests BuildStrategies and TaskRuns", func() { }) }) }) + + Context("buildstrategy without push operation", Label("managed-push"), func() { + + BeforeEach(func() { + // Create a Strategy with parameters + bsObject, err = tb.Catalog.LoadBuildStrategyFromBytes( + []byte(test.BuildStrategyWithoutPush), + ) + Expect(err).ToNot(HaveOccurred()) + + err = tb.CreateBuildStrategy(bsObject) + Expect(err).ToNot(HaveOccurred()) + + buildRunSample = []byte(test.MinimalBuildRun) + buildSample = []byte(test.BuildBSMinimalNoSource) + }) + + Context("build output without secret", func() { + + It("creates a TaskRun with Shipwright managed push", func() { + Expect(tb.CreateBuild(buildObject)).ToNot(HaveOccurred()) + + buildObject, err = tb.GetBuildTillValidation(buildObject.Name) + Expect(err).ToNot(HaveOccurred()) + + Expect(tb.CreateBR(buildRunObject)).ToNot(HaveOccurred()) + + buildRunObject, err = tb.GetBRTillStartTime(buildRunObject.Name) + Expect(err).ToNot(HaveOccurred()) + + taskRun, err := tb.GetTaskRunFromBuildRun(buildRunObject.Name) + Expect(err).ToNot(HaveOccurred()) + + // Verify the steps + Expect(taskRun.Spec.TaskSpec.Steps).To(HaveLen(2)) + Expect(taskRun.Spec.TaskSpec.Steps[0].Name).To(Equal("store-tarball")) + Expect(taskRun.Spec.TaskSpec.Steps[1].Name).To(Equal("image-processing")) + + // Verify the volume for the output directory + Expect(taskRun.Spec.TaskSpec.Volumes).To(utils.ContainNamedElement("shp-output-directory")) + Expect(taskRun.Spec.TaskSpec.Steps[0].VolumeMounts).To(utils.ContainNamedElement("shp-output-directory")) + Expect(taskRun.Spec.TaskSpec.Steps[1].VolumeMounts).To(utils.ContainNamedElement("shp-output-directory")) + + // Verify the param + Expect(taskRun.Spec.TaskSpec.Params).To(utils.ContainNamedElement("shp-output-directory")) + Expect(taskRun.Spec.Params).To(utils.ContainNamedElement("shp-output-directory")) + + // Verify the step args + Expect(taskRun.Spec.TaskSpec.Steps[1].Args).To(BeEquivalentTo([]string{ + "--push", + "$(params.shp-output-directory)", + "--image", + "$(params.shp-output-image)", + "--insecure=$(params.shp-output-insecure)", + "--result-file-image-digest", + "$(results.shp-image-digest.path)", + "--result-file-image-size", + "$(results.shp-image-size.path)", + })) + }) + }) + + Context("build output with secret", func() { + + BeforeEach(func() { + secret = tb.Catalog.SecretWithDockerConfigJson("registry", tb.Namespace, "registry.abc", "some-user", "some-password") + err := tb.CreateSecret(secret) + Expect(err).ToNot(HaveOccurred()) + }) + + It("creates a TaskRun with Shipwright managed push", func() { + buildObject.Spec.Output.Credentials = &corev1.LocalObjectReference{ + Name: secret.Name, + } + Expect(tb.CreateBuild(buildObject)).ToNot(HaveOccurred()) + + buildObject, err = tb.GetBuildTillValidation(buildObject.Name) + Expect(err).ToNot(HaveOccurred()) + + Expect(tb.CreateBR(buildRunObject)).ToNot(HaveOccurred()) + + buildRunObject, err = tb.GetBRTillStartTime(buildRunObject.Name) + Expect(err).ToNot(HaveOccurred()) + + taskRun, err := tb.GetTaskRunFromBuildRun(buildRunObject.Name) + Expect(err).ToNot(HaveOccurred()) + + // Verify the steps + Expect(taskRun.Spec.TaskSpec.Steps).To(HaveLen(2)) + Expect(taskRun.Spec.TaskSpec.Steps[0].Name).To(Equal("store-tarball")) + Expect(taskRun.Spec.TaskSpec.Steps[1].Name).To(Equal("image-processing")) + + // Verify the volume for the output directory + Expect(taskRun.Spec.TaskSpec.Volumes).To(utils.ContainNamedElement("shp-output-directory")) + Expect(taskRun.Spec.TaskSpec.Steps[0].VolumeMounts).To(utils.ContainNamedElement("shp-output-directory")) + Expect(taskRun.Spec.TaskSpec.Steps[1].VolumeMounts).To(utils.ContainNamedElement("shp-output-directory")) + + // Verify the volume for the output secret + Expect(taskRun.Spec.TaskSpec.Volumes).To(utils.ContainNamedElement(fmt.Sprintf("shp-%s", secret.Name))) + Expect(taskRun.Spec.TaskSpec.Steps[1].VolumeMounts).To(utils.ContainNamedElement(fmt.Sprintf("shp-%s", secret.Name))) + + // Verify the param + Expect(taskRun.Spec.TaskSpec.Params).To(utils.ContainNamedElement("shp-output-directory")) + Expect(taskRun.Spec.Params).To(utils.ContainNamedElement("shp-output-directory")) + + // Verify the step args + Expect(taskRun.Spec.TaskSpec.Steps[1].Args).To(BeEquivalentTo([]string{ + "--push", + "$(params.shp-output-directory)", + "--image", + "$(params.shp-output-image)", + "--insecure=$(params.shp-output-insecure)", + "--result-file-image-digest", + "$(results.shp-image-digest.path)", + "--result-file-image-size", + "$(results.shp-image-size.path)", + "--secret-path", + "/workspace/shp-push-secret", + })) + }) + }) + }) }) diff --git a/vendor/github.com/google/go-containerregistry/internal/httptest/httptest.go b/vendor/github.com/google/go-containerregistry/internal/httptest/httptest.go new file mode 100644 index 0000000000..85b1719072 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/internal/httptest/httptest.go @@ -0,0 +1,104 @@ +// Copyright 2020 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 httptest provides a method for testing a TLS server a la net/http/httptest. +package httptest + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "math/big" + "net" + "net/http" + "net/http/httptest" + "time" +) + +// NewTLSServer returns an httptest server, with an http client that has been configured to +// send all requests to the returned server. The TLS certs are generated for the given domain. +// If you need a transport, Client().Transport is correctly configured. +func NewTLSServer(domain string, handler http.Handler) (*httptest.Server, error) { + s := httptest.NewUnstartedServer(handler) + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(time.Hour), + IPAddresses: []net.IP{ + net.IPv4(127, 0, 0, 1), + net.IPv6loopback, + }, + DNSNames: []string{domain}, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return nil, err + } + + b, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, err + } + + pc := &bytes.Buffer{} + if err := pem.Encode(pc, &pem.Block{Type: "CERTIFICATE", Bytes: b}); err != nil { + return nil, err + } + + ek, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, err + } + + pk := &bytes.Buffer{} + if err := pem.Encode(pk, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ek}); err != nil { + return nil, err + } + + c, err := tls.X509KeyPair(pc.Bytes(), pk.Bytes()) + if err != nil { + return nil, err + } + s.TLS = &tls.Config{ + Certificates: []tls.Certificate{c}, + } + s.StartTLS() + + certpool := x509.NewCertPool() + certpool.AddCert(s.Certificate()) + + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certpool, + }, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial(s.Listener.Addr().Network(), s.Listener.Addr().String()) + }, + } + s.Client().Transport = t + + return s, nil +} diff --git a/vendor/github.com/google/go-containerregistry/internal/legacy/copy.go b/vendor/github.com/google/go-containerregistry/internal/legacy/copy.go deleted file mode 100644 index 10467ba10c..0000000000 --- a/vendor/github.com/google/go-containerregistry/internal/legacy/copy.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2019 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 legacy provides methods for interacting with legacy image formats. -package legacy - -import ( - "bytes" - "encoding/json" - - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" -) - -// CopySchema1 allows `[g]crane cp` to work with old images without adding -// full support for schema 1 images to this package. -func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, opts ...remote.Option) error { - m := schema1{} - if err := json.NewDecoder(bytes.NewReader(desc.Manifest)).Decode(&m); err != nil { - return err - } - - for _, layer := range m.FSLayers { - src := srcRef.Context().Digest(layer.BlobSum) - dst := dstRef.Context().Digest(layer.BlobSum) - - blob, err := remote.Layer(src, opts...) - if err != nil { - return err - } - - if err := remote.WriteLayer(dst.Context(), blob, opts...); err != nil { - return err - } - } - - return remote.Put(dstRef, desc, opts...) -} - -type fslayer struct { - BlobSum string `json:"blobSum"` -} - -type schema1 struct { - FSLayers []fslayer `json:"fsLayers"` -} diff --git a/vendor/github.com/google/go-containerregistry/internal/windows/windows.go b/vendor/github.com/google/go-containerregistry/internal/windows/windows.go deleted file mode 100644 index f6a1ade08b..0000000000 --- a/vendor/github.com/google/go-containerregistry/internal/windows/windows.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2021 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 windows - -import ( - "archive/tar" - "bytes" - "errors" - "fmt" - "io" - "io/ioutil" - "path" - "strings" - - "github.com/google/go-containerregistry/internal/gzip" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/tarball" -) - -// userOwnerAndGroupSID is a magic value needed to make the binary executable -// in a Windows container. -// -// owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU") -const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==" - -// Windows returns a Layer that is converted to be pullable on Windows. -func Windows(layer v1.Layer) (v1.Layer, error) { - // TODO: do this lazily. - - layerReader, err := layer.Uncompressed() - if err != nil { - return nil, fmt.Errorf("getting layer: %w", err) - } - defer layerReader.Close() - tarReader := tar.NewReader(layerReader) - w := new(bytes.Buffer) - tarWriter := tar.NewWriter(w) - defer tarWriter.Close() - - for _, dir := range []string{"Files", "Hives"} { - if err := tarWriter.WriteHeader(&tar.Header{ - Name: dir, - Typeflag: tar.TypeDir, - // Use a fixed Mode, so that this isn't sensitive to the directory and umask - // under which it was created. Additionally, windows can only set 0222, - // 0444, or 0666, none of which are executable. - Mode: 0555, - Format: tar.FormatPAX, - }); err != nil { - return nil, fmt.Errorf("writing %s directory: %w", dir, err) - } - } - - for { - header, err := tarReader.Next() - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return nil, fmt.Errorf("reading layer: %w", err) - } - - if strings.HasPrefix(header.Name, "Files/") { - return nil, fmt.Errorf("file path %q already suitable for Windows", header.Name) - } - - header.Name = path.Join("Files", header.Name) - header.Format = tar.FormatPAX - - // TODO: this seems to make the file executable on Windows; - // only do this if the file should be executable. - if header.PAXRecords == nil { - header.PAXRecords = map[string]string{} - } - header.PAXRecords["MSWINDOWS.rawsd"] = userOwnerAndGroupSID - - if err := tarWriter.WriteHeader(header); err != nil { - return nil, fmt.Errorf("writing tar header: %w", err) - } - - if header.Typeflag == tar.TypeReg { - if _, err = io.Copy(tarWriter, tarReader); err != nil { - return nil, fmt.Errorf("writing layer file: %w", err) - } - } - } - - if err := tarWriter.Close(); err != nil { - return nil, err - } - - b := w.Bytes() - // gzip the contents, then create the layer - opener := func() (io.ReadCloser, error) { - return gzip.ReadCloser(ioutil.NopCloser(bytes.NewReader(b))), nil - } - layer, err = tarball.LayerFromOpener(opener) - if err != nil { - return nil, fmt.Errorf("creating layer: %w", err) - } - - return layer, nil -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/append.go b/vendor/github.com/google/go-containerregistry/pkg/crane/append.go deleted file mode 100644 index 92d6b7b3b0..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/append.go +++ /dev/null @@ -1,100 +0,0 @@ -// 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 crane - -import ( - "fmt" - "os" - - "github.com/google/go-containerregistry/internal/windows" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/stream" - "github.com/google/go-containerregistry/pkg/v1/tarball" -) - -func isWindows(img v1.Image) (bool, error) { - if img == nil { - return false, nil - } - cfg, err := img.ConfigFile() - if err != nil { - return false, err - } - return cfg != nil && cfg.OS == "windows", nil -} - -// Append reads a layer from path and appends it the the v1.Image base. -// -// If the base image is a Windows base image (i.e., its config.OS is -// "windows"), the contents of the tarballs will be modified to be suitable for -// a Windows container image.`, -func Append(base v1.Image, paths ...string) (v1.Image, error) { - win, err := isWindows(base) - if err != nil { - return nil, fmt.Errorf("getting base image: %w", err) - } - - layers := make([]v1.Layer, 0, len(paths)) - for _, path := range paths { - layer, err := getLayer(path) - if err != nil { - return nil, fmt.Errorf("reading layer %q: %w", path, err) - } - - if win { - layer, err = windows.Windows(layer) - if err != nil { - return nil, fmt.Errorf("converting %q for Windows: %w", path, err) - } - } - - layers = append(layers, layer) - } - - return mutate.AppendLayers(base, layers...) -} - -func getLayer(path string) (v1.Layer, error) { - f, err := streamFile(path) - if err != nil { - return nil, err - } - if f != nil { - return stream.NewLayer(f), nil - } - - return tarball.LayerFromFile(path) -} - -// If we're dealing with a named pipe, trying to open it multiple times will -// fail, so we need to do a streaming upload. -// -// returns nil, nil for non-streaming files -func streamFile(path string) (*os.File, error) { - if path == "-" { - return os.Stdin, nil - } - fi, err := os.Stat(path) - if err != nil { - return nil, err - } - - if !fi.Mode().IsRegular() { - return os.Open(path) - } - - return nil, nil -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/catalog.go b/vendor/github.com/google/go-containerregistry/pkg/crane/catalog.go deleted file mode 100644 index f30800cca3..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/catalog.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2019 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 crane - -import ( - "context" - - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" -) - -// Catalog returns the repositories in a registry's catalog. -func Catalog(src string, opt ...Option) (res []string, err error) { - o := makeOptions(opt...) - reg, err := name.NewRegistry(src, o.Name...) - if err != nil { - return nil, err - } - - // This context gets overridden by remote.WithContext, which is set by - // crane.WithContext. - return remote.Catalog(context.Background(), reg, o.Remote...) -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/config.go b/vendor/github.com/google/go-containerregistry/pkg/crane/config.go deleted file mode 100644 index 3e55cc93a7..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/config.go +++ /dev/null @@ -1,24 +0,0 @@ -// 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 crane - -// Config returns the config file for the remote image ref. -func Config(ref string, opt ...Option) ([]byte, error) { - i, _, err := getImage(ref, opt...) - if err != nil { - return nil, err - } - return i.RawConfigFile() -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go b/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go deleted file mode 100644 index a606f96544..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/copy.go +++ /dev/null @@ -1,88 +0,0 @@ -// 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 crane - -import ( - "fmt" - - "github.com/google/go-containerregistry/internal/legacy" - "github.com/google/go-containerregistry/pkg/logs" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/types" -) - -// Copy copies a remote image or index from src to dst. -func Copy(src, dst string, opt ...Option) error { - o := makeOptions(opt...) - srcRef, err := name.ParseReference(src, o.Name...) - if err != nil { - return fmt.Errorf("parsing reference %q: %w", src, err) - } - - dstRef, err := name.ParseReference(dst, o.Name...) - if err != nil { - return fmt.Errorf("parsing reference for %q: %w", dst, err) - } - - logs.Progress.Printf("Copying from %v to %v", srcRef, dstRef) - desc, err := remote.Get(srcRef, o.Remote...) - if err != nil { - return fmt.Errorf("fetching %q: %w", src, err) - } - - switch desc.MediaType { - case types.OCIImageIndex, types.DockerManifestList: - // Handle indexes separately. - if o.Platform != nil { - // If platform is explicitly set, don't copy the whole index, just the appropriate image. - if err := copyImage(desc, dstRef, o); err != nil { - return fmt.Errorf("failed to copy image: %w", err) - } - } else { - if err := copyIndex(desc, dstRef, o); err != nil { - return fmt.Errorf("failed to copy index: %w", err) - } - } - case types.DockerManifestSchema1, types.DockerManifestSchema1Signed: - // Handle schema 1 images separately. - if err := legacy.CopySchema1(desc, srcRef, dstRef, o.Remote...); err != nil { - return fmt.Errorf("failed to copy schema 1 image: %w", err) - } - default: - // Assume anything else is an image, since some registries don't set mediaTypes properly. - if err := copyImage(desc, dstRef, o); err != nil { - return fmt.Errorf("failed to copy image: %w", err) - } - } - - return nil -} - -func copyImage(desc *remote.Descriptor, dstRef name.Reference, o Options) error { - img, err := desc.Image() - if err != nil { - return err - } - return remote.Write(dstRef, img, o.Remote...) -} - -func copyIndex(desc *remote.Descriptor, dstRef name.Reference, o Options) error { - idx, err := desc.ImageIndex() - if err != nil { - return err - } - return remote.WriteIndex(dstRef, idx, o.Remote...) -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go b/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go deleted file mode 100644 index 868a57010d..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/digest.go +++ /dev/null @@ -1,52 +0,0 @@ -// 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 crane - -import "github.com/google/go-containerregistry/pkg/logs" - -// Digest returns the sha256 hash of the remote image at ref. -func Digest(ref string, opt ...Option) (string, error) { - o := makeOptions(opt...) - if o.Platform != nil { - desc, err := getManifest(ref, opt...) - if err != nil { - return "", err - } - if !desc.MediaType.IsIndex() { - return desc.Digest.String(), nil - } - - // TODO: does not work for indexes which contain schema v1 manifests - img, err := desc.Image() - if err != nil { - return "", err - } - digest, err := img.Digest() - if err != nil { - return "", err - } - return digest.String(), nil - } - desc, err := Head(ref, opt...) - if err != nil { - logs.Warn.Printf("HEAD request failed, falling back on GET: %v", err) - rdesc, err := getManifest(ref, opt...) - if err != nil { - return "", err - } - return rdesc.Digest.String(), nil - } - return desc.Digest.String(), nil -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/export.go b/vendor/github.com/google/go-containerregistry/pkg/crane/export.go deleted file mode 100644 index 5d6da1dca8..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/export.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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 crane - -import ( - "io" - - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/mutate" -) - -// Export writes the filesystem contents (as a tarball) of img to w. -// If img has a single layer, just write the (uncompressed) contents to w so -// that this "just works" for images that just wrap a single blob. -func Export(img v1.Image, w io.Writer) error { - layers, err := img.Layers() - if err != nil { - return err - } - if len(layers) == 1 { - // If it's a single layer, we don't have to flatten the filesystem. - // An added perk of skipping mutate.Extract here is that this works - // for non-tarball layers. - l := layers[0] - rc, err := l.Uncompressed() - if err != nil { - return err - } - _, err = io.Copy(w, rc) - return err - } - fs := mutate.Extract(img) - _, err = io.Copy(w, fs) - return err -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/filemap.go b/vendor/github.com/google/go-containerregistry/pkg/crane/filemap.go deleted file mode 100644 index 2072c19587..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/filemap.go +++ /dev/null @@ -1,73 +0,0 @@ -// 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 crane - -import ( - "archive/tar" - "bytes" - "io" - "io/ioutil" - "sort" - - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/tarball" -) - -// Layer creates a layer from a single file map. These layers are reproducible and consistent. -// A filemap is a path -> file content map representing a file system. -func Layer(filemap map[string][]byte) (v1.Layer, error) { - b := &bytes.Buffer{} - w := tar.NewWriter(b) - - fn := []string{} - for f := range filemap { - fn = append(fn, f) - } - sort.Strings(fn) - - for _, f := range fn { - c := filemap[f] - if err := w.WriteHeader(&tar.Header{ - Name: f, - Size: int64(len(c)), - }); err != nil { - return nil, err - } - if _, err := w.Write(c); err != nil { - return nil, err - } - } - if err := w.Close(); err != nil { - return nil, err - } - - // Return a new copy of the buffer each time it's opened. - return tarball.LayerFromOpener(func() (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewBuffer(b.Bytes())), nil - }) -} - -// Image creates a image with the given filemaps as its contents. These images are reproducible and consistent. -// A filemap is a path -> file content map representing a file system. -func Image(filemap map[string][]byte) (v1.Image, error) { - y, err := Layer(filemap) - if err != nil { - return nil, err - } - - return mutate.AppendLayers(empty.Image, y) -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/get.go b/vendor/github.com/google/go-containerregistry/pkg/crane/get.go deleted file mode 100644 index 1f12f01d0d..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/get.go +++ /dev/null @@ -1,56 +0,0 @@ -// 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 crane - -import ( - "fmt" - - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" -) - -func getImage(r string, opt ...Option) (v1.Image, name.Reference, error) { - o := makeOptions(opt...) - ref, err := name.ParseReference(r, o.Name...) - if err != nil { - return nil, nil, fmt.Errorf("parsing reference %q: %w", r, err) - } - img, err := remote.Image(ref, o.Remote...) - if err != nil { - return nil, nil, fmt.Errorf("reading image %q: %w", ref, err) - } - return img, ref, nil -} - -func getManifest(r string, opt ...Option) (*remote.Descriptor, error) { - o := makeOptions(opt...) - ref, err := name.ParseReference(r, o.Name...) - if err != nil { - return nil, fmt.Errorf("parsing reference %q: %w", r, err) - } - return remote.Get(ref, o.Remote...) -} - -// Head performs a HEAD request for a manifest and returns a content descriptor -// based on the registry's response. -func Head(r string, opt ...Option) (*v1.Descriptor, error) { - o := makeOptions(opt...) - ref, err := name.ParseReference(r, o.Name...) - if err != nil { - return nil, err - } - return remote.Head(ref, o.Remote...) -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/list.go b/vendor/github.com/google/go-containerregistry/pkg/crane/list.go deleted file mode 100644 index 38352153bb..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/list.go +++ /dev/null @@ -1,33 +0,0 @@ -// 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 crane - -import ( - "fmt" - - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" -) - -// ListTags returns the tags in repository src. -func ListTags(src string, opt ...Option) ([]string, error) { - o := makeOptions(opt...) - repo, err := name.NewRepository(src, o.Name...) - if err != nil { - return nil, fmt.Errorf("parsing repo %q: %w", src, err) - } - - return remote.List(repo, o.Remote...) -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/manifest.go b/vendor/github.com/google/go-containerregistry/pkg/crane/manifest.go deleted file mode 100644 index a54926aef3..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/manifest.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 crane - -// Manifest returns the manifest for the remote image or index ref. -func Manifest(ref string, opt ...Option) ([]byte, error) { - desc, err := getManifest(ref, opt...) - if err != nil { - return nil, err - } - o := makeOptions(opt...) - if o.Platform != nil { - img, err := desc.Image() - if err != nil { - return nil, err - } - return img.RawManifest() - } - return desc.Manifest, nil -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/optimize.go b/vendor/github.com/google/go-containerregistry/pkg/crane/optimize.go deleted file mode 100644 index 74c665df7a..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/optimize.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2020 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 crane - -import ( - "errors" - "fmt" - - "github.com/containerd/stargz-snapshotter/estargz" - "github.com/google/go-containerregistry/pkg/logs" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/tarball" - "github.com/google/go-containerregistry/pkg/v1/types" -) - -// Optimize optimizes a remote image or index from src to dst. -// THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE WITHOUT WARNING. -func Optimize(src, dst string, prioritize []string, opt ...Option) error { - pset := newStringSet(prioritize) - o := makeOptions(opt...) - srcRef, err := name.ParseReference(src, o.Name...) - if err != nil { - return fmt.Errorf("parsing reference %q: %w", src, err) - } - - dstRef, err := name.ParseReference(dst, o.Name...) - if err != nil { - return fmt.Errorf("parsing reference for %q: %w", dst, err) - } - - logs.Progress.Printf("Optimizing from %v to %v", srcRef, dstRef) - desc, err := remote.Get(srcRef, o.Remote...) - if err != nil { - return fmt.Errorf("fetching %q: %w", src, err) - } - - switch desc.MediaType { - case types.OCIImageIndex, types.DockerManifestList: - // Handle indexes separately. - if o.Platform != nil { - // If platform is explicitly set, don't optimize the whole index, just the appropriate image. - if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil { - return fmt.Errorf("failed to optimize image: %w", err) - } - } else { - if err := optimizeAndPushIndex(desc, dstRef, pset, o); err != nil { - return fmt.Errorf("failed to optimize index: %w", err) - } - } - - case types.DockerManifestSchema1, types.DockerManifestSchema1Signed: - return errors.New("docker schema 1 images are not supported") - - default: - // Assume anything else is an image, since some registries don't set mediaTypes properly. - if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil { - return fmt.Errorf("failed to optimize image: %w", err) - } - } - - return nil -} - -func optimizeAndPushImage(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error { - img, err := desc.Image() - if err != nil { - return err - } - - missing, oimg, err := optimizeImage(img, prioritize) - if err != nil { - return err - } - - if len(missing) > 0 { - return fmt.Errorf("the following prioritized files were missing from image: %v", missing.List()) - } - - return remote.Write(dstRef, oimg, o.Remote...) -} - -func optimizeImage(img v1.Image, prioritize stringSet) (stringSet, v1.Image, error) { - cfg, err := img.ConfigFile() - if err != nil { - return nil, nil, err - } - ocfg := cfg.DeepCopy() - ocfg.History = nil - ocfg.RootFS.DiffIDs = nil - - oimg, err := mutate.ConfigFile(empty.Image, ocfg) - if err != nil { - return nil, nil, err - } - - layers, err := img.Layers() - if err != nil { - return nil, nil, err - } - - missingFromImage := newStringSet(prioritize.List()) - olayers := make([]mutate.Addendum, 0, len(layers)) - for _, layer := range layers { - missingFromLayer := []string{} - olayer, err := tarball.LayerFromOpener(layer.Uncompressed, - tarball.WithEstargz, - tarball.WithEstargzOptions( - estargz.WithPrioritizedFiles(prioritize.List()), - estargz.WithAllowPrioritizeNotFound(&missingFromLayer), - )) - if err != nil { - return nil, nil, err - } - missingFromImage = missingFromImage.Intersection(newStringSet(missingFromLayer)) - - olayers = append(olayers, mutate.Addendum{ - Layer: olayer, - MediaType: types.DockerLayer, - }) - } - - oimg, err = mutate.Append(oimg, olayers...) - if err != nil { - return nil, nil, err - } - return missingFromImage, oimg, nil -} - -func optimizeAndPushIndex(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error { - idx, err := desc.ImageIndex() - if err != nil { - return err - } - - missing, oidx, err := optimizeIndex(idx, prioritize) - if err != nil { - return err - } - - if len(missing) > 0 { - return fmt.Errorf("the following prioritized files were missing from all images: %v", missing.List()) - } - - return remote.WriteIndex(dstRef, oidx, o.Remote...) -} - -func optimizeIndex(idx v1.ImageIndex, prioritize stringSet) (stringSet, v1.ImageIndex, error) { - im, err := idx.IndexManifest() - if err != nil { - return nil, nil, err - } - - missingFromIndex := newStringSet(prioritize.List()) - - // Build an image for each child from the base and append it to a new index to produce the result. - adds := make([]mutate.IndexAddendum, 0, len(im.Manifests)) - for _, desc := range im.Manifests { - img, err := idx.Image(desc.Digest) - if err != nil { - return nil, nil, err - } - - missingFromImage, oimg, err := optimizeImage(img, prioritize) - if err != nil { - return nil, nil, err - } - missingFromIndex = missingFromIndex.Intersection(missingFromImage) - adds = append(adds, mutate.IndexAddendum{ - Add: oimg, - Descriptor: v1.Descriptor{ - URLs: desc.URLs, - MediaType: desc.MediaType, - Annotations: desc.Annotations, - Platform: desc.Platform, - }, - }) - } - - idxType, err := idx.MediaType() - if err != nil { - return nil, nil, err - } - - return missingFromIndex, mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), idxType), nil -} - -type stringSet map[string]struct{} - -func newStringSet(in []string) stringSet { - ss := stringSet{} - for _, s := range in { - ss[s] = struct{}{} - } - return ss -} - -func (s stringSet) List() []string { - result := make([]string, 0, len(s)) - for k := range s { - result = append(result, k) - } - return result -} - -func (s stringSet) Intersection(rhs stringSet) stringSet { - // To appease ST1016 - lhs := s - - // Make sure len(lhs) >= len(rhs) - if len(lhs) < len(rhs) { - return rhs.Intersection(lhs) - } - - result := stringSet{} - for k := range lhs { - if _, ok := rhs[k]; ok { - result[k] = struct{}{} - } - } - return result -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/options.go b/vendor/github.com/google/go-containerregistry/pkg/crane/options.go deleted file mode 100644 index 6e1177e4df..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/options.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2019 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 crane - -import ( - "context" - "net/http" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" -) - -// Options hold the options that crane uses when calling other packages. -type Options struct { - Name []name.Option - Remote []remote.Option - Platform *v1.Platform -} - -// GetOptions exposes the underlying []remote.Option, []name.Option, and -// platform, based on the passed Option. Generally, you shouldn't need to use -// this unless you've painted yourself into a dependency corner as we have -// with the crane and gcrane cli packages. -func GetOptions(opts ...Option) Options { - return makeOptions(opts...) -} - -func makeOptions(opts ...Option) Options { - opt := Options{ - Remote: []remote.Option{ - remote.WithAuthFromKeychain(authn.DefaultKeychain), - }, - } - for _, o := range opts { - o(&opt) - } - return opt -} - -// Option is a functional option for crane. -type Option func(*Options) - -// WithTransport is a functional option for overriding the default transport -// for remote operations. -func WithTransport(t http.RoundTripper) Option { - return func(o *Options) { - o.Remote = append(o.Remote, remote.WithTransport(t)) - } -} - -// Insecure is an Option that allows image references to be fetched without TLS. -func Insecure(o *Options) { - o.Name = append(o.Name, name.Insecure) -} - -// WithPlatform is an Option to specify the platform. -func WithPlatform(platform *v1.Platform) Option { - return func(o *Options) { - if platform != nil { - o.Remote = append(o.Remote, remote.WithPlatform(*platform)) - } - o.Platform = platform - } -} - -// WithAuthFromKeychain is a functional option for overriding the default -// authenticator for remote operations, using an authn.Keychain to find -// credentials. -// -// By default, crane will use authn.DefaultKeychain. -func WithAuthFromKeychain(keys authn.Keychain) Option { - return func(o *Options) { - // Replace the default keychain at position 0. - o.Remote[0] = remote.WithAuthFromKeychain(keys) - } -} - -// WithAuth is a functional option for overriding the default authenticator -// for remote operations. -// -// By default, crane will use authn.DefaultKeychain. -func WithAuth(auth authn.Authenticator) Option { - return func(o *Options) { - // Replace the default keychain at position 0. - o.Remote[0] = remote.WithAuth(auth) - } -} - -// WithUserAgent adds the given string to the User-Agent header for any HTTP -// requests. -func WithUserAgent(ua string) Option { - return func(o *Options) { - o.Remote = append(o.Remote, remote.WithUserAgent(ua)) - } -} - -// WithContext is a functional option for setting the context. -func WithContext(ctx context.Context) Option { - return func(o *Options) { - o.Remote = append(o.Remote, remote.WithContext(ctx)) - } -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go b/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go deleted file mode 100644 index b19ac7c212..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/pull.go +++ /dev/null @@ -1,145 +0,0 @@ -// 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 crane - -import ( - "fmt" - "os" - - legacy "github.com/google/go-containerregistry/pkg/legacy/tarball" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/layout" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/tarball" -) - -// Tag applied to images that were pulled by digest. This denotes that the -// image was (probably) never tagged with this, but lets us avoid applying the -// ":latest" tag which might be misleading. -const iWasADigestTag = "i-was-a-digest" - -// Pull returns a v1.Image of the remote image src. -func Pull(src string, opt ...Option) (v1.Image, error) { - o := makeOptions(opt...) - ref, err := name.ParseReference(src, o.Name...) - if err != nil { - return nil, fmt.Errorf("parsing reference %q: %w", src, err) - } - - return remote.Image(ref, o.Remote...) -} - -// Save writes the v1.Image img as a tarball at path with tag src. -func Save(img v1.Image, src, path string) error { - imgMap := map[string]v1.Image{src: img} - return MultiSave(imgMap, path) -} - -// MultiSave writes collection of v1.Image img with tag as a tarball. -func MultiSave(imgMap map[string]v1.Image, path string, opt ...Option) error { - o := makeOptions(opt...) - tagToImage := map[name.Tag]v1.Image{} - - for src, img := range imgMap { - ref, err := name.ParseReference(src, o.Name...) - if err != nil { - return fmt.Errorf("parsing ref %q: %w", src, err) - } - - // WriteToFile wants a tag to write to the tarball, but we might have - // been given a digest. - // If the original ref was a tag, use that. Otherwise, if it was a - // digest, tag the image with :i-was-a-digest instead. - tag, ok := ref.(name.Tag) - if !ok { - d, ok := ref.(name.Digest) - if !ok { - return fmt.Errorf("ref wasn't a tag or digest") - } - tag = d.Repository.Tag(iWasADigestTag) - } - tagToImage[tag] = img - } - // no progress channel (for now) - return tarball.MultiWriteToFile(path, tagToImage) -} - -// PullLayer returns the given layer from a registry. -func PullLayer(ref string, opt ...Option) (v1.Layer, error) { - o := makeOptions(opt...) - digest, err := name.NewDigest(ref, o.Name...) - if err != nil { - return nil, err - } - - return remote.Layer(digest, o.Remote...) -} - -// SaveLegacy writes the v1.Image img as a legacy tarball at path with tag src. -func SaveLegacy(img v1.Image, src, path string) error { - imgMap := map[string]v1.Image{src: img} - return MultiSave(imgMap, path) -} - -// MultiSaveLegacy writes collection of v1.Image img with tag as a legacy tarball. -func MultiSaveLegacy(imgMap map[string]v1.Image, path string) error { - refToImage := map[name.Reference]v1.Image{} - - for src, img := range imgMap { - ref, err := name.ParseReference(src) - if err != nil { - return fmt.Errorf("parsing ref %q: %w", src, err) - } - refToImage[ref] = img - } - - w, err := os.Create(path) - if err != nil { - return err - } - defer w.Close() - - return legacy.MultiWrite(refToImage, w) -} - -// SaveOCI writes the v1.Image img as an OCI Image Layout at path. If a layout -// already exists at that path, it will add the image to the index. -func SaveOCI(img v1.Image, path string) error { - imgMap := map[string]v1.Image{"": img} - return MultiSaveOCI(imgMap, path) -} - -// MultiSaveOCI writes collection of v1.Image img as an OCI Image Layout at path. If a layout -// already exists at that path, it will add the image to the index. -func MultiSaveOCI(imgMap map[string]v1.Image, path string) error { - p, err := layout.FromPath(path) - if err != nil { - p, err = layout.Write(path, empty.Index) - if err != nil { - return err - } - } - for ref, img := range imgMap { - anns := map[string]string{ - "dev.ggcr.image.name": ref, - } - if err = p.AppendImage(img, layout.WithAnnotations(anns)); err != nil { - return err - } - } - return nil -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/push.go b/vendor/github.com/google/go-containerregistry/pkg/crane/push.go deleted file mode 100644 index 6d1fbd6cef..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/push.go +++ /dev/null @@ -1,65 +0,0 @@ -// 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 crane - -import ( - "fmt" - - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/tarball" -) - -// Load reads the tarball at path as a v1.Image. -func Load(path string, opt ...Option) (v1.Image, error) { - return LoadTag(path, "") -} - -// LoadTag reads a tag from the tarball at path as a v1.Image. -// If tag is "", will attempt to read the tarball as a single image. -func LoadTag(path, tag string, opt ...Option) (v1.Image, error) { - if tag == "" { - return tarball.ImageFromPath(path, nil) - } - - o := makeOptions(opt...) - t, err := name.NewTag(tag, o.Name...) - if err != nil { - return nil, fmt.Errorf("parsing tag %q: %w", tag, err) - } - return tarball.ImageFromPath(path, &t) -} - -// Push pushes the v1.Image img to a registry as dst. -func Push(img v1.Image, dst string, opt ...Option) error { - o := makeOptions(opt...) - tag, err := name.ParseReference(dst, o.Name...) - if err != nil { - return fmt.Errorf("parsing reference %q: %w", dst, err) - } - return remote.Write(tag, img, o.Remote...) -} - -// Upload pushes the v1.Layer to a given repo. -func Upload(layer v1.Layer, repo string, opt ...Option) error { - o := makeOptions(opt...) - ref, err := name.NewRepository(repo, o.Name...) - if err != nil { - return fmt.Errorf("parsing repo %q: %w", repo, err) - } - - return remote.WriteLayer(ref, layer, o.Remote...) -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/tag.go b/vendor/github.com/google/go-containerregistry/pkg/crane/tag.go deleted file mode 100644 index 13bc395872..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/tag.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2019 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 crane - -import ( - "fmt" - - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" -) - -// Tag adds tag to the remote img. -func Tag(img, tag string, opt ...Option) error { - o := makeOptions(opt...) - ref, err := name.ParseReference(img, o.Name...) - if err != nil { - return fmt.Errorf("parsing reference %q: %w", img, err) - } - desc, err := remote.Get(ref, o.Remote...) - if err != nil { - return fmt.Errorf("fetching %q: %w", img, err) - } - - dst := ref.Context().Tag(tag) - - return remote.Tag(dst, desc, o.Remote...) -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/config.go b/vendor/github.com/google/go-containerregistry/pkg/legacy/config.go deleted file mode 100644 index 3364bec61c..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/legacy/config.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2019 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 legacy - -import ( - v1 "github.com/google/go-containerregistry/pkg/v1" -) - -// LayerConfigFile is the configuration file that holds the metadata describing -// a v1 layer. See: -// https://github.com/moby/moby/blob/master/image/spec/v1.md -type LayerConfigFile struct { - v1.ConfigFile - - ContainerConfig v1.Config `json:"container_config,omitempty"` - - ID string `json:"id,omitempty"` - Parent string `json:"parent,omitempty"` - Throwaway bool `json:"throwaway,omitempty"` - Comment string `json:"comment,omitempty"` -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/doc.go b/vendor/github.com/google/go-containerregistry/pkg/legacy/doc.go deleted file mode 100644 index 1d1668887a..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/legacy/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019 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 legacy provides functionality to work with docker images in the v1 -// format. -// See: https://github.com/moby/moby/blob/master/image/spec/v1.md -package legacy diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/README.md b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/README.md deleted file mode 100644 index 90b88c7578..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# `legacy/tarball` - -[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball) - -This package implements support for writing legacy tarballs, as described -[here](https://github.com/moby/moby/blob/749d90e10f989802638ae542daf54257f3bf71f2/image/spec/v1.2.md#combined-image-json--filesystem-changeset-format). diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/doc.go b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/doc.go deleted file mode 100644 index 62684d6e72..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019 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 tarball provides facilities for writing v1 docker images -// (https://github.com/moby/moby/blob/master/image/spec/v1.md) from/to a tarball -// on-disk. -package tarball diff --git a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/write.go b/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/write.go deleted file mode 100644 index 77b5eabcd7..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/legacy/tarball/write.go +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright 2019 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 tarball - -import ( - "archive/tar" - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "sort" - "strings" - - "github.com/google/go-containerregistry/pkg/legacy" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/partial" - "github.com/google/go-containerregistry/pkg/v1/tarball" -) - -// repositoriesTarDescriptor represents the repositories file inside a `docker save` tarball. -type repositoriesTarDescriptor map[string]map[string]string - -// v1Layer represents a layer with metadata needed by the v1 image spec https://github.com/moby/moby/blob/master/image/spec/v1.md. -type v1Layer struct { - // config is the layer metadata. - config *legacy.LayerConfigFile - // layer is the v1.Layer object this v1Layer represents. - layer v1.Layer -} - -// json returns the raw bytes of the json metadata of the given v1Layer. -func (l *v1Layer) json() ([]byte, error) { - return json.Marshal(l.config) -} - -// version returns the raw bytes of the "VERSION" file of the given v1Layer. -func (l *v1Layer) version() []byte { - return []byte("1.0") -} - -// v1LayerID computes the v1 image format layer id for the given v1.Layer with the given v1 parent ID and raw image config. -func v1LayerID(layer v1.Layer, parentID string, rawConfig []byte) (string, error) { - d, err := layer.Digest() - if err != nil { - return "", fmt.Errorf("unable to get layer digest to generate v1 layer ID: %w", err) - } - s := fmt.Sprintf("%s %s", d.Hex, parentID) - if len(rawConfig) != 0 { - s = fmt.Sprintf("%s %s", s, string(rawConfig)) - } - rawDigest := sha256.Sum256([]byte(s)) - return hex.EncodeToString(rawDigest[:]), nil -} - -// newTopV1Layer creates a new v1Layer for a layer other than the top layer in a v1 image tarball. -func newV1Layer(layer v1.Layer, parent *v1Layer, history v1.History) (*v1Layer, error) { - parentID := "" - if parent != nil { - parentID = parent.config.ID - } - id, err := v1LayerID(layer, parentID, nil) - if err != nil { - return nil, fmt.Errorf("unable to generate v1 layer ID: %w", err) - } - result := &v1Layer{ - layer: layer, - config: &legacy.LayerConfigFile{ - ConfigFile: v1.ConfigFile{ - Created: history.Created, - Author: history.Author, - }, - ContainerConfig: v1.Config{ - Cmd: []string{history.CreatedBy}, - }, - ID: id, - Parent: parentID, - Throwaway: history.EmptyLayer, - Comment: history.Comment, - }, - } - return result, nil -} - -// newTopV1Layer creates a new v1Layer for the top layer in a v1 image tarball. -func newTopV1Layer(layer v1.Layer, parent *v1Layer, history v1.History, imgConfig *v1.ConfigFile, rawConfig []byte) (*v1Layer, error) { - result, err := newV1Layer(layer, parent, history) - if err != nil { - return nil, err - } - id, err := v1LayerID(layer, result.config.Parent, rawConfig) - if err != nil { - return nil, fmt.Errorf("unable to generate v1 layer ID for top layer: %w", err) - } - result.config.ID = id - result.config.Architecture = imgConfig.Architecture - result.config.Container = imgConfig.Container - result.config.DockerVersion = imgConfig.DockerVersion - result.config.OS = imgConfig.OS - result.config.Config = imgConfig.Config - result.config.Created = imgConfig.Created - return result, nil -} - -// splitTag splits the given tagged image name /: -// into / and . -func splitTag(name string) (string, string) { - // Split on ":" - parts := strings.Split(name, ":") - // Verify that we aren't confusing a tag for a hostname w/ port for the purposes of weak validation. - if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") { - base := strings.Join(parts[:len(parts)-1], ":") - tag := parts[len(parts)-1] - return base, tag - } - return name, "" -} - -// addTags adds the given image tags to the given "repositories" file descriptor in a v1 image tarball. -func addTags(repos repositoriesTarDescriptor, tags []string, topLayerID string) { - for _, t := range tags { - base, tag := splitTag(t) - tagToID, ok := repos[base] - if !ok { - tagToID = make(map[string]string) - repos[base] = tagToID - } - tagToID[tag] = topLayerID - } -} - -// updateLayerSources updates the given layer digest to descriptor map with the descriptor of the given layer in the given image if it's an undistributable layer. -func updateLayerSources(layerSources map[v1.Hash]v1.Descriptor, layer v1.Layer, img v1.Image) error { - d, err := layer.Digest() - if err != nil { - return err - } - // Add to LayerSources if it's a foreign layer. - desc, err := partial.BlobDescriptor(img, d) - if err != nil { - return err - } - if !desc.MediaType.IsDistributable() { - diffid, err := partial.BlobToDiffID(img, d) - if err != nil { - return err - } - layerSources[diffid] = *desc - } - return nil -} - -// Write is a wrapper to write a single image in V1 format and tag to a tarball. -func Write(ref name.Reference, img v1.Image, w io.Writer) error { - return MultiWrite(map[name.Reference]v1.Image{ref: img}, w) -} - -// filterEmpty filters out the history corresponding to empty layers from the -// given history. -func filterEmpty(h []v1.History) []v1.History { - result := []v1.History{} - for _, i := range h { - if i.EmptyLayer { - continue - } - result = append(result, i) - } - return result -} - -// MultiWrite writes the contents of each image to the provided reader, in the V1 image tarball format. -// The contents are written in the following format: -// One manifest.json file at the top level containing information about several images. -// One repositories file mapping from the image / to to the id of the top most layer. -// For every layer, a directory named with the layer ID is created with the following contents: -// layer.tar - The uncompressed layer tarball. -// .json- Layer metadata json. -// VERSION- Schema version string. Always set to "1.0". -// One file for the config blob, named after its SHA. -func MultiWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error { - tf := tar.NewWriter(w) - defer tf.Close() - - sortedImages, imageToTags := dedupRefToImage(refToImage) - var m tarball.Manifest - repos := make(repositoriesTarDescriptor) - - seenLayerIDs := make(map[string]struct{}) - for _, img := range sortedImages { - tags := imageToTags[img] - - // Write the config. - cfgName, err := img.ConfigName() - if err != nil { - return err - } - cfgFileName := fmt.Sprintf("%s.json", cfgName.Hex) - cfgBlob, err := img.RawConfigFile() - if err != nil { - return err - } - if err := writeTarEntry(tf, cfgFileName, bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil { - return err - } - cfg, err := img.ConfigFile() - if err != nil { - return err - } - - // Store foreign layer info. - layerSources := make(map[v1.Hash]v1.Descriptor) - - // Write the layers. - layers, err := img.Layers() - if err != nil { - return err - } - history := filterEmpty(cfg.History) - // Create a blank config history if the config didn't have a history. - if len(history) == 0 && len(layers) != 0 { - history = make([]v1.History, len(layers)) - } else if len(layers) != len(history) { - return fmt.Errorf("image config had layer history which did not match the number of layers, got len(history)=%d, len(layers)=%d, want len(history)=len(layers)", len(history), len(layers)) - } - layerFiles := make([]string, len(layers)) - var prev *v1Layer - for i, l := range layers { - if err := updateLayerSources(layerSources, l, img); err != nil { - return fmt.Errorf("unable to update image metadata to include undistributable layer source information: %w", err) - } - var cur *v1Layer - if i < (len(layers) - 1) { - cur, err = newV1Layer(l, prev, history[i]) - } else { - cur, err = newTopV1Layer(l, prev, history[i], cfg, cfgBlob) - } - if err != nil { - return err - } - layerFiles[i] = fmt.Sprintf("%s/layer.tar", cur.config.ID) - if _, ok := seenLayerIDs[cur.config.ID]; ok { - prev = cur - continue - } - seenLayerIDs[cur.config.ID] = struct{}{} - - // If the v1.Layer implements UncompressedSize efficiently, use that - // for the tar header. Otherwise, this iterates over Uncompressed(). - // NOTE: If using a streaming layer, this may consume the layer. - size, err := partial.UncompressedSize(l) - if err != nil { - return err - } - u, err := l.Uncompressed() - if err != nil { - return err - } - defer u.Close() - if err := writeTarEntry(tf, layerFiles[i], u, size); err != nil { - return err - } - - j, err := cur.json() - if err != nil { - return err - } - if err := writeTarEntry(tf, fmt.Sprintf("%s/json", cur.config.ID), bytes.NewReader(j), int64(len(j))); err != nil { - return err - } - v := cur.version() - if err := writeTarEntry(tf, fmt.Sprintf("%s/VERSION", cur.config.ID), bytes.NewReader(v), int64(len(v))); err != nil { - return err - } - prev = cur - } - - // Generate the tar descriptor and write it. - m = append(m, tarball.Descriptor{ - Config: cfgFileName, - RepoTags: tags, - Layers: layerFiles, - LayerSources: layerSources, - }) - // prev should be the top layer here. Use it to add the image tags - // to the tarball repositories file. - addTags(repos, tags, prev.config.ID) - } - - mBytes, err := json.Marshal(m) - if err != nil { - return err - } - - if err := writeTarEntry(tf, "manifest.json", bytes.NewReader(mBytes), int64(len(mBytes))); err != nil { - return err - } - reposBytes, err := json.Marshal(&repos) - if err != nil { - return err - } - if err := writeTarEntry(tf, "repositories", bytes.NewReader(reposBytes), int64(len(reposBytes))); err != nil { - return err - } - return nil -} - -func dedupRefToImage(refToImage map[name.Reference]v1.Image) ([]v1.Image, map[v1.Image][]string) { - imageToTags := make(map[v1.Image][]string) - - for ref, img := range refToImage { - if tag, ok := ref.(name.Tag); ok { - if tags, ok := imageToTags[img]; ok && tags != nil { - imageToTags[img] = append(tags, tag.String()) - } else { - imageToTags[img] = []string{tag.String()} - } - } else { - if _, ok := imageToTags[img]; !ok { - imageToTags[img] = nil - } - } - } - - // Force specific order on tags - imgs := []v1.Image{} - for img, tags := range imageToTags { - sort.Strings(tags) - imgs = append(imgs, img) - } - - sort.Slice(imgs, func(i, j int) bool { - cfI, err := imgs[i].ConfigName() - if err != nil { - return false - } - cfJ, err := imgs[j].ConfigName() - if err != nil { - return false - } - return cfI.Hex < cfJ.Hex - }) - - return imgs, imageToTags -} - -// Writes 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, - Size: size, - Name: path, - } - if err := tf.WriteHeader(hdr); err != nil { - return err - } - _, err := io.Copy(tf, r) - return err -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/README.md b/vendor/github.com/google/go-containerregistry/pkg/registry/README.md new file mode 100644 index 0000000000..5e58bbcd5c --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/README.md @@ -0,0 +1,14 @@ +# `pkg/registry` + +This package implements a Docker v2 registry and the OCI distribution specification. + +It is designed to be used anywhere a low dependency container registry is needed, with an initial focus on tests. + +Its goal is to be standards compliant and its strictness will increase over time. + +This is currently a low flightmiles system. It's likely quite safe to use in tests; If you're using it in production, please let us know how and send us PRs for integration tests. + +Before sending a PR, understand that the expectation of this package is that it remain free of extraneous dependencies. +This means that we expect `pkg/registry` to only have dependencies on Go's standard library, and other packages in `go-containerregistry`. + +You may be asked to change your code to reduce dependencies, and your PR might be rejected if this is deemed impossible. diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go b/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go new file mode 100644 index 0000000000..9677d69d95 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go @@ -0,0 +1,445 @@ +// 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 registry + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "math/rand" + "net/http" + "path" + "strings" + "sync" + + "github.com/google/go-containerregistry/internal/verify" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// Returns whether this url should be handled by the blob handler +// This is complicated because blob is indicated by the trailing path, not the leading path. +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-a-layer +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-a-layer +func isBlob(req *http.Request) bool { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + if elem[len(elem)-1] == "" { + elem = elem[:len(elem)-1] + } + if len(elem) < 3 { + return false + } + return elem[len(elem)-2] == "blobs" || (elem[len(elem)-3] == "blobs" && + elem[len(elem)-2] == "uploads") +} + +// blobHandler represents a minimal blob storage backend, capable of serving +// blob contents. +type blobHandler interface { + // Get gets the blob contents, or errNotFound if the blob wasn't found. + Get(ctx context.Context, repo string, h v1.Hash) (io.ReadCloser, error) +} + +// blobStatHandler is an extension interface representing a blob storage +// backend that can serve metadata about blobs. +type blobStatHandler interface { + // Stat returns the size of the blob, or errNotFound if the blob wasn't + // found, or redirectError if the blob can be found elsewhere. + Stat(ctx context.Context, repo string, h v1.Hash) (int64, error) +} + +// blobPutHandler is an extension interface representing a blob storage backend +// that can write blob contents. +type blobPutHandler interface { + // Put puts the blob contents. + // + // The contents will be verified against the expected size and digest + // as the contents are read, and an error will be returned if these + // don't match. Implementations should return that error, or a wrapper + // around that error, to return the correct error when these don't match. + Put(ctx context.Context, repo string, h v1.Hash, rc io.ReadCloser) error +} + +// redirectError represents a signal that the blob handler doesn't have the blob +// contents, but that those contents are at another location which registry +// clients should redirect to. +type redirectError struct { + // Location is the location to find the contents. + Location string + + // Code is the HTTP redirect status code to return to clients. + Code int +} + +func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) } + +// errNotFound represents an error locating the blob. +var errNotFound = errors.New("not found") + +type memHandler struct { + m map[string][]byte + lock sync.Mutex +} + +func (m *memHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error) { + m.lock.Lock() + defer m.lock.Unlock() + + b, found := m.m[h.String()] + if !found { + return 0, errNotFound + } + return int64(len(b)), nil +} +func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, error) { + m.lock.Lock() + defer m.lock.Unlock() + + b, found := m.m[h.String()] + if !found { + return nil, errNotFound + } + return ioutil.NopCloser(bytes.NewReader(b)), nil +} +func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadCloser) error { + m.lock.Lock() + defer m.lock.Unlock() + + defer rc.Close() + all, err := ioutil.ReadAll(rc) + if err != nil { + return err + } + m.m[h.String()] = all + return nil +} + +// blobs +type blobs struct { + blobHandler blobHandler + + // Each upload gets a unique id that writes occur to until finalized. + uploads map[string][]byte + lock sync.Mutex +} + +func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + if elem[len(elem)-1] == "" { + elem = elem[:len(elem)-1] + } + // Must have a path of form /v2/{name}/blobs/{upload,sha256:} + if len(elem) < 4 { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "blobs must be attached to a repo", + } + } + target := elem[len(elem)-1] + service := elem[len(elem)-2] + digest := req.URL.Query().Get("digest") + contentRange := req.Header.Get("Content-Range") + + repo := req.URL.Host + path.Join(elem[1:len(elem)-2]...) + + switch req.Method { + case http.MethodHead: + h, err := v1.NewHash(target) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + var size int64 + if bsh, ok := b.blobHandler.(blobStatHandler); ok { + size, err = bsh.Stat(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + return regErrInternal(err) + } + } else { + rc, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + return regErrInternal(err) + } + defer rc.Close() + size, err = io.Copy(ioutil.Discard, rc) + if err != nil { + return regErrInternal(err) + } + } + + resp.Header().Set("Content-Length", fmt.Sprint(size)) + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.WriteHeader(http.StatusOK) + return nil + + case http.MethodGet: + h, err := v1.NewHash(target) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + var size int64 + var r io.Reader + if bsh, ok := b.blobHandler.(blobStatHandler); ok { + size, err = bsh.Stat(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + return regErrInternal(err) + } + + rc, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + + return regErrInternal(err) + } + defer rc.Close() + r = rc + } else { + tmp, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + + return regErrInternal(err) + } + defer tmp.Close() + var buf bytes.Buffer + io.Copy(&buf, tmp) + size = int64(buf.Len()) + r = &buf + } + + resp.Header().Set("Content-Length", fmt.Sprint(size)) + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, r) + return nil + + case http.MethodPost: + bph, ok := b.blobHandler.(blobPutHandler) + if !ok { + return regErrUnsupported + } + + // It is weird that this is "target" instead of "service", but + // that's how the index math works out above. + if target != "uploads" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: fmt.Sprintf("POST to /blobs must be followed by /uploads, got %s", target), + } + } + + if digest != "" { + h, err := v1.NewHash(digest) + if err != nil { + return regErrDigestInvalid + } + + vrc, err := verify.ReadCloser(req.Body, req.ContentLength, h) + if err != nil { + return regErrInternal(err) + } + defer vrc.Close() + + if err = bph.Put(req.Context(), repo, h, vrc); err != nil { + if errors.As(err, &verify.Error{}) { + log.Printf("Digest mismatch: %v", err) + return regErrDigestMismatch + } + return regErrInternal(err) + } + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.WriteHeader(http.StatusCreated) + return nil + } + + id := fmt.Sprint(rand.Int63()) + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id)) + resp.Header().Set("Range", "0-0") + resp.WriteHeader(http.StatusAccepted) + return nil + + case http.MethodPatch: + if service != "uploads" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: fmt.Sprintf("PATCH to /blobs must be followed by /uploads, got %s", service), + } + } + + if contentRange != "" { + start, end := 0, 0 + if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil { + return ®Error{ + Status: http.StatusRequestedRangeNotSatisfiable, + Code: "BLOB_UPLOAD_UNKNOWN", + Message: "We don't understand your Content-Range", + } + } + b.lock.Lock() + defer b.lock.Unlock() + if start != len(b.uploads[target]) { + return ®Error{ + Status: http.StatusRequestedRangeNotSatisfiable, + Code: "BLOB_UPLOAD_UNKNOWN", + Message: "Your content range doesn't match what we have", + } + } + l := bytes.NewBuffer(b.uploads[target]) + io.Copy(l, req.Body) + b.uploads[target] = l.Bytes() + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target)) + resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1)) + resp.WriteHeader(http.StatusNoContent) + return nil + } + + b.lock.Lock() + defer b.lock.Unlock() + if _, ok := b.uploads[target]; ok { + return ®Error{ + Status: http.StatusBadRequest, + Code: "BLOB_UPLOAD_INVALID", + Message: "Stream uploads after first write are not allowed", + } + } + + l := &bytes.Buffer{} + io.Copy(l, req.Body) + + b.uploads[target] = l.Bytes() + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target)) + resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1)) + resp.WriteHeader(http.StatusNoContent) + return nil + + case http.MethodPut: + bph, ok := b.blobHandler.(blobPutHandler) + if !ok { + return regErrUnsupported + } + + if service != "uploads" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: fmt.Sprintf("PUT to /blobs must be followed by /uploads, got %s", service), + } + } + + if digest == "" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest not specified", + } + } + + b.lock.Lock() + defer b.lock.Unlock() + + h, err := v1.NewHash(digest) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + defer req.Body.Close() + in := ioutil.NopCloser(io.MultiReader(bytes.NewBuffer(b.uploads[target]), req.Body)) + + size := int64(verify.SizeUnknown) + if req.ContentLength > 0 { + size = int64(len(b.uploads[target])) + req.ContentLength + } + + vrc, err := verify.ReadCloser(in, size, h) + if err != nil { + return regErrInternal(err) + } + defer vrc.Close() + + if err := bph.Put(req.Context(), repo, h, vrc); err != nil { + if errors.As(err, &verify.Error{}) { + log.Printf("Digest mismatch: %v", err) + return regErrDigestMismatch + } + return regErrInternal(err) + } + + delete(b.uploads, target) + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.WriteHeader(http.StatusCreated) + return nil + + default: + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } + } +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/error.go b/vendor/github.com/google/go-containerregistry/pkg/registry/error.go new file mode 100644 index 0000000000..f8e126dace --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/error.go @@ -0,0 +1,79 @@ +// 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 registry + +import ( + "encoding/json" + "net/http" +) + +type regError struct { + Status int + Code string + Message string +} + +func (r *regError) Write(resp http.ResponseWriter) error { + resp.WriteHeader(r.Status) + + type err struct { + Code string `json:"code"` + Message string `json:"message"` + } + type wrap struct { + Errors []err `json:"errors"` + } + return json.NewEncoder(resp).Encode(wrap{ + Errors: []err{ + { + Code: r.Code, + Message: r.Message, + }, + }, + }) +} + +// regErrInternal returns an internal server error. +func regErrInternal(err error) *regError { + return ®Error{ + Status: http.StatusInternalServerError, + Code: "INTERNAL_SERVER_ERROR", + Message: err.Error(), + } +} + +var regErrBlobUnknown = ®Error{ + Status: http.StatusNotFound, + Code: "BLOB_UNKNOWN", + Message: "Unknown blob", +} + +var regErrUnsupported = ®Error{ + Status: http.StatusMethodNotAllowed, + Code: "UNSUPPORTED", + Message: "Unsupported operation", +} + +var regErrDigestMismatch = ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest does not match contents", +} + +var regErrDigestInvalid = ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go b/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go new file mode 100644 index 0000000000..2a97239161 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go @@ -0,0 +1,333 @@ +// 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 registry + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sort" + "strconv" + "strings" + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type catalog struct { + Repos []string `json:"repositories"` +} + +type listTags struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} + +type manifest struct { + contentType string + blob []byte +} + +type manifests struct { + // maps repo -> manifest tag/digest -> manifest + manifests map[string]map[string]manifest + lock sync.Mutex + log *log.Logger +} + +func isManifest(req *http.Request) bool { + elems := strings.Split(req.URL.Path, "/") + elems = elems[1:] + if len(elems) < 4 { + return false + } + return elems[len(elems)-2] == "manifests" +} + +func isTags(req *http.Request) bool { + elems := strings.Split(req.URL.Path, "/") + elems = elems[1:] + if len(elems) < 4 { + return false + } + return elems[len(elems)-2] == "tags" +} + +func isCatalog(req *http.Request) bool { + elems := strings.Split(req.URL.Path, "/") + elems = elems[1:] + if len(elems) < 2 { + return false + } + + return elems[len(elems)-1] == "_catalog" +} + +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image +func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + target := elem[len(elem)-1] + repo := strings.Join(elem[1:len(elem)-2], "/") + + switch req.Method { + case http.MethodGet: + m.lock.Lock() + defer m.lock.Unlock() + + c, ok := m.manifests[repo] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + m, ok := c[target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: "Unknown manifest", + } + } + rd := sha256.Sum256(m.blob) + d := "sha256:" + hex.EncodeToString(rd[:]) + resp.Header().Set("Docker-Content-Digest", d) + resp.Header().Set("Content-Type", m.contentType) + resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob))) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader(m.blob)) + return nil + + case http.MethodHead: + m.lock.Lock() + defer m.lock.Unlock() + if _, ok := m.manifests[repo]; !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + m, ok := m.manifests[repo][target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: "Unknown manifest", + } + } + rd := sha256.Sum256(m.blob) + d := "sha256:" + hex.EncodeToString(rd[:]) + resp.Header().Set("Docker-Content-Digest", d) + resp.Header().Set("Content-Type", m.contentType) + resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob))) + resp.WriteHeader(http.StatusOK) + return nil + + case http.MethodPut: + m.lock.Lock() + defer m.lock.Unlock() + if _, ok := m.manifests[repo]; !ok { + m.manifests[repo] = map[string]manifest{} + } + b := &bytes.Buffer{} + io.Copy(b, req.Body) + rd := sha256.Sum256(b.Bytes()) + digest := "sha256:" + hex.EncodeToString(rd[:]) + mf := manifest{ + blob: b.Bytes(), + contentType: req.Header.Get("Content-Type"), + } + + // If the manifest is a manifest list, check that the manifest + // list's constituent manifests are already uploaded. + // This isn't strictly required by the registry API, but some + // registries require this. + if types.MediaType(mf.contentType).IsIndex() { + im, err := v1.ParseIndexManifest(b) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "MANIFEST_INVALID", + Message: err.Error(), + } + } + for _, desc := range im.Manifests { + if !desc.MediaType.IsDistributable() { + continue + } + if desc.MediaType.IsIndex() || desc.MediaType.IsImage() { + if _, found := m.manifests[repo][desc.Digest.String()]; !found { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest), + } + } + } else { + // TODO: Probably want to do an existence check for blobs. + m.log.Printf("TODO: Check blobs for %q", desc.Digest) + } + } + } + + // Allow future references by target (tag) and immutable digest. + // See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier. + m.manifests[repo][target] = mf + m.manifests[repo][digest] = mf + resp.Header().Set("Docker-Content-Digest", digest) + resp.WriteHeader(http.StatusCreated) + return nil + + case http.MethodDelete: + m.lock.Lock() + defer m.lock.Unlock() + if _, ok := m.manifests[repo]; !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + + _, ok := m.manifests[repo][target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: "Unknown manifest", + } + } + + delete(m.manifests[repo], target) + resp.WriteHeader(http.StatusAccepted) + return nil + + default: + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } + } +} + +func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *regError { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + repo := strings.Join(elem[1:len(elem)-2], "/") + query := req.URL.Query() + nStr := query.Get("n") + n := 1000 + if nStr != "" { + n, _ = strconv.Atoi(nStr) + } + + if req.Method == "GET" { + m.lock.Lock() + defer m.lock.Unlock() + + c, ok := m.manifests[repo] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + + var tags []string + countTags := 0 + // TODO: implement pagination https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated + for tag := range c { + if countTags >= n { + break + } + countTags++ + if !strings.Contains(tag, "sha256:") { + tags = append(tags, tag) + } + } + sort.Strings(tags) + + tagsToList := listTags{ + Name: repo, + Tags: tags, + } + + msg, _ := json.Marshal(tagsToList) + resp.Header().Set("Content-Length", fmt.Sprint(len(msg))) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader([]byte(msg))) + return nil + } + + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } +} + +func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError { + query := req.URL.Query() + nStr := query.Get("n") + n := 10000 + if nStr != "" { + n, _ = strconv.Atoi(nStr) + } + + if req.Method == "GET" { + m.lock.Lock() + defer m.lock.Unlock() + + var repos []string + countRepos := 0 + // TODO: implement pagination + for key := range m.manifests { + if countRepos >= n { + break + } + countRepos++ + + repos = append(repos, key) + } + + repositoriesToList := catalog{ + Repos: repos, + } + + msg, _ := json.Marshal(repositoriesToList) + resp.Header().Set("Content-Length", fmt.Sprint(len(msg))) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader([]byte(msg))) + return nil + } + + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go b/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go new file mode 100644 index 0000000000..00a7ff9265 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go @@ -0,0 +1,104 @@ +// 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 registry implements a docker V2 registry and the OCI distribution specification. +// +// It is designed to be used anywhere a low dependency container registry is needed, with an +// initial focus on tests. +// +// Its goal is to be standards compliant and its strictness will increase over time. +// +// This is currently a low flightmiles system. It's likely quite safe to use in tests; If you're using it +// in production, please let us know how and send us CL's for integration tests. +package registry + +import ( + "log" + "net/http" + "os" +) + +type registry struct { + log *log.Logger + blobs blobs + manifests manifests +} + +// https://docs.docker.com/registry/spec/api/#api-version-check +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#api-version-check +func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError { + if isBlob(req) { + return r.blobs.handle(resp, req) + } + if isManifest(req) { + return r.manifests.handle(resp, req) + } + if isTags(req) { + return r.manifests.handleTags(resp, req) + } + if isCatalog(req) { + return r.manifests.handleCatalog(resp, req) + } + resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0") + if req.URL.Path != "/v2/" && req.URL.Path != "/v2" { + return ®Error{ + Status: http.StatusNotFound, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } + } + resp.WriteHeader(200) + return nil +} + +func (r *registry) root(resp http.ResponseWriter, req *http.Request) { + if rerr := r.v2(resp, req); rerr != nil { + r.log.Printf("%s %s %d %s %s", req.Method, req.URL, rerr.Status, rerr.Code, rerr.Message) + rerr.Write(resp) + return + } + r.log.Printf("%s %s", req.Method, req.URL) +} + +// New returns a handler which implements the docker registry protocol. +// It should be registered at the site root. +func New(opts ...Option) http.Handler { + r := ®istry{ + log: log.New(os.Stderr, "", log.LstdFlags), + blobs: blobs{ + blobHandler: &memHandler{m: map[string][]byte{}}, + uploads: map[string][]byte{}, + }, + manifests: manifests{ + manifests: map[string]map[string]manifest{}, + log: log.New(os.Stderr, "", log.LstdFlags), + }, + } + for _, o := range opts { + o(r) + } + return http.HandlerFunc(r.root) +} + +// Option describes the available options +// for creating the registry. +type Option func(r *registry) + +// Logger overrides the logger used to record requests to the registry. +func Logger(l *log.Logger) Option { + return func(r *registry) { + r.log = l + r.manifests.log = l + } +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/delete.go b/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go similarity index 54% rename from vendor/github.com/google/go-containerregistry/pkg/crane/delete.go rename to vendor/github.com/google/go-containerregistry/pkg/registry/tls.go index 58a8be1f0b..cb2644e615 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/delete.go +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go @@ -12,22 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package crane +package registry import ( - "fmt" + "net/http/httptest" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" + ggcrtest "github.com/google/go-containerregistry/internal/httptest" ) -// Delete deletes the remote reference at src. -func Delete(src string, opt ...Option) error { - o := makeOptions(opt...) - ref, err := name.ParseReference(src, o.Name...) - if err != nil { - return fmt.Errorf("parsing reference %q: %w", src, err) - } - - return remote.Delete(ref, o.Remote...) +// TLS returns an httptest server, with an http client that has been configured to +// send all requests to the returned server. The TLS certs are generated for the given domain +// which should correspond to the domain the image is stored in. +// If you need a transport, Client().Transport is correctly configured. +func TLS(domain string) (*httptest.Server, error) { + return ggcrtest.NewTLSServer(domain, New()) } diff --git a/vendor/github.com/google/go-containerregistry/pkg/crane/doc.go b/vendor/github.com/google/go-containerregistry/pkg/v1/random/doc.go similarity index 79% rename from vendor/github.com/google/go-containerregistry/pkg/crane/doc.go rename to vendor/github.com/google/go-containerregistry/pkg/v1/random/doc.go index 7602d7953f..d3712767d2 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/crane/doc.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/random/doc.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC All Rights Reserved. +// 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. @@ -12,5 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package crane holds libraries used to implement the crane CLI. -package crane +// Package random provides a facility for synthesizing pseudo-random images. +package random diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/random/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/random/image.go new file mode 100644 index 0000000000..6399412bef --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/random/image.go @@ -0,0 +1,117 @@ +// 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 random + +import ( + "archive/tar" + "bytes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + mrand "math/rand" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// uncompressedLayer implements partial.UncompressedLayer from raw bytes. +type uncompressedLayer struct { + diffID v1.Hash + mediaType types.MediaType + content []byte +} + +// DiffID implements partial.UncompressedLayer +func (ul *uncompressedLayer) DiffID() (v1.Hash, error) { + return ul.diffID, nil +} + +// Uncompressed implements partial.UncompressedLayer +func (ul *uncompressedLayer) Uncompressed() (io.ReadCloser, error) { + return ioutil.NopCloser(bytes.NewBuffer(ul.content)), nil +} + +// MediaType returns the media type of the layer +func (ul *uncompressedLayer) MediaType() (types.MediaType, error) { + return ul.mediaType, nil +} + +var _ partial.UncompressedLayer = (*uncompressedLayer)(nil) + +// Image returns a pseudo-randomly generated Image. +func Image(byteSize, layers int64) (v1.Image, error) { + adds := make([]mutate.Addendum, 0, 5) + for i := int64(0); i < layers; i++ { + layer, err := Layer(byteSize, types.DockerLayer) + if err != nil { + return nil, err + } + adds = append(adds, mutate.Addendum{ + Layer: layer, + History: v1.History{ + Author: "random.Image", + Comment: fmt.Sprintf("this is a random history %d of %d", i, layers), + CreatedBy: "random", + Created: v1.Time{Time: time.Now()}, + }, + }) + } + + return mutate.Append(empty.Image, adds...) +} + +// Layer returns a layer with pseudo-randomly generated content. +func Layer(byteSize int64, mt types.MediaType) (v1.Layer, error) { + fileName := fmt.Sprintf("random_file_%d.txt", mrand.Int()) //nolint: gosec + + // Hash the contents as we write it out to the buffer. + var b bytes.Buffer + hasher := sha256.New() + mw := io.MultiWriter(&b, hasher) + + // Write a single file with a random name and random contents. + tw := tar.NewWriter(mw) + if err := tw.WriteHeader(&tar.Header{ + Name: fileName, + Size: byteSize, + Typeflag: tar.TypeRegA, + }); err != nil { + return nil, err + } + if _, err := io.CopyN(tw, rand.Reader, byteSize); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + + h := v1.Hash{ + Algorithm: "sha256", + Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))), + } + + return partial.UncompressedToLayer(&uncompressedLayer{ + diffID: h, + mediaType: mt, + content: b.Bytes(), + }) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/random/index.go b/vendor/github.com/google/go-containerregistry/pkg/v1/random/index.go new file mode 100644 index 0000000000..fb992211a5 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/random/index.go @@ -0,0 +1,110 @@ +// 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 random + +import ( + "bytes" + "encoding/json" + "fmt" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type randomIndex struct { + images map[v1.Hash]v1.Image + manifest *v1.IndexManifest +} + +// Index returns a pseudo-randomly generated ImageIndex with count images, each +// having the given number of layers of size byteSize. +func Index(byteSize, layers, count int64) (v1.ImageIndex, error) { + manifest := v1.IndexManifest{ + SchemaVersion: 2, + Manifests: []v1.Descriptor{}, + } + + images := make(map[v1.Hash]v1.Image) + for i := int64(0); i < count; i++ { + img, err := Image(byteSize, layers) + if err != nil { + return nil, err + } + + rawManifest, err := img.RawManifest() + if err != nil { + return nil, err + } + digest, size, err := v1.SHA256(bytes.NewReader(rawManifest)) + if err != nil { + return nil, err + } + mediaType, err := img.MediaType() + if err != nil { + return nil, err + } + + manifest.Manifests = append(manifest.Manifests, v1.Descriptor{ + Digest: digest, + Size: size, + MediaType: mediaType, + }) + + images[digest] = img + } + + return &randomIndex{ + images: images, + manifest: &manifest, + }, nil +} + +func (i *randomIndex) MediaType() (types.MediaType, error) { + return types.OCIImageIndex, nil +} + +func (i *randomIndex) Digest() (v1.Hash, error) { + return partial.Digest(i) +} + +func (i *randomIndex) Size() (int64, error) { + return partial.Size(i) +} + +func (i *randomIndex) IndexManifest() (*v1.IndexManifest, error) { + return i.manifest, nil +} + +func (i *randomIndex) RawManifest() ([]byte, error) { + m, err := i.IndexManifest() + if err != nil { + return nil, err + } + return json.Marshal(m) +} + +func (i *randomIndex) Image(h v1.Hash) (v1.Image, error) { + if img, ok := i.images[h]; ok { + return img, nil + } + + return nil, fmt.Errorf("image not found: %v", h) +} + +func (i *randomIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { + // This is a single level index (for now?). + return nil, fmt.Errorf("image not found: %v", h) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 95b8cdf645..911e84fb8f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -234,24 +234,22 @@ github.com/google/go-cmp/cmp/internal/value github.com/google/go-containerregistry/internal/and github.com/google/go-containerregistry/internal/estargz github.com/google/go-containerregistry/internal/gzip -github.com/google/go-containerregistry/internal/legacy +github.com/google/go-containerregistry/internal/httptest github.com/google/go-containerregistry/internal/redact github.com/google/go-containerregistry/internal/retry github.com/google/go-containerregistry/internal/retry/wait github.com/google/go-containerregistry/internal/verify -github.com/google/go-containerregistry/internal/windows github.com/google/go-containerregistry/pkg/authn -github.com/google/go-containerregistry/pkg/crane -github.com/google/go-containerregistry/pkg/legacy -github.com/google/go-containerregistry/pkg/legacy/tarball github.com/google/go-containerregistry/pkg/logs github.com/google/go-containerregistry/pkg/name +github.com/google/go-containerregistry/pkg/registry github.com/google/go-containerregistry/pkg/v1 github.com/google/go-containerregistry/pkg/v1/empty github.com/google/go-containerregistry/pkg/v1/layout github.com/google/go-containerregistry/pkg/v1/match github.com/google/go-containerregistry/pkg/v1/mutate github.com/google/go-containerregistry/pkg/v1/partial +github.com/google/go-containerregistry/pkg/v1/random github.com/google/go-containerregistry/pkg/v1/remote github.com/google/go-containerregistry/pkg/v1/remote/transport github.com/google/go-containerregistry/pkg/v1/stream