diff --git a/README.md b/README.md index 4647473..3cfb221 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ brew install hauler ## Acknowledgements `Hauler` wouldn't be possible without the open-source community, but there are a few projects that stand out: - -- [oras cli](https://github.com/oras-project/oras) -- [cosign](https://github.com/sigstore/cosign) +- [containerd](https://github.com/containerd/containerd) - [go-containerregistry](https://github.com/google/go-containerregistry) +- [cosign](https://github.com/sigstore/cosign) \ No newline at end of file diff --git a/cmd/hauler/cli/cli.go b/cmd/hauler/cli/cli.go index 3cb68b8..ff64527 100644 --- a/cmd/hauler/cli/cli.go +++ b/cmd/hauler/cli/cli.go @@ -4,6 +4,7 @@ import ( "context" cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "hauler.dev/go/hauler/internal/flags" "hauler.dev/go/hauler/pkg/consts" @@ -20,6 +21,14 @@ func New(ctx context.Context, ro *flags.CliRootOpts) *cobra.Command { l.SetLevel(ro.LogLevel) l.Debugf("running cli command [%s]", cmd.CommandPath()) + // Suppress WARN-level messages from containerd and other + // libraries that use the global logrus logger. + if ro.LogLevel == "debug" { + logrus.SetLevel(logrus.DebugLevel) + } else { + logrus.SetLevel(logrus.ErrorLevel) + } + return nil }, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/hauler/cli/store.go b/cmd/hauler/cli/store.go index 089ec79..1010a28 100644 --- a/cmd/hauler/cli/store.go +++ b/cmd/hauler/cli/store.go @@ -216,7 +216,7 @@ func addStoreSave(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman func addStoreInfo(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Command { o := &flags.InfoOpts{StoreRootOpts: rso} - var allowedValues = []string{"image", "chart", "file", "sigs", "atts", "sbom", "all"} + var allowedValues = []string{"image", "chart", "file", "sigs", "atts", "sbom", "referrer", "all"} cmd := &cobra.Command{ Use: "info", diff --git a/cmd/hauler/cli/store/add.go b/cmd/hauler/cli/store/add.go index 97ec6c9..84dd50f 100644 --- a/cmd/hauler/cli/store/add.go +++ b/cmd/hauler/cli/store/add.go @@ -25,6 +25,7 @@ import ( "hauler.dev/go/hauler/pkg/getter" "hauler.dev/go/hauler/pkg/log" "hauler.dev/go/hauler/pkg/reference" + "hauler.dev/go/hauler/pkg/retry" "hauler.dev/go/hauler/pkg/store" ) @@ -52,7 +53,7 @@ func storeFile(ctx context.Context, s *store.Layout, fi v1.File) error { } l.Infof("adding file [%s] to the store as [%s]", fi.Path, ref.Name()) - _, err = s.AddOCI(ctx, f, ref.Name()) + _, err = s.AddArtifact(ctx, f, ref.Name()) if err != nil { return err } @@ -73,15 +74,18 @@ func AddImageCmd(ctx context.Context, o *flags.AddImageOpts, s *store.Layout, re // Check if the user provided a key. if o.Key != "" { // verify signature using the provided key. - err := cosign.VerifySignature(ctx, s, o.Key, o.Tlog, cfg.Name, rso, ro) + err := cosign.VerifySignature(ctx, o.Key, o.Tlog, cfg.Name, rso, ro) if err != nil { return err } l.Infof("signature verified for image [%s]", cfg.Name) } else if o.CertIdentityRegexp != "" || o.CertIdentity != "" { - // verify signature using keyless details + // verify signature using keyless details. + // Keyless (Fulcio) certificates expire after ~10 minutes, so the transparency + // log is always required to prove the cert was valid at signing time — ignore + // --use-tlog-verify for this path and always check tlog. l.Infof("verifying keyless signature for [%s]", cfg.Name) - err := cosign.VerifyKeylessSignature(ctx, s, o.CertIdentity, o.CertIdentityRegexp, o.CertOidcIssuer, o.CertOidcIssuerRegexp, o.CertGithubWorkflowRepository, o.Tlog, cfg.Name, rso, ro) + err := cosign.VerifyKeylessSignature(ctx, o.CertIdentity, o.CertIdentityRegexp, o.CertOidcIssuer, o.CertOidcIssuerRegexp, o.CertGithubWorkflowRepository, cfg.Name, rso, ro) if err != nil { return err } @@ -114,8 +118,10 @@ func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform strin } } - // copy and sig verification - err = cosign.SaveImage(ctx, s, r.Name(), platform, rso, ro) + // fetch image along with any associated signatures and attestations + err = retry.Operation(ctx, rso, ro, func() error { + return s.AddImage(ctx, r.Name(), platform) + }) if err != nil { if ro.IgnoreErrors { l.Warnf("unable to add image [%s] to store: %v... skipping...", r.Name(), err) @@ -129,14 +135,20 @@ func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform strin if rewrite != "" { rewrite = strings.TrimPrefix(rewrite, "/") if !strings.Contains(rewrite, ":") { - rewrite = strings.Join([]string{rewrite, r.(name.Tag).TagStr()}, ":") + if tag, ok := r.(name.Tag); ok { + rewrite = rewrite + ":" + tag.TagStr() + } else { + return fmt.Errorf("cannot rewrite digest reference [%s] without an explicit tag in the rewrite", r.Name()) + } } // rename image name in store newRef, err := name.ParseReference(rewrite) if err != nil { - l.Errorf("unable to parse rewrite name: %w", err) + return fmt.Errorf("unable to parse rewrite name [%s]: %w", rewrite, err) + } + if err := rewriteReference(ctx, s, r, newRef); err != nil { + return err } - rewriteReference(ctx, s, r, newRef) } l.Infof("successfully added image [%s]", r.Name()) @@ -146,19 +158,33 @@ func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform strin func rewriteReference(ctx context.Context, s *store.Layout, oldRef name.Reference, newRef name.Reference) error { l := log.FromContext(ctx) - s.OCI.LoadIndex() + if err := s.OCI.LoadIndex(); err != nil { + return fmt.Errorf("failed to load index: %w", err) + } //TODO: improve string manipulation oldRefContext := oldRef.Context() newRefContext := newRef.Context() oldRepo := oldRefContext.RepositoryStr() newRepo := newRefContext.RepositoryStr() - oldTag := oldRef.(name.Tag).TagStr() - newTag := newRef.(name.Tag).TagStr() - oldRegistry := strings.TrimPrefix(oldRefContext.RegistryStr(), "index.") - newRegistry := strings.TrimPrefix(newRefContext.RegistryStr(), "index.") - // If new registry not set in rewrite, keep old registry instead of defaulting to docker.io - if newRegistry == "docker.io" && oldRegistry != "docker.io" { + + oldTag := oldRef.Identifier() + if tag, ok := oldRef.(name.Tag); ok { + oldTag = tag.TagStr() + } + newTag := newRef.Identifier() + if tag, ok := newRef.(name.Tag); ok { + newTag = tag.TagStr() + } + + // ContainerdImageNameKey stores annotationRef.Name() verbatim, which includes the + // "index.docker.io" prefix for docker.io images. Do not strip "index." here or the + // comparison will never match images stored by writeImage/writeIndex. + oldRegistry := oldRefContext.RegistryStr() + newRegistry := newRefContext.RegistryStr() + // If user omitted a registry in the rewrite string, go-containerregistry defaults to + // index.docker.io. Preserve the original registry when the source is non-docker. + if newRegistry == "index.docker.io" && oldRegistry != "index.docker.io" { newRegistry = oldRegistry } oldTotal := oldRepo + ":" + oldTag @@ -349,7 +375,7 @@ func storeChart(ctx context.Context, s *store.Layout, cfg v1.Chart, opts *flags. return err } - if _, err := s.AddOCI(ctx, chrt, ref.Name()); err != nil { + if _, err := s.AddArtifact(ctx, chrt, ref.Name()); err != nil { return err } if err := s.OCI.SaveIndex(); err != nil { @@ -501,7 +527,9 @@ func storeChart(ctx context.Context, s *store.Layout, cfg v1.Chart, opts *flags. } return fmt.Errorf("failed to store image [%s]: %w", image, err) } - s.OCI.LoadIndex() + if err := s.OCI.LoadIndex(); err != nil { + return err + } if err := s.OCI.SaveIndex(); err != nil { return err } @@ -558,7 +586,10 @@ func storeChart(ctx context.Context, s *store.Layout, cfg v1.Chart, opts *flags. } // if rewrite omits a tag... keep the existing tag - oldTag := ref.(name.Tag).TagStr() + oldTag := ref.Identifier() + if tag, ok := ref.(name.Tag); ok { + oldTag = tag.TagStr() + } if !strings.Contains(rewrite, ":") { rewrite = strings.Join([]string{rewrite, oldTag}, ":") newRef, err = name.ParseReference(rewrite) @@ -568,14 +599,19 @@ func storeChart(ctx context.Context, s *store.Layout, cfg v1.Chart, opts *flags. } // rename chart name in store - s.OCI.LoadIndex() + if err := s.OCI.LoadIndex(); err != nil { + return err + } oldRefContext := ref.Context() newRefContext := newRef.Context() oldRepo := oldRefContext.RepositoryStr() newRepo := newRefContext.RepositoryStr() - newTag := newRef.(name.Tag).TagStr() + newTag := newRef.Identifier() + if tag, ok := newRef.(name.Tag); ok { + newTag = tag.TagStr() + } oldTotal := oldRepo + ":" + oldTag newTotal := newRepo + ":" + newTag diff --git a/cmd/hauler/cli/store/copy.go b/cmd/hauler/cli/store/copy.go index 0b80a98..756b562 100644 --- a/cmd/hauler/cli/store/copy.go +++ b/cmd/hauler/cli/store/copy.go @@ -2,14 +2,22 @@ package store import ( "context" + "encoding/json" "fmt" + "io" + "os" "strings" - "oras.land/oras-go/pkg/content" + "github.com/containerd/containerd/remotes" + "github.com/containerd/errdefs" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "hauler.dev/go/hauler/internal/flags" - "hauler.dev/go/hauler/pkg/cosign" + "hauler.dev/go/hauler/internal/mapper" + "hauler.dev/go/hauler/pkg/consts" + "hauler.dev/go/hauler/pkg/content" "hauler.dev/go/hauler/pkg/log" + "hauler.dev/go/hauler/pkg/retry" "hauler.dev/go/hauler/pkg/store" ) @@ -20,26 +28,248 @@ func CopyCmd(ctx context.Context, o *flags.CopyOpts, s *store.Layout, targetRef return fmt.Errorf("--username/--password have been deprecated, please use 'hauler login'") } + if !s.IndexExists() { + return fmt.Errorf("store index not found: run 'hauler store add/sync/load' first") + } + components := strings.SplitN(targetRef, "://", 2) switch components[0] { case "dir": l.Debugf("identified directory target reference of [%s]", components[1]) - fs := content.NewFile(components[1]) - defer fs.Close() - _, err := s.CopyAll(ctx, fs, nil) + // Create destination directory if it doesn't exist + if err := os.MkdirAll(components[1], 0755); err != nil { + return fmt.Errorf("failed to create destination directory: %w", err) + } + + // For directory targets, extract files and charts (not images) + err := s.Walk(func(reference string, desc ocispec.Descriptor) error { + // Skip cosign sig/att/sbom artifacts — they're registry-only metadata, + // not extractable as files or charts. + kind := desc.Annotations[consts.KindAnnotationName] + switch kind { + case consts.KindAnnotationSigs, consts.KindAnnotationAtts, consts.KindAnnotationSboms: + l.Debugf("skipping cosign artifact [%s] for directory target", reference) + return nil + } + if strings.HasPrefix(kind, consts.KindAnnotationReferrers) { + l.Debugf("skipping OCI referrer [%s] for directory target", reference) + return nil + } + + // Handle different media types + switch desc.MediaType { + case ocispec.MediaTypeImageIndex, consts.DockerManifestListSchema2: + // Multi-platform index - process each child manifest + rc, err := s.Fetch(ctx, desc) + if err != nil { + l.Warnf("failed to fetch index [%s]: %v", reference, err) + return nil + } + + var index ocispec.Index + if err := json.NewDecoder(rc).Decode(&index); err != nil { + if cerr := rc.Close(); cerr != nil { + l.Warnf("failed to close index reader for [%s]: %v", reference, cerr) + } + l.Warnf("failed to decode index for [%s]: %v", reference, err) + return nil + } + + // Close rc immediately after decoding - we're done reading from it + if cerr := rc.Close(); cerr != nil { + l.Warnf("failed to close index reader for [%s]: %v", reference, cerr) + } + + // Process each manifest in the index + for _, manifestDesc := range index.Manifests { + manifestRC, err := s.Fetch(ctx, manifestDesc) + if err != nil { + l.Warnf("failed to fetch child manifest: %v", err) + continue + } + + var m ocispec.Manifest + if err := json.NewDecoder(manifestRC).Decode(&m); err != nil { + manifestRC.Close() + l.Warnf("failed to decode child manifest: %v", err) + continue + } + manifestRC.Close() + + // Skip images - only extract files and charts + if m.Config.MediaType == consts.DockerConfigJSON || + m.Config.MediaType == ocispec.MediaTypeImageConfig { + l.Debugf("skipping image manifest in index [%s]", reference) + continue + } + + // Create mapper and extract + mapperStore, err := mapper.FromManifest(m, components[1]) + if err != nil { + l.Warnf("failed to create mapper for child: %v", err) + continue + } + + // Note: We can't call s.Copy with manifestDesc because it's not in the nameMap + // Instead, we need to manually push through the mapper + if err := extractManifestContent(ctx, s, manifestDesc, m, mapperStore); err != nil { + l.Warnf("failed to extract child: %v", err) + continue + } + + l.Debugf("extracted child manifest from index [%s]", reference) + } + + case ocispec.MediaTypeImageManifest, consts.DockerManifestSchema2: + // Single-platform manifest + rc, err := s.Fetch(ctx, desc) + if err != nil { + l.Warnf("failed to fetch [%s]: %v", reference, err) + return nil + } + + var m ocispec.Manifest + if err := json.NewDecoder(rc).Decode(&m); err != nil { + rc.Close() + l.Warnf("failed to decode manifest for [%s]: %v", reference, err) + return nil + } + + // Skip images - only extract files and charts for directory targets + if m.Config.MediaType == consts.DockerConfigJSON || + m.Config.MediaType == ocispec.MediaTypeImageConfig { + rc.Close() + l.Debugf("skipping image [%s] for directory target", reference) + return nil + } + + // Create a mapper store based on the manifest type + mapperStore, err := mapper.FromManifest(m, components[1]) + if err != nil { + rc.Close() + l.Warnf("failed to create mapper for [%s]: %v", reference, err) + return nil + } + + // Copy/extract the content + _, err = s.Copy(ctx, reference, mapperStore, "") + if err != nil { + rc.Close() + l.Warnf("failed to extract [%s]: %v", reference, err) + return nil + } + rc.Close() + + l.Debugf("extracted [%s] to directory", reference) + + default: + l.Debugf("skipping unsupported media type [%s] for [%s]", desc.MediaType, reference) + } + + return nil + }) if err != nil { return err } case "registry": l.Debugf("identified registry target reference of [%s]", components[1]) - ropts := content.RegistryOptions{ - Insecure: o.Insecure, + registryOpts := content.RegistryOptions{ PlainHTTP: o.PlainHTTP, + Insecure: o.Insecure, } - err := cosign.LoadImages(ctx, s, components[1], o.Only, ropts, ro) + // Pre-build a map from base ref → image manifest digest so that sig/att/sbom + // descriptors (which store the base image ref, not the cosign tag) can be routed + // to the correct destination tag using the cosign tag convention. + refDigest := make(map[string]string) + if err := s.Walk(func(_ string, desc ocispec.Descriptor) error { + kind := desc.Annotations[consts.KindAnnotationName] + if kind == consts.KindAnnotationImage || kind == consts.KindAnnotationIndex { + if baseRef := desc.Annotations[ocispec.AnnotationRefName]; baseRef != "" { + refDigest[baseRef] = desc.Digest.String() + } + } + return nil + }); err != nil { + return err + } + + sigExts := map[string]string{ + consts.KindAnnotationSigs: ".sig", + consts.KindAnnotationAtts: ".att", + consts.KindAnnotationSboms: ".sbom", + } + + var fatalErr error + err := s.Walk(func(reference string, desc ocispec.Descriptor) error { + if fatalErr != nil { + return nil + } + baseRef := desc.Annotations[ocispec.AnnotationRefName] + if baseRef == "" { + return nil + } + if o.Only != "" && !strings.Contains(baseRef, o.Only) { + l.Debugf("skipping [%s] (not matching --only filter)", baseRef) + return nil + } + + // For sig/att/sbom descriptors, derive the cosign tag from the parent + // image's manifest digest rather than using AnnotationRefName directly. + destRef := baseRef + kind := desc.Annotations[consts.KindAnnotationName] + if ext, isSigKind := sigExts[kind]; isSigKind { + if imgDigest, ok := refDigest[baseRef]; ok { + digestTag := strings.ReplaceAll(imgDigest, ":", "-") + repo := baseRef + if colon := strings.LastIndex(baseRef, ":"); colon != -1 { + repo = baseRef[:colon] + } + destRef = repo + ":" + digestTag + ext + } + } else if strings.HasPrefix(kind, consts.KindAnnotationReferrers) { + // OCI 1.1 referrer (cosign v3 new-bundle-format): push by manifest digest so + // the target registry wires it up via the OCI Referrers API (subject field). + // For registries that don't support the Referrers API natively, the manifest + // is still pushed intact; the subject linkage depends on registry support. + repo := baseRef + if colon := strings.LastIndex(baseRef, ":"); colon != -1 { + repo = baseRef[:colon] + } + destRef = repo + "@" + desc.Digest.String() + } + + toRef, err := content.RewriteRefToRegistry(destRef, components[1]) + if err != nil { + l.Warnf("failed to rewrite ref [%s]: %v", baseRef, err) + return nil + } + l.Infof("%s", destRef) + // A fresh target per artifact gives each push its own in-memory status + // tracker. Containerd's tracker keys blobs by digest only (not repo), + // so a shared tracker would mark shared blobs as "already exists" after + // the first image, skipping the per-repository blob link creation that + // Docker Distribution requires for manifest validation. + target := content.NewRegistryTarget(components[1], registryOpts) + var pushed ocispec.Descriptor + if err := retry.Operation(ctx, o.StoreRootOpts, ro, func() error { + var copyErr error + pushed, copyErr = s.Copy(ctx, reference, target, toRef) + return copyErr + }); err != nil { + if !ro.IgnoreErrors { + fatalErr = err + } + return nil + } + l.Infof("%s: digest: %s size: %d", toRef, pushed.Digest, pushed.Size) + return nil + }) + if fatalErr != nil { + return fatalErr + } if err != nil { return err } @@ -51,3 +281,73 @@ func CopyCmd(ctx context.Context, o *flags.CopyOpts, s *store.Layout, targetRef l.Infof("copied artifacts to [%s]", components[1]) return nil } + +// extractManifestContent extracts a manifest's layers through a mapper target +// This is used for child manifests in indexes that aren't in the store's nameMap +func extractManifestContent(ctx context.Context, s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, target content.Target) error { + // Get a pusher from the target + pusher, err := target.Pusher(ctx, "") + if err != nil { + return fmt.Errorf("failed to get pusher: %w", err) + } + + // Copy config blob + if err := copyBlobDescriptor(ctx, s, m.Config, pusher); err != nil { + return fmt.Errorf("failed to copy config: %w", err) + } + + // Copy each layer blob + for _, layer := range m.Layers { + if err := copyBlobDescriptor(ctx, s, layer, pusher); err != nil { + return fmt.Errorf("failed to copy layer: %w", err) + } + } + + // Copy the manifest itself + if err := copyBlobDescriptor(ctx, s, desc, pusher); err != nil { + return fmt.Errorf("failed to copy manifest: %w", err) + } + + return nil +} + +// copyBlobDescriptor copies a single descriptor blob from the store to a pusher +func copyBlobDescriptor(ctx context.Context, s *store.Layout, desc ocispec.Descriptor, pusher remotes.Pusher) (err error) { + // Fetch the content from the store + rc, err := s.OCI.Fetch(ctx, desc) + if err != nil { + return fmt.Errorf("failed to fetch blob: %w", err) + } + defer func() { + if closeErr := rc.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("failed to close reader: %w", closeErr) + } + }() + + // Get a writer from the pusher + writer, err := pusher.Push(ctx, desc) + if err != nil { + if errdefs.IsAlreadyExists(err) { + return nil // content already present on remote + } + return fmt.Errorf("failed to push: %w", err) + } + defer func() { + if closeErr := writer.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("failed to close writer: %w", closeErr) + } + }() + + // Copy the content + n, err := io.Copy(writer, rc) + if err != nil { + return fmt.Errorf("failed to copy content: %w", err) + } + + // Commit the written content + if err := writer.Commit(ctx, n, desc.Digest); err != nil { + return fmt.Errorf("failed to commit: %w", err) + } + + return nil +} diff --git a/cmd/hauler/cli/store/info.go b/cmd/hauler/cli/store/info.go index faa2e9d..086aa4c 100644 --- a/cmd/hauler/cli/store/info.go +++ b/cmd/hauler/cli/store/info.go @@ -300,13 +300,15 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat ctype = "image" } - switch desc.Annotations["kind"] { - case "dev.cosignproject.cosign/sigs": + switch { + case desc.Annotations[consts.KindAnnotationName] == consts.KindAnnotationSigs: ctype = "sigs" - case "dev.cosignproject.cosign/atts": + case desc.Annotations[consts.KindAnnotationName] == consts.KindAnnotationAtts: ctype = "atts" - case "dev.cosignproject.cosign/sboms": + case desc.Annotations[consts.KindAnnotationName] == consts.KindAnnotationSboms: ctype = "sbom" + case strings.HasPrefix(desc.Annotations[consts.KindAnnotationName], consts.KindAnnotationReferrers): + ctype = "referrer" } refName := desc.Annotations["io.containerd.image.name"] diff --git a/cmd/hauler/cli/store/save.go b/cmd/hauler/cli/store/save.go index 68150f8..0feb014 100644 --- a/cmd/hauler/cli/store/save.go +++ b/cmd/hauler/cli/store/save.go @@ -116,59 +116,68 @@ func writeExportsManifest(ctx context.Context, dir string, platformStr string) e l.Debugf("descriptor [%s] <<< SKIPPING ARTIFACT [%q]", desc.Digest.String(), desc.ArtifactType) continue } - if desc.Annotations != nil { - // we only care about images that cosign has added to the layout index - if kind, hasKind := desc.Annotations[consts.KindAnnotationName]; hasKind { - if refName, hasRefName := desc.Annotations["io.containerd.image.name"]; hasRefName { - // branch on image (aka image manifest) or image index - switch kind { - case consts.KindAnnotationImage: - if err := x.record(ctx, idx, desc, refName); err != nil { - return err - } - case consts.KindAnnotationIndex: - l.Debugf("index [%s]: digest=[%s]... type=[%s]... size=[%d]", refName, desc.Digest.String(), desc.MediaType, desc.Size) + // The kind annotation is the only reliable way to distinguish container images from + // cosign signatures/attestations/SBOMs: those are stored as standard Docker/OCI + // manifests (same media type as real images) so media type alone is insufficient. + kind := desc.Annotations[consts.KindAnnotationName] + if kind != consts.KindAnnotationImage && kind != consts.KindAnnotationIndex { + l.Debugf("descriptor [%s] <<< SKIPPING KIND [%q]", desc.Digest.String(), kind) + continue + } - // when no platform is inputted... warn the user of potential mismatch on import for docker - // required for docker to be able to interpret and load the image correctly - if platform.String() == "" { - l.Warnf("compatibility warning... docker... specify platform to prevent potential mismatch on import of index [%s]", refName) - } + refName, hasRefName := desc.Annotations[consts.ContainerdImageNameKey] + if !hasRefName { + l.Debugf("descriptor [%s] <<< SKIPPING (no containerd image name)", desc.Digest.String()) + continue + } - iix, err := idx.ImageIndex(desc.Digest) - if err != nil { - return err - } - ixm, err := iix.IndexManifest() - if err != nil { - return err - } - for _, ixd := range ixm.Manifests { - if ixd.MediaType.IsImage() { - if platform.String() != "" { - if ixd.Platform.Architecture != platform.Architecture || ixd.Platform.OS != platform.OS { - l.Debugf("index [%s]: digest=[%s], platform=[%s/%s]: does not match the supplied platform... skipping...", refName, desc.Digest.String(), ixd.Platform.OS, ixd.Platform.Architecture) - continue - } - } + // Use the descriptor's actual media type to discriminate single-image manifests + // from multi-arch indexes, rather than relying on the kind string for this. + switch { + case desc.MediaType.IsImage(): + if err := x.record(ctx, idx, desc, refName); err != nil { + return err + } + case desc.MediaType.IsIndex(): + l.Debugf("index [%s]: digest=[%s]... type=[%s]... size=[%d]", refName, desc.Digest.String(), desc.MediaType, desc.Size) - // skip any platforms of 'unknown/unknown'... docker hates - // required for docker to be able to interpret and load the image correctly - if ixd.Platform.Architecture == "unknown" && ixd.Platform.OS == "unknown" { - l.Debugf("index [%s]: digest=[%s], platform=[%s/%s]: matches unknown platform... skipping...", refName, desc.Digest.String(), ixd.Platform.OS, ixd.Platform.Architecture) - continue - } + // when no platform is inputted... warn the user of potential mismatch on import for docker + // required for docker to be able to interpret and load the image correctly + if platform.String() == "" { + l.Warnf("compatibility warning... docker... specify platform to prevent potential mismatch on import of index [%s]", refName) + } - if err := x.record(ctx, iix, ixd, refName); err != nil { - return err - } - } + iix, err := idx.ImageIndex(desc.Digest) + if err != nil { + return err + } + ixm, err := iix.IndexManifest() + if err != nil { + return err + } + for _, ixd := range ixm.Manifests { + if ixd.MediaType.IsImage() { + if platform.String() != "" { + if ixd.Platform.Architecture != platform.Architecture || ixd.Platform.OS != platform.OS { + l.Debugf("index [%s]: digest=[%s], platform=[%s/%s]: does not match the supplied platform... skipping...", refName, desc.Digest.String(), ixd.Platform.OS, ixd.Platform.Architecture) + continue } - default: - l.Debugf("descriptor [%s] <<< SKIPPING KIND [%q]", desc.Digest.String(), kind) + } + + // skip any platforms of 'unknown/unknown'... docker hates + // required for docker to be able to interpret and load the image correctly + if ixd.Platform.Architecture == "unknown" && ixd.Platform.OS == "unknown" { + l.Debugf("index [%s]: digest=[%s], platform=[%s/%s]: matches unknown platform... skipping...", refName, desc.Digest.String(), ixd.Platform.OS, ixd.Platform.Architecture) + continue + } + + if err := x.record(ctx, iix, ixd, refName); err != nil { + return err } } } + default: + l.Debugf("descriptor [%s] <<< SKIPPING media type [%q]", desc.Digest.String(), desc.MediaType) } } @@ -199,6 +208,17 @@ func (x *exports) record(ctx context.Context, index libv1.ImageIndex, desc libv1 return err } + // Verify this is a real container image by inspecting its manifest config media type. + // Non-image OCI artifacts (Helm charts, files, cosign sigs) use distinct config types. + manifest, err := image.Manifest() + if err != nil { + return err + } + if manifest.Config.MediaType != types.DockerConfigJSON && manifest.Config.MediaType != types.OCIConfigJSON { + l.Debugf("descriptor [%s] <<< SKIPPING NON-IMAGE config media type [%q]", desc.Digest.String(), manifest.Config.MediaType) + return nil + } + config, err := image.ConfigName() if err != nil { return err diff --git a/cmd/hauler/cli/store/serve.go b/cmd/hauler/cli/store/serve.go index 806632b..35401f7 100644 --- a/cmd/hauler/cli/store/serve.go +++ b/cmd/hauler/cli/store/serve.go @@ -51,6 +51,7 @@ func loadConfig(filename string) (*configuration.Configuration, error) { if err != nil { return nil, err } + defer f.Close() return configuration.Parse(f) } @@ -96,7 +97,7 @@ func ServeRegistryCmd(ctx context.Context, o *flags.ServeRegistryOpts, s *store. return err } - opts := &flags.CopyOpts{} + opts := &flags.CopyOpts{StoreRootOpts: rso, PlainHTTP: true} if err := CopyCmd(ctx, opts, s, "registry://"+tr.Registry(), ro); err != nil { return err } @@ -143,7 +144,7 @@ func ServeFilesCmd(ctx context.Context, o *flags.ServeFilesOpts, s *store.Layout return err } - opts := &flags.CopyOpts{} + opts := &flags.CopyOpts{StoreRootOpts: &flags.StoreRootOpts{}} if err := CopyCmd(ctx, opts, s, "dir://"+o.RootDir, ro); err != nil { return err } diff --git a/cmd/hauler/cli/store/sync.go b/cmd/hauler/cli/store/sync.go index 7e81967..9e0ab69 100644 --- a/cmd/hauler/cli/store/sync.go +++ b/cmd/hauler/cli/store/sync.go @@ -77,6 +77,7 @@ func SyncCmd(ctx context.Context, o *flags.SyncOpts, s *store.Layout, rso *flags if err != nil { return err } + defer fi.Close() err = processContent(ctx, fi, o, s, rso, ro) if err != nil { return err @@ -263,7 +264,7 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor } l.Debugf("transparency log for verification [%b]", tlog) - if err := cosign.VerifySignature(ctx, s, key, tlog, i.Name, rso, ro); err != nil { + if err := cosign.VerifySignature(ctx, key, tlog, i.Name, rso, ro); err != nil { l.Errorf("signature verification failed for image [%s]... skipping...\n%v", i.Name, err) continue } @@ -314,17 +315,10 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor } l.Debugf("certGithubWorkflowRepository for image [%s]", certGithubWorkflowRepository) - tlog := o.Tlog - if !o.Tlog && a[consts.ImageAnnotationTlog] == "true" { - tlog = true - } - if i.Tlog { - tlog = i.Tlog - } - l.Debugf("transparency log for verification [%b]", tlog) - - if err := cosign.VerifyKeylessSignature(ctx, s, certIdentity, certIdentityRegexp, certOidcIssuer, certOidcIssuerRegexp, certGithubWorkflowRepository, tlog, i.Name, rso, ro); err != nil { - l.Errorf("keyless signature verification failed for image [%s]... skipping...\n%v", i.Name, err) + // Keyless (Fulcio) certs expire after ~10 min; tlog is always + // required to prove the cert was valid at signing time. + if err := cosign.VerifyKeylessSignature(ctx, certIdentity, certIdentityRegexp, certOidcIssuer, certOidcIssuerRegexp, certGithubWorkflowRepository, i.Name, rso, ro); err != nil { + l.Errorf("signature verification failed for image [%s]... skipping...\n%v", i.Name, err) continue } l.Infof("keyless signature verified for image [%s]", i.Name) @@ -404,7 +398,7 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor } l.Debugf("transparency log for verification [%b]", tlog) - if err := cosign.VerifySignature(ctx, s, key, tlog, i.Name, rso, ro); err != nil { + if err := cosign.VerifySignature(ctx, key, tlog, i.Name, rso, ro); err != nil { l.Errorf("signature verification failed for image [%s]... skipping...\n%v", i.Name, err) continue } @@ -455,17 +449,10 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor } l.Debugf("certGithubWorkflowRepository for image [%s]", certGithubWorkflowRepository) - tlog := o.Tlog - if !o.Tlog && a[consts.ImageAnnotationTlog] == "true" { - tlog = true - } - if i.Tlog { - tlog = i.Tlog - } - l.Debugf("transparency log for verification [%b]", tlog) - - if err := cosign.VerifyKeylessSignature(ctx, s, certIdentity, certIdentityRegexp, certOidcIssuer, certOidcIssuerRegexp, certGithubWorkflowRepository, tlog, i.Name, rso, ro); err != nil { - l.Errorf("keyless signature verification failed for image [%s]... skipping...\n%v", i.Name, err) + // Keyless (Fulcio) certs expire after ~10 min; tlog is always + // required to prove the cert was valid at signing time. + if err := cosign.VerifyKeylessSignature(ctx, certIdentity, certIdentityRegexp, certOidcIssuer, certOidcIssuerRegexp, certGithubWorkflowRepository, i.Name, rso, ro); err != nil { + l.Errorf("signature verification failed for image [%s]... skipping...\n%v", i.Name, err) continue } l.Infof("keyless signature verified for image [%s]", i.Name) @@ -578,7 +565,7 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor if err != nil { return err } - if _, err := s.AddOCICollection(ctx, tc); err != nil { + if _, err := s.AddArtifactCollection(ctx, tc); err != nil { return err } } @@ -596,7 +583,7 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor if err != nil { return err } - if _, err := s.AddOCICollection(ctx, tc); err != nil { + if _, err := s.AddArtifactCollection(ctx, tc); err != nil { return err } } @@ -626,7 +613,7 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor if err != nil { return fmt.Errorf("convert ImageTxt %s: %v", v1Cfg.Name, err) } - if _, err := s.AddOCICollection(ctx, it); err != nil { + if _, err := s.AddArtifactCollection(ctx, it); err != nil { return fmt.Errorf("add ImageTxt %s to store: %v", v1Cfg.Name, err) } } @@ -644,7 +631,7 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor if err != nil { return fmt.Errorf("convert ImageTxt %s: %v", cfg.Name, err) } - if _, err := s.AddOCICollection(ctx, it); err != nil { + if _, err := s.AddArtifactCollection(ctx, it); err != nil { return fmt.Errorf("add ImageTxt %s to store: %v", cfg.Name, err) } } diff --git a/go.mod b/go.mod index a9da2ac..6dcbb93 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,12 @@ module hauler.dev/go/hauler go 1.25.5 -replace github.com/sigstore/cosign/v3 => github.com/hauler-dev/cosign/v3 v3.0.5-0.20260212234448-00b85d677dfc - replace github.com/distribution/distribution/v3 => github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 -replace github.com/docker/cli => github.com/docker/cli v28.5.1+incompatible // needed to keep oras v1.2.7 working, which depends on docker/cli v28.5.1+incompatible - require ( github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/containerd/containerd v1.7.29 + github.com/containerd/errdefs v1.0.0 github.com/distribution/distribution/v3 v3.0.0 github.com/google/go-containerregistry v0.20.7 github.com/gorilla/handlers v1.5.2 @@ -22,24 +19,23 @@ require ( github.com/opencontainers/image-spec v1.1.1 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 - github.com/sigstore/cosign/v3 v3.0.2 + github.com/sigstore/cosign/v3 v3.0.5 github.com/sirupsen/logrus v1.9.4 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 golang.org/x/sync v0.19.0 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.19.0 - k8s.io/apimachinery v0.35.0 - k8s.io/client-go v0.35.0 - oras.land/oras-go v1.2.7 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 ) require ( - cloud.google.com/go/auth v0.18.0 // indirect + cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084 // indirect - cuelang.org/go v0.15.3 // indirect + cuelang.org/go v0.15.4 // indirect dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/provider v0.14.0 // indirect @@ -77,21 +73,21 @@ require ( github.com/aliyun/credentials-go v1.3.2 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.5 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/service/ecr v1.51.2 // indirect github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/aws/smithy-go v1.24.0 // indirect github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -104,7 +100,7 @@ require ( github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd // indirect github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b // indirect github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect - github.com/buildkite/agent/v3 v3.115.2 // indirect + github.com/buildkite/agent/v3 v3.115.4 // indirect github.com/buildkite/go-pipeline v0.16.0 // indirect github.com/buildkite/interpolate v0.1.5 // indirect github.com/buildkite/roko v1.4.0 // indirect @@ -112,13 +108,11 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect - github.com/chzyer/readline v1.5.1 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/clipperhouse/displaywidth v0.6.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect - github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v1.0.0-rc.2 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect @@ -130,12 +124,9 @@ require ( github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.0.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.4 // indirect - github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect @@ -193,12 +184,12 @@ require ( github.com/google/go-querystring v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect - github.com/googleapis/gax-go/v2 v2.16.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -227,11 +218,10 @@ require ( github.com/letsencrypt/boulder v0.20251110.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/miekg/pkcs11 v1.1.2 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -253,7 +243,7 @@ require ( github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.1.3 // indirect - github.com/open-policy-agent/opa v1.12.1 // indirect + github.com/open-policy-agent/opa v1.12.3 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect @@ -263,7 +253,7 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.17.0 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -279,7 +269,7 @@ require ( github.com/sigstore/fulcio v1.8.5 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect github.com/sigstore/rekor v1.5.0 // indirect - github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect + github.com/sigstore/rekor-tiles/v2 v2.2.0 // indirect github.com/sigstore/sigstore v1.10.4 // indirect github.com/sigstore/sigstore-go v1.1.4 // indirect github.com/sigstore/timestamp-authority/v2 v2.0.4 // indirect @@ -303,7 +293,6 @@ require ( github.com/valyala/fastjson v1.6.4 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/vektah/gqlparser/v2 v2.5.31 // indirect - github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -312,7 +301,7 @@ require ( github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 // indirect github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 // indirect github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f // indirect - gitlab.com/gitlab-org/api/client-go v1.11.0 // indirect + gitlab.com/gitlab-org/api/client-go v1.25.0 // indirect go.mongodb.org/mongo-driver v1.17.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect @@ -325,24 +314,24 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/api v0.260.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/api v0.267.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/api v0.35.0 // indirect + k8s.io/api v0.35.1 // indirect k8s.io/apiextensions-apiserver v0.34.0 // indirect k8s.io/apiserver v0.34.0 // indirect k8s.io/cli-runtime v0.34.0 // indirect diff --git a/go.sum b/go.sum index 0d845ff..d32ef3b 100644 --- a/go.sum +++ b/go.sum @@ -9,10 +9,10 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= -cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= -cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0= -cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= +cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -22,23 +22,24 @@ cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCB cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= -cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= -cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= +cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084 h1:4k1yAtPvZJZQTu8DRY8muBo0LHv6TqtrE0AO5n6IPYs= cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084/go.mod h1:4WWeZNxUO1vRoZWAHIG0KZOd6dA25ypyWuwD3ti0Tdc= -cuelang.org/go v0.15.3 h1:JKR/lZVwuIGlLTGIaJ0jONz9+CK3UDx06sQ6DDxNkaE= -cuelang.org/go v0.15.3/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE= +cuelang.org/go v0.15.4 h1:lrkTDhqy8dveHgX1ZLQ6WmgbhD8+rXa0fD25hxEKYhw= +cuelang.org/go v0.15.4/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= @@ -47,8 +48,8 @@ github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/provider v0.14.0 github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/provider v0.14.0/go.mod h1:tlqp9mUGbsP+0z3Q+c0Q5MgSdq/OMwQhm5bffR3Q3ss= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= @@ -168,18 +169,18 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= -github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= -github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8= -github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4= -github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/service/ecr v1.51.2 h1:aq2N/9UkbEyljIQ7OFcudEgUsJzO8MYucmfsM/k/dmc= @@ -188,18 +189,18 @@ github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.2 h1:9fe6w8bydUwNAhFVmjo+SR github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.2/go.mod h1:x7gU4CAyAz4BsM9hlRkhHiYw2GIr1QCmN45uwQw9l/E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= -github.com/aws/aws-sdk-go-v2/service/kms v1.49.4 h1:2gom8MohxN0SnhHZBYAC4S8jHG+ENEnXjyJ5xKe3vLc= -github.com/aws/aws-sdk-go-v2/service/kms v1.49.4/go.mod h1:HO31s0qt0lso/ADvZQyzKs8js/ku0fMHsfyXW8OPVYc= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0 h1:GOPttfOAf5qAgx7r6b+zCWZrvCsfKffkL4H6mSYx1kA= @@ -228,8 +229,8 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/buildkite/agent/v3 v3.115.2 h1:26A/dEabfzjorS3Wh/low+yOBM/u8QaT59BYWu0M92w= -github.com/buildkite/agent/v3 v3.115.2/go.mod h1:a3t090/PPxAIIPCjlXF5fhfRvG0E9huFsnMX7B76iIQ= +github.com/buildkite/agent/v3 v3.115.4 h1:oxuLAjwHADBlTZuTrTb0JPt0FBcbGo55ZqDHPJ0jn+E= +github.com/buildkite/agent/v3 v3.115.4/go.mod h1:LKY99ujcnFwX8ihEXuMLuPIy3SPL2unKWGJ/DRLICr0= github.com/buildkite/go-pipeline v0.16.0 h1:wEgWUMRAgSg1ZnWOoA3AovtYYdTvN0dLY1zwUWmPP+4= github.com/buildkite/go-pipeline v0.16.0/go.mod h1:VE37qY3X5pmAKKUMoDZvPsHOQuyakB9cmXj9Qn6QasA= github.com/buildkite/interpolate v0.1.5 h1:v2Ji3voik69UZlbfoqzx+qfcsOKLA61nHdU79VV+tPU= @@ -251,14 +252,8 @@ github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHe github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= @@ -325,20 +320,14 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= -github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= +github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI= github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= @@ -550,12 +539,12 @@ github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81z github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU= -github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= -github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= @@ -570,8 +559,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5 h1:jP1RStw811EvUDzsUQ9oESqw2e4RqCjSAD9qIL8eMns= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5/go.mod h1:WXNBZ64q3+ZUemCMXD9kYnr56H7CgZxDBHCVwstfl3s= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -601,8 +590,6 @@ github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= -github.com/hauler-dev/cosign/v3 v3.0.5-0.20260212234448-00b85d677dfc h1:heoWkq5ahyHmVyBVtLLPH9JTNVoUr1gBW91yFopk5yQ= -github.com/hauler-dev/cosign/v3 v3.0.5-0.20260212234448-00b85d677dfc/go.mod h1:DJY5LPzHiI6bWpG/Q/NQUTfeASjkN8TDAUx1Nnt3I0I= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -688,8 +675,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= -github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -708,8 +693,8 @@ github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPp github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= -github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= +github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= @@ -790,8 +775,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/open-policy-agent/opa v1.12.1 h1:MWfmXuXB119O7rSOJ5GdKAaW15yBirjnLkFRBGy0EX0= -github.com/open-policy-agent/opa v1.12.1/go.mod h1:RnDgm04GA1RjEXJvrsG9uNT/+FyBNmozcPvA2qz60M4= +github.com/open-policy-agent/opa v1.12.3 h1:qe3m/w52baKC/HJtippw+hYBUKCzuBCPjB+D5P9knfc= +github.com/open-policy-agent/opa v1.12.3/go.mod h1:RnDgm04GA1RjEXJvrsG9uNT/+FyBNmozcPvA2qz60M4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -834,8 +819,8 @@ github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxza github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= -github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 h1:s1LvMaU6mVwoFtbxv/rCZKE7/fwDmDY684FfUe4c1Io= github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= @@ -871,26 +856,28 @@ github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sigstore/cosign/v3 v3.0.5 h1:c1zPqjU+H4wmirgysC+AkWMg7a7fykyOYF/m+F1150I= +github.com/sigstore/cosign/v3 v3.0.5/go.mod h1:ble1vMvJagCFyTIDkibCq6MIHiWDw00JNYl0f9rB4T4= github.com/sigstore/fulcio v1.8.5 h1:HYTD1/L5wlBp8JxsWxUf8hmfaNBBF/x3r3p5l6tZwbA= github.com/sigstore/fulcio v1.8.5/go.mod h1:tSLYK3JsKvJpDW1BsIsVHZgHj+f8TjXARzqIUWSsSPQ= github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= github.com/sigstore/rekor v1.5.0 h1:rL7SghHd5HLCtsCrxw0yQg+NczGvM75EjSPPWuGjaiQ= github.com/sigstore/rekor v1.5.0/go.mod h1:D7JoVCUkxwQOpPDNYeu+CE8zeBC18Y5uDo6tF8s2rcQ= -github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= -github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/rekor-tiles/v2 v2.2.0 h1:QwJNwxT+k5A3id+Hrg+8vYcNsTaB0Sj51xjfW2rKyAs= +github.com/sigstore/rekor-tiles/v2 v2.2.0/go.mod h1:/WNRYctHKdxcjgXydYwO5OclW72Zqh6fNHSyGE8zQOE= github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.3 h1:D/FRl5J9UYAJPGZRAJbP0dH78pfwWnKsyCSBwFBU8CI= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.3/go.mod h1:2GIWuNvTRMvrzd0Nl8RNqxrt9H7X0OBStwOSzGYRjYw= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.3 h1:k5VMLf/ms7hh6MLgVoorM0K+hSMwZLXoywlxh4CXqP8= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.3/go.mod h1:S1Bp3dmP7jYlXcGLAxG81wRbE01NIZING8ZIy0dJlAI= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.3 h1:AVWs0E6rVZMoDTE0Iyezrpo1J6RlI5B4QZhAC4FLE30= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.3/go.mod h1:nxQYF0D6u7mVtiP1azj1YVDIrtz7S0RYCVTqUG8IcCk= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.3 h1:lJSdaC/aOlFHlvqmmV696n1HdXLMLEKGwpNZMV0sKts= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.3/go.mod h1:b2rV9qPbt/jv/Yy75AIOZThP8j+pe1ZdLEjOwmjPdoA= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4 h1:VZ+L6SKVWbLPHznIF0tBuO7qKMFdJiJMVwFKu9DlY5o= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4/go.mod h1:Rstj47WpJym25il8j4jTL0BfikzP/9AhVD+DsBcYzZc= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4 h1:G7yOv8bxk3zIEEZyVCixPxtePIAm+t3ZWSaKRPzVw+o= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4/go.mod h1:hxJelB/bRItMYOzi6qD9xEKjse2QZcikh4TbysfdDHc= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4 h1:Qxt6dE4IwhJ6gIXmg2q4S/SeqEDSZ29nmfsv7Zb6LL4= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4/go.mod h1:hJVeNOwarqfyALjOwsf0OR8YA/A96NABucEaQumPr30= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4 h1:KVavYMPfSf5NryOl6VrZ9nRG3fXOOJOPp7Czk/YCPkM= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4/go.mod h1:J7CA1AaBkyK8dYq6EdQANhj+8oEcsA7PrIp088qgPiY= github.com/sigstore/timestamp-authority/v2 v2.0.4 h1:65IBa4LUeFWDQu9hiTt5lBpi/F5jonJWZtH6VLn4InU= github.com/sigstore/timestamp-authority/v2 v2.0.4/go.mod h1:EXJLiMDBqRPlzC02hPiFSiYTCqSuUpU68a4vr0DFePM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -975,8 +962,6 @@ github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CP github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= -github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 h1:+dBg5k7nuTE38VVdoroRsT0Z88fmvdYrI2EjzJst35I= -github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1/go.mod h1:nmuySobZb4kFgFy6BptpXp/BBw+xFSyvVPP6auoJB4k= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= @@ -1011,8 +996,8 @@ github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1 github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= -gitlab.com/gitlab-org/api/client-go v1.11.0 h1:L+qzw4kiCf3jKdKHQAwiqYKITvzBrW/tl8ampxNLlv0= -gitlab.com/gitlab-org/api/client-go v1.11.0/go.mod h1:adtVJ4zSTEJ2fP5Pb1zF4Ox1OKFg0MH43yxpb0T0248= +gitlab.com/gitlab-org/api/client-go v1.25.0 h1:9YVk2o1CjZWKh2/KGOsNbOReBSxFIdBv6LrdOnBfEQY= +gitlab.com/gitlab-org/api/client-go v1.25.0/go.mod h1:r060AandE8Md/L5oKdUVjljL8YQprOAxKzUnpqWqP3A= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1045,8 +1030,8 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.step.sm/crypto v0.75.0 h1:UAHYD6q6ggYyzLlIKHv1MCUVjZIesXRZpGTlRC/HSHw= -go.step.sm/crypto v0.75.0/go.mod h1:wwQ57+ajmDype9mrI/2hRyrvJd7yja5xVgWYqpUN3PE= +go.step.sm/crypto v0.76.0 h1:K23BSaeoiY7Y5dvvijTeYC9EduDBetNwQYMBwMhi1aA= +go.step.sm/crypto v0.76.0/go.mod h1:PXYJdKkK8s+GHLwLguFaLxHNAFsFL3tL1vSBrYfey5k= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -1074,8 +1059,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1084,8 +1069,6 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= -golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1106,8 +1089,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1140,15 +1123,15 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1166,7 +1149,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1191,7 +1173,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1211,8 +1192,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1224,8 +1205,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -1278,8 +1259,8 @@ google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.260.0 h1:XbNi5E6bOVEj/uLXQRlt6TKuEzMD7zvW/6tNwltE4P4= -google.golang.org/api v0.260.0/go.mod h1:Shj1j0Phr/9sloYrKomICzdYgsSDImpTxME8rGLaZ/o= +google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= +google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1298,12 +1279,12 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= -google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= -google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= -google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1361,18 +1342,18 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= -k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= -k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= -k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= k8s.io/cli-runtime v0.34.0 h1:N2/rUlJg6TMEBgtQ3SDRJwa8XyKUizwjlOknT1mB2Cw= k8s.io/cli-runtime v0.34.0/go.mod h1:t/skRecS73Piv+J+FmWIQA2N2/rDjdYSQzEE67LUUs8= -k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= -k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= @@ -1383,8 +1364,6 @@ k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs= k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go v1.2.7 h1:KF9rBAtKYMGB5gjgHV5XquUfYDER3ecQBEXjdI7KZWI= -oras.land/oras-go v1.2.7/go.mod h1:WVpIPbm82xjWT/GJU3TqZ0y9Ctj3DGco4wLYvGdOVvA= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/flags/add.go b/internal/flags/add.go index 92089a9..e2bb725 100644 --- a/internal/flags/add.go +++ b/internal/flags/add.go @@ -27,7 +27,7 @@ func (o *AddImageOpts) AddFlags(cmd *cobra.Command) { f.StringVar(&o.CertOidcIssuer, "certificate-oidc-issuer", "", "(Optional) Cosign option to validate oidc issuer") f.StringVar(&o.CertOidcIssuerRegexp, "certificate-oidc-issuer-regexp", "", "(Optional) Cosign option to validate oidc issuer with regex") f.StringVar(&o.CertGithubWorkflowRepository, "certificate-github-workflow-repository", "", "(Optional) Cosign certificate-github-workflow-repository option") - f.BoolVar(&o.Tlog, "use-tlog-verify", false, "(Optional) Allow transparency log verification (defaults to false)") + f.BoolVar(&o.Tlog, "use-tlog-verify", false, "(Optional) Enable transparency log verification for key-based signature verification (keyless/OIDC verification always uses the tlog)") f.StringVarP(&o.Platform, "platform", "p", "", "(Optional) Specify the platform of the image... i.e. linux/amd64 (defaults to all)") f.StringVar(&o.Rewrite, "rewrite", "", "(EXPERIMENTAL & Optional) Rewrite artifact path to specified string") } diff --git a/internal/flags/info.go b/internal/flags/info.go index ead8996..bc3ac42 100644 --- a/internal/flags/info.go +++ b/internal/flags/info.go @@ -16,7 +16,7 @@ func (o *InfoOpts) AddFlags(cmd *cobra.Command) { f := cmd.Flags() f.StringVarP(&o.OutputFormat, "output", "o", "table", "(Optional) Specify the output format (table | json)") - f.StringVar(&o.TypeFilter, "type", "all", "(Optional) Filter on content type (image | chart | file | sigs | atts | sbom)") + f.StringVar(&o.TypeFilter, "type", "all", "(Optional) Filter on content type (image | chart | file | sigs | atts | sbom | referrer)") f.BoolVar(&o.ListRepos, "list-repos", false, "(Optional) List all repository names") f.BoolVar(&o.ShowDigests, "digests", false, "(Optional) Show digests of each artifact in the output table") } diff --git a/internal/mapper/filestore.go b/internal/mapper/filestore.go index 984266e..f86fa55 100644 --- a/internal/mapper/filestore.go +++ b/internal/mapper/filestore.go @@ -2,6 +2,7 @@ package mapper import ( "context" + "fmt" "io" "os" "path/filepath" @@ -11,18 +12,21 @@ import ( "github.com/containerd/containerd/remotes" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" - "oras.land/oras-go/pkg/content" + "hauler.dev/go/hauler/pkg/content" ) // NewMapperFileStore creates a new file store that uses mapper functions for each detected descriptor. // -// This extends content.File, and differs in that it allows much more functionality into how each descriptor is written. -func NewMapperFileStore(root string, mapper map[string]Fn) *store { - fs := content.NewFile(root) - return &store{ - File: fs, - mapper: mapper, +// This extends content.OCI, and differs in that it allows much more functionality into how each descriptor is written. +func NewMapperFileStore(root string, mapper map[string]Fn) (*store, error) { + fs, err := content.NewOCI(root) + if err != nil { + return nil, err } + return &store{ + OCI: fs, + mapper: mapper, + }, nil } func (s *store) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) { @@ -35,7 +39,7 @@ func (s *store) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) hash = parts[1] } return &pusher{ - store: s.File, + store: s.OCI, tag: tag, ref: hash, mapper: s.mapper, @@ -43,43 +47,53 @@ func (s *store) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) } type store struct { - *content.File + *content.OCI mapper map[string]Fn } func (s *pusher) Push(ctx context.Context, desc ocispec.Descriptor) (ccontent.Writer, error) { - // TODO: This is suuuuuper ugly... redo this when oras v2 is out + // For manifests and indexes (which have AnnotationRefName), discard them + // They're metadata and don't need to be extracted if _, ok := content.ResolveName(desc); ok { - p, err := s.store.Pusher(ctx, s.ref) - if err != nil { - return nil, err - } - return p.Push(ctx, desc) + // Discard manifests/indexes, they're just metadata + return content.NewIoContentWriter(&nopCloser{io.Discard}, content.WithOutputHash(desc.Digest.String())), nil } - // If no custom mapper found, fall back to content.File mapper - if _, ok := s.mapper[desc.MediaType]; !ok { - return content.NewIoContentWriter(io.Discard, content.WithOutputHash(desc.Digest)), nil + // Check if this descriptor has a mapper for its media type + mapperFn, hasMapper := s.mapper[desc.MediaType] + if !hasMapper { + // No mapper for this media type, discard it (config blobs, etc.) + return content.NewIoContentWriter(&nopCloser{io.Discard}, content.WithOutputHash(desc.Digest.String())), nil } - filename, err := s.mapper[desc.MediaType](desc) + // Get the filename from the mapper function + filename, err := mapperFn(desc) if err != nil { return nil, err } - fullFileName := filepath.Join(s.store.ResolvePath(""), filename) - // TODO: Don't rewrite everytime, we can check the digest + // Get the destination directory and create the full path + destDir := s.store.ResolvePath("") + fullFileName := filepath.Join(destDir, filename) + + // Create the file f, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { - return nil, errors.Wrap(err, "pushing file") + return nil, errors.Wrap(err, fmt.Sprintf("creating file %s", fullFileName)) } - w := content.NewIoContentWriter(f, content.WithInputHash(desc.Digest), content.WithOutputHash(desc.Digest)) + w := content.NewIoContentWriter(f, content.WithOutputHash(desc.Digest.String())) return w, nil } +type nopCloser struct { + io.Writer +} + +func (*nopCloser) Close() error { return nil } + type pusher struct { - store *content.File + store *content.OCI tag string ref string mapper map[string]Fn diff --git a/internal/mapper/mappers.go b/internal/mapper/mappers.go index 154daef..ad41bb9 100644 --- a/internal/mapper/mappers.go +++ b/internal/mapper/mappers.go @@ -4,32 +4,41 @@ import ( "fmt" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "oras.land/oras-go/pkg/target" "hauler.dev/go/hauler/pkg/consts" + "hauler.dev/go/hauler/pkg/content" ) type Fn func(desc ocispec.Descriptor) (string, error) // FromManifest will return the appropriate content store given a reference and source type adequate for storing the results on disk -func FromManifest(manifest ocispec.Manifest, root string) (target.Target, error) { - // TODO: Don't rely solely on config mediatype +func FromManifest(manifest ocispec.Manifest, root string) (content.Target, error) { + // First, switch on config mediatype to identify known types switch manifest.Config.MediaType { - case consts.DockerConfigJSON, consts.OCIManifestSchema1: - s := NewMapperFileStore(root, Images()) - defer s.Close() - return s, nil + case consts.DockerConfigJSON, ocispec.MediaTypeImageConfig: + return NewMapperFileStore(root, Images()) case consts.ChartLayerMediaType, consts.ChartConfigMediaType: - s := NewMapperFileStore(root, Chart()) - defer s.Close() - return s, nil + return NewMapperFileStore(root, Chart()) - default: - s := NewMapperFileStore(root, nil) - defer s.Close() - return s, nil + case consts.FileLocalConfigMediaType, consts.FileDirectoryConfigMediaType, consts.FileHttpConfigMediaType: + return NewMapperFileStore(root, Files()) } + + // For unknown config types, check if any layer has a title annotation, which indicates a file artifact + hasFileLayer := false + for _, layer := range manifest.Layers { + if _, ok := layer.Annotations[ocispec.AnnotationTitle]; ok { + hasFileLayer = true + break + } + } + if hasFileLayer { + return NewMapperFileStore(root, Files()) + } + + // Default fallback + return NewMapperFileStore(root, nil) } func Images() map[string]Fn { @@ -81,3 +90,24 @@ func Chart() map[string]Fn { m[consts.ProvLayerMediaType] = provMapperFn return m } + +func Files() map[string]Fn { + m := make(map[string]Fn) + + fileMapperFn := Fn(func(desc ocispec.Descriptor) (string, error) { + // Use the title annotation to determine the filename + if title, ok := desc.Annotations[ocispec.AnnotationTitle]; ok { + return title, nil + } + // Fallback to digest-based filename if no title + return fmt.Sprintf("%s.file", desc.Digest.String()), nil + }) + + // Match the media type that's actually used in the manifest + // (set by getter.LayerFrom in pkg/getter/getter.go) + m[consts.FileLayerMediaType] = fileMapperFn + m[consts.OCILayer] = fileMapperFn // Also handle standard OCI layers that have title annotation + m["application/vnd.oci.image.layer.v1.tar"] = fileMapperFn // And the tar variant + + return m +} diff --git a/pkg/artifacts/file/file.go b/pkg/artifacts/file/file.go index 214c909..0adafa0 100644 --- a/pkg/artifacts/file/file.go +++ b/pkg/artifacts/file/file.go @@ -6,6 +6,7 @@ import ( gv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/partial" gtypes "github.com/google/go-containerregistry/pkg/v1/types" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "hauler.dev/go/hauler/pkg/artifacts" "hauler.dev/go/hauler/pkg/consts" @@ -90,6 +91,13 @@ func (f *File) compute() error { return err } + // Manually preserve the Title annotation from the layer + // The layer was created with this annotation in getter.LayerFrom + if layer.Annotations == nil { + layer.Annotations = make(map[string]string) + } + layer.Annotations[ocispec.AnnotationTitle] = f.client.Name(f.Path) + cfg := f.client.Config(f.Path) if cfg == nil { cfg = f.client.Config(f.Path) diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go index 34d4249..884dbb0 100644 --- a/pkg/consts/consts.go +++ b/pkg/consts/consts.go @@ -42,10 +42,23 @@ const ( HaulerVendorPrefix = "vnd.hauler" // annotation keys - ContainerdImageNameKey = "io.containerd.image.name" - KindAnnotationName = "kind" - KindAnnotationImage = "dev.cosignproject.cosign/image" - KindAnnotationIndex = "dev.cosignproject.cosign/imageIndex" + ContainerdImageNameKey = "io.containerd.image.name" + KindAnnotationName = "kind" + KindAnnotationImage = "dev.cosignproject.cosign/image" + KindAnnotationIndex = "dev.cosignproject.cosign/imageIndex" + KindAnnotationSigs = "dev.cosignproject.cosign/sigs" + KindAnnotationAtts = "dev.cosignproject.cosign/atts" + KindAnnotationSboms = "dev.cosignproject.cosign/sboms" + // KindAnnotationReferrers is the kind prefix for OCI 1.1 referrer manifests (cosign v3 + // new-bundle-format). Each referrer gets a unique kind with the referrer manifest digest + // appended (e.g. "dev.cosignproject.cosign/referrers/sha256hex") so multiple referrers + // for the same base image coexist in the OCI index. + KindAnnotationReferrers = "dev.cosignproject.cosign/referrers" + + // Sigstore / OCI 1.1 artifact media types used by cosign v3 new-bundle-format. + SigstoreBundleMediaType = "application/vnd.dev.sigstore.bundle.v0.3+json" + OCIEmptyConfigMediaType = "application/vnd.oci.empty.v1+json" + ImageAnnotationKey = "hauler.dev/key" ImageAnnotationPlatform = "hauler.dev/platform" ImageAnnotationRegistry = "hauler.dev/registry" diff --git a/pkg/content/oci.go b/pkg/content/oci.go index 9f5773c..a2a3edc 100644 --- a/pkg/content/oci.go +++ b/pkg/content/oci.go @@ -17,14 +17,12 @@ import ( "github.com/containerd/containerd/remotes" "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "oras.land/oras-go/pkg/content" - "oras.land/oras-go/pkg/target" "hauler.dev/go/hauler/pkg/consts" "hauler.dev/go/hauler/pkg/reference" ) -var _ target.Target = (*OCI)(nil) +var _ Target = (*OCI)(nil) type OCI struct { root string @@ -76,6 +74,7 @@ func (o *OCI) LoadIndex() error { Versioned: specs.Versioned{ SchemaVersion: 2, }, + MediaType: ocispec.MediaTypeImageIndex, } return nil } @@ -88,15 +87,22 @@ func (o *OCI) LoadIndex() error { for _, desc := range o.index.Manifests { key, err := reference.Parse(desc.Annotations[ocispec.AnnotationRefName]) if err != nil { - return err + // skip malformed entries rather than making the entire store unreadable + continue + } + + // Set default kind if missing + kind := desc.Annotations[consts.KindAnnotationName] + if kind == "" { + kind = consts.KindAnnotationImage } if strings.TrimSpace(key.String()) != "--" { switch key.(type) { case name.Digest: - o.nameMap.Store(fmt.Sprintf("%s-%s", key.Context().String(), desc.Annotations[consts.KindAnnotationName]), desc) + o.nameMap.Store(fmt.Sprintf("%s-%s", key.Context().String(), kind), desc) case name.Tag: - o.nameMap.Store(fmt.Sprintf("%s-%s", key.String(), desc.Annotations[consts.KindAnnotationName]), desc) + o.nameMap.Store(fmt.Sprintf("%s-%s", key.String(), kind), desc) } } } @@ -152,16 +158,16 @@ func (o *OCI) SaveIndex() error { // While the name may differ from ref, it should itself be a valid ref. // // If the resolution fails, an error will be returned. -func (o *OCI) Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) { +func (o *OCI) Resolve(ctx context.Context, ref string) (ocispec.Descriptor, error) { if err := o.LoadIndex(); err != nil { - return "", ocispec.Descriptor{}, err + return ocispec.Descriptor{}, err } d, ok := o.nameMap.Load(ref) if !ok { - return "", ocispec.Descriptor{}, err + return ocispec.Descriptor{}, fmt.Errorf("reference %s not found", ref) } - desc = d.(ocispec.Descriptor) - return ref, desc, nil + desc := d.(ocispec.Descriptor) + return desc, nil } // Fetcher returns a new fetcher for the provided reference. @@ -271,6 +277,12 @@ func (o *OCI) path(elem ...string) string { return filepath.Join(append(complete, elem...)...) } +// IndexExists reports whether the store's OCI layout index.json exists on disk. +func (o *OCI) IndexExists() bool { + _, err := os.Stat(o.path(ocispec.ImageIndexFile)) + return err == nil +} + type ociPusher struct { oci *OCI ref string @@ -287,7 +299,13 @@ func (p *ociPusher) Push(ctx context.Context, d ocispec.Descriptor) (ccontent.Wr if err := p.oci.LoadIndex(); err != nil { return nil, err } - p.oci.nameMap.Store(p.ref, d) + // Use compound key format: "reference-kind" + kind := d.Annotations[consts.KindAnnotationName] + if kind == "" { + kind = consts.KindAnnotationImage + } + key := fmt.Sprintf("%s-%s", p.ref, kind) + p.oci.nameMap.Store(key, d) if err := p.oci.SaveIndex(); err != nil { return nil, err } @@ -301,7 +319,7 @@ func (p *ociPusher) Push(ctx context.Context, d ocispec.Descriptor) (ccontent.Wr if _, err := os.Stat(blobPath); err == nil { // file already exists, discard (but validate digest) - return content.NewIoContentWriter(io.Discard, content.WithOutputHash(d.Digest)), nil + return NewIoContentWriter(nopCloser{io.Discard}, WithOutputHash(d.Digest.String())), nil } f, err := os.Create(blobPath) @@ -309,10 +327,25 @@ func (p *ociPusher) Push(ctx context.Context, d ocispec.Descriptor) (ccontent.Wr return nil, err } - w := content.NewIoContentWriter(f, content.WithInputHash(d.Digest), content.WithOutputHash(d.Digest)) + w := NewIoContentWriter(f, WithOutputHash(d.Digest.String())) return w, nil } func (o *OCI) RemoveFromIndex(ref string) { o.nameMap.Delete(ref) } + +// ResolvePath returns the absolute path for a given relative path within the OCI root +func (o *OCI) ResolvePath(elem string) string { + if elem == "" { + return o.root + } + return filepath.Join(o.root, elem) +} + +// nopCloser wraps an io.Writer to implement io.WriteCloser +type nopCloser struct { + io.Writer +} + +func (nopCloser) Close() error { return nil } diff --git a/pkg/content/registry.go b/pkg/content/registry.go new file mode 100644 index 0000000..ff2a5e8 --- /dev/null +++ b/pkg/content/registry.go @@ -0,0 +1,102 @@ +package content + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/containerd/containerd/remotes" + cdocker "github.com/containerd/containerd/remotes/docker" + goauthn "github.com/google/go-containerregistry/pkg/authn" + goname "github.com/google/go-containerregistry/pkg/name" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +var _ Target = (*RegistryTarget)(nil) + +// RegistryTarget implements Target for pushing to a remote OCI registry. +// Authentication is sourced from the local Docker credential store via go-containerregistry's +// default keychain unless explicit credentials are provided in RegistryOptions. +type RegistryTarget struct { + resolver remotes.Resolver +} + +// NewRegistryTarget returns a RegistryTarget that pushes to host (e.g. "localhost:5000"). +func NewRegistryTarget(host string, opts RegistryOptions) *RegistryTarget { + authorizer := cdocker.NewDockerAuthorizer( + cdocker.WithAuthCreds(func(h string) (string, string, error) { + if opts.Username != "" { + return opts.Username, opts.Password, nil + } + // Bridge to go-containerregistry's keychain for credential lookup. + reg, err := goname.NewRegistry(h, goname.Insecure) + if err != nil { + return "", "", nil + } + a, err := goauthn.DefaultKeychain.Resolve(reg) + if err != nil || a == goauthn.Anonymous { + return "", "", nil + } + cfg, err := a.Authorization() + if err != nil { + return "", "", nil + } + return cfg.Username, cfg.Password, nil + }), + ) + + hosts := func(h string) ([]cdocker.RegistryHost, error) { + scheme := "https" + if opts.PlainHTTP || opts.Insecure { + scheme = "http" + } + return []cdocker.RegistryHost{{ + Client: http.DefaultClient, + Authorizer: authorizer, + Scheme: scheme, + Host: h, + Path: "/v2", + Capabilities: cdocker.HostCapabilityPull | cdocker.HostCapabilityResolve | cdocker.HostCapabilityPush, + }}, nil + } + + return &RegistryTarget{ + resolver: cdocker.NewResolver(cdocker.ResolverOptions{ + Hosts: hosts, + }), + } +} + +func (t *RegistryTarget) Resolve(ctx context.Context, ref string) (ocispec.Descriptor, error) { + _, desc, err := t.resolver.Resolve(ctx, ref) + return desc, err +} + +func (t *RegistryTarget) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { + return t.resolver.Fetcher(ctx, ref) +} + +func (t *RegistryTarget) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) { + return t.resolver.Pusher(ctx, ref) +} + +// RewriteRefToRegistry rewrites sourceRef to use targetRegistry as its host, preserving the +// repository path and tag or digest. For example: +// +// "index.docker.io/library/nginx:latest" + "localhost:5000" → "localhost:5000/library/nginx:latest" +func RewriteRefToRegistry(sourceRef string, targetRegistry string) (string, error) { + ref, err := goname.ParseReference(sourceRef) + if err != nil { + return "", fmt.Errorf("parsing reference %q: %w", sourceRef, err) + } + repo := strings.TrimPrefix(ref.Context().RepositoryStr(), "/") + switch r := ref.(type) { + case goname.Tag: + return fmt.Sprintf("%s/%s:%s", targetRegistry, repo, r.TagStr()), nil + case goname.Digest: + return fmt.Sprintf("%s/%s@%s", targetRegistry, repo, r.DigestStr()), nil + default: + return fmt.Sprintf("%s/%s:latest", targetRegistry, repo), nil + } +} diff --git a/pkg/content/types.go b/pkg/content/types.go new file mode 100644 index 0000000..9accd72 --- /dev/null +++ b/pkg/content/types.go @@ -0,0 +1,106 @@ +package content + +import ( + "context" + "fmt" + "io" + + ccontent "github.com/containerd/containerd/content" + "github.com/containerd/containerd/remotes" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Target represents a content storage target with resolver, fetcher, and pusher capabilities +type Target interface { + Resolve(ctx context.Context, ref string) (ocispec.Descriptor, error) + Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) + Pusher(ctx context.Context, ref string) (remotes.Pusher, error) +} + +// RegistryOptions holds registry configuration +type RegistryOptions struct { + PlainHTTP bool + Insecure bool + Username string + Password string +} + +// ResolveName extracts the reference name from a descriptor's annotations +func ResolveName(desc ocispec.Descriptor) (string, bool) { + name, ok := desc.Annotations[ocispec.AnnotationRefName] + return name, ok +} + +// IoContentWriter wraps an io.Writer to implement containerd's content.Writer +type IoContentWriter struct { + writer io.WriteCloser + digester digest.Digester + status ccontent.Status + outputHash string +} + +// Write writes data to the underlying writer and updates the digest +func (w *IoContentWriter) Write(p []byte) (n int, err error) { + n, err = w.writer.Write(p) + if n > 0 { + w.digester.Hash().Write(p[:n]) + } + return n, err +} + +// Close closes the writer and verifies the digest if configured +func (w *IoContentWriter) Close() error { + if w.outputHash != "" { + computed := w.digester.Digest().String() + if computed != w.outputHash { + return fmt.Errorf("digest mismatch: expected %s, got %s", w.outputHash, computed) + } + } + return w.writer.Close() +} + +// Digest returns the current digest of written data +func (w *IoContentWriter) Digest() digest.Digest { + return w.digester.Digest() +} + +// Commit is a no-op for this implementation +func (w *IoContentWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...ccontent.Opt) error { + return nil +} + +// Status returns the current status +func (w *IoContentWriter) Status() (ccontent.Status, error) { + return w.status, nil +} + +// Truncate is not supported +func (w *IoContentWriter) Truncate(size int64) error { + return fmt.Errorf("truncate not supported") +} + +type writerOption func(*IoContentWriter) + +// WithOutputHash configures expected output hash for verification +func WithOutputHash(hash string) writerOption { + return func(w *IoContentWriter) { + w.outputHash = hash + } +} + +// NewIoContentWriter creates a new IoContentWriter +func NewIoContentWriter(writer io.WriteCloser, opts ...writerOption) *IoContentWriter { + w := &IoContentWriter{ + writer: writer, + digester: digest.Canonical.Digester(), + status: ccontent.Status{}, + } + for _, opt := range opts { + opt(w) + } + return w +} + +// AnnotationUnpack is the annotation key for unpacking +const AnnotationUnpack = "io.containerd.image.unpack" diff --git a/pkg/cosign/cosign.go b/pkg/cosign/cosign.go index 8f0ac7b..56fd3a5 100644 --- a/pkg/cosign/cosign.go +++ b/pkg/cosign/cosign.go @@ -2,24 +2,16 @@ package cosign import ( "context" - "fmt" - "os" - "strings" - "time" - "github.com/sigstore/cosign/v3/cmd/cosign/cli" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/verify" "hauler.dev/go/hauler/internal/flags" - "hauler.dev/go/hauler/pkg/artifacts/image" - "hauler.dev/go/hauler/pkg/consts" "hauler.dev/go/hauler/pkg/log" - "hauler.dev/go/hauler/pkg/store" - "oras.land/oras-go/pkg/content" + "hauler.dev/go/hauler/pkg/retry" ) -// VerifySignature verifies the digital signature of a file using Sigstore/Cosign. -func VerifySignature(ctx context.Context, s *store.Layout, keyPath string, useTlog bool, ref string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error { +// VerifySignature verifies the digital signature of an image using Sigstore/Cosign. +func VerifySignature(ctx context.Context, keyPath string, useTlog bool, ref string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error { l := log.FromContext(ctx) operation := func() error { v := &verify.VerifyCommand{ @@ -28,29 +20,21 @@ func VerifySignature(ctx context.Context, s *store.Layout, keyPath string, useTl NewBundleFormat: true, } - // if the user wants to use the transparency log, set the flag to false if useTlog { v.IgnoreTlog = false } - err := log.CaptureOutput(l, true, func() error { + return log.CaptureOutput(l, true, func() error { return v.Exec(ctx, []string{ref}) }) - if err != nil { - return err - } - - return nil } - - return RetryOperation(ctx, rso, ro, operation) + return retry.Operation(ctx, rso, ro, operation) } -// VerifyKeylessSignature verifies the digital signature of a file using Sigstore/Cosign. -func VerifyKeylessSignature(ctx context.Context, s *store.Layout, identity string, identityRegexp string, oidcIssuer string, oidcIssuerRegexp string, ghWorkflowRepository string, useTlog bool, ref string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error { +// VerifyKeylessSignature verifies an image signature using keyless/OIDC identity. +func VerifyKeylessSignature(ctx context.Context, identity string, identityRegexp string, oidcIssuer string, oidcIssuerRegexp string, ghWorkflowRepository string, ref string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error { l := log.FromContext(ctx) operation := func() error { - certVerifyOptions := options.CertVerifyOptions{ CertOidcIssuer: oidcIssuer, CertOidcIssuerRegexp: oidcIssuerRegexp, @@ -61,153 +45,14 @@ func VerifyKeylessSignature(ctx context.Context, s *store.Layout, identity strin v := &verify.VerifyCommand{ CertVerifyOptions: certVerifyOptions, - IgnoreTlog: false, // Ignore transparency log is set to false by default for keyless signature verification + IgnoreTlog: false, // Use transparency log by default for keyless verification. CertGithubWorkflowRepository: ghWorkflowRepository, NewBundleFormat: true, } - // if the user wants to use the transparency log, set the flag to false - if useTlog { - v.IgnoreTlog = false - } - - err := log.CaptureOutput(l, true, func() error { + return log.CaptureOutput(l, true, func() error { return v.Exec(ctx, []string{ref}) }) - if err != nil { - return err - } - - return nil } - - return RetryOperation(ctx, rso, ro, operation) -} - -// SaveImage saves image and any signatures/attestations to the store. -func SaveImage(ctx context.Context, s *store.Layout, ref string, platform string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error { - l := log.FromContext(ctx) - - if !ro.IgnoreErrors { - envVar := os.Getenv(consts.HaulerIgnoreErrors) - if envVar == "true" { - ro.IgnoreErrors = true - } - } - - operation := func() error { - o := &options.SaveOptions{ - Directory: s.Root, - } - - // check to see if the image is multi-arch - isMultiArch, err := image.IsMultiArchImage(ref) - if err != nil { - return err - } - l.Debugf("multi-arch image [%v]", isMultiArch) - - // Conditionally add platform. - if platform != "" && isMultiArch { - l.Debugf("platform for image [%s]", platform) - o.Platform = platform - } - - err = cli.SaveCmd(ctx, *o, ref) - if err != nil { - return err - } - - return nil - - } - - return RetryOperation(ctx, rso, ro, operation) -} - -// LoadImage loads store to a remote registry. -func LoadImages(ctx context.Context, s *store.Layout, registry string, only string, ropts content.RegistryOptions, ro *flags.CliRootOpts) error { - l := log.FromContext(ctx) - - o := &options.LoadOptions{ - Directory: s.Root, - Registry: options.RegistryOptions{ - Name: registry, - }, - } - - // Conditionally add extra flags. - if len(only) > 0 { - o.LoadOnly = only - } - - if ropts.Insecure { - o.Registry.AllowInsecure = true - } - - if ropts.PlainHTTP { - o.Registry.AllowHTTPRegistry = true - } - - if ropts.Username != "" { - o.Registry.AuthConfig.Username = ropts.Username - o.Registry.AuthConfig.Password = ropts.Password - } - - // execute the cosign load and capture the output in our logger - err := log.CaptureOutput(l, false, func() error { - return cli.LoadCmd(ctx, *o, "") - }) - if err != nil { - return err - } - - return nil -} - -func RetryOperation(ctx context.Context, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, operation func() error) error { - l := log.FromContext(ctx) - - if !ro.IgnoreErrors { - envVar := os.Getenv(consts.HaulerIgnoreErrors) - if envVar == "true" { - ro.IgnoreErrors = true - } - } - - // Validate retries and fall back to a default - retries := rso.Retries - if retries <= 0 { - retries = consts.DefaultRetries - } - - for attempt := 1; attempt <= rso.Retries; attempt++ { - err := operation() - if err == nil { - // If the operation succeeds, return nil (no error) - return nil - } - - if ro.IgnoreErrors { - if strings.HasPrefix(err.Error(), "function execution failed: no matching signatures: rekor client not provided for online verification") { - l.Warnf("warning (attempt %d/%d)... failed tlog verification", attempt, rso.Retries) - } else { - l.Warnf("warning (attempt %d/%d)... %v", attempt, rso.Retries, err) - } - } else { - if strings.HasPrefix(err.Error(), "function execution failed: no matching signatures: rekor client not provided for online verification") { - l.Errorf("error (attempt %d/%d)... failed tlog verification", attempt, rso.Retries) - } else { - l.Errorf("error (attempt %d/%d)... %v", attempt, rso.Retries, err) - } - } - - // If this is not the last attempt, wait before retrying - if attempt < rso.Retries { - time.Sleep(time.Second * consts.RetriesInterval) - } - } - - // If all attempts fail, return an error - return fmt.Errorf("operation unsuccessful after %d attempts", rso.Retries) + return retry.Operation(ctx, rso, ro, operation) } diff --git a/pkg/getter/directory.go b/pkg/getter/directory.go index 2399393..88ec04c 100644 --- a/pkg/getter/directory.go +++ b/pkg/getter/directory.go @@ -33,30 +33,37 @@ func (d directory) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) digester := digest.Canonical.Digester() zw := gzip.NewWriter(io.MultiWriter(tmpfile, digester.Hash())) - defer zw.Close() tarDigester := digest.Canonical.Digester() if err := tarDir(d.path(u), d.Name(u), io.MultiWriter(zw, tarDigester.Hash()), false); err != nil { + zw.Close() + tmpfile.Close() + os.Remove(tmpfile.Name()) return nil, err } if err := zw.Close(); err != nil { + tmpfile.Close() + os.Remove(tmpfile.Name()) return nil, err } if err := tmpfile.Sync(); err != nil { + tmpfile.Close() + os.Remove(tmpfile.Name()) return nil, err } - fi, err := os.Open(tmpfile.Name()) + // Close the write handle; re-open as read-only + tmpName := tmpfile.Name() + tmpfile.Close() + + fi, err := os.Open(tmpName) if err != nil { + os.Remove(tmpName) return nil, err } - // rc := &closer{ - // t: io.TeeReader(tmpfile, fi), - // closes: []func() error{fi.Close, tmpfile.Close, zw.Close}, - // } - return fi, nil + return &tempFileReadCloser{File: fi, path: tmpName}, nil } func (d directory) Detect(u *url.URL) bool { @@ -144,22 +151,15 @@ func tarDir(root string, prefix string, w io.Writer, stripTimes bool) error { return nil } -type closer struct { - t io.Reader - closes []func() error +// tempFileReadCloser wraps an *os.File and removes the underlying +// temp file when closed. +type tempFileReadCloser struct { + *os.File + path string } -func (c *closer) Read(p []byte) (n int, err error) { - return c.t.Read(p) -} - -func (c *closer) Close() error { - var err error - for _, c := range c.closes { - lastErr := c() - if err == nil { - err = lastErr - } - } +func (t *tempFileReadCloser) Close() error { + err := t.File.Close() + os.Remove(t.path) return err } diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 63274df..c022c21 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -9,10 +9,10 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" - "oras.land/oras-go/pkg/content" content2 "hauler.dev/go/hauler/pkg/artifacts" "hauler.dev/go/hauler/pkg/consts" + "hauler.dev/go/hauler/pkg/content" "hauler.dev/go/hauler/pkg/layer" ) diff --git a/pkg/getter/https.go b/pkg/getter/https.go index ee2ae73..4bd4d23 100644 --- a/pkg/getter/https.go +++ b/pkg/getter/https.go @@ -2,6 +2,7 @@ package getter import ( "context" + "fmt" "io" "mime" "net/http" @@ -24,8 +25,9 @@ func (h Http) Name(u *url.URL) string { if err != nil { return "" } + defer resp.Body.Close() - name, _ := url.PathUnescape(u.String()) + unescaped, err := url.PathUnescape(u.String()) if err != nil { return "" } @@ -40,8 +42,7 @@ func (h Http) Name(u *url.URL) string { _ = t } - // TODO: Not this - return filepath.Base(name) + return filepath.Base(unescaped) } func (h Http) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) { @@ -49,6 +50,10 @@ func (h Http) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) { if err != nil { return nil, err } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("unexpected status fetching %s: %s", u.String(), resp.Status) + } return resp.Body, nil } diff --git a/pkg/layer/filesystem.go b/pkg/layer/filesystem.go index 8330fdb..0abce5a 100644 --- a/pkg/layer/filesystem.go +++ b/pkg/layer/filesystem.go @@ -66,10 +66,11 @@ func (l *cachedLayer) create(h v1.Hash) (io.WriteCloser, error) { func (l *cachedLayer) Compressed() (io.ReadCloser, error) { f, err := l.create(l.digest) if err != nil { - return nil, nil + return nil, err } rc, err := l.Layer.Compressed() if err != nil { + f.Close() return nil, err } return &readcloser{ @@ -85,6 +86,7 @@ func (l *cachedLayer) Uncompressed() (io.ReadCloser, error) { } rc, err := l.Layer.Uncompressed() if err != nil { + f.Close() return nil, err } return &readcloser{ diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go new file mode 100644 index 0000000..4e93e46 --- /dev/null +++ b/pkg/retry/retry.go @@ -0,0 +1,57 @@ +package retry + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "hauler.dev/go/hauler/internal/flags" + "hauler.dev/go/hauler/pkg/consts" + "hauler.dev/go/hauler/pkg/log" +) + +// Operation retries the given operation according to the retry settings in rso/ro. +func Operation(ctx context.Context, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, operation func() error) error { + l := log.FromContext(ctx) + + if !ro.IgnoreErrors { + if os.Getenv(consts.HaulerIgnoreErrors) == "true" { + ro.IgnoreErrors = true + } + } + + retries := rso.Retries + if retries <= 0 { + retries = consts.DefaultRetries + } + + for attempt := 1; attempt <= retries; attempt++ { + err := operation() + if err == nil { + return nil + } + + isTlogErr := strings.HasPrefix(err.Error(), "function execution failed: no matching signatures: rekor client not provided for online verification") + if ro.IgnoreErrors { + if isTlogErr { + l.Warnf("warning (attempt %d/%d)... failed tlog verification", attempt, retries) + } else { + l.Warnf("warning (attempt %d/%d)... %v", attempt, retries, err) + } + } else { + if isTlogErr { + l.Errorf("error (attempt %d/%d)... failed tlog verification", attempt, retries) + } else { + l.Errorf("error (attempt %d/%d)... %v", attempt, retries, err) + } + } + + if attempt < retries { + time.Sleep(time.Second * consts.RetriesInterval) + } + } + + return fmt.Errorf("operation unsuccessful after %d attempts", retries) +} diff --git a/pkg/store/store.go b/pkg/store/store.go index cef607a..9e6993c 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -1,20 +1,26 @@ package store import ( + "bytes" "context" "encoding/json" "fmt" "io" "os" "path/filepath" + "strings" + "github.com/containerd/containerd/remotes" + "github.com/containerd/errdefs" + "github.com/google/go-containerregistry/pkg/authn" + gname "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/static" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/rs/zerolog" "golang.org/x/sync/errgroup" - "oras.land/oras-go/pkg/oras" - "oras.land/oras-go/pkg/target" "hauler.dev/go/hauler/pkg/artifacts" "hauler.dev/go/hauler/pkg/consts" @@ -58,13 +64,13 @@ func NewLayout(rootdir string, opts ...Options) (*Layout, error) { return l, nil } -// AddOCI adds an artifacts.OCI to the store +// AddArtifact adds an artifacts.OCI to the store // // The method to achieve this is to save artifact.OCI to a temporary directory in an OCI layout compatible form. Once // saved, the entirety of the layout is copied to the store (which is just a registry). This allows us to not only use // strict types to define generic content, but provides a processing pipeline suitable for extensibility. In the // future we'll allow users to define their own content that must adhere either by artifact.OCI or simply an OCI layout. -func (l *Layout) AddOCI(ctx context.Context, oci artifacts.OCI, ref string) (ocispec.Descriptor, error) { +func (l *Layout) AddArtifact(ctx context.Context, oci artifacts.OCI, ref string) (ocispec.Descriptor, error) { if l.cache != nil { cached := layer.OCICache(oci, l.cache) oci = cached @@ -90,8 +96,6 @@ func (l *Layout) AddOCI(ctx context.Context, oci artifacts.OCI, ref string) (oci return ocispec.Descriptor{}, err } - static.NewLayer(cdata, "") - if err := l.writeBlobData(cdata); err != nil { return ocispec.Descriptor{}, err } @@ -129,8 +133,8 @@ func (l *Layout) AddOCI(ctx context.Context, oci artifacts.OCI, ref string) (oci return idx, l.OCI.AddIndex(idx) } -// AddOCICollection . -func (l *Layout) AddOCICollection(ctx context.Context, collection artifacts.OCICollection) ([]ocispec.Descriptor, error) { +// AddArtifactCollection . +func (l *Layout) AddArtifactCollection(ctx context.Context, collection artifacts.OCICollection) ([]ocispec.Descriptor, error) { cnts, err := collection.Contents() if err != nil { return nil, err @@ -138,7 +142,7 @@ func (l *Layout) AddOCICollection(ctx context.Context, collection artifacts.OCIC var descs []ocispec.Descriptor for ref, oci := range cnts { - desc, err := l.AddOCI(ctx, oci, ref) + desc, err := l.AddArtifact(ctx, oci, ref) if err != nil { return nil, err } @@ -147,6 +151,325 @@ func (l *Layout) AddOCICollection(ctx context.Context, collection artifacts.OCIC return descs, nil } +// AddImage fetches a container image (or full index for multi-arch images) from a remote registry +// and saves it to the store along with any associated signatures, attestations, and SBOMs +// discovered via cosign's tag convention (.sig, .att, .sbom). +// When platform is non-empty and the ref is a multi-arch index, only that platform is fetched. +func (l *Layout) AddImage(ctx context.Context, ref string, platform string, opts ...remote.Option) error { + allOpts := append([]remote.Option{ + remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithContext(ctx), + }, opts...) + + parsedRef, err := gname.ParseReference(ref) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", ref, err) + } + + desc, err := remote.Get(parsedRef, allOpts...) + if err != nil { + return fmt.Errorf("fetching descriptor for %q: %w", ref, err) + } + + var imageDigest v1.Hash + + if idx, idxErr := desc.ImageIndex(); idxErr == nil && platform == "" { + // Multi-arch image with no platform filter: save the full index. + imageDigest, err = idx.Digest() + if err != nil { + return fmt.Errorf("getting index digest for %q: %w", ref, err) + } + if err := l.writeIndex(parsedRef, idx, consts.KindAnnotationIndex); err != nil { + return err + } + } else { + // Single-platform image, or the caller requested a specific platform. + imgOpts := append([]remote.Option{}, allOpts...) + if platform != "" { + p, err := parsePlatform(platform) + if err != nil { + return err + } + imgOpts = append(imgOpts, remote.WithPlatform(p)) + } + img, err := remote.Image(parsedRef, imgOpts...) + if err != nil { + return fmt.Errorf("fetching image %q: %w", ref, err) + } + imageDigest, err = img.Digest() + if err != nil { + return fmt.Errorf("getting image digest for %q: %w", ref, err) + } + if err := l.writeImage(parsedRef, img, consts.KindAnnotationImage, ""); err != nil { + return err + } + } + + savedDigests, err := l.saveRelatedArtifacts(ctx, parsedRef, imageDigest, allOpts...) + if err != nil { + return err + } + return l.saveReferrers(ctx, parsedRef, imageDigest, savedDigests, allOpts...) +} + +// writeImageBlobs writes all blobs for a single image (layers, config, manifest) to the store's +// blob directory. It does not add an entry to the OCI index. +func (l *Layout) writeImageBlobs(img v1.Image) error { + layers, err := img.Layers() + if err != nil { + return fmt.Errorf("getting layers: %w", err) + } + var g errgroup.Group + for _, lyr := range layers { + lyr := lyr + g.Go(func() error { return l.writeLayer(lyr) }) + } + if err := g.Wait(); err != nil { + return err + } + + cfgData, err := img.RawConfigFile() + if err != nil { + return fmt.Errorf("getting config: %w", err) + } + if err := l.writeBlobData(cfgData); err != nil { + return fmt.Errorf("writing config blob: %w", err) + } + + manifestData, err := img.RawManifest() + if err != nil { + return fmt.Errorf("getting manifest: %w", err) + } + return l.writeBlobData(manifestData) +} + +// writeImage writes all blobs for img and adds a descriptor entry to the OCI index with the +// given annotationRef and kind. containerdName overrides the io.containerd.image.name annotation; +// if empty it defaults to annotationRef.Name(). +func (l *Layout) writeImage(annotationRef gname.Reference, img v1.Image, kind string, containerdName string) error { + if err := l.writeImageBlobs(img); err != nil { + return err + } + + mt, err := img.MediaType() + if err != nil { + return fmt.Errorf("getting media type: %w", err) + } + hash, err := img.Digest() + if err != nil { + return fmt.Errorf("getting digest: %w", err) + } + d, err := digest.Parse(hash.String()) + if err != nil { + return fmt.Errorf("parsing digest: %w", err) + } + raw, err := img.RawManifest() + if err != nil { + return fmt.Errorf("getting raw manifest size: %w", err) + } + + if containerdName == "" { + containerdName = annotationRef.Name() + } + desc := ocispec.Descriptor{ + MediaType: string(mt), + Digest: d, + Size: int64(len(raw)), + Annotations: map[string]string{ + consts.KindAnnotationName: kind, + ocispec.AnnotationRefName: strings.TrimPrefix(annotationRef.Name(), annotationRef.Context().RegistryStr()+"/"), + consts.ContainerdImageNameKey: containerdName, + }, + } + return l.OCI.AddIndex(desc) +} + +// writeIndexBlobs recursively writes all child image blobs for an image index to the store's blob +// directory. It does not write the top-level index manifest or add index entries. +func (l *Layout) writeIndexBlobs(idx v1.ImageIndex) error { + manifest, err := idx.IndexManifest() + if err != nil { + return fmt.Errorf("getting index manifest: %w", err) + } + + for _, childDesc := range manifest.Manifests { + // Try as a nested index first, then fall back to a regular image. + if childIdx, err := idx.ImageIndex(childDesc.Digest); err == nil { + if err := l.writeIndexBlobs(childIdx); err != nil { + return err + } + raw, err := childIdx.RawManifest() + if err != nil { + return fmt.Errorf("getting nested index manifest: %w", err) + } + if err := l.writeBlobData(raw); err != nil { + return err + } + } else { + childImg, err := idx.Image(childDesc.Digest) + if err != nil { + return fmt.Errorf("getting child image %v: %w", childDesc.Digest, err) + } + if err := l.writeImageBlobs(childImg); err != nil { + return err + } + } + } + return nil +} + +// writeIndex writes all blobs for an image index (including all child platform images) and adds +// a descriptor entry to the OCI index with the given annotationRef and kind. +func (l *Layout) writeIndex(annotationRef gname.Reference, idx v1.ImageIndex, kind string) error { + if err := l.writeIndexBlobs(idx); err != nil { + return err + } + + raw, err := idx.RawManifest() + if err != nil { + return fmt.Errorf("getting index manifest: %w", err) + } + if err := l.writeBlobData(raw); err != nil { + return fmt.Errorf("writing index manifest blob: %w", err) + } + + mt, err := idx.MediaType() + if err != nil { + return fmt.Errorf("getting index media type: %w", err) + } + hash, err := idx.Digest() + if err != nil { + return fmt.Errorf("getting index digest: %w", err) + } + d, err := digest.Parse(hash.String()) + if err != nil { + return fmt.Errorf("parsing index digest: %w", err) + } + + desc := ocispec.Descriptor{ + MediaType: string(mt), + Digest: d, + Size: int64(len(raw)), + Annotations: map[string]string{ + consts.KindAnnotationName: kind, + ocispec.AnnotationRefName: strings.TrimPrefix(annotationRef.Name(), annotationRef.Context().RegistryStr()+"/"), + consts.ContainerdImageNameKey: annotationRef.Name(), + }, + } + return l.OCI.AddIndex(desc) +} + +// saveReferrers discovers and saves OCI 1.1 referrers for the image identified by ref/hash. +// This captures cosign v3 new-bundle-format signatures/attestations stored as OCI referrers +// (via the subject field) rather than the legacy sha256-.sig/.att/.sbom tag convention. +// go-containerregistry handles both the native referrers API and the tag-based fallback. +// Missing referrers and fetch errors are logged at debug level and silently skipped. +func (l *Layout) saveReferrers(ctx context.Context, ref gname.Reference, hash v1.Hash, alreadySaved map[string]bool, opts ...remote.Option) error { + log := zerolog.Ctx(ctx) + + imageDigestRef, err := gname.NewDigest(ref.Context().String() + "@" + hash.String()) + if err != nil { + log.Debug().Err(err).Msgf("saveReferrers: could not construct digest ref for %s", ref.Name()) + return nil + } + + idx, err := remote.Referrers(imageDigestRef, opts...) + if err != nil { + // Most registries that don't support the referrers API return 404; not an error. + log.Debug().Err(err).Msgf("no OCI referrers found for %s@%s", ref.Name(), hash) + return nil + } + + idxManifest, err := idx.IndexManifest() + if err != nil { + log.Debug().Err(err).Msgf("saveReferrers: could not read referrers index for %s", ref.Name()) + return nil + } + + for _, referrerDesc := range idxManifest.Manifests { + digestRef, err := gname.NewDigest(ref.Context().String() + "@" + referrerDesc.Digest.String()) + if err != nil { + log.Debug().Err(err).Msgf("saveReferrers: could not construct digest ref for referrer %s", referrerDesc.Digest) + continue + } + + img, err := remote.Image(digestRef, opts...) + if err != nil { + log.Debug().Err(err).Msgf("saveReferrers: could not fetch referrer manifest %s", referrerDesc.Digest) + continue + } + + // Skip referrers already saved via the cosign tag convention to avoid duplicates. + // Registries like Harbor expose the same manifest via both the .sig/.att/.sbom tags + // and the OCI Referrers API when the manifest carries a subject field. + if alreadySaved[referrerDesc.Digest.String()] { + log.Debug().Msgf("saveReferrers: skipping referrer %s (already saved via tag convention)", referrerDesc.Digest) + continue + } + + // Embed the referrer manifest digest in the kind annotation so that multiple + // referrers for the same base image each get a unique entry in the OCI index. + kind := consts.KindAnnotationReferrers + "/" + referrerDesc.Digest.Hex + if err := l.writeImage(ref, img, kind, ""); err != nil { + return fmt.Errorf("saving OCI referrer %s for %s: %w", referrerDesc.Digest, ref.Name(), err) + } + log.Debug().Msgf("saved OCI referrer %s (%s) for %s", referrerDesc.Digest, string(referrerDesc.ArtifactType), ref.Name()) + } + return nil +} + +// saveRelatedArtifacts discovers and saves cosign-compatible signature, attestation, and SBOM +// artifacts for the image identified by ref/hash. Missing artifacts are silently skipped. +// Returns the set of manifest digest strings (e.g. "sha256:abc...") that were saved, so that +// saveReferrers can skip duplicates when a registry exposes the same manifest via both paths. +func (l *Layout) saveRelatedArtifacts(ctx context.Context, ref gname.Reference, hash v1.Hash, opts ...remote.Option) (map[string]bool, error) { + saved := make(map[string]bool) + + // Cosign tag convention: "sha256:hexvalue" → "sha256-hexvalue.sig" / ".att" / ".sbom" + tagPrefix := strings.ReplaceAll(hash.String(), ":", "-") + + related := []struct { + tag string + kind string + }{ + {tagPrefix + ".sig", consts.KindAnnotationSigs}, + {tagPrefix + ".att", consts.KindAnnotationAtts}, + {tagPrefix + ".sbom", consts.KindAnnotationSboms}, + } + + for _, r := range related { + artifactRef, err := gname.ParseReference(ref.Context().String() + ":" + r.tag) + if err != nil { + continue + } + img, err := remote.Image(artifactRef, opts...) + if err != nil { + // Artifact doesn't exist at this registry; skip silently. + continue + } + if err := l.writeImage(ref, img, r.kind, ""); err != nil { + return saved, fmt.Errorf("saving %s for %s: %w", r.kind, ref.Name(), err) + } + if d, err := img.Digest(); err == nil { + saved[d.String()] = true + } + } + return saved, nil +} + +// parsePlatform parses a platform string in "os/arch[/variant]" format into a v1.Platform. +func parsePlatform(s string) (v1.Platform, error) { + parts := strings.SplitN(s, "/", 3) + if len(parts) < 2 { + return v1.Platform{}, fmt.Errorf("invalid platform %q: expected os/arch[/variant]", s) + } + p := v1.Platform{OS: parts[0], Architecture: parts[1]} + if len(parts) == 3 { + p.Variant = parts[2] + } + return p, nil +} + // Flush is a fancy name for delete-all-the-things, in this case it's as trivial as deleting oci-layout content // // This can be a highly destructive operation if the store's directory happens to be inline with other non-store contents @@ -170,27 +493,217 @@ func (l *Layout) Flush(ctx context.Context) error { return nil } -// Copy will copy a given reference to a given target.Target +// Copy will copy a given reference to a given content.Target // -// This is essentially a wrapper around oras.Copy, but locked to this content store -func (l *Layout) Copy(ctx context.Context, ref string, to target.Target, toRef string) (ocispec.Descriptor, error) { - return oras.Copy(ctx, l.OCI, ref, to, toRef, - oras.WithAdditionalCachedMediaTypes(consts.DockerManifestSchema2, consts.DockerManifestListSchema2)) +// This is essentially a replacement for oras.Copy, custom implementation for content stores +func (l *Layout) Copy(ctx context.Context, ref string, to content.Target, toRef string) (ocispec.Descriptor, error) { + // Resolve the source descriptor + desc, err := l.OCI.Resolve(ctx, ref) + if err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to resolve reference: %w", err) + } + + // Get fetcher and pusher + fetcher, err := l.OCI.Fetcher(ctx, ref) + if err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to get fetcher: %w", err) + } + + pusher, err := to.Pusher(ctx, toRef) + if err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to get pusher: %w", err) + } + + // Recursively copy the descriptor graph (matches oras.Copy behavior) + if err := l.copyDescriptorGraph(ctx, desc, fetcher, pusher); err != nil { + return ocispec.Descriptor{}, err + } + + return desc, nil } -// CopyAll performs bulk copy operations on the stores oci layout to a provided target.Target -func (l *Layout) CopyAll(ctx context.Context, to target.Target, toMapper func(string) (string, error)) ([]ocispec.Descriptor, error) { +// copyDescriptorGraph recursively copies a descriptor and all its referenced content +// This matches the behavior of oras.Copy by walking the entire descriptor graph +func (l *Layout) copyDescriptorGraph(ctx context.Context, desc ocispec.Descriptor, fetcher remotes.Fetcher, pusher remotes.Pusher) (err error) { + switch desc.MediaType { + case ocispec.MediaTypeImageManifest, consts.DockerManifestSchema2: + // Fetch and parse the manifest + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return fmt.Errorf("failed to fetch manifest: %w", err) + } + defer func() { + if closeErr := rc.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("failed to close manifest reader: %w", closeErr) + } + }() + + data, err := io.ReadAll(rc) + if err != nil { + return fmt.Errorf("failed to read manifest: %w", err) + } + + var manifest ocispec.Manifest + if err := json.Unmarshal(data, &manifest); err != nil { + return fmt.Errorf("failed to unmarshal manifest: %w", err) + } + + // Copy config blob + if err := l.copyDescriptor(ctx, manifest.Config, fetcher, pusher); err != nil { + return fmt.Errorf("failed to copy config: %w", err) + } + + // Copy all layer blobs + for _, layer := range manifest.Layers { + if err := l.copyDescriptor(ctx, layer, fetcher, pusher); err != nil { + return fmt.Errorf("failed to copy layer: %w", err) + } + } + + // Push the manifest itself using the already-fetched data to avoid double-fetching + if err := l.pushData(ctx, desc, data, pusher); err != nil { + return fmt.Errorf("failed to push manifest: %w", err) + } + + case ocispec.MediaTypeImageIndex, consts.DockerManifestListSchema2: + // Fetch and parse the index + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return fmt.Errorf("failed to fetch index: %w", err) + } + defer func() { + if closeErr := rc.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("failed to close index reader: %w", closeErr) + } + }() + + data, err := io.ReadAll(rc) + if err != nil { + return fmt.Errorf("failed to read index: %w", err) + } + + var index ocispec.Index + if err := json.Unmarshal(data, &index); err != nil { + return fmt.Errorf("failed to unmarshal index: %w", err) + } + + // Recursively copy each child (could be manifest or nested index) + for _, child := range index.Manifests { + if err := l.copyDescriptorGraph(ctx, child, fetcher, pusher); err != nil { + return fmt.Errorf("failed to copy child: %w", err) + } + } + + // Push the index itself using the already-fetched data to avoid double-fetching + if err := l.pushData(ctx, desc, data, pusher); err != nil { + return fmt.Errorf("failed to push index: %w", err) + } + + default: + // For other types (config blobs, layers, etc.), just copy the blob + if err := l.copyDescriptor(ctx, desc, fetcher, pusher); err != nil { + return fmt.Errorf("failed to copy descriptor: %w", err) + } + } + + return nil +} + +// copyDescriptor copies a single descriptor from source to target +func (l *Layout) copyDescriptor(ctx context.Context, desc ocispec.Descriptor, fetcher remotes.Fetcher, pusher remotes.Pusher) (err error) { + // Fetch the content + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return err + } + defer func() { + if closeErr := rc.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("failed to close reader: %w", closeErr) + } + }() + + // Get a writer from the pusher + writer, err := pusher.Push(ctx, desc) + if err != nil { + if errdefs.IsAlreadyExists(err) { + zerolog.Ctx(ctx).Debug().Msgf("existing blob: %s", desc.Digest) + return nil // content already present on remote + } + return err + } + defer func() { + if closeErr := writer.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() + + // Copy the content + n, err := io.Copy(writer, rc) + if err != nil { + return err + } + + // Commit the written content with the expected digest + if err := writer.Commit(ctx, n, desc.Digest); err != nil { + return err + } + zerolog.Ctx(ctx).Debug().Msgf("pushed blob: %s", desc.Digest) + return nil +} + +// pushData pushes already-fetched data to the pusher without re-fetching. +// This is used when we've already read the data for parsing and want to avoid double-fetching. +func (l *Layout) pushData(ctx context.Context, desc ocispec.Descriptor, data []byte, pusher remotes.Pusher) (err error) { + // Get a writer from the pusher + writer, err := pusher.Push(ctx, desc) + if err != nil { + if errdefs.IsAlreadyExists(err) { + return nil // content already present on remote + } + return fmt.Errorf("failed to get writer: %w", err) + } + defer func() { + if closeErr := writer.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("failed to close writer: %w", closeErr) + } + }() + + // Write the data using io.Copy to handle short writes properly + n, err := io.Copy(writer, bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("failed to write data: %w", err) + } + + // Commit the written content with the expected digest + return writer.Commit(ctx, n, desc.Digest) +} + +// CopyAll performs bulk copy operations on the stores oci layout to a provided target +func (l *Layout) CopyAll(ctx context.Context, to content.Target, toMapper func(string) (string, error)) ([]ocispec.Descriptor, error) { var descs []ocispec.Descriptor err := l.OCI.Walk(func(reference string, desc ocispec.Descriptor) error { - toRef := "" + // Use the clean reference from annotations (without -kind suffix) as the base + // The reference parameter from Walk is the nameMap key with format "ref-kind", + // but we need the clean ref for the destination to avoid double-appending kind + baseRef := desc.Annotations[ocispec.AnnotationRefName] + if baseRef == "" { + return fmt.Errorf("descriptor %s missing required annotation %q", reference, ocispec.AnnotationRefName) + } + toRef := baseRef if toMapper != nil { - tr, err := toMapper(reference) + tr, err := toMapper(baseRef) if err != nil { return err } toRef = tr } + // Append the digest to help the target pusher identify the root descriptor + // Format: "reference@digest" allows the pusher to update its index.json + if desc.Digest.Validate() == nil { + toRef = fmt.Sprintf("%s@%s", toRef, desc.Digest) + } + desc, err := l.Copy(ctx, reference, to, toRef) if err != nil { return err @@ -236,11 +749,6 @@ func (l *Layout) writeLayer(layer v1.Layer) error { return err } - r, err := layer.Compressed() - if err != nil { - return err - } - dir := filepath.Join(l.Root, ocispec.ImageBlobsDir, d.Algorithm) if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) { return err @@ -252,14 +760,29 @@ func (l *Layout) writeLayer(layer v1.Layer) error { return nil } + r, err := layer.Compressed() + if err != nil { + return err + } + defer r.Close() + w, err := os.Create(blobPath) if err != nil { return err } - defer w.Close() - _, err = io.Copy(w, r) - return err + _, copyErr := io.Copy(w, r) + if closeErr := w.Close(); closeErr != nil && copyErr == nil { + copyErr = closeErr + } + + // Remove a partially-written or corrupt blob on any failure so retries + // can attempt a fresh download rather than skipping the file. + if copyErr != nil { + os.Remove(blobPath) + } + + return copyErr } // Remove artifact reference from the store @@ -280,7 +803,7 @@ func (l *Layout) CleanUp(ctx context.Context) (int, int64, error) { } var processManifest func(desc ocispec.Descriptor) error - processManifest = func(desc ocispec.Descriptor) error { + processManifest = func(desc ocispec.Descriptor) (err error) { if desc.Digest.Validate() != nil { return nil } @@ -293,7 +816,11 @@ func (l *Layout) CleanUp(ctx context.Context) (int, int64, error) { if err != nil { return nil // skip if can't be read } - defer rc.Close() + defer func() { + if closeErr := rc.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() var manifest struct { Config struct { @@ -350,7 +877,7 @@ func (l *Layout) CleanUp(ctx context.Context) (int, int64, error) { } // read all entries - blobsPath := filepath.Join(l.Root, "blobs", "sha256") + blobsPath := filepath.Join(l.Root, ocispec.ImageBlobsDir, digest.Canonical.String()) entries, err := os.ReadDir(blobsPath) if err != nil { return 0, 0, fmt.Errorf("failed to read blobs directory: %w", err) diff --git a/pkg/store/store_test.go b/pkg/store/store_test.go index c7bcf76..8ae8cbd 100644 --- a/pkg/store/store_test.go +++ b/pkg/store/store_test.go @@ -1,14 +1,32 @@ package store_test import ( + "bytes" "context" + "encoding/json" + "fmt" + "io" + "net/http/httptest" "os" + "path/filepath" + "strings" "testing" + ccontent "github.com/containerd/containerd/content" + gname "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "hauler.dev/go/hauler/pkg/artifacts" + "hauler.dev/go/hauler/pkg/consts" "hauler.dev/go/hauler/pkg/store" ) @@ -17,7 +35,7 @@ var ( root string ) -func TestLayout_AddOCI(t *testing.T) { +func TestLayout_AddArtifact(t *testing.T) { teardown := setup(t) defer teardown() @@ -46,16 +64,16 @@ func TestLayout_AddOCI(t *testing.T) { } moci := genArtifact(t, tt.args.ref) - got, err := s.AddOCI(ctx, moci, tt.args.ref) + got, err := s.AddArtifact(ctx, moci, tt.args.ref) if (err != nil) != tt.wantErr { - t.Errorf("AddOCI() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("AddArtifact() error = %v, wantErr %v", err, tt.wantErr) return } _ = got - _, err = s.AddOCI(ctx, moci, tt.args.ref) + _, err = s.AddArtifact(ctx, moci, tt.args.ref) if err != nil { - t.Errorf("AddOCI() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("AddArtifact() error = %v, wantErr %v", err, tt.wantErr) return } }) @@ -103,3 +121,669 @@ func genArtifact(t *testing.T, ref string) artifacts.OCI { img, } } + +// Mock fetcher/pusher for testing +type mockFetcher struct { + blobs map[digest.Digest][]byte +} + +func newMockFetcher() *mockFetcher { + return &mockFetcher{ + blobs: make(map[digest.Digest][]byte), + } +} + +func (m *mockFetcher) addBlob(data []byte) ocispec.Descriptor { + dgst := digest.FromBytes(data) + m.blobs[dgst] = data + return ocispec.Descriptor{ + MediaType: "application/octet-stream", + Digest: dgst, + Size: int64(len(data)), + } +} + +func (m *mockFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) { + data, ok := m.blobs[desc.Digest] + if !ok { + return nil, fmt.Errorf("blob not found: %s", desc.Digest) + } + return io.NopCloser(bytes.NewReader(data)), nil +} + +type mockPusher struct { + blobs map[digest.Digest][]byte +} + +func newMockPusher() *mockPusher { + return &mockPusher{ + blobs: make(map[digest.Digest][]byte), + } +} + +func (m *mockPusher) Push(ctx context.Context, desc ocispec.Descriptor) (ccontent.Writer, error) { + return &mockWriter{ + pusher: m, + desc: desc, + buf: &bytes.Buffer{}, + }, nil +} + +type mockWriter struct { + pusher *mockPusher + desc ocispec.Descriptor + buf *bytes.Buffer + closed bool +} + +func (m *mockWriter) Write(p []byte) (int, error) { + if m.closed { + return 0, fmt.Errorf("writer closed") + } + return m.buf.Write(p) +} + +func (m *mockWriter) Close() error { + m.closed = true + return nil +} + +func (m *mockWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...ccontent.Opt) error { + data := m.buf.Bytes() + if int64(len(data)) != size { + return fmt.Errorf("size mismatch: expected %d, got %d", size, len(data)) + } + dgst := digest.FromBytes(data) + if expected != "" && dgst != expected { + return fmt.Errorf("digest mismatch: expected %s, got %s", expected, dgst) + } + m.pusher.blobs[dgst] = data + return nil +} + +func (m *mockWriter) Digest() digest.Digest { + return digest.FromBytes(m.buf.Bytes()) +} + +func (m *mockWriter) Status() (ccontent.Status, error) { + return ccontent.Status{}, nil +} + +func (m *mockWriter) Truncate(size int64) error { + return fmt.Errorf("truncate not supported") +} + +// blobPath returns the expected filesystem path for a blob in an OCI layout store. +func blobPath(root string, d digest.Digest) string { + return filepath.Join(root, "blobs", d.Algorithm().String(), d.Encoded()) +} + +// findRefKey walks the store's index and returns the nameMap key for the first +// descriptor whose AnnotationRefName matches ref. +func findRefKey(t *testing.T, s *store.Layout, ref string) string { + t.Helper() + var key string + _ = s.OCI.Walk(func(reference string, desc ocispec.Descriptor) error { + if desc.Annotations[ocispec.AnnotationRefName] == ref && key == "" { + key = reference + } + return nil + }) + if key == "" { + t.Fatalf("reference %q not found in store", ref) + } + return key +} + +// findRefKeyByKind walks the store's index and returns the nameMap key for the +// descriptor whose AnnotationRefName matches ref and whose kind annotation matches kind. +func findRefKeyByKind(t *testing.T, s *store.Layout, ref, kind string) string { + t.Helper() + var key string + _ = s.OCI.Walk(func(reference string, desc ocispec.Descriptor) error { + if desc.Annotations[ocispec.AnnotationRefName] == ref && + desc.Annotations[consts.KindAnnotationName] == kind { + key = reference + } + return nil + }) + if key == "" { + t.Fatalf("reference %q with kind %q not found in store", ref, kind) + } + return key +} + +// readManifestBlob reads and parses an OCI manifest from the store's blob directory. +func readManifestBlob(t *testing.T, root string, d digest.Digest) ocispec.Manifest { + t.Helper() + data, err := os.ReadFile(blobPath(root, d)) + if err != nil { + t.Fatalf("read manifest blob %s: %v", d, err) + } + var m ocispec.Manifest + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("unmarshal manifest: %v", err) + } + return m +} + +// TestCopyDescriptor verifies that copyDescriptor (exercised via Copy) transfers +// each individual blob — config and every layer — into the destination store's blob +// directory, and that a second Copy of the same content succeeds gracefully when +// blobs are already present (AlreadyExists path). +func TestCopyDescriptor(t *testing.T) { + teardown := setup(t) + defer teardown() + + srcRoot := t.TempDir() + src, err := store.NewLayout(srcRoot) + if err != nil { + t.Fatal(err) + } + + ref := "test/blob:v1" + // genArtifact creates random.Image(1024, 3): 1 config blob + 3 layer blobs. + manifestDesc, err := src.AddArtifact(ctx, genArtifact(t, ref), ref) + if err != nil { + t.Fatal(err) + } + if err := src.OCI.SaveIndex(); err != nil { + t.Fatal(err) + } + + refKey := findRefKey(t, src, ref) + manifest := readManifestBlob(t, srcRoot, manifestDesc.Digest) + + dstRoot := t.TempDir() + dst, err := store.NewLayout(dstRoot) + if err != nil { + t.Fatal(err) + } + + // First copy: should transfer all individual blobs via copyDescriptor. + gotDesc, err := src.Copy(ctx, refKey, dst.OCI, "test/blob:dst") + if err != nil { + t.Fatalf("Copy failed: %v", err) + } + if gotDesc.Digest != manifestDesc.Digest { + t.Errorf("returned descriptor digest mismatch: got %s, want %s", gotDesc.Digest, manifestDesc.Digest) + } + + // Verify the config blob is present in the destination. + if _, err := os.Stat(blobPath(dstRoot, manifest.Config.Digest)); err != nil { + t.Errorf("config blob missing in dest: %v", err) + } + + // Verify every layer blob is present in the destination. + for i, layer := range manifest.Layers { + if _, err := os.Stat(blobPath(dstRoot, layer.Digest)); err != nil { + t.Errorf("layer[%d] blob missing in dest: %v", i, err) + } + } + + // Verify the manifest blob itself was pushed. + if _, err := os.Stat(blobPath(dstRoot, manifestDesc.Digest)); err != nil { + t.Errorf("manifest blob missing in dest: %v", err) + } + + // Second copy: blobs already exist — AlreadyExists must be handled without error. + gotDesc2, err := src.Copy(ctx, refKey, dst.OCI, "test/blob:dst2") + if err != nil { + t.Fatalf("second Copy failed (AlreadyExists should be a no-op): %v", err) + } + if gotDesc2.Digest != manifestDesc.Digest { + t.Errorf("second Copy digest mismatch: got %s, want %s", gotDesc2.Digest, manifestDesc.Digest) + } +} + +// TestCopyDescriptorGraph_Manifest verifies that copyDescriptorGraph reconstructs a +// complete manifest in the destination (config digest and each layer digest match the +// source), and returns an error when a required blob is absent from the source. +func TestCopyDescriptorGraph_Manifest(t *testing.T) { + teardown := setup(t) + defer teardown() + + srcRoot := t.TempDir() + src, err := store.NewLayout(srcRoot) + if err != nil { + t.Fatal(err) + } + + ref := "test/manifest:v1" + manifestDesc, err := src.AddArtifact(ctx, genArtifact(t, ref), ref) + if err != nil { + t.Fatal(err) + } + if err := src.OCI.SaveIndex(); err != nil { + t.Fatal(err) + } + + refKey := findRefKey(t, src, ref) + srcManifest := readManifestBlob(t, srcRoot, manifestDesc.Digest) + + // --- Happy path: all blobs present, manifest structure preserved --- + dstRoot := t.TempDir() + dst, err := store.NewLayout(dstRoot) + if err != nil { + t.Fatal(err) + } + + gotDesc, err := src.Copy(ctx, refKey, dst.OCI, "test/manifest:dst") + if err != nil { + t.Fatalf("Copy failed: %v", err) + } + + // Parse the manifest from the destination and compare structure with source. + dstManifest := readManifestBlob(t, dstRoot, gotDesc.Digest) + if dstManifest.Config.Digest != srcManifest.Config.Digest { + t.Errorf("config digest mismatch: got %s, want %s", + dstManifest.Config.Digest, srcManifest.Config.Digest) + } + if len(dstManifest.Layers) != len(srcManifest.Layers) { + t.Fatalf("layer count mismatch: dst=%d src=%d", + len(dstManifest.Layers), len(srcManifest.Layers)) + } + for i, l := range srcManifest.Layers { + if dstManifest.Layers[i].Digest != l.Digest { + t.Errorf("layer[%d] digest mismatch: got %s, want %s", + i, dstManifest.Layers[i].Digest, l.Digest) + } + } + + // --- Error path: delete a layer blob from source, expect Copy to fail --- + if len(srcManifest.Layers) == 0 { + t.Skip("artifact has no layers; skipping missing-blob error path") + } + if err := os.Remove(blobPath(srcRoot, srcManifest.Layers[0].Digest)); err != nil { + t.Fatalf("could not remove layer blob to simulate corruption: %v", err) + } + + dst2Root := t.TempDir() + dst2, err := store.NewLayout(dst2Root) + if err != nil { + t.Fatal(err) + } + + _, err = src.Copy(ctx, refKey, dst2.OCI, "test/manifest:missing-blob") + if err == nil { + t.Error("expected Copy to fail when a source layer blob is missing, but it succeeded") + } +} + +// TestCopyDescriptorGraph_Index verifies that copyDescriptorGraph handles an OCI +// image index (multi-platform) by recursively copying all child manifests and their +// blobs into the destination store, and that the index blob itself is present. +func TestCopyDescriptorGraph_Index(t *testing.T) { + teardown := setup(t) + defer teardown() + + // Start an in-process OCI registry. + srv := httptest.NewServer(registry.New()) + t.Cleanup(srv.Close) + host := strings.TrimPrefix(srv.URL, "http://") + remoteOpts := []remote.Option{remote.WithTransport(srv.Client().Transport)} + + // Build a 2-platform image index. + img1, err := random.Image(512, 2) + if err != nil { + t.Fatalf("random image (amd64): %v", err) + } + img2, err := random.Image(512, 2) + if err != nil { + t.Fatalf("random image (arm64): %v", err) + } + idx := mutate.AppendManifests( + empty.Index, + mutate.IndexAddendum{ + Add: img1, + Descriptor: v1.Descriptor{ + MediaType: types.OCIManifestSchema1, + Platform: &v1.Platform{OS: "linux", Architecture: "amd64"}, + }, + }, + mutate.IndexAddendum{ + Add: img2, + Descriptor: v1.Descriptor{ + MediaType: types.OCIManifestSchema1, + Platform: &v1.Platform{OS: "linux", Architecture: "arm64"}, + }, + }, + ) + + idxTag, err := gname.NewTag(host+"/test/multiarch:v1", gname.Insecure) + if err != nil { + t.Fatalf("new tag: %v", err) + } + if err := remote.WriteIndex(idxTag, idx, remoteOpts...); err != nil { + t.Fatalf("push index: %v", err) + } + + // Pull the index into a hauler store via AddImage. + srcRoot := t.TempDir() + src, err := store.NewLayout(srcRoot) + if err != nil { + t.Fatal(err) + } + if err := src.AddImage(ctx, idxTag.Name(), "", remoteOpts...); err != nil { + t.Fatalf("AddImage: %v", err) + } + if err := src.OCI.SaveIndex(); err != nil { + t.Fatal(err) + } + + // Locate the index descriptor (kind=imageIndex) in the source store. + refKey := findRefKeyByKind(t, src, "test/multiarch:v1", consts.KindAnnotationIndex) + + // Copy the entire index graph to a fresh destination store. + dstRoot := t.TempDir() + dst, err := store.NewLayout(dstRoot) + if err != nil { + t.Fatal(err) + } + gotDesc, err := src.Copy(ctx, refKey, dst.OCI, "test/multiarch:copied") + if err != nil { + t.Fatalf("Copy of image index failed: %v", err) + } + + // The index blob itself must be present in the destination. + if _, err := os.Stat(blobPath(dstRoot, gotDesc.Digest)); err != nil { + t.Errorf("index manifest blob missing in dest: %v", err) + } + + // Parse the index from the source and verify every child manifest blob landed + // in the destination (exercising recursive copyDescriptorGraph for each child). + var ociIdx ocispec.Index + if err := json.Unmarshal(mustReadFile(t, blobPath(srcRoot, gotDesc.Digest)), &ociIdx); err != nil { + t.Fatalf("unmarshal index: %v", err) + } + if len(ociIdx.Manifests) < 2 { + t.Fatalf("expected ≥2 child manifests in index, got %d", len(ociIdx.Manifests)) + } + for i, child := range ociIdx.Manifests { + if _, err := os.Stat(blobPath(dstRoot, child.Digest)); err != nil { + t.Errorf("child manifest[%d] (platform=%v) blob missing in dest: %v", + i, child.Platform, err) + } + } +} + +// mustReadFile reads a file and fails the test on error. +func mustReadFile(t *testing.T, path string) []byte { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return data +} + +// TestCopy_Integration tests the full Copy workflow including copyDescriptorGraph +func TestCopy_Integration(t *testing.T) { + teardown := setup(t) + defer teardown() + + // Create source store + sourceRoot, err := os.MkdirTemp("", "hauler-source") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(sourceRoot) + + sourceStore, err := store.NewLayout(sourceRoot) + if err != nil { + t.Fatal(err) + } + + // Add an artifact to source + ref := "test/image:v1" + artifact := genArtifact(t, ref) + _, err = sourceStore.AddArtifact(ctx, artifact, ref) + if err != nil { + t.Fatal(err) + } + + // Save the index to persist the reference + if err := sourceStore.OCI.SaveIndex(); err != nil { + t.Fatalf("Failed to save index: %v", err) + } + + // Find the actual reference key in the nameMap (includes kind suffix) + var sourceRefKey string + err = sourceStore.OCI.Walk(func(reference string, desc ocispec.Descriptor) error { + if desc.Annotations[ocispec.AnnotationRefName] == ref { + sourceRefKey = reference + } + return nil + }) + if err != nil { + t.Fatalf("Failed to walk source store: %v", err) + } + if sourceRefKey == "" { + t.Fatal("Failed to find reference in source store") + } + + // Create destination store + destRoot, err := os.MkdirTemp("", "hauler-dest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(destRoot) + + destStore, err := store.NewLayout(destRoot) + if err != nil { + t.Fatal(err) + } + + // Copy from source to destination + destRef := "test/image:copied" + desc, err := sourceStore.Copy(ctx, sourceRefKey, destStore.OCI, destRef) + if err != nil { + t.Fatalf("Copy failed: %v", err) + } + + // Copy doesn't automatically add to destination index for generic targets + // For OCI stores, we need to add the descriptor manually with the reference + desc.Annotations = map[string]string{ + ocispec.AnnotationRefName: destRef, + consts.KindAnnotationName: consts.KindAnnotationImage, + } + if err := destStore.OCI.AddIndex(desc); err != nil { + t.Fatalf("Failed to add descriptor to destination index: %v", err) + } + + // Verify the descriptor was copied + if desc.Digest == "" { + t.Error("Expected non-empty digest") + } + + // Find the copied reference in destination + var foundInDest bool + var destDesc ocispec.Descriptor + err = destStore.OCI.Walk(func(reference string, d ocispec.Descriptor) error { + if d.Digest == desc.Digest { + foundInDest = true + destDesc = d + } + return nil + }) + if err != nil { + t.Fatalf("Failed to walk destination store: %v", err) + } + + if !foundInDest { + t.Error("Copied descriptor not found in destination store") + } + + if destDesc.Digest != desc.Digest { + t.Errorf("Digest mismatch: got %s, want %s", destDesc.Digest, desc.Digest) + } +} + +// TestCopy_ErrorHandling tests error cases +func TestCopy_ErrorHandling(t *testing.T) { + teardown := setup(t) + defer teardown() + + sourceRoot, err := os.MkdirTemp("", "hauler-source") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(sourceRoot) + + sourceStore, err := store.NewLayout(sourceRoot) + if err != nil { + t.Fatal(err) + } + + destRoot, err := os.MkdirTemp("", "hauler-dest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(destRoot) + + destStore, err := store.NewLayout(destRoot) + if err != nil { + t.Fatal(err) + } + + // Test copying non-existent reference + _, err = sourceStore.Copy(ctx, "nonexistent:tag", destStore.OCI, "dest:tag") + if err == nil { + t.Error("Expected error when copying non-existent reference") + } +} + +// TestCopy_DockerFormats tests copying Docker manifest formats +func TestCopy_DockerFormats(t *testing.T) { + // This test verifies that Docker format media types are recognized + // The actual copying is tested in the integration test + if consts.DockerManifestSchema2 == "" { + t.Error("DockerManifestSchema2 constant should not be empty") + } + t.Skip("Docker format copying is tested via integration tests") +} + +// TestCopy_MultiPlatform tests copying multi-platform images with manifest lists +func TestCopy_MultiPlatform(t *testing.T) { + teardown := setup(t) + defer teardown() + + sourceRoot, err := os.MkdirTemp("", "hauler-source") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(sourceRoot) + + // This test would require creating a multi-platform image + // which is more complex - marking as future enhancement + t.Skip("Multi-platform image test requires additional setup") +} + +// TestAddImage_OCI11Referrers verifies that AddImage captures OCI 1.1 referrers +// (cosign v3 new-bundle-format) stored via the subject field rather than the legacy +// sha256-.sig/.att/.sbom tag convention. +// +// The test: +// 1. Starts an in-process OCI 1.1–capable registry (go-containerregistry/pkg/registry) +// 2. Pushes a random base image to it +// 3. Builds a synthetic cosign v3-style Sigstore bundle referrer manifest (with a +// "subject" field pointing at the base image) and pushes it so the registry +// registers it in the referrers index automatically +// 4. Calls store.AddImage and then walks the OCI layout to confirm that a +// KindAnnotationReferrers-prefixed entry was saved +func TestAddImage_OCI11Referrers(t *testing.T) { + // 1. Start an in-process OCI 1.1 registry. + srv := httptest.NewServer(registry.New()) + t.Cleanup(srv.Close) + host := strings.TrimPrefix(srv.URL, "http://") + + remoteOpts := []remote.Option{ + remote.WithTransport(srv.Client().Transport), + } + + // 2. Push a random base image. + baseTag, err := gname.NewTag(host+"/test/image:v1", gname.Insecure) + if err != nil { + t.Fatalf("new tag: %v", err) + } + baseImg, err := random.Image(512, 2) + if err != nil { + t.Fatalf("random image: %v", err) + } + if err := remote.Write(baseTag, baseImg, remoteOpts...); err != nil { + t.Fatalf("push base image: %v", err) + } + + // Build the v1.Descriptor for the base image so we can set it as the referrer subject. + baseHash, err := baseImg.Digest() + if err != nil { + t.Fatalf("base image digest: %v", err) + } + baseRawManifest, err := baseImg.RawManifest() + if err != nil { + t.Fatalf("base image raw manifest: %v", err) + } + baseMT, err := baseImg.MediaType() + if err != nil { + t.Fatalf("base image media type: %v", err) + } + baseDesc := v1.Descriptor{ + MediaType: baseMT, + Digest: baseHash, + Size: int64(len(baseRawManifest)), + } + + // 3. Build a synthetic cosign v3 Sigstore bundle referrer. + // + // Real cosign new-bundle-format: artifactType=application/vnd.dev.sigstore.bundle.v0.3+json, + // config.mediaType=application/vnd.oci.empty.v1+json, single layer containing the bundle JSON, + // and a "subject" field pointing at the base image digest. + bundleJSON := []byte(`{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json",` + + `"verificationMaterial":{},"messageSignature":{"messageDigest":` + + `{"algorithm":"SHA2_256","digest":"AAAA"},"signature":"AAAA"}}`) + bundleLayer := static.NewLayer(bundleJSON, types.MediaType(consts.SigstoreBundleMediaType)) + + referrerImg, err := mutate.AppendLayers(empty.Image, bundleLayer) + if err != nil { + t.Fatalf("append bundle layer: %v", err) + } + referrerImg = mutate.MediaType(referrerImg, types.OCIManifestSchema1) + referrerImg = mutate.ConfigMediaType(referrerImg, types.MediaType(consts.OCIEmptyConfigMediaType)) + referrerImg = mutate.Subject(referrerImg, baseDesc).(v1.Image) + + // Push the referrer under an arbitrary tag; the in-process registry auto-wires the + // subject field and makes the manifest discoverable via GET /v2/.../referrers/. + referrerTag, err := gname.NewTag(host+"/test/image:bundle-referrer", gname.Insecure) + if err != nil { + t.Fatalf("referrer tag: %v", err) + } + if err := remote.Write(referrerTag, referrerImg, remoteOpts...); err != nil { + t.Fatalf("push referrer: %v", err) + } + + // 4. Let hauler add the base image (which should also fetch its OCI referrers). + storeRoot := t.TempDir() + s, err := store.NewLayout(storeRoot) + if err != nil { + t.Fatalf("new layout: %v", err) + } + if err := s.AddImage(context.Background(), baseTag.Name(), "", remoteOpts...); err != nil { + t.Fatalf("AddImage: %v", err) + } + + // 5. Walk the store and verify that at least one referrer entry was captured. + var referrerCount int + if err := s.Walk(func(_ string, desc ocispec.Descriptor) error { + if strings.HasPrefix(desc.Annotations[consts.KindAnnotationName], consts.KindAnnotationReferrers) { + referrerCount++ + } + return nil + }); err != nil { + t.Fatalf("Walk: %v", err) + } + + if referrerCount == 0 { + t.Fatal("expected at least one OCI referrer entry in the store, got none") + } + t.Logf("captured %d OCI referrer(s) for %s", referrerCount, baseTag.Name()) +}