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/" to match // the normal path's cache keys for branches. Tags must be passed fully // qualified ("refs/tags/"). 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://@sha256: // oci-layout+blob://@sha256: // // [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 }