Skip to content

Commit

Permalink
Implement RFC-0096 Remove Stacks & Mixins: phase 1
Browse files Browse the repository at this point in the history
Signed-off-by: Joe Kutner <jpkutner@gmail.com>
  • Loading branch information
jkutner committed Mar 4, 2023
1 parent f1cde32 commit 36284b6
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 54 deletions.
61 changes: 55 additions & 6 deletions builder/config_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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{}
Expand Down
44 changes: 40 additions & 4 deletions builder/config_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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")
})
})
}
31 changes: 31 additions & 0 deletions internal/build/container_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 stack 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 {
Expand Down
96 changes: 96 additions & 0 deletions internal/build/container_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,102 @@ drwsrwsrwt 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"
Expand Down
5 changes: 5 additions & 0 deletions internal/build/fakes/fake_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type FakeBuilder struct {
ReturnForGID int
ReturnForLifecycleDescriptor builder.LifecycleDescriptor
ReturnForStack builder.StackMetadata
ReturnForRunImages []builder.RunImageMetadata
ReturnForOrderExtensions dist.Order
}

Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,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...)
Expand All @@ -527,6 +528,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...)
Expand All @@ -551,6 +553,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach
flagsOp,
cacheBindOp,
stackOp,
runOp,
)

analyze = phaseFactory.New(configProvider)
Expand All @@ -572,6 +575,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach
WithNetwork(l.opts.Network),
cacheBindOp,
stackOp,
runOp,
)

analyze = phaseFactory.New(configProvider)
Expand Down
1 change: 1 addition & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Builder interface {
GID() int
LifecycleDescriptor() builder.LifecycleDescriptor
Stack() builder.StackMetadata
RunImages() []builder.RunImageMetadata
Image() imgutil.Image
OrderExtensions() dist.Order
}
Expand Down
4 changes: 4 additions & 0 deletions internal/build/mount_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
5 changes: 5 additions & 0 deletions internal/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,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
Expand Down
Loading

0 comments on commit 36284b6

Please sign in to comment.