557 lines
12 KiB
Go
557 lines
12 KiB
Go
package llb
|
|
|
|
import (
|
|
"context"
|
|
_ "crypto/sha256" // for opencontainers/go-digest
|
|
"encoding/json"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/moby/buildkit/util/apicaps"
|
|
"github.com/moby/buildkit/util/sshutil"
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type SourceOp struct {
|
|
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) 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) {
|
|
if s.Cached(constraints) {
|
|
return s.Load()
|
|
}
|
|
if err := s.Validate(ctx); 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 := proto.Marshal()
|
|
if err != nil {
|
|
return "", nil, nil, nil, err
|
|
}
|
|
|
|
s.Store(dt, md, s.constraints.SourceLocations, constraints)
|
|
return s.Load()
|
|
}
|
|
|
|
func (s *SourceOp) Output() Output {
|
|
return s.output
|
|
}
|
|
|
|
func (s *SourceOp) Inputs() []Output {
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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) (State, error) {
|
|
_, dt, err := info.metaResolver.ResolveImageConfig(ctx, ref, ResolveImageConfigOpt{
|
|
Platform: info.Constraints.Platform,
|
|
ResolveMode: info.resolveMode.String(),
|
|
})
|
|
if err != nil {
|
|
return State{}, err
|
|
}
|
|
return st.WithImageConfig(dt)
|
|
})
|
|
}
|
|
return Scratch().Async(func(ctx context.Context, _ State) (State, error) {
|
|
dgst, dt, err := info.metaResolver.ResolveImageConfig(context.TODO(), ref, ResolveImageConfigOpt{
|
|
Platform: info.Constraints.Platform,
|
|
ResolveMode: info.resolveMode.String(),
|
|
})
|
|
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)
|
|
}
|
|
|
|
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
|
|
RecordType string
|
|
}
|
|
|
|
const (
|
|
gitProtocolHTTP = iota + 1
|
|
gitProtocolHTTPS
|
|
gitProtocolSSH
|
|
gitProtocolGit
|
|
gitProtocolUnknown
|
|
)
|
|
|
|
func getGitProtocol(remote string) (string, int) {
|
|
prefixes := map[string]int{
|
|
"http://": gitProtocolHTTP,
|
|
"https://": gitProtocolHTTPS,
|
|
"git://": gitProtocolGit,
|
|
"ssh://": gitProtocolSSH,
|
|
}
|
|
protocolType := gitProtocolUnknown
|
|
for prefix, potentialType := range prefixes {
|
|
if strings.HasPrefix(remote, prefix) {
|
|
remote = strings.TrimPrefix(remote, prefix)
|
|
protocolType = potentialType
|
|
}
|
|
}
|
|
|
|
if protocolType == gitProtocolUnknown && sshutil.IsSSHTransport(remote) {
|
|
protocolType = gitProtocolSSH
|
|
}
|
|
|
|
// remove name from ssh
|
|
if protocolType == gitProtocolSSH {
|
|
parts := strings.SplitN(remote, "@", 2)
|
|
if len(parts) == 2 {
|
|
remote = parts[1]
|
|
}
|
|
}
|
|
|
|
return remote, protocolType
|
|
}
|
|
|
|
func Git(remote, ref string, opts ...GitOption) State {
|
|
url := strings.Split(remote, "#")[0]
|
|
|
|
var protocolType int
|
|
remote, protocolType = getGitProtocol(remote)
|
|
|
|
var sshHost string
|
|
if protocolType == gitProtocolSSH {
|
|
parts := strings.SplitN(remote, ":", 2)
|
|
if len(parts) == 2 {
|
|
sshHost = parts[0]
|
|
// keep remote consistent with http(s) version
|
|
remote = parts[0] + "/" + parts[1]
|
|
}
|
|
}
|
|
if protocolType == gitProtocolUnknown {
|
|
url = "https://" + url
|
|
}
|
|
|
|
id := remote
|
|
|
|
if ref != "" {
|
|
id += "#" + ref
|
|
}
|
|
|
|
gi := &GitInfo{
|
|
AuthHeaderSecret: "GIT_AUTH_HEADER",
|
|
AuthTokenSecret: "GIT_AUTH_TOKEN",
|
|
}
|
|
for _, o := range opts {
|
|
o.SetGitOption(gi)
|
|
}
|
|
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 protocolType == gitProtocolSSH {
|
|
if gi.KnownSSHHosts != "" {
|
|
attrs[pb.AttrKnownSSHHosts] = gi.KnownSSHHosts
|
|
} else if sshHost != "" {
|
|
keyscan, err := sshutil.SSHKeyScan(sshHost)
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 AuthHeaderSecret(v string) GitOption {
|
|
return gitOptionFunc(func(gi *GitInfo) {
|
|
gi.AuthHeaderSecret = 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 Scratch() State {
|
|
return NewState(nil)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
})
|
|
}
|
|
|
|
type LocalInfo struct {
|
|
constraintsWrapper
|
|
SessionID string
|
|
IncludePatterns string
|
|
ExcludePatterns string
|
|
FollowPaths string
|
|
SharedKeyHint 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)
|
|
}
|
|
|
|
addCap(&hi.Constraints, pb.CapSourceHTTP)
|
|
source := NewSource(url, attrs, hi.Constraints)
|
|
return NewState(source.Output())
|
|
}
|
|
|
|
type HTTPInfo struct {
|
|
constraintsWrapper
|
|
Checksum digest.Digest
|
|
Filename string
|
|
Perm int
|
|
UID int
|
|
GID int
|
|
}
|
|
|
|
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) HTTPOption {
|
|
return httpOptionFunc(func(hi *HTTPInfo) {
|
|
hi.Perm = int(perm) & 0777
|
|
})
|
|
}
|
|
|
|
func Filename(name string) HTTPOption {
|
|
return httpOptionFunc(func(hi *HTTPInfo) {
|
|
hi.Filename = name
|
|
})
|
|
}
|
|
|
|
func Chown(uid, gid int) HTTPOption {
|
|
return httpOptionFunc(func(hi *HTTPInfo) {
|
|
hi.UID = uid
|
|
hi.GID = gid
|
|
})
|
|
}
|
|
|
|
func platformSpecificSource(id string) bool {
|
|
return strings.HasPrefix(id, "docker-image://")
|
|
}
|
|
|
|
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
|
|
}
|