cleanup: solver/fs

- Solver: Encapsulates all access to Buildkit. Can solve plain LLB, invoke external frontends (for DockerBuild) and export (for ContainerPush)
- FS (now BuildkitFS) implements the standard Go 1.16 io/fs.FS interface and provides a read-only filesystem on top of a buildkit result. It can be used with built-ins such as fs.WalkDir (no need to have our own Walk functions anymore)
- Moved CueBuild into compiler.Build since it no longer depends on Buildkit. Instead it relies on the io/fs.FS interface, which is used both for the base config and the stdlib (go:embed also uses io/fs.FS). Overlaying base and the stdlib is now done by the same code.

Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
This commit is contained in:
Andrea Luzzardi
2021-03-12 13:00:11 -08:00
parent c35eca99e1
commit c923e5042b
8 changed files with 365 additions and 536 deletions

View File

@@ -22,23 +22,32 @@ import (
// An execution pipeline
type Pipeline struct {
name string
s Solver
fs FS
out *Fillable
name string
s Solver
state llb.State
result bkgw.Reference
out *Fillable
}
func NewPipeline(name string, s Solver, out *Fillable) *Pipeline {
return &Pipeline{
name: name,
s: s,
fs: s.Scratch(),
out: out,
name: name,
s: s,
state: llb.Scratch(),
out: out,
}
}
func (p *Pipeline) FS() FS {
return p.fs
func (p *Pipeline) State() llb.State {
return p.state
}
func (p *Pipeline) Result() bkgw.Reference {
return p.result
}
func (p *Pipeline) FS() fs.FS {
return NewBuildkitFS(p.result)
}
func isComponent(v *compiler.Value) bool {
@@ -129,54 +138,54 @@ func (p *Pipeline) Do(ctx context.Context, code ...*compiler.Value) error {
Msg("pipeline was partially executed because of missing inputs")
return nil
}
if err := p.doOp(ctx, op); err != nil {
p.state, err = p.doOp(ctx, op, p.state)
if err != nil {
return err
}
// Force a buildkit solve request at each operation,
// so that errors map to the correct cue path.
// FIXME: might as well change FS to make every operation
// synchronous.
fs, err := p.fs.Solve(ctx)
p.result, err = p.s.Solve(ctx, p.state)
if err != nil {
return err
}
p.fs = fs
}
return nil
}
func (p *Pipeline) doOp(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) doOp(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
do, err := op.Get("do").String()
if err != nil {
return err
return st, err
}
switch do {
case "copy":
return p.Copy(ctx, op)
return p.Copy(ctx, op, st)
case "exec":
return p.Exec(ctx, op)
return p.Exec(ctx, op, st)
case "export":
return p.Export(ctx, op)
return p.Export(ctx, op, st)
case "fetch-container":
return p.FetchContainer(ctx, op)
return p.FetchContainer(ctx, op, st)
case "push-container":
return p.PushContainer(ctx, op)
return p.PushContainer(ctx, op, st)
case "fetch-git":
return p.FetchGit(ctx, op)
return p.FetchGit(ctx, op, st)
case "local":
return p.Local(ctx, op)
return p.Local(ctx, op, st)
case "load":
return p.Load(ctx, op)
return p.Load(ctx, op, st)
case "subdir":
return p.Subdir(ctx, op)
return p.Subdir(ctx, op, st)
case "docker-build":
return p.DockerBuild(ctx, op)
return p.DockerBuild(ctx, op, st)
case "write-file":
return p.WriteFile(ctx, op)
return p.WriteFile(ctx, op, st)
case "mkdir":
return p.Mkdir(ctx, op)
return p.Mkdir(ctx, op, st)
default:
return fmt.Errorf("invalid operation: %s", op.JSON())
return st, fmt.Errorf("invalid operation: %s", op.JSON())
}
}
@@ -192,74 +201,68 @@ func (p *Pipeline) Tmp(name string) *Pipeline {
return NewPipeline(name, p.s, nil)
}
func (p *Pipeline) Subdir(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) Subdir(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
// FIXME: this could be more optimized by carrying subdir path as metadata,
// and using it in copy, load or mount.
dir, err := op.Get("dir").String()
if err != nil {
return err
return st, err
}
p.fs = p.fs.Change(func(st llb.State) llb.State {
return st.File(
llb.Copy(
p.fs.LLB(),
dir,
"/",
&llb.CopyInfo{
CopyDirContentsOnly: true,
},
),
llb.WithCustomName(p.vertexNamef("Subdir %s", dir)),
)
})
return nil
return st.File(
llb.Copy(
st,
dir,
"/",
&llb.CopyInfo{
CopyDirContentsOnly: true,
},
),
llb.WithCustomName(p.vertexNamef("Subdir %s", dir)),
), nil
}
func (p *Pipeline) Copy(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) Copy(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
// Decode copy options
src, err := op.Get("src").String()
if err != nil {
return err
return st, err
}
dest, err := op.Get("dest").String()
if err != nil {
return err
return st, err
}
// Execute 'from' in a tmp pipeline, and use the resulting fs
from := p.Tmp(op.Get("from").Path().String())
if err := from.Do(ctx, op.Get("from")); err != nil {
return err
return st, err
}
p.fs = p.fs.Change(func(st llb.State) llb.State {
return st.File(
llb.Copy(
from.FS().LLB(),
src,
dest,
// FIXME: allow more configurable llb options
// For now we define the following convenience presets:
&llb.CopyInfo{
CopyDirContentsOnly: true,
CreateDestPath: true,
AllowWildcard: true,
},
),
llb.WithCustomName(p.vertexNamef("Copy %s %s", src, dest)),
)
})
return nil
return st.File(
llb.Copy(
from.State(),
src,
dest,
// FIXME: allow more configurable llb options
// For now we define the following convenience presets:
&llb.CopyInfo{
CopyDirContentsOnly: true,
CreateDestPath: true,
AllowWildcard: true,
},
),
llb.WithCustomName(p.vertexNamef("Copy %s %s", src, dest)),
), nil
}
func (p *Pipeline) Local(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) Local(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
dir, err := op.Get("dir").String()
if err != nil {
return err
return st, err
}
var include []string
if inc := op.Get("include"); inc.Exists() {
if err := inc.Decode(&include); err != nil {
return err
return st, err
}
}
// FIXME: Remove the `Copy` and use `Local` directly.
@@ -270,30 +273,26 @@ func (p *Pipeline) Local(ctx context.Context, op *compiler.Value) error {
//
// By wrapping `llb.Local` inside `llb.Copy`, we get the same digest for
// the same content.
p.fs = p.fs.Change(func(st llb.State) llb.State {
return st.File(
llb.Copy(
llb.Local(
dir,
llb.FollowPaths(include),
llb.WithCustomName(p.vertexNamef("Local %s [transfer]", dir)),
return st.File(
llb.Copy(
llb.Local(
dir,
llb.FollowPaths(include),
llb.WithCustomName(p.vertexNamef("Local %s [transfer]", dir)),
// Without hint, multiple `llb.Local` operations on the
// same path get a different digest.
llb.SessionID(p.s.SessionID()),
llb.SharedKeyHint(dir),
),
"/",
"/",
// Without hint, multiple `llb.Local` operations on the
// same path get a different digest.
llb.SessionID(p.s.SessionID()),
llb.SharedKeyHint(dir),
),
llb.WithCustomName(p.vertexNamef("Local %s [copy]", dir)),
)
})
return nil
"/",
"/",
),
llb.WithCustomName(p.vertexNamef("Local %s [copy]", dir)),
), nil
}
func (p *Pipeline) Exec(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) Exec(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
opts := []llb.RunOption{}
var cmd struct {
Args []string
@@ -303,7 +302,7 @@ func (p *Pipeline) Exec(ctx context.Context, op *compiler.Value) error {
}
if err := op.Decode(&cmd); err != nil {
return err
return st, err
}
// args
opts = append(opts, llb.Args(cmd.Args))
@@ -318,7 +317,7 @@ func (p *Pipeline) Exec(ctx context.Context, op *compiler.Value) error {
if cmd.Always {
cacheBuster, err := randomID(8)
if err != nil {
return err
return st, err
}
opts = append(opts, llb.AddEnv("DAGGER_CACHEBUSTER", cacheBuster))
}
@@ -326,7 +325,7 @@ func (p *Pipeline) Exec(ctx context.Context, op *compiler.Value) error {
if mounts := op.Lookup("mount"); mounts.Exists() {
mntOpts, err := p.mountAll(ctx, mounts)
if err != nil {
return err
return st, err
}
opts = append(opts, mntOpts...)
}
@@ -340,10 +339,7 @@ func (p *Pipeline) Exec(ctx context.Context, op *compiler.Value) error {
opts = append(opts, llb.WithCustomName(p.vertexNamef("Exec [%s]", strings.Join(args, ", "))))
// --> Execute
p.fs = p.fs.Change(func(st llb.State) llb.State {
return st.Run(opts...).Root()
})
return nil
return st.Run(opts...).Root(), nil
}
func (p *Pipeline) mountAll(ctx context.Context, mounts *compiler.Value) ([]llb.RunOption, error) {
@@ -397,21 +393,21 @@ func (p *Pipeline) mount(ctx context.Context, dest string, mnt *compiler.Value)
}
mo = append(mo, llb.SourcePath(mps))
}
return llb.AddMount(dest, from.FS().LLB(), mo...), nil
return llb.AddMount(dest, from.State(), mo...), nil
}
func (p *Pipeline) Export(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) Export(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
source, err := op.Get("source").String()
if err != nil {
return err
return st, err
}
format, err := op.Get("format").String()
if err != nil {
return err
return st, err
}
contents, err := p.fs.ReadFile(ctx, source)
contents, err := fs.ReadFile(p.FS(), source)
if err != nil {
return fmt.Errorf("export %s: %w", source, err)
return st, fmt.Errorf("export %s: %w", source, err)
}
switch format {
case "string":
@@ -422,13 +418,13 @@ func (p *Pipeline) Export(ctx context.Context, op *compiler.Value) error {
Msg("exporting string")
if err := p.out.Fill(string(contents)); err != nil {
return err
return st, err
}
case "json":
var o interface{}
o, err := unmarshalAnything(contents, json.Unmarshal)
if err != nil {
return err
return st, err
}
log.
@@ -438,13 +434,13 @@ func (p *Pipeline) Export(ctx context.Context, op *compiler.Value) error {
Msg("exporting json")
if err := p.out.Fill(o); err != nil {
return err
return st, err
}
case "yaml":
var o interface{}
o, err := unmarshalAnything(contents, yaml.Unmarshal)
if err != nil {
return err
return st, err
}
log.
@@ -454,12 +450,12 @@ func (p *Pipeline) Export(ctx context.Context, op *compiler.Value) error {
Msg("exporting yaml")
if err := p.out.Fill(o); err != nil {
return err
return st, err
}
default:
return fmt.Errorf("unsupported export format: %q", format)
return st, fmt.Errorf("unsupported export format: %q", format)
}
return nil
return st, nil
}
type unmarshaller func([]byte, interface{}) error
@@ -481,31 +477,30 @@ func unmarshalAnything(data []byte, fn unmarshaller) (interface{}, error) {
return o, err
}
func (p *Pipeline) Load(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) Load(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
// Execute 'from' in a tmp pipeline, and use the resulting fs
from := p.Tmp(op.Get("from").Path().String())
if err := from.Do(ctx, op.Get("from")); err != nil {
return err
return st, err
}
p.fs = p.fs.Set(from.FS().LLB())
return nil
return from.State(), nil
}
func (p *Pipeline) FetchContainer(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) FetchContainer(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
rawRef, err := op.Get("ref").String()
if err != nil {
return err
return st, err
}
ref, err := reference.ParseNormalizedNamed(rawRef)
if err != nil {
return fmt.Errorf("failed to parse ref %s: %w", rawRef, err)
return st, fmt.Errorf("failed to parse ref %s: %w", rawRef, err)
}
// Add the default tag "latest" to a reference if it only has a repo name.
ref = reference.TagNameOnly(ref)
state := llb.Image(
st = llb.Image(
ref.String(),
llb.WithCustomName(p.vertexNamef("FetchContainer %s", rawRef)),
)
@@ -517,21 +512,20 @@ func (p *Pipeline) FetchContainer(ctx context.Context, op *compiler.Value) error
LogName: p.vertexNamef("load metadata for %s", ref.String()),
})
if err != nil {
return err
return st, err
}
for _, env := range image.Config.Env {
k, v := parseKeyValue(env)
state = state.AddEnv(k, v)
st = st.AddEnv(k, v)
}
if image.Config.WorkingDir != "" {
state = state.Dir(image.Config.WorkingDir)
st = st.Dir(image.Config.WorkingDir)
}
if image.Config.User != "" {
state = state.User(image.Config.User)
st = st.User(image.Config.User)
}
p.fs = p.fs.Set(state)
return nil
return st, nil
}
func parseKeyValue(env string) (string, string) {
@@ -544,45 +538,52 @@ func parseKeyValue(env string) (string, string) {
return parts[0], v
}
func (p *Pipeline) PushContainer(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) PushContainer(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
rawRef, err := op.Get("ref").String()
if err != nil {
return err
return st, err
}
ref, err := reference.ParseNormalizedNamed(rawRef)
if err != nil {
return fmt.Errorf("failed to parse ref %s: %w", rawRef, err)
return st, fmt.Errorf("failed to parse ref %s: %w", rawRef, err)
}
// Add the default tag "latest" to a reference if it only has a repo name.
ref = reference.TagNameOnly(ref)
_, err = p.fs.Export(ctx, bk.ExportEntry{
pushSt, err := p.result.ToState()
if err != nil {
return st, err
}
_, err = p.s.Export(ctx, pushSt, bk.ExportEntry{
Type: bk.ExporterImage,
Attrs: map[string]string{
"name": ref.String(),
"push": "true",
},
})
return err
return st, err
}
func (p *Pipeline) FetchGit(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) FetchGit(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
remote, err := op.Get("remote").String()
if err != nil {
return err
return st, err
}
ref, err := op.Get("ref").String()
if err != nil {
return err
return st, err
}
p.fs = p.fs.Set(
llb.Git(remote, ref, llb.WithCustomName(p.vertexNamef("FetchGit %s@%s", remote, ref))),
)
return nil
return llb.Git(
remote,
ref,
llb.WithCustomName(p.vertexNamef("FetchGit %s@%s", remote, ref)),
), nil
}
func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
var (
context = op.Lookup("context")
dockerfile = op.Lookup("dockerfile")
@@ -594,7 +595,7 @@ func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
)
if !context.Exists() && !dockerfile.Exists() {
return errors.New("context or dockerfile required")
return st, errors.New("context or dockerfile required")
}
// docker build context. This can come from another component, so we need to
@@ -602,11 +603,11 @@ func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
if context.Exists() {
from := p.Tmp(op.Lookup("context").Path().String())
if err := from.Do(ctx, context); err != nil {
return err
return st, err
}
contextDef, err = from.FS().Def(ctx)
contextDef, err = p.s.Marshal(ctx, from.State())
if err != nil {
return err
return st, err
}
dockerfileDef = contextDef
}
@@ -615,15 +616,15 @@ func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
if dockerfile.Exists() {
content, err := dockerfile.String()
if err != nil {
return err
return st, err
}
dockerfileDef, err = p.s.Scratch().Set(
dockerfileDef, err = p.s.Marshal(ctx,
llb.Scratch().File(
llb.Mkfile("/Dockerfile", 0644, []byte(content)),
),
).Def(ctx)
)
if err != nil {
return err
return st, err
}
if contextDef == nil {
contextDef = dockerfileDef
@@ -642,7 +643,7 @@ func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
if dockerfilePath := op.Lookup("dockerfilePath"); dockerfilePath.Exists() {
filename, err := dockerfilePath.String()
if err != nil {
return err
return st, err
}
req.FrontendOpt["filename"] = filename
}
@@ -657,7 +658,7 @@ func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
return nil
})
if err != nil {
return err
return st, err
}
}
@@ -671,7 +672,7 @@ func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
return nil
})
if err != nil {
return err
return st, err
}
}
@@ -679,13 +680,13 @@ func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
p := []string{}
list, err := platforms.List()
if err != nil {
return err
return st, err
}
for _, platform := range list {
s, err := platform.String()
if err != nil {
return err
return st, err
}
p = append(p, s)
}
@@ -700,65 +701,51 @@ func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value) error {
res, err := p.s.SolveRequest(ctx, req)
if err != nil {
return err
return st, err
}
st, err := res.ToState()
if err != nil {
return err
}
p.fs = p.fs.Set(st)
return nil
return res.ToState()
}
func (p *Pipeline) WriteFile(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) WriteFile(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
content, err := op.Get("content").String()
if err != nil {
return err
return st, err
}
dest, err := op.Get("dest").String()
if err != nil {
return err
return st, err
}
mode, err := op.Get("mode").Int64()
if err != nil {
return err
return st, err
}
p.fs = p.fs.Change(func(st llb.State) llb.State {
return st.File(
llb.Mkfile(dest, fs.FileMode(mode), []byte(content)),
llb.WithCustomName(p.vertexNamef("WriteFile %s", dest)),
)
})
return nil
return st.File(
llb.Mkfile(dest, fs.FileMode(mode), []byte(content)),
llb.WithCustomName(p.vertexNamef("WriteFile %s", dest)),
), nil
}
func (p *Pipeline) Mkdir(ctx context.Context, op *compiler.Value) error {
func (p *Pipeline) Mkdir(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
path, err := op.Get("path").String()
if err != nil {
return err
return st, err
}
dir, err := op.Get("dir").String()
if err != nil {
return err
return st, err
}
mode, err := op.Get("mode").Int64()
if err != nil {
return err
return st, err
}
p.fs = p.fs.Change(func(st llb.State) llb.State {
return st.Dir(dir).File(
llb.Mkdir(path, fs.FileMode(mode)),
llb.WithCustomName(p.vertexNamef("Mkdir %s", path)),
)
})
return nil
return st.Dir(dir).File(
llb.Mkdir(path, fs.FileMode(mode)),
llb.WithCustomName(p.vertexNamef("Mkdir %s", path)),
), nil
}