diff --git a/.gitignore b/.gitignore index 82a3d66dc..0951b114f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,12 @@ .tool-versions /out .vscode + acceptance/testdata/*/**/container/cnb/lifecycle/* acceptance/testdata/*/**/container/docker-config/* + acceptance/testdata/exporter/container/cnb/run.toml acceptance/testdata/exporter/container/layers/*analyzed.toml acceptance/testdata/exporter/container/other_layers/*analyzed.toml + acceptance/testdata/restorer/container/layers/*analyzed.toml diff --git a/acceptance/builder_test.go b/acceptance/builder_test.go index e4371e666..4e5f7763f 100644 --- a/acceptance/builder_test.go +++ b/acceptance/builder_test.go @@ -27,7 +27,6 @@ var ( func TestBuilder(t *testing.T) { h.SkipIf(t, runtime.GOOS == "windows", "Builder acceptance tests are not yet supported on Windows") - h.SkipIf(t, runtime.GOARCH != "amd64", "Builder acceptance tests are not yet supported on non-amd64") info, err := h.DockerCli(t).Info(context.TODO()) h.AssertNil(t, err) @@ -39,6 +38,8 @@ func TestBuilder(t *testing.T) { builderDaemonArch = info.Architecture if builderDaemonArch == "x86_64" { builderDaemonArch = "amd64" + } else if builderDaemonArch == "aarch64" { + builderDaemonArch = "arm64" } h.MakeAndCopyLifecycle(t, builderDaemonOS, builderDaemonArch, builderBinaryDir) diff --git a/acceptance/detector_test.go b/acceptance/detector_test.go index 0681707d4..91b97a534 100644 --- a/acceptance/detector_test.go +++ b/acceptance/detector_test.go @@ -4,6 +4,7 @@ package acceptance import ( + "context" "fmt" "os" "os/exec" @@ -22,17 +23,29 @@ import ( ) var ( - detectDockerContext = filepath.Join("testdata", "detector") - detectorBinaryDir = filepath.Join("testdata", "detector", "container", "cnb", "lifecycle") - detectImage = "lifecycle/acceptance/detector" - userID = "1234" + detectDockerContext = filepath.Join("testdata", "detector") + detectorBinaryDir = filepath.Join("testdata", "detector", "container", "cnb", "lifecycle") + detectImage = "lifecycle/acceptance/detector" + userID = "1234" + detectorDaemonOS, detectorDaemonArch string ) func TestDetector(t *testing.T) { h.SkipIf(t, runtime.GOOS == "windows", "Detector acceptance tests are not yet supported on Windows") - h.SkipIf(t, runtime.GOARCH != "amd64", "Detector acceptance tests are not yet supported on non-amd64") - h.MakeAndCopyLifecycle(t, "linux", "amd64", detectorBinaryDir) + info, err := h.DockerCli(t).Info(context.TODO()) + h.AssertNil(t, err) + + detectorDaemonOS = info.OSType + detectorDaemonArch = info.Architecture + if detectorDaemonArch == "x86_64" { + detectorDaemonArch = "amd64" + } + if detectorDaemonArch == "aarch64" { + detectorDaemonArch = "arm64" + } + + h.MakeAndCopyLifecycle(t, detectorDaemonOS, detectorDaemonArch, detectorBinaryDir) h.DockerBuild(t, detectImage, detectDockerContext, diff --git a/acceptance/exporter_test.go b/acceptance/exporter_test.go index 3b0f84d14..e374dd7e5 100644 --- a/acceptance/exporter_test.go +++ b/acceptance/exporter_test.go @@ -246,6 +246,33 @@ func testExporterFunc(platformAPI string) func(t *testing.T, when spec.G, it spe }) }) + when("app using insecure registry", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.12"), "") + }) + + it("does an http request", func() { + var exportFlags []string + exportArgs := append([]string{ctrPath(exporterPath)}, exportFlags...) + exportedImageName = exportTest.RegRepoName("some-insecure-exported-image-" + h.RandString(10)) + exportArgs = append(exportArgs, exportedImageName) + insecureRegistry := "host.docker.internal/bar" + insecureAnalyzed := "/layers/analyzed_insecure.toml" + + _, _, err := h.DockerRunWithError(t, + exportImage, + h.WithFlags( + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_INSECURE_REGISTRIES="+insecureRegistry, + "--env", "CNB_ANALYZED_PATH="+insecureAnalyzed, + "--network", exportRegNetwork, + ), + h.WithArgs(exportArgs...), + ) + h.AssertStringContains(t, err.Error(), "http://host.docker.internal") + }) + }) + when("SOURCE_DATE_EPOCH is set", func() { it("Image CreatedAt is set to SOURCE_DATE_EPOCH", func() { h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.9"), "SOURCE_DATE_EPOCH support added in 0.9") diff --git a/acceptance/rebaser_test.go b/acceptance/rebaser_test.go new file mode 100644 index 000000000..d0dfa95ae --- /dev/null +++ b/acceptance/rebaser_test.go @@ -0,0 +1,59 @@ +//go:build acceptance +// +build acceptance + +package acceptance + +import ( + "path/filepath" + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/lifecycle/api" + h "github.com/buildpacks/lifecycle/testhelpers" +) + +var ( + rebaserTest *PhaseTest + rebaserPath string + rebaserImage string +) + +func TestRebaser(t *testing.T) { + testImageDockerContextFolder := filepath.Join("testdata", "rebaser") + rebaserTest = NewPhaseTest(t, "rebaser", testImageDockerContextFolder) + rebaserTest.Start(t, updateTOMLFixturesWithTestRegistry) + defer rebaserTest.Stop(t) + + rebaserImage = rebaserTest.testImageRef + rebaserPath = rebaserTest.containerBinaryPath + + for _, platformAPI := range api.Platform.Supported { + spec.Run(t, "acceptance-rebaser/"+platformAPI.String(), testRebaser(platformAPI.String()), spec.Sequential(), spec.Report(report.Terminal{})) + } +} + +func testRebaser(platformAPI string) func(t *testing.T, when spec.G, it spec.S) { + return func(t *testing.T, when spec.G, it spec.S) { + when("called with insecure registry flag", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.12"), "") + }) + it("should do an http request", func() { + insecureRegistry := "host.docker.internal" + rebaserOutputImageName := insecureRegistry + "/bar" + _, _, err := h.DockerRunWithError(t, + rebaserImage, + h.WithFlags( + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_INSECURE_REGISTRIES="+insecureRegistry, + ), + h.WithArgs(ctrPath(rebaserPath), rebaserOutputImageName), + ) + + h.AssertStringContains(t, err.Error(), "http://host.docker.internal") + }) + }) + } +} diff --git a/acceptance/restorer_test.go b/acceptance/restorer_test.go index a356967ef..8ec3ec5ad 100644 --- a/acceptance/restorer_test.go +++ b/acceptance/restorer_test.go @@ -34,7 +34,6 @@ var ( func TestRestorer(t *testing.T) { h.SkipIf(t, runtime.GOOS == "windows", "Restorer acceptance tests are not yet supported on Windows") - h.SkipIf(t, runtime.GOARCH != "amd64", "Restorer acceptance tests are not yet supported on non-amd64") testImageDockerContext := filepath.Join("testdata", "restorer") restoreTest = NewPhaseTest(t, "restorer", testImageDockerContext) @@ -106,6 +105,27 @@ func testRestorerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe h.AssertStringContains(t, output, "Restoring metadata for \"some-buildpack-id:launch-layer\"") }) + + when("restores app metadata using an insecure registry", func() { + it.Before(func() { + h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.12"), "") + }) + it("does an http request ", func() { + insecureRegistry := "host.docker.internal" + + _, _, err := h.DockerRunWithError(t, + restoreImage, + h.WithFlags(append( + dockerSocketMount, + "--env", "CNB_PLATFORM_API="+platformAPI, + "--env", "CNB_INSECURE_REGISTRIES="+insecureRegistry, + "--env", "CNB_BUILD_IMAGE="+insecureRegistry+"/bar", + )...), + ) + + h.AssertStringContains(t, err.Error(), "http://host.docker.internal") + }) + }) }) when("using cache-dir", func() { diff --git a/acceptance/testdata/exporter/container/layers/analyzed_insecure.toml b/acceptance/testdata/exporter/container/layers/analyzed_insecure.toml new file mode 100644 index 000000000..69678202a --- /dev/null +++ b/acceptance/testdata/exporter/container/layers/analyzed_insecure.toml @@ -0,0 +1,2 @@ +[run-image] + reference = "host.docker.internal/bar" \ No newline at end of file diff --git a/acceptance/testdata/rebaser/Dockerfile b/acceptance/testdata/rebaser/Dockerfile new file mode 100644 index 000000000..d35f68c89 --- /dev/null +++ b/acceptance/testdata/rebaser/Dockerfile @@ -0,0 +1,3 @@ +FROM ubuntu:bionic + +COPY ./container/ / diff --git a/acceptance/testdata/rebaser/Dockerfile.windows b/acceptance/testdata/rebaser/Dockerfile.windows new file mode 100644 index 000000000..2e0762721 --- /dev/null +++ b/acceptance/testdata/rebaser/Dockerfile.windows @@ -0,0 +1,10 @@ +FROM mcr.microsoft.com/windows/nanoserver:1809 +USER ContainerAdministrator + +COPY container / + +ENV CNB_USER_ID=1 + +ENV CNB_GROUP_ID=1 + +ENV CNB_PLATFORM_API=${cnb_platform_api} diff --git a/analyzer.go b/analyzer.go index 16f43fbb4..1321c85b4 100644 --- a/analyzer.go +++ b/analyzer.go @@ -20,7 +20,7 @@ type AnalyzerFactory struct { cacheHandler CacheHandler configHandler ConfigHandler imageHandler image.Handler - registryHandler RegistryHandler + registryHandler image.RegistryHandler } func NewAnalyzerFactory( @@ -29,7 +29,7 @@ func NewAnalyzerFactory( cacheHandler CacheHandler, configHandler ConfigHandler, imageHandler image.Handler, - registryHandler RegistryHandler, + registryHandler image.RegistryHandler, ) *AnalyzerFactory { return &AnalyzerFactory{ platformAPI: platformAPI, diff --git a/cmd/lifecycle/analyzer.go b/cmd/lifecycle/analyzer.go index e8ff5e8b1..070a1d169 100644 --- a/cmd/lifecycle/analyzer.go +++ b/cmd/lifecycle/analyzer.go @@ -30,6 +30,9 @@ func (a *analyzeCmd) DefineFlags() { cli.FlagStackPath(&a.StackPath) } switch { + case a.PlatformAPI.AtLeast("0.13"): + cli.FlagInsecureRegistries(&a.InsecureRegistries) + fallthrough case a.PlatformAPI.AtLeast("0.12"): cli.FlagLayoutDir(&a.LayoutDir) cli.FlagUseLayout(&a.UseLayout) @@ -99,8 +102,8 @@ func (a *analyzeCmd) Exec() error { &cmd.BuildpackAPIVerifier{}, NewCacheHandler(a.keychain), lifecycle.NewConfigHandler(), - image.NewHandler(a.docker, a.keychain, a.LayoutDir, a.UseLayout), - NewRegistryHandler(a.keychain), + image.NewHandler(a.docker, a.keychain, a.LayoutDir, a.UseLayout, a.InsecureRegistries), + image.NewRegistryHandler(a.keychain, a.InsecureRegistries), ) analyzer, err := factory.NewAnalyzer(a.AdditionalTags, a.CacheImageRef, a.LaunchCacheDir, a.LayersDir, a.OutputImageRef, a.PreviousImageRef, a.RunImageRef, a.SkipLayers, cmd.DefaultLogger) if err != nil { diff --git a/cmd/lifecycle/cli/flags.go b/cmd/lifecycle/cli/flags.go index a469a60bd..411390352 100644 --- a/cmd/lifecycle/cli/flags.go +++ b/cmd/lifecycle/cli/flags.go @@ -168,6 +168,11 @@ func FlagForceRebase(force *bool) { flagSet.BoolVar(force, "force", *force, "execute rebase even if operation is unsafe") } +// FlagInsecureRegistries sets insecure-registry parameter as available +func FlagInsecureRegistries(insecureRegistries *str.Slice) { + flagSet.Var(insecureRegistries, "insecure-registry", "insecure registries") +} + // deprecated func DeprecatedFlagRunImage(deprecatedRunImage *string) { diff --git a/cmd/lifecycle/creator.go b/cmd/lifecycle/creator.go index e67cb334e..531da1d31 100644 --- a/cmd/lifecycle/creator.go +++ b/cmd/lifecycle/creator.go @@ -35,6 +35,11 @@ func (c *createCmd) DefineFlags() { cli.FlagUseLayout(&c.UseLayout) cli.FlagRunPath(&c.RunPath) } + + if c.PlatformAPI.AtLeast("0.13") { + cli.FlagInsecureRegistries(&c.InsecureRegistries) + } + if c.PlatformAPI.AtLeast("0.11") { cli.FlagBuildConfigDir(&c.BuildConfigDir) cli.FlagLauncherSBOMDir(&c.LauncherSBOMDir) @@ -124,8 +129,8 @@ func (c *createCmd) Exec() error { &cmd.BuildpackAPIVerifier{}, NewCacheHandler(c.keychain), lifecycle.NewConfigHandler(), - image.NewHandler(c.docker, c.keychain, c.LayoutDir, c.UseLayout), - NewRegistryHandler(c.keychain), + image.NewHandler(c.docker, c.keychain, c.LayoutDir, c.UseLayout, c.InsecureRegistries), + image.NewRegistryHandler(c.keychain, c.InsecureRegistries), ) analyzer, err := analyzerFactory.NewAnalyzer(c.AdditionalTags, c.CacheImageRef, c.LaunchCacheDir, c.LayersDir, c.OutputImageRef, c.PreviousImageRef, c.RunImageRef, c.SkipLayers, cmd.DefaultLogger) if err != nil { diff --git a/cmd/lifecycle/exporter.go b/cmd/lifecycle/exporter.go index f45d2be8c..bc9006e9e 100644 --- a/cmd/lifecycle/exporter.go +++ b/cmd/lifecycle/exporter.go @@ -57,6 +57,11 @@ func (e *exportCmd) DefineFlags() { } else { cli.FlagStackPath(&e.StackPath) } + + if e.PlatformAPI.AtLeast("0.13") { + cli.FlagInsecureRegistries(&e.InsecureRegistries) + } + if e.PlatformAPI.AtLeast("0.11") { cli.FlagLauncherSBOMDir(&e.LauncherSBOMDir) } @@ -340,6 +345,7 @@ func (e *exportCmd) initRemoteAppImage(analyzedMD files.Analyzed) (imgutil.Image var opts = []remote.ImageOption{ remote.FromBaseImage(e.RunImageRef), } + if e.supportsRunImageExtension() { extendedConfig, err := e.getExtendedConfig(analyzedMD.RunImage) if err != nil { @@ -355,6 +361,8 @@ func (e *exportCmd) initRemoteAppImage(analyzedMD files.Analyzed) (imgutil.Image opts = append(opts, remote.WithHistory()) } + opts = append(opts, image.GetInsecureOptions(e.InsecureRegistries)...) + if analyzedMD.PreviousImageRef() != "" { cmd.DefaultLogger.Infof("Reusing layers from image '%s'", analyzedMD.PreviousImageRef()) opts = append(opts, remote.WithPreviousImage(analyzedMD.PreviousImageRef())) diff --git a/cmd/lifecycle/main.go b/cmd/lifecycle/main.go index 5632025a7..fdcfdc33e 100644 --- a/cmd/lifecycle/main.go +++ b/cmd/lifecycle/main.go @@ -5,7 +5,6 @@ import ( "path/filepath" "strings" - "github.com/buildpacks/imgutil/remote" "github.com/google/go-containerregistry/pkg/authn" "github.com/pkg/errors" @@ -112,60 +111,6 @@ func (ch *DefaultCacheHandler) InitCache(cacheImageRef string, cacheDir string, return cacheStore, nil } -type DefaultRegistryHandler struct { - keychain authn.Keychain -} - -func NewRegistryHandler(keychain authn.Keychain) *DefaultRegistryHandler { - return &DefaultRegistryHandler{ - keychain: keychain, - } -} - -func (rv *DefaultRegistryHandler) EnsureReadAccess(imageRefs ...string) error { - for _, imageRef := range imageRefs { - if err := verifyReadAccess(imageRef, rv.keychain); err != nil { - return err - } - } - return nil -} - -func (rv *DefaultRegistryHandler) EnsureWriteAccess(imageRefs ...string) error { - for _, imageRef := range imageRefs { - if err := verifyReadWriteAccess(imageRef, rv.keychain); err != nil { - return err - } - } - return nil -} - -func verifyReadAccess(imageRef string, keychain authn.Keychain) error { - if imageRef == "" { - return nil - } - img, _ := remote.NewImage(imageRef, keychain) - canRead, err := img.CheckReadAccess() - if !canRead { - cmd.DefaultLogger.Debugf("Error checking read access: %s", err) - return errors.Errorf("ensure registry read access to %s", imageRef) - } - return nil -} - -func verifyReadWriteAccess(imageRef string, keychain authn.Keychain) error { - if imageRef == "" { - return nil - } - img, _ := remote.NewImage(imageRef, keychain) - canReadWrite, err := img.CheckReadWriteAccess() - if !canReadWrite { - cmd.DefaultLogger.Debugf("Error checking read/write access: %s", err) - return errors.Errorf("ensure registry read/write access to %s", imageRef) - } - return nil -} - // helpers func initCache(cacheImageTag, cacheDir string, keychain authn.Keychain, deletionEnabled bool) (lifecycle.Cache, error) { diff --git a/cmd/lifecycle/rebaser.go b/cmd/lifecycle/rebaser.go index 2e22beee8..448beb924 100644 --- a/cmd/lifecycle/rebaser.go +++ b/cmd/lifecycle/rebaser.go @@ -47,6 +47,10 @@ func (r *rebaseCmd) DefineFlags() { if r.PlatformAPI.AtLeast("0.12") { cli.FlagForceRebase(&r.ForceRebase) } + + if r.PlatformAPI.AtLeast("0.13") { + cli.FlagInsecureRegistries(&r.InsecureRegistries) + } } // Args validates arguments and flags, and fills in default values. @@ -111,10 +115,13 @@ func (r *rebaseCmd) Exec() error { local.FromBaseImage(r.RunImageRef), ) } else { + var opts []remote.ImageOption + opts = append(opts, append(image.GetInsecureOptions(r.InsecureRegistries), remote.FromBaseImage(r.RunImageRef))...) + newBaseImage, err = remote.NewImage( r.RunImageRef, r.keychain, - remote.FromBaseImage(r.RunImageRef), + opts..., ) } if err != nil || !newBaseImage.Found() { @@ -163,10 +170,17 @@ func (r *rebaseCmd) setAppImage() error { if err != nil { return err } + + var opts = []remote.ImageOption{ + remote.FromBaseImage(targetImageRef), + } + + opts = append(opts, image.GetInsecureOptions(r.InsecureRegistries)...) + r.appImage, err = remote.NewImage( targetImageRef, keychain, - remote.FromBaseImage(targetImageRef), + opts..., ) } if err != nil || !r.appImage.Found() { diff --git a/cmd/lifecycle/restorer.go b/cmd/lifecycle/restorer.go index dde3604fc..536d61cd5 100644 --- a/cmd/lifecycle/restorer.go +++ b/cmd/lifecycle/restorer.go @@ -43,9 +43,15 @@ func (r *restoreCmd) DefineFlags() { cli.FlagUseLayout(&r.UseLayout) cli.FlagLayoutDir(&r.LayoutDir) } + + if r.PlatformAPI.AtLeast("0.13") { + cli.FlagInsecureRegistries(&r.InsecureRegistries) + } + if r.PlatformAPI.AtLeast("0.10") { cli.FlagBuildImage(&r.BuildImageRef) } + cli.FlagAnalyzedPath(&r.AnalyzedPath) cli.FlagCacheDir(&r.CacheDir) cli.FlagCacheImage(&r.CacheImageRef) @@ -127,7 +133,7 @@ func (r *restoreCmd) Exec() error { } } else if r.supportsTargetData() && needsUpdating(analyzedMD.RunImage) { cmd.DefaultLogger.Debugf("Updating run image info in analyzed metadata...") - h := image.NewHandler(r.docker, r.keychain, r.LayoutDir, r.UseLayout) + h := image.NewHandler(r.docker, r.keychain, r.LayoutDir, r.UseLayout, r.InsecureRegistries) runImage, err = h.InitImage(runImageName) if err != nil || !runImage.Found() { return cmd.FailErr(err, fmt.Sprintf("pull run image %s", runImageName)) @@ -217,8 +223,12 @@ func (r *restoreCmd) pullSparse(imageRef string) (imgutil.Image, error) { if err := os.MkdirAll(baseCacheDir, 0755); err != nil { return nil, fmt.Errorf("failed to create cache directory: %w", err) } + + var opts []remote.ImageOption + opts = append(opts, append(image.GetInsecureOptions(r.InsecureRegistries), remote.FromBaseImage(imageRef))...) + // get remote image - remoteImage, err := remote.NewImage(imageRef, r.keychain, remote.FromBaseImage(imageRef)) + remoteImage, err := remote.NewImage(imageRef, r.keychain, opts...) if err != nil { return nil, fmt.Errorf("failed to initialize remote image: %w", err) } diff --git a/go.mod b/go.mod index cdf4fdf8b..29ccc7544 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ require ( github.com/GoogleContainerTools/kaniko v1.15.0 github.com/apex/log v1.9.0 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230906235100-ae4cac8b496c - github.com/buildpacks/imgutil v0.0.0-20230918203216-a995227559a3 + github.com/buildpacks/imgutil v0.0.0-20230919143643-4ec9360d5f02 github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 github.com/containerd/containerd v1.7.6 github.com/docker/docker v24.0.6+incompatible diff --git a/go.sum b/go.sum index 451dc5bf1..1389c198c 100644 --- a/go.sum +++ b/go.sum @@ -122,8 +122,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/buildpacks/imgutil v0.0.0-20230918203216-a995227559a3 h1:p4spj0vuCHFopXMPm/7IgC0nXe2fBjJxOmpM5C1mUvE= -github.com/buildpacks/imgutil v0.0.0-20230918203216-a995227559a3/go.mod h1:Ade+4Q1OovFw6Zdzd+/UVaqWptZSlpnZ8n/vlkgS7M8= +github.com/buildpacks/imgutil v0.0.0-20230919143643-4ec9360d5f02 h1:Ac/FoFzAhz34zIDvrC3ivShQgoywg/HrA+Kkcb13Mr4= +github.com/buildpacks/imgutil v0.0.0-20230919143643-4ec9360d5f02/go.mod h1:Ade+4Q1OovFw6Zdzd+/UVaqWptZSlpnZ8n/vlkgS7M8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/handlers.go b/handlers.go index 6610f20b7..2c4b3c3e0 100644 --- a/handlers.go +++ b/handlers.go @@ -26,12 +26,6 @@ type DirStore interface { //go:generate mockgen -package testmock -destination testmock/image_handler.go github.com/buildpacks/lifecycle/image Handler -//go:generate mockgen -package testmock -destination testmock/registry_handler.go github.com/buildpacks/lifecycle RegistryHandler -type RegistryHandler interface { - EnsureReadAccess(imageRefs ...string) error - EnsureWriteAccess(imageRefs ...string) error -} - //go:generate mockgen -package testmock -destination testmock/buildpack_api_verifier.go github.com/buildpacks/lifecycle BuildpackAPIVerifier type BuildpackAPIVerifier interface { VerifyBuildpackAPI(kind, name, requested string, logger log.Logger) error diff --git a/image/handler.go b/image/handler.go index 26137ca91..f579f5e78 100644 --- a/image/handler.go +++ b/image/handler.go @@ -1,3 +1,4 @@ +// Package image implements functions for manipulating images package image import ( @@ -16,7 +17,7 @@ type Handler interface { // - WHEN a docker client is provided then it returns a LocalHandler // - WHEN an auth.Keychain is provided then it returns a RemoteHandler // - Otherwise nil is returned -func NewHandler(docker client.CommonAPIClient, keychain authn.Keychain, layoutDir string, useLayout bool) Handler { +func NewHandler(docker client.CommonAPIClient, keychain authn.Keychain, layoutDir string, useLayout bool, insecureRegistries []string) Handler { if layoutDir != "" && useLayout { return &LayoutHandler{ layoutDir: layoutDir, @@ -29,7 +30,8 @@ func NewHandler(docker client.CommonAPIClient, keychain authn.Keychain, layoutDi } if keychain != nil { return &RemoteHandler{ - keychain: keychain, + keychain: keychain, + insecureRegistries: insecureRegistries, } } return nil diff --git a/image/handler_test.go b/image/handler_test.go new file mode 100644 index 000000000..f97ff4f67 --- /dev/null +++ b/image/handler_test.go @@ -0,0 +1,68 @@ +package image + +import ( + "testing" + + "github.com/docker/docker/client" + + "github.com/golang/mock/gomock" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + h "github.com/buildpacks/lifecycle/testhelpers" + testmockauth "github.com/buildpacks/lifecycle/testmock/auth" +) + +//go:generate mockgen -package testmockauth -destination ../testmock/auth/mock_keychain.go github.com/google/go-containerregistry/pkg/authn Keychain + +func TestHandler(t *testing.T) { + spec.Run(t, "ImageHandler", testHandler, spec.Sequential(), spec.Report(report.Terminal{})) +} + +func testHandler(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockKeychain *testmockauth.MockKeychain + dockerClient client.CommonAPIClient + ) + + it.Before(func() { + mockController = gomock.NewController(t) + mockKeychain = testmockauth.NewMockKeychain(mockController) + dockerClient = h.DockerCli(t) + }) + + it.After(func() { + mockController.Finish() + }) + + when("Remote handler", func() { + it("returns a remote handler", func() { + handler := NewHandler(nil, mockKeychain, "", false, []string{"insecure-registry"}) + + _, ok := handler.(*RemoteHandler) + + h.AssertEq(t, ok, true) + }) + }) + + when("Local handler", func() { + it("returns a local handler", func() { + handler := NewHandler(dockerClient, mockKeychain, "", false, []string{}) + + _, ok := handler.(*LocalHandler) + + h.AssertEq(t, ok, true) + }) + }) + + when("Layout handler", func() { + it("returns a layout handler", func() { + handler := NewHandler(nil, mockKeychain, "random-dir", true, []string{}) + + _, ok := handler.(*LayoutHandler) + + h.AssertEq(t, ok, true) + }) + }) +} diff --git a/image/layout_handler_test.go b/image/layout_handler_test.go index 70b35d8b8..404d29f9d 100644 --- a/image/layout_handler_test.go +++ b/image/layout_handler_test.go @@ -21,7 +21,7 @@ const ( ) func TestLayoutImageHandler(t *testing.T) { - spec.Run(t, "VerifyAPIs", testLayoutImageHandler, spec.Sequential(), spec.Report(report.Terminal{})) + spec.Run(t, "layoutImageHandler", testLayoutImageHandler, spec.Sequential(), spec.Report(report.Terminal{})) } func testLayoutImageHandler(t *testing.T, when spec.G, it spec.S) { @@ -36,7 +36,7 @@ func testLayoutImageHandler(t *testing.T, when spec.G, it spec.S) { when("layout handler", func() { it.Before(func() { layoutDir = "layout-repo" - imageHandler = image.NewHandler(nil, nil, layoutDir, true) + imageHandler = image.NewHandler(nil, nil, layoutDir, true, []string{}) h.AssertNotNil(t, imageHandler) }) diff --git a/image/local_handler_test.go b/image/local_handler_test.go index 190264e32..c856cf901 100644 --- a/image/local_handler_test.go +++ b/image/local_handler_test.go @@ -12,7 +12,7 @@ import ( ) func TestLocalImageHandler(t *testing.T) { - spec.Run(t, "VerifyAPIs", testLocalImageHandler, spec.Sequential(), spec.Report(report.Terminal{})) + spec.Run(t, "localImageHandler", testLocalImageHandler, spec.Sequential(), spec.Report(report.Terminal{})) } func testLocalImageHandler(t *testing.T, when spec.G, it spec.S) { @@ -24,7 +24,7 @@ func testLocalImageHandler(t *testing.T, when spec.G, it spec.S) { when("Local handler", func() { it.Before(func() { dockerClient = h.DockerCli(t) - imageHandler = image.NewHandler(dockerClient, nil, "", false) + imageHandler = image.NewHandler(dockerClient, nil, "", false, []string{}) h.AssertNotNil(t, imageHandler) }) diff --git a/image/registry_handler.go b/image/registry_handler.go new file mode 100644 index 000000000..83ef8578d --- /dev/null +++ b/image/registry_handler.go @@ -0,0 +1,93 @@ +package image + +import ( + "github.com/buildpacks/imgutil/remote" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/pkg/errors" + + "github.com/buildpacks/lifecycle/cmd" +) + +// RegistryHandler takes care of the registry settings and checks +// +//go:generate mockgen -package testmock -destination testmock/registry_handler.go github.com/buildpacks/lifecycle RegistryHandler +type RegistryHandler interface { + EnsureReadAccess(imageRefs ...string) error + EnsureWriteAccess(imageRefs ...string) error +} + +// DefaultRegistryHandler is the struct that implements the RegistryHandler methods +type DefaultRegistryHandler struct { + keychain authn.Keychain + insecureRegistry []string +} + +// NewRegistryHandler creates a new DefaultRegistryHandler +func NewRegistryHandler(keychain authn.Keychain, insecureRegistries []string) *DefaultRegistryHandler { + return &DefaultRegistryHandler{ + keychain: keychain, + insecureRegistry: insecureRegistries, + } +} + +// EnsureReadAccess ensures that we can read from the registry +func (rv *DefaultRegistryHandler) EnsureReadAccess(imageRefs ...string) error { + for _, imageRef := range imageRefs { + if err := verifyReadAccess(imageRef, rv.keychain, GetInsecureOptions(rv.insecureRegistry)); err != nil { + return err + } + } + return nil +} + +// EnsureWriteAccess ensures that we can write to the registry +func (rv *DefaultRegistryHandler) EnsureWriteAccess(imageRefs ...string) error { + for _, imageRef := range imageRefs { + if err := verifyReadWriteAccess(imageRef, rv.keychain, GetInsecureOptions(rv.insecureRegistry)); err != nil { + return err + } + } + return nil +} + +// GetInsecureOptions returns a list of WithRegistrySetting imageOptions matching the specified imageRef prefix +/* +TODO: This is a temporary solution in order to get insecure registries in other components too +TODO: Ideally we should fix the `imgutil.options` struct visibility in order to mock and test the `remote.WithRegistrySetting` +TODO: function correctly and use the RegistryHandler everywhere it is needed. +*/ +func GetInsecureOptions(insecureRegistries []string) []remote.ImageOption { + var opts []remote.ImageOption + for _, insecureRegistry := range insecureRegistries { + opts = append(opts, remote.WithRegistrySetting(insecureRegistry, true)) + } + return opts +} + +func verifyReadAccess(imageRef string, keychain authn.Keychain, opts []remote.ImageOption) error { + if imageRef == "" { + return nil + } + + img, _ := remote.NewImage(imageRef, keychain, opts...) + canRead, err := img.CheckReadAccess() + if !canRead { + cmd.DefaultLogger.Debugf("Error checking read access: %s", err) + return errors.Errorf("ensure registry read access to %s", imageRef) + } + return nil +} + +func verifyReadWriteAccess(imageRef string, keychain authn.Keychain, opts []remote.ImageOption) error { + if imageRef == "" { + return nil + } + + img, _ := remote.NewImage(imageRef, keychain, opts...) + canReadWrite, err := img.CheckReadWriteAccess() + if !canReadWrite { + cmd.DefaultLogger.Debugf("Error checking read/write access: %s", err) + return errors.Errorf("ensure registry read/write access to %s", imageRef) + } + return nil +} diff --git a/image/registry_handler_test.go b/image/registry_handler_test.go new file mode 100644 index 000000000..0b4faaa63 --- /dev/null +++ b/image/registry_handler_test.go @@ -0,0 +1,41 @@ +package image + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + h "github.com/buildpacks/lifecycle/testhelpers" +) + +func TestRegistryHandler(t *testing.T) { + spec.Run(t, "RegistryHandler", testRegistryHandler, spec.Parallel(), spec.Report(report.Terminal{})) +} +func testRegistryHandler(t *testing.T, when spec.G, it spec.S) { + when("insecure registry", func() { + it("returns WithRegistrySetting options for the domain specified", func() { + registryOptions := GetInsecureOptions([]string{"host.docker.internal"}) + + h.AssertEq(t, len(registryOptions), 1) + }) + + it("returns multiple WithRegistrySetting options for the domains specified", func() { + registryOptions := GetInsecureOptions([]string{"host.docker.internal", "another.docker.internal"}) + + h.AssertEq(t, len(registryOptions), 2) + }) + + it("returns empty options if any domain hasn't been specified", func() { + options := GetInsecureOptions(nil) + + h.AssertEq(t, len(options), 0) + }) + + it("returns empty options if an empty list of insecure registries has been passed", func() { + options := GetInsecureOptions([]string{}) + + h.AssertEq(t, len(options), 0) + }) + }) +} diff --git a/image/remote_handler.go b/image/remote_handler.go index 187c7e97b..519acc1f4 100644 --- a/image/remote_handler.go +++ b/image/remote_handler.go @@ -9,7 +9,8 @@ import ( const RemoteKind = "remote" type RemoteHandler struct { - keychain authn.Keychain + keychain authn.Keychain + insecureRegistries []string } func (h *RemoteHandler) InitImage(imageRef string) (imgutil.Image, error) { @@ -17,10 +18,16 @@ func (h *RemoteHandler) InitImage(imageRef string) (imgutil.Image, error) { return nil, nil } + options := []remote.ImageOption{ + remote.FromBaseImage(imageRef), + } + + options = append(options, GetInsecureOptions(h.insecureRegistries)...) + return remote.NewImage( imageRef, h.keychain, - remote.FromBaseImage(imageRef), + options..., ) } diff --git a/image/remote_handler_test.go b/image/remote_handler_test.go index e14400ccc..a8b785a87 100644 --- a/image/remote_handler_test.go +++ b/image/remote_handler_test.go @@ -12,19 +12,21 @@ import ( ) func TestRemoteImageHandler(t *testing.T) { - spec.Run(t, "VerifyAPIs", testRemoteImageHandler, spec.Sequential(), spec.Report(report.Terminal{})) + spec.Run(t, "remoteImageHandler", testRemoteImageHandler, spec.Sequential(), spec.Report(report.Terminal{})) } func testRemoteImageHandler(t *testing.T, when spec.G, it spec.S) { var ( - imageHandler image.Handler - auth authn.Keychain + imageHandler image.Handler + auth authn.Keychain + insecureRegistries []string ) when("Remote handler", func() { it.Before(func() { auth = authn.DefaultKeychain - imageHandler = image.NewHandler(nil, auth, "", false) + insecureRegistries = []string{"host.docker.internal", "another.host.internal"} + imageHandler = image.NewHandler(nil, auth, "", false, insecureRegistries) h.AssertNotNil(t, imageHandler) }) @@ -37,18 +39,32 @@ func testRemoteImageHandler(t *testing.T, when spec.G, it spec.S) { when("#InitImage", func() { when("no image reference is provided", func() { it("nil image is return", func() { - image, err := imageHandler.InitImage("") + newImage, err := imageHandler.InitImage("") h.AssertNil(t, err) - h.AssertNil(t, image) + h.AssertNil(t, newImage) }) }) when("image reference is provided", func() { it("creates an image", func() { - image, err := imageHandler.InitImage("busybox") + newImage, err := imageHandler.InitImage("busybox") h.AssertNil(t, err) - h.AssertNotNil(t, image) - h.AssertEq(t, image.Name(), "busybox") + h.AssertNotNil(t, newImage) + h.AssertEq(t, newImage.Name(), "busybox") + }) + it("creates an image using insecure registries", func() { + _, err := imageHandler.InitImage("host.docker.internal/bar") + h.AssertNotNil(t, err) + h.AssertError(t, err, "http://") + + _, err = imageHandler.InitImage("another.host.internal/bar") + h.AssertNotNil(t, err) + h.AssertError(t, err, "http://") + + _, err = imageHandler.InitImage("my.secure.domain/bar") + h.AssertNotNil(t, err) + h.AssertError(t, err, "https://") + h.AssertStringDoesNotContain(t, err.Error(), "http://") }) }) diff --git a/platform/defaults.go b/platform/defaults.go index cfd1d60c9..fa3931f75 100644 --- a/platform/defaults.go +++ b/platform/defaults.go @@ -56,6 +56,9 @@ const ( // via a credential helper, or via the `CNB_REGISTRY_AUTH` environment variable. See [auth.DefaultKeychain] for further information. const EnvUseDaemon = "CNB_USE_DAEMON" +// EnvInsecureRegistries configures the lifecycle to export the application to a remote "insecure" registry. +const EnvInsecureRegistries = "CNB_INSECURE_REGISTRIES" + // ## Provided to handle inputs and outputs in OCI layout format // The lifecycle can be configured to read the input images like `run-image` or `previous-image` in OCI layout format instead of from a diff --git a/platform/lifecycle_inputs.go b/platform/lifecycle_inputs.go index 81c8e4409..faf2e4fe5 100644 --- a/platform/lifecycle_inputs.go +++ b/platform/lifecycle_inputs.go @@ -59,6 +59,7 @@ type LifecycleInputs struct { UseLayout bool AdditionalTags str.Slice // str.Slice satisfies the `Value` interface required by the `flag` package KanikoCacheTTL time.Duration + InsecureRegistries str.Slice } const PlaceholderLayers = "" @@ -85,11 +86,12 @@ func NewLifecycleInputs(platformAPI *api.Version) *LifecycleInputs { inputs := &LifecycleInputs{ // Operator config - LogLevel: envOrDefault(EnvLogLevel, DefaultLogLevel), - PlatformAPI: platformAPI, - ExtendKind: envOrDefault(EnvExtendKind, DefaultExtendKind), - UseDaemon: boolEnv(EnvUseDaemon), - UseLayout: boolEnv(EnvUseLayout), + LogLevel: envOrDefault(EnvLogLevel, DefaultLogLevel), + PlatformAPI: platformAPI, + ExtendKind: envOrDefault(EnvExtendKind, DefaultExtendKind), + UseDaemon: boolEnv(EnvUseDaemon), + InsecureRegistries: sliceEnv(EnvInsecureRegistries), + UseLayout: boolEnv(EnvUseLayout), // Provided by the base image @@ -235,6 +237,14 @@ func envOrDefault(key string, defaultVal string) string { return defaultVal } +func sliceEnv(k string) str.Slice { + envVal := os.Getenv(k) + if envVal != "" { + return strings.Split(envVal, ",") + } + return str.Slice(nil) +} + func intEnv(k string) int { v := os.Getenv(k) d, err := strconv.Atoi(v) diff --git a/platform/lifecycle_inputs_test.go b/platform/lifecycle_inputs_test.go index f857c5827..f3f53f750 100644 --- a/platform/lifecycle_inputs_test.go +++ b/platform/lifecycle_inputs_test.go @@ -61,6 +61,7 @@ func testLifecycleInputs(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, inputs.UID, 0) h.AssertEq(t, inputs.UseDaemon, false) h.AssertEq(t, inputs.UseLayout, false) + h.AssertEq(t, inputs.InsecureRegistries, str.Slice(nil)) }) when("env vars are set", func() { @@ -96,6 +97,7 @@ func testLifecycleInputs(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, os.Setenv(platform.EnvUID, "1234")) h.AssertNil(t, os.Setenv(platform.EnvUseDaemon, "true")) h.AssertNil(t, os.Setenv(platform.EnvUseLayout, "true")) + h.AssertNil(t, os.Setenv(platform.EnvInsecureRegistries, "some-insecure-registry,another-insecure-registry,just-another-registry")) }) it.After(func() { @@ -130,6 +132,7 @@ func testLifecycleInputs(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, os.Unsetenv(platform.EnvUID)) h.AssertNil(t, os.Unsetenv(platform.EnvUseDaemon)) h.AssertNil(t, os.Unsetenv(platform.EnvUseLayout)) + h.AssertNil(t, os.Unsetenv(platform.EnvInsecureRegistries)) }) it("returns lifecycle inputs with env values fill in", func() { @@ -172,6 +175,11 @@ func testLifecycleInputs(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, inputs.UID, 1234) h.AssertEq(t, inputs.UseDaemon, true) h.AssertEq(t, inputs.UseLayout, true) + h.AssertEq(t, inputs.InsecureRegistries, str.Slice{ + "some-insecure-registry", + "another-insecure-registry", + "just-another-registry", + }) }) }) diff --git a/testhelpers/docker.go b/testhelpers/docker.go index 8a0a3ba7c..0779d5294 100644 --- a/testhelpers/docker.go +++ b/testhelpers/docker.go @@ -49,6 +49,13 @@ func DockerRun(t *testing.T, image string, ops ...DockerCmdOp) string { return Run(t, exec.Command("docker", append([]string{"run", "--rm"}, args...)...)) // #nosec G204 } +// DockerRunWithError allows to run docker command that might fail, reporting the error back to the caller +func DockerRunWithError(t *testing.T, image string, ops ...DockerCmdOp) (string, int, error) { + t.Helper() + args := formatArgs([]string{image}, ops...) + return RunE(exec.Command("docker", append([]string{"run", "--rm"}, args...)...)) // #nosec G204 +} + func DockerRunWithCombinedOutput(t *testing.T, image string, ops ...DockerCmdOp) string { t.Helper() args := formatArgs([]string{image}, ops...) diff --git a/testmock/auth/mock_keychain.go b/testmock/auth/mock_keychain.go new file mode 100644 index 000000000..411143ec7 --- /dev/null +++ b/testmock/auth/mock_keychain.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/google/go-containerregistry/pkg/authn (interfaces: Keychain) + +// Package testmockauth is a generated GoMock package. +package testmockauth + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + authn "github.com/google/go-containerregistry/pkg/authn" +) + +// MockKeychain is a mock of Keychain interface. +type MockKeychain struct { + ctrl *gomock.Controller + recorder *MockKeychainMockRecorder +} + +// MockKeychainMockRecorder is the mock recorder for MockKeychain. +type MockKeychainMockRecorder struct { + mock *MockKeychain +} + +// NewMockKeychain creates a new mock instance. +func NewMockKeychain(ctrl *gomock.Controller) *MockKeychain { + mock := &MockKeychain{ctrl: ctrl} + mock.recorder = &MockKeychainMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKeychain) EXPECT() *MockKeychainMockRecorder { + return m.recorder +} + +// Resolve mocks base method. +func (m *MockKeychain) Resolve(arg0 authn.Resource) (authn.Authenticator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Resolve", arg0) + ret0, _ := ret[0].(authn.Authenticator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Resolve indicates an expected call of Resolve. +func (mr *MockKeychainMockRecorder) Resolve(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockKeychain)(nil).Resolve), arg0) +}