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 }