Files
buildkit/source/git/identifier.go
Tonis Tiigi 98e1113c6d lint: modernize Go lint findings
Update non-generated code for the newer lint recommendations by using typed
atomic values, strings.Cut, and slices.Backward where applicable.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-05-14 10:13:21 -07:00

183 lines
5.9 KiB
Go

package git
import (
"strings"
"github.com/containerd/containerd/v2/pkg/reference"
"github.com/moby/buildkit/solver/llbsolver/provenance"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
"github.com/moby/buildkit/source"
srctypes "github.com/moby/buildkit/source/types"
"github.com/moby/buildkit/util/gitutil"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
type GitIdentifier struct {
Remote string
Ref string
Checksum string
Subdir string
KeepGitDir bool
AuthTokenSecret string
AuthHeaderSecret string
MountSSHSock string
KnownSSHHosts string
SkipSubmodules bool
MTime string // "checkout" (default) or "commit"
FetchByCommit bool
// Bundle, when non-empty, instructs the git source to fetch commits from a
// pre-built git bundle stored as a blob instead of fetching from the remote
// repository. The locator must use the "docker-image+blob://" or
// "oci-layout+blob://" scheme.
Bundle string
// BundleOCISessionID, when set, pins the OCI-layout bundle fetch to a
// specific client session. Only meaningful when Bundle uses the
// oci-layout+blob:// scheme.
BundleOCISessionID string
// BundleOCIStoreID, when set, overrides the OCI-layout store name
// derived from the bundle locator. Only meaningful when Bundle uses the
// oci-layout+blob:// scheme.
BundleOCIStoreID string
// CheckoutBundle, when true, produces a single-file git bundle at the
// checkout mount root (filename "bundle") instead of a worktree.
CheckoutBundle bool
VerifySignature *GitSignatureVerifyOptions
}
type GitSignatureVerifyOptions struct {
PubKey []byte
RejectExpiredKeys bool
RequireSignedTag bool // signed tag must be present
IgnoreSignedTag bool // even if signed tag is present, verify signature on commit object
}
func NewGitIdentifier(remoteURL string) (*GitIdentifier, error) {
if !gitutil.IsGitTransport(remoteURL) {
remoteURL = "https://" + remoteURL
}
u, err := gitutil.ParseURL(remoteURL)
if err != nil {
return nil, err
}
repo := GitIdentifier{Remote: u.Remote}
if u.Opts != nil {
repo.Ref = u.Opts.Ref
repo.Subdir = u.Opts.Subdir
}
return &repo, nil
}
func (GitIdentifier) Scheme() string {
return srctypes.GitScheme
}
var _ source.Identifier = (*GitIdentifier)(nil)
func (id *GitIdentifier) Capture(c *provenance.Capture, pin string) error {
remoteURL := id.Remote
if id.Ref != "" {
remoteURL += "#" + id.Ref
}
gs := provenancetypes.GitSource{
URL: remoteURL,
Commit: pin,
}
if id.Bundle != "" {
gs.Bundle = &provenancetypes.GitBundle{URL: id.Bundle}
}
c.AddGit(gs)
if id.AuthTokenSecret != "" {
c.AddSecret(provenancetypes.Secret{
ID: id.AuthTokenSecret,
Optional: true,
})
}
if id.AuthHeaderSecret != "" {
c.AddSecret(provenancetypes.Secret{
ID: id.AuthHeaderSecret,
Optional: true,
})
}
if id.MountSSHSock != "" {
c.AddSSH(provenancetypes.SSH{
ID: id.MountSSHSock,
Optional: true,
})
}
return nil
}
// validateBundleAttrs enforces the bundle-mode constraints that are statically
// decidable at identifier parse time.
func validateBundleAttrs(id *GitIdentifier) error {
if id.Bundle != "" {
if _, _, _, _, err := parseBundleLocator(id.Bundle); err != nil {
return err
}
if id.Checksum == "" {
return errors.Errorf("git.bundle requires git.checksum to be set")
}
// Reject the pathological "Ref=sha1, Checksum=sha2" case: if both
// are set and Ref is itself a commit SHA, the two must agree.
// Without this check bundle mode silently overwrites Ref with
// Checksum in tryRemoteFetch and the mismatch never surfaces.
if id.Ref != "" && gitutil.IsCommitSHA(id.Ref) && !strings.HasPrefix(id.Ref, id.Checksum) {
return errors.Errorf("expected checksum to match %s, got %s", id.Checksum, id.Ref)
}
}
if id.CheckoutBundle {
if id.KeepGitDir {
return errors.Errorf("git.checkoutbundle is incompatible with git.keepgitdir")
}
if id.Subdir != "" {
return errors.Errorf("git.checkoutbundle is incompatible with git.subdir")
}
}
return nil
}
// splitBundleLocator splits a locator of the form "<scheme>://<body>" into
// (scheme, body). It returns ("","") if raw does not contain a "://" separator.
// This avoids net/url for schemes like "oci-layout+blob" whose body contains
// a colon after '@' (the digest) and confuses url.Parse.
func splitBundleLocator(raw string) (string, string) {
const sep = "://"
scheme, body, ok := strings.Cut(raw, sep)
if !ok {
return "", ""
}
return scheme, body
}
// parseBundleLocator extracts the scheme, normalized reference string, the
// reference locator (i.e. the body before '@digest', used as the OCI-layout
// store id), and the digest from a bundle locator of the form
// "<scheme>://<ref>@sha256:<hex>". Only sha256 digests are accepted because
// downloadBundleToFile hashes with sha256 and would fail to match any other
// algorithm.
func parseBundleLocator(raw string) (string, string, string, digest.Digest, error) {
scheme, body := splitBundleLocator(raw)
if scheme == "" {
return "", "", "", "", errors.Errorf("failed to parse git.bundle locator %q: missing scheme", raw)
}
if scheme != srctypes.DockerImageBlobScheme && scheme != srctypes.OCIBlobScheme {
return "", "", "", "", errors.Errorf("git.bundle locator scheme %q is not supported; expected %q or %q", scheme, srctypes.DockerImageBlobScheme, srctypes.OCIBlobScheme)
}
ref, err := reference.Parse(body)
if err != nil {
return "", "", "", "", errors.Wrapf(err, "failed to parse git.bundle locator %q", raw)
}
dgst := ref.Digest()
if err := dgst.Validate(); err != nil {
return "", "", "", "", errors.Wrapf(err, "failed to parse git.bundle locator %q: invalid digest", raw)
}
if dgst.Algorithm() != digest.SHA256 {
return "", "", "", "", errors.Errorf("git.bundle locator %q: digest algorithm %q not supported, expected %q", raw, dgst.Algorithm(), digest.SHA256)
}
return scheme, ref.String(), ref.Locator, dgst, nil
}