diff --git a/go.mod b/go.mod index ca5ffce88..3ac34e9ae 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/deckhouse/deckhouse/go_lib/controlplane v0.0.0-20260602082302-91957e5e124e github.com/deckhouse/deckhouse/pkg/log v0.2.0 - github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260414112803-53a5662881d9 + github.com/deckhouse/deckhouse/pkg/registry v0.0.1 github.com/deckhouse/virtualization/src/cli v1.11.1 github.com/fatih/color v1.19.0 github.com/fluxcd/flagger v1.36.1 diff --git a/go.sum b/go.sum index e3de2745e..1898f4483 100644 --- a/go.sum +++ b/go.sum @@ -440,8 +440,8 @@ github.com/deckhouse/deckhouse/go_lib/controlplane v0.0.0-20260602082302-91957e5 github.com/deckhouse/deckhouse/go_lib/controlplane v0.0.0-20260602082302-91957e5e124e/go.mod h1:LXzIV70U2qmIKPwLHqreCT8dbfYV1w6Qn2Tdb+jU5gY= github.com/deckhouse/deckhouse/pkg/log v0.2.0 h1:6tmZQLwNb1o/hP1gzJQBjcwfA/bubbgObovXzxq+Exo= github.com/deckhouse/deckhouse/pkg/log v0.2.0/go.mod h1:pbAxTSDcPmwyl3wwKDcEB3qdxHnRxqTV+J0K+sha8bw= -github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260414112803-53a5662881d9 h1:Il2d6wB6SdgjmD5ojC48qT9eQyITuANzIFjSd0DCdUI= -github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260414112803-53a5662881d9/go.mod h1:KDf44MqEif8jAKCehKJqOg0k4sJcnetKJKDGd0IFQjI= +github.com/deckhouse/deckhouse/pkg/registry v0.0.1 h1:Ob6NpHRsVwHci1uK+6wLZ1NQxr5QS+e5ergFIkt7odI= +github.com/deckhouse/deckhouse/pkg/registry v0.0.1/go.mod h1:KDf44MqEif8jAKCehKJqOg0k4sJcnetKJKDGd0IFQjI= github.com/deckhouse/delivery-kit-sdk v1.2.0 h1:EuaaWVVe5GEe1xkKMPHo1rT1dz+wkiQ78R6sBd6oXPs= github.com/deckhouse/delivery-kit-sdk v1.2.0/go.mod h1:HgPk8RNOxcy1FZ37pB9RMQRdYHa13pGYAmKnLJh0I9A= github.com/deckhouse/delivery-kit/v2 v2.69.0-dk h1:00PJzpfVObzurArl1oxy+/VYwFfMhxSa52n74l0efVw= diff --git a/internal/cr/cmd/basic/digest.go b/internal/cr/cmd/basic/digest.go index 0ae99c512..dbe14a364 100644 --- a/internal/cr/cmd/basic/digest.go +++ b/internal/cr/cmd/basic/digest.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" - "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" "github.com/deckhouse/deckhouse-cli/internal/cr/cmd/completion" @@ -66,9 +65,9 @@ selects an entry by tag (the first entry is used if omitted). } // fullRef branch is reachable only when tarballPath == "" (rejected above) // and len(args) > 0 (rejected above when tarballPath is also empty). - ref, err := name.ParseReference(args[0], opts.Name...) + ref, err := registry.ParseReference(args[0], opts) if err != nil { - return fmt.Errorf("parse reference %q: %w", args[0], err) + return err } _, err = fmt.Fprintln(w, ref.Context().Digest(digest)) diff --git a/internal/cr/cmd/basic/ls.go b/internal/cr/cmd/basic/ls.go index 1a13cc831..2d1253fe7 100644 --- a/internal/cr/cmd/basic/ls.go +++ b/internal/cr/cmd/basic/ls.go @@ -56,9 +56,9 @@ func runLs(ctx context.Context, w io.Writer, src string, fullRef, omitDigestTags var repo name.Repository if fullRef { - r, err := name.NewRepository(src, opts.Name...) + r, err := registry.ParseRepository(src, opts) if err != nil { - return fmt.Errorf("parse repository %q: %w", src, err) + return err } repo = r diff --git a/internal/cr/cmd/basic/push.go b/internal/cr/cmd/basic/push.go index dfaeaa8f1..dfc8c8951 100644 --- a/internal/cr/cmd/basic/push.go +++ b/internal/cr/cmd/basic/push.go @@ -22,7 +22,6 @@ import ( "io" "os" - "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" "github.com/deckhouse/deckhouse-cli/internal/cr/cmd/completion" @@ -60,9 +59,9 @@ func runPush(ctx context.Context, w io.Writer, path, tagRef string, asIndex bool // Validate tagRef before reading any OCI layout from disk - layouts can // be tens of GB, and a typo in the destination ref should not require // loading the source first. - parsed, err := name.ParseReference(tagRef, opts.Name...) + parsed, err := registry.ParseReference(tagRef, opts) if err != nil { - return fmt.Errorf("parse reference %q: %w", tagRef, err) + return err } obj, err := imageio.LoadLocal(path, asIndex) diff --git a/internal/cr/cmd/completion/completion.go b/internal/cr/cmd/completion/completion.go index 2b580442e..fd1676a18 100644 --- a/internal/cr/cmd/completion/completion.go +++ b/internal/cr/cmd/completion/completion.go @@ -310,7 +310,7 @@ func completionContext(cmd *cobra.Command) (context.Context, context.CancelFunc) func buildCompletionOpts(cmd *cobra.Command) *registry.Options { opts := registry.New() if insecure, err := cmd.Flags().GetBool(rootflagnames.Insecure); err == nil && insecure { - opts.WithInsecure().WithTransport(registry.InsecureTransport()) + opts.WithInsecure() } if platform, err := cmd.Flags().GetString(rootflagnames.Platform); err == nil && platform != "" { diff --git a/internal/cr/cmd/integration_test.go b/internal/cr/cmd/integration_test.go index 9831dd3eb..21cfc1792 100644 --- a/internal/cr/cmd/integration_test.go +++ b/internal/cr/cmd/integration_test.go @@ -37,8 +37,10 @@ import ( 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/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" cr "github.com/deckhouse/deckhouse-cli/internal/cr/cmd" ) @@ -586,3 +588,113 @@ func TestIntegration_PushRoundTrip(t *testing.T) { } }) } + +// ---------- multi-arch ---------- + +// The global --platform used to be silently ignored by manifest and digest: +// both went through the remote descriptor, which is the index itself, so a user +// pinning a deployment got the index digest instead of the digest of the image +// that would actually run. +func TestIntegration_PlatformResolvesIndex(t *testing.T) { + env := setupEnv(t) + ref := env.Host + "/multiarch:v1" + + base, err := random.Index(1, 1, 0) + if err != nil { + t.Fatalf("random.Index: %v", err) + } + + idx := mutate.IndexMediaType( + mutate.AppendManifests(base, platformImage(t, "amd64"), platformImage(t, "arm64")), + types.OCIImageIndex, + ) + + parsed, err := name.ParseReference(ref, name.Insecure) + if err != nil { + t.Fatalf("parse %s: %v", ref, err) + } + + if err := remote.WriteIndex(parsed, idx); err != nil { + t.Fatalf("write index: %v", err) + } + + indexDigest, err := idx.Digest() + if err != nil { + t.Fatalf("index digest: %v", err) + } + + manifests, err := idx.IndexManifest() + if err != nil { + t.Fatalf("index manifest: %v", err) + } + + want := map[string]string{} + for _, child := range manifests.Manifests { + want[child.Platform.Architecture] = child.Digest.String() + } + + t.Run("digest without a platform is the index", func(t *testing.T) { + got := strings.TrimSpace(mustRun(t, "digest", ref)) + if got != indexDigest.String() { + t.Errorf("digest = %s, want the index %s", got, indexDigest) + } + }) + + t.Run("digest with a platform is that child", func(t *testing.T) { + got := strings.TrimSpace(mustRun(t, "--platform", "linux/arm64", "digest", ref)) + if got != want["arm64"] { + t.Errorf("digest = %s, want the arm64 child %s", got, want["arm64"]) + } + + if got == indexDigest.String() { + t.Errorf("--platform was ignored: got the index digest") + } + }) + + t.Run("manifest with a platform is an image manifest", func(t *testing.T) { + out := mustRun(t, "--platform", "linux/arm64", "manifest", ref) + + var probe struct { + MediaType string `json:"mediaType"` + } + if err := json.Unmarshal([]byte(out), &probe); err != nil { + t.Fatalf("unmarshal manifest: %v", err) + } + + if types.MediaType(probe.MediaType).IsIndex() { + t.Errorf("--platform was ignored: still an index (%s)", probe.MediaType) + } + }) + + t.Run("a platform the index does not carry fails", func(t *testing.T) { + mustFail(t, "--platform", "windows/amd64", "digest", ref) + }) +} + +// platformImage builds an index entry whose config and descriptor both name the +// given architecture, so --platform has something real to match on. +func platformImage(t *testing.T, arch string) mutate.IndexAddendum { + t.Helper() + + img, err := random.Image(64, 1) + if err != nil { + t.Fatalf("random.Image: %v", err) + } + + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("ConfigFile: %v", err) + } + + cfg.OS, cfg.Architecture = "linux", arch + + img, err = mutate.ConfigFile(img, cfg) + if err != nil { + t.Fatalf("mutate.ConfigFile: %v", err) + } + + return mutate.IndexAddendum{ + Add: img, + Descriptor: v1.Descriptor{Platform: &v1.Platform{OS: "linux", Architecture: arch}}, + } +} diff --git a/internal/cr/cmd/rootflags.go b/internal/cr/cmd/rootflags.go index 56f14c2ff..71e922cac 100644 --- a/internal/cr/cmd/rootflags.go +++ b/internal/cr/cmd/rootflags.go @@ -21,6 +21,7 @@ import ( "io" "os" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/logs" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/spf13/cobra" @@ -65,11 +66,15 @@ func setupRootFlags(cmd *cobra.Command, opts *registry.Options) { // the same command, an embedder running PersistentPreRunE twice) // can never double-append to opts.Remote / opts.Name. *opts = *registry.New() - opts.WithContext(c.Context()) + applyVerbose(verbose) + if verbose { + opts.WithVerbose() + } + if insecure { - opts.WithInsecure().WithTransport(registry.InsecureTransport()) + opts.WithInsecure() } if ndLayers { @@ -95,7 +100,14 @@ func setupRootFlags(cmd *cobra.Command, opts *registry.Options) { if c.Name() != "login" { switch { case username != "" && password != "": - opts.WithKeychain(registry.NewStaticKeychain(username, password)) + // An authenticator rather than a keychain: the registry client + // is built per reference, so these credentials reach only the + // registry the user actually named. The old static keychain + // ignored the requested resource and answered for any host. + opts.WithAuth(authn.FromConfig(authn.AuthConfig{ + Username: username, + Password: password, + })) case username != "" || password != "": return fmt.Errorf("--%s and --%s must be used together", rootflagnames.Username, rootflagnames.Password) } @@ -105,10 +117,13 @@ func setupRootFlags(cmd *cobra.Command, opts *registry.Options) { } } -// applyVerbose toggles go-containerregistry's debug logger. logs.Debug is a -// package-level *log.Logger, so we must explicitly route to io.Discard when -// verbose is off - otherwise a previous "-v" run in the same process would -// keep leaking debug output. +// applyVerbose toggles go-containerregistry's debug logger, which sits below +// the registry client and logs redirects, retries and token exchanges. +// +// logs.Debug is a package-level *log.Logger, so we must explicitly route to +// io.Discard when verbose is off - otherwise a previous "-v" run in the same +// process would keep leaking debug output. The client's own structured log is +// configured separately, via Options.WithVerbose. func applyVerbose(verbose bool) { if verbose { logs.Debug.SetOutput(os.Stderr) diff --git a/internal/cr/cmd/rootflags_test.go b/internal/cr/cmd/rootflags_test.go index dcbacec5f..1f20e5f33 100644 --- a/internal/cr/cmd/rootflags_test.go +++ b/internal/cr/cmd/rootflags_test.go @@ -17,10 +17,9 @@ limitations under the License. package cr import ( - "reflect" + "strings" "testing" - "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" "github.com/deckhouse/deckhouse-cli/internal/cr/internal/registry" @@ -31,74 +30,156 @@ import ( // can inspect the side-effects of the flag. func runPreRun(t *testing.T, args []string) *registry.Options { t.Helper() + opts := registry.New() cmd := &cobra.Command{Use: "cr"} setupRootFlags(cmd, opts) + if err := cmd.ParseFlags(args); err != nil { t.Fatalf("ParseFlags: %v", err) } + if err := cmd.PersistentPreRunE(cmd, nil); err != nil { t.Fatalf("PersistentPreRunE: %v", err) } - return opts -} -// hasInsecureName looks for the exact name.Insecure marker in opts.Name. -// name.Option is a func type, so values are not directly comparable - we -// compare function pointers via reflect. This is more precise than counting -// slice length, which would silently break if a future default option were -// added to New(). -func hasInsecureName(opts *registry.Options) bool { - want := reflect.ValueOf(name.Insecure).Pointer() - for _, opt := range opts.Name { - if reflect.ValueOf(opt).Pointer() == want { - return true - } - } - return false + return opts } func TestInsecureFlag_Off(t *testing.T) { opts := runPreRun(t, nil) - if hasInsecureName(opts) { - t.Fatalf("expected insecure off when --insecure is not passed") + if opts.PlainHTTP || opts.TLSSkipVerify { + t.Fatalf("expected TLS to be enforced without --insecure; got %+v", opts) } } +// --insecure has always meant both "talk HTTP" and "accept any certificate". +// The client separates the two, so this guards the flag against quietly +// covering only one of them if it is ever split. func TestInsecureFlag_On(t *testing.T) { opts := runPreRun(t, []string{"--insecure"}) - if !hasInsecureName(opts) { - t.Fatalf("expected insecure on when --insecure is passed") + if !opts.PlainHTTP { + t.Errorf("--insecure should permit plain HTTP") + } + + if !opts.TLSSkipVerify { + t.Errorf("--insecure should skip TLS verification") + } +} + +func TestPlatformFlag(t *testing.T) { + opts := runPreRun(t, []string{"--platform", "linux/arm64/v8"}) + if opts.Platform == nil { + t.Fatalf("--platform was not applied") + } + + if got := opts.Platform.String(); got != "linux/arm64/v8" { + t.Errorf("platform = %q, want linux/arm64/v8", got) + } +} + +func TestPlatformFlag_RejectsGarbage(t *testing.T) { + opts := registry.New() + cmd := &cobra.Command{Use: "cr"} + setupRootFlags(cmd, opts) + + if err := cmd.ParseFlags([]string{"--platform", "not//a//platform"}); err != nil { + t.Fatalf("ParseFlags: %v", err) + } + + if err := cmd.PersistentPreRunE(cmd, nil); err == nil { + t.Fatalf("expected an error for an unparseable --platform") } } -// PersistentPreRunE must reset opts before applying flag-driven mutators, -// so re-entry (test harness, embedder, retry) cannot double-append to -// opts.Remote / opts.Name and end up with a malformed merge of options. +// Credentials become an authenticator rather than a keychain, so they reach +// only the registry the user named: the client is built per reference, and the +// old static keychain answered for any host it was asked about. +func TestCredentialFlags_BecomeAuth(t *testing.T) { + opts := runPreRun(t, []string{"--username", "robot", "--password", "s3cret"}) + if opts.Auth == nil { + t.Fatalf("--username/--password should install an authenticator") + } + + cfg, err := opts.Auth.Authorization() + if err != nil { + t.Fatalf("Authorization: %v", err) + } + + if cfg.Username != "robot" || cfg.Password != "s3cret" { + t.Errorf("authenticator carries %+v, want robot/s3cret", cfg) + } +} + +// Half a credential pair would otherwise be dropped silently and the command +// would fall back to the Docker config, masking the mistake with a 401. +func TestCredentialFlags_MustBePaired(t *testing.T) { + for _, args := range [][]string{{"--username", "robot"}, {"--password", "s3cret"}} { + opts := registry.New() + cmd := &cobra.Command{Use: "cr"} + setupRootFlags(cmd, opts) + + if err := cmd.ParseFlags(args); err != nil { + t.Fatalf("ParseFlags: %v", err) + } + + err := cmd.PersistentPreRunE(cmd, nil) + if err == nil { + t.Fatalf("%v: expected an error for a lone credential flag", args) + } + + if !strings.Contains(err.Error(), "must be used together") { + t.Errorf("%v: unexpected error: %v", args, err) + } + } +} + +// login reads the credential flags itself and prompts for whichever half is +// missing, so the pairing rule must not apply to it. +func TestCredentialFlags_LoginIsExempt(t *testing.T) { + opts := registry.New() + root := &cobra.Command{Use: "cr"} + setupRootFlags(root, opts) + + login := &cobra.Command{Use: "login"} + root.AddCommand(login) + + if err := root.ParseFlags([]string{"--username", "robot"}); err != nil { + t.Fatalf("ParseFlags: %v", err) + } + + if err := root.PersistentPreRunE(login, nil); err != nil { + t.Errorf("login must tolerate a lone --username: %v", err) + } +} + +// PersistentPreRunE resets opts before applying the flags, so re-entry (a test +// harness, an embedder, a retry) starts from a clean state instead of merging +// into whatever a previous run left behind. func TestPersistentPreRunE_IsIdempotent(t *testing.T) { opts := registry.New() cmd := &cobra.Command{Use: "cr"} setupRootFlags(cmd, opts) - if err := cmd.ParseFlags([]string{"--insecure"}); err != nil { + + if err := cmd.ParseFlags([]string{"--insecure", "--platform", "linux/arm64"}); err != nil { t.Fatalf("ParseFlags: %v", err) } if err := cmd.PersistentPreRunE(cmd, nil); err != nil { t.Fatalf("first PersistentPreRunE: %v", err) } - firstName := len(opts.Name) - firstRemote := len(opts.Remote) + + first := *opts if err := cmd.PersistentPreRunE(cmd, nil); err != nil { t.Fatalf("second PersistentPreRunE: %v", err) } - if got := len(opts.Name); got != firstName { - t.Errorf("opts.Name grew on re-entry: was %d, now %d", firstName, got) - } - if got := len(opts.Remote); got != firstRemote { - t.Errorf("opts.Remote grew on re-entry: was %d, now %d", firstRemote, got) + + if opts.PlainHTTP != first.PlainHTTP || opts.TLSSkipVerify != first.TLSSkipVerify { + t.Errorf("insecure flags drifted across re-entry: %+v then %+v", first, *opts) } - if !hasInsecureName(opts) { - t.Errorf("insecure flag was lost across re-entry") + + if opts.Platform == nil || opts.Platform.String() != first.Platform.String() { + t.Errorf("platform drifted across re-entry: %+v then %+v", first.Platform, opts.Platform) } } diff --git a/internal/cr/internal/image/resolve.go b/internal/cr/internal/image/resolve.go index 10d3d796a..11b46e81b 100644 --- a/internal/cr/internal/image/resolve.go +++ b/internal/cr/internal/image/resolve.go @@ -76,31 +76,37 @@ func Resolve(ctx context.Context, srcList []string, keepMultiArchIndex bool, cac } for _, src := range srcList { - desc, err := registry.FetchDescriptor(ctx, src, opts) - if err != nil { - return nil, err - } - - if keepMultiArchIndex && desc.MediaType.IsIndex() && opts.Platform == nil { - idx, err := desc.ImageIndex() + // The index branch costs one extra manifest GET (classify, then fetch), + // which is cheap next to the layer traffic that follows and keeps the + // domain surface to "give me an image" / "give me an index" rather than + // leaking a registry descriptor to callers. + if keepMultiArchIndex && opts.Platform == nil { + isIndex, err := registry.IsIndex(ctx, src, opts) if err != nil { - return nil, fmt.Errorf("read index %s: %w", src, err) + return nil, err } - if fsCache != nil { - // Without this, --cache-path was a no-op for OCI pulls of - // multi-arch images (the most common shape, e.g. alpine). - idx = cache.ImageIndex(idx, fsCache) - } + if isIndex { + idx, err := registry.FetchIndex(ctx, src, opts) + if err != nil { + return nil, err + } + + if fsCache != nil { + // Without this, --cache-path was a no-op for OCI pulls of + // multi-arch images (the most common shape, e.g. alpine). + idx = cache.ImageIndex(idx, fsCache) + } - out.Indices[src] = idx + out.Indices[src] = idx - continue + continue + } } - img, err := desc.Image() + img, err := registry.Fetch(ctx, src, opts) if err != nil { - return nil, fmt.Errorf("read image %s: %w", src, err) + return nil, err } if fsCache != nil { diff --git a/internal/cr/internal/image/resolve_test.go b/internal/cr/internal/image/resolve_test.go index 34ca1bb33..5fe9925a8 100644 --- a/internal/cr/internal/image/resolve_test.go +++ b/internal/cr/internal/image/resolve_test.go @@ -18,88 +18,101 @@ package image_test import ( "context" - "net/http/httptest" "os" "strings" "testing" - "github.com/google/go-containerregistry/pkg/name" - regsrv "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/types" + dkpreg "github.com/deckhouse/deckhouse/pkg/registry" + dkpclient "github.com/deckhouse/deckhouse/pkg/registry/client" + upfake "github.com/deckhouse/deckhouse/pkg/registry/fake" + "github.com/deckhouse/deckhouse-cli/internal/cr/internal/image" "github.com/deckhouse/deckhouse-cli/internal/cr/internal/registry" ) -// resolveTestEnv stands up an in-memory registry with a single-arch image and -// a multi-arch index already pushed under fixed tags. +const resolveHost = "registry.example.com" + +// resolveTestEnv holds an in-memory registry with a single-arch image and a +// multi-arch index under fixed tags, plus Options wired to reach it. +// +// The fake replaces the HTTP registry these tests used to stand up: Resolve is +// classification and cache-wrapping logic, so the wire adds seconds per test +// and, as the completion suite records, timeout flake under parallel +// `go test ./...` load. The layer that does need real HTTP is covered by +// cmd/integration_test.go. type resolveTestEnv struct { - imageRef string // host/app:single - simple manifest - indexRef string // host/app:multi - OCI index with linux/amd64 + linux/arm64 + opts *registry.Options + imageRef string // single-manifest image + indexRef string // OCI index with linux/amd64 + linux/arm64 } func setupResolveEnv(t *testing.T) *resolveTestEnv { t.Helper() - srv := httptest.NewServer(regsrv.New()) - t.Cleanup(srv.Close) - host := strings.TrimPrefix(srv.URL, "http://") - img, err := random.Image(64, 1) - if err != nil { - t.Fatalf("random.Image: %v", err) - } - imgRef, err := name.ParseReference(host+"/app:single", name.Insecure) - if err != nil { - t.Fatalf("parse image ref: %v", err) - } - if err := remote.Write(imgRef, img); err != nil { - t.Fatalf("push image: %v", err) + reg := upfake.NewRegistry(resolveHost) + reg.MustAddImage("app", "single", randomImage(t, 64)) + reg.MustAddIndex("app", "multi", multiArchIndex(t)) + + opts := registry.New() + opts.ClientFactory = func(string, ...dkpclient.Option) dkpreg.Client { + return upfake.NewClient(reg) } - imgAmd, err := random.Image(32, 1) - if err != nil { - t.Fatalf("random.Image amd64: %v", err) + return &resolveTestEnv{ + opts: opts, + imageRef: resolveHost + "/app:single", + indexRef: resolveHost + "/app:multi", } - imgArm, err := random.Image(32, 1) +} + +func randomImage(t *testing.T, size int64) v1.Image { + t.Helper() + + img, err := random.Image(size, 1) if err != nil { - t.Fatalf("random.Image arm64: %v", err) + t.Fatalf("random.Image: %v", err) } - idx := mutate.AppendManifests( - mutate.IndexMediaType(empty.Index, types.OCIImageIndex), - mutate.IndexAddendum{ - Add: imgAmd, - Descriptor: v1.Descriptor{ - Platform: &v1.Platform{OS: "linux", Architecture: "amd64"}, - }, - }, - mutate.IndexAddendum{ - Add: imgArm, + + return img +} + +// multiArchIndex builds an index whose children carry real platform +// descriptors, so --platform has something to match on. +func multiArchIndex(t *testing.T) v1.ImageIndex { + t.Helper() + + idx := mutate.IndexMediaType(empty.Index, types.OCIImageIndex) + + for _, arch := range []string{"amd64", "arm64"} { + img := randomImage(t, 32) + + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("ConfigFile: %v", err) + } + + cfg.OS, cfg.Architecture = "linux", arch + + img, err = mutate.ConfigFile(img, cfg) + if err != nil { + t.Fatalf("mutate.ConfigFile: %v", err) + } + + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: img, Descriptor: v1.Descriptor{ - Platform: &v1.Platform{OS: "linux", Architecture: "arm64"}, + Platform: &v1.Platform{OS: "linux", Architecture: arch}, }, - }, - ) - idxRef, err := name.ParseReference(host+"/app:multi", name.Insecure) - if err != nil { - t.Fatalf("parse index ref: %v", err) - } - if err := remote.WriteIndex(idxRef, idx); err != nil { - t.Fatalf("push index: %v", err) + }) } - return &resolveTestEnv{ - imageRef: host + "/app:single", - indexRef: host + "/app:multi", - } -} - -func newOpts() *registry.Options { - return registry.New().WithInsecure() + return idx } func mapKeys[V any](m map[string]V) []string { @@ -112,7 +125,7 @@ func mapKeys[V any](m map[string]V) []string { func TestResolve_SingleImage(t *testing.T) { env := setupResolveEnv(t) - out, err := image.Resolve(context.Background(), []string{env.imageRef}, false, "", newOpts()) + out, err := image.Resolve(context.Background(), []string{env.imageRef}, false, "", env.opts) if err != nil { t.Fatalf("Resolve: %v", err) } @@ -126,7 +139,7 @@ func TestResolve_SingleImage(t *testing.T) { func TestResolve_IndexKeptWhenNoPlatform(t *testing.T) { env := setupResolveEnv(t) - out, err := image.Resolve(context.Background(), []string{env.indexRef}, true, "", newOpts()) + out, err := image.Resolve(context.Background(), []string{env.indexRef}, true, "", env.opts) if err != nil { t.Fatalf("Resolve: %v", err) } @@ -138,7 +151,7 @@ func TestResolve_IndexKeptWhenNoPlatform(t *testing.T) { func TestResolve_IndexFlattenedWhenPlatformPinned(t *testing.T) { env := setupResolveEnv(t) - opts := newOpts().WithPlatform(&v1.Platform{OS: "linux", Architecture: "amd64"}) + opts := env.opts.WithPlatform(&v1.Platform{OS: "linux", Architecture: "amd64"}) out, err := image.Resolve(context.Background(), []string{env.indexRef}, true, "", opts) if err != nil { t.Fatalf("Resolve: %v", err) @@ -154,7 +167,7 @@ func TestResolve_IndexFlattenedWhenPlatformPinned(t *testing.T) { func TestResolve_IndexFlattenedWhenKeepFalse(t *testing.T) { env := setupResolveEnv(t) - out, err := image.Resolve(context.Background(), []string{env.indexRef}, false, "", newOpts()) + out, err := image.Resolve(context.Background(), []string{env.indexRef}, false, "", env.opts) if err != nil { t.Fatalf("Resolve: %v", err) } @@ -167,7 +180,7 @@ func TestResolve_IndexFlattenedWhenKeepFalse(t *testing.T) { func TestResolve_CachePathWrapsImage(t *testing.T) { env := setupResolveEnv(t) cacheDir := t.TempDir() - out, err := image.Resolve(context.Background(), []string{env.imageRef}, false, cacheDir, newOpts()) + out, err := image.Resolve(context.Background(), []string{env.imageRef}, false, cacheDir, env.opts) if err != nil { t.Fatalf("Resolve: %v", err) } @@ -209,7 +222,7 @@ func TestResolve_CachePathWrapsImage(t *testing.T) { func TestResolve_CachePathWrapsIndex(t *testing.T) { env := setupResolveEnv(t) cacheDir := t.TempDir() - out, err := image.Resolve(context.Background(), []string{env.indexRef}, true, cacheDir, newOpts()) + out, err := image.Resolve(context.Background(), []string{env.indexRef}, true, cacheDir, env.opts) if err != nil { t.Fatalf("Resolve: %v", err) } @@ -257,7 +270,7 @@ func TestResolve_MultipleSources(t *testing.T) { env := setupResolveEnv(t) out, err := image.Resolve(context.Background(), []string{env.imageRef, env.indexRef}, - true, "", newOpts(), + true, "", env.opts, ) if err != nil { t.Fatalf("Resolve: %v", err) @@ -277,7 +290,7 @@ func TestResolve_DuplicateRefsAreRejected(t *testing.T) { env := setupResolveEnv(t) _, err := image.Resolve(context.Background(), []string{env.imageRef, env.imageRef}, - false, "", newOpts(), + false, "", env.opts, ) if err == nil { t.Fatalf("expected duplicate-ref error, got nil") @@ -288,7 +301,7 @@ func TestResolve_DuplicateRefsAreRejected(t *testing.T) { } // nil opts is a programmer error - explicit early failure beats a panic -// inside FetchDescriptor's name.ParseReference. +// inside the reference parsing that clientForRef does. func TestResolve_NilOptsReturnsError(t *testing.T) { _, err := image.Resolve(context.Background(), []string{"alpine:3.19"}, false, "", nil) if err == nil { diff --git a/internal/cr/internal/registry/adapter_fake_test.go b/internal/cr/internal/registry/adapter_fake_test.go new file mode 100644 index 000000000..349066d9d --- /dev/null +++ b/internal/cr/internal/registry/adapter_fake_test.go @@ -0,0 +1,504 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Tests for the adapter between `d8 cr`'s reference-oriented commands and the +// repository-scoped deckhouse/pkg/registry client, driven against that +// package's in-memory fake. +// +// The fake keeps these off the network: an HTTP registry per test costs seconds +// and, as the completion suite records, produces timeout flake under parallel +// `go test ./...` load. What the fake cannot stand in for stays on a real +// registry - see tags_test.go for the Link-cursor walk and login_test.go for +// the auth handshakes, neither of which the fake models. +package registry_test + +import ( + "context" + "errors" + "slices" + "strings" + "testing" + + 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/types" + + dkpreg "github.com/deckhouse/deckhouse/pkg/registry" + dkpclient "github.com/deckhouse/deckhouse/pkg/registry/client" + upfake "github.com/deckhouse/deckhouse/pkg/registry/fake" + + "github.com/deckhouse/deckhouse-cli/internal/cr/internal/registry" +) + +const ( + fakeHost = "registry.example.com" + otherHost = "mirror.example.com" +) + +// fakeEnv wires Options to a set of in-memory registries keyed by host. +type fakeEnv struct { + opts *registry.Options + registries map[string]*upfake.Registry +} + +// newFakeEnv seeds fakeHost with app:v1 (plus a second tag) and returns Options +// whose ClientFactory routes by host. +// +// Routing matters: the fake resolves a bare path against the first registry it +// was given, so the factory has to hand back a client whose default host is the +// one the reference actually named - otherwise a two-registry test (push from +// one to another) would silently talk to the wrong one. +func newFakeEnv(t *testing.T) *fakeEnv { + t.Helper() + + primary := upfake.NewRegistry(fakeHost) + primary.MustAddImage("app", "v1", upfake.NewImageBuilder(). + WithFile("etc/version", "1.0\n"). + WithLabel("owner", "platform"). + MustBuild()) + primary.MustAddImage("app", "v2", upfake.NewImageBuilder().MustBuild()) + primary.MustAddImage("tools/scanner", "latest", upfake.NewImageBuilder().MustBuild()) + + env := &fakeEnv{ + registries: map[string]*upfake.Registry{ + fakeHost: primary, + otherHost: upfake.NewRegistry(otherHost), + }, + } + + env.opts = registry.New() + env.opts.ClientFactory = func(host string, _ ...dkpclient.Option) dkpreg.Client { + reg, ok := env.registries[host] + if !ok { + // An unknown host must fail the way a real one does rather than + // falling back to some other registry in the map. + return upfake.NewClient(upfake.NewRegistry(host)) + } + + return upfake.NewClient(reg) + } + + return env +} + +func (e *fakeEnv) ref(repoTag string) string { return fakeHost + "/" + repoTag } + +func TestFetch_ResolvesReferenceToImage(t *testing.T) { + env := newFakeEnv(t) + + img, err := registry.Fetch(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("Fetch: %v", err) + } + + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("ConfigFile: %v", err) + } + + if got := cfg.Config.Labels["owner"]; got != "platform" { + t.Errorf("resolved the wrong image: label owner = %q, want platform", got) + } +} + +// A repository path with several segments has to survive the split into client +// segments; joining it wrong addresses "host/tools" or "host/tools%2Fscanner". +func TestFetch_HandlesNestedRepositoryPath(t *testing.T) { + env := newFakeEnv(t) + + if _, err := registry.Fetch(context.Background(), env.ref("tools/scanner:latest"), env.opts); err != nil { + t.Fatalf("Fetch on a nested repository path: %v", err) + } +} + +func TestFetch_MissingTagReportsNotFound(t *testing.T) { + env := newFakeEnv(t) + + _, err := registry.Fetch(context.Background(), env.ref("app:nope"), env.opts) + if err == nil { + t.Fatalf("expected an error for a missing tag") + } + + // The sentinel has to survive the adapter's wrapping, or every caller is + // back to matching on message text. + if !errors.Is(err, dkpreg.ErrImageNotFound) { + t.Errorf("error should unwrap to ErrImageNotFound; got %v", err) + } + + if !strings.Contains(err.Error(), env.ref("app:nope")) { + t.Errorf("error should name the reference the user typed; got %v", err) + } +} + +func TestFetch_UnknownHostFails(t *testing.T) { + env := newFakeEnv(t) + + if _, err := registry.Fetch(context.Background(), "nowhere.example.com/app:v1", env.opts); err == nil { + t.Fatalf("expected an error for a host with no such image") + } +} + +func TestFetch_RejectsUnparseableReference(t *testing.T) { + env := newFakeEnv(t) + + _, err := registry.Fetch(context.Background(), "NOT A REFERENCE", env.opts) + if err == nil { + t.Fatalf("expected a parse error") + } + + if !strings.Contains(err.Error(), "parse reference") { + t.Errorf("error should say the reference failed to parse; got %v", err) + } +} + +// `cr manifest` exists to hand exact bytes to a signature verifier, so the +// adapter must not round-trip the manifest through a decoder. +func TestFetchManifest_ReturnsBytesAsServed(t *testing.T) { + env := newFakeEnv(t) + + got, err := registry.FetchManifest(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("FetchManifest: %v", err) + } + + img, err := registry.Fetch(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("Fetch: %v", err) + } + + want, err := img.RawManifest() + if err != nil { + t.Fatalf("RawManifest: %v", err) + } + + if string(got) != string(want) { + t.Errorf("manifest bytes differ from the stored manifest\n got: %s\nwant: %s", got, want) + } +} + +func TestFetchConfig_ReturnsRawJSON(t *testing.T) { + env := newFakeEnv(t) + + got, err := registry.FetchConfig(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("FetchConfig: %v", err) + } + + if !strings.Contains(string(got), `"owner":"platform"`) { + t.Errorf("config JSON should carry the image labels verbatim; got %s", got) + } +} + +// The digest of a manifest is the hash of its own bytes, so the adapter's +// answer has to match what the stored image reports. +func TestFetchDigest_MatchesTheImageDigest(t *testing.T) { + env := newFakeEnv(t) + + got, err := registry.FetchDigest(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("FetchDigest: %v", err) + } + + img, err := registry.Fetch(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("Fetch: %v", err) + } + + want, err := img.Digest() + if err != nil { + t.Fatalf("Digest: %v", err) + } + + if got != want.String() { + t.Errorf("digest = %s, want %s", got, want) + } +} + +// A digest reference must address the same manifest a tag does; the client +// builds "host/repo@sha256:..." from the identifier, and getting that wrong +// silently turns the digest into a tag named "sha256:...". +func TestFetch_AcceptsDigestReference(t *testing.T) { + env := newFakeEnv(t) + + digest, err := registry.FetchDigest(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("FetchDigest: %v", err) + } + + if _, err := registry.Fetch(context.Background(), env.ref("app@"+digest), env.opts); err != nil { + t.Errorf("Fetch by digest: %v", err) + } +} + +func TestIsIndex_SingleImageIsNotAnIndex(t *testing.T) { + env := newFakeEnv(t) + + isIndex, err := registry.IsIndex(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("IsIndex: %v", err) + } + + if isIndex { + t.Errorf("a single-manifest image must not be classified as an index") + } +} + +func TestListTags_ReturnsEveryTag(t *testing.T) { + env := newFakeEnv(t) + + tags, err := registry.ListTags(context.Background(), fakeHost+"/app", env.opts) + if err != nil { + t.Fatalf("ListTags: %v", err) + } + + if want := []string{"v1", "v2"}; !slices.Equal(tags, want) { + t.Errorf("tags = %v, want %v", tags, want) + } +} + +func TestListCatalog_ReturnsEveryRepository(t *testing.T) { + env := newFakeEnv(t) + + repos, err := registry.ListCatalog(context.Background(), fakeHost, env.opts) + if err != nil { + t.Fatalf("ListCatalog: %v", err) + } + + if !slices.Contains(repos, "app") || !slices.Contains(repos, "tools/scanner") { + t.Errorf("catalog should list every repository; got %v", repos) + } +} + +func TestPush_WritesImageAndReturnsItsDigest(t *testing.T) { + env := newFakeEnv(t) + + img, err := registry.Fetch(context.Background(), env.ref("app:v1"), env.opts) + if err != nil { + t.Fatalf("Fetch: %v", err) + } + + dst := otherHost + "/copied:v1" + + digest, err := registry.Push(context.Background(), dst, img, env.opts) + if err != nil { + t.Fatalf("Push: %v", err) + } + + want, err := img.Digest() + if err != nil { + t.Fatalf("Digest: %v", err) + } + + if digest != want { + t.Errorf("Push returned %s, want the pushed image's digest %s", digest, want) + } + + // Pushing to the second host must land there, not back in the source. + if _, err := registry.Fetch(context.Background(), dst, env.opts); err != nil { + t.Errorf("pushed image is not readable back from %s: %v", otherHost, err) + } +} + +func TestPush_RejectsNilObject(t *testing.T) { + env := newFakeEnv(t) + + if _, err := registry.Push(context.Background(), env.ref("app:v1"), nil, env.opts); err == nil { + t.Fatalf("expected an error for a nil object") + } +} + +// A typed-nil v1.Image arrives as a non-nil interface holding a nil pointer, +// which the plain nil check above does not catch. +func TestPush_RejectsUnsupportedType(t *testing.T) { + env := newFakeEnv(t) + + if _, err := registry.Push(context.Background(), env.ref("app:v1"), notAnImage{}, env.opts); err == nil { + t.Fatalf("expected an error for an object that is neither an image nor an index") + } +} + +type notAnImage struct{} + +func (notAnImage) RawManifest() ([]byte, error) { return nil, nil } + +func (notAnImage) MediaType() (types.MediaType, error) { return "", nil } + +func (notAnImage) Digest() (v1.Hash, error) { return v1.Hash{}, nil } + +func (notAnImage) Size() (int64, error) { return 0, nil } + +// ---- multi-arch, now that the fake models indexes ---- + +// withIndex adds a two-platform index under app:multi. +func (e *fakeEnv) withIndex(t *testing.T) string { + t.Helper() + + idx := mutate.IndexMediaType(empty.Index, types.OCIImageIndex) + + for _, arch := range []string{"amd64", "arm64"} { + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: platformImage(t, arch), + Descriptor: v1.Descriptor{Platform: &v1.Platform{OS: "linux", Architecture: arch}}, + }) + } + + e.registries[fakeHost].MustAddIndex("app", "multi", idx) + + return e.ref("app:multi") +} + +func TestIsIndex_MultiArchReferenceIsAnIndex(t *testing.T) { + env := newFakeEnv(t) + ref := env.withIndex(t) + + isIndex, err := registry.IsIndex(context.Background(), ref, env.opts) + if err != nil { + t.Fatalf("IsIndex: %v", err) + } + + if !isIndex { + t.Errorf("a multi-arch reference must be classified as an index") + } +} + +func TestFetchIndex_ReturnsEveryPlatform(t *testing.T) { + env := newFakeEnv(t) + ref := env.withIndex(t) + + idx, err := registry.FetchIndex(context.Background(), ref, env.opts) + if err != nil { + t.Fatalf("FetchIndex: %v", err) + } + + manifest, err := idx.IndexManifest() + if err != nil { + t.Fatalf("IndexManifest: %v", err) + } + + if got := len(manifest.Manifests); got != 2 { + t.Errorf("index resolved to %d manifests, want both platforms", got) + } +} + +// FetchIndex on a plain image must fail rather than wrap it in a synthetic +// one-entry index, which pull would then write to disk as a bogus layout. +func TestFetchIndex_RejectsSingleImage(t *testing.T) { + env := newFakeEnv(t) + + if _, err := registry.FetchIndex(context.Background(), env.ref("app:v1"), env.opts); err == nil { + t.Fatalf("expected an error for a single-image reference") + } +} + +// The digest of a multi-arch reference is the index digest; with a platform it +// has to be that child's, otherwise pinning a deployment pins the wrong thing. +func TestFetchDigest_PlatformResolvesToChild(t *testing.T) { + env := newFakeEnv(t) + ref := env.withIndex(t) + + indexDigest, err := registry.FetchDigest(context.Background(), ref, env.opts) + if err != nil { + t.Fatalf("FetchDigest: %v", err) + } + + env.opts.WithPlatform(&v1.Platform{OS: "linux", Architecture: "arm64"}) + + childDigest, err := registry.FetchDigest(context.Background(), ref, env.opts) + if err != nil { + t.Fatalf("FetchDigest with a platform: %v", err) + } + + if childDigest == indexDigest { + t.Errorf("--platform was ignored: still the index digest %s", indexDigest) + } + + // The child digest must be one the index actually lists. + full, err := registry.FetchIndex(context.Background(), ref, env.opts) + if err != nil { + t.Fatalf("FetchIndex: %v", err) + } + + manifest, err := full.IndexManifest() + if err != nil { + t.Fatalf("IndexManifest: %v", err) + } + + var found bool + + for _, child := range manifest.Manifests { + if child.Digest.String() == childDigest { + found = true + + if child.Platform.Architecture != "arm64" { + t.Errorf("resolved to %s, want arm64", child.Platform.Architecture) + } + } + } + + if !found { + t.Errorf("digest %s is not a child of the index", childDigest) + } +} + +// Without a platform the underlying library resolves an index to a hardcoded +// linux/amd64 rather than the host's platform. That is a trap worth pinning +// down: a caller on arm64 who omits --platform silently gets amd64. +func TestFetch_IndexWithoutPlatformDefaultsToAmd64(t *testing.T) { + env := newFakeEnv(t) + ref := env.withIndex(t) + + img, err := registry.Fetch(context.Background(), ref, env.opts) + if err != nil { + t.Fatalf("Fetch: %v", err) + } + + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("ConfigFile: %v", err) + } + + if cfg.Architecture != "amd64" { + t.Errorf("architecture = %q, want amd64 (the library's hardcoded default)", cfg.Architecture) + } +} + +// platformImage builds a child whose config names the architecture, not just +// its index descriptor: resolving a platform hands back the image, so a test +// that reads the architecture back has to find it in the config. +func platformImage(t *testing.T, arch string) v1.Image { + t.Helper() + + img, err := random.Image(32, 1) + if err != nil { + t.Fatalf("random.Image: %v", err) + } + + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("ConfigFile: %v", err) + } + + cfg.OS, cfg.Architecture = "linux", arch + + img, err = mutate.ConfigFile(img, cfg) + if err != nil { + t.Fatalf("mutate.ConfigFile: %v", err) + } + + return img +} diff --git a/internal/cr/internal/registry/catalog.go b/internal/cr/internal/registry/catalog.go index 3d54440bc..d56620f1e 100644 --- a/internal/cr/internal/registry/catalog.go +++ b/internal/cr/internal/registry/catalog.go @@ -18,86 +18,32 @@ package registry import ( "context" + "errors" "fmt" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" + dkpreg "github.com/deckhouse/deckhouse/pkg/registry" ) -// ListCatalog returns every repository on the given registry. Like ListTags, -// it hides the page-by-page wire protocol: callers get the complete list or -// an error, never a truncated list; see ListTags. +// ListCatalog returns every repository on the given registry. // -// Not every registry implements /v2/_catalog - Docker Hub and GCR/GAR do not - -// and the underlying call surfaces that as a 404 through the error chain. +// Registries that do not implement /v2/_catalog - Docker Hub, GCR and Artifact +// Registry among them - are reported as such rather than as a bare 404, which +// otherwise reads like a missing repository and sends users hunting for a +// permissions problem they do not have. func ListCatalog(ctx context.Context, regRef string, opts *Options) ([]string, error) { - reg, err := name.NewRegistry(regRef, opts.Name...) - if err != nil { - return nil, fmt.Errorf("parse registry %q: %w", regRef, err) - } - - catalogger, err := newCatalogger(ctx, reg, opts) + client, err := clientForRegistryRef(regRef, opts) if err != nil { return nil, err } - return walkCatalogPages(ctx, catalogger, reg) -} - -// newCatalogger mirrors newTagLister, including the retry that drops the `n` -// query parameter for registries that reject it. -func newCatalogger(ctx context.Context, reg name.Registry, opts *Options) (*remote.Catalogger, error) { - catalogger, err := openCatalogger(ctx, reg, opts.remoteWithContext(ctx)) - if err == nil { - return catalogger, nil - } - - catalogger, retryErr := openCatalogger(ctx, reg, append(opts.remoteWithContext(ctx), remote.WithPageSize(0))) - if retryErr != nil { - return nil, fmt.Errorf("read catalog for %s: %w", reg, err) - } - - return catalogger, nil -} - -func openCatalogger(ctx context.Context, reg name.Registry, remoteOpts []remote.Option) (*remote.Catalogger, error) { - puller, err := remote.NewPuller(remoteOpts...) + repos, err := client.ListRepositories(ctx) if err != nil { - return nil, fmt.Errorf("create puller: %w", err) - } - - return puller.Catalogger(ctx, reg) -} - -// walkCatalogPages is the ListCatalog counterpart of walkTagPages; see there -// for why a repeated cursor is refused. -func walkCatalogPages(ctx context.Context, catalogger *remote.Catalogger, reg name.Registry) ([]string, error) { - var out []string - - seen := make(map[string]struct{}) - - for catalogger.HasNext() { - if err := ctx.Err(); err != nil { - return nil, err - } - - page, err := catalogger.Next(ctx) - if err != nil { - return nil, fmt.Errorf("read catalog for %s: %w", reg, err) - } - - out = append(out, page.Repos...) - - if page.Next == "" { - break - } - - if _, dup := seen[page.Next]; dup { - return nil, fmt.Errorf("read catalog for %s: registry keeps returning the same pagination cursor %q, refusing to loop", reg, page.Next) + if errors.Is(err, dkpreg.ErrCatalogNotSupported) { + return nil, fmt.Errorf("%s does not support listing repositories (no /v2/_catalog): %w", regRef, err) } - seen[page.Next] = struct{}{} + return nil, fmt.Errorf("read catalog for %s: %w", regRef, err) } - return out, nil + return repos, nil } diff --git a/internal/cr/internal/registry/client.go b/internal/cr/internal/registry/client.go new file mode 100644 index 000000000..9191fee85 --- /dev/null +++ b/internal/cr/internal/registry/client.go @@ -0,0 +1,141 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "fmt" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + + dkpreg "github.com/deckhouse/deckhouse/pkg/registry" + dkpclient "github.com/deckhouse/deckhouse/pkg/registry/client" +) + +// ParseReference parses a user-supplied image reference under the flags on +// Options - notably --insecure, which permits a plain-HTTP registry. +// +// Commands need this to echo a canonical reference back to the user (push +// prints the pushed digest reference, ls --full-ref prefixes every tag), and +// routing it through here keeps one set of parsing flags for the whole subtree. +func ParseReference(ref string, opts *Options) (name.Reference, error) { + parsed, err := name.ParseReference(ref, opts.nameOptions()...) + if err != nil { + return nil, fmt.Errorf("parse reference %q: %w", ref, err) + } + + return parsed, nil +} + +// ParseRepository is ParseReference for arguments that name a repository. +func ParseRepository(repoRef string, opts *Options) (name.Repository, error) { + repo, err := name.NewRepository(repoRef, opts.nameOptions()...) + if err != nil { + return name.Repository{}, fmt.Errorf("parse repository %q: %w", repoRef, err) + } + + return repo, nil +} + +// clientForRef parses a user-supplied image reference and returns a client +// scoped to its repository, plus the identifier (tag or "sha256:...") that +// addresses one manifest inside it. +// +// This is the whole adapter between the two shapes: `d8 cr` arguments are full +// references, while dkpreg.Client is scoped by host plus path segments. Parsing +// stays on this side because only the CLI knows the reference is user input +// that has to be validated before anything touches the network. +func clientForRef(ref string, opts *Options) (dkpreg.Client, string, error) { + parsed, err := name.ParseReference(ref, opts.nameOptions()...) + if err != nil { + return nil, "", fmt.Errorf("parse reference %q: %w", ref, err) + } + + repo := parsed.Context() + + return clientForRepository(repo, opts), parsed.Identifier(), nil +} + +// clientForRepoRef is clientForRef for arguments that name a repository and +// never a tag, such as `cr ls REPO`. +func clientForRepoRef(repoRef string, opts *Options) (dkpreg.Client, error) { + repo, err := name.NewRepository(repoRef, opts.nameOptions()...) + if err != nil { + return nil, fmt.Errorf("parse repository %q: %w", repoRef, err) + } + + return clientForRepository(repo, opts), nil +} + +// clientForRegistryRef is clientForRef for arguments that name a registry, such +// as `cr catalog REGISTRY`. +func clientForRegistryRef(regRef string, opts *Options) (dkpreg.Client, error) { + reg, err := name.NewRegistry(regRef, opts.nameOptions()...) + if err != nil { + return nil, fmt.Errorf("parse registry %q: %w", regRef, err) + } + + return opts.newClient(reg.RegistryStr()), nil +} + +// newClient builds the client for one host, through the test seam when set. +func (o *Options) newClient(host string) dkpreg.Client { + if o.ClientFactory != nil { + return o.ClientFactory(host, o.clientOptions()...) + } + + return dkpclient.New(host, o.clientOptions()...) +} + +func clientForRepository(repo name.Repository, opts *Options) dkpreg.Client { + client := opts.newClient(repo.RegistryStr()) + + // A bare registry reference has no repository path; WithSegment("") would + // otherwise append an empty segment and address "host/". + if path := repo.RepositoryStr(); path != "" { + return client.WithSegment(strings.Split(path, "/")...) + } + + return client +} + +// pushOptions are the client push options implied by Options. +func (o *Options) pushOptions() []dkpreg.ImagePushOption { + if o.Nondistributable { + return []dkpreg.ImagePushOption{dkpclient.WithNondistributable()} + } + + return nil +} + +// imageGetOptions are the client image-get options implied by Options. +func (o *Options) imageGetOptions() []dkpreg.ImageGetOption { + if o.Platform != nil { + return []dkpreg.ImageGetOption{dkpclient.WithPlatform{Platform: o.Platform}} + } + + return nil +} + +// manifestGetOptions are the client manifest-get options implied by Options. +func (o *Options) manifestGetOptions() []dkpreg.ManifestGetOption { + if o.Platform != nil { + return []dkpreg.ManifestGetOption{dkpclient.WithPlatform{Platform: o.Platform}} + } + + return nil +} diff --git a/internal/cr/internal/registry/fetch.go b/internal/cr/internal/registry/fetch.go index d889912c9..21e968aa1 100644 --- a/internal/cr/internal/registry/fetch.go +++ b/internal/cr/internal/registry/fetch.go @@ -20,21 +20,22 @@ import ( "context" "fmt" - "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" ) -// Fetch resolves ref and returns a v1.Image. For multi-arch indices -// remote.Image picks the current runtime platform unless opts.Platform pins -// another one. +// Fetch resolves ref to a single image. +// +// For a multi-arch index the platform pinned on Options wins; without one the +// underlying library falls back to a hardcoded linux/amd64 rather than the +// host's platform, so commands that must match the caller's architecture have +// to pass --platform. func Fetch(ctx context.Context, ref string, opts *Options) (v1.Image, error) { - parsed, err := name.ParseReference(ref, opts.Name...) + client, id, err := clientForRef(ref, opts) if err != nil { - return nil, fmt.Errorf("parse reference %q: %w", ref, err) + return nil, err } - img, err := remote.Image(parsed, opts.remoteWithContext(ctx)...) + img, err := client.GetImage(ctx, id, opts.imageGetOptions()...) if err != nil { return nil, fmt.Errorf("fetch %s: %w", ref, err) } @@ -42,47 +43,37 @@ func Fetch(ctx context.Context, ref string, opts *Options) (v1.Image, error) { return img, nil } -// FetchDescriptor returns the raw remote descriptor, leaving media-type -// dispatch to the caller (pull uses it to tell an index from an image). -func FetchDescriptor(ctx context.Context, ref string, opts *Options) (*remote.Descriptor, error) { - parsed, err := name.ParseReference(ref, opts.Name...) +// FetchIndex resolves ref to a multi-arch index, resolving nothing. An image +// reference is an error - the caller asked for an index. +func FetchIndex(ctx context.Context, ref string, opts *Options) (v1.ImageIndex, error) { + client, id, err := clientForRef(ref, opts) if err != nil { - return nil, fmt.Errorf("parse reference %q: %w", ref, err) + return nil, err } - desc, err := remote.Get(parsed, opts.remoteWithContext(ctx)...) + idx, err := client.GetIndex(ctx, id) if err != nil { - return nil, fmt.Errorf("fetch descriptor %s: %w", ref, err) + return nil, fmt.Errorf("fetch index %s: %w", ref, err) } - return desc, nil + return idx, nil } -// remoteWithContext is the single point where Options is converted to a -// []remote.Option. Keychain / platform / context are finalized here so that -// repeated builder calls (e.g. WithPlatform twice) cannot stack duplicate -// upstream options on o.Remote and rely on go-containerregistry's -// last-write-wins semantics. o.Remote stays untouched, so the same Options -// can be used to dispatch calls with different per-call contexts. -func (o *Options) remoteWithContext(ctx context.Context) []remote.Option { - if ctx == nil { - ctx = o.Context - } - - out := make([]remote.Option, 0, len(o.Remote)+3) - - out = append(out, o.Remote...) - if o.Keychain != nil { - out = append(out, remote.WithAuthFromKeychain(o.Keychain)) - } - - if o.Platform != nil { - out = append(out, remote.WithPlatform(*o.Platform)) +// IsIndex reports whether ref is a multi-arch index rather than a single image, +// which is what pull needs to know before deciding what to write to disk. +func IsIndex(ctx context.Context, ref string, opts *Options) (bool, error) { + client, id, err := clientForRef(ref, opts) + if err != nil { + return false, err } - if ctx != nil { - out = append(out, remote.WithContext(ctx)) + // No platform here on purpose: the question is what the registry serves for + // this reference, and resolving a platform first would answer "image" for + // every index. + res, err := client.GetManifest(ctx, id) + if err != nil { + return false, fmt.Errorf("inspect %s: %w", ref, err) } - return out + return res.GetMediaType().IsIndex(), nil } diff --git a/internal/cr/internal/registry/inspect.go b/internal/cr/internal/registry/inspect.go index 34ac84dd4..2256b1346 100644 --- a/internal/cr/internal/registry/inspect.go +++ b/internal/cr/internal/registry/inspect.go @@ -21,19 +21,28 @@ import ( "fmt" ) -// FetchManifest returns the raw manifest bytes as the registry served them. -// This preserves signatures and byte-for-byte JSON the user may want to pipe. +// FetchManifest returns the raw manifest bytes as the registry served them, +// which is what signature verification and audit trails need - a manifest +// decoded and re-encoded no longer hashes to its own digest. +// +// With a platform pinned, a multi-arch reference resolves to that child's +// manifest instead of the index. Without one the index is returned as served. func FetchManifest(ctx context.Context, ref string, opts *Options) ([]byte, error) { - desc, err := FetchDescriptor(ctx, ref, opts) + client, id, err := clientForRef(ref, opts) if err != nil { return nil, err } - return desc.Manifest, nil + res, err := client.GetManifest(ctx, id, opts.manifestGetOptions()...) + if err != nil { + return nil, fmt.Errorf("fetch manifest %s: %w", ref, err) + } + + return res.GetRaw(), nil } -// FetchConfig returns the config JSON for ref. Multi-arch indices are -// resolved via the caller's platform (set on Options). +// FetchConfig returns the raw config JSON for ref, byte-for-byte as stored, so +// it stays pipeable into jq and comparable across pulls. func FetchConfig(ctx context.Context, ref string, opts *Options) ([]byte, error) { img, err := Fetch(ctx, ref, opts) if err != nil { @@ -48,12 +57,26 @@ func FetchConfig(ctx context.Context, ref string, opts *Options) ([]byte, error) return cfg, nil } -// FetchDigest returns "sha256:" for ref's manifest as served. +// FetchDigest returns "sha256:" for ref. +// +// With a platform pinned this is the digest of that child image, not of the +// index - the whole point of asking for a digest is to pin what will actually +// run, and an index digest does not identify a single image. func FetchDigest(ctx context.Context, ref string, opts *Options) (string, error) { - desc, err := FetchDescriptor(ctx, ref, opts) + client, id, err := clientForRef(ref, opts) if err != nil { return "", err } - return desc.Digest.String(), nil + res, err := client.GetManifest(ctx, id, opts.manifestGetOptions()...) + if err != nil { + return "", fmt.Errorf("fetch digest %s: %w", ref, err) + } + + desc := res.GetDescriptor() + if desc == nil { + return "", fmt.Errorf("fetch digest %s: registry returned a manifest without a descriptor", ref) + } + + return desc.GetDigest().String(), nil } diff --git a/internal/cr/internal/registry/login.go b/internal/cr/internal/registry/login.go index 3d21f5cef..77b35afdb 100644 --- a/internal/cr/internal/registry/login.go +++ b/internal/cr/internal/registry/login.go @@ -18,6 +18,7 @@ package registry import ( "context" + "crypto/tls" "fmt" "net/http" @@ -54,17 +55,14 @@ func Login(ctx context.Context, host, username, password string, opts *Options) host = name.DefaultRegistry } - reg, err := name.NewRegistry(host, opts.Name...) + reg, err := name.NewRegistry(host, opts.nameOptions()...) if err != nil { return nil, fmt.Errorf("parse registry %q: %w", host, err) } auth := authn.FromConfig(authn.AuthConfig{Username: username, Password: password}) - rt := opts.Transport - if rt == nil { - rt = remote.DefaultTransport - } + rt := loginTransport(opts) // Build the registry's auth transport. Empty scope keeps this a // registry-level check (a repository/catalog scope would demand @@ -130,6 +128,34 @@ func verifyCredentials(ctx context.Context, reg name.Registry, rt http.RoundTrip return transport.CheckError(resp, http.StatusOK) } +// loginTransport builds the HTTP transport for the credential probe. +// +// Login stays outside the registry client on purpose: it writes to the Docker +// config, which is a CLI concern the client has no notion of, and it probes +// /v2/ rather than any repository. That leaves it needing its own transport. +func loginTransport(opts *Options) http.RoundTripper { + if !opts.TLSSkipVerify { + return remote.DefaultTransport + } + + base, ok := remote.DefaultTransport.(*http.Transport) + if !ok { + // Upstream changed DefaultTransport's concrete type. Better to verify + // certificates against a default we do not recognise than to panic on + // an unchecked type assertion. + return remote.DefaultTransport + } + + t := base.Clone() + if t.TLSClientConfig == nil { + t.TLSClientConfig = &tls.Config{} //nolint:gosec // MinVersion comes from the cloned default + } + + t.TLSClientConfig.InsecureSkipVerify = true //nolint:gosec // user-opted via --insecure + + return t +} + // dockerServerAddress maps a parsed registry to the key Docker stores // credentials under. Docker Hub is special-cased to the canonical // "https://index.docker.io/v1/" so authn.DefaultKeychain (which rewrites the diff --git a/internal/cr/internal/registry/login_test.go b/internal/cr/internal/registry/login_test.go index 46a1aefb9..ae4c2d774 100644 --- a/internal/cr/internal/registry/login_test.go +++ b/internal/cr/internal/registry/login_test.go @@ -25,8 +25,6 @@ import ( "path/filepath" "strings" "testing" - - "github.com/google/go-containerregistry/pkg/name" ) const ( @@ -135,7 +133,5 @@ func TestLoginVerifiesCredentials(t *testing.T) { // insecureOptions parses the httptest host as plain HTTP (it serves no TLS), // matching how `--insecure` / a localhost target is handled in real use. func insecureOptions() *Options { - o := New() - o.Name = append(o.Name, name.Insecure) - return o + return New().WithInsecure() } diff --git a/internal/cr/internal/registry/options.go b/internal/cr/internal/registry/options.go index da7bab09c..f6e051c91 100644 --- a/internal/cr/internal/registry/options.go +++ b/internal/cr/internal/registry/options.go @@ -17,64 +17,78 @@ limitations under the License. package registry import ( - "context" - "net/http" + "io" + "log/slog" + "os" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" + + dkplog "github.com/deckhouse/deckhouse/pkg/log" + dkpreg "github.com/deckhouse/deckhouse/pkg/registry" + dkpclient "github.com/deckhouse/deckhouse/pkg/registry/client" ) -// Options accumulates everything the domain layer needs to talk to a -// registry: auth, transport, platform hint, name-parsing flags. Each builder -// mutates the receiver and returns it so calls chain. +// Options carries the `d8 cr` persistent flags and turns them into a +// deckhouse/pkg/registry client scoped to whatever reference a command names. // -// The two slices (Remote, Name) are what actually gets passed to -// go-containerregistry: Remote to remote.*, Name to name.ParseReference / -// name.NewRepository / name.NewRegistry / name.NewTag. +// The flags live here rather than in a client instance because `d8 cr` is +// reference-oriented - every command takes a full "host/repo:tag" argument - +// while the client is scoped to one repository. clientForRef bridges the two. type Options struct { - Remote []remote.Option - Name []name.Option + // PlainHTTP talks to the registry over HTTP instead of HTTPS. + PlainHTTP bool + // TLSSkipVerify accepts any server certificate. + TLSSkipVerify bool + // Nondistributable uploads foreign layers on push instead of skipping them. + Nondistributable bool + // Verbose routes the client's debug log to stderr. + Verbose bool + + // Platform pins a target platform for multi-arch indices. Nil means the + // reference is used as served, except where a command must resolve to a + // single image - see Fetch. Platform *v1.Platform + + // Auth authenticates every request when the user passed --username/--password. + // It takes precedence over Keychain, and being per-client it cannot leak + // credentials to a registry the user did not name. + Auth authn.Authenticator + // Keychain resolves credentials from the Docker config when Auth is unset. Keychain authn.Keychain - Context context.Context - // Transport mirrors the http.RoundTripper installed via WithTransport. - // remote.* receives it through o.Remote; we also keep a direct handle so - // auth-only flows (e.g. Login) that talk to the registry transport - // package can honour --insecure without re-deriving it from o.Remote. - Transport http.RoundTripper + // ClientFactory builds the registry client for one host. Nil means the real + // deckhouse/pkg/registry client. + // + // Tests set it to drive this package against pkg/registry's in-memory fake + // and stay off the network entirely: standing up an HTTP registry per test + // both slows the suite down and produces timeout flake under parallel + // `go test ./...` load. It is a field rather than a package-level hook so + // tests can run in parallel without fighting over global state. + ClientFactory func(host string, opts ...dkpclient.Option) dkpreg.Client } -// New returns Options seeded with the default Docker keychain and a -// background context. Keychain / platform / context are NOT baked into -// o.Remote here - they are finalized lazily by remoteWithContext at fetch -// time so repeated builder calls (e.g. WithPlatform twice with different -// values) cannot stack duplicate options on the slice. +// New returns Options seeded with the default Docker keychain, so every command +// authenticates from ~/.docker/config.json without a prior `cr login`. func New() *Options { - return &Options{ - Keychain: authn.DefaultKeychain, - Context: context.Background(), - } -} - -// WithContext replaces the ambient context. -func (o *Options) WithContext(ctx context.Context) *Options { - o.Context = ctx - return o + return &Options{Keychain: authn.DefaultKeychain} } // WithKeychain replaces the keychain that authenticates registry calls. -// Last call wins. func (o *Options) WithKeychain(kc authn.Keychain) *Options { o.Keychain = kc return o } -// WithPlatform pins a target platform for multi-arch indices. Nil is a no-op -// (so a flag-driven caller can pass the parsed result directly without -// branching). Last non-nil call wins. +// WithAuth pins explicit credentials, as --username/--password do. +func (o *Options) WithAuth(auth authn.Authenticator) *Options { + o.Auth = auth + return o +} + +// WithPlatform pins a target platform. Nil is a no-op so a flag-driven caller +// can pass the parsed result without branching. func (o *Options) WithPlatform(p *v1.Platform) *Options { if p == nil { return o @@ -85,46 +99,78 @@ func (o *Options) WithPlatform(p *v1.Platform) *Options { return o } -// WithInsecure tolerates non-TLS references during name parsing. The HTTP -// transport itself is configured separately via WithTransport. +// WithInsecure opts into plain HTTP and accepts any TLS certificate. +// +// The two are separate on the client (WithInsecure / WithTLSSkipVerify) and +// deserve separate flags here too, but `--insecure` has always meant both, so +// the flag keeps setting both until it is split. func (o *Options) WithInsecure() *Options { - o.Name = append(o.Name, name.Insecure) + o.PlainHTTP = true + o.TLSSkipVerify = true + return o } // WithNondistributable allows pushing foreign (non-distributable) layers. func (o *Options) WithNondistributable() *Options { - o.Remote = append(o.Remote, remote.WithNondistributable) + o.Nondistributable = true return o } -// WithTransport installs a custom HTTP transport (typically a clone of -// remote.DefaultTransport with TLS skip-verify toggled). -func (o *Options) WithTransport(t http.RoundTripper) *Options { - o.Transport = t - o.Remote = append(o.Remote, remote.WithTransport(t)) - +// WithVerbose enables the client's debug log on stderr. +func (o *Options) WithVerbose() *Options { + o.Verbose = true return o } -// staticKeychain authenticates every registry with a single set of -// credentials. Used when the user passes --username/--password so all cr -// commands can talk to the registry without a prior `cr login`. -type staticKeychain struct { - auth authn.Authenticator +// nameOptions are the go-containerregistry parsing flags implied by Options. +// Reference parsing stays here because `d8 cr` has to split a user-supplied +// reference into registry, repository and tag before it can build a client. +func (o *Options) nameOptions() []name.Option { + if o.PlainHTTP { + return []name.Option{name.Insecure} + } + + return nil } -func (k staticKeychain) Resolve(authn.Resource) (authn.Authenticator, error) { - return k.auth, nil +// clientOptions translates the flags into client options. +func (o *Options) clientOptions() []dkpclient.Option { + opts := []dkpclient.Option{ + dkpclient.WithInsecure(o.PlainHTTP), + dkpclient.WithTLSSkipVerify(o.TLSSkipVerify), + dkpclient.WithLogger(o.logger()), + } + + // Auth wins over the keychain, matching the client's own precedence. + if o.Auth != nil { + return append(opts, dkpclient.WithAuth(o.Auth)) + } + + if o.Keychain != nil { + return append(opts, dkpclient.WithKeychain(o.Keychain)) + } + + return opts } -// NewStaticKeychain returns a keychain that always resolves to the given -// username/password, regardless of the target registry. -func NewStaticKeychain(username, password string) authn.Keychain { - return staticKeychain{ - auth: authn.FromConfig(authn.AuthConfig{ - Username: username, - Password: password, - }), +// logger keeps the client's log off stdout. +// +// The client logs at debug on every operation and its default logger writes to +// stdout, which would corrupt the one thing several commands exist to produce: +// "d8 cr manifest ref | jq" and "d8 cr export ref - | tar tf -" both put bytes +// on stdout that must stay pristine. So the sink is always stderr, and the +// level is silent unless -v asked for it. +func (o *Options) logger() *dkplog.Logger { + if o.Verbose { + return dkplog.NewLogger( + dkplog.WithLevel(slog.LevelDebug), + dkplog.WithOutput(os.Stderr), + ).Named("cr") } + + return dkplog.NewLogger( + dkplog.WithLevel(slog.LevelError), + dkplog.WithOutput(io.Discard), + ).Named("cr") } diff --git a/internal/cr/internal/registry/options_test.go b/internal/cr/internal/registry/options_test.go index f7fdd1b3b..c04bdb78f 100644 --- a/internal/cr/internal/registry/options_test.go +++ b/internal/cr/internal/registry/options_test.go @@ -17,166 +17,150 @@ limitations under the License. package registry import ( - "context" + "os" "testing" "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" ) func TestNew_HasDefaults(t *testing.T) { o := New() + if o.Keychain == nil { - t.Errorf("New() should seed a default keychain") + t.Errorf("New() should seed the Docker keychain so commands authenticate without a prior login") } - if o.Context == nil { - t.Errorf("New() should seed a background context") + + if o.Auth != nil { + t.Errorf("New() should leave Auth unset; got %+v", o.Auth) } + if o.Platform != nil { t.Errorf("New() should leave Platform nil; got %+v", o.Platform) } - // Keychain / platform / context are finalized lazily by remoteWithContext; - // New() must not pre-bake them into o.Remote, otherwise repeat builder - // calls would silently stack duplicate upstream options. - if len(o.Remote) != 0 { - t.Errorf("New() must leave Remote empty; got %d entries", len(o.Remote)) + + if o.PlainHTTP || o.TLSSkipVerify || o.Nondistributable || o.Verbose { + t.Errorf("New() should leave every flag off; got %+v", o) } } -func TestWithContext_ReplacesCtx(t *testing.T) { - o := New() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() +func TestChainableBuilders(t *testing.T) { + platform := &v1.Platform{OS: "linux", Architecture: "arm64"} + auth := authn.FromConfig(authn.AuthConfig{Username: "u", Password: "p"}) - o.WithContext(ctx) - if o.Context != ctx { - t.Errorf("WithContext did not replace Context") - } -} + o := New(). + WithInsecure(). + WithNondistributable(). + WithVerbose(). + WithPlatform(platform). + WithAuth(auth) -func TestWithContext_DoesNotMutateRemote(t *testing.T) { - // remote.WithContext is produced exclusively by remoteWithContext at fetch - // time. Pre-baking it into o.Remote would stack a second WithContext when - // callers pass a derived ctx, relying on go-containerregistry's last-wins - // semantics - fragile. Guard the contract here. - o := New() - before := len(o.Remote) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - o.WithContext(ctx) - if len(o.Remote) != before { - t.Errorf("WithContext should not append to o.Remote; len before=%d after=%d", before, len(o.Remote)) + if !o.PlainHTTP || !o.TLSSkipVerify { + t.Errorf("WithInsecure must set both plain HTTP and TLS skip-verify; got %+v", o) } -} -func TestRemoteWithContext_FinalizesLazily(t *testing.T) { - // Default Options + ctx => keychain + ctx are appended at finalize time; - // o.Remote stays empty (it gets stuff only via WithTransport / WithNondistributable). - o := New() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - got := o.remoteWithContext(ctx) - if len(got) != 2 { - t.Errorf("expected 2 finalized options (keychain + ctx); got %d", len(got)) - } - if len(o.Remote) != 0 { - t.Errorf("remoteWithContext mutated o.Remote: got len=%d", len(o.Remote)) - } - // Calling again must not stack more options - finalize is pure of o.Remote. - got2 := o.remoteWithContext(ctx) - if len(got2) != len(got) { - t.Errorf("second call to remoteWithContext returned different length: %d vs %d", len(got2), len(got)) + if !o.Nondistributable || !o.Verbose { + t.Errorf("builders did not set their flags; got %+v", o) } -} -func TestWithPlatform_NilIsNoop(t *testing.T) { - o := New() - before := len(o.Remote) - o.WithPlatform(nil) - if o.Platform != nil { - t.Errorf("nil platform should not be stored; got %+v", o.Platform) - } - if len(o.Remote) != before { - t.Errorf("nil platform should not append remote options") + if o.Platform != platform { + t.Errorf("WithPlatform did not take; got %+v", o.Platform) } -} -func TestWithPlatform_Stores(t *testing.T) { - o := New() - p, err := v1.ParsePlatform("linux/arm64") - if err != nil { - t.Fatalf("ParsePlatform: %v", err) - } - o.WithPlatform(p) - if o.Platform == nil || o.Platform.OS != "linux" || o.Platform.Architecture != "arm64" { - t.Errorf("Platform not stored: %+v", o.Platform) + if o.Auth != auth { + t.Errorf("WithAuth did not take") } } -func TestChainableBuilders(t *testing.T) { - ctx := context.Background() - o := New().WithContext(ctx).WithInsecure().WithNondistributable() +// A flag-driven caller passes the parsed result straight through, so a nil +// platform has to mean "unset" rather than clearing a previous value. +func TestWithPlatform_NilIsNoOp(t *testing.T) { + platform := &v1.Platform{OS: "linux", Architecture: "amd64"} - if o.Context != ctx { - t.Errorf("WithContext did not propagate") + o := New().WithPlatform(platform).WithPlatform(nil) + if o.Platform != platform { + t.Errorf("WithPlatform(nil) must not clear the pinned platform; got %+v", o.Platform) } - if len(o.Name) == 0 { - t.Errorf("WithInsecure should have appended a name.Option") - } -} - -// stubKeychain is a sentinel implementation - we only need pointer identity -// for the anti-duplication tests below. -type stubKeychain struct{ tag string } - -func (stubKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { - return authn.Anonymous, nil } -func TestWithKeychain_LastWriteReplaces(t *testing.T) { - custom := stubKeychain{tag: "custom"} - o := New().WithKeychain(custom) - if _, ok := o.Keychain.(stubKeychain); !ok { - t.Errorf("Keychain not replaced; got %T", o.Keychain) +// --insecure must reach reference parsing too: without name.Insecure a +// plain-HTTP reference is rejected before any request is made. +func TestNameOptions_TracksPlainHTTP(t *testing.T) { + if got := len(New().nameOptions()); got != 0 { + t.Errorf("secure options should add no name options; got %d", got) } - // Pre-fix behaviour appended a second WithAuthFromKeychain to o.Remote; - // finalize-on-read makes that impossible by construction. - if len(o.Remote) != 0 { - t.Errorf("WithKeychain must not stack options on o.Remote; got %d", len(o.Remote)) + + if got := len(New().WithInsecure().nameOptions()); got != 1 { + t.Errorf("--insecure should permit plain-HTTP references; got %d name options", got) } - // Finalized output must still carry exactly one keychain option (no dupes - // across repeated finalize calls) plus a ctx. - got := o.remoteWithContext(context.Background()) - if len(got) != 2 { - t.Errorf("expected 2 finalized options (keychain + ctx), got %d", len(got)) + + if _, err := name.ParseReference("localhost:5000/app:v1", New().WithInsecure().nameOptions()...); err != nil { + t.Errorf("plain-HTTP reference should parse under --insecure: %v", err) } } -func TestWithPlatform_RepeatedCallsDoNotStack(t *testing.T) { - p1, _ := v1.ParsePlatform("linux/amd64") - p2, _ := v1.ParsePlatform("linux/arm64") - o := New().WithPlatform(p1).WithPlatform(p2) +func TestPushOptions_TrackNondistributable(t *testing.T) { + if got := len(New().pushOptions()); got != 0 { + t.Errorf("foreign layers are skipped by default; got %d push options", got) + } - if o.Platform == nil || o.Platform.Architecture != "arm64" { - t.Errorf("last WithPlatform must win; got %+v", o.Platform) + if got := len(New().WithNondistributable().pushOptions()); got != 1 { + t.Errorf("--allow-nondistributable-artifacts should reach the client; got %d push options", got) } - if len(o.Remote) != 0 { - t.Errorf("WithPlatform must not stack options on o.Remote; got %d", len(o.Remote)) +} + +func TestGetOptions_TrackPlatform(t *testing.T) { + o := New() + if len(o.imageGetOptions()) != 0 || len(o.manifestGetOptions()) != 0 { + t.Errorf("no platform pinned means no platform option") } - got := o.remoteWithContext(context.Background()) - // keychain + platform + ctx - if len(got) != 3 { - t.Errorf("expected 3 finalized options (keychain + platform + ctx), got %d", len(got)) + + o.WithPlatform(&v1.Platform{OS: "linux", Architecture: "arm64"}) + if len(o.imageGetOptions()) != 1 || len(o.manifestGetOptions()) != 1 { + t.Errorf("--platform must reach both the image and the manifest call") } } -func TestInsecureTransport_Cloned(t *testing.T) { - t1 := InsecureTransport() - t2 := InsecureTransport() - if t1 == nil || t2 == nil { - t.Fatalf("InsecureTransport returned nil") - } - if t1 == t2 { - t.Errorf("InsecureTransport should return distinct clones, got the same instance") +// The registry client logs at debug on every operation and its own default +// logger writes to stdout. Several commands exist to put exact bytes there - +// "cr manifest ref | jq", "cr export ref - | tar tf -" - so anything landing on +// stdout corrupts the output the user asked for. +func TestLogger_NeverWritesToStdout(t *testing.T) { + for _, verbose := range []bool{false, true} { + o := New() + if verbose { + o.WithVerbose() + } + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + + saved := os.Stdout + os.Stdout = w + + logger := o.logger() + logger.Error("error level") + logger.Info("info level") + logger.Debug("debug level") + + os.Stdout = saved + + if err := w.Close(); err != nil { + t.Fatalf("close: %v", err) + } + + buf := make([]byte, 1024) + n, _ := r.Read(buf) + + if err := r.Close(); err != nil { + t.Fatalf("close read end: %v", err) + } + + if n > 0 { + t.Errorf("verbose=%v: client logger wrote %d bytes to stdout: %q", verbose, n, buf[:n]) + } } } diff --git a/internal/cr/internal/registry/push.go b/internal/cr/internal/registry/push.go index 64b25a736..f848a5135 100644 --- a/internal/cr/internal/registry/push.go +++ b/internal/cr/internal/registry/push.go @@ -20,39 +20,37 @@ import ( "context" "fmt" - "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/partial" - "github.com/google/go-containerregistry/pkg/v1/remote" ) // Push writes obj (v1.Image or v1.ImageIndex) under ref and returns the // resulting digest. Anything else is a programmer error. func Push(ctx context.Context, ref string, obj partial.WithRawManifest, opts *Options) (v1.Hash, error) { - // A literal-nil and a typed-nil v1.Image/v1.ImageIndex both land here - // as a nil interface (since v1.Image and v1.ImageIndex are themselves - // interfaces). Catching it up front keeps the type switch from doing - // remote.Write on a nil object and panicking inside go-containerregistry. + // A literal-nil and a typed-nil v1.Image/v1.ImageIndex both land here as a + // nil interface, since v1.Image and v1.ImageIndex are themselves + // interfaces. Catching it up front keeps the type switch from pushing a nil + // object and panicking deeper down. if obj == nil { return v1.Hash{}, fmt.Errorf("push %s: object is nil", ref) } - parsed, err := name.ParseReference(ref, opts.Name...) + client, id, err := clientForRef(ref, opts) if err != nil { - return v1.Hash{}, fmt.Errorf("parse reference %q: %w", ref, err) + return v1.Hash{}, err } - remoteOpts := opts.remoteWithContext(ctx) + pushOpts := opts.pushOptions() switch t := obj.(type) { case v1.Image: - if err := remote.Write(parsed, t, remoteOpts...); err != nil { + if err := client.PushImage(ctx, id, t, pushOpts...); err != nil { return v1.Hash{}, fmt.Errorf("push image %s: %w", ref, err) } return t.Digest() case v1.ImageIndex: - if err := remote.WriteIndex(parsed, t, remoteOpts...); err != nil { + if err := client.PushIndex(ctx, id, t, pushOpts...); err != nil { return v1.Hash{}, fmt.Errorf("push index %s: %w", ref, err) } diff --git a/internal/cr/internal/registry/tags.go b/internal/cr/internal/registry/tags.go index fca5860cb..7ca6ffbfb 100644 --- a/internal/cr/internal/registry/tags.go +++ b/internal/cr/internal/registry/tags.go @@ -19,104 +19,23 @@ package registry import ( "context" "fmt" - - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" ) // ListTags returns every tag of repoRef. // -// The registry API serves tags in pages (`?n=` plus a `Link: rel="next"` -// cursor), but that is a wire detail with no console equivalent: there is no -// way for a user to ask for "the next range", so callers always get the -// complete list or an error - never a page, and never a truncated list. -// Pages are walked internally by walkTagPages. There is deliberately no cap: -// a partial tag list is indistinguishable from a complete one at the call -// site, which is exactly the failure mode this package avoids. +// The registry's page-by-page protocol is the client's business and has no +// console equivalent - there is no way for a user to ask for "the next range" - +// so callers get the complete list or an error, never a truncated one. func ListTags(ctx context.Context, repoRef string, opts *Options) ([]string, error) { - repo, err := name.NewRepository(repoRef, opts.Name...) - if err != nil { - return nil, fmt.Errorf("parse repository %q: %w", repoRef, err) - } - - lister, err := newTagLister(ctx, repo, opts) + client, err := clientForRepoRef(repoRef, opts) if err != nil { return nil, err } - return walkTagPages(ctx, lister, repo) -} - -// newTagLister opens a tag listing, retrying once without the `n` query -// parameter. -// -// go-containerregistry sends `n=1000` by default to ask for large pages. Some -// registries reject an `n` they do not implement (400/UNSUPPORTED) instead of -// ignoring it, which would make `d8 cr ls` unusable against them for no good -// reason: dropping `n` costs only extra round trips, since the cursor walk -// collects the full list either way. remote.WithPageSize(0) omits the -// parameter entirely. -func newTagLister(ctx context.Context, repo name.Repository, opts *Options) (*remote.Lister, error) { - lister, err := tagLister(ctx, repo, opts.remoteWithContext(ctx)) - if err == nil { - return lister, nil - } - - lister, retryErr := tagLister(ctx, repo, append(opts.remoteWithContext(ctx), remote.WithPageSize(0))) - if retryErr != nil { - // Surface the original failure: the retry is a compatibility - // workaround, so its error (an identical 401, most of the time) is - // rarely the more informative of the two. - return nil, fmt.Errorf("read tags for %s: %w", repo, err) - } - - return lister, nil -} - -func tagLister(ctx context.Context, repo name.Repository, remoteOpts []remote.Option) (*remote.Lister, error) { - puller, err := remote.NewPuller(remoteOpts...) + tags, err := client.ListTags(ctx) if err != nil { - return nil, fmt.Errorf("create puller: %w", err) - } - - return puller.Lister(ctx, repo) -} - -// walkTagPages concatenates every page into one slice. -// -// A registry that echoes the same `Link: rel="next"` cursor on every response -// (i.e. one that ignores `last=`) keeps HasNext true forever, so the walk -// would spin and accumulate duplicates until the user interrupted it. -// Refusing a cursor we have already followed turns that into a clean error; -// a repeated cursor can never make progress, so this cannot reject a -// legitimate listing. -func walkTagPages(ctx context.Context, lister *remote.Lister, repo name.Repository) ([]string, error) { - var out []string - - seen := make(map[string]struct{}) - - for lister.HasNext() { - if err := ctx.Err(); err != nil { - return nil, err - } - - page, err := lister.Next(ctx) - if err != nil { - return nil, fmt.Errorf("read tags for %s: %w", repo, err) - } - - out = append(out, page.Tags...) - - if page.Next == "" { - break - } - - if _, dup := seen[page.Next]; dup { - return nil, fmt.Errorf("read tags for %s: registry keeps returning the same pagination cursor %q, refusing to loop", repo, page.Next) - } - - seen[page.Next] = struct{}{} + return nil, fmt.Errorf("read tags for %s: %w", repoRef, err) } - return out, nil + return tags, nil } diff --git a/internal/cr/internal/registry/transport.go b/internal/cr/internal/registry/transport.go deleted file mode 100644 index d08ab81ce..000000000 --- a/internal/cr/internal/registry/transport.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2026 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package registry - -import ( - "crypto/tls" - "net/http" - - "github.com/google/go-containerregistry/pkg/v1/remote" -) - -// InsecureTransport returns a fresh http.Transport cloned from remote's -// default with TLS verification disabled. Use only when the user explicitly -// opts in via --insecure. -func InsecureTransport() http.RoundTripper { - t := remote.DefaultTransport.(*http.Transport).Clone() - t.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // user-opted via --insecure - - return t -} diff --git a/internal/plugins/source_legacy_test.go b/internal/plugins/source_legacy_test.go index 4b8513138..f974e1552 100644 --- a/internal/plugins/source_legacy_test.go +++ b/internal/plugins/source_legacy_test.go @@ -306,7 +306,7 @@ type fakeManifestClient struct { gotTags []string } -func (c *fakeManifestClient) GetManifest(_ context.Context, tag string) (dkpreg.ManifestResult, error) { +func (c *fakeManifestClient) GetManifest(_ context.Context, tag string, _ ...dkpreg.ManifestGetOption) (dkpreg.ManifestResult, error) { c.gotTags = append(c.gotTags, tag) res, ok := c.byTag[tag] diff --git a/pkg/mock/registry_client_mock.go b/pkg/mock/registry_client_mock.go index 9ab7039b7..60e0c12b4 100644 --- a/pkg/mock/registry_client_mock.go +++ b/pkg/mock/registry_client_mock.go @@ -69,9 +69,16 @@ type RegistryClientMock struct { beforeGetImageConfigCounter uint64 GetImageConfigMock mRegistryClientMockGetImageConfig - funcGetManifest func(ctx context.Context, tag string) (m1 mm_registry.ManifestResult, err error) + funcGetIndex func(ctx context.Context, tag string) (i1 v1.ImageIndex, err error) + funcGetIndexOrigin string + inspectFuncGetIndex func(ctx context.Context, tag string) + afterGetIndexCounter uint64 + beforeGetIndexCounter uint64 + GetIndexMock mRegistryClientMockGetIndex + + funcGetManifest func(ctx context.Context, tag string, opts ...mm_registry.ManifestGetOption) (m1 mm_registry.ManifestResult, err error) funcGetManifestOrigin string - inspectFuncGetManifest func(ctx context.Context, tag string) + inspectFuncGetManifest func(ctx context.Context, tag string, opts ...mm_registry.ManifestGetOption) afterGetManifestCounter uint64 beforeGetManifestCounter uint64 GetManifestMock mRegistryClientMockGetManifest @@ -111,6 +118,20 @@ type RegistryClientMock struct { beforePushIndexCounter uint64 PushIndexMock mRegistryClientMockPushIndex + funcStreamRepositories func(ctx context.Context, visit func(repos []string) error, opts ...mm_registry.ListRepositoriesOption) (err error) + funcStreamRepositoriesOrigin string + inspectFuncStreamRepositories func(ctx context.Context, visit func(repos []string) error, opts ...mm_registry.ListRepositoriesOption) + afterStreamRepositoriesCounter uint64 + beforeStreamRepositoriesCounter uint64 + StreamRepositoriesMock mRegistryClientMockStreamRepositories + + funcStreamTags func(ctx context.Context, visit func(tags []string) error, opts ...mm_registry.ListTagsOption) (err error) + funcStreamTagsOrigin string + inspectFuncStreamTags func(ctx context.Context, visit func(tags []string) error, opts ...mm_registry.ListTagsOption) + afterStreamTagsCounter uint64 + beforeStreamTagsCounter uint64 + StreamTagsMock mRegistryClientMockStreamTags + funcTagImage func(ctx context.Context, sourceTag string, destTag string) (err error) funcTagImageOrigin string inspectFuncTagImage func(ctx context.Context, sourceTag string, destTag string) @@ -155,6 +176,9 @@ func NewRegistryClientMock(t minimock.Tester) *RegistryClientMock { m.GetImageConfigMock = mRegistryClientMockGetImageConfig{mock: m} m.GetImageConfigMock.callArgs = []*RegistryClientMockGetImageConfigParams{} + m.GetIndexMock = mRegistryClientMockGetIndex{mock: m} + m.GetIndexMock.callArgs = []*RegistryClientMockGetIndexParams{} + m.GetManifestMock = mRegistryClientMockGetManifest{mock: m} m.GetManifestMock.callArgs = []*RegistryClientMockGetManifestParams{} @@ -172,6 +196,12 @@ func NewRegistryClientMock(t minimock.Tester) *RegistryClientMock { m.PushIndexMock = mRegistryClientMockPushIndex{mock: m} m.PushIndexMock.callArgs = []*RegistryClientMockPushIndexParams{} + m.StreamRepositoriesMock = mRegistryClientMockStreamRepositories{mock: m} + m.StreamRepositoriesMock.callArgs = []*RegistryClientMockStreamRepositoriesParams{} + + m.StreamTagsMock = mRegistryClientMockStreamTags{mock: m} + m.StreamTagsMock.callArgs = []*RegistryClientMockStreamTagsParams{} + m.TagImageMock = mRegistryClientMockTagImage{mock: m} m.TagImageMock.callArgs = []*RegistryClientMockTagImageParams{} @@ -2673,6 +2703,349 @@ func (m *RegistryClientMock) MinimockGetImageConfigInspect() { } } +type mRegistryClientMockGetIndex struct { + optional bool + mock *RegistryClientMock + defaultExpectation *RegistryClientMockGetIndexExpectation + expectations []*RegistryClientMockGetIndexExpectation + + callArgs []*RegistryClientMockGetIndexParams + mutex sync.RWMutex + + expectedInvocations uint64 + expectedInvocationsOrigin string +} + +// RegistryClientMockGetIndexExpectation specifies expectation struct of the Client.GetIndex +type RegistryClientMockGetIndexExpectation struct { + mock *RegistryClientMock + params *RegistryClientMockGetIndexParams + paramPtrs *RegistryClientMockGetIndexParamPtrs + expectationOrigins RegistryClientMockGetIndexExpectationOrigins + results *RegistryClientMockGetIndexResults + returnOrigin string + Counter uint64 +} + +// RegistryClientMockGetIndexParams contains parameters of the Client.GetIndex +type RegistryClientMockGetIndexParams struct { + ctx context.Context + tag string +} + +// RegistryClientMockGetIndexParamPtrs contains pointers to parameters of the Client.GetIndex +type RegistryClientMockGetIndexParamPtrs struct { + ctx *context.Context + tag *string +} + +// RegistryClientMockGetIndexResults contains results of the Client.GetIndex +type RegistryClientMockGetIndexResults struct { + i1 v1.ImageIndex + err error +} + +// RegistryClientMockGetIndexOrigins contains origins of expectations of the Client.GetIndex +type RegistryClientMockGetIndexExpectationOrigins struct { + origin string + originCtx string + originTag string +} + +// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning +// the test will fail minimock's automatic final call check if the mocked method was not called at least once. +// Optional() makes method check to work in '0 or more' mode. +// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to +// catch the problems when the expected method call is totally skipped during test run. +func (mmGetIndex *mRegistryClientMockGetIndex) Optional() *mRegistryClientMockGetIndex { + mmGetIndex.optional = true + return mmGetIndex +} + +// Expect sets up expected params for Client.GetIndex +func (mmGetIndex *mRegistryClientMockGetIndex) Expect(ctx context.Context, tag string) *mRegistryClientMockGetIndex { + if mmGetIndex.mock.funcGetIndex != nil { + mmGetIndex.mock.t.Fatalf("RegistryClientMock.GetIndex mock is already set by Set") + } + + if mmGetIndex.defaultExpectation == nil { + mmGetIndex.defaultExpectation = &RegistryClientMockGetIndexExpectation{} + } + + if mmGetIndex.defaultExpectation.paramPtrs != nil { + mmGetIndex.mock.t.Fatalf("RegistryClientMock.GetIndex mock is already set by ExpectParams functions") + } + + mmGetIndex.defaultExpectation.params = &RegistryClientMockGetIndexParams{ctx, tag} + mmGetIndex.defaultExpectation.expectationOrigins.origin = minimock.CallerInfo(1) + for _, e := range mmGetIndex.expectations { + if minimock.Equal(e.params, mmGetIndex.defaultExpectation.params) { + mmGetIndex.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGetIndex.defaultExpectation.params) + } + } + + return mmGetIndex +} + +// ExpectCtxParam1 sets up expected param ctx for Client.GetIndex +func (mmGetIndex *mRegistryClientMockGetIndex) ExpectCtxParam1(ctx context.Context) *mRegistryClientMockGetIndex { + if mmGetIndex.mock.funcGetIndex != nil { + mmGetIndex.mock.t.Fatalf("RegistryClientMock.GetIndex mock is already set by Set") + } + + if mmGetIndex.defaultExpectation == nil { + mmGetIndex.defaultExpectation = &RegistryClientMockGetIndexExpectation{} + } + + if mmGetIndex.defaultExpectation.params != nil { + mmGetIndex.mock.t.Fatalf("RegistryClientMock.GetIndex mock is already set by Expect") + } + + if mmGetIndex.defaultExpectation.paramPtrs == nil { + mmGetIndex.defaultExpectation.paramPtrs = &RegistryClientMockGetIndexParamPtrs{} + } + mmGetIndex.defaultExpectation.paramPtrs.ctx = &ctx + mmGetIndex.defaultExpectation.expectationOrigins.originCtx = minimock.CallerInfo(1) + + return mmGetIndex +} + +// ExpectTagParam2 sets up expected param tag for Client.GetIndex +func (mmGetIndex *mRegistryClientMockGetIndex) ExpectTagParam2(tag string) *mRegistryClientMockGetIndex { + if mmGetIndex.mock.funcGetIndex != nil { + mmGetIndex.mock.t.Fatalf("RegistryClientMock.GetIndex mock is already set by Set") + } + + if mmGetIndex.defaultExpectation == nil { + mmGetIndex.defaultExpectation = &RegistryClientMockGetIndexExpectation{} + } + + if mmGetIndex.defaultExpectation.params != nil { + mmGetIndex.mock.t.Fatalf("RegistryClientMock.GetIndex mock is already set by Expect") + } + + if mmGetIndex.defaultExpectation.paramPtrs == nil { + mmGetIndex.defaultExpectation.paramPtrs = &RegistryClientMockGetIndexParamPtrs{} + } + mmGetIndex.defaultExpectation.paramPtrs.tag = &tag + mmGetIndex.defaultExpectation.expectationOrigins.originTag = minimock.CallerInfo(1) + + return mmGetIndex +} + +// Inspect accepts an inspector function that has same arguments as the Client.GetIndex +func (mmGetIndex *mRegistryClientMockGetIndex) Inspect(f func(ctx context.Context, tag string)) *mRegistryClientMockGetIndex { + if mmGetIndex.mock.inspectFuncGetIndex != nil { + mmGetIndex.mock.t.Fatalf("Inspect function is already set for RegistryClientMock.GetIndex") + } + + mmGetIndex.mock.inspectFuncGetIndex = f + + return mmGetIndex +} + +// Return sets up results that will be returned by Client.GetIndex +func (mmGetIndex *mRegistryClientMockGetIndex) Return(i1 v1.ImageIndex, err error) *RegistryClientMock { + if mmGetIndex.mock.funcGetIndex != nil { + mmGetIndex.mock.t.Fatalf("RegistryClientMock.GetIndex mock is already set by Set") + } + + if mmGetIndex.defaultExpectation == nil { + mmGetIndex.defaultExpectation = &RegistryClientMockGetIndexExpectation{mock: mmGetIndex.mock} + } + mmGetIndex.defaultExpectation.results = &RegistryClientMockGetIndexResults{i1, err} + mmGetIndex.defaultExpectation.returnOrigin = minimock.CallerInfo(1) + return mmGetIndex.mock +} + +// Set uses given function f to mock the Client.GetIndex method +func (mmGetIndex *mRegistryClientMockGetIndex) Set(f func(ctx context.Context, tag string) (i1 v1.ImageIndex, err error)) *RegistryClientMock { + if mmGetIndex.defaultExpectation != nil { + mmGetIndex.mock.t.Fatalf("Default expectation is already set for the Client.GetIndex method") + } + + if len(mmGetIndex.expectations) > 0 { + mmGetIndex.mock.t.Fatalf("Some expectations are already set for the Client.GetIndex method") + } + + mmGetIndex.mock.funcGetIndex = f + mmGetIndex.mock.funcGetIndexOrigin = minimock.CallerInfo(1) + return mmGetIndex.mock +} + +// When sets expectation for the Client.GetIndex which will trigger the result defined by the following +// Then helper +func (mmGetIndex *mRegistryClientMockGetIndex) When(ctx context.Context, tag string) *RegistryClientMockGetIndexExpectation { + if mmGetIndex.mock.funcGetIndex != nil { + mmGetIndex.mock.t.Fatalf("RegistryClientMock.GetIndex mock is already set by Set") + } + + expectation := &RegistryClientMockGetIndexExpectation{ + mock: mmGetIndex.mock, + params: &RegistryClientMockGetIndexParams{ctx, tag}, + expectationOrigins: RegistryClientMockGetIndexExpectationOrigins{origin: minimock.CallerInfo(1)}, + } + mmGetIndex.expectations = append(mmGetIndex.expectations, expectation) + return expectation +} + +// Then sets up Client.GetIndex return parameters for the expectation previously defined by the When method +func (e *RegistryClientMockGetIndexExpectation) Then(i1 v1.ImageIndex, err error) *RegistryClientMock { + e.results = &RegistryClientMockGetIndexResults{i1, err} + return e.mock +} + +// Times sets number of times Client.GetIndex should be invoked +func (mmGetIndex *mRegistryClientMockGetIndex) Times(n uint64) *mRegistryClientMockGetIndex { + if n == 0 { + mmGetIndex.mock.t.Fatalf("Times of RegistryClientMock.GetIndex mock can not be zero") + } + mm_atomic.StoreUint64(&mmGetIndex.expectedInvocations, n) + mmGetIndex.expectedInvocationsOrigin = minimock.CallerInfo(1) + return mmGetIndex +} + +func (mmGetIndex *mRegistryClientMockGetIndex) invocationsDone() bool { + if len(mmGetIndex.expectations) == 0 && mmGetIndex.defaultExpectation == nil && mmGetIndex.mock.funcGetIndex == nil { + return true + } + + totalInvocations := mm_atomic.LoadUint64(&mmGetIndex.mock.afterGetIndexCounter) + expectedInvocations := mm_atomic.LoadUint64(&mmGetIndex.expectedInvocations) + + return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations) +} + +// GetIndex implements mm_registry.Client +func (mmGetIndex *RegistryClientMock) GetIndex(ctx context.Context, tag string) (i1 v1.ImageIndex, err error) { + mm_atomic.AddUint64(&mmGetIndex.beforeGetIndexCounter, 1) + defer mm_atomic.AddUint64(&mmGetIndex.afterGetIndexCounter, 1) + + mmGetIndex.t.Helper() + + if mmGetIndex.inspectFuncGetIndex != nil { + mmGetIndex.inspectFuncGetIndex(ctx, tag) + } + + mm_params := RegistryClientMockGetIndexParams{ctx, tag} + + // Record call args + mmGetIndex.GetIndexMock.mutex.Lock() + mmGetIndex.GetIndexMock.callArgs = append(mmGetIndex.GetIndexMock.callArgs, &mm_params) + mmGetIndex.GetIndexMock.mutex.Unlock() + + for _, e := range mmGetIndex.GetIndexMock.expectations { + if minimock.Equal(*e.params, mm_params) { + mm_atomic.AddUint64(&e.Counter, 1) + return e.results.i1, e.results.err + } + } + + if mmGetIndex.GetIndexMock.defaultExpectation != nil { + mm_atomic.AddUint64(&mmGetIndex.GetIndexMock.defaultExpectation.Counter, 1) + mm_want := mmGetIndex.GetIndexMock.defaultExpectation.params + mm_want_ptrs := mmGetIndex.GetIndexMock.defaultExpectation.paramPtrs + + mm_got := RegistryClientMockGetIndexParams{ctx, tag} + + if mm_want_ptrs != nil { + + if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) { + mmGetIndex.t.Errorf("RegistryClientMock.GetIndex got unexpected parameter ctx, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmGetIndex.GetIndexMock.defaultExpectation.expectationOrigins.originCtx, *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx)) + } + + if mm_want_ptrs.tag != nil && !minimock.Equal(*mm_want_ptrs.tag, mm_got.tag) { + mmGetIndex.t.Errorf("RegistryClientMock.GetIndex got unexpected parameter tag, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmGetIndex.GetIndexMock.defaultExpectation.expectationOrigins.originTag, *mm_want_ptrs.tag, mm_got.tag, minimock.Diff(*mm_want_ptrs.tag, mm_got.tag)) + } + + } else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { + mmGetIndex.t.Errorf("RegistryClientMock.GetIndex got unexpected parameters, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmGetIndex.GetIndexMock.defaultExpectation.expectationOrigins.origin, *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) + } + + mm_results := mmGetIndex.GetIndexMock.defaultExpectation.results + if mm_results == nil { + mmGetIndex.t.Fatal("No results are set for the RegistryClientMock.GetIndex") + } + return (*mm_results).i1, (*mm_results).err + } + if mmGetIndex.funcGetIndex != nil { + return mmGetIndex.funcGetIndex(ctx, tag) + } + mmGetIndex.t.Fatalf("Unexpected call to RegistryClientMock.GetIndex. %v %v", ctx, tag) + return +} + +// GetIndexAfterCounter returns a count of finished RegistryClientMock.GetIndex invocations +func (mmGetIndex *RegistryClientMock) GetIndexAfterCounter() uint64 { + return mm_atomic.LoadUint64(&mmGetIndex.afterGetIndexCounter) +} + +// GetIndexBeforeCounter returns a count of RegistryClientMock.GetIndex invocations +func (mmGetIndex *RegistryClientMock) GetIndexBeforeCounter() uint64 { + return mm_atomic.LoadUint64(&mmGetIndex.beforeGetIndexCounter) +} + +// Calls returns a list of arguments used in each call to RegistryClientMock.GetIndex. +// The list is in the same order as the calls were made (i.e. recent calls have a higher index) +func (mmGetIndex *mRegistryClientMockGetIndex) Calls() []*RegistryClientMockGetIndexParams { + mmGetIndex.mutex.RLock() + + argCopy := make([]*RegistryClientMockGetIndexParams, len(mmGetIndex.callArgs)) + copy(argCopy, mmGetIndex.callArgs) + + mmGetIndex.mutex.RUnlock() + + return argCopy +} + +// MinimockGetIndexDone returns true if the count of the GetIndex invocations corresponds +// the number of defined expectations +func (m *RegistryClientMock) MinimockGetIndexDone() bool { + if m.GetIndexMock.optional { + // Optional methods provide '0 or more' call count restriction. + return true + } + + for _, e := range m.GetIndexMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + return false + } + } + + return m.GetIndexMock.invocationsDone() +} + +// MinimockGetIndexInspect logs each unmet expectation +func (m *RegistryClientMock) MinimockGetIndexInspect() { + for _, e := range m.GetIndexMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + m.t.Errorf("Expected call to RegistryClientMock.GetIndex at\n%s with params: %#v", e.expectationOrigins.origin, *e.params) + } + } + + afterGetIndexCounter := mm_atomic.LoadUint64(&m.afterGetIndexCounter) + // if default expectation was set then invocations count should be greater than zero + if m.GetIndexMock.defaultExpectation != nil && afterGetIndexCounter < 1 { + if m.GetIndexMock.defaultExpectation.params == nil { + m.t.Errorf("Expected call to RegistryClientMock.GetIndex at\n%s", m.GetIndexMock.defaultExpectation.returnOrigin) + } else { + m.t.Errorf("Expected call to RegistryClientMock.GetIndex at\n%s with params: %#v", m.GetIndexMock.defaultExpectation.expectationOrigins.origin, *m.GetIndexMock.defaultExpectation.params) + } + } + // if func was set then invocations count should be greater than zero + if m.funcGetIndex != nil && afterGetIndexCounter < 1 { + m.t.Errorf("Expected call to RegistryClientMock.GetIndex at\n%s", m.funcGetIndexOrigin) + } + + if !m.GetIndexMock.invocationsDone() && afterGetIndexCounter > 0 { + m.t.Errorf("Expected %d calls to RegistryClientMock.GetIndex at\n%s but found %d calls", + mm_atomic.LoadUint64(&m.GetIndexMock.expectedInvocations), m.GetIndexMock.expectedInvocationsOrigin, afterGetIndexCounter) + } +} + type mRegistryClientMockGetManifest struct { optional bool mock *RegistryClientMock @@ -2699,14 +3072,16 @@ type RegistryClientMockGetManifestExpectation struct { // RegistryClientMockGetManifestParams contains parameters of the Client.GetManifest type RegistryClientMockGetManifestParams struct { - ctx context.Context - tag string + ctx context.Context + tag string + opts []mm_registry.ManifestGetOption } // RegistryClientMockGetManifestParamPtrs contains pointers to parameters of the Client.GetManifest type RegistryClientMockGetManifestParamPtrs struct { - ctx *context.Context - tag *string + ctx *context.Context + tag *string + opts *[]mm_registry.ManifestGetOption } // RegistryClientMockGetManifestResults contains results of the Client.GetManifest @@ -2717,9 +3092,10 @@ type RegistryClientMockGetManifestResults struct { // RegistryClientMockGetManifestOrigins contains origins of expectations of the Client.GetManifest type RegistryClientMockGetManifestExpectationOrigins struct { - origin string - originCtx string - originTag string + origin string + originCtx string + originTag string + originOpts string } // Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning @@ -2733,7 +3109,7 @@ func (mmGetManifest *mRegistryClientMockGetManifest) Optional() *mRegistryClient } // Expect sets up expected params for Client.GetManifest -func (mmGetManifest *mRegistryClientMockGetManifest) Expect(ctx context.Context, tag string) *mRegistryClientMockGetManifest { +func (mmGetManifest *mRegistryClientMockGetManifest) Expect(ctx context.Context, tag string, opts ...mm_registry.ManifestGetOption) *mRegistryClientMockGetManifest { if mmGetManifest.mock.funcGetManifest != nil { mmGetManifest.mock.t.Fatalf("RegistryClientMock.GetManifest mock is already set by Set") } @@ -2746,7 +3122,7 @@ func (mmGetManifest *mRegistryClientMockGetManifest) Expect(ctx context.Context, mmGetManifest.mock.t.Fatalf("RegistryClientMock.GetManifest mock is already set by ExpectParams functions") } - mmGetManifest.defaultExpectation.params = &RegistryClientMockGetManifestParams{ctx, tag} + mmGetManifest.defaultExpectation.params = &RegistryClientMockGetManifestParams{ctx, tag, opts} mmGetManifest.defaultExpectation.expectationOrigins.origin = minimock.CallerInfo(1) for _, e := range mmGetManifest.expectations { if minimock.Equal(e.params, mmGetManifest.defaultExpectation.params) { @@ -2803,8 +3179,31 @@ func (mmGetManifest *mRegistryClientMockGetManifest) ExpectTagParam2(tag string) return mmGetManifest } +// ExpectOptsParam3 sets up expected param opts for Client.GetManifest +func (mmGetManifest *mRegistryClientMockGetManifest) ExpectOptsParam3(opts ...mm_registry.ManifestGetOption) *mRegistryClientMockGetManifest { + if mmGetManifest.mock.funcGetManifest != nil { + mmGetManifest.mock.t.Fatalf("RegistryClientMock.GetManifest mock is already set by Set") + } + + if mmGetManifest.defaultExpectation == nil { + mmGetManifest.defaultExpectation = &RegistryClientMockGetManifestExpectation{} + } + + if mmGetManifest.defaultExpectation.params != nil { + mmGetManifest.mock.t.Fatalf("RegistryClientMock.GetManifest mock is already set by Expect") + } + + if mmGetManifest.defaultExpectation.paramPtrs == nil { + mmGetManifest.defaultExpectation.paramPtrs = &RegistryClientMockGetManifestParamPtrs{} + } + mmGetManifest.defaultExpectation.paramPtrs.opts = &opts + mmGetManifest.defaultExpectation.expectationOrigins.originOpts = minimock.CallerInfo(1) + + return mmGetManifest +} + // Inspect accepts an inspector function that has same arguments as the Client.GetManifest -func (mmGetManifest *mRegistryClientMockGetManifest) Inspect(f func(ctx context.Context, tag string)) *mRegistryClientMockGetManifest { +func (mmGetManifest *mRegistryClientMockGetManifest) Inspect(f func(ctx context.Context, tag string, opts ...mm_registry.ManifestGetOption)) *mRegistryClientMockGetManifest { if mmGetManifest.mock.inspectFuncGetManifest != nil { mmGetManifest.mock.t.Fatalf("Inspect function is already set for RegistryClientMock.GetManifest") } @@ -2829,7 +3228,7 @@ func (mmGetManifest *mRegistryClientMockGetManifest) Return(m1 mm_registry.Manif } // Set uses given function f to mock the Client.GetManifest method -func (mmGetManifest *mRegistryClientMockGetManifest) Set(f func(ctx context.Context, tag string) (m1 mm_registry.ManifestResult, err error)) *RegistryClientMock { +func (mmGetManifest *mRegistryClientMockGetManifest) Set(f func(ctx context.Context, tag string, opts ...mm_registry.ManifestGetOption) (m1 mm_registry.ManifestResult, err error)) *RegistryClientMock { if mmGetManifest.defaultExpectation != nil { mmGetManifest.mock.t.Fatalf("Default expectation is already set for the Client.GetManifest method") } @@ -2845,14 +3244,14 @@ func (mmGetManifest *mRegistryClientMockGetManifest) Set(f func(ctx context.Cont // When sets expectation for the Client.GetManifest which will trigger the result defined by the following // Then helper -func (mmGetManifest *mRegistryClientMockGetManifest) When(ctx context.Context, tag string) *RegistryClientMockGetManifestExpectation { +func (mmGetManifest *mRegistryClientMockGetManifest) When(ctx context.Context, tag string, opts ...mm_registry.ManifestGetOption) *RegistryClientMockGetManifestExpectation { if mmGetManifest.mock.funcGetManifest != nil { mmGetManifest.mock.t.Fatalf("RegistryClientMock.GetManifest mock is already set by Set") } expectation := &RegistryClientMockGetManifestExpectation{ mock: mmGetManifest.mock, - params: &RegistryClientMockGetManifestParams{ctx, tag}, + params: &RegistryClientMockGetManifestParams{ctx, tag, opts}, expectationOrigins: RegistryClientMockGetManifestExpectationOrigins{origin: minimock.CallerInfo(1)}, } mmGetManifest.expectations = append(mmGetManifest.expectations, expectation) @@ -2887,17 +3286,17 @@ func (mmGetManifest *mRegistryClientMockGetManifest) invocationsDone() bool { } // GetManifest implements mm_registry.Client -func (mmGetManifest *RegistryClientMock) GetManifest(ctx context.Context, tag string) (m1 mm_registry.ManifestResult, err error) { +func (mmGetManifest *RegistryClientMock) GetManifest(ctx context.Context, tag string, opts ...mm_registry.ManifestGetOption) (m1 mm_registry.ManifestResult, err error) { mm_atomic.AddUint64(&mmGetManifest.beforeGetManifestCounter, 1) defer mm_atomic.AddUint64(&mmGetManifest.afterGetManifestCounter, 1) mmGetManifest.t.Helper() if mmGetManifest.inspectFuncGetManifest != nil { - mmGetManifest.inspectFuncGetManifest(ctx, tag) + mmGetManifest.inspectFuncGetManifest(ctx, tag, opts...) } - mm_params := RegistryClientMockGetManifestParams{ctx, tag} + mm_params := RegistryClientMockGetManifestParams{ctx, tag, opts} // Record call args mmGetManifest.GetManifestMock.mutex.Lock() @@ -2916,7 +3315,7 @@ func (mmGetManifest *RegistryClientMock) GetManifest(ctx context.Context, tag st mm_want := mmGetManifest.GetManifestMock.defaultExpectation.params mm_want_ptrs := mmGetManifest.GetManifestMock.defaultExpectation.paramPtrs - mm_got := RegistryClientMockGetManifestParams{ctx, tag} + mm_got := RegistryClientMockGetManifestParams{ctx, tag, opts} if mm_want_ptrs != nil { @@ -2930,6 +3329,11 @@ func (mmGetManifest *RegistryClientMock) GetManifest(ctx context.Context, tag st mmGetManifest.GetManifestMock.defaultExpectation.expectationOrigins.originTag, *mm_want_ptrs.tag, mm_got.tag, minimock.Diff(*mm_want_ptrs.tag, mm_got.tag)) } + if mm_want_ptrs.opts != nil && !minimock.Equal(*mm_want_ptrs.opts, mm_got.opts) { + mmGetManifest.t.Errorf("RegistryClientMock.GetManifest got unexpected parameter opts, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmGetManifest.GetManifestMock.defaultExpectation.expectationOrigins.originOpts, *mm_want_ptrs.opts, mm_got.opts, minimock.Diff(*mm_want_ptrs.opts, mm_got.opts)) + } + } else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { mmGetManifest.t.Errorf("RegistryClientMock.GetManifest got unexpected parameters, expected at\n%s:\nwant: %#v\n got: %#v%s\n", mmGetManifest.GetManifestMock.defaultExpectation.expectationOrigins.origin, *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) @@ -2942,9 +3346,9 @@ func (mmGetManifest *RegistryClientMock) GetManifest(ctx context.Context, tag st return (*mm_results).m1, (*mm_results).err } if mmGetManifest.funcGetManifest != nil { - return mmGetManifest.funcGetManifest(ctx, tag) + return mmGetManifest.funcGetManifest(ctx, tag, opts...) } - mmGetManifest.t.Fatalf("Unexpected call to RegistryClientMock.GetManifest. %v %v", ctx, tag) + mmGetManifest.t.Fatalf("Unexpected call to RegistryClientMock.GetManifest. %v %v %v", ctx, tag, opts) return } @@ -4696,55 +5100,55 @@ func (m *RegistryClientMock) MinimockPushIndexInspect() { } } -type mRegistryClientMockTagImage struct { +type mRegistryClientMockStreamRepositories struct { optional bool mock *RegistryClientMock - defaultExpectation *RegistryClientMockTagImageExpectation - expectations []*RegistryClientMockTagImageExpectation + defaultExpectation *RegistryClientMockStreamRepositoriesExpectation + expectations []*RegistryClientMockStreamRepositoriesExpectation - callArgs []*RegistryClientMockTagImageParams + callArgs []*RegistryClientMockStreamRepositoriesParams mutex sync.RWMutex expectedInvocations uint64 expectedInvocationsOrigin string } -// RegistryClientMockTagImageExpectation specifies expectation struct of the Client.TagImage -type RegistryClientMockTagImageExpectation struct { +// RegistryClientMockStreamRepositoriesExpectation specifies expectation struct of the Client.StreamRepositories +type RegistryClientMockStreamRepositoriesExpectation struct { mock *RegistryClientMock - params *RegistryClientMockTagImageParams - paramPtrs *RegistryClientMockTagImageParamPtrs - expectationOrigins RegistryClientMockTagImageExpectationOrigins - results *RegistryClientMockTagImageResults + params *RegistryClientMockStreamRepositoriesParams + paramPtrs *RegistryClientMockStreamRepositoriesParamPtrs + expectationOrigins RegistryClientMockStreamRepositoriesExpectationOrigins + results *RegistryClientMockStreamRepositoriesResults returnOrigin string Counter uint64 } -// RegistryClientMockTagImageParams contains parameters of the Client.TagImage -type RegistryClientMockTagImageParams struct { - ctx context.Context - sourceTag string - destTag string +// RegistryClientMockStreamRepositoriesParams contains parameters of the Client.StreamRepositories +type RegistryClientMockStreamRepositoriesParams struct { + ctx context.Context + visit func(repos []string) error + opts []mm_registry.ListRepositoriesOption } -// RegistryClientMockTagImageParamPtrs contains pointers to parameters of the Client.TagImage -type RegistryClientMockTagImageParamPtrs struct { - ctx *context.Context - sourceTag *string - destTag *string +// RegistryClientMockStreamRepositoriesParamPtrs contains pointers to parameters of the Client.StreamRepositories +type RegistryClientMockStreamRepositoriesParamPtrs struct { + ctx *context.Context + visit *func(repos []string) error + opts *[]mm_registry.ListRepositoriesOption } -// RegistryClientMockTagImageResults contains results of the Client.TagImage -type RegistryClientMockTagImageResults struct { +// RegistryClientMockStreamRepositoriesResults contains results of the Client.StreamRepositories +type RegistryClientMockStreamRepositoriesResults struct { err error } -// RegistryClientMockTagImageOrigins contains origins of expectations of the Client.TagImage -type RegistryClientMockTagImageExpectationOrigins struct { - origin string - originCtx string - originSourceTag string - originDestTag string +// RegistryClientMockStreamRepositoriesOrigins contains origins of expectations of the Client.StreamRepositories +type RegistryClientMockStreamRepositoriesExpectationOrigins struct { + origin string + originCtx string + originVisit string + originOpts string } // Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning @@ -4752,22 +5156,768 @@ type RegistryClientMockTagImageExpectationOrigins struct { // Optional() makes method check to work in '0 or more' mode. // It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to // catch the problems when the expected method call is totally skipped during test run. -func (mmTagImage *mRegistryClientMockTagImage) Optional() *mRegistryClientMockTagImage { - mmTagImage.optional = true - return mmTagImage +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) Optional() *mRegistryClientMockStreamRepositories { + mmStreamRepositories.optional = true + return mmStreamRepositories } -// Expect sets up expected params for Client.TagImage -func (mmTagImage *mRegistryClientMockTagImage) Expect(ctx context.Context, sourceTag string, destTag string) *mRegistryClientMockTagImage { - if mmTagImage.mock.funcTagImage != nil { - mmTagImage.mock.t.Fatalf("RegistryClientMock.TagImage mock is already set by Set") +// Expect sets up expected params for Client.StreamRepositories +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) Expect(ctx context.Context, visit func(repos []string) error, opts ...mm_registry.ListRepositoriesOption) *mRegistryClientMockStreamRepositories { + if mmStreamRepositories.mock.funcStreamRepositories != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Set") } - if mmTagImage.defaultExpectation == nil { - mmTagImage.defaultExpectation = &RegistryClientMockTagImageExpectation{} + if mmStreamRepositories.defaultExpectation == nil { + mmStreamRepositories.defaultExpectation = &RegistryClientMockStreamRepositoriesExpectation{} } - if mmTagImage.defaultExpectation.paramPtrs != nil { + if mmStreamRepositories.defaultExpectation.paramPtrs != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by ExpectParams functions") + } + + mmStreamRepositories.defaultExpectation.params = &RegistryClientMockStreamRepositoriesParams{ctx, visit, opts} + mmStreamRepositories.defaultExpectation.expectationOrigins.origin = minimock.CallerInfo(1) + for _, e := range mmStreamRepositories.expectations { + if minimock.Equal(e.params, mmStreamRepositories.defaultExpectation.params) { + mmStreamRepositories.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmStreamRepositories.defaultExpectation.params) + } + } + + return mmStreamRepositories +} + +// ExpectCtxParam1 sets up expected param ctx for Client.StreamRepositories +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) ExpectCtxParam1(ctx context.Context) *mRegistryClientMockStreamRepositories { + if mmStreamRepositories.mock.funcStreamRepositories != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Set") + } + + if mmStreamRepositories.defaultExpectation == nil { + mmStreamRepositories.defaultExpectation = &RegistryClientMockStreamRepositoriesExpectation{} + } + + if mmStreamRepositories.defaultExpectation.params != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Expect") + } + + if mmStreamRepositories.defaultExpectation.paramPtrs == nil { + mmStreamRepositories.defaultExpectation.paramPtrs = &RegistryClientMockStreamRepositoriesParamPtrs{} + } + mmStreamRepositories.defaultExpectation.paramPtrs.ctx = &ctx + mmStreamRepositories.defaultExpectation.expectationOrigins.originCtx = minimock.CallerInfo(1) + + return mmStreamRepositories +} + +// ExpectVisitParam2 sets up expected param visit for Client.StreamRepositories +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) ExpectVisitParam2(visit func(repos []string) error) *mRegistryClientMockStreamRepositories { + if mmStreamRepositories.mock.funcStreamRepositories != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Set") + } + + if mmStreamRepositories.defaultExpectation == nil { + mmStreamRepositories.defaultExpectation = &RegistryClientMockStreamRepositoriesExpectation{} + } + + if mmStreamRepositories.defaultExpectation.params != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Expect") + } + + if mmStreamRepositories.defaultExpectation.paramPtrs == nil { + mmStreamRepositories.defaultExpectation.paramPtrs = &RegistryClientMockStreamRepositoriesParamPtrs{} + } + mmStreamRepositories.defaultExpectation.paramPtrs.visit = &visit + mmStreamRepositories.defaultExpectation.expectationOrigins.originVisit = minimock.CallerInfo(1) + + return mmStreamRepositories +} + +// ExpectOptsParam3 sets up expected param opts for Client.StreamRepositories +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) ExpectOptsParam3(opts ...mm_registry.ListRepositoriesOption) *mRegistryClientMockStreamRepositories { + if mmStreamRepositories.mock.funcStreamRepositories != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Set") + } + + if mmStreamRepositories.defaultExpectation == nil { + mmStreamRepositories.defaultExpectation = &RegistryClientMockStreamRepositoriesExpectation{} + } + + if mmStreamRepositories.defaultExpectation.params != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Expect") + } + + if mmStreamRepositories.defaultExpectation.paramPtrs == nil { + mmStreamRepositories.defaultExpectation.paramPtrs = &RegistryClientMockStreamRepositoriesParamPtrs{} + } + mmStreamRepositories.defaultExpectation.paramPtrs.opts = &opts + mmStreamRepositories.defaultExpectation.expectationOrigins.originOpts = minimock.CallerInfo(1) + + return mmStreamRepositories +} + +// Inspect accepts an inspector function that has same arguments as the Client.StreamRepositories +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) Inspect(f func(ctx context.Context, visit func(repos []string) error, opts ...mm_registry.ListRepositoriesOption)) *mRegistryClientMockStreamRepositories { + if mmStreamRepositories.mock.inspectFuncStreamRepositories != nil { + mmStreamRepositories.mock.t.Fatalf("Inspect function is already set for RegistryClientMock.StreamRepositories") + } + + mmStreamRepositories.mock.inspectFuncStreamRepositories = f + + return mmStreamRepositories +} + +// Return sets up results that will be returned by Client.StreamRepositories +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) Return(err error) *RegistryClientMock { + if mmStreamRepositories.mock.funcStreamRepositories != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Set") + } + + if mmStreamRepositories.defaultExpectation == nil { + mmStreamRepositories.defaultExpectation = &RegistryClientMockStreamRepositoriesExpectation{mock: mmStreamRepositories.mock} + } + mmStreamRepositories.defaultExpectation.results = &RegistryClientMockStreamRepositoriesResults{err} + mmStreamRepositories.defaultExpectation.returnOrigin = minimock.CallerInfo(1) + return mmStreamRepositories.mock +} + +// Set uses given function f to mock the Client.StreamRepositories method +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) Set(f func(ctx context.Context, visit func(repos []string) error, opts ...mm_registry.ListRepositoriesOption) (err error)) *RegistryClientMock { + if mmStreamRepositories.defaultExpectation != nil { + mmStreamRepositories.mock.t.Fatalf("Default expectation is already set for the Client.StreamRepositories method") + } + + if len(mmStreamRepositories.expectations) > 0 { + mmStreamRepositories.mock.t.Fatalf("Some expectations are already set for the Client.StreamRepositories method") + } + + mmStreamRepositories.mock.funcStreamRepositories = f + mmStreamRepositories.mock.funcStreamRepositoriesOrigin = minimock.CallerInfo(1) + return mmStreamRepositories.mock +} + +// When sets expectation for the Client.StreamRepositories which will trigger the result defined by the following +// Then helper +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) When(ctx context.Context, visit func(repos []string) error, opts ...mm_registry.ListRepositoriesOption) *RegistryClientMockStreamRepositoriesExpectation { + if mmStreamRepositories.mock.funcStreamRepositories != nil { + mmStreamRepositories.mock.t.Fatalf("RegistryClientMock.StreamRepositories mock is already set by Set") + } + + expectation := &RegistryClientMockStreamRepositoriesExpectation{ + mock: mmStreamRepositories.mock, + params: &RegistryClientMockStreamRepositoriesParams{ctx, visit, opts}, + expectationOrigins: RegistryClientMockStreamRepositoriesExpectationOrigins{origin: minimock.CallerInfo(1)}, + } + mmStreamRepositories.expectations = append(mmStreamRepositories.expectations, expectation) + return expectation +} + +// Then sets up Client.StreamRepositories return parameters for the expectation previously defined by the When method +func (e *RegistryClientMockStreamRepositoriesExpectation) Then(err error) *RegistryClientMock { + e.results = &RegistryClientMockStreamRepositoriesResults{err} + return e.mock +} + +// Times sets number of times Client.StreamRepositories should be invoked +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) Times(n uint64) *mRegistryClientMockStreamRepositories { + if n == 0 { + mmStreamRepositories.mock.t.Fatalf("Times of RegistryClientMock.StreamRepositories mock can not be zero") + } + mm_atomic.StoreUint64(&mmStreamRepositories.expectedInvocations, n) + mmStreamRepositories.expectedInvocationsOrigin = minimock.CallerInfo(1) + return mmStreamRepositories +} + +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) invocationsDone() bool { + if len(mmStreamRepositories.expectations) == 0 && mmStreamRepositories.defaultExpectation == nil && mmStreamRepositories.mock.funcStreamRepositories == nil { + return true + } + + totalInvocations := mm_atomic.LoadUint64(&mmStreamRepositories.mock.afterStreamRepositoriesCounter) + expectedInvocations := mm_atomic.LoadUint64(&mmStreamRepositories.expectedInvocations) + + return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations) +} + +// StreamRepositories implements mm_registry.Client +func (mmStreamRepositories *RegistryClientMock) StreamRepositories(ctx context.Context, visit func(repos []string) error, opts ...mm_registry.ListRepositoriesOption) (err error) { + mm_atomic.AddUint64(&mmStreamRepositories.beforeStreamRepositoriesCounter, 1) + defer mm_atomic.AddUint64(&mmStreamRepositories.afterStreamRepositoriesCounter, 1) + + mmStreamRepositories.t.Helper() + + if mmStreamRepositories.inspectFuncStreamRepositories != nil { + mmStreamRepositories.inspectFuncStreamRepositories(ctx, visit, opts...) + } + + mm_params := RegistryClientMockStreamRepositoriesParams{ctx, visit, opts} + + // Record call args + mmStreamRepositories.StreamRepositoriesMock.mutex.Lock() + mmStreamRepositories.StreamRepositoriesMock.callArgs = append(mmStreamRepositories.StreamRepositoriesMock.callArgs, &mm_params) + mmStreamRepositories.StreamRepositoriesMock.mutex.Unlock() + + for _, e := range mmStreamRepositories.StreamRepositoriesMock.expectations { + if minimock.Equal(*e.params, mm_params) { + mm_atomic.AddUint64(&e.Counter, 1) + return e.results.err + } + } + + if mmStreamRepositories.StreamRepositoriesMock.defaultExpectation != nil { + mm_atomic.AddUint64(&mmStreamRepositories.StreamRepositoriesMock.defaultExpectation.Counter, 1) + mm_want := mmStreamRepositories.StreamRepositoriesMock.defaultExpectation.params + mm_want_ptrs := mmStreamRepositories.StreamRepositoriesMock.defaultExpectation.paramPtrs + + mm_got := RegistryClientMockStreamRepositoriesParams{ctx, visit, opts} + + if mm_want_ptrs != nil { + + if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) { + mmStreamRepositories.t.Errorf("RegistryClientMock.StreamRepositories got unexpected parameter ctx, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmStreamRepositories.StreamRepositoriesMock.defaultExpectation.expectationOrigins.originCtx, *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx)) + } + + if mm_want_ptrs.visit != nil && !minimock.Equal(*mm_want_ptrs.visit, mm_got.visit) { + mmStreamRepositories.t.Errorf("RegistryClientMock.StreamRepositories got unexpected parameter visit, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmStreamRepositories.StreamRepositoriesMock.defaultExpectation.expectationOrigins.originVisit, *mm_want_ptrs.visit, mm_got.visit, minimock.Diff(*mm_want_ptrs.visit, mm_got.visit)) + } + + if mm_want_ptrs.opts != nil && !minimock.Equal(*mm_want_ptrs.opts, mm_got.opts) { + mmStreamRepositories.t.Errorf("RegistryClientMock.StreamRepositories got unexpected parameter opts, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmStreamRepositories.StreamRepositoriesMock.defaultExpectation.expectationOrigins.originOpts, *mm_want_ptrs.opts, mm_got.opts, minimock.Diff(*mm_want_ptrs.opts, mm_got.opts)) + } + + } else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { + mmStreamRepositories.t.Errorf("RegistryClientMock.StreamRepositories got unexpected parameters, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmStreamRepositories.StreamRepositoriesMock.defaultExpectation.expectationOrigins.origin, *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) + } + + mm_results := mmStreamRepositories.StreamRepositoriesMock.defaultExpectation.results + if mm_results == nil { + mmStreamRepositories.t.Fatal("No results are set for the RegistryClientMock.StreamRepositories") + } + return (*mm_results).err + } + if mmStreamRepositories.funcStreamRepositories != nil { + return mmStreamRepositories.funcStreamRepositories(ctx, visit, opts...) + } + mmStreamRepositories.t.Fatalf("Unexpected call to RegistryClientMock.StreamRepositories. %v %v %v", ctx, visit, opts) + return +} + +// StreamRepositoriesAfterCounter returns a count of finished RegistryClientMock.StreamRepositories invocations +func (mmStreamRepositories *RegistryClientMock) StreamRepositoriesAfterCounter() uint64 { + return mm_atomic.LoadUint64(&mmStreamRepositories.afterStreamRepositoriesCounter) +} + +// StreamRepositoriesBeforeCounter returns a count of RegistryClientMock.StreamRepositories invocations +func (mmStreamRepositories *RegistryClientMock) StreamRepositoriesBeforeCounter() uint64 { + return mm_atomic.LoadUint64(&mmStreamRepositories.beforeStreamRepositoriesCounter) +} + +// Calls returns a list of arguments used in each call to RegistryClientMock.StreamRepositories. +// The list is in the same order as the calls were made (i.e. recent calls have a higher index) +func (mmStreamRepositories *mRegistryClientMockStreamRepositories) Calls() []*RegistryClientMockStreamRepositoriesParams { + mmStreamRepositories.mutex.RLock() + + argCopy := make([]*RegistryClientMockStreamRepositoriesParams, len(mmStreamRepositories.callArgs)) + copy(argCopy, mmStreamRepositories.callArgs) + + mmStreamRepositories.mutex.RUnlock() + + return argCopy +} + +// MinimockStreamRepositoriesDone returns true if the count of the StreamRepositories invocations corresponds +// the number of defined expectations +func (m *RegistryClientMock) MinimockStreamRepositoriesDone() bool { + if m.StreamRepositoriesMock.optional { + // Optional methods provide '0 or more' call count restriction. + return true + } + + for _, e := range m.StreamRepositoriesMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + return false + } + } + + return m.StreamRepositoriesMock.invocationsDone() +} + +// MinimockStreamRepositoriesInspect logs each unmet expectation +func (m *RegistryClientMock) MinimockStreamRepositoriesInspect() { + for _, e := range m.StreamRepositoriesMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + m.t.Errorf("Expected call to RegistryClientMock.StreamRepositories at\n%s with params: %#v", e.expectationOrigins.origin, *e.params) + } + } + + afterStreamRepositoriesCounter := mm_atomic.LoadUint64(&m.afterStreamRepositoriesCounter) + // if default expectation was set then invocations count should be greater than zero + if m.StreamRepositoriesMock.defaultExpectation != nil && afterStreamRepositoriesCounter < 1 { + if m.StreamRepositoriesMock.defaultExpectation.params == nil { + m.t.Errorf("Expected call to RegistryClientMock.StreamRepositories at\n%s", m.StreamRepositoriesMock.defaultExpectation.returnOrigin) + } else { + m.t.Errorf("Expected call to RegistryClientMock.StreamRepositories at\n%s with params: %#v", m.StreamRepositoriesMock.defaultExpectation.expectationOrigins.origin, *m.StreamRepositoriesMock.defaultExpectation.params) + } + } + // if func was set then invocations count should be greater than zero + if m.funcStreamRepositories != nil && afterStreamRepositoriesCounter < 1 { + m.t.Errorf("Expected call to RegistryClientMock.StreamRepositories at\n%s", m.funcStreamRepositoriesOrigin) + } + + if !m.StreamRepositoriesMock.invocationsDone() && afterStreamRepositoriesCounter > 0 { + m.t.Errorf("Expected %d calls to RegistryClientMock.StreamRepositories at\n%s but found %d calls", + mm_atomic.LoadUint64(&m.StreamRepositoriesMock.expectedInvocations), m.StreamRepositoriesMock.expectedInvocationsOrigin, afterStreamRepositoriesCounter) + } +} + +type mRegistryClientMockStreamTags struct { + optional bool + mock *RegistryClientMock + defaultExpectation *RegistryClientMockStreamTagsExpectation + expectations []*RegistryClientMockStreamTagsExpectation + + callArgs []*RegistryClientMockStreamTagsParams + mutex sync.RWMutex + + expectedInvocations uint64 + expectedInvocationsOrigin string +} + +// RegistryClientMockStreamTagsExpectation specifies expectation struct of the Client.StreamTags +type RegistryClientMockStreamTagsExpectation struct { + mock *RegistryClientMock + params *RegistryClientMockStreamTagsParams + paramPtrs *RegistryClientMockStreamTagsParamPtrs + expectationOrigins RegistryClientMockStreamTagsExpectationOrigins + results *RegistryClientMockStreamTagsResults + returnOrigin string + Counter uint64 +} + +// RegistryClientMockStreamTagsParams contains parameters of the Client.StreamTags +type RegistryClientMockStreamTagsParams struct { + ctx context.Context + visit func(tags []string) error + opts []mm_registry.ListTagsOption +} + +// RegistryClientMockStreamTagsParamPtrs contains pointers to parameters of the Client.StreamTags +type RegistryClientMockStreamTagsParamPtrs struct { + ctx *context.Context + visit *func(tags []string) error + opts *[]mm_registry.ListTagsOption +} + +// RegistryClientMockStreamTagsResults contains results of the Client.StreamTags +type RegistryClientMockStreamTagsResults struct { + err error +} + +// RegistryClientMockStreamTagsOrigins contains origins of expectations of the Client.StreamTags +type RegistryClientMockStreamTagsExpectationOrigins struct { + origin string + originCtx string + originVisit string + originOpts string +} + +// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning +// the test will fail minimock's automatic final call check if the mocked method was not called at least once. +// Optional() makes method check to work in '0 or more' mode. +// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to +// catch the problems when the expected method call is totally skipped during test run. +func (mmStreamTags *mRegistryClientMockStreamTags) Optional() *mRegistryClientMockStreamTags { + mmStreamTags.optional = true + return mmStreamTags +} + +// Expect sets up expected params for Client.StreamTags +func (mmStreamTags *mRegistryClientMockStreamTags) Expect(ctx context.Context, visit func(tags []string) error, opts ...mm_registry.ListTagsOption) *mRegistryClientMockStreamTags { + if mmStreamTags.mock.funcStreamTags != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Set") + } + + if mmStreamTags.defaultExpectation == nil { + mmStreamTags.defaultExpectation = &RegistryClientMockStreamTagsExpectation{} + } + + if mmStreamTags.defaultExpectation.paramPtrs != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by ExpectParams functions") + } + + mmStreamTags.defaultExpectation.params = &RegistryClientMockStreamTagsParams{ctx, visit, opts} + mmStreamTags.defaultExpectation.expectationOrigins.origin = minimock.CallerInfo(1) + for _, e := range mmStreamTags.expectations { + if minimock.Equal(e.params, mmStreamTags.defaultExpectation.params) { + mmStreamTags.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmStreamTags.defaultExpectation.params) + } + } + + return mmStreamTags +} + +// ExpectCtxParam1 sets up expected param ctx for Client.StreamTags +func (mmStreamTags *mRegistryClientMockStreamTags) ExpectCtxParam1(ctx context.Context) *mRegistryClientMockStreamTags { + if mmStreamTags.mock.funcStreamTags != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Set") + } + + if mmStreamTags.defaultExpectation == nil { + mmStreamTags.defaultExpectation = &RegistryClientMockStreamTagsExpectation{} + } + + if mmStreamTags.defaultExpectation.params != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Expect") + } + + if mmStreamTags.defaultExpectation.paramPtrs == nil { + mmStreamTags.defaultExpectation.paramPtrs = &RegistryClientMockStreamTagsParamPtrs{} + } + mmStreamTags.defaultExpectation.paramPtrs.ctx = &ctx + mmStreamTags.defaultExpectation.expectationOrigins.originCtx = minimock.CallerInfo(1) + + return mmStreamTags +} + +// ExpectVisitParam2 sets up expected param visit for Client.StreamTags +func (mmStreamTags *mRegistryClientMockStreamTags) ExpectVisitParam2(visit func(tags []string) error) *mRegistryClientMockStreamTags { + if mmStreamTags.mock.funcStreamTags != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Set") + } + + if mmStreamTags.defaultExpectation == nil { + mmStreamTags.defaultExpectation = &RegistryClientMockStreamTagsExpectation{} + } + + if mmStreamTags.defaultExpectation.params != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Expect") + } + + if mmStreamTags.defaultExpectation.paramPtrs == nil { + mmStreamTags.defaultExpectation.paramPtrs = &RegistryClientMockStreamTagsParamPtrs{} + } + mmStreamTags.defaultExpectation.paramPtrs.visit = &visit + mmStreamTags.defaultExpectation.expectationOrigins.originVisit = minimock.CallerInfo(1) + + return mmStreamTags +} + +// ExpectOptsParam3 sets up expected param opts for Client.StreamTags +func (mmStreamTags *mRegistryClientMockStreamTags) ExpectOptsParam3(opts ...mm_registry.ListTagsOption) *mRegistryClientMockStreamTags { + if mmStreamTags.mock.funcStreamTags != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Set") + } + + if mmStreamTags.defaultExpectation == nil { + mmStreamTags.defaultExpectation = &RegistryClientMockStreamTagsExpectation{} + } + + if mmStreamTags.defaultExpectation.params != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Expect") + } + + if mmStreamTags.defaultExpectation.paramPtrs == nil { + mmStreamTags.defaultExpectation.paramPtrs = &RegistryClientMockStreamTagsParamPtrs{} + } + mmStreamTags.defaultExpectation.paramPtrs.opts = &opts + mmStreamTags.defaultExpectation.expectationOrigins.originOpts = minimock.CallerInfo(1) + + return mmStreamTags +} + +// Inspect accepts an inspector function that has same arguments as the Client.StreamTags +func (mmStreamTags *mRegistryClientMockStreamTags) Inspect(f func(ctx context.Context, visit func(tags []string) error, opts ...mm_registry.ListTagsOption)) *mRegistryClientMockStreamTags { + if mmStreamTags.mock.inspectFuncStreamTags != nil { + mmStreamTags.mock.t.Fatalf("Inspect function is already set for RegistryClientMock.StreamTags") + } + + mmStreamTags.mock.inspectFuncStreamTags = f + + return mmStreamTags +} + +// Return sets up results that will be returned by Client.StreamTags +func (mmStreamTags *mRegistryClientMockStreamTags) Return(err error) *RegistryClientMock { + if mmStreamTags.mock.funcStreamTags != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Set") + } + + if mmStreamTags.defaultExpectation == nil { + mmStreamTags.defaultExpectation = &RegistryClientMockStreamTagsExpectation{mock: mmStreamTags.mock} + } + mmStreamTags.defaultExpectation.results = &RegistryClientMockStreamTagsResults{err} + mmStreamTags.defaultExpectation.returnOrigin = minimock.CallerInfo(1) + return mmStreamTags.mock +} + +// Set uses given function f to mock the Client.StreamTags method +func (mmStreamTags *mRegistryClientMockStreamTags) Set(f func(ctx context.Context, visit func(tags []string) error, opts ...mm_registry.ListTagsOption) (err error)) *RegistryClientMock { + if mmStreamTags.defaultExpectation != nil { + mmStreamTags.mock.t.Fatalf("Default expectation is already set for the Client.StreamTags method") + } + + if len(mmStreamTags.expectations) > 0 { + mmStreamTags.mock.t.Fatalf("Some expectations are already set for the Client.StreamTags method") + } + + mmStreamTags.mock.funcStreamTags = f + mmStreamTags.mock.funcStreamTagsOrigin = minimock.CallerInfo(1) + return mmStreamTags.mock +} + +// When sets expectation for the Client.StreamTags which will trigger the result defined by the following +// Then helper +func (mmStreamTags *mRegistryClientMockStreamTags) When(ctx context.Context, visit func(tags []string) error, opts ...mm_registry.ListTagsOption) *RegistryClientMockStreamTagsExpectation { + if mmStreamTags.mock.funcStreamTags != nil { + mmStreamTags.mock.t.Fatalf("RegistryClientMock.StreamTags mock is already set by Set") + } + + expectation := &RegistryClientMockStreamTagsExpectation{ + mock: mmStreamTags.mock, + params: &RegistryClientMockStreamTagsParams{ctx, visit, opts}, + expectationOrigins: RegistryClientMockStreamTagsExpectationOrigins{origin: minimock.CallerInfo(1)}, + } + mmStreamTags.expectations = append(mmStreamTags.expectations, expectation) + return expectation +} + +// Then sets up Client.StreamTags return parameters for the expectation previously defined by the When method +func (e *RegistryClientMockStreamTagsExpectation) Then(err error) *RegistryClientMock { + e.results = &RegistryClientMockStreamTagsResults{err} + return e.mock +} + +// Times sets number of times Client.StreamTags should be invoked +func (mmStreamTags *mRegistryClientMockStreamTags) Times(n uint64) *mRegistryClientMockStreamTags { + if n == 0 { + mmStreamTags.mock.t.Fatalf("Times of RegistryClientMock.StreamTags mock can not be zero") + } + mm_atomic.StoreUint64(&mmStreamTags.expectedInvocations, n) + mmStreamTags.expectedInvocationsOrigin = minimock.CallerInfo(1) + return mmStreamTags +} + +func (mmStreamTags *mRegistryClientMockStreamTags) invocationsDone() bool { + if len(mmStreamTags.expectations) == 0 && mmStreamTags.defaultExpectation == nil && mmStreamTags.mock.funcStreamTags == nil { + return true + } + + totalInvocations := mm_atomic.LoadUint64(&mmStreamTags.mock.afterStreamTagsCounter) + expectedInvocations := mm_atomic.LoadUint64(&mmStreamTags.expectedInvocations) + + return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations) +} + +// StreamTags implements mm_registry.Client +func (mmStreamTags *RegistryClientMock) StreamTags(ctx context.Context, visit func(tags []string) error, opts ...mm_registry.ListTagsOption) (err error) { + mm_atomic.AddUint64(&mmStreamTags.beforeStreamTagsCounter, 1) + defer mm_atomic.AddUint64(&mmStreamTags.afterStreamTagsCounter, 1) + + mmStreamTags.t.Helper() + + if mmStreamTags.inspectFuncStreamTags != nil { + mmStreamTags.inspectFuncStreamTags(ctx, visit, opts...) + } + + mm_params := RegistryClientMockStreamTagsParams{ctx, visit, opts} + + // Record call args + mmStreamTags.StreamTagsMock.mutex.Lock() + mmStreamTags.StreamTagsMock.callArgs = append(mmStreamTags.StreamTagsMock.callArgs, &mm_params) + mmStreamTags.StreamTagsMock.mutex.Unlock() + + for _, e := range mmStreamTags.StreamTagsMock.expectations { + if minimock.Equal(*e.params, mm_params) { + mm_atomic.AddUint64(&e.Counter, 1) + return e.results.err + } + } + + if mmStreamTags.StreamTagsMock.defaultExpectation != nil { + mm_atomic.AddUint64(&mmStreamTags.StreamTagsMock.defaultExpectation.Counter, 1) + mm_want := mmStreamTags.StreamTagsMock.defaultExpectation.params + mm_want_ptrs := mmStreamTags.StreamTagsMock.defaultExpectation.paramPtrs + + mm_got := RegistryClientMockStreamTagsParams{ctx, visit, opts} + + if mm_want_ptrs != nil { + + if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) { + mmStreamTags.t.Errorf("RegistryClientMock.StreamTags got unexpected parameter ctx, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmStreamTags.StreamTagsMock.defaultExpectation.expectationOrigins.originCtx, *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx)) + } + + if mm_want_ptrs.visit != nil && !minimock.Equal(*mm_want_ptrs.visit, mm_got.visit) { + mmStreamTags.t.Errorf("RegistryClientMock.StreamTags got unexpected parameter visit, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmStreamTags.StreamTagsMock.defaultExpectation.expectationOrigins.originVisit, *mm_want_ptrs.visit, mm_got.visit, minimock.Diff(*mm_want_ptrs.visit, mm_got.visit)) + } + + if mm_want_ptrs.opts != nil && !minimock.Equal(*mm_want_ptrs.opts, mm_got.opts) { + mmStreamTags.t.Errorf("RegistryClientMock.StreamTags got unexpected parameter opts, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmStreamTags.StreamTagsMock.defaultExpectation.expectationOrigins.originOpts, *mm_want_ptrs.opts, mm_got.opts, minimock.Diff(*mm_want_ptrs.opts, mm_got.opts)) + } + + } else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { + mmStreamTags.t.Errorf("RegistryClientMock.StreamTags got unexpected parameters, expected at\n%s:\nwant: %#v\n got: %#v%s\n", + mmStreamTags.StreamTagsMock.defaultExpectation.expectationOrigins.origin, *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) + } + + mm_results := mmStreamTags.StreamTagsMock.defaultExpectation.results + if mm_results == nil { + mmStreamTags.t.Fatal("No results are set for the RegistryClientMock.StreamTags") + } + return (*mm_results).err + } + if mmStreamTags.funcStreamTags != nil { + return mmStreamTags.funcStreamTags(ctx, visit, opts...) + } + mmStreamTags.t.Fatalf("Unexpected call to RegistryClientMock.StreamTags. %v %v %v", ctx, visit, opts) + return +} + +// StreamTagsAfterCounter returns a count of finished RegistryClientMock.StreamTags invocations +func (mmStreamTags *RegistryClientMock) StreamTagsAfterCounter() uint64 { + return mm_atomic.LoadUint64(&mmStreamTags.afterStreamTagsCounter) +} + +// StreamTagsBeforeCounter returns a count of RegistryClientMock.StreamTags invocations +func (mmStreamTags *RegistryClientMock) StreamTagsBeforeCounter() uint64 { + return mm_atomic.LoadUint64(&mmStreamTags.beforeStreamTagsCounter) +} + +// Calls returns a list of arguments used in each call to RegistryClientMock.StreamTags. +// The list is in the same order as the calls were made (i.e. recent calls have a higher index) +func (mmStreamTags *mRegistryClientMockStreamTags) Calls() []*RegistryClientMockStreamTagsParams { + mmStreamTags.mutex.RLock() + + argCopy := make([]*RegistryClientMockStreamTagsParams, len(mmStreamTags.callArgs)) + copy(argCopy, mmStreamTags.callArgs) + + mmStreamTags.mutex.RUnlock() + + return argCopy +} + +// MinimockStreamTagsDone returns true if the count of the StreamTags invocations corresponds +// the number of defined expectations +func (m *RegistryClientMock) MinimockStreamTagsDone() bool { + if m.StreamTagsMock.optional { + // Optional methods provide '0 or more' call count restriction. + return true + } + + for _, e := range m.StreamTagsMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + return false + } + } + + return m.StreamTagsMock.invocationsDone() +} + +// MinimockStreamTagsInspect logs each unmet expectation +func (m *RegistryClientMock) MinimockStreamTagsInspect() { + for _, e := range m.StreamTagsMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + m.t.Errorf("Expected call to RegistryClientMock.StreamTags at\n%s with params: %#v", e.expectationOrigins.origin, *e.params) + } + } + + afterStreamTagsCounter := mm_atomic.LoadUint64(&m.afterStreamTagsCounter) + // if default expectation was set then invocations count should be greater than zero + if m.StreamTagsMock.defaultExpectation != nil && afterStreamTagsCounter < 1 { + if m.StreamTagsMock.defaultExpectation.params == nil { + m.t.Errorf("Expected call to RegistryClientMock.StreamTags at\n%s", m.StreamTagsMock.defaultExpectation.returnOrigin) + } else { + m.t.Errorf("Expected call to RegistryClientMock.StreamTags at\n%s with params: %#v", m.StreamTagsMock.defaultExpectation.expectationOrigins.origin, *m.StreamTagsMock.defaultExpectation.params) + } + } + // if func was set then invocations count should be greater than zero + if m.funcStreamTags != nil && afterStreamTagsCounter < 1 { + m.t.Errorf("Expected call to RegistryClientMock.StreamTags at\n%s", m.funcStreamTagsOrigin) + } + + if !m.StreamTagsMock.invocationsDone() && afterStreamTagsCounter > 0 { + m.t.Errorf("Expected %d calls to RegistryClientMock.StreamTags at\n%s but found %d calls", + mm_atomic.LoadUint64(&m.StreamTagsMock.expectedInvocations), m.StreamTagsMock.expectedInvocationsOrigin, afterStreamTagsCounter) + } +} + +type mRegistryClientMockTagImage struct { + optional bool + mock *RegistryClientMock + defaultExpectation *RegistryClientMockTagImageExpectation + expectations []*RegistryClientMockTagImageExpectation + + callArgs []*RegistryClientMockTagImageParams + mutex sync.RWMutex + + expectedInvocations uint64 + expectedInvocationsOrigin string +} + +// RegistryClientMockTagImageExpectation specifies expectation struct of the Client.TagImage +type RegistryClientMockTagImageExpectation struct { + mock *RegistryClientMock + params *RegistryClientMockTagImageParams + paramPtrs *RegistryClientMockTagImageParamPtrs + expectationOrigins RegistryClientMockTagImageExpectationOrigins + results *RegistryClientMockTagImageResults + returnOrigin string + Counter uint64 +} + +// RegistryClientMockTagImageParams contains parameters of the Client.TagImage +type RegistryClientMockTagImageParams struct { + ctx context.Context + sourceTag string + destTag string +} + +// RegistryClientMockTagImageParamPtrs contains pointers to parameters of the Client.TagImage +type RegistryClientMockTagImageParamPtrs struct { + ctx *context.Context + sourceTag *string + destTag *string +} + +// RegistryClientMockTagImageResults contains results of the Client.TagImage +type RegistryClientMockTagImageResults struct { + err error +} + +// RegistryClientMockTagImageOrigins contains origins of expectations of the Client.TagImage +type RegistryClientMockTagImageExpectationOrigins struct { + origin string + originCtx string + originSourceTag string + originDestTag string +} + +// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning +// the test will fail minimock's automatic final call check if the mocked method was not called at least once. +// Optional() makes method check to work in '0 or more' mode. +// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to +// catch the problems when the expected method call is totally skipped during test run. +func (mmTagImage *mRegistryClientMockTagImage) Optional() *mRegistryClientMockTagImage { + mmTagImage.optional = true + return mmTagImage +} + +// Expect sets up expected params for Client.TagImage +func (mmTagImage *mRegistryClientMockTagImage) Expect(ctx context.Context, sourceTag string, destTag string) *mRegistryClientMockTagImage { + if mmTagImage.mock.funcTagImage != nil { + mmTagImage.mock.t.Fatalf("RegistryClientMock.TagImage mock is already set by Set") + } + + if mmTagImage.defaultExpectation == nil { + mmTagImage.defaultExpectation = &RegistryClientMockTagImageExpectation{} + } + + if mmTagImage.defaultExpectation.paramPtrs != nil { mmTagImage.mock.t.Fatalf("RegistryClientMock.TagImage mock is already set by ExpectParams functions") } @@ -5398,6 +6548,8 @@ func (m *RegistryClientMock) MinimockFinish() { m.MinimockGetImageConfigInspect() + m.MinimockGetIndexInspect() + m.MinimockGetManifestInspect() m.MinimockGetRegistryInspect() @@ -5410,6 +6562,10 @@ func (m *RegistryClientMock) MinimockFinish() { m.MinimockPushIndexInspect() + m.MinimockStreamRepositoriesInspect() + + m.MinimockStreamTagsInspect() + m.MinimockTagImageInspect() m.MinimockWithSegmentInspect() @@ -5443,12 +6599,15 @@ func (m *RegistryClientMock) minimockDone() bool { m.MinimockGetDigestDone() && m.MinimockGetImageDone() && m.MinimockGetImageConfigDone() && + m.MinimockGetIndexDone() && m.MinimockGetManifestDone() && m.MinimockGetRegistryDone() && m.MinimockListRepositoriesDone() && m.MinimockListTagsDone() && m.MinimockPushImageDone() && m.MinimockPushIndexDone() && + m.MinimockStreamRepositoriesDone() && + m.MinimockStreamTagsDone() && m.MinimockTagImageDone() && m.MinimockWithSegmentDone() } diff --git a/pkg/registry/service/plugin_service_test.go b/pkg/registry/service/plugin_service_test.go index 6c549f6dc..d638c26e1 100644 --- a/pkg/registry/service/plugin_service_test.go +++ b/pkg/registry/service/plugin_service_test.go @@ -191,7 +191,7 @@ type fakeManifestClient struct { gotTags []string } -func (c *fakeManifestClient) GetManifest(_ context.Context, tag string) (dkpreg.ManifestResult, error) { +func (c *fakeManifestClient) GetManifest(_ context.Context, tag string, _ ...dkpreg.ManifestGetOption) (dkpreg.ManifestResult, error) { c.gotTags = append(c.gotTags, tag) res, ok := c.byTag[tag]