Merge pull request #1733 from jedevc/use-dockerui-context-detection
Use dockerui context detectionpull/1740/head
commit
7d35a3b8d8
@ -0,0 +1,81 @@
|
|||||||
|
package attestations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyTypeSbom = "sbom"
|
||||||
|
KeyTypeProvenance = "provenance"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultSBOMGenerator = "docker/buildkit-syft-scanner:stable-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Filter(v map[string]string) map[string]string {
|
||||||
|
attests := make(map[string]string)
|
||||||
|
for k, v := range v {
|
||||||
|
if strings.HasPrefix(k, "attest:") {
|
||||||
|
attests[k] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(k, "build-arg:BUILDKIT_ATTEST_") {
|
||||||
|
attests[k] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attests
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(values map[string]map[string]string) (map[string]map[string]string, error) {
|
||||||
|
for k := range values {
|
||||||
|
if k != KeyTypeSbom && k != KeyTypeProvenance {
|
||||||
|
return nil, errors.Errorf("unknown attestation type %q", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(values map[string]string) (map[string]map[string]string, error) {
|
||||||
|
attests := make(map[string]string)
|
||||||
|
for k, v := range values {
|
||||||
|
if strings.HasPrefix(k, "attest:") {
|
||||||
|
attests[strings.ToLower(strings.TrimPrefix(k, "attest:"))] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(k, "build-arg:BUILDKIT_ATTEST_") {
|
||||||
|
attests[strings.ToLower(strings.TrimPrefix(k, "build-arg:BUILDKIT_ATTEST_"))] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]map[string]string)
|
||||||
|
for k, v := range attests {
|
||||||
|
attrs := make(map[string]string)
|
||||||
|
out[k] = attrs
|
||||||
|
if k == KeyTypeSbom {
|
||||||
|
attrs["generator"] = defaultSBOMGenerator
|
||||||
|
}
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
csvReader := csv.NewReader(strings.NewReader(v))
|
||||||
|
fields, err := csvReader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to parse %s", k)
|
||||||
|
}
|
||||||
|
for _, field := range fields {
|
||||||
|
parts := strings.SplitN(field, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
parts = append(parts, "")
|
||||||
|
}
|
||||||
|
attrs[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Validate(out)
|
||||||
|
}
|
65
vendor/github.com/moby/buildkit/frontend/dockerfile/dockerignore/dockerignore.go
generated
vendored
65
vendor/github.com/moby/buildkit/frontend/dockerfile/dockerignore/dockerignore.go
generated
vendored
@ -0,0 +1,65 @@
|
|||||||
|
package dockerignore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadAll reads a .dockerignore file and returns the list of file patterns
|
||||||
|
// to ignore. Note this will trim whitespace from each line as well
|
||||||
|
// as use GO's "clean" func to get the shortest/cleanest path for each.
|
||||||
|
func ReadAll(reader io.Reader) ([]string, error) {
|
||||||
|
if reader == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
var excludes []string
|
||||||
|
currentLine := 0
|
||||||
|
|
||||||
|
utf8bom := []byte{0xEF, 0xBB, 0xBF}
|
||||||
|
for scanner.Scan() {
|
||||||
|
scannedBytes := scanner.Bytes()
|
||||||
|
// We trim UTF8 BOM
|
||||||
|
if currentLine == 0 {
|
||||||
|
scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom)
|
||||||
|
}
|
||||||
|
pattern := string(scannedBytes)
|
||||||
|
currentLine++
|
||||||
|
// Lines starting with # (comments) are ignored before processing
|
||||||
|
if strings.HasPrefix(pattern, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pattern = strings.TrimSpace(pattern)
|
||||||
|
if pattern == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// normalize absolute paths to paths relative to the context
|
||||||
|
// (taking care of '!' prefix)
|
||||||
|
invert := pattern[0] == '!'
|
||||||
|
if invert {
|
||||||
|
pattern = strings.TrimSpace(pattern[1:])
|
||||||
|
}
|
||||||
|
if len(pattern) > 0 {
|
||||||
|
pattern = filepath.Clean(pattern)
|
||||||
|
pattern = filepath.ToSlash(pattern)
|
||||||
|
if len(pattern) > 1 && pattern[0] == '/' {
|
||||||
|
pattern = pattern[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if invert {
|
||||||
|
pattern = "!" + pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
excludes = append(excludes, pattern)
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error reading .dockerignore")
|
||||||
|
}
|
||||||
|
return excludes, nil
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
package dockerui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/platforms"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/moby/buildkit/client/llb"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parsePlatforms(v string) ([]ocispecs.Platform, error) {
|
||||||
|
var pp []ocispecs.Platform
|
||||||
|
for _, v := range strings.Split(v, ",") {
|
||||||
|
p, err := platforms.Parse(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to parse target platform %s", v)
|
||||||
|
}
|
||||||
|
pp = append(pp, platforms.Normalize(p))
|
||||||
|
}
|
||||||
|
return pp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResolveMode(v string) (llb.ResolveMode, error) {
|
||||||
|
switch v {
|
||||||
|
case pb.AttrImageResolveModeDefault, "":
|
||||||
|
return llb.ResolveModeDefault, nil
|
||||||
|
case pb.AttrImageResolveModeForcePull:
|
||||||
|
return llb.ResolveModeForcePull, nil
|
||||||
|
case pb.AttrImageResolveModePreferLocal:
|
||||||
|
return llb.ResolveModePreferLocal, nil
|
||||||
|
default:
|
||||||
|
return 0, errors.Errorf("invalid image-resolve-mode: %s", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExtraHosts(v string) ([]llb.HostIP, error) {
|
||||||
|
if v == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
out := make([]llb.HostIP, 0)
|
||||||
|
csvReader := csv.NewReader(strings.NewReader(v))
|
||||||
|
fields, err := csvReader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, field := range fields {
|
||||||
|
key, val, ok := strings.Cut(strings.ToLower(field), "=")
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("invalid key-value pair %s", field)
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(val)
|
||||||
|
if ip == nil {
|
||||||
|
return nil, errors.Errorf("failed to parse IP %s", val)
|
||||||
|
}
|
||||||
|
out = append(out, llb.HostIP{Host: key, IP: ip})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseShmSize(v string) (int64, error) {
|
||||||
|
if len(v) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
kb, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return kb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUlimits(v string) ([]pb.Ulimit, error) {
|
||||||
|
if v == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
out := make([]pb.Ulimit, 0)
|
||||||
|
csvReader := csv.NewReader(strings.NewReader(v))
|
||||||
|
fields, err := csvReader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, field := range fields {
|
||||||
|
ulimit, err := units.ParseUlimit(field)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, pb.Ulimit{
|
||||||
|
Name: ulimit.Name,
|
||||||
|
Soft: ulimit.Soft,
|
||||||
|
Hard: ulimit.Hard,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNetMode(v string) (pb.NetMode, error) {
|
||||||
|
if v == "" {
|
||||||
|
return llb.NetModeSandbox, nil
|
||||||
|
}
|
||||||
|
switch v {
|
||||||
|
case "none":
|
||||||
|
return llb.NetModeNone, nil
|
||||||
|
case "host":
|
||||||
|
return llb.NetModeHost, nil
|
||||||
|
case "sandbox":
|
||||||
|
return llb.NetModeSandbox, nil
|
||||||
|
default:
|
||||||
|
return 0, errors.Errorf("invalid netmode %s", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSourceDateEpoch(v string) (*time.Time, error) {
|
||||||
|
if v == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
sde, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "invalid SOURCE_DATE_EPOCH: %s", v)
|
||||||
|
}
|
||||||
|
tm := time.Unix(sde, 0).UTC()
|
||||||
|
return &tm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter(opt map[string]string, key string) map[string]string {
|
||||||
|
m := map[string]string{}
|
||||||
|
for k, v := range opt {
|
||||||
|
if strings.HasPrefix(k, key) {
|
||||||
|
m[strings.TrimPrefix(k, key)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
package dockerui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/platforms"
|
||||||
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||||
|
"github.com/moby/buildkit/exporter/containerimage/image"
|
||||||
|
"github.com/moby/buildkit/frontend/gateway/client"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuildFunc func(ctx context.Context, platform *ocispecs.Platform, idx int) (client.Reference, *image.Image, error)
|
||||||
|
|
||||||
|
func (bc *Client) Build(ctx context.Context, fn BuildFunc) (*ResultBuilder, error) {
|
||||||
|
res := client.NewResult()
|
||||||
|
|
||||||
|
targets := make([]*ocispecs.Platform, 0, len(bc.TargetPlatforms))
|
||||||
|
for _, p := range bc.TargetPlatforms {
|
||||||
|
p := p
|
||||||
|
targets = append(targets, &p)
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
targets = append(targets, nil)
|
||||||
|
}
|
||||||
|
expPlatforms := &exptypes.Platforms{
|
||||||
|
Platforms: make([]exptypes.Platform, len(targets)),
|
||||||
|
}
|
||||||
|
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
for i, tp := range targets {
|
||||||
|
i, tp := i, tp
|
||||||
|
eg.Go(func() error {
|
||||||
|
ref, img, err := fn(ctx, tp, i)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := json.Marshal(img)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to marshal image config")
|
||||||
|
}
|
||||||
|
|
||||||
|
p := platforms.DefaultSpec()
|
||||||
|
if tp != nil {
|
||||||
|
p = *tp
|
||||||
|
}
|
||||||
|
|
||||||
|
// in certain conditions we allow input platform to be extended from base image
|
||||||
|
if p.OS == "windows" && img.OS == p.OS {
|
||||||
|
if p.OSVersion == "" && img.OSVersion != "" {
|
||||||
|
p.OSVersion = img.OSVersion
|
||||||
|
}
|
||||||
|
if p.OSFeatures == nil && len(img.OSFeatures) > 0 {
|
||||||
|
p.OSFeatures = img.OSFeatures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p = platforms.Normalize(p)
|
||||||
|
k := platforms.Format(p)
|
||||||
|
|
||||||
|
if bc.MultiPlatformRequested {
|
||||||
|
res.AddRef(k, ref)
|
||||||
|
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, k), config)
|
||||||
|
} else {
|
||||||
|
res.SetRef(ref)
|
||||||
|
res.AddMeta(exptypes.ExporterImageConfigKey, config)
|
||||||
|
}
|
||||||
|
expPlatforms.Platforms[i] = exptypes.Platform{
|
||||||
|
ID: k,
|
||||||
|
Platform: p,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ResultBuilder{
|
||||||
|
Result: res,
|
||||||
|
expPlatforms: expPlatforms,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResultBuilder struct {
|
||||||
|
*client.Result
|
||||||
|
expPlatforms *exptypes.Platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *ResultBuilder) Finalize() (*client.Result, error) {
|
||||||
|
dt, err := json.Marshal(rb.expPlatforms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rb.AddMeta(exptypes.ExporterPlatformsKey, dt)
|
||||||
|
|
||||||
|
return rb.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *ResultBuilder) EachPlatform(ctx context.Context, fn func(ctx context.Context, id string, p ocispecs.Platform) error) error {
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
for _, p := range rb.expPlatforms.Platforms {
|
||||||
|
p := p
|
||||||
|
eg.Go(func() error {
|
||||||
|
return fn(ctx, p.ID, p.Platform)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
@ -0,0 +1,497 @@
|
|||||||
|
package dockerui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/platforms"
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
|
controlapi "github.com/moby/buildkit/api/services/control"
|
||||||
|
"github.com/moby/buildkit/client/llb"
|
||||||
|
"github.com/moby/buildkit/exporter/containerimage/image"
|
||||||
|
"github.com/moby/buildkit/frontend/attestations"
|
||||||
|
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
|
||||||
|
"github.com/moby/buildkit/frontend/gateway/client"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
"github.com/moby/buildkit/util/flightcontrol"
|
||||||
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
buildArgPrefix = "build-arg:"
|
||||||
|
labelPrefix = "label:"
|
||||||
|
|
||||||
|
keyTarget = "target"
|
||||||
|
keyCgroupParent = "cgroup-parent"
|
||||||
|
keyForceNetwork = "force-network-mode"
|
||||||
|
keyGlobalAddHosts = "add-hosts"
|
||||||
|
keyHostname = "hostname"
|
||||||
|
keyImageResolveMode = "image-resolve-mode"
|
||||||
|
keyMultiPlatform = "multi-platform"
|
||||||
|
keyNoCache = "no-cache"
|
||||||
|
keyShmSize = "shm-size"
|
||||||
|
keyTargetPlatform = "platform"
|
||||||
|
keyUlimit = "ulimit"
|
||||||
|
keyCacheFrom = "cache-from" // for registry only. deprecated in favor of keyCacheImports
|
||||||
|
keyCacheImports = "cache-imports" // JSON representation of []CacheOptionsEntry
|
||||||
|
|
||||||
|
// Don't forget to update frontend documentation if you add
|
||||||
|
// a new build-arg: frontend/dockerfile/docs/reference.md
|
||||||
|
keyCacheNSArg = "build-arg:BUILDKIT_CACHE_MOUNT_NS"
|
||||||
|
keyMultiPlatformArg = "build-arg:BUILDKIT_MULTI_PLATFORM"
|
||||||
|
keyHostnameArg = "build-arg:BUILDKIT_SANDBOX_HOSTNAME"
|
||||||
|
keyContextKeepGitDirArg = "build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR"
|
||||||
|
keySourceDateEpoch = "build-arg:SOURCE_DATE_EPOCH"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
BuildArgs map[string]string
|
||||||
|
CacheIDNamespace string
|
||||||
|
CgroupParent string
|
||||||
|
Epoch *time.Time
|
||||||
|
ExtraHosts []llb.HostIP
|
||||||
|
Hostname string
|
||||||
|
ImageResolveMode llb.ResolveMode
|
||||||
|
Labels map[string]string
|
||||||
|
NetworkMode pb.NetMode
|
||||||
|
ShmSize int64
|
||||||
|
Target string
|
||||||
|
Ulimits []pb.Ulimit
|
||||||
|
|
||||||
|
CacheImports []client.CacheOptionsEntry
|
||||||
|
TargetPlatforms []ocispecs.Platform // nil means default
|
||||||
|
BuildPlatforms []ocispecs.Platform
|
||||||
|
MultiPlatformRequested bool
|
||||||
|
SBOM *SBOM
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Config
|
||||||
|
client client.Client
|
||||||
|
ignoreCache []string
|
||||||
|
bctx *buildContext
|
||||||
|
g flightcontrol.Group
|
||||||
|
bopts client.BuildOpts
|
||||||
|
|
||||||
|
dockerignore []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type SBOM struct {
|
||||||
|
Generator string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Source struct {
|
||||||
|
*llb.SourceMap
|
||||||
|
Warn func(context.Context, string, client.WarnOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextOpt struct {
|
||||||
|
NoDockerignore bool
|
||||||
|
LocalOpts []llb.LocalOption
|
||||||
|
Platform *ocispecs.Platform
|
||||||
|
ResolveMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateMinCaps(c client.Client) error {
|
||||||
|
opts := c.BuildOpts().Opts
|
||||||
|
caps := c.BuildOpts().LLBCaps
|
||||||
|
|
||||||
|
if err := caps.Supports(pb.CapFileBase); err != nil {
|
||||||
|
return errors.Wrap(err, "needs BuildKit 0.5 or later")
|
||||||
|
}
|
||||||
|
if opts["override-copy-image"] != "" {
|
||||||
|
return errors.New("support for \"override-copy-image\" was removed in BuildKit 0.11")
|
||||||
|
}
|
||||||
|
if v, ok := opts["build-arg:BUILDKIT_DISABLE_FILEOP"]; ok {
|
||||||
|
if b, err := strconv.ParseBool(v); err == nil && b {
|
||||||
|
return errors.New("support for \"BUILDKIT_DISABLE_FILEOP\" build-arg was removed in BuildKit 0.11")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(c client.Client) (*Client, error) {
|
||||||
|
if err := validateMinCaps(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bc := &Client{
|
||||||
|
client: c,
|
||||||
|
bopts: c.BuildOpts(), // avoid grpc on every call
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bc.init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) BuildOpts() client.BuildOpts {
|
||||||
|
return bc.bopts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) init() error {
|
||||||
|
opts := bc.bopts.Opts
|
||||||
|
|
||||||
|
defaultBuildPlatform := platforms.Normalize(platforms.DefaultSpec())
|
||||||
|
if workers := bc.bopts.Workers; len(workers) > 0 && len(workers[0].Platforms) > 0 {
|
||||||
|
defaultBuildPlatform = workers[0].Platforms[0]
|
||||||
|
}
|
||||||
|
buildPlatforms := []ocispecs.Platform{defaultBuildPlatform}
|
||||||
|
targetPlatforms := []ocispecs.Platform{}
|
||||||
|
if v := opts[keyTargetPlatform]; v != "" {
|
||||||
|
var err error
|
||||||
|
targetPlatforms, err = parsePlatforms(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bc.BuildPlatforms = buildPlatforms
|
||||||
|
bc.TargetPlatforms = targetPlatforms
|
||||||
|
|
||||||
|
resolveMode, err := parseResolveMode(opts[keyImageResolveMode])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bc.ImageResolveMode = resolveMode
|
||||||
|
|
||||||
|
extraHosts, err := parseExtraHosts(opts[keyGlobalAddHosts])
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to parse additional hosts")
|
||||||
|
}
|
||||||
|
bc.ExtraHosts = extraHosts
|
||||||
|
|
||||||
|
shmSize, err := parseShmSize(opts[keyShmSize])
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to parse shm size")
|
||||||
|
}
|
||||||
|
bc.ShmSize = shmSize
|
||||||
|
|
||||||
|
ulimits, err := parseUlimits(opts[keyUlimit])
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to parse ulimit")
|
||||||
|
}
|
||||||
|
bc.Ulimits = ulimits
|
||||||
|
|
||||||
|
defaultNetMode, err := parseNetMode(opts[keyForceNetwork])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bc.NetworkMode = defaultNetMode
|
||||||
|
|
||||||
|
var ignoreCache []string
|
||||||
|
if v, ok := opts[keyNoCache]; ok {
|
||||||
|
if v == "" {
|
||||||
|
ignoreCache = []string{} // means all stages
|
||||||
|
} else {
|
||||||
|
ignoreCache = strings.Split(v, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bc.ignoreCache = ignoreCache
|
||||||
|
|
||||||
|
multiPlatform := len(targetPlatforms) > 1
|
||||||
|
if v := opts[keyMultiPlatformArg]; v != "" {
|
||||||
|
opts[keyMultiPlatform] = v
|
||||||
|
}
|
||||||
|
if v := opts[keyMultiPlatform]; v != "" {
|
||||||
|
b, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("invalid boolean value for multi-platform: %s", v)
|
||||||
|
}
|
||||||
|
if !b && multiPlatform {
|
||||||
|
return errors.Errorf("conflicting config: returning multiple target platforms is not allowed")
|
||||||
|
}
|
||||||
|
multiPlatform = b
|
||||||
|
}
|
||||||
|
bc.MultiPlatformRequested = multiPlatform
|
||||||
|
|
||||||
|
var cacheImports []client.CacheOptionsEntry
|
||||||
|
// new API
|
||||||
|
if cacheImportsStr := opts[keyCacheImports]; cacheImportsStr != "" {
|
||||||
|
var cacheImportsUM []controlapi.CacheOptionsEntry
|
||||||
|
if err := json.Unmarshal([]byte(cacheImportsStr), &cacheImportsUM); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to unmarshal %s (%q)", keyCacheImports, cacheImportsStr)
|
||||||
|
}
|
||||||
|
for _, um := range cacheImportsUM {
|
||||||
|
cacheImports = append(cacheImports, client.CacheOptionsEntry{Type: um.Type, Attrs: um.Attrs})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// old API
|
||||||
|
if cacheFromStr := opts[keyCacheFrom]; cacheFromStr != "" {
|
||||||
|
cacheFrom := strings.Split(cacheFromStr, ",")
|
||||||
|
for _, s := range cacheFrom {
|
||||||
|
im := client.CacheOptionsEntry{
|
||||||
|
Type: "registry",
|
||||||
|
Attrs: map[string]string{
|
||||||
|
"ref": s,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// FIXME(AkihiroSuda): skip append if already exists
|
||||||
|
cacheImports = append(cacheImports, im)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bc.CacheImports = cacheImports
|
||||||
|
|
||||||
|
epoch, err := parseSourceDateEpoch(opts[keySourceDateEpoch])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bc.Epoch = epoch
|
||||||
|
|
||||||
|
attests, err := attestations.Parse(opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if attrs, ok := attests[attestations.KeyTypeSbom]; ok {
|
||||||
|
src, ok := attrs["generator"]
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("sbom scanner cannot be empty")
|
||||||
|
}
|
||||||
|
ref, err := reference.ParseNormalizedNamed(src)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to parse sbom scanner %s", src)
|
||||||
|
}
|
||||||
|
ref = reference.TagNameOnly(ref)
|
||||||
|
bc.SBOM = &SBOM{
|
||||||
|
Generator: ref.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bc.BuildArgs = filter(opts, buildArgPrefix)
|
||||||
|
bc.Labels = filter(opts, labelPrefix)
|
||||||
|
bc.CacheIDNamespace = opts[keyCacheNSArg]
|
||||||
|
bc.CgroupParent = opts[keyCgroupParent]
|
||||||
|
bc.Target = opts[keyTarget]
|
||||||
|
|
||||||
|
if v, ok := opts[keyHostnameArg]; ok && len(v) > 0 {
|
||||||
|
opts[keyHostname] = v
|
||||||
|
}
|
||||||
|
bc.Hostname = opts[keyHostname]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) buildContext(ctx context.Context) (*buildContext, error) {
|
||||||
|
bctx, err := bc.g.Do(ctx, "initcontext", func(ctx context.Context) (interface{}, error) {
|
||||||
|
if bc.bctx != nil {
|
||||||
|
return bc.bctx, nil
|
||||||
|
}
|
||||||
|
bctx, err := bc.initContext(ctx)
|
||||||
|
if err == nil {
|
||||||
|
bc.bctx = bctx
|
||||||
|
}
|
||||||
|
return bctx, err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bctx.(*buildContext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) ReadEntrypoint(ctx context.Context, opts ...llb.LocalOption) (*Source, error) {
|
||||||
|
bctx, err := bc.buildContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var src *llb.State
|
||||||
|
|
||||||
|
if !bctx.forceLocalDockerfile {
|
||||||
|
if bctx.dockerfile != nil {
|
||||||
|
src = bctx.dockerfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if src == nil {
|
||||||
|
name := "load build definition from " + bctx.filename
|
||||||
|
|
||||||
|
filenames := []string{bctx.filename, bctx.filename + ".dockerignore"}
|
||||||
|
|
||||||
|
// dockerfile is also supported casing moby/moby#10858
|
||||||
|
if path.Base(bctx.filename) == DefaultDockerfileName {
|
||||||
|
filenames = append(filenames, path.Join(path.Dir(bctx.filename), strings.ToLower(DefaultDockerfileName)))
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append([]llb.LocalOption{
|
||||||
|
llb.FollowPaths(filenames),
|
||||||
|
llb.SessionID(bc.bopts.SessionID),
|
||||||
|
llb.SharedKeyHint(bctx.dockerfileLocalName),
|
||||||
|
WithInternalName(name),
|
||||||
|
llb.Differ(llb.DiffNone, false),
|
||||||
|
}, opts...)
|
||||||
|
|
||||||
|
lsrc := llb.Local(bctx.dockerfileLocalName, opts...)
|
||||||
|
src = &lsrc
|
||||||
|
}
|
||||||
|
|
||||||
|
def, err := src.Marshal(ctx, bc.marshalOpts()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to marshal local source")
|
||||||
|
}
|
||||||
|
|
||||||
|
defVtx, err := def.Head()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := bc.client.Solve(ctx, client.SolveRequest{
|
||||||
|
Definition: def.ToPB(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to resolve dockerfile")
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := res.SingleRef()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, err := ref.ReadFile(ctx, client.ReadRequest{
|
||||||
|
Filename: bctx.filename,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if path.Base(bctx.filename) == DefaultDockerfileName {
|
||||||
|
var err1 error
|
||||||
|
dt, err1 = ref.ReadFile(ctx, client.ReadRequest{
|
||||||
|
Filename: path.Join(path.Dir(bctx.filename), strings.ToLower(DefaultDockerfileName)),
|
||||||
|
})
|
||||||
|
if err1 == nil {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to read dockerfile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
smap := llb.NewSourceMap(src, bctx.filename, dt)
|
||||||
|
smap.Definition = def
|
||||||
|
|
||||||
|
dt, err = ref.ReadFile(ctx, client.ReadRequest{
|
||||||
|
Filename: bctx.filename + ".dockerignore",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
bc.dockerignore = dt
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Source{
|
||||||
|
SourceMap: smap,
|
||||||
|
Warn: func(ctx context.Context, msg string, opts client.WarnOpts) {
|
||||||
|
if opts.Level == 0 {
|
||||||
|
opts.Level = 1
|
||||||
|
}
|
||||||
|
if opts.SourceInfo == nil {
|
||||||
|
opts.SourceInfo = &pb.SourceInfo{
|
||||||
|
Data: smap.Data,
|
||||||
|
Filename: smap.Filename,
|
||||||
|
Definition: smap.Definition.ToPB(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bc.client.Warn(ctx, defVtx, msg, opts)
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) MainContext(ctx context.Context, opts ...llb.LocalOption) (*llb.State, error) {
|
||||||
|
bctx, err := bc.buildContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bctx.context != nil {
|
||||||
|
return bctx.context, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.dockerignore == nil {
|
||||||
|
st := llb.Local(bctx.contextLocalName,
|
||||||
|
llb.SessionID(bc.bopts.SessionID),
|
||||||
|
llb.FollowPaths([]string{DefaultDockerignoreName}),
|
||||||
|
llb.SharedKeyHint(bctx.contextLocalName+"-"+DefaultDockerignoreName),
|
||||||
|
WithInternalName("load "+DefaultDockerignoreName),
|
||||||
|
llb.Differ(llb.DiffNone, false),
|
||||||
|
)
|
||||||
|
def, err := st.Marshal(ctx, bc.marshalOpts()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := bc.client.Solve(ctx, client.SolveRequest{
|
||||||
|
Definition: def.ToPB(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ref, err := res.SingleRef()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dt, _ := ref.ReadFile(ctx, client.ReadRequest{ // ignore error
|
||||||
|
Filename: DefaultDockerignoreName,
|
||||||
|
})
|
||||||
|
if dt == nil {
|
||||||
|
dt = []byte{}
|
||||||
|
}
|
||||||
|
bc.dockerignore = dt
|
||||||
|
}
|
||||||
|
|
||||||
|
var excludes []string
|
||||||
|
if len(bc.dockerignore) != 0 {
|
||||||
|
excludes, err = dockerignore.ReadAll(bytes.NewBuffer(bc.dockerignore))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to parse dockerignore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append([]llb.LocalOption{
|
||||||
|
llb.SessionID(bc.bopts.SessionID),
|
||||||
|
llb.ExcludePatterns(excludes),
|
||||||
|
llb.SharedKeyHint(bctx.contextLocalName),
|
||||||
|
WithInternalName("load build context"),
|
||||||
|
}, opts...)
|
||||||
|
|
||||||
|
st := llb.Local(bctx.contextLocalName, opts...)
|
||||||
|
|
||||||
|
return &st, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) NamedContext(ctx context.Context, name string, opt ContextOpt) (*llb.State, *image.Image, error) {
|
||||||
|
named, err := reference.ParseNormalizedNamed(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "invalid context name %s", name)
|
||||||
|
}
|
||||||
|
name = strings.TrimSuffix(reference.FamiliarString(named), ":latest")
|
||||||
|
|
||||||
|
pp := platforms.DefaultSpec()
|
||||||
|
if opt.Platform != nil {
|
||||||
|
pp = *opt.Platform
|
||||||
|
}
|
||||||
|
pname := name + "::" + platforms.Format(platforms.Normalize(pp))
|
||||||
|
st, img, err := bc.namedContext(ctx, name, pname, opt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if st != nil {
|
||||||
|
return st, img, nil
|
||||||
|
}
|
||||||
|
return bc.namedContext(ctx, name, name, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) IsNoCache(name string) bool {
|
||||||
|
if len(bc.ignoreCache) == 0 {
|
||||||
|
return bc.ignoreCache != nil
|
||||||
|
}
|
||||||
|
for _, n := range bc.ignoreCache {
|
||||||
|
if strings.EqualFold(n, name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithInternalName(name string) llb.ConstraintsOpt {
|
||||||
|
return llb.WithCustomName("[internal] " + name)
|
||||||
|
}
|
@ -0,0 +1,194 @@
|
|||||||
|
package dockerui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/moby/buildkit/client/llb"
|
||||||
|
"github.com/moby/buildkit/frontend/gateway/client"
|
||||||
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
|
"github.com/moby/buildkit/util/gitutil"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultLocalNameContext = "context"
|
||||||
|
DefaultLocalNameDockerfile = "dockerfile"
|
||||||
|
DefaultDockerfileName = "Dockerfile"
|
||||||
|
DefaultDockerignoreName = ".dockerignore"
|
||||||
|
EmptyImageName = "scratch"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyFilename = "filename"
|
||||||
|
keyContextSubDir = "contextsubdir"
|
||||||
|
keyNameContext = "contextkey"
|
||||||
|
keyNameDockerfile = "dockerfilekey"
|
||||||
|
)
|
||||||
|
|
||||||
|
var httpPrefix = regexp.MustCompile(`^https?://`)
|
||||||
|
|
||||||
|
type buildContext struct {
|
||||||
|
context *llb.State // set if not local
|
||||||
|
dockerfile *llb.State // override remoteContext if set
|
||||||
|
contextLocalName string
|
||||||
|
dockerfileLocalName string
|
||||||
|
filename string
|
||||||
|
forceLocalDockerfile bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) marshalOpts() []llb.ConstraintsOpt {
|
||||||
|
return []llb.ConstraintsOpt{llb.WithCaps(bc.bopts.Caps)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) initContext(ctx context.Context) (*buildContext, error) {
|
||||||
|
opts := bc.bopts.Opts
|
||||||
|
gwcaps := bc.bopts.Caps
|
||||||
|
|
||||||
|
localNameContext := DefaultLocalNameContext
|
||||||
|
if v, ok := opts[keyNameContext]; ok {
|
||||||
|
localNameContext = v
|
||||||
|
}
|
||||||
|
|
||||||
|
bctx := &buildContext{
|
||||||
|
contextLocalName: DefaultLocalNameContext,
|
||||||
|
dockerfileLocalName: DefaultLocalNameDockerfile,
|
||||||
|
filename: DefaultDockerfileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := opts[keyFilename]; ok {
|
||||||
|
bctx.filename = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := opts[keyNameDockerfile]; ok {
|
||||||
|
bctx.forceLocalDockerfile = true
|
||||||
|
bctx.dockerfileLocalName = v
|
||||||
|
}
|
||||||
|
|
||||||
|
keepGit := false
|
||||||
|
if v, err := strconv.ParseBool(opts[keyContextKeepGitDirArg]); err == nil {
|
||||||
|
keepGit = v
|
||||||
|
}
|
||||||
|
if st, ok := DetectGitContext(opts[localNameContext], keepGit); ok {
|
||||||
|
bctx.context = st
|
||||||
|
bctx.dockerfile = st
|
||||||
|
} else if st, filename, ok := DetectHTTPContext(opts[localNameContext]); ok {
|
||||||
|
def, err := st.Marshal(ctx, bc.marshalOpts()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to marshal httpcontext")
|
||||||
|
}
|
||||||
|
res, err := bc.client.Solve(ctx, client.SolveRequest{
|
||||||
|
Definition: def.ToPB(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to resolve httpcontext")
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := res.SingleRef()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, err := ref.ReadFile(ctx, client.ReadRequest{
|
||||||
|
Filename: filename,
|
||||||
|
Range: &client.FileRange{
|
||||||
|
Length: 1024,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to read downloaded context")
|
||||||
|
}
|
||||||
|
if isArchive(dt) {
|
||||||
|
bc := llb.Scratch().File(llb.Copy(*st, filepath.Join("/", filename), "/", &llb.CopyInfo{
|
||||||
|
AttemptUnpack: true,
|
||||||
|
}))
|
||||||
|
bctx.context = &bc
|
||||||
|
} else {
|
||||||
|
bctx.filename = filename
|
||||||
|
bctx.context = st
|
||||||
|
}
|
||||||
|
bctx.dockerfile = bctx.context
|
||||||
|
} else if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil {
|
||||||
|
inputs, err := bc.client.Inputs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to get frontend inputs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bctx.forceLocalDockerfile {
|
||||||
|
inputDockerfile, ok := inputs[bctx.dockerfileLocalName]
|
||||||
|
if ok {
|
||||||
|
bctx.dockerfile = &inputDockerfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputCtx, ok := inputs[DefaultLocalNameContext]
|
||||||
|
if ok {
|
||||||
|
bctx.context = &inputCtx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bctx.context != nil {
|
||||||
|
if sub, ok := opts[keyContextSubDir]; ok {
|
||||||
|
bctx.context = scopeToSubDir(bctx.context, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetectGitContext(ref string, keepGit bool) (*llb.State, bool) {
|
||||||
|
g, err := gitutil.ParseGitRef(ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
commit := g.Commit
|
||||||
|
if g.SubDir != "" {
|
||||||
|
commit += ":" + g.SubDir
|
||||||
|
}
|
||||||
|
gitOpts := []llb.GitOption{WithInternalName("load git source " + ref)}
|
||||||
|
if keepGit {
|
||||||
|
gitOpts = append(gitOpts, llb.KeepGitDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
st := llb.Git(g.Remote, commit, gitOpts...)
|
||||||
|
return &st, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetectHTTPContext(ref string) (*llb.State, string, bool) {
|
||||||
|
filename := "context"
|
||||||
|
if httpPrefix.MatchString(ref) {
|
||||||
|
st := llb.HTTP(ref, llb.Filename(filename), WithInternalName("load remote build context"))
|
||||||
|
return &st, filename, true
|
||||||
|
}
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isArchive(header []byte) bool {
|
||||||
|
for _, m := range [][]byte{
|
||||||
|
{0x42, 0x5A, 0x68}, // bzip2
|
||||||
|
{0x1F, 0x8B, 0x08}, // gzip
|
||||||
|
{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, // xz
|
||||||
|
} {
|
||||||
|
if len(header) < len(m) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if bytes.Equal(m, header[:len(m)]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r := tar.NewReader(bytes.NewBuffer(header))
|
||||||
|
_, err := r.Next()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scopeToSubDir(c *llb.State, dir string) *llb.State {
|
||||||
|
bc := llb.Scratch().File(llb.Copy(*c, dir, "/", &llb.CopyInfo{
|
||||||
|
CopyDirContentsOnly: true,
|
||||||
|
}))
|
||||||
|
return &bc
|
||||||
|
}
|
@ -0,0 +1,231 @@
|
|||||||
|
package dockerui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
|
"github.com/moby/buildkit/client/llb"
|
||||||
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||||
|
"github.com/moby/buildkit/exporter/containerimage/image"
|
||||||
|
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
|
||||||
|
"github.com/moby/buildkit/frontend/gateway/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextPrefix = "context:"
|
||||||
|
inputMetadataPrefix = "input-metadata:"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (bc *Client) namedContext(ctx context.Context, name string, nameWithPlatform string, opt ContextOpt) (*llb.State, *image.Image, error) {
|
||||||
|
opts := bc.bopts.Opts
|
||||||
|
v, ok := opts[contextPrefix+nameWithPlatform]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vv := strings.SplitN(v, ":", 2)
|
||||||
|
if len(vv) != 2 {
|
||||||
|
return nil, nil, errors.Errorf("invalid context specifier %s for %s", v, nameWithPlatform)
|
||||||
|
}
|
||||||
|
// allow git@ without protocol for SSH URLs for backwards compatibility
|
||||||
|
if strings.HasPrefix(vv[0], "git@") {
|
||||||
|
vv[0] = "git"
|
||||||
|
}
|
||||||
|
switch vv[0] {
|
||||||
|
case "docker-image":
|
||||||
|
ref := strings.TrimPrefix(vv[1], "//")
|
||||||
|
if ref == EmptyImageName {
|
||||||
|
st := llb.Scratch()
|
||||||
|
return &st, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
imgOpt := []llb.ImageOption{
|
||||||
|
llb.WithCustomName("[context " + nameWithPlatform + "] " + ref),
|
||||||
|
}
|
||||||
|
if opt.Platform != nil {
|
||||||
|
imgOpt = append(imgOpt, llb.Platform(*opt.Platform))
|
||||||
|
}
|
||||||
|
|
||||||
|
named, err := reference.ParseNormalizedNamed(ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
named = reference.TagNameOnly(named)
|
||||||
|
|
||||||
|
_, data, err := bc.client.ResolveImageConfig(ctx, named.String(), llb.ResolveImageConfigOpt{
|
||||||
|
Platform: opt.Platform,
|
||||||
|
ResolveMode: opt.ResolveMode,
|
||||||
|
LogName: fmt.Sprintf("[context %s] load metadata for %s", nameWithPlatform, ref),
|
||||||
|
ResolverType: llb.ResolverTypeRegistry,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var img image.Image
|
||||||
|
if err := json.Unmarshal(data, &img); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
img.Created = nil
|
||||||
|
|
||||||
|
st := llb.Image(ref, imgOpt...)
|
||||||
|
st, err = st.WithImageConfig(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return &st, &img, nil
|
||||||
|
case "git":
|
||||||
|
st, ok := DetectGitContext(v, true)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.Errorf("invalid git context %s", v)
|
||||||
|
}
|
||||||
|
return st, nil, nil
|
||||||
|
case "http", "https":
|
||||||
|
st, ok := DetectGitContext(v, true)
|
||||||
|
if !ok {
|
||||||
|
httpst := llb.HTTP(v, llb.WithCustomName("[context "+nameWithPlatform+"] "+v))
|
||||||
|
st = &httpst
|
||||||
|
}
|
||||||
|
return st, nil, nil
|
||||||
|
case "oci-layout":
|
||||||
|
refSpec := strings.TrimPrefix(vv[1], "//")
|
||||||
|
ref, err := reference.Parse(refSpec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "could not parse oci-layout reference %q", refSpec)
|
||||||
|
}
|
||||||
|
named, ok := ref.(reference.Named)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.Errorf("oci-layout reference %q has no name", ref.String())
|
||||||
|
}
|
||||||
|
dgstd, ok := named.(reference.Digested)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.Errorf("oci-layout reference %q has no digest", named.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// for the dummy ref primarily used in log messages, we can use the
|
||||||
|
// original name, since the store key may not be significant
|
||||||
|
dummyRef, err := reference.ParseNormalizedNamed(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "could not parse oci-layout reference %q", name)
|
||||||
|
}
|
||||||
|
dummyRef, err = reference.WithDigest(dummyRef, dgstd.Digest())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "could not wrap %q with digest", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, data, err := bc.client.ResolveImageConfig(ctx, dummyRef.String(), llb.ResolveImageConfigOpt{
|
||||||
|
Platform: opt.Platform,
|
||||||
|
ResolveMode: opt.ResolveMode,
|
||||||
|
LogName: fmt.Sprintf("[context %s] load metadata for %s", nameWithPlatform, dummyRef.String()),
|
||||||
|
ResolverType: llb.ResolverTypeOCILayout,
|
||||||
|
Store: llb.ResolveImageConfigOptStore{
|
||||||
|
SessionID: bc.bopts.SessionID,
|
||||||
|
StoreID: named.Name(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var img image.Image
|
||||||
|
if err := json.Unmarshal(data, &img); err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "could not parse oci-layout image config")
|
||||||
|
}
|
||||||
|
|
||||||
|
ociOpt := []llb.OCILayoutOption{
|
||||||
|
llb.WithCustomName("[context " + nameWithPlatform + "] OCI load from client"),
|
||||||
|
llb.OCIStore(bc.bopts.SessionID, named.Name()),
|
||||||
|
}
|
||||||
|
if opt.Platform != nil {
|
||||||
|
ociOpt = append(ociOpt, llb.Platform(*opt.Platform))
|
||||||
|
}
|
||||||
|
st := llb.OCILayout(
|
||||||
|
dummyRef.String(),
|
||||||
|
ociOpt...,
|
||||||
|
)
|
||||||
|
st, err = st.WithImageConfig(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return &st, &img, nil
|
||||||
|
case "local":
|
||||||
|
st := llb.Local(vv[1],
|
||||||
|
llb.SessionID(bc.bopts.SessionID),
|
||||||
|
llb.FollowPaths([]string{DefaultDockerignoreName}),
|
||||||
|
llb.SharedKeyHint("context:"+nameWithPlatform+"-"+DefaultDockerignoreName),
|
||||||
|
llb.WithCustomName("[context "+nameWithPlatform+"] load "+DefaultDockerignoreName),
|
||||||
|
llb.Differ(llb.DiffNone, false),
|
||||||
|
)
|
||||||
|
def, err := st.Marshal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
res, err := bc.client.Solve(ctx, client.SolveRequest{
|
||||||
|
Evaluate: true,
|
||||||
|
Definition: def.ToPB(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ref, err := res.SingleRef()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var excludes []string
|
||||||
|
if !opt.NoDockerignore {
|
||||||
|
dt, _ := ref.ReadFile(ctx, client.ReadRequest{
|
||||||
|
Filename: DefaultDockerignoreName,
|
||||||
|
}) // error ignored
|
||||||
|
|
||||||
|
if len(dt) != 0 {
|
||||||
|
excludes, err = dockerignore.ReadAll(bytes.NewBuffer(dt))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st = llb.Local(vv[1],
|
||||||
|
llb.WithCustomName("[context "+nameWithPlatform+"] load from client"),
|
||||||
|
llb.SessionID(bc.bopts.SessionID),
|
||||||
|
llb.SharedKeyHint("context:"+nameWithPlatform),
|
||||||
|
llb.ExcludePatterns(excludes),
|
||||||
|
)
|
||||||
|
return &st, nil, nil
|
||||||
|
case "input":
|
||||||
|
inputs, err := bc.client.Inputs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
st, ok := inputs[vv[1]]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.Errorf("invalid input %s for %s", vv[1], nameWithPlatform)
|
||||||
|
}
|
||||||
|
md, ok := opts[inputMetadataPrefix+vv[1]]
|
||||||
|
if ok {
|
||||||
|
m := make(map[string][]byte)
|
||||||
|
if err := json.Unmarshal([]byte(md), &m); err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "failed to parse input metadata %s", md)
|
||||||
|
}
|
||||||
|
var img *image.Image
|
||||||
|
if dtic, ok := m[exptypes.ExporterImageConfigKey]; ok {
|
||||||
|
st, err = st.WithImageConfig(dtic)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(dtic, &img); err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "failed to parse image config for %s", nameWithPlatform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &st, img, nil
|
||||||
|
}
|
||||||
|
return &st, nil, nil
|
||||||
|
default:
|
||||||
|
return nil, nil, errors.Errorf("unsupported context source %s for %s", vv[0], nameWithPlatform)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package dockerui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/moby/buildkit/frontend/gateway/client"
|
||||||
|
"github.com/moby/buildkit/frontend/subrequests"
|
||||||
|
"github.com/moby/buildkit/frontend/subrequests/outline"
|
||||||
|
"github.com/moby/buildkit/frontend/subrequests/targets"
|
||||||
|
"github.com/moby/buildkit/solver/errdefs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyRequestID = "requestid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestHandler struct {
|
||||||
|
Outline func(context.Context) (*outline.Outline, error)
|
||||||
|
ListTargets func(context.Context) (*targets.List, error)
|
||||||
|
AllowOther bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *Client) HandleSubrequest(ctx context.Context, h RequestHandler) (*client.Result, bool, error) {
|
||||||
|
req, ok := bc.bopts.Opts[keyRequestID]
|
||||||
|
if !ok {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
switch req {
|
||||||
|
case subrequests.RequestSubrequestsDescribe:
|
||||||
|
res, err := describe(h)
|
||||||
|
return res, true, err
|
||||||
|
case outline.SubrequestsOutlineDefinition.Name:
|
||||||
|
if f := h.Outline; f != nil {
|
||||||
|
o, err := f(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if o == nil {
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
res, err := o.ToResult()
|
||||||
|
return res, true, err
|
||||||
|
}
|
||||||
|
case targets.SubrequestsTargetsDefinition.Name:
|
||||||
|
if f := h.ListTargets; f != nil {
|
||||||
|
targets, err := f(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if targets == nil {
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
res, err := targets.ToResult()
|
||||||
|
return res, true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if h.AllowOther {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return nil, false, errdefs.NewUnsupportedSubrequestError(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func describe(h RequestHandler) (*client.Result, error) {
|
||||||
|
all := []subrequests.Request{}
|
||||||
|
if h.Outline != nil {
|
||||||
|
all = append(all, outline.SubrequestsOutlineDefinition)
|
||||||
|
}
|
||||||
|
if h.ListTargets != nil {
|
||||||
|
all = append(all, targets.SubrequestsTargetsDefinition)
|
||||||
|
}
|
||||||
|
all = append(all, subrequests.SubrequestsDescribeDefinition)
|
||||||
|
dt, err := json.MarshalIndent(all, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := bytes.NewBuffer(nil)
|
||||||
|
if err := subrequests.PrintDescribe(dt, b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := client.NewResult()
|
||||||
|
res.Metadata = map[string][]byte{
|
||||||
|
"result.json": dt,
|
||||||
|
"result.txt": b.Bytes(),
|
||||||
|
"version": []byte(subrequests.SubrequestsDescribeDefinition.Version),
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
Loading…
Reference in New Issue