mirror of
https://github.com/moby/buildkit.git
synced 2026-08-05 15:20:22 +00:00
Support importing git sources from OCI or registry-backed bundle blobs and exporting resolved checkouts as single-file git bundles. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
1117 lines
28 KiB
Go
1117 lines
28 KiB
Go
package llb
|
|
|
|
import (
|
|
"context"
|
|
_ "crypto/sha256" // for opencontainers/go-digest
|
|
"encoding/json"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/distribution/reference"
|
|
"github.com/moby/buildkit/client/llb/sourceresolver"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/moby/buildkit/util/apicaps"
|
|
"github.com/moby/buildkit/util/gitutil"
|
|
"github.com/moby/buildkit/util/sshutil"
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type SourceOp struct {
|
|
cache MarshalCache
|
|
id string
|
|
attrs map[string]string
|
|
output Output
|
|
constraints Constraints
|
|
err error
|
|
}
|
|
|
|
func NewSource(id string, attrs map[string]string, c Constraints) *SourceOp {
|
|
s := &SourceOp{
|
|
id: id,
|
|
attrs: attrs,
|
|
constraints: c,
|
|
}
|
|
s.output = &output{vertex: s, platform: c.Platform}
|
|
return s
|
|
}
|
|
|
|
func (s *SourceOp) Validate(ctx context.Context, c *Constraints) error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
if s.id == "" {
|
|
return errors.Errorf("source identifier can't be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SourceOp) Marshal(ctx context.Context, constraints *Constraints) (digest.Digest, []byte, *pb.OpMetadata, []*SourceLocation, error) {
|
|
cache := s.cache.Acquire()
|
|
defer cache.Release()
|
|
|
|
if dgst, dt, md, srcs, err := cache.Load(constraints); err == nil {
|
|
return dgst, dt, md, srcs, nil
|
|
}
|
|
|
|
if err := s.Validate(ctx, constraints); err != nil {
|
|
return "", nil, nil, nil, err
|
|
}
|
|
|
|
if strings.HasPrefix(s.id, "local://") {
|
|
if _, hasSession := s.attrs[pb.AttrLocalSessionID]; !hasSession {
|
|
uid := s.constraints.LocalUniqueID
|
|
if uid == "" {
|
|
uid = constraints.LocalUniqueID
|
|
}
|
|
s.attrs[pb.AttrLocalUniqueID] = uid
|
|
addCap(&s.constraints, pb.CapSourceLocalUnique)
|
|
}
|
|
}
|
|
proto, md := MarshalConstraints(constraints, &s.constraints)
|
|
|
|
proto.Op = &pb.Op_Source{
|
|
Source: &pb.SourceOp{Identifier: s.id, Attrs: s.attrs},
|
|
}
|
|
|
|
if !platformSpecificSource(s.id) {
|
|
proto.Platform = nil
|
|
}
|
|
|
|
dt, err := deterministicMarshal(proto)
|
|
if err != nil {
|
|
return "", nil, nil, nil, err
|
|
}
|
|
|
|
return cache.Store(dt, md, s.constraints.SourceLocations, constraints)
|
|
}
|
|
|
|
func (s *SourceOp) Output() Output {
|
|
return s.output
|
|
}
|
|
|
|
func (s *SourceOp) Inputs() []Output {
|
|
return nil
|
|
}
|
|
|
|
type ImageBlobInfo struct {
|
|
constraintsWrapper
|
|
fileinfoWrapper
|
|
sessionID string
|
|
storeID string
|
|
}
|
|
|
|
type ImageBlobOption interface {
|
|
SetImageBlobOption(*ImageBlobInfo)
|
|
}
|
|
|
|
type FileInfoOption interface {
|
|
HTTPOption
|
|
ImageBlobOption
|
|
}
|
|
|
|
func ImageBlob(ref string, opts ...ImageBlobOption) State {
|
|
bi := &ImageBlobInfo{}
|
|
for _, o := range opts {
|
|
o.SetImageBlobOption(bi)
|
|
}
|
|
attrs := map[string]string{}
|
|
|
|
if bi.Filename != "" {
|
|
attrs[pb.AttrHTTPFilename] = bi.Filename
|
|
}
|
|
if bi.Perm != 0 {
|
|
attrs[pb.AttrHTTPPerm] = "0" + strconv.FormatInt(int64(bi.Perm), 8)
|
|
}
|
|
if bi.UID != 0 {
|
|
attrs[pb.AttrHTTPUID] = strconv.Itoa(bi.UID)
|
|
}
|
|
if bi.GID != 0 {
|
|
attrs[pb.AttrHTTPGID] = strconv.Itoa(bi.GID)
|
|
}
|
|
|
|
addCap(&bi.Constraints, pb.CapSourceImageBlob)
|
|
|
|
var digested reference.Digested
|
|
|
|
r, err := reference.ParseNormalizedNamed(ref)
|
|
if err == nil {
|
|
if _, tagged := r.(reference.Tagged); tagged {
|
|
err = errors.Errorf("tagged image reference not allowed for blob reference")
|
|
} else if ref, ok := r.(reference.Digested); !ok {
|
|
err = errors.Errorf("checksum required in blob reference")
|
|
} else {
|
|
digested = ref
|
|
}
|
|
}
|
|
|
|
repoName := "invalid"
|
|
if digested != nil {
|
|
repoName = digested.String()
|
|
}
|
|
|
|
source := NewSource("docker-image+blob://"+repoName, attrs, bi.Constraints)
|
|
if err != nil {
|
|
source.err = err
|
|
}
|
|
return NewState(source.Output())
|
|
}
|
|
|
|
// OCILayoutBlob returns a state that represents a single digest-addressed blob from an OCI layout store.
|
|
func OCILayoutBlob(ref string, opts ...ImageBlobOption) State {
|
|
bi := &ImageBlobInfo{}
|
|
for _, o := range opts {
|
|
o.SetImageBlobOption(bi)
|
|
}
|
|
attrs := map[string]string{}
|
|
|
|
if bi.Filename != "" {
|
|
attrs[pb.AttrHTTPFilename] = bi.Filename
|
|
}
|
|
if bi.Perm != 0 {
|
|
attrs[pb.AttrHTTPPerm] = "0" + strconv.FormatInt(int64(bi.Perm), 8)
|
|
}
|
|
if bi.UID != 0 {
|
|
attrs[pb.AttrHTTPUID] = strconv.Itoa(bi.UID)
|
|
}
|
|
if bi.GID != 0 {
|
|
attrs[pb.AttrHTTPGID] = strconv.Itoa(bi.GID)
|
|
}
|
|
if bi.sessionID != "" {
|
|
attrs[pb.AttrOCILayoutSessionID] = bi.sessionID
|
|
}
|
|
if bi.storeID != "" {
|
|
attrs[pb.AttrOCILayoutStoreID] = bi.storeID
|
|
}
|
|
|
|
addCap(&bi.Constraints, pb.CapSourceImageBlob)
|
|
|
|
var digested reference.Digested
|
|
|
|
r, err := reference.ParseNormalizedNamed(ref)
|
|
if err == nil {
|
|
if _, tagged := r.(reference.Tagged); tagged {
|
|
err = errors.Errorf("tagged image reference not allowed for blob reference")
|
|
} else if ref, ok := r.(reference.Digested); !ok {
|
|
err = errors.Errorf("checksum required in blob reference")
|
|
} else {
|
|
digested = ref
|
|
}
|
|
}
|
|
|
|
repoName := "invalid"
|
|
if digested != nil {
|
|
repoName = digested.String()
|
|
}
|
|
|
|
source := NewSource("oci-layout+blob://"+repoName, attrs, bi.Constraints)
|
|
if err != nil {
|
|
source.err = err
|
|
}
|
|
return NewState(source.Output())
|
|
}
|
|
|
|
// Image returns a state that represents a docker image in a registry.
|
|
// Example:
|
|
//
|
|
// st := llb.Image("busybox:latest")
|
|
func Image(ref string, opts ...ImageOption) State {
|
|
r, err := reference.ParseNormalizedNamed(ref)
|
|
if err == nil {
|
|
r = reference.TagNameOnly(r)
|
|
ref = r.String()
|
|
}
|
|
var info ImageInfo
|
|
for _, opt := range opts {
|
|
opt.SetImageOption(&info)
|
|
}
|
|
|
|
addCap(&info.Constraints, pb.CapSourceImage)
|
|
|
|
attrs := map[string]string{}
|
|
if info.resolveMode != 0 {
|
|
attrs[pb.AttrImageResolveMode] = info.resolveMode.String()
|
|
if info.resolveMode == ResolveModeForcePull {
|
|
addCap(&info.Constraints, pb.CapSourceImageResolveMode) // only require cap for security enforced mode
|
|
}
|
|
}
|
|
|
|
if info.RecordType != "" {
|
|
attrs[pb.AttrImageRecordType] = info.RecordType
|
|
}
|
|
|
|
if ll := info.layerLimit; ll != nil {
|
|
attrs[pb.AttrImageLayerLimit] = strconv.FormatInt(int64(*ll), 10)
|
|
addCap(&info.Constraints, pb.CapSourceImageLayerLimit)
|
|
}
|
|
|
|
if info.checksum != "" {
|
|
attrs[pb.AttrImageChecksum] = info.checksum.String()
|
|
addCap(&info.Constraints, pb.CapSourceImageChecksum)
|
|
}
|
|
|
|
src := NewSource("docker-image://"+ref, attrs, info.Constraints) // controversial
|
|
if err != nil {
|
|
src.err = err
|
|
} else if info.metaResolver != nil {
|
|
if _, ok := r.(reference.Digested); ok || !info.resolveDigest {
|
|
return NewState(src.Output()).Async(func(ctx context.Context, st State, c *Constraints) (State, error) {
|
|
p := info.Platform
|
|
if p == nil {
|
|
p = c.Platform
|
|
}
|
|
_, _, dt, err := info.metaResolver.ResolveImageConfig(ctx, ref, sourceresolver.Opt{
|
|
ImageOpt: &sourceresolver.ResolveImageOpt{
|
|
Platform: p,
|
|
ResolveMode: info.resolveMode.String(),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return State{}, err
|
|
}
|
|
return st.WithImageConfig(dt)
|
|
})
|
|
}
|
|
return Scratch().Async(func(ctx context.Context, _ State, c *Constraints) (State, error) {
|
|
p := info.Platform
|
|
if p == nil {
|
|
p = c.Platform
|
|
}
|
|
ref, dgst, dt, err := info.metaResolver.ResolveImageConfig(context.TODO(), ref, sourceresolver.Opt{
|
|
ImageOpt: &sourceresolver.ResolveImageOpt{
|
|
Platform: p,
|
|
ResolveMode: info.resolveMode.String(),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return State{}, err
|
|
}
|
|
r, err := reference.ParseNormalizedNamed(ref)
|
|
if err != nil {
|
|
return State{}, err
|
|
}
|
|
if dgst != "" {
|
|
r, err = reference.WithDigest(r, dgst)
|
|
if err != nil {
|
|
return State{}, err
|
|
}
|
|
}
|
|
return NewState(NewSource("docker-image://"+r.String(), attrs, info.Constraints).Output()).WithImageConfig(dt)
|
|
})
|
|
}
|
|
return NewState(src.Output())
|
|
}
|
|
|
|
type ImageOption interface {
|
|
SetImageOption(*ImageInfo)
|
|
}
|
|
|
|
type imageOptionFunc func(*ImageInfo)
|
|
|
|
func (fn imageOptionFunc) SetImageOption(ii *ImageInfo) {
|
|
fn(ii)
|
|
}
|
|
|
|
type imageBlobOptionFunc func(*ImageBlobInfo)
|
|
|
|
func (fn imageBlobOptionFunc) SetImageBlobOption(ib *ImageBlobInfo) {
|
|
fn(ib)
|
|
}
|
|
|
|
var MarkImageInternal = imageOptionFunc(func(ii *ImageInfo) {
|
|
ii.RecordType = "internal"
|
|
})
|
|
|
|
type ResolveMode int
|
|
|
|
const (
|
|
ResolveModeDefault ResolveMode = iota
|
|
ResolveModeForcePull
|
|
ResolveModePreferLocal
|
|
)
|
|
|
|
func (r ResolveMode) SetImageOption(ii *ImageInfo) {
|
|
ii.resolveMode = r
|
|
}
|
|
|
|
func (r ResolveMode) String() string {
|
|
switch r {
|
|
case ResolveModeDefault:
|
|
return pb.AttrImageResolveModeDefault
|
|
case ResolveModeForcePull:
|
|
return pb.AttrImageResolveModeForcePull
|
|
case ResolveModePreferLocal:
|
|
return pb.AttrImageResolveModePreferLocal
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
type ImageInfo struct {
|
|
constraintsWrapper
|
|
metaResolver ImageMetaResolver
|
|
resolveDigest bool
|
|
resolveMode ResolveMode
|
|
layerLimit *int
|
|
checksum digest.Digest
|
|
RecordType string
|
|
}
|
|
|
|
const (
|
|
GitAuthHeaderKey = "GIT_AUTH_HEADER"
|
|
GitAuthTokenKey = "GIT_AUTH_TOKEN"
|
|
)
|
|
|
|
// Git returns a state that represents a git repository.
|
|
// Example:
|
|
//
|
|
// st := llb.Git("https://github.com/moby/buildkit.git", "v0.11.6")
|
|
//
|
|
// The example fetches the v0.11.6 tag of the buildkit repository.
|
|
// You can also use a commit hash or a branch name.
|
|
//
|
|
// Other URL formats are supported such as "git@github.com:moby/buildkit.git", "git://...", "ssh://..."
|
|
// Formats that utilize SSH may need to supply credentials as a [GitOption].
|
|
// You may need to check the source code for a full list of supported formats.
|
|
//
|
|
// Fragment can be used to pass ref:subdir format that can set in (old-style)
|
|
// Docker Git URL format after # . This is provided for backwards compatibility.
|
|
// It is recommended to leave it empty and call GitRef(), GitSubdir() options instead.
|
|
//
|
|
// By default the git repository is cloned with `--depth=1` to reduce the amount of data downloaded.
|
|
// Additionally the ".git" directory is removed after the clone, you can keep ith with the [KeepGitDir] [GitOption].
|
|
func Git(url, fragment string, opts ...GitOption) State {
|
|
remote, err := gitutil.ParseURL(url)
|
|
if errors.Is(err, gitutil.ErrUnknownProtocol) {
|
|
url = "https://" + url
|
|
remote, err = gitutil.ParseURL(url)
|
|
}
|
|
if remote != nil {
|
|
url = remote.Remote
|
|
}
|
|
|
|
gi := &GitInfo{
|
|
AuthHeaderSecret: GitAuthHeaderKey,
|
|
AuthTokenSecret: GitAuthTokenKey,
|
|
}
|
|
ref, subdir, ok := strings.Cut(fragment, ":")
|
|
subdir = path.Join("/", subdir)
|
|
subdir = strings.TrimPrefix(subdir, "/")
|
|
if ref != "" {
|
|
GitRef(ref).SetGitOption(gi)
|
|
}
|
|
if ok && subdir != "" {
|
|
GitSubDir(subdir).SetGitOption(gi)
|
|
}
|
|
for _, o := range opts {
|
|
o.SetGitOption(gi)
|
|
}
|
|
var id string
|
|
if err != nil {
|
|
// If we can't parse the URL, just use the full URL as the ID. The git
|
|
// operation will fail later on.
|
|
id = url
|
|
} else {
|
|
// We construct the ID manually here, so that we can create the same ID
|
|
// for different protocols (e.g. https and ssh) that have the same
|
|
// host/path/fragment combination.
|
|
id = remote.Host + path.Join("/", remote.Path)
|
|
if gi.Ref != "" || gi.SubDir != "" {
|
|
id += "#" + gi.Ref
|
|
if gi.SubDir != "" {
|
|
id += ":" + gi.SubDir
|
|
}
|
|
}
|
|
}
|
|
attrs := map[string]string{}
|
|
if gi.KeepGitDir {
|
|
attrs[pb.AttrKeepGitDir] = "true"
|
|
addCap(&gi.Constraints, pb.CapSourceGitKeepDir)
|
|
}
|
|
if url != "" {
|
|
attrs[pb.AttrFullRemoteURL] = url
|
|
addCap(&gi.Constraints, pb.CapSourceGitFullURL)
|
|
}
|
|
if gi.AuthTokenSecret != "" {
|
|
attrs[pb.AttrAuthTokenSecret] = gi.AuthTokenSecret
|
|
if gi.addAuthCap {
|
|
addCap(&gi.Constraints, pb.CapSourceGitHTTPAuth)
|
|
}
|
|
}
|
|
if gi.AuthHeaderSecret != "" {
|
|
attrs[pb.AttrAuthHeaderSecret] = gi.AuthHeaderSecret
|
|
if gi.addAuthCap {
|
|
addCap(&gi.Constraints, pb.CapSourceGitHTTPAuth)
|
|
}
|
|
}
|
|
if remote != nil && remote.Scheme == gitutil.SSHProtocol {
|
|
if gi.KnownSSHHosts != "" {
|
|
attrs[pb.AttrKnownSSHHosts] = gi.KnownSSHHosts
|
|
} else {
|
|
keyscan, err := sshutil.SSHKeyScan(remote.Host)
|
|
if err == nil {
|
|
// best effort
|
|
attrs[pb.AttrKnownSSHHosts] = keyscan
|
|
}
|
|
}
|
|
addCap(&gi.Constraints, pb.CapSourceGitKnownSSHHosts)
|
|
|
|
if gi.MountSSHSock == "" {
|
|
attrs[pb.AttrMountSSHSock] = "default"
|
|
} else {
|
|
attrs[pb.AttrMountSSHSock] = gi.MountSSHSock
|
|
}
|
|
addCap(&gi.Constraints, pb.CapSourceGitMountSSHSock)
|
|
}
|
|
|
|
checksum := gi.Checksum
|
|
if checksum != "" {
|
|
attrs[pb.AttrGitChecksum] = checksum
|
|
addCap(&gi.Constraints, pb.CapSourceGitChecksum)
|
|
}
|
|
|
|
if gi.SkipSubmodules {
|
|
attrs[pb.AttrGitSkipSubmodules] = "true"
|
|
addCap(&gi.Constraints, pb.CapSourceGitSkipSubmodules)
|
|
}
|
|
|
|
if gi.MTime != "" {
|
|
attrs[pb.AttrGitMTime] = gi.MTime
|
|
addCap(&gi.Constraints, pb.CapSourceGitMTime)
|
|
}
|
|
|
|
if gi.FetchByCommit {
|
|
attrs[pb.AttrGitFetchByCommit] = "true"
|
|
addCap(&gi.Constraints, pb.CapSourceGitFetchByCommit)
|
|
}
|
|
if gi.Bundle != "" {
|
|
attrs[pb.AttrGitBundle] = gi.Bundle
|
|
addCap(&gi.Constraints, pb.CapSourceGitBundle)
|
|
}
|
|
if gi.BundleOCISessionID != "" {
|
|
attrs[pb.AttrOCILayoutSessionID] = gi.BundleOCISessionID
|
|
}
|
|
if gi.BundleOCIStoreID != "" {
|
|
attrs[pb.AttrOCILayoutStoreID] = gi.BundleOCIStoreID
|
|
}
|
|
|
|
if gi.CheckoutBundle {
|
|
attrs[pb.AttrGitCheckoutBundle] = "true"
|
|
addCap(&gi.Constraints, pb.CapSourceGitCheckoutBundle)
|
|
}
|
|
|
|
addCap(&gi.Constraints, pb.CapSourceGit)
|
|
|
|
source := NewSource("git://"+id, attrs, gi.Constraints)
|
|
return NewState(source.Output())
|
|
}
|
|
|
|
type GitOption interface {
|
|
SetGitOption(*GitInfo)
|
|
}
|
|
type gitOptionFunc func(*GitInfo)
|
|
|
|
func (fn gitOptionFunc) SetGitOption(gi *GitInfo) {
|
|
fn(gi)
|
|
}
|
|
|
|
type GitInfo struct {
|
|
constraintsWrapper
|
|
KeepGitDir bool
|
|
AuthTokenSecret string
|
|
AuthHeaderSecret string
|
|
addAuthCap bool
|
|
KnownSSHHosts string
|
|
MountSSHSock string
|
|
Checksum string
|
|
Ref string
|
|
SubDir string
|
|
SkipSubmodules bool
|
|
MTime string
|
|
Bundle string
|
|
BundleOCISessionID string
|
|
BundleOCIStoreID string
|
|
CheckoutBundle bool
|
|
FetchByCommit bool
|
|
}
|
|
|
|
func GitRef(v string) GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.Ref = v
|
|
})
|
|
}
|
|
|
|
func GitSubDir(v string) GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.SubDir = v
|
|
})
|
|
}
|
|
|
|
func GitSkipSubmodules() GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.SkipSubmodules = true
|
|
})
|
|
}
|
|
|
|
// GitMTimeCommit sets file modification times to the commit timestamp
|
|
// of the resolved commit, rather than the checkout time.
|
|
func GitMTimeCommit() GitOption {
|
|
return GitMTime("commit")
|
|
}
|
|
|
|
// GitMTime sets the file modification time policy for git sources.
|
|
// Valid values are "checkout" (default) and "commit".
|
|
func GitMTime(v string) GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.MTime = v
|
|
})
|
|
}
|
|
|
|
func KeepGitDir() GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.KeepGitDir = true
|
|
})
|
|
}
|
|
|
|
func AuthTokenSecret(v string) GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.AuthTokenSecret = v
|
|
gi.addAuthCap = true
|
|
})
|
|
}
|
|
|
|
func KnownSSHHosts(key string) GitOption {
|
|
key = strings.TrimSuffix(key, "\n")
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.KnownSSHHosts = gi.KnownSSHHosts + key + "\n"
|
|
})
|
|
}
|
|
|
|
func MountSSHSock(sshID string) GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.MountSSHSock = sshID
|
|
})
|
|
}
|
|
|
|
func GitChecksum(v string) GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.Checksum = v
|
|
})
|
|
}
|
|
|
|
// GitFetchByCommit makes the git source trust the provided checksum as the
|
|
// commit to fetch, without resolving the ref against the remote. The ref, if
|
|
// set, is applied locally after the commit is fetched so that cache keys
|
|
// still depend on it. This is useful when the remote ref may have moved
|
|
// since the original resolution.
|
|
//
|
|
// Unqualified ref names are canonicalized to "refs/heads/<name>" to match
|
|
// the normal path's cache keys for branches. Tags must be passed fully
|
|
// qualified ("refs/tags/<name>").
|
|
func GitFetchByCommit() GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.FetchByCommit = true
|
|
})
|
|
}
|
|
|
|
// GitBundleInfo carries scheme-specific configuration for [GitBundleURL].
|
|
// It is populated by [GitBundleOption] values and consumed by GitBundleURL.
|
|
type GitBundleInfo struct {
|
|
// OCISessionID pins the OCI-layout bundle fetch to a specific client
|
|
// session. Only meaningful when the locator uses the
|
|
// "oci-layout+blob://" scheme.
|
|
OCISessionID string
|
|
// OCIStoreID overrides the OCI-layout store name derived from the
|
|
// bundle locator body. Only meaningful when the locator uses the
|
|
// "oci-layout+blob://" scheme.
|
|
OCIStoreID string
|
|
}
|
|
|
|
// GitBundleOption configures bundle-specific behavior for [GitBundleURL].
|
|
type GitBundleOption interface {
|
|
SetGitBundleOption(*GitBundleInfo)
|
|
}
|
|
|
|
type gitBundleOptionFunc func(*GitBundleInfo)
|
|
|
|
func (fn gitBundleOptionFunc) SetGitBundleOption(bi *GitBundleInfo) {
|
|
fn(bi)
|
|
}
|
|
|
|
// GitBundleURL configures the git source to import the commit history from a
|
|
// pre-built git bundle instead of fetching from the upstream Git remote. The
|
|
// locator must use one of the following schemes:
|
|
//
|
|
// docker-image+blob://<registry-ref>@sha256:<digest>
|
|
// oci-layout+blob://<ref>@sha256:<digest>
|
|
//
|
|
// [GitChecksum] is required when GitBundleURL is used and the commit it
|
|
// identifies must be reachable from a ref inside the bundle (i.e. the bundle
|
|
// must contain that commit; BuildKit validates this after import). Auth and
|
|
// SSH options are ignored in bundle mode. Submodules, if present, still fetch
|
|
// from their own remotes. See [GitCheckoutBundle] for the checkout side; it
|
|
// is mutually exclusive with [KeepGitDir] and [GitSubDir].
|
|
//
|
|
// Scheme-specific behavior can be tuned via [GitBundleOption] values, e.g.
|
|
// [GitBundleOCIStore] for the "oci-layout+blob://" scheme.
|
|
func GitBundleURL(locator string, opts ...GitBundleOption) GitOption {
|
|
bi := &GitBundleInfo{}
|
|
for _, o := range opts {
|
|
o.SetGitBundleOption(bi)
|
|
}
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.Bundle = locator
|
|
gi.BundleOCISessionID = bi.OCISessionID
|
|
gi.BundleOCIStoreID = bi.OCIStoreID
|
|
})
|
|
}
|
|
|
|
// GitBundleOCIStore configures the OCI-layout session and store id used to
|
|
// resolve a bundle locator with the "oci-layout+blob://" scheme. When unset,
|
|
// the locator body (the part before "@digest") is used as the store id and no
|
|
// session is pinned. This mirrors [ImageBlobOCIStore] for regular OCI-layout
|
|
// blobs.
|
|
//
|
|
// Only meaningful when passed to [GitBundleURL] together with a locator that
|
|
// uses the "oci-layout+blob://" scheme.
|
|
func GitBundleOCIStore(sessionID, storeID string) GitBundleOption {
|
|
return gitBundleOptionFunc(func(bi *GitBundleInfo) {
|
|
bi.OCISessionID = sessionID
|
|
bi.OCIStoreID = storeID
|
|
})
|
|
}
|
|
|
|
// GitCheckoutBundle produces a single-file git bundle at the checkout mount
|
|
// root (filename "bundle") containing the resolved commit, instead of a
|
|
// worktree. Mutually exclusive with [KeepGitDir] and [GitSubDir]. Submodules
|
|
// are not included in the bundle. Pair with [GitBundleURL] to re-import the
|
|
// bundle back into a later build.
|
|
func GitCheckoutBundle() GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.CheckoutBundle = true
|
|
})
|
|
}
|
|
|
|
// AuthOption can be used with either HTTP or Git sources.
|
|
type AuthOption interface {
|
|
GitOption
|
|
HTTPOption
|
|
}
|
|
|
|
// AuthHeaderSecret returns an AuthOption that defines the name of a
|
|
// secret to use for HTTP based authentication.
|
|
func AuthHeaderSecret(secretName string) AuthOption {
|
|
return struct {
|
|
GitOption
|
|
HTTPOption
|
|
}{
|
|
GitOption: gitOptionFunc(func(gi *GitInfo) {
|
|
gi.AuthHeaderSecret = secretName
|
|
gi.addAuthCap = true
|
|
}),
|
|
HTTPOption: httpOptionFunc(func(hi *HTTPInfo) {
|
|
hi.AuthHeaderSecret = secretName
|
|
}),
|
|
}
|
|
}
|
|
|
|
// Scratch returns a state that represents an empty filesystem.
|
|
func Scratch() State {
|
|
return NewState(nil)
|
|
}
|
|
|
|
// Local returns a state that represents a directory local to the client.
|
|
func Local(name string, opts ...LocalOption) State {
|
|
gi := &LocalInfo{}
|
|
|
|
for _, o := range opts {
|
|
o.SetLocalOption(gi)
|
|
}
|
|
attrs := map[string]string{}
|
|
if gi.SessionID != "" {
|
|
attrs[pb.AttrLocalSessionID] = gi.SessionID
|
|
addCap(&gi.Constraints, pb.CapSourceLocalSessionID)
|
|
}
|
|
if gi.IncludePatterns != "" {
|
|
attrs[pb.AttrIncludePatterns] = gi.IncludePatterns
|
|
addCap(&gi.Constraints, pb.CapSourceLocalIncludePatterns)
|
|
}
|
|
if gi.FollowPaths != "" {
|
|
attrs[pb.AttrFollowPaths] = gi.FollowPaths
|
|
addCap(&gi.Constraints, pb.CapSourceLocalFollowPaths)
|
|
}
|
|
if gi.ExcludePatterns != "" {
|
|
attrs[pb.AttrExcludePatterns] = gi.ExcludePatterns
|
|
addCap(&gi.Constraints, pb.CapSourceLocalExcludePatterns)
|
|
}
|
|
if gi.SharedKeyHint != "" {
|
|
attrs[pb.AttrSharedKeyHint] = gi.SharedKeyHint
|
|
addCap(&gi.Constraints, pb.CapSourceLocalSharedKeyHint)
|
|
}
|
|
if gi.Differ.Type != "" {
|
|
attrs[pb.AttrLocalDiffer] = string(gi.Differ.Type)
|
|
if gi.Differ.Required {
|
|
addCap(&gi.Constraints, pb.CapSourceLocalDiffer)
|
|
}
|
|
}
|
|
if gi.MetadataOnlyCollector {
|
|
attrs[pb.AttrMetadataTransfer] = "true"
|
|
if gi.MetadataOnlyExceptions != "" {
|
|
attrs[pb.AttrMetadataTransferExclude] = gi.MetadataOnlyExceptions
|
|
}
|
|
addCap(&gi.Constraints, pb.CapSourceMetadataTransfer)
|
|
}
|
|
|
|
addCap(&gi.Constraints, pb.CapSourceLocal)
|
|
|
|
source := NewSource("local://"+name, attrs, gi.Constraints)
|
|
return NewState(source.Output())
|
|
}
|
|
|
|
type LocalOption interface {
|
|
SetLocalOption(*LocalInfo)
|
|
}
|
|
|
|
type localOptionFunc func(*LocalInfo)
|
|
|
|
func (fn localOptionFunc) SetLocalOption(li *LocalInfo) {
|
|
fn(li)
|
|
}
|
|
|
|
func SessionID(id string) LocalOption {
|
|
return localOptionFunc(func(li *LocalInfo) {
|
|
li.SessionID = id
|
|
})
|
|
}
|
|
|
|
func IncludePatterns(p []string) LocalOption {
|
|
return localOptionFunc(func(li *LocalInfo) {
|
|
if len(p) == 0 {
|
|
li.IncludePatterns = ""
|
|
return
|
|
}
|
|
dt, _ := json.Marshal(p) // empty on error
|
|
li.IncludePatterns = string(dt)
|
|
})
|
|
}
|
|
|
|
func FollowPaths(p []string) LocalOption {
|
|
return localOptionFunc(func(li *LocalInfo) {
|
|
if len(p) == 0 {
|
|
li.FollowPaths = ""
|
|
return
|
|
}
|
|
dt, _ := json.Marshal(p) // empty on error
|
|
li.FollowPaths = string(dt)
|
|
})
|
|
}
|
|
|
|
func ExcludePatterns(p []string) LocalOption {
|
|
return localOptionFunc(func(li *LocalInfo) {
|
|
if len(p) == 0 {
|
|
li.ExcludePatterns = ""
|
|
return
|
|
}
|
|
dt, _ := json.Marshal(p) // empty on error
|
|
li.ExcludePatterns = string(dt)
|
|
})
|
|
}
|
|
|
|
func SharedKeyHint(h string) LocalOption {
|
|
return localOptionFunc(func(li *LocalInfo) {
|
|
li.SharedKeyHint = h
|
|
})
|
|
}
|
|
|
|
func Differ(t DiffType, required bool) LocalOption {
|
|
return localOptionFunc(func(li *LocalInfo) {
|
|
li.Differ = DifferInfo{
|
|
Type: t,
|
|
Required: required,
|
|
}
|
|
})
|
|
}
|
|
|
|
func MetadataOnlyTransfer(exceptions []string) LocalOption {
|
|
return localOptionFunc(func(li *LocalInfo) {
|
|
li.MetadataOnlyCollector = true
|
|
if len(exceptions) == 0 {
|
|
li.MetadataOnlyExceptions = ""
|
|
} else {
|
|
dt, _ := json.Marshal(exceptions) // empty on error
|
|
li.MetadataOnlyExceptions = string(dt)
|
|
}
|
|
})
|
|
}
|
|
|
|
func OCILayout(ref string, opts ...OCILayoutOption) State {
|
|
gi := &OCILayoutInfo{}
|
|
|
|
for _, o := range opts {
|
|
o.SetOCILayoutOption(gi)
|
|
}
|
|
attrs := map[string]string{}
|
|
if gi.sessionID != "" {
|
|
attrs[pb.AttrOCILayoutSessionID] = gi.sessionID
|
|
}
|
|
if gi.storeID != "" {
|
|
attrs[pb.AttrOCILayoutStoreID] = gi.storeID
|
|
}
|
|
if gi.layerLimit != nil {
|
|
attrs[pb.AttrOCILayoutLayerLimit] = strconv.FormatInt(int64(*gi.layerLimit), 10)
|
|
}
|
|
|
|
addCap(&gi.Constraints, pb.CapSourceOCILayout)
|
|
|
|
source := NewSource("oci-layout://"+ref, attrs, gi.Constraints)
|
|
return NewState(source.Output())
|
|
}
|
|
|
|
type OCILayoutOption interface {
|
|
SetOCILayoutOption(*OCILayoutInfo)
|
|
}
|
|
|
|
type ociLayoutOptionFunc func(*OCILayoutInfo)
|
|
|
|
func (fn ociLayoutOptionFunc) SetOCILayoutOption(li *OCILayoutInfo) {
|
|
fn(li)
|
|
}
|
|
|
|
func OCIStore(sessionID string, storeID string) OCILayoutOption {
|
|
return ociLayoutOptionFunc(func(oi *OCILayoutInfo) {
|
|
oi.sessionID = sessionID
|
|
oi.storeID = storeID
|
|
})
|
|
}
|
|
|
|
// ImageBlobOCIStore returns an [ImageBlobOption] that configures the OCI layout session/store used by [OCILayoutBlob].
|
|
func ImageBlobOCIStore(sessionID string, storeID string) ImageBlobOption {
|
|
return imageBlobOptionFunc(func(ib *ImageBlobInfo) {
|
|
ib.sessionID = sessionID
|
|
ib.storeID = storeID
|
|
})
|
|
}
|
|
|
|
func OCILayerLimit(limit int) OCILayoutOption {
|
|
return ociLayoutOptionFunc(func(oi *OCILayoutInfo) {
|
|
oi.layerLimit = &limit
|
|
})
|
|
}
|
|
|
|
func OCIChecksum(dgst digest.Digest) OCILayoutOption {
|
|
return ociLayoutOptionFunc(func(oi *OCILayoutInfo) {
|
|
oi.checksum = dgst
|
|
})
|
|
}
|
|
|
|
type OCILayoutInfo struct {
|
|
constraintsWrapper
|
|
sessionID string
|
|
storeID string
|
|
layerLimit *int
|
|
checksum digest.Digest
|
|
}
|
|
|
|
type DiffType string
|
|
|
|
const (
|
|
// DiffNone will do no file comparisons, all files in the Local source will
|
|
// be retransmitted.
|
|
DiffNone DiffType = pb.AttrLocalDifferNone
|
|
// DiffMetadata will compare file metadata (size, modified time, mode, owner,
|
|
// group, device and link name) to determine if the files in the Local source need
|
|
// to be retransmitted. This is the default behavior.
|
|
DiffMetadata DiffType = pb.AttrLocalDifferMetadata
|
|
)
|
|
|
|
type DifferInfo struct {
|
|
Type DiffType
|
|
Required bool
|
|
}
|
|
|
|
type LocalInfo struct {
|
|
constraintsWrapper
|
|
SessionID string
|
|
IncludePatterns string
|
|
ExcludePatterns string
|
|
FollowPaths string
|
|
SharedKeyHint string
|
|
Differ DifferInfo
|
|
MetadataOnlyCollector bool
|
|
MetadataOnlyExceptions string
|
|
}
|
|
|
|
func HTTP(url string, opts ...HTTPOption) State {
|
|
hi := &HTTPInfo{}
|
|
for _, o := range opts {
|
|
o.SetHTTPOption(hi)
|
|
}
|
|
attrs := map[string]string{}
|
|
if hi.Checksum != "" {
|
|
attrs[pb.AttrHTTPChecksum] = hi.Checksum.String()
|
|
addCap(&hi.Constraints, pb.CapSourceHTTPChecksum)
|
|
}
|
|
if hi.Filename != "" {
|
|
attrs[pb.AttrHTTPFilename] = hi.Filename
|
|
}
|
|
if hi.Perm != 0 {
|
|
attrs[pb.AttrHTTPPerm] = "0" + strconv.FormatInt(int64(hi.Perm), 8)
|
|
addCap(&hi.Constraints, pb.CapSourceHTTPPerm)
|
|
}
|
|
if hi.UID != 0 {
|
|
attrs[pb.AttrHTTPUID] = strconv.Itoa(hi.UID)
|
|
addCap(&hi.Constraints, pb.CapSourceHTTPUIDGID)
|
|
}
|
|
if hi.GID != 0 {
|
|
attrs[pb.AttrHTTPGID] = strconv.Itoa(hi.GID)
|
|
addCap(&hi.Constraints, pb.CapSourceHTTPUIDGID)
|
|
}
|
|
if hi.AuthHeaderSecret != "" {
|
|
attrs[pb.AttrHTTPAuthHeaderSecret] = hi.AuthHeaderSecret
|
|
addCap(&hi.Constraints, pb.CapSourceHTTPAuth)
|
|
}
|
|
if hi.Header != nil {
|
|
hi.Header.setAttrs(attrs)
|
|
addCap(&hi.Constraints, pb.CapSourceHTTPHeader)
|
|
}
|
|
if hi.Signature != nil {
|
|
if len(hi.Signature.PubKey) > 0 {
|
|
attrs[pb.AttrHTTPSignatureVerifyPubKey] = string(hi.Signature.PubKey)
|
|
}
|
|
if len(hi.Signature.Signature) > 0 {
|
|
attrs[pb.AttrHTTPSignatureVerify] = string(hi.Signature.Signature)
|
|
}
|
|
addCap(&hi.Constraints, pb.CapSourceHTTPSignatureVerify)
|
|
}
|
|
|
|
addCap(&hi.Constraints, pb.CapSourceHTTP)
|
|
source := NewSource(url, attrs, hi.Constraints)
|
|
return NewState(source.Output())
|
|
}
|
|
|
|
type fileInfo struct {
|
|
Filename string
|
|
Perm int
|
|
UID int
|
|
GID int
|
|
}
|
|
|
|
type fileinfoWrapper struct {
|
|
fileInfo
|
|
}
|
|
|
|
type fileInfoOptFunc func(f *fileInfo)
|
|
|
|
func (fn fileInfoOptFunc) SetHTTPOption(hi *HTTPInfo) {
|
|
fn(&hi.fileInfo)
|
|
}
|
|
|
|
func (fn fileInfoOptFunc) SetImageBlobOption(ib *ImageBlobInfo) {
|
|
fn(&ib.fileInfo)
|
|
}
|
|
|
|
// HTTPSignatureInfo configures detached-signature verification for HTTP
|
|
// sources. The current implementation uses inline armored signatures.
|
|
type HTTPSignatureInfo struct {
|
|
PubKey []byte
|
|
|
|
// Signature is an inline detached armored OpenPGP signature.
|
|
Signature []byte
|
|
}
|
|
|
|
type HTTPInfo struct {
|
|
constraintsWrapper
|
|
fileinfoWrapper
|
|
Checksum digest.Digest
|
|
AuthHeaderSecret string
|
|
Header *HTTPHeader
|
|
Signature *HTTPSignatureInfo
|
|
}
|
|
|
|
type HTTPOption interface {
|
|
SetHTTPOption(*HTTPInfo)
|
|
}
|
|
|
|
type httpOptionFunc func(*HTTPInfo)
|
|
|
|
func (fn httpOptionFunc) SetHTTPOption(hi *HTTPInfo) {
|
|
fn(hi)
|
|
}
|
|
|
|
func Checksum(dgst digest.Digest) HTTPOption {
|
|
return httpOptionFunc(func(hi *HTTPInfo) {
|
|
hi.Checksum = dgst
|
|
})
|
|
}
|
|
|
|
func Chmod(perm os.FileMode) FileInfoOption {
|
|
return fileInfoOptFunc(func(fi *fileInfo) {
|
|
fi.Perm = int(perm) & 0777
|
|
})
|
|
}
|
|
|
|
func Filename(name string) FileInfoOption {
|
|
return fileInfoOptFunc(func(fi *fileInfo) {
|
|
fi.Filename = name
|
|
})
|
|
}
|
|
|
|
func Chown(uid, gid int) FileInfoOption {
|
|
return fileInfoOptFunc(func(fi *fileInfo) {
|
|
fi.UID = uid
|
|
fi.GID = gid
|
|
})
|
|
}
|
|
|
|
// VerifyPGPSignature returns an [HTTPOption] for detached OpenPGP signature
|
|
// verification of the downloaded HTTP payload.
|
|
func VerifyPGPSignature(info HTTPSignatureInfo) HTTPOption {
|
|
return httpOptionFunc(func(hi *HTTPInfo) {
|
|
hi.Signature = &HTTPSignatureInfo{
|
|
PubKey: slices.Clone(info.PubKey),
|
|
Signature: slices.Clone(info.Signature),
|
|
}
|
|
})
|
|
}
|
|
|
|
// Header returns an [HTTPOption] that ensures additional request headers will
|
|
// be sent when retrieving the HTTP source.
|
|
func Header(header HTTPHeader) HTTPOption {
|
|
return httpOptionFunc(func(hi *HTTPInfo) {
|
|
hi.Header = &header
|
|
})
|
|
}
|
|
|
|
type HTTPHeader struct {
|
|
Accept string
|
|
UserAgent string
|
|
}
|
|
|
|
func (hh *HTTPHeader) setAttrs(attrs map[string]string) {
|
|
if hh.Accept != "" {
|
|
attrs[hh.attr("accept")] = hh.Accept
|
|
}
|
|
|
|
if hh.UserAgent != "" {
|
|
attrs[hh.attr("user-agent")] = hh.UserAgent
|
|
}
|
|
}
|
|
|
|
func (hh *HTTPHeader) attr(name string) string {
|
|
return pb.AttrHTTPHeaderPrefix + name
|
|
}
|
|
|
|
func platformSpecificSource(id string) bool {
|
|
return strings.HasPrefix(id, "docker-image://") || strings.HasPrefix(id, "oci-layout://")
|
|
}
|
|
|
|
func addCap(c *Constraints, id apicaps.CapID) {
|
|
if c.Metadata.Caps == nil {
|
|
c.Metadata.Caps = make(map[apicaps.CapID]bool)
|
|
}
|
|
c.Metadata.Caps[id] = true
|
|
}
|