diff --git a/builder/config_reader.go b/builder/config_reader.go index 921d3dd29..c4637dd34 100644 --- a/builder/config_reader.go +++ b/builder/config_reader.go @@ -22,6 +22,8 @@ type Config struct { OrderExtensions dist.Order `toml:"order-extensions"` Stack StackConfig `toml:"stack"` Lifecycle LifecycleConfig `toml:"lifecycle"` + Run RunConfig `toml:"run"` + Build BuildConfig `toml:"build"` } // ModuleCollection is a list of ModuleConfigs @@ -55,6 +57,22 @@ type LifecycleConfig struct { Version string `toml:"version"` } +// RunConfig set of run image configuration +type RunConfig struct { + Images []RunImageConfig `toml:"images"` +} + +// RunImageConfig run image id and mirrors +type RunImageConfig struct { + Image string `toml:"image"` + Mirrors []string `toml:"run-image-mirrors,omitempty"` +} + +// BuildConfig build image configuration +type BuildConfig struct { + Image string `toml:"image"` +} + // ReadConfig reads a builder configuration from the file path provided and returns the // configuration along with any warnings encountered while parsing func ReadConfig(path string) (config Config, warnings []string, err error) { @@ -73,26 +91,57 @@ func ReadConfig(path string) (config Config, warnings []string, err error) { warnings = append(warnings, fmt.Sprintf("empty %s definition", style.Symbol("order"))) } + config.mergeStackWithImages() + return config, warnings, nil } // ValidateConfig validates the config func ValidateConfig(c Config) error { - if c.Stack.ID == "" { - return errors.New("stack.id is required") + if c.Build.Image == "" && c.Stack.BuildImage == "" { + return errors.New("build.image is required") + } else if c.Build.Image != "" && c.Stack.BuildImage != "" && c.Build.Image != c.Stack.BuildImage { + return errors.New("build.image and stack.build-image do not match") } - if c.Stack.BuildImage == "" { - return errors.New("stack.build-image is required") + if len(c.Run.Images) == 0 && (c.Stack.RunImage == "" || c.Stack.ID == "") { + return errors.New("run.images are required") } - if c.Stack.RunImage == "" { - return errors.New("stack.run-image is required") + for _, runImage := range c.Run.Images { + if runImage.Image == "" { + return errors.New("run.images.image is required") + } + } + + if c.Stack.RunImage != "" && c.Run.Images[0].Image != c.Stack.RunImage { + return errors.New("run.images and stack.run-image do not match") } return nil } +func (c *Config) mergeStackWithImages() { + // RFC-0096 + if c.Build.Image != "" { + c.Stack.BuildImage = c.Build.Image + } else if c.Build.Image == "" && c.Stack.BuildImage != "" { + c.Build.Image = c.Stack.BuildImage + } + + if len(c.Run.Images) != 0 { + // use the first run image as the "stack" + c.Stack.RunImage = c.Run.Images[0].Image + c.Stack.RunImageMirrors = c.Run.Images[0].Mirrors + } else if len(c.Run.Images) == 0 && c.Stack.RunImage != "" { + c.Run.Images = []RunImageConfig{{ + Image: c.Stack.RunImage, + Mirrors: c.Stack.RunImageMirrors, + }, + } + } +} + // parseConfig reads a builder configuration from file func parseConfig(file *os.File) (Config, error) { builderConfig := Config{} diff --git a/builder/config_reader_test.go b/builder/config_reader_test.go index b3c6409a7..93a1bf851 100644 --- a/builder/config_reader_test.go +++ b/builder/config_reader_test.go @@ -166,13 +166,13 @@ uri = "noop-buildpack.tgz" testBuildImage = "test-build-image" ) - it("returns error if no id", func() { + it("returns error if no stack id and no run images", func() { config := builder.Config{ Stack: builder.StackConfig{ BuildImage: testBuildImage, RunImage: testRunImage, }} - h.AssertError(t, builder.ValidateConfig(config), "stack.id is required") + h.AssertError(t, builder.ValidateConfig(config), "run.images are required") }) it("returns error if no build image", func() { @@ -181,7 +181,7 @@ uri = "noop-buildpack.tgz" ID: testID, RunImage: testRunImage, }} - h.AssertError(t, builder.ValidateConfig(config), "stack.build-image is required") + h.AssertError(t, builder.ValidateConfig(config), "build.image is required") }) it("returns error if no run image", func() { @@ -190,7 +190,43 @@ uri = "noop-buildpack.tgz" ID: testID, BuildImage: testBuildImage, }} - h.AssertError(t, builder.ValidateConfig(config), "stack.run-image is required") + h.AssertError(t, builder.ValidateConfig(config), "run.images are required") + }) + + it("returns error if no run images image", func() { + config := builder.Config{ + Build: builder.BuildConfig{ + Image: testBuildImage, + }, + Run: builder.RunConfig{ + Images: []builder.RunImageConfig{{ + Image: "", + }}, + }} + h.AssertError(t, builder.ValidateConfig(config), "run.images.image is required") + }) + + it("returns error if no stack or run image", func() { + config := builder.Config{ + Build: builder.BuildConfig{ + Image: testBuildImage, + }} + h.AssertError(t, builder.ValidateConfig(config), "run.images are required") + }) + + it("returns error if no stack and no build image", func() { + config := builder.Config{ + Run: builder.RunConfig{ + Images: []builder.RunImageConfig{{ + Image: testBuildImage, + }}, + }} + h.AssertError(t, builder.ValidateConfig(config), "build.image is required") + }) + + it("returns error if no stack, run, or build image", func() { + config := builder.Config{} + h.AssertError(t, builder.ValidateConfig(config), "build.image is required") }) }) } diff --git a/internal/build/container_ops.go b/internal/build/container_ops.go index 6bf5ffbb1..c151ba2d1 100644 --- a/internal/build/container_ops.go +++ b/internal/build/container_ops.go @@ -228,6 +228,37 @@ func WriteStackToml(dstPath string, stack builder.StackMetadata, os string) Cont } } +// WriteRunToml writes a `run.toml` based on the RunConfig provided to the destination path. +func WriteRunToml(dstPath string, runImages []builder.RunImageMetadata, os string) ContainerOperation { + return func(ctrClient DockerClient, ctx context.Context, containerID string, stdout, stderr io.Writer) error { + buf := &bytes.Buffer{} + err := toml.NewEncoder(buf).Encode(builder.RunImages{ + Images: runImages, + }) + if err != nil { + return errors.Wrap(err, "marshaling run metadata") + } + + tarBuilder := archive.TarBuilder{} + + tarPath := dstPath + if os == "windows" { + tarPath = paths.WindowsToSlash(dstPath) + } + + tarBuilder.AddFile(tarPath, 0755, archive.NormalizedDateTime, buf.Bytes()) + reader := tarBuilder.Reader(archive.DefaultTarWriterFactory()) + defer reader.Close() + + if os == "windows" { + dirName := paths.WindowsDir(dstPath) + return copyDirWindows(ctx, ctrClient, containerID, reader, dirName, stdout, stderr) + } + + return ctrClient.CopyToContainer(ctx, containerID, "/", reader, types.CopyToContainerOptions{}) + } +} + func createReader(src, dst string, uid, gid int, includeRoot bool, fileFilter func(string) bool) (io.ReadCloser, error) { fi, err := os.Stat(src) if err != nil { diff --git a/internal/build/container_ops_test.go b/internal/build/container_ops_test.go index 90e011573..8fb066786 100644 --- a/internal/build/container_ops_test.go +++ b/internal/build/container_ops_test.go @@ -404,6 +404,102 @@ drwxr-xr-x 2 123 456 (.*) some-vol }) }) + when("#WriteRunToml", func() { + it("writes file", func() { + containerDir := "/layers-vol" + containerPath := "/layers-vol/run.toml" + if osType == "windows" { + containerDir = `c:\layers-vol` + containerPath = `c:\layers-vol\run.toml` + } + + ctrCmd := []string{"ls", "-al", "/layers-vol/run.toml"} + if osType == "windows" { + ctrCmd = []string{"cmd", "/c", `dir /q /n c:\layers-vol\run.toml`} + } + ctx := context.Background() + ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...) + h.AssertNil(t, err) + defer cleanupContainer(ctx, ctr.ID) + + writeOp := build.WriteRunToml(containerPath, []builder.RunImageMetadata{builder.RunImageMetadata{ + Image: "image-1", + Mirrors: []string{ + "mirror-1", + "mirror-2", + }, + }, + }, osType) + + var outBuf, errBuf bytes.Buffer + err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) + h.AssertNil(t, err) + + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) + h.AssertNil(t, err) + + h.AssertEq(t, errBuf.String(), "") + if osType == "windows" { + h.AssertContains(t, outBuf.String(), `01/01/1980 12:00 AM 68 ... run.toml`) + } else { + h.AssertContains(t, outBuf.String(), `-rwxr-xr-x 1 root root 68 Jan 1 1980 /layers-vol/run.toml`) + } + }) + + it("has expected contents", func() { + containerDir := "/layers-vol" + containerPath := "/layers-vol/run.toml" + if osType == "windows" { + containerDir = `c:\layers-vol` + containerPath = `c:\layers-vol\run.toml` + } + + ctrCmd := []string{"cat", "/layers-vol/run.toml"} + if osType == "windows" { + ctrCmd = []string{"cmd", "/c", `type c:\layers-vol\run.toml`} + } + + ctx := context.Background() + ctr, err := createContainer(ctx, imageName, containerDir, osType, ctrCmd...) + h.AssertNil(t, err) + defer cleanupContainer(ctx, ctr.ID) + + writeOp := build.WriteRunToml(containerPath, []builder.RunImageMetadata{ + { + Image: "image-1", + Mirrors: []string{ + "mirror-1", + "mirror-2", + }, + }, + { + Image: "image-2", + Mirrors: []string{ + "mirror-3", + "mirror-4", + }, + }, + }, osType) + + var outBuf, errBuf bytes.Buffer + err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) + h.AssertNil(t, err) + + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) + h.AssertNil(t, err) + + h.AssertEq(t, errBuf.String(), "") + h.AssertContains(t, outBuf.String(), `[[images]] + image = "image-1" + mirrors = ["mirror-1", "mirror-2"] + +[[images]] + image = "image-2" + mirrors = ["mirror-3", "mirror-4"] +`) + }) + }) + when("#WriteProjectMetadata", func() { it("writes file", func() { containerDir := "/layers-vol" diff --git a/internal/build/fakes/fake_builder.go b/internal/build/fakes/fake_builder.go index c09121d48..2bcd9db86 100644 --- a/internal/build/fakes/fake_builder.go +++ b/internal/build/fakes/fake_builder.go @@ -17,6 +17,7 @@ type FakeBuilder struct { ReturnForGID int ReturnForLifecycleDescriptor builder.LifecycleDescriptor ReturnForStack builder.StackMetadata + ReturnForRunImages []builder.RunImageMetadata ReturnForOrderExtensions dist.Order } @@ -112,6 +113,10 @@ func (b *FakeBuilder) Stack() builder.StackMetadata { return b.ReturnForStack } +func (b *FakeBuilder) RunImages() []builder.RunImageMetadata { + return b.ReturnForRunImages +} + func WithBuilder(builder *FakeBuilder) func(*build.LifecycleOptions) { return func(opts *build.LifecycleOptions) { opts.Builder = builder diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index 34711ea17..7fce6a6fd 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -527,6 +527,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach } stackOp := NullOp() + runOp := NullOp() if !platformAPILessThan07 { for _, tag := range l.opts.AdditionalTags { args = append([]string{"-tag", tag}, args...) @@ -536,6 +537,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach } args = append([]string{"-stack", l.mountPaths.stackPath()}, args...) stackOp = WithContainerOperations(WriteStackToml(l.mountPaths.stackPath(), l.opts.Builder.Stack(), l.os)) + runOp = WithContainerOperations(WriteRunToml(l.mountPaths.runPath(), l.opts.Builder.RunImages(), l.os)) } flagsOp := WithFlags(flags...) @@ -560,6 +562,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach flagsOp, cacheBindOp, stackOp, + runOp, ) analyze = phaseFactory.New(configProvider) @@ -581,6 +584,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach WithNetwork(l.opts.Network), cacheBindOp, stackOp, + runOp, ) analyze = phaseFactory.New(configProvider) @@ -695,6 +699,7 @@ func (l *LifecycleExecution) Export(ctx context.Context, buildCache, launchCache WithNetwork(l.opts.Network), cacheBindOp, WithContainerOperations(WriteStackToml(l.mountPaths.stackPath(), l.opts.Builder.Stack(), l.os)), + WithContainerOperations(WriteRunToml(l.mountPaths.runPath(), l.opts.Builder.RunImages(), l.os)), WithContainerOperations(WriteProjectMetadata(l.mountPaths.projectPath(), l.opts.ProjectMetadata, l.os)), If(l.opts.SBOMDestinationDir != "", WithPostContainerRunOperations( EnsureVolumeAccess(l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.layersVolume, l.appVolume), diff --git a/internal/build/lifecycle_execution_test.go b/internal/build/lifecycle_execution_test.go index 996c5b91f..ddb9d461a 100644 --- a/internal/build/lifecycle_execution_test.go +++ b/internal/build/lifecycle_execution_test.go @@ -1838,9 +1838,10 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { expectedBinds := []string{"some-cache:/cache"} h.AssertSliceContains(t, configProvider.HostConfig().Binds, expectedBinds...) - h.AssertEq(t, len(configProvider.ContainerOps()), 2) + h.AssertEq(t, len(configProvider.ContainerOps()), 3) h.AssertFunctionName(t, configProvider.ContainerOps()[0], "WriteStackToml") - h.AssertFunctionName(t, configProvider.ContainerOps()[1], "WriteProjectMetadata") + h.AssertFunctionName(t, configProvider.ContainerOps()[1], "WriteRunToml") + h.AssertFunctionName(t, configProvider.ContainerOps()[2], "WriteProjectMetadata") }) when("default process type", func() { @@ -1945,9 +1946,10 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { expectedBinds := []string{"some-cache:/cache", "some-launch-cache:/launch-cache"} h.AssertSliceContains(t, configProvider.HostConfig().Binds, expectedBinds...) - h.AssertEq(t, len(configProvider.ContainerOps()), 2) + h.AssertEq(t, len(configProvider.ContainerOps()), 3) h.AssertFunctionName(t, configProvider.ContainerOps()[0], "WriteStackToml") - h.AssertFunctionName(t, configProvider.ContainerOps()[1], "WriteProjectMetadata") + h.AssertFunctionName(t, configProvider.ContainerOps()[1], "WriteRunToml") + h.AssertFunctionName(t, configProvider.ContainerOps()[2], "WriteProjectMetadata") }) when("default process type", func() { diff --git a/internal/build/lifecycle_executor.go b/internal/build/lifecycle_executor.go index 08e1c9856..1317d0bde 100644 --- a/internal/build/lifecycle_executor.go +++ b/internal/build/lifecycle_executor.go @@ -40,6 +40,7 @@ type Builder interface { GID() int LifecycleDescriptor() builder.LifecycleDescriptor Stack() builder.StackMetadata + RunImages() []builder.RunImageMetadata Image() imgutil.Image OrderExtensions() dist.Order } diff --git a/internal/build/mount_paths.go b/internal/build/mount_paths.go index 89d266b6f..ad4bd9113 100644 --- a/internal/build/mount_paths.go +++ b/internal/build/mount_paths.go @@ -38,6 +38,10 @@ func (m mountPaths) stackPath() string { return m.join(m.layersDir(), "stack.toml") } +func (m mountPaths) runPath() string { + return m.join(m.layersDir(), "run.toml") +} + func (m mountPaths) projectPath() string { return m.join(m.layersDir(), "project-metadata.toml") } diff --git a/internal/builder/builder.go b/internal/builder/builder.go index d840007a5..6b809be97 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -38,6 +38,7 @@ const ( orderPath = "/cnb/order.toml" stackPath = "/cnb/stack.toml" + runPath = "/cnb/run.toml" platformDir = "/platform" lifecycleDir = "/cnb/lifecycle" compatLifecycleDir = "/lifecycle" @@ -160,9 +161,6 @@ func addImgLabelsToBuildr(bldr *Builder) error { if err != nil { return errors.Wrapf(err, "get label %s from image %s", style.Symbol(stackLabel), style.Symbol(bldr.image.Name())) } - if bldr.StackID == "" { - return fmt.Errorf("image %s missing label %s", style.Symbol(bldr.image.Name()), style.Symbol(stackLabel)) - } if _, err = dist.GetLabel(bldr.image, stack.MixinsLabel, &bldr.mixins); err != nil { return errors.Wrapf(err, "getting label %s", stack.MixinsLabel) @@ -236,6 +234,11 @@ func (b *Builder) Stack() StackMetadata { return b.metadata.Stack } +// RunImages returns the run image metadata +func (b *Builder) RunImages() []RunImageMetadata { + return b.metadata.RunImages +} + // Mixins returns the mixins of the builder func (b *Builder) Mixins() []string { return b.mixins @@ -310,6 +313,18 @@ func (b *Builder) SetStack(stackConfig builder.StackConfig) { } } +// SetRunImage sets the run image of the builder +func (b *Builder) SetRunImage(runConfig builder.RunConfig) { + var runImages []RunImageMetadata + for _, i := range runConfig.Images { + runImages = append(runImages, RunImageMetadata{ + Image: i.Image, + Mirrors: i.Mirrors, + }) + } + b.metadata.RunImages = runImages +} + // Save saves the builder func (b *Builder) Save(logger logging.Logger, creatorMetadata CreatorMetadata) error { logger.Debugf("Creating builder with the following buildpacks:") @@ -410,6 +425,14 @@ func (b *Builder) Save(logger logging.Logger, creatorMetadata CreatorMetadata) e return errors.Wrap(err, "adding stack.tar layer") } + runImageTar, err := b.runImageLayer(tmpDir) + if err != nil { + return err + } + if err := b.image.AddLayer(runImageTar); err != nil { + return errors.Wrap(err, "adding run.tar layer") + } + if len(b.env) > 0 { logger.Debugf("Provided Environment Variables\n %s", style.Map(b.env, " ", "\n")) } @@ -895,6 +918,24 @@ func (b *Builder) stackLayer(dest string) (string, error) { return layerTar, nil } +func (b *Builder) runImageLayer(dest string) (string, error) { + buf := &bytes.Buffer{} + err := toml.NewEncoder(buf).Encode(RunImages{ + Images: b.metadata.RunImages, + }) + if err != nil { + return "", errors.Wrapf(err, "failed to marshal run.toml") + } + + layerTar := filepath.Join(dest, "run.tar") + err = layer.CreateSingleFileTar(layerTar, runPath, buf.String(), b.layerWriterFactory) + if err != nil { + return "", errors.Wrapf(err, "failed to create run.toml layer tar") + } + + return layerTar, nil +} + func (b *Builder) envLayer(dest string, env map[string]string) (string, error) { fh, err := os.Create(filepath.Join(dest, "env.tar")) if err != nil { diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go index 80f09f762..a5d5e7b1d 100644 --- a/internal/builder/builder_test.go +++ b/internal/builder/builder_test.go @@ -240,15 +240,15 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { }) }) - when("missing stack id label", func() { + when("missing stack id label and run image", func() { it.Before(func() { h.AssertNil(t, baseImage.SetEnv("CNB_USER_ID", "1234")) h.AssertNil(t, baseImage.SetEnv("CNB_GROUP_ID", "4321")) }) - it("returns an error", func() { + it("does not return an error", func() { _, err := builder.New(baseImage, "some/builder") - h.AssertError(t, err, "image 'base/image' missing label 'io.buildpacks.stack.id'") + h.AssertNilE(t, err) }) }) @@ -685,7 +685,8 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { // - 2 from buildpacks // - 1 from orderLayer // - 1 from stackLayer - h.AssertEq(t, baseImage.NumberOfAddedLayers(), 6) + // - 1 from runImageLayer + h.AssertEq(t, baseImage.NumberOfAddedLayers(), 7) }) when("duplicated buildpack, has different contents", func() { @@ -763,7 +764,8 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { // - 1 from buildpacks // - 1 from orderLayer // - 1 from stackLayer - h.AssertEq(t, baseImage.NumberOfAddedLayers(), 5) + // - 1 from runImageLayer + h.AssertEq(t, baseImage.NumberOfAddedLayers(), 6) oldSha256 := "4dc0072c61fc2bd7118bbc93a432eae0012082de094455cf0a9fed20e3c44789" newSha256 := "29cb2bce4c2350f0e86f3dd30fa3810beb409b910126a18651de750f457fedfb" if runtime.GOOS == "windows" { @@ -1559,6 +1561,40 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { }) }) + when("#SetRunImage", func() { + it.Before(func() { + subject.SetRunImage(pubbldr.RunConfig{Images: []pubbldr.RunImageConfig{{ + Image: "some/run", + Mirrors: []string{"some/mirror", "other/mirror"}, + }}}) + h.AssertNil(t, subject.Save(logger, builder.CreatorMetadata{})) + h.AssertEq(t, baseImage.IsSaved(), true) + }) + + it("adds the run.toml to the image", func() { + layerTar, err := baseImage.FindLayerWithPath("/cnb/run.toml") + h.AssertNil(t, err) + h.AssertOnTarEntry(t, layerTar, "/cnb/run.toml", + h.ContentEquals(`[[images]] + image = "some/run" + mirrors = ["some/mirror", "other/mirror"] +`), + h.HasModTime(archive.NormalizedDateTime), + ) + }) + + it("adds the run image to the metadata", func() { + label, err := baseImage.Label("io.buildpacks.builder.metadata") + h.AssertNil(t, err) + + var metadata builder.Metadata + h.AssertNil(t, json.Unmarshal([]byte(label), &metadata)) + h.AssertEq(t, metadata.RunImages[0].Image, "some/run") + h.AssertEq(t, metadata.RunImages[0].Mirrors[0], "some/mirror") + h.AssertEq(t, metadata.RunImages[0].Mirrors[1], "other/mirror") + }) + }) + when("#SetEnv", func() { it.Before(func() { subject.SetEnv(map[string]string{ diff --git a/internal/builder/metadata.go b/internal/builder/metadata.go index c16e55ff3..67333e430 100644 --- a/internal/builder/metadata.go +++ b/internal/builder/metadata.go @@ -8,12 +8,13 @@ const ( ) type Metadata struct { - Description string `json:"description"` - Buildpacks []dist.ModuleInfo `json:"buildpacks"` - Extensions []dist.ModuleInfo `json:"extensions"` - Stack StackMetadata `json:"stack"` - Lifecycle LifecycleMetadata `json:"lifecycle"` - CreatedBy CreatorMetadata `json:"createdBy"` + Description string `json:"description"` + Buildpacks []dist.ModuleInfo `json:"buildpacks"` + Extensions []dist.ModuleInfo `json:"extensions"` + Stack StackMetadata `json:"stack"` + Lifecycle LifecycleMetadata `json:"lifecycle"` + CreatedBy CreatorMetadata `json:"createdBy"` + RunImages []RunImageMetadata `json:"images"` } type CreatorMetadata struct { @@ -32,6 +33,10 @@ type StackMetadata struct { RunImage RunImageMetadata `json:"runImage" toml:"run-image"` } +type RunImages struct { + Images []RunImageMetadata `json:"images" toml:"images"` +} + type RunImageMetadata struct { Image string `json:"image" toml:"image"` Mirrors []string `json:"mirrors" toml:"mirrors"` diff --git a/pkg/client/create_builder.go b/pkg/client/create_builder.go index 2f728efa6..788313f47 100644 --- a/pkg/client/create_builder.go +++ b/pkg/client/create_builder.go @@ -63,7 +63,11 @@ func (c *Client) CreateBuilder(ctx context.Context, opts CreateBuilderOptions) e bldr.SetOrder(opts.Config.Order) bldr.SetOrderExtensions(opts.Config.OrderExtensions) - bldr.SetStack(opts.Config.Stack) + + if opts.Config.Stack.ID != "" { + bldr.SetStack(opts.Config.Stack) + } + bldr.SetRunImage(opts.Config.Run) return bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version}) } @@ -82,43 +86,47 @@ func (c *Client) validateConfig(ctx context.Context, opts CreateBuilderOptions) func (c *Client) validateRunImageConfig(ctx context.Context, opts CreateBuilderOptions) error { var runImages []imgutil.Image - for _, i := range append([]string{opts.Config.Stack.RunImage}, opts.Config.Stack.RunImageMirrors...) { - if !opts.Publish { - img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy}) + for _, r := range opts.Config.Run.Images { + for _, i := range append([]string{r.Image}, r.Mirrors...) { + if !opts.Publish { + img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy}) + if err != nil { + if errors.Cause(err) != image.ErrNotFound { + return errors.Wrap(err, "failed to fetch image") + } + } else { + runImages = append(runImages, img) + continue + } + } + + img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: false, PullPolicy: opts.PullPolicy}) if err != nil { if errors.Cause(err) != image.ErrNotFound { return errors.Wrap(err, "failed to fetch image") } + c.logger.Warnf("run image %s is not accessible", style.Symbol(i)) } else { runImages = append(runImages, img) - continue } } - - img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: false, PullPolicy: opts.PullPolicy}) - if err != nil { - if errors.Cause(err) != image.ErrNotFound { - return errors.Wrap(err, "failed to fetch image") - } - c.logger.Warnf("run image %s is not accessible", style.Symbol(i)) - } else { - runImages = append(runImages, img) - } } for _, img := range runImages { - stackID, err := img.Label("io.buildpacks.stack.id") - if err != nil { - return errors.Wrap(err, "failed to label image") - } + if opts.Config.Stack.ID != "" { + stackID, err := img.Label("io.buildpacks.stack.id") + if err != nil { + return errors.Wrap(err, "failed to label image") + } - if stackID != opts.Config.Stack.ID { - return fmt.Errorf( - "stack %s from builder config is incompatible with stack %s from run image %s", - style.Symbol(opts.Config.Stack.ID), - style.Symbol(stackID), - style.Symbol(img.Name()), - ) + if stackID != opts.Config.Stack.ID { + return fmt.Errorf( + "stack %s from builder config is incompatible with stack %s from run image %s", + style.Symbol(opts.Config.Stack.ID), + style.Symbol(stackID), + style.Symbol(img.Name()), + ) + } } } @@ -126,7 +134,7 @@ func (c *Client) validateRunImageConfig(ctx context.Context, opts CreateBuilderO } func (c *Client) createBaseBuilder(ctx context.Context, opts CreateBuilderOptions) (*builder.Builder, error) { - baseImage, err := c.imageFetcher.Fetch(ctx, opts.Config.Stack.BuildImage, image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy}) + baseImage, err := c.imageFetcher.Fetch(ctx, opts.Config.Build.Image, image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy}) if err != nil { return nil, errors.Wrap(err, "fetch build image") } @@ -153,7 +161,7 @@ func (c *Client) createBaseBuilder(ctx context.Context, opts CreateBuilderOption bldr.SetDescription(opts.Config.Description) - if bldr.StackID != opts.Config.Stack.ID { + if opts.Config.Stack.ID != "" && bldr.StackID != opts.Config.Stack.ID { return nil, fmt.Errorf( "stack %s from builder config is incompatible with stack %s from build image", style.Symbol(opts.Config.Stack.ID), diff --git a/pkg/client/create_builder_test.go b/pkg/client/create_builder_test.go index e7a703cf1..1e9b592ea 100644 --- a/pkg/client/create_builder_test.go +++ b/pkg/client/create_builder_test.go @@ -167,10 +167,16 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { }}, }, Stack: pubbldr.StackConfig{ - ID: "some.stack.id", - BuildImage: "some/build-image", - RunImage: "some/run-image", - RunImageMirrors: []string{"localhost:5000/some/run-image"}, + ID: "some.stack.id", + }, + Run: pubbldr.RunConfig{ + Images: []pubbldr.RunImageConfig{{ + Image: "some/run-image", + Mirrors: []string{"localhost:5000/some/run-image"}, + }}, + }, + Build: pubbldr.BuildConfig{ + Image: "some/build-image", }, Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"}, }, @@ -201,12 +207,14 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { } when("validating the builder config", func() { - it("should fail when the stack ID is empty", func() { + it("should not fail when the stack ID is empty", func() { opts.Config.Stack.ID = "" + prepareFetcherWithBuildImage() + prepareFetcherWithRunImages() err := subject.CreateBuilder(context.TODO(), opts) - h.AssertError(t, err, "stack.id is required") + h.AssertNil(t, err) }) it("should fail when the stack ID from the builder config does not match the stack ID from the build image", func() { @@ -219,20 +227,56 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertError(t, err, "stack 'some.stack.id' from builder config is incompatible with stack 'other.stack.id' from build image") }) - it("should fail when the build image is empty", func() { + it("should not fail when the stack is empty", func() { + opts.Config.Stack.ID = "" + opts.Config.Stack.BuildImage = "" + opts.Config.Stack.RunImage = "" + prepareFetcherWithBuildImage() + prepareFetcherWithRunImages() + + err := subject.CreateBuilder(context.TODO(), opts) + + h.AssertNil(t, err) + }) + + it("should fail when the run images and stack are empty", func() { opts.Config.Stack.BuildImage = "" + opts.Config.Stack.RunImage = "" + + opts.Config.Run = pubbldr.RunConfig{} err := subject.CreateBuilder(context.TODO(), opts) - h.AssertError(t, err, "stack.build-image is required") + h.AssertError(t, err, "run.images are required") }) - it("should fail when the run image is empty", func() { + it("should fail when the run images image and stack are empty", func() { + opts.Config.Stack.BuildImage = "" opts.Config.Stack.RunImage = "" + opts.Config.Run = pubbldr.RunConfig{ + Images: []pubbldr.RunImageConfig{{}}, + } + + err := subject.CreateBuilder(context.TODO(), opts) + + h.AssertError(t, err, "run.images.image is required") + }) + + it("should fail if stack and run image are different", func() { + opts.Config.Stack.RunImage = "some-other-stack-run-image" + + err := subject.CreateBuilder(context.TODO(), opts) + + h.AssertError(t, err, "run.images and stack.run-image do not match") + }) + + it("should fail if stack and build image are different", func() { + opts.Config.Stack.BuildImage = "some-other-stack-build-image" + err := subject.CreateBuilder(context.TODO(), opts) - h.AssertError(t, err, "stack.run-image is required") + h.AssertError(t, err, "build.image and stack.build-image do not match") }) it("should fail when lifecycle version is not a semver", func() {