From 20cebe729e1888d6fa842104ed86321e4e157d6c Mon Sep 17 00:00:00 2001 From: Jimmi Dyson Date: Wed, 14 Sep 2022 14:41:37 +0100 Subject: [PATCH] test: Add e2e tests for image bundle serve and push (#186) --- .../push/imagebundle/image_bundle.go | 51 +++- .../serve/imagebundle/image_bundle.go | 25 +- cmd/mindthegap/serve/serve.go | 3 +- cmd/mindthegap/utils/copy_file.go | 37 +++ config/images_config.go | 2 +- make/go.mk | 3 +- make/skopeo.mk | 2 +- skopeo/skopeo.go | 18 ++ test/e2e/helmbundle/helpers/helpers.go | 2 +- test/e2e/helmbundle/push_bundle_test.go | 4 +- test/e2e/imagebundle/helpers/helpers.go | 217 ++++++++++++++++++ .../e2e/imagebundle/imagebundle_suite_test.go | 16 ++ test/e2e/imagebundle/push_bundle_test.go | 178 ++++++++++++++ test/e2e/imagebundle/serve_bundle_test.go | 161 +++++++++++++ .../imagebundle/testdata/create-success.yaml | 8 + 15 files changed, 700 insertions(+), 27 deletions(-) create mode 100644 cmd/mindthegap/utils/copy_file.go create mode 100644 test/e2e/imagebundle/helpers/helpers.go create mode 100644 test/e2e/imagebundle/imagebundle_suite_test.go create mode 100644 test/e2e/imagebundle/push_bundle_test.go create mode 100644 test/e2e/imagebundle/serve_bundle_test.go create mode 100644 test/e2e/imagebundle/testdata/create-success.yaml diff --git a/cmd/mindthegap/push/imagebundle/image_bundle.go b/cmd/mindthegap/push/imagebundle/image_bundle.go index 058a4bd2..111133a9 100644 --- a/cmd/mindthegap/push/imagebundle/image_bundle.go +++ b/cmd/mindthegap/push/imagebundle/image_bundle.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "path/filepath" "github.com/spf13/cobra" @@ -22,12 +23,13 @@ import ( func NewCommand(out output.Output) *cobra.Command { var ( - imageBundleFiles []string - destRegistry string - destRegistrySkipTLSVerify bool - destRegistryUsername string - destRegistryPassword string - ecrLifecyclePolicy string + imageBundleFiles []string + destRegistry string + destRegistryCACertificateFile string + destRegistrySkipTLSVerify bool + destRegistryUsername string + destRegistryPassword string + ecrLifecyclePolicy string ) cmd := &cobra.Command{ @@ -113,6 +115,7 @@ func NewCommand(out output.Output) *cobra.Command { reg.Address(), destRegistry, skopeoOpts, + destRegistryCACertificateFile, destRegistrySkipTLSVerify, out, skopeoRunner, @@ -126,8 +129,14 @@ func NewCommand(out output.Output) *cobra.Command { _ = cmd.MarkFlagRequired("image-bundle") cmd.Flags().StringVar(&destRegistry, "to-registry", "", "Registry to push images to") _ = cmd.MarkFlagRequired("to-registry") + cmd.Flags().StringVar(&destRegistryCACertificateFile, "to-registry-ca-cert-file", "", + "CA certificate file used to verify TLS verification of registry to push images to (use for http registries)") cmd.Flags().BoolVar(&destRegistrySkipTLSVerify, "to-registry-insecure-skip-tls-verify", false, - "Skip TLS verification of registry to push images to (use for http registries)") + "Skip TLS verification of registry to push images to (use for non-TLS http registries)") + cmd.MarkFlagsMutuallyExclusive( + "to-registry-ca-cert-file", + "to-registry-insecure-skip-tls-verify", + ) cmd.Flags().StringVar(&destRegistryUsername, "to-registry-username", "", "Username to use to log in to destination registry") cmd.Flags().StringVar(&destRegistryPassword, "to-registry-password", "", @@ -145,6 +154,7 @@ func pushImages( cfg config.ImagesConfig, sourceRegistry, destRegistry string, skopeoOpts []skopeo.SkopeoOption, + destRegistryCACertificateFile string, destRegistrySkipTLSVerify bool, out output.Output, skopeoRunner *skopeo.Runner, @@ -153,12 +163,31 @@ func pushImages( // Sort registries for deterministic ordering. regNames := cfg.SortedRegistryNames() + if destRegistryCACertificateFile != "" { + tmpDir, err := os.MkdirTemp("", ".skopeo-certs-*") + if err != nil { + return fmt.Errorf( + "failed to create temporary directory for destination registry certificates: %w", + err, + ) + } + defer os.RemoveAll(tmpDir) + + if err := utils.CopyFile(destRegistryCACertificateFile, filepath.Join(tmpDir, "ca.crt")); err != nil { + return err + } + + skopeoOpts = append(skopeoOpts, skopeo.DestCertDir(tmpDir)) + } + + skopeoOpts = append(skopeoOpts, skopeo.DisableSrcTLSVerify()) + + if destRegistrySkipTLSVerify { + skopeoOpts = append(skopeoOpts, skopeo.DisableDestTLSVerify()) + } + for _, registryName := range regNames { registryConfig := cfg[registryName] - skopeoOpts = append(skopeoOpts, skopeo.DisableSrcTLSVerify()) - if destRegistrySkipTLSVerify { - skopeoOpts = append(skopeoOpts, skopeo.DisableDestTLSVerify()) - } // Sort images for deterministic ordering. imageNames := registryConfig.SortedImageNames() diff --git a/cmd/mindthegap/serve/imagebundle/image_bundle.go b/cmd/mindthegap/serve/imagebundle/image_bundle.go index fe186ea0..45ae9a38 100644 --- a/cmd/mindthegap/serve/imagebundle/image_bundle.go +++ b/cmd/mindthegap/serve/imagebundle/image_bundle.go @@ -5,6 +5,7 @@ package imagebundle import ( "fmt" + "net/http" "os" "path/filepath" @@ -18,7 +19,7 @@ import ( "github.com/mesosphere/mindthegap/docker/registry" ) -func NewCommand(out output.Output) *cobra.Command { +func NewCommand(out output.Output) (cmd *cobra.Command, stopCh chan struct{}) { var ( imageBundleFiles []string listenAddress string @@ -27,7 +28,9 @@ func NewCommand(out output.Output) *cobra.Command { tlsKey string ) - cmd := &cobra.Command{ + stopCh = make(chan struct{}) + + cmd = &cobra.Command{ Use: "image-bundle", Short: "Serve an OCI registry from image bundles", RunE: func(cmd *cobra.Command, args []string) error { @@ -74,23 +77,27 @@ func NewCommand(out output.Output) *cobra.Command { } out.EndOperation(true) out.Infof("Listening on %s\n", reg.Address()) - if err := reg.ListenAndServe(); err != nil { - out.Error(err, "error serving Docker registry") - os.Exit(2) - } + + go func() { + if err := reg.ListenAndServe(); err != nil && err != http.ErrServerClosed { + out.Error(err, "error serving Docker registry") + os.Exit(2) + } + }() + <-stopCh return nil }, } - cmd.Flags().StringSliceVar(&imageBundleFiles, "images-bundle", nil, + cmd.Flags().StringSliceVar(&imageBundleFiles, "image-bundle", nil, "Tarball of images to serve. Can also be a glob pattern.") - _ = cmd.MarkFlagRequired("images-bundle") + _ = cmd.MarkFlagRequired("image-bundle") cmd.Flags().StringVar(&listenAddress, "listen-address", "localhost", "Address to listen on") cmd.Flags(). Uint16Var(&listenPort, "listen-port", 0, "Port to listen on (0 means use any free port)") cmd.Flags().StringVar(&tlsCertificate, "tls-cert-file", "", "TLS certificate file") cmd.Flags().StringVar(&tlsKey, "tls-private-key-file", "", "TLS private key file") - return cmd + return cmd, stopCh } diff --git a/cmd/mindthegap/serve/serve.go b/cmd/mindthegap/serve/serve.go index e04dac89..b0e36574 100644 --- a/cmd/mindthegap/serve/serve.go +++ b/cmd/mindthegap/serve/serve.go @@ -18,7 +18,8 @@ func NewCommand(out output.Output) *cobra.Command { Short: "Serve image or Helm chart bundles from an OCI registry", } - cmd.AddCommand(imagebundle.NewCommand(out)) + imageBundleCmd, _ := imagebundle.NewCommand(out) + cmd.AddCommand(imageBundleCmd) helmBundleCmd, _ := helmbundle.NewCommand(out) cmd.AddCommand(helmBundleCmd) diff --git a/cmd/mindthegap/utils/copy_file.go b/cmd/mindthegap/utils/copy_file.go new file mode 100644 index 00000000..31ebb6e7 --- /dev/null +++ b/cmd/mindthegap/utils/copy_file.go @@ -0,0 +1,37 @@ +// Copyright 2021 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "fmt" + "io" + "os" +) + +func CopyFile(src, dst string) error { + s, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file: %w", err) + } + defer s.Close() + + d, err := os.Create(dst) + if err != nil { + return fmt.Errorf("failed to create destination file: %w", err) + } + defer d.Close() + + // Copy the contents of the source file into the destination file + _, err = io.Copy(d, s) + if err != nil { + return fmt.Errorf("failed to copy file contents file: %w", err) + } + + // Return any errors that result from closing the destination file + // Will return nil if no errors occurred + if err := d.Close(); err != nil { + return fmt.Errorf("failed to close destination file: %w", err) + } + return nil +} diff --git a/config/images_config.go b/config/images_config.go index d4083dc8..9a1ef235 100644 --- a/config/images_config.go +++ b/config/images_config.go @@ -164,7 +164,7 @@ func ParseImagesConfigFile(configFile string) (ImagesConfig, error) { } named, nameErr := reference.ParseNamed(trimmedLine) if nameErr != nil { - return ImagesConfig{}, fmt.Errorf("failed to parse config file: %w", yamlParseErr) + return ImagesConfig{}, fmt.Errorf("failed to parse config file: %w", nameErr) } namedTagged, ok := named.(reference.NamedTagged) if !ok { diff --git a/make/go.mk b/make/go.mk index cb361063..23936f7f 100644 --- a/make/go.mk +++ b/make/go.mk @@ -77,7 +77,8 @@ E2E_FLAKE_ATTEMPTS ?= 1 .PHONY: e2e-test e2e-test: ## Runs e2e tests -e2e-test: install-tool.golang install-tool.ginkgo skopeo.build; $(info $(M) running e2e tests$(if $(E2E_FOCUS), matching "$(E2E_FOCUS)")) +e2e-test: install-tool.golang install-tool.ginkgo skopeo.build + $(info $(M) running e2e tests$(if $(E2E_LABEL), labelled "$(E2E_LABEL)")$(if $(E2E_FOCUS), matching "$(E2E_FOCUS)")) ginkgo run \ --r \ --race \ diff --git a/make/skopeo.mk b/make/skopeo.mk index dd945373..f1ce5fd8 100644 --- a/make/skopeo.mk +++ b/make/skopeo.mk @@ -22,7 +22,7 @@ skopeo.build.all: $(MAKE) --no-print-directory GOOS=windows GOARCH=arm64 skopeo.build .PHONY: skopeo/static/skopeo-$(GOOS)-$(GOARCH)$(if $(filter $(GOOS),windows),.exe) -skopeo/static/skopeo-$(GOOS)-$(GOARCH)$(if $(filter $(GOOS),windows),.exe): ; $(info $(M) Building skopeo for $(GOOS)/$(GOARCH)) +skopeo/static/skopeo-$(GOOS)-$(GOARCH)$(if $(filter $(GOOS),windows),.exe): ; $(info $(M) building skopeo for $(GOOS)/$(GOARCH)) mkdir -p $(dir $@) rm -f $(REPO_ROOT)/$@ cd skopeo-static && \ diff --git a/skopeo/skopeo.go b/skopeo/skopeo.go index bd1298b4..f40cc5a8 100644 --- a/skopeo/skopeo.go +++ b/skopeo/skopeo.go @@ -38,12 +38,30 @@ func DisableSrcTLSVerify() SkopeoOption { } } +func DisableTLSVerify() SkopeoOption { + return func() string { + return "--tls-verify=false" + } +} + +func CertDir(dir string) SkopeoOption { + return func() string { + return fmt.Sprintf("--cert-dir=%s", dir) + } +} + func DisableDestTLSVerify() SkopeoOption { return func() string { return "--dest-tls-verify=false" } } +func DestCertDir(dir string) SkopeoOption { + return func() string { + return fmt.Sprintf("--dest-cert-dir=%s", dir) + } +} + func AllImages() SkopeoOption { return func() string { return "--all" diff --git a/test/e2e/helmbundle/helpers/helpers.go b/test/e2e/helmbundle/helpers/helpers.go index 8e890899..bdaa96a3 100644 --- a/test/e2e/helmbundle/helpers/helpers.go +++ b/test/e2e/helmbundle/helpers/helpers.go @@ -193,7 +193,7 @@ func ValidateChartIsAvailable( d, err := h.GetChartFromRepo( helmTmpDir, "", - fmt.Sprintf("%s://%s:%v/charts/%s", helm.OCIScheme, addr, port, chartName), + fmt.Sprintf("%s://%s:%d/charts/%s", helm.OCIScheme, addr, port, chartName), chartVersion, []helm.ConfigOpt{helm.RegistryClientConfigOpt()}, pullOpts..., diff --git a/test/e2e/helmbundle/push_bundle_test.go b/test/e2e/helmbundle/push_bundle_test.go index a3732109..9a12726e 100644 --- a/test/e2e/helmbundle/push_bundle_test.go +++ b/test/e2e/helmbundle/push_bundle_test.go @@ -65,7 +65,7 @@ var _ = Describe("Push Bundle", func() { cmd.SetArgs([]string{ "--helm-bundle", bundleFile, - "--to-registry", fmt.Sprintf("localhost:%v/charts", port), + "--to-registry", fmt.Sprintf("localhost:%d/charts", port), "--to-registry-insecure-skip-tls-verify", }) @@ -127,7 +127,7 @@ var _ = Describe("Push Bundle", func() { cmd.SetArgs([]string{ "--helm-bundle", bundleFile, - "--to-registry", fmt.Sprintf("%s:%v/charts", ipAddr, port), + "--to-registry", fmt.Sprintf("%s:%d/charts", ipAddr, port), "--to-registry-insecure-skip-tls-verify", }) diff --git a/test/e2e/imagebundle/helpers/helpers.go b/test/e2e/imagebundle/helpers/helpers.go new file mode 100644 index 00000000..589f6498 --- /dev/null +++ b/test/e2e/imagebundle/helpers/helpers.go @@ -0,0 +1,217 @@ +// Copyright 2021 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build e2e +// +build e2e + +package helpers + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/distribution/distribution/v3/manifest/manifestlist" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/onsi/gomega/gstruct" + "github.com/spf13/cobra" + + "github.com/mesosphere/dkp-cli-runtime/core/output" + + createimagebundle "github.com/mesosphere/mindthegap/cmd/mindthegap/create/imagebundle" + "github.com/mesosphere/mindthegap/skopeo" +) + +func CreateBundle(t ginkgo.GinkgoTInterface, bundleFile, cfgFile string, platforms ...string) { + createBundleCmd := NewCommand(t, createimagebundle.NewCommand) + createBundleCmd.SetArgs([]string{ + "--output-file", bundleFile, + "--images-file", cfgFile, + }) + gomega.ExpectWithOffset(1, createBundleCmd.Execute()).To(gomega.Succeed()) +} + +func NewCommand( + t ginkgo.GinkgoTInterface, + newFn func(out output.Output) *cobra.Command, +) *cobra.Command { + t.Helper() + cmd := newFn(output.NewNonInteractiveShell(ginkgo.GinkgoWriter, ginkgo.GinkgoWriter, 10)) + cmd.SilenceUsage = true + return cmd +} + +// GetFirstNonLoopbackIP returns the first non-loopback IP of the current host. +func GetFirstNonLoopbackIP(t ginkgo.GinkgoTInterface) net.IP { + t.Helper() + addrs, err := net.InterfaceAddrs() + gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred()) + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP + } + } + } + ginkgo.Fail("no available non-loopback IP address") + return net.IP{} +} + +func WaitForTCPPort(t ginkgo.GinkgoTInterface, addr string, port int) { + t.Helper() + gomega.EventuallyWithOffset(1, func() error { + conn, err := net.DialTimeout( + "tcp", + net.JoinHostPort(addr, strconv.Itoa(port)), + 1*time.Second, + ) + if err != nil { + return err + } + defer conn.Close() + return nil + }, 5*time.Second).Should(gomega.Succeed()) +} + +func GenerateCertificateAndKeyWithIPSAN( + t ginkgo.GinkgoTInterface, destDir string, ipAddr net.IP, +) (caCertFile, caKeyFile, certFile, keyFile string) { + t.Helper() + + caPriv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + caTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"d2iq", "mindthegap", "e2e-ca"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(1 * time.Hour), + + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + caDerBytes, err := x509.CreateCertificate( + rand.Reader, + &caTemplate, + &caTemplate, + &caPriv.PublicKey, + caPriv, + ) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + + caCertFile = filepath.Join(destDir, "ca.crt") + caCertF, err := os.Create(caCertFile) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + defer caCertF.Close() + gomega.ExpectWithOffset(1, pem.Encode(caCertF, &pem.Block{Type: "CERTIFICATE", Bytes: caDerBytes})). + To(gomega.Succeed()) + + b, err := x509.MarshalECPrivateKey(caPriv) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + pemBlock := pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + caKeyFile = filepath.Join(destDir, "ca.key") + caKeyF, err := os.Create(caKeyFile) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + defer caKeyF.Close() + gomega.ExpectWithOffset(1, pem.Encode(caKeyF, &pemBlock)).To(gomega.Succeed()) + + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + template := x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + Organization: []string{"d2iq", "mindthegap", "e2e"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(1 * time.Hour), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + template.IPAddresses = append(template.IPAddresses, ipAddr) + + derBytes, err := x509.CreateCertificate( + rand.Reader, + &template, + &caTemplate, + &priv.PublicKey, + caPriv, + ) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + + certFile = filepath.Join(destDir, "tls.crt") + certF, err := os.Create(certFile) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + defer certF.Close() + gomega.ExpectWithOffset(1, pem.Encode(certF, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})). + To(gomega.Succeed()) + + b, err = x509.MarshalECPrivateKey(priv) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + pemBlock = pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + keyFile = filepath.Join(destDir, "tls.key") + keyF, err := os.Create(keyFile) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + defer keyF.Close() + gomega.ExpectWithOffset(1, pem.Encode(keyF, &pemBlock)).To(gomega.Succeed()) + + return caCertFile, caKeyFile, certFile, keyFile +} + +func ValidateImageIsAvailable( + t ginkgo.GinkgoTInterface, + addr string, + port int, + image, tag string, + platforms []manifestlist.PlatformSpec, + opts ...skopeo.SkopeoOption, +) { + t.Helper() + + r, cleanup := skopeo.NewRunner() + defer cleanup() + + ml, stdout, stderr, err := r.InspectManifest( + context.Background(), + fmt.Sprintf("docker://%s:%d/%s:%s", addr, port, image, tag), + append(opts, skopeo.Debug())..., + ) + + t.Log("skopeo stdout: ", string(stdout)) + t.Log("skopeo stderr: ", string(stderr)) + + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + gomega.ExpectWithOffset(1, ml.Manifests).To(gomega.HaveLen(len(platforms))) + + for _, p := range platforms { + gomega.ExpectWithOffset(1, ml.Manifests).To( + gomega.ContainElement( + gstruct.MatchFields( + gstruct.IgnoreExtras|gstruct.IgnoreMissing, + gstruct.Fields{ + "Platform": gomega.Equal(p), + }, + ), + ), + ) + } +} diff --git a/test/e2e/imagebundle/imagebundle_suite_test.go b/test/e2e/imagebundle/imagebundle_suite_test.go new file mode 100644 index 00000000..32c8f9a9 --- /dev/null +++ b/test/e2e/imagebundle/imagebundle_suite_test.go @@ -0,0 +1,16 @@ +// Copyright 2021 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package imagebundle_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestImagebundle(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Image Bundle Suite", Label("image", "imagebundle")) +} diff --git a/test/e2e/imagebundle/push_bundle_test.go b/test/e2e/imagebundle/push_bundle_test.go new file mode 100644 index 00000000..80a2165f --- /dev/null +++ b/test/e2e/imagebundle/push_bundle_test.go @@ -0,0 +1,178 @@ +// Copyright 2021 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build e2e + +package imagebundle_test + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + + "github.com/distribution/distribution/v3/manifest/manifestlist" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/phayes/freeport" + "github.com/spf13/cobra" + + pushimagebundle "github.com/mesosphere/mindthegap/cmd/mindthegap/push/imagebundle" + "github.com/mesosphere/mindthegap/cmd/mindthegap/utils" + "github.com/mesosphere/mindthegap/docker/registry" + "github.com/mesosphere/mindthegap/skopeo" + "github.com/mesosphere/mindthegap/test/e2e/imagebundle/helpers" +) + +var _ = Describe("Push Bundle", func() { + var ( + bundleFile string + cmd *cobra.Command + tmpDir string + ) + + BeforeEach(func() { + tmpDir = GinkgoT().TempDir() + + bundleFile = filepath.Join(tmpDir, "image-bundle.tar") + + cmd = helpers.NewCommand(GinkgoT(), pushimagebundle.NewCommand) + }) + + It("Without TLS", func() { + helpers.CreateBundle( + GinkgoT(), + bundleFile, + filepath.Join("testdata", "create-success.yaml"), + ) + + port, err := freeport.GetFreePort() + Expect(err).NotTo(HaveOccurred()) + reg, err := registry.NewRegistry(registry.Config{ + StorageDirectory: filepath.Join(tmpDir, "registry"), + Port: uint16(port), + }) + Expect(err).NotTo(HaveOccurred()) + + done := make(chan struct{}) + go func() { + defer GinkgoRecover() + + Expect(reg.ListenAndServe()).To(Succeed()) + + close(done) + }() + + helpers.WaitForTCPPort(GinkgoT(), "localhost", port) + + cmd.SetArgs([]string{ + "--image-bundle", bundleFile, + "--to-registry", fmt.Sprintf("localhost:%d", port), + "--to-registry-insecure-skip-tls-verify", + }) + + Expect(cmd.Execute()).To(Succeed()) + + helpers.ValidateImageIsAvailable( + GinkgoT(), + "localhost", + port, + "stefanprodan/podinfo", + "6.2.0", + []manifestlist.PlatformSpec{{ + OS: "linux", + Architecture: runtime.GOARCH, + }}, + skopeo.DisableTLSVerify(), + ) + + Expect(reg.Shutdown(context.Background())).To((Succeed())) + + Eventually(done).Should(BeClosed()) + }) + + It("With TLS", func() { + helpers.CreateBundle( + GinkgoT(), + bundleFile, + filepath.Join("testdata", "create-success.yaml"), + ) + + ipAddr := helpers.GetFirstNonLoopbackIP(GinkgoT()) + + tempCertDir := GinkgoT().TempDir() + caCertFile, _, certFile, keyFile := helpers.GenerateCertificateAndKeyWithIPSAN( + GinkgoT(), + tempCertDir, + ipAddr, + ) + + port, err := freeport.GetFreePort() + Expect(err).NotTo(HaveOccurred()) + reg, err := registry.NewRegistry(registry.Config{ + StorageDirectory: filepath.Join(tmpDir, "registry"), + Host: ipAddr.String(), + Port: uint16(port), + TLS: registry.TLS{ + Certificate: certFile, + Key: keyFile, + }, + }) + Expect(err).NotTo(HaveOccurred()) + + done := make(chan struct{}) + go func() { + defer GinkgoRecover() + + Expect(reg.ListenAndServe()).To(Succeed()) + + close(done) + }() + + helpers.WaitForTCPPort(GinkgoT(), ipAddr.String(), port) + + cmd.SetArgs([]string{ + "--image-bundle", bundleFile, + "--to-registry", fmt.Sprintf("%s:%d", ipAddr, port), + "--to-registry-ca-cert-file", caCertFile, + }) + + Expect(cmd.Execute()).To(Succeed()) + + tmpCACertDir := GinkgoT().TempDir() + err = utils.CopyFile( + caCertFile, + filepath.Join(tmpCACertDir, filepath.Base(caCertFile)), + ) + Expect(err).NotTo(HaveOccurred()) + + helpers.ValidateImageIsAvailable( + GinkgoT(), + ipAddr.String(), + port, + "stefanprodan/podinfo", + "6.2.0", + []manifestlist.PlatformSpec{{ + OS: "linux", + Architecture: runtime.GOARCH, + }}, + skopeo.CertDir(tmpCACertDir), + ) + + Expect(reg.Shutdown(context.Background())).To((Succeed())) + + Eventually(done).Should(BeClosed()) + }) + + It("Bundle does not exist", func() { + cmd.SetArgs([]string{ + "--image-bundle", bundleFile, + "--to-registry", "localhost:unused/charts", + "--to-registry-insecure-skip-tls-verify", + }) + + Expect( + cmd.Execute(), + ).To(MatchError(fmt.Sprintf("did find any matching files for %q", bundleFile))) + }) +}) diff --git a/test/e2e/imagebundle/serve_bundle_test.go b/test/e2e/imagebundle/serve_bundle_test.go new file mode 100644 index 00000000..f9dcbe3e --- /dev/null +++ b/test/e2e/imagebundle/serve_bundle_test.go @@ -0,0 +1,161 @@ +// Copyright 2021 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build e2e + +package imagebundle_test + +import ( + "fmt" + "path/filepath" + "runtime" + "strconv" + + "github.com/distribution/distribution/v3/manifest/manifestlist" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/phayes/freeport" + "github.com/spf13/cobra" + + "github.com/mesosphere/dkp-cli-runtime/core/output" + + serveimagebundle "github.com/mesosphere/mindthegap/cmd/mindthegap/serve/imagebundle" + "github.com/mesosphere/mindthegap/cmd/mindthegap/utils" + "github.com/mesosphere/mindthegap/skopeo" + "github.com/mesosphere/mindthegap/test/e2e/imagebundle/helpers" +) + +var _ = Describe("Serve Bundle", func() { + var ( + bundleFile string + cmd *cobra.Command + stopCh chan struct{} + ) + + BeforeEach(func() { + tmpDir := GinkgoT().TempDir() + + bundleFile = filepath.Join(tmpDir, "image-bundle.tar") + + cmd = helpers.NewCommand(GinkgoT(), func(out output.Output) *cobra.Command { + var c *cobra.Command + c, stopCh = serveimagebundle.NewCommand(out) + return c + }) + }) + + It("Without TLS", func() { + helpers.CreateBundle( + GinkgoT(), + bundleFile, + filepath.Join("testdata", "create-success.yaml"), + ) + + port, err := freeport.GetFreePort() + Expect(err).NotTo(HaveOccurred()) + cmd.SetArgs([]string{ + "--image-bundle", bundleFile, + "--listen-port", strconv.Itoa(port), + }) + + done := make(chan struct{}) + go func() { + defer GinkgoRecover() + + Expect(cmd.Execute()).To(Succeed()) + + close(done) + }() + + helpers.WaitForTCPPort(GinkgoT(), "localhost", port) + + helpers.ValidateImageIsAvailable( + GinkgoT(), + "localhost", + port, + "stefanprodan/podinfo", + "6.2.0", + []manifestlist.PlatformSpec{{ + OS: "linux", + Architecture: runtime.GOARCH, + }}, + skopeo.DisableTLSVerify(), + ) + + close(stopCh) + + Eventually(done).Should(BeClosed()) + }) + + It("With TLS", func() { + ipAddr := helpers.GetFirstNonLoopbackIP(GinkgoT()) + + tempCertDir := GinkgoT().TempDir() + caCertFile, _, certFile, keyFile := helpers.GenerateCertificateAndKeyWithIPSAN( + GinkgoT(), + tempCertDir, + ipAddr, + ) + + helpers.CreateBundle( + GinkgoT(), + bundleFile, + filepath.Join("testdata", "create-success.yaml"), + ) + + port, err := freeport.GetFreePort() + Expect(err).NotTo(HaveOccurred()) + cmd.SetArgs([]string{ + "--image-bundle", bundleFile, + "--listen-address", ipAddr.String(), + "--listen-port", strconv.Itoa(port), + "--tls-cert-file", certFile, + "--tls-private-key-file", keyFile, + }) + + done := make(chan struct{}) + go func() { + defer GinkgoRecover() + + Expect(cmd.Execute()).To(Succeed()) + + close(done) + }() + + helpers.WaitForTCPPort(GinkgoT(), ipAddr.String(), port) + + tmpCACertDir := GinkgoT().TempDir() + err = utils.CopyFile( + caCertFile, + filepath.Join(tmpCACertDir, filepath.Base(caCertFile)), + ) + Expect(err).NotTo(HaveOccurred()) + + helpers.ValidateImageIsAvailable( + GinkgoT(), + ipAddr.String(), + port, + "stefanprodan/podinfo", + "6.2.0", + []manifestlist.PlatformSpec{{ + OS: "linux", + Architecture: runtime.GOARCH, + }}, + skopeo.CertDir(tmpCACertDir), + ) + + close(stopCh) + + Eventually(done).Should(BeClosed()) + }) + + It("Bundle does not exist", func() { + cmd.SetArgs([]string{ + "--image-bundle", bundleFile, + }) + + Expect( + cmd.Execute(), + ).To(MatchError(fmt.Sprintf("did find any matching files for %q", bundleFile))) + }) +}) diff --git a/test/e2e/imagebundle/testdata/create-success.yaml b/test/e2e/imagebundle/testdata/create-success.yaml new file mode 100644 index 00000000..81667b3d --- /dev/null +++ b/test/e2e/imagebundle/testdata/create-success.yaml @@ -0,0 +1,8 @@ +# Copyright 2021 D2iQ, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +docker.io: + images: + stefanprodan/podinfo: + - 6.2.0 + - 6.1.0