# Merge pull request #1676 from entireio/paul/mirror-get-cluster-routing

`e980d1b`→[main](/content/gh/entireio/cli/commits/main/index.html)·

Soph·1w ago·3 files·+169 added/-10 removed

fix(repo): resolve `mirror get` clone URLs via that cluster's login server

## Changes

3

- cmd/entire/cli

- Mcorecmd.go+25/-3

- Mrepo_mirror.go+35/-7

- Mrepo_mirror_test.go+109

```
214 unmodified lines

// field/value list (default) or raw JSON (--json), reusing the same column
// definition as the matching list view.
func runCoreObject[T any](cmd *cobra.Command, headers []string, row func(T) []string, fn func(ctx context.Context, c *coreapi.Client) (*T, error)) error {
	return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
		return runCore(cmd, renderCoreObject(cmd, headers, row, fn))
	})
}

// runCoreObjectForCluster is runCoreObject for a resource-provider command (see
// runCoreForCluster): identical field/JSON rendering, but dialing the core that
// fronts clusterHost rather than the active context.
func runCoreObjectForCluster[T any](cmd *cobra.Command, clusterHost string, headers []string, row func(T) []string, fn func(ctx context.Context, c *coreapi.Client) (*T, error)) error {
	return runCoreForCluster(cmd, clusterHost, renderCoreObject(cmd, headers, row, fn))
}

// renderCoreObject builds the run-function shared by runCoreObject and
// runCoreObjectForCluster: fetch via fn, then render as a field/value list
// (default) or raw JSON (--json). Kept separate from the client-selection so
// the two object variants differ only in which core they dial (mirroring
// renderCoreList).
func renderCoreObject[T any](cmd *cobra.Command, headers []string, row func(T) []string, fn func(ctx context.Context, c *coreapi.Client) (*T, error)) func(context.Context, *coreapi.Client) error {
	return func(ctx context.Context, c *coreapi.Client) error {
		item, err := fn(ctx, c)
		if err != nil {
			return err
		}

return printFields(cmd.OutOrStdout(), headers, row(*item))
	}
}

// TestRepoMirrorGet_Routing pins which core `mirror get <ref>` dials. A clone
// URL names its cluster, so it must be resolved on the core fronting that
// cluster (clusterCoreClient), not the active context — the original bug:
// `mirror get entire://<cluster>/…` for a cluster in a federation other than
// the active login failed with "no mirror matching" until the user switched
// contexts. A ULID carries no cluster coordinate and stays on the active
// context; an unparseable ref must error before dialing anything.
func TestRepoMirrorGet_Routing(t *testing.T) {
	const mirrorULID = "0123456789ABCDEFGHJKMNPQRS"
	const clusterHost = "eukanuba.partial.to"
	const cloneURL = "entire://" + clusterHost + "/gh/entirehq/librarian"
	// mirrorServer answers both the list (clone-URL resolution) and the
	// GetMirror-by-ULID calls for the librarian mirror.
	mirrorServer := func(t *testing.T) *httptest.Server {
		t.Helper()
		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			switch r.URL.Path {
			case mirrorsAPIPath:
				assert.NoError(t, printJSON(w, &coreapi.ListMirrorsOutputBody{Mirrors: []coreapi.Mirror{
					{MirrorId: mirrorULID, Owner: "entirehq", Repo: "librarian", ClusterHost: clusterHost},
				}}))
			case mirrorsAPIPath + "/" + mirrorULID:
				assert.NoError(t, printJSON(w, &coreapi.Mirror{
					MirrorId: mirrorULID, Owner: "entirehq", Repo: "librarian", ClusterHost: clusterHost,
					IsPrivate: coreapi.NewOptBool(true),
				}))
			default:
				t.Errorf("unexpected request path %q", r.URL.Path)
				w.WriteHeader(http.StatusNotFound)
			}
		}))
		t.Cleanup(srv.Close)
		return srv
	}

t.Run("clone URL dials the cluster's core, not the active context", func(t *testing.T) {
		srv := mirrorServer(t)
		seamActive(t, func(context.Context) (*coreapi.Client, error) {
			t.Error("clone-URL get dialed the active context's core")
			return nil, errors.New("wrong core")
		})
		var gotHost string
		seamCluster(t, func(_ context.Context, host string) (*coreapi.Client, error) {
			gotHost = host
			return coreapi.NewWithBearer(srv.URL, "tok")
		})
		out, err := runGet(t, cloneURL)
		require.NoError(t, err)
		require.Equal(t, clusterHost, gotHost, "must resolve on the clone URL's cluster")
		require.Contains(t, out, "entirehq/librarian")
		require.Contains(t, out, cloneURL)
	})

}
