diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index e7f52815d0..f837988e09 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -38,6 +38,7 @@ import ( "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/archive" "github.com/buildpacks/pack/pkg/cache" + "github.com/buildpacks/pack/pkg/logging" h "github.com/buildpacks/pack/testhelpers" ) @@ -1162,8 +1163,9 @@ func testAcceptance( ref, err := name.ParseReference(repoName, name.WeakValidation) assert.Nil(err) cacheImage := cache.NewImageCache(ref, dockerCli) - buildCacheVolume := cache.NewVolumeCache(ref, cache.CacheInfo{}, "build", dockerCli) - launchCacheVolume := cache.NewVolumeCache(ref, cache.CacheInfo{}, "launch", dockerCli) + logger := logging.NewSimpleLogger(&bytes.Buffer{}) + buildCacheVolume, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "build", dockerCli, logger) + launchCacheVolume, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "launch", dockerCli, logger) cacheImage.Clear(context.TODO()) buildCacheVolume.Clear(context.TODO()) launchCacheVolume.Clear(context.TODO()) @@ -1282,8 +1284,9 @@ func testAcceptance( ref, err := name.ParseReference(repoName, name.WeakValidation) assert.Nil(err) cacheImage := cache.NewImageCache(ref, dockerCli) - buildCacheVolume := cache.NewVolumeCache(ref, cache.CacheInfo{}, "build", dockerCli) - launchCacheVolume := cache.NewVolumeCache(ref, cache.CacheInfo{}, "launch", dockerCli) + logger := logging.NewSimpleLogger(&bytes.Buffer{}) + buildCacheVolume, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "build", dockerCli, logger) + launchCacheVolume, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "launch", dockerCli, logger) cacheImage.Clear(context.TODO()) buildCacheVolume.Clear(context.TODO()) launchCacheVolume.Clear(context.TODO()) @@ -1627,6 +1630,7 @@ func testAcceptance( it.Before(func() { h.SkipIf(t, os.Getenv("DOCKER_HOST") != "", "cannot mount volume when DOCKER_HOST is set") + h.SkipIf(t, imageManager.HostOS() == "windows", "These tests are broken on Windows Containers on Windows when not using the creator; see https://github.com/buildpacks/pack/issues/2147") if imageManager.HostOS() == "windows" { volumeRoot = `c:\` @@ -3167,8 +3171,9 @@ include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ] imageManager.CleanupImages(origID, repoName, runBefore) ref, err := name.ParseReference(repoName, name.WeakValidation) assert.Nil(err) - buildCacheVolume := cache.NewVolumeCache(ref, cache.CacheInfo{}, "build", dockerCli) - launchCacheVolume := cache.NewVolumeCache(ref, cache.CacheInfo{}, "launch", dockerCli) + logger := logging.NewSimpleLogger(&bytes.Buffer{}) + buildCacheVolume, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "build", dockerCli, logger) + launchCacheVolume, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "launch", dockerCli, logger) assert.Succeeds(buildCacheVolume.Clear(context.TODO())) assert.Succeeds(launchCacheVolume.Clear(context.TODO())) }) diff --git a/go.mod b/go.mod index 21f3365e4e..7a352b64e6 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/buildpacks/pack require ( github.com/BurntSushi/toml v1.3.2 + github.com/GoogleContainerTools/kaniko v1.22.0 github.com/Masterminds/semver v1.5.0 github.com/Microsoft/go-winio v0.6.2 github.com/apex/log v1.9.0 @@ -108,7 +109,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/buildkit v0.13.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect diff --git a/go.sum b/go.sum index 0c6572a040..99c97160e5 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/GoogleContainerTools/kaniko v1.22.0 h1:WIL8Wuc+lQW8sv1R+zOZsCy4lQtTzrVJ76K2VMkB++0= +github.com/GoogleContainerTools/kaniko v1.22.0/go.mod h1:Kki7uX+HlskobmD7PRrGZvL0S9Aejf8kzfzoQUv68pQ= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -272,8 +274,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e h1:Qa6dnn8DlasdXRnacluu8HzPts0S1I9zvvUPDbBnXFI= github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e/go.mod h1:waEya8ee1Ro/lgxpVhkJI4BVASzkm3UZqkx/cFJiYHM= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/buildkit v0.13.2 h1:nXNszM4qD9E7QtG7bFWPnDI1teUQFQglBzon/IU3SzI= github.com/moby/buildkit v0.13.2/go.mod h1:2cyVOv9NoHM7arphK9ZfHIWKn9YVZRFd1wXB8kKmEzY= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -533,8 +535,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= @@ -561,5 +563,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= diff --git a/internal/build/docker.go b/internal/build/docker.go index 6fd7d54fe9..64db04d5df 100644 --- a/internal/build/docker.go +++ b/internal/build/docker.go @@ -23,6 +23,8 @@ type DockerClient interface { ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error) ContainerRemove(ctx context.Context, container string, options containertypes.RemoveOptions) error CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error + NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) + NetworkRemove(ctx context.Context, network string) error } var _ DockerClient = dockerClient.CommonAPIClient(nil) diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index 2f017dd70f..1911c0eed2 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -12,6 +12,7 @@ import ( "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/auth" "github.com/buildpacks/lifecycle/platform/files" + "github.com/docker/docker/api/types" "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" "golang.org/x/sync/errgroup" @@ -165,6 +166,7 @@ func (l *LifecycleExecution) PrevImageName() string { func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseFactoryCreator) error { phaseFactory := phaseFactoryCreator(l) + var buildCache Cache if l.opts.CacheImage != "" || (l.opts.Cache.Build.Format == cache.CacheImage) { cacheImageName := l.opts.CacheImage @@ -179,7 +181,11 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF } else { switch l.opts.Cache.Build.Format { case cache.CacheVolume: - buildCache = cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Build, "build", l.docker) + var err error + buildCache, err = cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Build, "build", l.docker, l.logger) + if err != nil { + return err + } l.logger.Debugf("Using build cache volume %s", style.Symbol(buildCache.Name())) case cache.CacheBind: buildCache = cache.NewBindCache(l.opts.Cache.Build, l.docker) @@ -194,7 +200,33 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF l.logger.Debugf("Build cache %s cleared", style.Symbol(buildCache.Name())) } - launchCache := cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Launch, "launch", l.docker) + launchCache, err := cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Launch, "launch", l.docker, l.logger) + if err != nil { + return err + } + + if l.opts.Network == "" { + // start an ephemeral bridge network + driver := "bridge" + if l.os == "windows" { + driver = "nat" + } + networkName := fmt.Sprintf("pack.local/network/%x", randString(10)) + resp, err := l.docker.NetworkCreate(ctx, networkName, types.NetworkCreate{ + Driver: driver, + }) + if err != nil { + return fmt.Errorf("failed to create ephemeral %s network: %w", driver, err) + } + defer func() { + _ = l.docker.NetworkRemove(ctx, networkName) + }() + l.logger.Debugf("Created ephemeral bridge network %s with ID %s", networkName, resp.ID) + if resp.Warning != "" { + l.logger.Warn(resp.Warning) + } + l.opts.Network = networkName + } if !l.opts.UseCreator { if l.platformAPI.LessThan("0.7") { @@ -224,7 +256,10 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF // lifecycle 0.17.0 (introduces support for Platform API 0.12) and above will ensure that // this volume is owned by the CNB user, // and hence the restorer (after dropping privileges) will be able to write to it. - kanikoCache = cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Kaniko, "kaniko", l.docker) + kanikoCache, err = cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Kaniko, "kaniko", l.docker, l.logger) + if err != nil { + return err + } } else { switch { case buildCache.Type() == cache.Volume: @@ -236,7 +271,10 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF return fmt.Errorf("build cache must be volume cache when building with extensions") default: // The kaniko cache is unused, so it doesn't matter that it's not usable. - kanikoCache = cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Kaniko, "kaniko", l.docker) + kanikoCache, err = cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Kaniko, "kaniko", l.docker, l.logger) + if err != nil { + return err + } } } diff --git a/internal/build/lifecycle_execution_test.go b/internal/build/lifecycle_execution_test.go index 6aa126a25a..4ab8077122 100644 --- a/internal/build/lifecycle_execution_test.go +++ b/internal/build/lifecycle_execution_test.go @@ -16,6 +16,7 @@ import ( ifakes "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/platform/files" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" @@ -275,7 +276,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakeBuilder *fakes.FakeBuilder outBuf bytes.Buffer logger *logging.LogWithWriters - docker *client.Client + docker *fakeDockerClient fakeTermui *fakes.FakeTermui ) @@ -289,7 +290,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakeBuilder, err = fakes.NewFakeBuilder(fakes.WithSupportedPlatformAPIs([]*api.Version{api.MustParse("0.3")})) h.AssertNil(t, err) logger = logging.NewLogWithWriters(&outBuf, &outBuf) - docker, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) + docker = &fakeDockerClient{} h.AssertNil(t, err) fakePhaseFactory = fakes.NewFakePhaseFactory() }) @@ -780,6 +781,46 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) }) + when("network is not provided", func() { + it("creates an ephemeral bridge network", func() { + beforeNetworks := func() int { + networks, err := docker.NetworkList(context.Background(), types.NetworkListOptions{}) + h.AssertNil(t, err) + return len(networks) + }() + + opts := build.LifecycleOptions{ + Image: imageName, + Builder: fakeBuilder, + Termui: fakeTermui, + } + + lifecycle, err := build.NewLifecycleExecution(logger, docker, "some-temp-dir", opts) + h.AssertNil(t, err) + + err = lifecycle.Run(context.Background(), func(execution *build.LifecycleExecution) build.PhaseFactory { + return fakePhaseFactory + }) + h.AssertNil(t, err) + + for _, entry := range fakePhaseFactory.NewCalledWithProvider { + h.AssertContains(t, string(entry.HostConfig().NetworkMode), "pack.local/network/") + h.AssertEq(t, entry.HostConfig().NetworkMode.IsDefault(), false) + h.AssertEq(t, entry.HostConfig().NetworkMode.IsHost(), false) + h.AssertEq(t, entry.HostConfig().NetworkMode.IsNone(), false) + h.AssertEq(t, entry.HostConfig().NetworkMode.IsPrivate(), true) + h.AssertEq(t, entry.HostConfig().NetworkMode.IsUserDefined(), true) + } + + afterNetworks := func() int { + networks, err := docker.NetworkList(context.Background(), types.NetworkListOptions{}) + h.AssertNil(t, err) + return len(networks) + }() + h.AssertEq(t, beforeNetworks, afterNetworks) + }) + }) + when("Error cases", func() { when("passed invalid", func() { it("fails for cache-image", func() { @@ -2657,6 +2698,26 @@ func (f *fakeImageFetcher) fetchRunImage(name string) error { return nil } +type fakeDockerClient struct { + nNetworks int + build.DockerClient +} + +func (f *fakeDockerClient) NetworkList(ctx context.Context, opts types.NetworkListOptions) ([]types.NetworkResource, error) { + ret := make([]types.NetworkResource, f.nNetworks) + return ret, nil +} + +func (f *fakeDockerClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) { + f.nNetworks++ + return types.NetworkCreateResponse{}, nil +} + +func (f *fakeDockerClient) NetworkRemove(ctx context.Context, network string) error { + f.nNetworks-- + return nil +} + func newTestLifecycleExecErr(t *testing.T, logVerbose bool, tmpDir string, ops ...func(*build.LifecycleOptions)) (*build.LifecycleExecution, error) { docker, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) h.AssertNil(t, err) diff --git a/internal/build/phase_config_provider.go b/internal/build/phase_config_provider.go index 15434c7a70..4226928d93 100644 --- a/internal/build/phase_config_provider.go +++ b/internal/build/phase_config_provider.go @@ -34,9 +34,14 @@ type PhaseConfigProvider struct { } func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops ...PhaseConfigProviderOperation) *PhaseConfigProvider { + hostConf := new(container.HostConfig) + hostConf.UsernsMode = "host" + if lifecycleExec.os != "windows" { + hostConf.SecurityOpt = []string{"no-new-privileges=true"} + } provider := &PhaseConfigProvider{ ctrConf: new(container.Config), - hostConf: new(container.HostConfig), + hostConf: hostConf, name: name, os: lifecycleExec.os, infoWriter: logging.GetWriterForLevel(lifecycleExec.logger, logging.InfoLevel), diff --git a/internal/build/phase_config_provider_test.go b/internal/build/phase_config_provider_test.go index ed54a6ea1e..93cce0ed52 100644 --- a/internal/build/phase_config_provider_test.go +++ b/internal/build/phase_config_provider_test.go @@ -59,6 +59,8 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { h.AssertSliceContainsMatch(t, phaseConfigProvider.HostConfig().Binds, "pack-app-.*:/workspace") h.AssertEq(t, phaseConfigProvider.HostConfig().Isolation, container.IsolationEmpty) + h.AssertEq(t, phaseConfigProvider.HostConfig().UsernsMode, container.UsernsMode("host")) + h.AssertSliceContains(t, phaseConfigProvider.HostConfig().SecurityOpt, "no-new-privileges=true") }) when("building for Windows", func() { @@ -72,6 +74,7 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle) h.AssertEq(t, phaseConfigProvider.HostConfig().Isolation, container.IsolationProcess) + h.AssertSliceNotContains(t, phaseConfigProvider.HostConfig().SecurityOpt, "no-new-privileges=true") }) }) diff --git a/internal/commands/build.go b/internal/commands/build.go index 50ccebeabd..2118a0444e 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -1,6 +1,7 @@ package commands import ( + "fmt" "os" "path/filepath" "strconv" @@ -143,6 +144,11 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob lifecycleImage = ref.Name() } + err = isForbiddenTag(cfg, inputImageName.Name(), lifecycleImage, builder) + if err != nil { + return errors.Wrapf(err, "forbidden image name") + } + var gid = -1 if cmd.Flags().Changed("gid") { gid = flags.GID @@ -385,3 +391,57 @@ func parseProjectToml(appPath, descriptorPath string, logger logging.Logger) (pr descriptor, err := project.ReadProjectDescriptor(actualPath, logger) return descriptor, actualPath, err } + +func isForbiddenTag(cfg config.Config, input, lifecycle, builder string) error { + inputImage, err := name.ParseReference(input) + if err != nil { + return errors.Wrapf(err, "invalid image name %s", input) + } + + if builder != "" { + builderImage, err := name.ParseReference(builder) + if err != nil { + return errors.Wrapf(err, "parsing builder image %s", builder) + } + if inputImage.Context().RepositoryStr() == builderImage.Context().RepositoryStr() { + return fmt.Errorf("name must not match builder image name") + } + } + + if lifecycle != "" { + lifecycleImage, err := name.ParseReference(lifecycle) + if err != nil { + return errors.Wrapf(err, "parsing lifecycle image %s", lifecycle) + } + if inputImage.Context().RepositoryStr() == lifecycleImage.Context().RepositoryStr() { + return fmt.Errorf("name must not match lifecycle image name") + } + } + + trustedBuilders := getTrustedBuilders(cfg) + for _, trustedBuilder := range trustedBuilders { + builder, err := name.ParseReference(trustedBuilder) + if err != nil { + return err + } + if inputImage.Context().RepositoryStr() == builder.Context().RepositoryStr() { + return fmt.Errorf("name must not match trusted builder name") + } + } + + if inputImage.Context().RepositoryStr() == config.DefaultLifecycleImageRepo { + return fmt.Errorf("name must not match default lifecycle image name") + } + + if cfg.DefaultBuilder != "" { + defaultBuilderImage, err := name.ParseReference(cfg.DefaultBuilder) + if err != nil { + return errors.Wrapf(err, "parsing default builder %s", cfg.DefaultBuilder) + } + if inputImage.Context().RepositoryStr() == defaultBuilderImage.Context().RegistryStr() { + return fmt.Errorf("name must not match default builder image name") + } + } + + return nil +} diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index 9201e505b9..4e4d03c69b 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -140,6 +140,33 @@ func testBuildCommand(t *testing.T, when spec.G, it spec.S) { }) }) + when("the image name matches a builder name", func() { + it("refuses to build", func() { + logger.WantVerbose(true) + command.SetArgs([]string{"heroku/builder:test", "--builder", "heroku/builder:24"}) + h.AssertNotNil(t, command.Execute()) + h.AssertContains(t, outBuf.String(), "name must not match builder image name") + }) + }) + + when("the image name matches a trusted-builder name", func() { + it("refuses to build", func() { + logger.WantVerbose(true) + command.SetArgs([]string{"heroku/builder:test", "--builder", "test", "--trust-builder"}) + h.AssertNotNil(t, command.Execute()) + h.AssertContains(t, outBuf.String(), "name must not match trusted builder name") + }) + }) + + when("the image name matches a lifecycle image name", func() { + it("refuses to build", func() { + logger.WantVerbose(true) + command.SetArgs([]string{"buildpacksio/lifecycle:test", "--builder", "test", "--trust-builder"}) + h.AssertNotNil(t, command.Execute()) + h.AssertContains(t, outBuf.String(), "name must not match default lifecycle image name") + }) + }) + when("the builder is not trusted", func() { it("warns the user that the builder is untrusted", func() { mockClient.EXPECT(). @@ -803,13 +830,9 @@ builder = "my-builder" when("previous-image flag is provided", func() { when("image is invalid", func() { it("error must be thrown", func() { - mockClient.EXPECT(). - Build(gomock.Any(), EqBuildOptionsWithPreviousImage("previous-image")). - Return(errors.New("")) - command.SetArgs([]string{"--builder", "my-builder", "/x@/y/?!z", "--previous-image", "previous-image"}) err := command.Execute() - h.AssertError(t, err, "failed to build") + h.AssertError(t, err, "forbidden image name") }) }) diff --git a/internal/commands/config_trusted_builder.go b/internal/commands/config_trusted_builder.go index e8d32bed58..4ccc469249 100644 --- a/internal/commands/config_trusted_builder.go +++ b/internal/commands/config_trusted_builder.go @@ -98,9 +98,7 @@ func removeTrustedBuilder(args []string, logger logging.Logger, cfg config.Confi return nil } -func listTrustedBuilders(args []string, logger logging.Logger, cfg config.Config) { - logger.Info("Trusted Builders:") - +func getTrustedBuilders(cfg config.Config) []string { var trustedBuilders []string for _, knownBuilder := range bldr.KnownBuilders { if knownBuilder.Trusted { @@ -113,7 +111,13 @@ func listTrustedBuilders(args []string, logger logging.Logger, cfg config.Config } sort.Strings(trustedBuilders) + return trustedBuilders +} + +func listTrustedBuilders(args []string, logger logging.Logger, cfg config.Config) { + logger.Info("Trusted Builders:") + trustedBuilders := getTrustedBuilders(cfg) for _, builder := range trustedBuilders { logger.Infof(" %s", builder) } diff --git a/internal/config/config.go b/internal/config/config.go index de370ef727..e15da68f03 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,6 +25,10 @@ type Config struct { LayoutRepositoryDir string `toml:"layout-repo-dir,omitempty"` } +type VolumeConfig struct { + VolumeKeys map[string]string `toml:"volume-keys,omitempty"` +} + type Registry struct { Name string `toml:"name"` Type string `toml:"type"` @@ -58,6 +62,14 @@ func DefaultConfigPath() (string, error) { return filepath.Join(home, "config.toml"), nil } +func DefaultVolumeKeysPath() (string, error) { + home, err := PackHome() + if err != nil { + return "", errors.Wrap(err, "getting pack home") + } + return filepath.Join(home, "volume-keys.toml"), nil +} + func PackHome() (string, error) { packHome := os.Getenv("PACK_HOME") if packHome == "" { @@ -79,7 +91,16 @@ func Read(path string) (Config, error) { return cfg, nil } -func Write(cfg Config, path string) error { +func ReadVolumeKeys(path string) (VolumeConfig, error) { + cfg := VolumeConfig{} + _, err := toml.DecodeFile(path, &cfg) + if err != nil && !os.IsNotExist(err) { + return VolumeConfig{}, errors.Wrapf(err, "failed to read config file at path %s", path) + } + return cfg, nil +} + +func Write(cfg interface{}, path string) error { if err := MkdirAll(filepath.Dir(path)); err != nil { return err } diff --git a/pkg/cache/volume_cache.go b/pkg/cache/volume_cache.go index b274d3d3ff..f414bcd903 100644 --- a/pkg/cache/volume_cache.go +++ b/pkg/cache/volume_cache.go @@ -2,25 +2,36 @@ package cache import ( "context" + "crypto/rand" "crypto/sha256" "fmt" + "os" "strings" + "github.com/GoogleContainerTools/kaniko/pkg/util/proc" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/name" + "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/paths" + "github.com/buildpacks/pack/pkg/logging" ) +const EnvVolumeKey = "PACK_VOLUME_KEY" + type VolumeCache struct { docker DockerClient volume string } -func NewVolumeCache(imageRef name.Reference, cacheType CacheInfo, suffix string, dockerClient DockerClient) *VolumeCache { +func NewVolumeCache(imageRef name.Reference, cacheType CacheInfo, suffix string, dockerClient DockerClient, logger logging.Logger) (*VolumeCache, error) { var volumeName string if cacheType.Source == "" { - sum := sha256.Sum256([]byte(imageRef.Name())) + volumeKey, err := getVolumeKey(imageRef, logger) + if err != nil { + return nil, err + } + sum := sha256.Sum256([]byte(imageRef.Name() + volumeKey)) vol := paths.FilterReservedNames(fmt.Sprintf("%s-%x", sanitizedRef(imageRef), sum[:6])) volumeName = fmt.Sprintf("pack-cache-%s.%s", vol, suffix) } else { @@ -30,7 +41,66 @@ func NewVolumeCache(imageRef name.Reference, cacheType CacheInfo, suffix string, return &VolumeCache{ volume: volumeName, docker: dockerClient, + }, nil +} + +func getVolumeKey(imageRef name.Reference, logger logging.Logger) (string, error) { + var foundKey string + + // first, look for key in env + + foundKey = os.Getenv(EnvVolumeKey) + if foundKey != "" { + return foundKey, nil + } + + // then, look for key in existing config + + volumeKeysPath, err := config.DefaultVolumeKeysPath() + if err != nil { + return "", err + } + cfg, err := config.ReadVolumeKeys(volumeKeysPath) + if err != nil { + return "", err + } + + foundKey = cfg.VolumeKeys[imageRef.Name()] + if foundKey != "" { + return foundKey, nil + } + + // finally, create new key and store it in config + + // if we're running in a container, we should log a warning + // so that we don't always re-create the cache + if RunningInContainer() { + logger.Warnf("%s is unset; set this environment variable to a secret value to avoid creating a new volume cache on every build", EnvVolumeKey) + } + + newKey := randString(20) + if cfg.VolumeKeys == nil { + cfg.VolumeKeys = make(map[string]string) + } + cfg.VolumeKeys[imageRef.Name()] = newKey + if err = config.Write(cfg, volumeKeysPath); err != nil { + return "", err + } + + return newKey, nil +} + +// Returns a string iwith lowercase a-z, of length n +func randString(n int) string { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + panic(err) } + for i := range b { + b[i] = 'a' + (b[i] % 26) + } + return string(b) } func (c *VolumeCache) Name() string { @@ -56,3 +126,7 @@ func sanitizedRef(ref name.Reference) string { result = strings.ReplaceAll(result, "/", "_") return fmt.Sprintf("%s_%s", result, ref.Identifier()) } + +var RunningInContainer = func() bool { + return proc.GetContainerRuntime(0, 0) != proc.RuntimeNotFound +} diff --git a/pkg/cache/volume_cache_test.go b/pkg/cache/volume_cache_test.go index 5ae1e14a92..685f34d2be 100644 --- a/pkg/cache/volume_cache_test.go +++ b/pkg/cache/volume_cache_test.go @@ -1,11 +1,16 @@ package cache_test import ( + "bytes" "context" + "os" + "path/filepath" "strings" "testing" + "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/pkg/cache" + "github.com/buildpacks/pack/pkg/logging" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/volume" @@ -24,23 +29,29 @@ func TestVolumeCache(t *testing.T) { color.Disable(true) defer color.Disable(false) - spec.Run(t, "VolumeCache", testCache, spec.Parallel(), spec.Report(report.Terminal{})) + spec.Run(t, "VolumeCache", testCache, spec.Sequential(), spec.Report(report.Terminal{})) } func testCache(t *testing.T, when spec.G, it spec.S) { - var dockerClient client.CommonAPIClient + var ( + dockerClient client.CommonAPIClient + outBuf bytes.Buffer + logger logging.Logger + ) it.Before(func() { var err error dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) h.AssertNil(t, err) + logger = logging.NewSimpleLogger(&outBuf) }) + when("#NewVolumeCache", func() { when("volume cache name is empty", func() { it("adds suffix to calculated name", func() { ref, err := name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) if !strings.HasSuffix(subject.Name(), ".some-suffix") { t.Fatalf("Calculated volume name '%s' should end with '.some-suffix'", subject.Name()) } @@ -50,8 +61,8 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) - expected := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) + expected, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) if subject.Name() != expected.Name() { t.Fatalf("The same repo name should result in the same volume") } @@ -61,11 +72,11 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("my/repo:other-tag", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) ref, err = name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - notExpected := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + notExpected, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) if subject.Name() == notExpected.Name() { t.Fatalf("Different image tags should result in different volumes") } @@ -75,11 +86,11 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("registry.com/my/repo:other-tag", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) ref, err = name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - notExpected := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + notExpected, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) if subject.Name() == notExpected.Name() { t.Fatalf("Different image registries should result in different volumes") } @@ -89,11 +100,11 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("my/repo:latest", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) ref, err = name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - expected := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + expected, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) h.AssertEq(t, subject.Name(), expected.Name()) }) @@ -101,11 +112,11 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("index.docker.io/my/repo", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) ref, err = name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - expected := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + expected, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) h.AssertEq(t, subject.Name(), expected.Name()) }) @@ -113,11 +124,95 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("myregistryhost:5000/fedora/httpd:version1.0", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) h.AssertContains(t, subject.Name(), "fedora_httpd_version1.0") h.AssertTrue(t, names.RestrictedNamePattern.MatchString(subject.Name())) }) + + when("PACK_VOLUME_KEY", func() { + when("is set", func() { + it.After(func() { + h.AssertNil(t, os.Unsetenv("PACK_VOLUME_KEY")) + }) + + it("uses it to construct the volume name", func() { + ref, err := name.ParseReference("my/repo:some-tag", name.WeakValidation) + h.AssertNil(t, err) + + nameFromNewKey, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) // sources a new key + h.AssertNil(t, os.Setenv("PACK_VOLUME_KEY", "some-volume-key")) + nameFromEnvKey, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) // sources key from env + h.AssertNotEq(t, nameFromNewKey.Name(), nameFromEnvKey.Name()) + }) + }) + + when("is unset", func() { + var tmpPackHome string + + it.Before(func() { + var err error + tmpPackHome, err = os.MkdirTemp("", "") + h.AssertNil(t, err) + h.AssertNil(t, os.Setenv("PACK_HOME", tmpPackHome)) + }) + + it.After(func() { + h.AssertNil(t, os.RemoveAll(tmpPackHome)) + }) + + when("~/.pack/volume-keys.toml contains key for repo name", func() { + it("sources the key from ~/.pack/volume-keys.toml", func() { + ref, err := name.ParseReference("my/repo:some-tag", name.WeakValidation) + h.AssertNil(t, err) + + nameFromNewKey, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) // sources a new key + + cfgContents := ` +[volume-keys] +"index.docker.io/my/repo:some-tag" = "SOME_VOLUME_KEY" +` + h.AssertNil(t, os.WriteFile(filepath.Join(tmpPackHome, "volume-keys.toml"), []byte(cfgContents), 0755)) // overrides the key that was set + + nameFromConfigKey, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) // sources key from config + h.AssertNotEq(t, nameFromNewKey.Name(), nameFromConfigKey.Name()) + }) + }) + + when("~/.pack/volume-keys.toml missing key for repo name", func() { + it("generates a new key and saves it to ~/.pack/volume-keys.toml", func() { + ref, err := name.ParseReference("my/repo:some-tag", name.WeakValidation) + h.AssertNil(t, err) + + nameFromNewKey, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) // sources a new key + nameFromConfigKey, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) // sources same key from config + h.AssertEq(t, nameFromNewKey.Name(), nameFromConfigKey.Name()) + + cfg, err := config.ReadVolumeKeys(filepath.Join(tmpPackHome, "volume-keys.toml")) + h.AssertNil(t, err) + h.AssertNotNil(t, cfg.VolumeKeys["index.docker.io/my/repo:some-tag"]) + }) + + when("containerized pack", func() { + it.Before(func() { + cache.RunningInContainer = func() bool { + return true + } + }) + + it("logs a warning", func() { + ref, err := name.ParseReference("my/repo:some-tag", name.WeakValidation) + h.AssertNil(t, err) + + _, _ = cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) // sources a new key + _, _ = cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) // sources same key from config + h.AssertContains(t, outBuf.String(), "PACK_VOLUME_KEY is unset; set this environment variable to a secret value to avoid creating a new volume cache on every build") + h.AssertEq(t, strings.Count(outBuf.String(), "PACK_VOLUME_KEY is unset"), 1) // the second call to NewVolumeCache reads from the config + }) + }) + }) + }) + }) }) when("volume cache name is not empty", func() { @@ -131,7 +226,7 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cacheInfo, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cacheInfo, "some-suffix", dockerClient, logger) if volumeName != subject.Name() { t.Fatalf("Volume name '%s' should be same as the name specified '%s'", subject.Name(), volumeName) @@ -142,9 +237,9 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cacheInfo, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cacheInfo, "some-suffix", dockerClient, logger) - expected := cache.NewVolumeCache(ref, cacheInfo, "some-suffix", dockerClient) + expected, _ := cache.NewVolumeCache(ref, cacheInfo, "some-suffix", dockerClient, logger) if subject.Name() != expected.Name() { t.Fatalf("The same repo name should result in the same volume") } @@ -154,11 +249,11 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("registry.com/my/repo:other-tag", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) ref, err = name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - notExpected := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + notExpected, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) if subject.Name() == notExpected.Name() { t.Fatalf("Different image registries should result in different volumes") } @@ -168,11 +263,11 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("my/repo:latest", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) ref, err = name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - expected := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + expected, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) h.AssertEq(t, subject.Name(), expected.Name()) }) @@ -180,11 +275,11 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("index.docker.io/my/repo", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) ref, err = name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - expected := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + expected, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) h.AssertEq(t, subject.Name(), expected.Name()) }) @@ -192,7 +287,7 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference("myregistryhost:5000/fedora/httpd:version1.0", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) h.AssertContains(t, subject.Name(), "fedora_httpd_version1.0") h.AssertTrue(t, names.RestrictedNamePattern.MatchString(subject.Name())) @@ -217,7 +312,7 @@ func testCache(t *testing.T, when spec.G, it spec.S) { ref, err := name.ParseReference(h.RandString(10), name.WeakValidation) h.AssertNil(t, err) - subject = cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ = cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) volumeName = subject.Name() }) @@ -255,7 +350,7 @@ func testCache(t *testing.T, when spec.G, it spec.S) { it("returns the cache type", func() { ref, err := name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) - subject := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient) + subject, _ := cache.NewVolumeCache(ref, cache.CacheInfo{}, "some-suffix", dockerClient, logger) expected := cache.Volume h.AssertEq(t, subject.Type(), expected) }) diff --git a/pkg/client/build.go b/pkg/client/build.go index 23665cec0a..73b1f455bb 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -16,6 +16,7 @@ import ( "strings" "time" + "github.com/GoogleContainerTools/kaniko/pkg/util/proc" "github.com/Masterminds/semver" "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/layout" @@ -55,6 +56,10 @@ const ( minLifecycleVersionSupportingCreatorWithExtensions = "0.19.0" ) +var RunningInContainer = func() bool { + return proc.GetContainerRuntime(0, 0) != proc.RuntimeNotFound +} + // LifecycleExecutor executes the lifecycle which satisfies the Cloud Native Buildpacks Lifecycle specification. // Implementations of the Lifecycle must execute the following phases by calling the // phase-specific lifecycle binary in order: @@ -284,6 +289,13 @@ type layoutPathConfig struct { func (c *Client) Build(ctx context.Context, opts BuildOptions) error { var pathsConfig layoutPathConfig + if RunningInContainer() && !(opts.PullPolicy == image.PullAlways) { + c.logger.Warnf("Detected pack is running in a container; if using a shared docker host, failing to pull build inputs from a remote registry is insecure - " + + "other tenants may have compromised build inputs stored in the daemon." + + "This configuration is insecure and may become unsupported in the future." + + "Re-run with '--pull-policy=always' to silence this warning.") + } + imageRef, err := c.parseReference(opts) if err != nil { return errors.Wrapf(err, "invalid image name '%s'", opts.Image) @@ -390,7 +402,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return err } - fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts, targetToUse) + fetchedBPs, nInlineBPs, order, err := c.processBuildpacks(ctx, bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts, targetToUse) if err != nil { return err } @@ -418,6 +430,19 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { // Get the platform API version to use lifecycleVersion := bldr.LifecycleDescriptor().Info.Version useCreator := supportsCreator(lifecycleVersion) && opts.TrustBuilder(opts.Builder) + hasAdditionalModules := func() bool { + if len(fetchedBPs) == 0 && len(fetchedExs) == 0 { + return false + } + if len(fetchedBPs) == nInlineBPs && len(fetchedExs) == 0 { + return false + } + return true + }() + if useCreator && hasAdditionalModules { + c.logger.Warnf("Builder is trusted but additional modules were added; using the untrusted (5 phases) build flow") + useCreator = false + } var ( lifecycleOptsLifecycleImage string lifecycleAPIs []string @@ -1140,18 +1165,21 @@ func (c *Client) processProxyConfig(config *ProxyConfig) ProxyConfig { // ---------- // - group: // - A -func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions, targetToUse *dist.Target) (fetchedBPs []buildpack.BuildModule, order dist.Order, err error) { +func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions, targetToUse *dist.Target) (fetchedBPs []buildpack.BuildModule, nInlineBPs int, order dist.Order, err error) { relativeBaseDir := opts.RelativeBaseDir declaredBPs := opts.Buildpacks - // declare buildpacks provided by project descriptor when no buildpacks are declared + // Buildpacks from --buildpack override buildpacks from project descriptor if len(declaredBPs) == 0 && len(opts.ProjectDescriptor.Build.Buildpacks) != 0 { relativeBaseDir = opts.ProjectDescriptorBaseDir for _, bp := range opts.ProjectDescriptor.Build.Buildpacks { - buildpackLocator, err := getBuildpackLocator(bp, stackID) + buildpackLocator, isInline, err := getBuildpackLocator(bp, stackID) if err != nil { - return nil, nil, err + return nil, 0, nil, err + } + if isInline { + nInlineBPs++ } declaredBPs = append(declaredBPs, buildpackLocator) } @@ -1161,7 +1189,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.Module for _, bp := range declaredBPs { locatorType, err := buildpack.GetLocatorType(bp, relativeBaseDir, builderBPs) if err != nil { - return nil, nil, err + return nil, 0, nil, err } switch locatorType { @@ -1171,7 +1199,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.Module order = builderOrder case len(order) > 1: // This should only ever be possible if they are using from=builder twice which we don't allow - return nil, nil, errors.New("buildpacks from builder can only be defined once") + return nil, 0, nil, errors.New("buildpacks from builder can only be defined once") default: newOrder := dist.Order{} groupToAdd := order[0].Group @@ -1185,7 +1213,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.Module default: newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse) if err != nil { - return fetchedBPs, order, err + return fetchedBPs, 0, order, err } fetchedBPs = append(fetchedBPs, newFetchedBPs...) order = appendBuildpackToOrder(order, *moduleInfo) @@ -1195,20 +1223,28 @@ func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.Module if (len(order) == 0 || len(order[0].Group) == 0) && len(builderOrder) > 0 { preBuildpacks := opts.PreBuildpacks postBuildpacks := opts.PostBuildpacks + // Pre-buildpacks from --pre-buildpack override pre-buildpacks from project descriptor if len(preBuildpacks) == 0 && len(opts.ProjectDescriptor.Build.Pre.Buildpacks) > 0 { for _, bp := range opts.ProjectDescriptor.Build.Pre.Buildpacks { - buildpackLocator, err := getBuildpackLocator(bp, stackID) + buildpackLocator, isInline, err := getBuildpackLocator(bp, stackID) if err != nil { - return nil, nil, errors.Wrap(err, "get pre-buildpack locator") + return nil, 0, nil, errors.Wrap(err, "get pre-buildpack locator") + } + if isInline { + nInlineBPs++ } preBuildpacks = append(preBuildpacks, buildpackLocator) } } + // Post-buildpacks from --post-buildpack override post-buildpacks from project descriptor if len(postBuildpacks) == 0 && len(opts.ProjectDescriptor.Build.Post.Buildpacks) > 0 { for _, bp := range opts.ProjectDescriptor.Build.Post.Buildpacks { - buildpackLocator, err := getBuildpackLocator(bp, stackID) + buildpackLocator, isInline, err := getBuildpackLocator(bp, stackID) if err != nil { - return nil, nil, errors.Wrap(err, "get post-buildpack locator") + return nil, 0, nil, errors.Wrap(err, "get post-buildpack locator") + } + if isInline { + nInlineBPs++ } postBuildpacks = append(postBuildpacks, buildpackLocator) } @@ -1219,7 +1255,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.Module for _, bp := range preBuildpacks { newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse) if err != nil { - return fetchedBPs, order, err + return fetchedBPs, 0, order, err } fetchedBPs = append(fetchedBPs, newFetchedBPs...) order = prependBuildpackToOrder(order, *moduleInfo) @@ -1228,7 +1264,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.Module for _, bp := range postBuildpacks { newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse) if err != nil { - return fetchedBPs, order, err + return fetchedBPs, 0, order, err } fetchedBPs = append(fetchedBPs, newFetchedBPs...) order = appendBuildpackToOrder(order, *moduleInfo) @@ -1236,7 +1272,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.Module } } - return fetchedBPs, order, nil + return fetchedBPs, nInlineBPs, order, nil } func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir string, builderBPs []dist.ModuleInfo, opts BuildOptions, kind string, targetToUse *dist.Target) ([]buildpack.BuildModule, *dist.ModuleInfo, error) { @@ -1315,26 +1351,26 @@ func (c *Client) fetchBuildpackDependencies(ctx context.Context, bp string, pack return nil, err } -func getBuildpackLocator(bp projectTypes.Buildpack, stackID string) (string, error) { +func getBuildpackLocator(bp projectTypes.Buildpack, stackID string) (locator string, isInline bool, err error) { switch { case bp.ID != "" && bp.Script.Inline != "" && bp.URI == "": if bp.Script.API == "" { - return "", errors.New("Missing API version for inline buildpack") + return "", false, errors.New("Missing API version for inline buildpack") } pathToInlineBuildpack, err := createInlineBuildpack(bp, stackID) if err != nil { - return "", errors.Wrap(err, "Could not create temporary inline buildpack") + return "", false, errors.Wrap(err, "Could not create temporary inline buildpack") } - return pathToInlineBuildpack, nil + return pathToInlineBuildpack, true, nil case bp.URI != "": - return bp.URI, nil + return bp.URI, false, nil case bp.ID != "" && bp.Version != "": - return fmt.Sprintf("%s@%s", bp.ID, bp.Version), nil + return fmt.Sprintf("%s@%s", bp.ID, bp.Version), false, nil case bp.ID != "" && bp.Version == "": - return bp.ID, nil + return bp.ID, false, nil default: - return "", errors.New("Invalid buildpack definition") + return "", false, errors.New("Invalid buildpack definition") } } diff --git a/pkg/client/build_test.go b/pkg/client/build_test.go index 0c97f16978..9b2dcd7984 100644 --- a/pkg/client/build_test.go +++ b/pkg/client/build_test.go @@ -2050,9 +2050,6 @@ api = "0.2" h.AssertEq(t, args.PullPolicy, image.PullAlways) h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") }) - it("uses the api versions of the lifecycle image", func() { - h.AssertTrue(t, true) - }) it("parses the versions correctly", func() { fakeLifecycleImage.SetLabel("io.buildpacks.lifecycle.apis", "{\"platform\":{\"deprecated\":[\"0.1\",\"0.2\",\"0.3\",\"0.4\",\"0.5\",\"0.6\"],\"supported\":[\"0.7\",\"0.8\",\"0.9\",\"0.10\",\"0.11\",\"0.12\"]}}") @@ -2092,6 +2089,87 @@ api = "0.2" args := fakeImageFetcher.FetchCalls[fakeLifecycleImage.Name()] h.AssertNil(t, args) }) + + when("additional buildpacks were added", func() { + it("uses the 5 phases with the lifecycle image", func() { + additionalBP := ifakes.CreateBuildpackTar(t, tmpDir, dist.BuildpackDescriptor{ + WithAPI: api.MustParse("0.3"), + WithInfo: dist.ModuleInfo{ + ID: "buildpack.add.1.id", + Version: "buildpack.add.1.version", + }, + WithStacks: []dist.Stack{{ID: defaultBuilderStackID}}, + WithOrder: nil, + }) + + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + Publish: true, + TrustBuilder: func(string) bool { return true }, + Buildpacks: []string{additionalBP}, + })) + h.AssertEq(t, fakeLifecycle.Opts.UseCreator, false) + h.AssertEq(t, fakeLifecycle.Opts.LifecycleImage, fakeLifecycleImage.Name()) + + h.AssertContains(t, outBuf.String(), "Builder is trusted but additional modules were added; using the untrusted (5 phases) build flow") + }) + + when("from project descriptor", func() { + it("uses the 5 phases with the lifecycle image", func() { + additionalBP := ifakes.CreateBuildpackTar(t, tmpDir, dist.BuildpackDescriptor{ + WithAPI: api.MustParse("0.3"), + WithInfo: dist.ModuleInfo{ + ID: "buildpack.add.1.id", + Version: "buildpack.add.1.version", + }, + WithStacks: []dist.Stack{{ID: defaultBuilderStackID}}, + WithOrder: nil, + }) + + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + Publish: true, + TrustBuilder: func(string) bool { return true }, + ProjectDescriptor: projectTypes.Descriptor{Build: projectTypes.Build{ + Buildpacks: []projectTypes.Buildpack{{ + URI: additionalBP, + }}, + }}, + })) + h.AssertEq(t, fakeLifecycle.Opts.UseCreator, false) + h.AssertEq(t, fakeLifecycle.Opts.LifecycleImage, fakeLifecycleImage.Name()) + + h.AssertContains(t, outBuf.String(), "Builder is trusted but additional modules were added; using the untrusted (5 phases) build flow") + }) + + when("inline buildpack", func() { + it("uses the creator with the provided builder", func() { + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + Publish: true, + TrustBuilder: func(string) bool { return true }, + ProjectDescriptor: projectTypes.Descriptor{Build: projectTypes.Build{ + Buildpacks: []projectTypes.Buildpack{{ + ID: "buildpack.add.1.id", + Version: "buildpack.add.1.version", + Script: projectTypes.Script{ + API: "0.10", + Inline: "echo hello", + }, + }}, + }}, + })) + h.AssertEq(t, fakeLifecycle.Opts.UseCreator, true) + + args := fakeImageFetcher.FetchCalls[fakeLifecycleImage.Name()] + h.AssertNil(t, args) + }) + }) + }) + }) }) when("lifecycle doesn't support creator", func() { @@ -2301,6 +2379,38 @@ api = "0.2" }) }) + when("containerized pack", func() { + it.Before(func() { + RunningInContainer = func() bool { + return true + } + }) + + when("--pull-policy=always", func() { + it("does not warn", func() { + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + PullPolicy: image.PullAlways, + })) + + h.AssertNotContains(t, outBuf.String(), "failing to pull build inputs from a remote registry is insecure") + }) + }) + + when("not --pull-policy=always", func() { + it("warns", func() { + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + PullPolicy: image.PullNever, + })) + + h.AssertContains(t, outBuf.String(), "failing to pull build inputs from a remote registry is insecure") + }) + }) + }) + when("always", func() { it("uses pulls the builder and run image before using them", func() { h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ diff --git a/pkg/client/docker.go b/pkg/client/docker.go index 11b0fe1c01..d5a020f476 100644 --- a/pkg/client/docker.go +++ b/pkg/client/docker.go @@ -32,4 +32,6 @@ type DockerClient interface { ContainerWait(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.WaitResponse, <-chan error) ContainerAttach(ctx context.Context, container string, options containertypes.AttachOptions) (types.HijackedResponse, error) ContainerStart(ctx context.Context, container string, options containertypes.StartOptions) error + NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) + NetworkRemove(ctx context.Context, network string) error }