ac34df319a
- This PR adds a new mount type: `docker.sock` (in addition to `cache` and `tmp`) - It's then able to mount the LOCAL (as in, from the machine running dagger) docker socket inside the container by pretending to be an SSH Agent (hijacking the SSH agent forwarding support of buildkit) Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
1045 lines
24 KiB
Go
1045 lines
24 KiB
Go
package environment
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"cuelang.org/go/cue"
|
|
"github.com/docker/distribution/reference"
|
|
bk "github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/client/llb"
|
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
|
dockerfilebuilder "github.com/moby/buildkit/frontend/dockerfile/builder"
|
|
"github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb"
|
|
bkgw "github.com/moby/buildkit/frontend/gateway/client"
|
|
bkpb "github.com/moby/buildkit/solver/pb"
|
|
"github.com/rs/zerolog/log"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"go.dagger.io/dagger/compiler"
|
|
"go.dagger.io/dagger/solver"
|
|
)
|
|
|
|
// An execution pipeline
|
|
type Pipeline struct {
|
|
code *compiler.Value
|
|
name string
|
|
s solver.Solver
|
|
state llb.State
|
|
result bkgw.Reference
|
|
image dockerfile2llb.Image
|
|
computed *compiler.Value
|
|
}
|
|
|
|
func NewPipeline(code *compiler.Value, s solver.Solver) *Pipeline {
|
|
return &Pipeline{
|
|
code: code,
|
|
name: code.Path().String(),
|
|
s: s,
|
|
state: llb.Scratch(),
|
|
computed: compiler.NewValue(),
|
|
}
|
|
}
|
|
|
|
func (p *Pipeline) WithCustomName(name string) *Pipeline {
|
|
p.name = name
|
|
return p
|
|
}
|
|
|
|
func (p *Pipeline) State() llb.State {
|
|
return p.state
|
|
}
|
|
|
|
func (p *Pipeline) Result() (llb.State, error) {
|
|
if p.result == nil {
|
|
return llb.Scratch(), nil
|
|
}
|
|
return p.result.ToState()
|
|
}
|
|
|
|
func (p *Pipeline) FS() fs.FS {
|
|
return solver.NewBuildkitFS(p.result)
|
|
}
|
|
|
|
func (p *Pipeline) ImageConfig() dockerfile2llb.Image {
|
|
return p.image
|
|
}
|
|
|
|
func (p *Pipeline) Computed() *compiler.Value {
|
|
return p.computed
|
|
}
|
|
|
|
func isComponent(v *compiler.Value) bool {
|
|
return v.Lookup("#up").Exists()
|
|
}
|
|
|
|
func ops(code *compiler.Value) ([]*compiler.Value, error) {
|
|
ops := []*compiler.Value{}
|
|
// 1. attachment array
|
|
if isComponent(code) {
|
|
xops, err := code.Lookup("#up").List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 'from' has an executable attached
|
|
ops = append(ops, xops...)
|
|
// 2. individual op
|
|
} else if _, err := code.Lookup("do").String(); err == nil {
|
|
ops = append(ops, code)
|
|
// 3. op array
|
|
} else if xops, err := code.List(); err == nil {
|
|
ops = append(ops, xops...)
|
|
} else {
|
|
// 4. error
|
|
source, err := code.Source()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return nil, fmt.Errorf("not executable: %s", source)
|
|
}
|
|
return ops, nil
|
|
}
|
|
|
|
func Analyze(fn func(*compiler.Value) error, code *compiler.Value) error {
|
|
ops, err := ops(code)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, op := range ops {
|
|
if err := analyzeOp(fn, op); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func analyzeOp(fn func(*compiler.Value) error, op *compiler.Value) error {
|
|
if err := fn(op); err != nil {
|
|
return err
|
|
}
|
|
do, err := op.Lookup("do").String()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch do {
|
|
case "load", "copy":
|
|
return Analyze(fn, op.Lookup("from"))
|
|
case "exec":
|
|
fields, err := op.Lookup("mount").Fields()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, mnt := range fields {
|
|
if from := mnt.Value.Lookup("from"); from.Exists() {
|
|
return Analyze(fn, from)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Pipeline) Run(ctx context.Context) error {
|
|
ops, err := ops(p.code)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Execute each operation in sequence
|
|
for idx, op := range ops {
|
|
// If op not concrete, interrupt without error.
|
|
// This allows gradual resolution:
|
|
// compute what you can compute.. leave the rest incomplete.
|
|
if err := op.IsConcreteR(); err != nil {
|
|
log.
|
|
Ctx(ctx).
|
|
Warn().
|
|
Str("original_cue_error", err.Error()).
|
|
Int("op", idx).
|
|
Msg("pipeline was partially executed because of missing inputs")
|
|
return 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.
|
|
p.result, err = p.s.Solve(ctx, p.state)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Pipeline) doOp(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
do, err := op.Lookup("do").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
// FIXME: make this more readable then promote to INFO
|
|
// we need a readable trace of what operations are executed.
|
|
log.
|
|
Ctx(ctx).
|
|
Debug().
|
|
Str("pipeline", p.name).
|
|
Str("do", do).
|
|
Msg("executing operation")
|
|
switch do {
|
|
case "copy":
|
|
return p.Copy(ctx, op, st)
|
|
case "exec":
|
|
return p.Exec(ctx, op, st)
|
|
case "export":
|
|
return p.Export(ctx, op, st)
|
|
case "docker-login":
|
|
return p.DockerLogin(ctx, op, st)
|
|
case "fetch-container":
|
|
return p.FetchContainer(ctx, op, st)
|
|
case "push-container":
|
|
return p.PushContainer(ctx, op, st)
|
|
case "fetch-git":
|
|
return p.FetchGit(ctx, op, st)
|
|
case "local":
|
|
return p.Local(ctx, op, st)
|
|
case "load":
|
|
return p.Load(ctx, op, st)
|
|
case "workdir":
|
|
return p.Workdir(ctx, op, st)
|
|
case "subdir":
|
|
return p.Subdir(ctx, op, st)
|
|
case "docker-build":
|
|
return p.DockerBuild(ctx, op, st)
|
|
case "write-file":
|
|
return p.WriteFile(ctx, op, st)
|
|
case "mkdir":
|
|
return p.Mkdir(ctx, op, st)
|
|
default:
|
|
return st, fmt.Errorf("invalid operation: %s", op.JSON())
|
|
}
|
|
}
|
|
|
|
func (p *Pipeline) vertexNamef(format string, a ...interface{}) string {
|
|
prefix := fmt.Sprintf("@%s@", p.name)
|
|
name := fmt.Sprintf(format, a...)
|
|
return prefix + " " + name
|
|
}
|
|
|
|
func (p *Pipeline) Workdir(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
path, err := op.Lookup("path").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
return st.Dir(path), nil
|
|
}
|
|
|
|
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.Lookup("dir").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
return llb.Scratch().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, st llb.State) (llb.State, error) {
|
|
// Decode copy options
|
|
src, err := op.Lookup("src").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
dest, err := op.Lookup("dest").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
// Execute 'from' in a tmp pipeline, and use the resulting fs
|
|
from := NewPipeline(op.Lookup("from"), p.s)
|
|
if err := from.Run(ctx); err != nil {
|
|
return st, err
|
|
}
|
|
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, st llb.State) (llb.State, error) {
|
|
dir, err := op.Lookup("dir").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
opts := []llb.LocalOption{
|
|
llb.WithCustomName(p.vertexNamef("Local %s", dir)),
|
|
// Without hint, multiple `llb.Local` operations on the
|
|
// same path get a different digest.
|
|
llb.SessionID(p.s.SessionID()),
|
|
llb.SharedKeyHint(dir),
|
|
}
|
|
|
|
includes, err := op.Lookup("include").List()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
if len(includes) > 0 {
|
|
includePatterns := []string{}
|
|
for _, i := range includes {
|
|
pattern, err := i.String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
includePatterns = append(includePatterns, pattern)
|
|
}
|
|
opts = append(opts, llb.IncludePatterns(includePatterns))
|
|
}
|
|
|
|
excludes, err := op.Lookup("exclude").List()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
if len(excludes) > 0 {
|
|
excludePatterns := []string{}
|
|
for _, i := range excludes {
|
|
pattern, err := i.String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
excludePatterns = append(excludePatterns, pattern)
|
|
}
|
|
|
|
opts = append(opts, llb.ExcludePatterns(excludePatterns))
|
|
}
|
|
|
|
return llb.Local(
|
|
dir,
|
|
opts...,
|
|
), nil
|
|
}
|
|
|
|
func (p *Pipeline) Exec(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
opts := []llb.RunOption{}
|
|
var cmd struct {
|
|
Args []string
|
|
Dir string
|
|
Always bool
|
|
}
|
|
|
|
if err := op.Decode(&cmd); err != nil {
|
|
return st, err
|
|
}
|
|
// args
|
|
opts = append(opts, llb.Args(cmd.Args))
|
|
// dir
|
|
opts = append(opts, llb.Dir(cmd.Dir))
|
|
|
|
// env
|
|
if env := op.Lookup("env"); env.Exists() {
|
|
envs, err := op.Lookup("env").Fields()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
for _, env := range envs {
|
|
v, err := env.Value.String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
opts = append(opts, llb.AddEnv(env.Label(), v))
|
|
}
|
|
}
|
|
|
|
// always?
|
|
if cmd.Always {
|
|
// FIXME: also disables persistent cache directories
|
|
// There's an ongoing proposal that would fix this: https://github.com/moby/buildkit/issues/1213
|
|
opts = append(opts, llb.IgnoreCache)
|
|
}
|
|
|
|
if hosts := op.Lookup("hosts"); hosts.Exists() {
|
|
fields, err := hosts.Fields()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
for _, host := range fields {
|
|
s, err := host.Value.String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
opts = append(opts, llb.AddExtraHost(host.Label(), net.ParseIP(s)))
|
|
}
|
|
}
|
|
|
|
if user := op.Lookup("user"); user.Exists() {
|
|
u, err := user.String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
opts = append(opts, llb.User(u))
|
|
}
|
|
|
|
// mounts
|
|
if mounts := op.Lookup("mount"); mounts.Exists() {
|
|
mntOpts, err := p.mountAll(ctx, mounts)
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
opts = append(opts, mntOpts...)
|
|
}
|
|
|
|
// marker for status events
|
|
// FIXME
|
|
args := make([]string, 0, len(cmd.Args))
|
|
for _, a := range cmd.Args {
|
|
args = append(args, fmt.Sprintf("%q", a))
|
|
}
|
|
opts = append(opts, llb.WithCustomName(p.vertexNamef("Exec [%s]", strings.Join(args, ", "))))
|
|
|
|
// --> Execute
|
|
return st.Run(opts...).Root(), nil
|
|
}
|
|
|
|
func (p *Pipeline) mountAll(ctx context.Context, mounts *compiler.Value) ([]llb.RunOption, error) {
|
|
opts := []llb.RunOption{}
|
|
fields, err := mounts.Fields()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, mnt := range fields {
|
|
o, err := p.mount(ctx, mnt.Label(), mnt.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts = append(opts, o)
|
|
}
|
|
return opts, err
|
|
}
|
|
|
|
func (p *Pipeline) mount(ctx context.Context, dest string, mnt *compiler.Value) (llb.RunOption, error) {
|
|
if s, err := mnt.String(); err == nil {
|
|
// eg. mount: "/foo": "cache"
|
|
switch s {
|
|
case "cache":
|
|
return llb.AddMount(
|
|
dest,
|
|
llb.Scratch(),
|
|
llb.AsPersistentCacheDir(
|
|
p.canonicalPath(mnt),
|
|
llb.CacheMountShared,
|
|
),
|
|
), nil
|
|
case "tmpfs":
|
|
return llb.AddMount(
|
|
dest,
|
|
llb.Scratch(),
|
|
llb.Tmpfs(),
|
|
), nil
|
|
case "docker.sock":
|
|
return llb.AddSSHSocket(
|
|
llb.SSHID(solver.DockerSocketID),
|
|
llb.SSHSocketTarget(dest),
|
|
), nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid mount source: %q", s)
|
|
}
|
|
}
|
|
// eg. mount: "/foo": secret: mysecret
|
|
if secret := mnt.Lookup("secret"); secret.Exists() {
|
|
if !secret.HasAttr("secret") {
|
|
return nil, fmt.Errorf("invalid secret %q: not a secret", secret.Path().String())
|
|
}
|
|
idValue := secret.Lookup("id")
|
|
if !idValue.Exists() {
|
|
return nil, fmt.Errorf("invalid secret %q: no id field", secret.Path().String())
|
|
}
|
|
id, err := idValue.String()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid secret id: %w", err)
|
|
}
|
|
return llb.AddSecret(dest,
|
|
llb.SecretID(id),
|
|
llb.SecretFileOpt(0, 0, 0400), // uid, gid, mask)
|
|
), nil
|
|
}
|
|
|
|
// eg. mount: "/foo": { from: www.source }
|
|
from := NewPipeline(mnt.Lookup("from"), p.s)
|
|
if err := from.Run(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
// possibly construct mount options for LLB from
|
|
var mo []llb.MountOption
|
|
// handle "path" option
|
|
if mp := mnt.Lookup("path"); mp.Exists() {
|
|
mps, err := mp.String()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mo = append(mo, llb.SourcePath(mps))
|
|
}
|
|
return llb.AddMount(dest, from.State(), mo...), nil
|
|
}
|
|
|
|
// canonicalPath returns the canonical path of `v`
|
|
// If the pipeline is a reference to another pipeline, `canonicalPath()` will
|
|
// return the path of the reference of `v`.
|
|
// FIXME: this doesn't work with references of references.
|
|
func (p *Pipeline) canonicalPath(v *compiler.Value) string {
|
|
// value path
|
|
vPath := v.Path().Selectors()
|
|
|
|
// pipeline path
|
|
pipelinePath := p.code.Path().Selectors()
|
|
|
|
// check if the pipeline is a reference
|
|
_, ref := p.code.ReferencePath()
|
|
if len(ref.Selectors()) == 0 {
|
|
return v.Path().String()
|
|
}
|
|
canonicalPipelinePath := ref.Selectors()
|
|
|
|
// replace the pipeline path with the canonical pipeline path
|
|
// 1. strip the pipeline path from the value path
|
|
vPath = vPath[len(pipelinePath):]
|
|
// 2. inject the canonical pipeline path
|
|
vPath = append(canonicalPipelinePath, vPath...)
|
|
|
|
return cue.MakePath(vPath...).String()
|
|
}
|
|
|
|
func (p *Pipeline) Export(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
source, err := op.Lookup("source").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
format, err := op.Lookup("format").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
contents, err := fs.ReadFile(p.FS(), source)
|
|
if err != nil {
|
|
return st, fmt.Errorf("export %s: %w", source, err)
|
|
}
|
|
switch format {
|
|
case "string":
|
|
log.
|
|
Ctx(ctx).
|
|
Debug().
|
|
Bytes("contents", contents).
|
|
Msg("exporting string")
|
|
|
|
if err := p.computed.FillPath(cue.MakePath(), string(contents)); err != nil {
|
|
return st, err
|
|
}
|
|
case "json":
|
|
var o interface{}
|
|
o, err := unmarshalAnything(contents, json.Unmarshal)
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
log.
|
|
Ctx(ctx).
|
|
Debug().
|
|
Interface("contents", o).
|
|
Msg("exporting json")
|
|
|
|
if err := p.computed.FillPath(cue.MakePath(), o); err != nil {
|
|
return st, err
|
|
}
|
|
case "yaml":
|
|
var o interface{}
|
|
o, err := unmarshalAnything(contents, yaml.Unmarshal)
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
log.
|
|
Ctx(ctx).
|
|
Debug().
|
|
Interface("contents", o).
|
|
Msg("exporting yaml")
|
|
|
|
if err := p.computed.FillPath(cue.MakePath(), o); err != nil {
|
|
return st, err
|
|
}
|
|
default:
|
|
return st, fmt.Errorf("unsupported export format: %q", format)
|
|
}
|
|
return st, nil
|
|
}
|
|
|
|
type unmarshaller func([]byte, interface{}) error
|
|
|
|
func unmarshalAnything(data []byte, fn unmarshaller) (interface{}, error) {
|
|
// unmarshalling a map into interface{} yields an error:
|
|
// "unsupported Go type for map key (interface {})"
|
|
// we want to attempt to unmarshal to a map[string]interface{} first
|
|
var oMap map[string]interface{}
|
|
if err := fn(data, &oMap); err == nil {
|
|
return oMap, nil
|
|
}
|
|
|
|
// If the previous attempt didn't work, we might be facing a scalar (e.g.
|
|
// bool).
|
|
// Try to unmarshal to interface{} directly.
|
|
var o interface{}
|
|
err := fn(data, &o)
|
|
return o, err
|
|
}
|
|
|
|
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 := NewPipeline(op.Lookup("from"), p.s)
|
|
if err := from.Run(ctx); err != nil {
|
|
return st, err
|
|
}
|
|
p.image = from.ImageConfig()
|
|
return from.State(), nil
|
|
}
|
|
|
|
func (p *Pipeline) DockerLogin(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
username, err := op.Lookup("username").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
secret, err := op.Lookup("secret").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
target, err := op.Lookup("target").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
p.s.AddCredentials(target, username, secret)
|
|
log.
|
|
Ctx(ctx).
|
|
Debug().
|
|
Str("target", target).
|
|
Msg("docker login to registry")
|
|
|
|
return st, nil
|
|
}
|
|
|
|
func (p *Pipeline) FetchContainer(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
rawRef, err := op.Lookup("ref").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
ref, err := reference.ParseNormalizedNamed(rawRef)
|
|
if err != nil {
|
|
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)
|
|
|
|
st = llb.Image(
|
|
ref.String(),
|
|
llb.WithCustomName(p.vertexNamef("FetchContainer %s", rawRef)),
|
|
)
|
|
|
|
// Load image metadata and convert to to LLB.
|
|
p.image, err = p.s.ResolveImageConfig(ctx, ref.String(), llb.ResolveImageConfigOpt{
|
|
LogName: p.vertexNamef("load metadata for %s", ref.String()),
|
|
})
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
return applyImageToState(p.image, st), nil
|
|
}
|
|
|
|
// applyImageToState converts an image config into LLB instructions
|
|
func applyImageToState(image dockerfile2llb.Image, st llb.State) llb.State {
|
|
// FIXME: there are unhandled sections of the image config
|
|
for _, env := range image.Config.Env {
|
|
k, v := parseKeyValue(env)
|
|
st = st.AddEnv(k, v)
|
|
}
|
|
if image.Config.WorkingDir != "" {
|
|
st = st.Dir(image.Config.WorkingDir)
|
|
}
|
|
if image.Config.User != "" {
|
|
st = st.User(image.Config.User)
|
|
}
|
|
return st
|
|
}
|
|
|
|
func parseKeyValue(env string) (string, string) {
|
|
parts := strings.SplitN(env, "=", 2)
|
|
v := ""
|
|
if len(parts) > 1 {
|
|
v = parts[1]
|
|
}
|
|
|
|
return parts[0], v
|
|
}
|
|
|
|
func (p *Pipeline) PushContainer(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
rawRef, err := op.Lookup("ref").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
ref, err := reference.ParseNormalizedNamed(rawRef)
|
|
if err != nil {
|
|
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)
|
|
|
|
resp, err := p.s.Export(ctx, p.State(), &p.image, bk.ExportEntry{
|
|
Type: bk.ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": ref.String(),
|
|
"push": "true",
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
if digest, ok := resp.ExporterResponse["containerimage.digest"]; ok {
|
|
imageRef := fmt.Sprintf(
|
|
"%s@%s",
|
|
resp.ExporterResponse["image.name"],
|
|
digest,
|
|
)
|
|
|
|
return st.File(
|
|
llb.Mkdir("/dagger", fs.FileMode(0755)),
|
|
llb.WithCustomName(p.vertexNamef("Mkdir /dagger")),
|
|
).File(
|
|
llb.Mkfile("/dagger/image_digest", fs.FileMode(0644), []byte(digest)),
|
|
llb.WithCustomName(p.vertexNamef("Storing image digest to /dagger/image_digest")),
|
|
).File(
|
|
llb.Mkfile("/dagger/image_ref", fs.FileMode(0644), []byte(imageRef)),
|
|
llb.WithCustomName(p.vertexNamef("Storing image ref to /dagger/image_ref")),
|
|
), nil
|
|
}
|
|
|
|
return st, err
|
|
}
|
|
|
|
func (p *Pipeline) FetchGit(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
remote, err := op.Lookup("remote").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
ref, err := op.Lookup("ref").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
remoteRedacted := remote
|
|
if u, err := url.Parse(remote); err == nil {
|
|
remoteRedacted = u.Redacted()
|
|
}
|
|
|
|
gitOpts := []llb.GitOption{}
|
|
var opts struct {
|
|
AuthTokenSecret string
|
|
AuthHeaderSecret string
|
|
KeepGitDir bool
|
|
}
|
|
|
|
if err := op.Decode(&opts); err != nil {
|
|
return st, err
|
|
}
|
|
|
|
if opts.KeepGitDir {
|
|
gitOpts = append(gitOpts, llb.KeepGitDir())
|
|
}
|
|
if opts.AuthTokenSecret != "" {
|
|
gitOpts = append(gitOpts, llb.AuthTokenSecret(opts.AuthTokenSecret))
|
|
}
|
|
if opts.AuthHeaderSecret != "" {
|
|
gitOpts = append(gitOpts, llb.AuthTokenSecret(opts.AuthHeaderSecret))
|
|
}
|
|
|
|
gitOpts = append(gitOpts, llb.WithCustomName(p.vertexNamef("FetchGit %s@%s", remoteRedacted, ref)))
|
|
|
|
return llb.Git(
|
|
remote,
|
|
ref,
|
|
gitOpts...,
|
|
), nil
|
|
}
|
|
|
|
func (p *Pipeline) DockerBuild(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
var (
|
|
dockerContext = op.Lookup("context")
|
|
dockerfile = op.Lookup("dockerfile")
|
|
|
|
contextDef *bkpb.Definition
|
|
dockerfileDef *bkpb.Definition
|
|
|
|
err error
|
|
)
|
|
|
|
if !dockerContext.Exists() && !dockerfile.Exists() {
|
|
return st, errors.New("context or dockerfile required")
|
|
}
|
|
|
|
// docker build context. This can come from another component, so we need to
|
|
// compute it first.
|
|
if dockerContext.Exists() {
|
|
from := NewPipeline(op.Lookup("context"), p.s)
|
|
if err := from.Run(ctx); err != nil {
|
|
return st, err
|
|
}
|
|
contextDef, err = p.s.Marshal(ctx, from.State())
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
dockerfileDef = contextDef
|
|
}
|
|
|
|
// Inlined dockerfile: need to be converted to LLB
|
|
if dockerfile.Exists() {
|
|
content, err := dockerfile.String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
dockerfileDef, err = p.s.Marshal(ctx,
|
|
llb.Scratch().File(
|
|
llb.Mkfile("/Dockerfile", 0644, []byte(content)),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
if contextDef == nil {
|
|
contextDef = dockerfileDef
|
|
}
|
|
}
|
|
|
|
opts, err := dockerBuildOpts(op)
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
if p.s.NoCache() {
|
|
opts["no-cache"] = ""
|
|
}
|
|
|
|
req := bkgw.SolveRequest{
|
|
Frontend: "dockerfile.v0",
|
|
FrontendOpt: opts,
|
|
FrontendInputs: map[string]*bkpb.Definition{
|
|
dockerfilebuilder.DefaultLocalNameContext: contextDef,
|
|
dockerfilebuilder.DefaultLocalNameDockerfile: dockerfileDef,
|
|
},
|
|
}
|
|
|
|
res, err := p.s.SolveRequest(ctx, req)
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
if meta, ok := res.Metadata[exptypes.ExporterImageConfigKey]; ok {
|
|
if err := json.Unmarshal(meta, &p.image); err != nil {
|
|
return st, fmt.Errorf("failed to unmarshal image config: %w", err)
|
|
}
|
|
}
|
|
|
|
ref, err := res.SingleRef()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
st, err = ref.ToState()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
return applyImageToState(p.image, st), nil
|
|
}
|
|
|
|
func dockerBuildOpts(op *compiler.Value) (map[string]string, error) {
|
|
opts := map[string]string{}
|
|
|
|
if dockerfilePath := op.Lookup("dockerfilePath"); dockerfilePath.Exists() {
|
|
filename, err := dockerfilePath.String()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts["filename"] = filename
|
|
}
|
|
|
|
if target := op.Lookup("target"); target.Exists() {
|
|
tgr, err := target.String()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts["target"] = tgr
|
|
}
|
|
|
|
if hosts := op.Lookup("hosts"); hosts.Exists() {
|
|
p := []string{}
|
|
fields, err := hosts.Fields()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, host := range fields {
|
|
s, err := host.Value.String()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p = append(p, host.Label()+"="+s)
|
|
}
|
|
if len(p) > 0 {
|
|
opts["add-hosts"] = strings.Join(p, ",")
|
|
}
|
|
}
|
|
|
|
if buildArgs := op.Lookup("buildArg"); buildArgs.Exists() {
|
|
fields, err := buildArgs.Fields()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, buildArg := range fields {
|
|
v, err := buildArg.Value.String()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts["build-arg:"+buildArg.Label()] = v
|
|
}
|
|
}
|
|
|
|
if labels := op.Lookup("label"); labels.Exists() {
|
|
fields, err := labels.Fields()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, label := range fields {
|
|
s, err := label.Value.String()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts["label:"+label.Label()] = s
|
|
}
|
|
}
|
|
|
|
if platforms := op.Lookup("platforms"); platforms.Exists() {
|
|
p := []string{}
|
|
list, err := platforms.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, platform := range list {
|
|
s, err := platform.String()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p = append(p, s)
|
|
}
|
|
|
|
if len(p) > 0 {
|
|
opts["platform"] = strings.Join(p, ",")
|
|
}
|
|
if len(p) > 1 {
|
|
opts["multi-platform"] = "true"
|
|
}
|
|
}
|
|
|
|
return opts, nil
|
|
}
|
|
|
|
func (p *Pipeline) WriteFile(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
var content []byte
|
|
var err error
|
|
|
|
switch kind := op.Lookup("content").Kind(); kind {
|
|
case cue.BytesKind:
|
|
content, err = op.Lookup("content").Bytes()
|
|
case cue.StringKind:
|
|
var str string
|
|
str, err = op.Lookup("content").String()
|
|
if err == nil {
|
|
content = []byte(str)
|
|
}
|
|
case cue.BottomKind:
|
|
err = fmt.Errorf("%s: WriteFile content is not set", p.canonicalPath(op))
|
|
default:
|
|
err = fmt.Errorf("%s: unhandled data type in WriteFile: %s", p.canonicalPath(op), kind)
|
|
}
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
dest, err := op.Lookup("dest").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
mode, err := op.Lookup("mode").Int64()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
return st.File(
|
|
llb.Mkfile(dest, fs.FileMode(mode), content),
|
|
llb.WithCustomName(p.vertexNamef("WriteFile %s", dest)),
|
|
), nil
|
|
}
|
|
|
|
func (p *Pipeline) Mkdir(ctx context.Context, op *compiler.Value, st llb.State) (llb.State, error) {
|
|
pathString, err := op.Lookup("path").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
dir, err := op.Lookup("dir").String()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
mode, err := op.Lookup("mode").Int64()
|
|
if err != nil {
|
|
return st, err
|
|
}
|
|
|
|
return st.Dir(dir).File(
|
|
llb.Mkdir(pathString, fs.FileMode(mode)),
|
|
llb.WithCustomName(p.vertexNamef("Mkdir %s", pathString)),
|
|
), nil
|
|
}
|