Skip to content

Commit

Permalink
Add support for adding symlinks post-install
Browse files Browse the repository at this point in the history
Not to be confused with post-install scripts.
This is handled directly in buildkit after the package is installed.
This allows us to customize things for the container without requiring
extra tooling in the image to perform those actions.

Signed-off-by: Brian Goff <[email protected]>
  • Loading branch information
cpuguy83 committed Feb 16, 2024
1 parent 4b05e77 commit 7cc5bab
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 8 deletions.
45 changes: 45 additions & 0 deletions frontend/mariner2/handle_container.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mariner2

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -220,9 +221,53 @@ rm -rf ` + rpmdbDir + `
// The return value is the state representing the contents of the mounted directory after the commands are run
rootfs := worker.AddMount(workPath, baseImg)

rootfs = worker.
Run(dalec.WithConstraints(opts...), addImagePost(getImagePostInstall(spec, target), workPath)).
AddMount(workPath, rootfs)

return rootfs, nil
}

func getImagePostInstall(spec *dalec.Spec, targetKey string) *dalec.PostInstall {
tgt, ok := spec.Targets[targetKey]
if ok && tgt.Image != nil && tgt.Image.Post != nil {
return tgt.Image.Post
}

if spec.Image == nil {
return nil
}
return spec.Image.Post
}

func addImagePost(post *dalec.PostInstall, rootfsPath string) llb.RunOption {
return runOptionFunc(func(ei *llb.ExecInfo) {
if post == nil {
return
}

if len(post.Symlinks) == 0 {
return
}

buf := bytes.NewBuffer(nil)
buf.WriteString("set -ex\n")
fmt.Fprintf(buf, "cd %q\n", rootfsPath)

for src, tgt := range post.Symlinks {
fmt.Fprintf(buf, "ln -s %q %q\n", src, filepath.Join(rootfsPath, tgt.Path))
}
shArgs(buf.String()).SetRunOption(ei)
dalec.ProgressGroup("Add post-install symlinks").SetRunOption(ei)
})
}

type runOptionFunc func(*llb.ExecInfo)

func (f runOptionFunc) SetRunOption(ei *llb.ExecInfo) {
f(ei)
}

func copyImageConfig(dst *image.Image, src *dalec.ImageConfig) error {
if src == nil {
return nil
Expand Down
20 changes: 12 additions & 8 deletions frontend/test_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ func runTest(ctx context.Context, t *dalec.TestSpec, st llb.State, ios map[int]l
return err
}

var outErr error
for p, check := range t.Files {
stat, err := ref.StatFile(ctx, gwclient.StatRequest{
Path: p,
Expand All @@ -198,30 +199,33 @@ func runTest(ctx context.Context, t *dalec.TestSpec, st llb.State, ios map[int]l
Filename: p,
})
if err != nil {
return errors.Wrapf(err, "read failed: %s", p)
outErr = stderrors.Join(errors.Wrapf(err, "read failed: %s", p))
}
}
if err := check.Check(string(dt), fs.FileMode(stat.Mode), stat.IsDir(), p); err != nil {
return errors.WithStack(err)
outErr = stderrors.Join(errors.WithStack(err))
}
}

for i, st := range ios {
def, err := st.Marshal(ctx)
if err != nil {
return err
outErr = stderrors.Join(errors.Wrap(err, "failed to marshal stdio state"))
continue
}
res, err := client.Solve(ctx, gwclient.SolveRequest{
Definition: def.ToPB(),
Evaluate: true,
})
if err != nil {
return err
outErr = stderrors.Join(errors.Wrap(err, "failed to solve stdio state"))
continue
}

ref, err := res.SingleRef()
if err != nil {
return err
outErr = stderrors.Join(errors.Wrap(err, "failed to get stdio ref for %d"))
continue
}

checkFile := func(c dalec.CheckOutput, name string) error {
Expand All @@ -242,14 +246,14 @@ func runTest(ctx context.Context, t *dalec.TestSpec, st llb.State, ios map[int]l

step := t.Steps[i]
if err := checkFile(step.Stdout, "stdout"); err != nil {
return err
outErr = stderrors.Join(err)
}
if err := checkFile(step.Stderr, "stderr"); err != nil {
return err
outErr = stderrors.Join(err)
}
}

return nil
return outErr
}

type errorList struct {
Expand Down
23 changes: 23 additions & 0 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,26 @@ type ImageConfig struct {
// Base is the base image to use for the output image.
// This only affects the output image, not the intermediate build image.
Base string `yaml:"base,omitempty" json:"base,omitempty"`

// Post is the post install configuration for the image.
// This allows making additional modifications to the container rootfs after the package(s) are installed.
//
// Use this to perform actions that would otherwise require additional tooling inside the container that is not relavent to
// the resulting container and makes a post-install script as part of the package unnecessary.
Post *PostInstall `yaml:"post,omitempty" json:"post,omitempty"`
}

// PostInstall is the post install configuration for the image.
type PostInstall struct {
// Symlinks is the list of symlinks to create in the container rootfs after the package(s) are installed.
// The key is the path the symlink should point to.
Symlinks map[string]SymlinkTarget `yaml:"symlinks,omitempty" json:"symlinks,omitempty"`
}

// SymlinkTarget specifies the properties of a symlink
type SymlinkTarget struct {
// Path is the path where the symlink should be placed
Path string `yaml:"path" json:"path" jsonschema:"required"`
}

type SourceDockerImage struct {
Expand Down Expand Up @@ -525,6 +545,9 @@ type FileCheckOutput struct {
IsDir bool `yaml:"is_dir,omitempty" json:"is_dir,omitempty"`
// NotExist is used to check that the file does not exist.
NotExist bool `yaml:"not_exist,omitempty" json:"not_exist,omitempty"`

// TODO: Support checking symlinks
// This is not currently possible with buildkit as it does not expose information about the symlink
}

// Check is used to check the output file.
Expand Down
25 changes: 25 additions & 0 deletions test/mariner2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ index 0000000..5260cb1
},
},

Dependencies: &dalec.PackageDependencies{
Runtime: map[string][]string{
"bash": {},
},
},

Build: dalec.ArtifactBuild{
Steps: []dalec.BuildStep{
// These are "build" steps where we aren't really building things just verifying
Expand Down Expand Up @@ -119,6 +125,14 @@ echo "$BAR" > bar.txt
},
},

Image: &dalec.ImageConfig{
Post: &dalec.PostInstall{
Symlinks: map[string]dalec.SymlinkTarget{
"/usr/bin/src1": {Path: "/src1"},
},
},
},

Artifacts: dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"src1": {},
Expand Down Expand Up @@ -163,6 +177,17 @@ echo "$BAR" > bar.txt
"/usr/bin/bar.txt": {CheckOutput: dalec.CheckOutput{StartsWith: "bar\n"}},
},
},
{
Name: "Post-install symlinks should be created",
Files: map[string]dalec.FileCheckOutput{
"/src1": {},
},
Steps: []dalec.TestStep{
{Command: "/bin/bash -c 'test -L /src1'"},
{Command: "/bin/bash -c 'test \"$(readlink /src1)\" = \"/usr/bin/src1\"'"},
{Command: "/src1", Stdout: dalec.CheckOutput{Equals: "hello world\n"}, Stderr: dalec.CheckOutput{Empty: true}},
},
},
},
}

Expand Down

0 comments on commit 7cc5bab

Please sign in to comment.