Skip to content

Commit 111649e

Browse files
authored
Merge pull request #1645 from entireio/feat/mirror-create-one-shot-cluster-picker
feat(mirror): cluster picker for interactive one-shot mirror create
2 parents 6fdf86c + cbffb79 commit 111649e

3 files changed

Lines changed: 119 additions & 11 deletions

File tree

cmd/entire/cli/repo_mirror.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ func availableMirrorRow(m coreapi.AvailableMirror) []string {
4949

5050
// defaultClusterHost is the cluster the positional-arg mirror commands target
5151
// when the caller omits the <cluster-host> argument. The no-arg create wizard
52-
// instead enumerates real clusters from the catalog (GET /api/v1/clusters, see
53-
// availableRegions in repo_mirror_create_wizard.go); this stays as the
54-
// single-region fallback for the explicit `create <github-url>` form.
52+
// and the interactive one-shot `create <github-url>` instead enumerate real
53+
// clusters from the catalog (GET /api/v1/clusters, see availableRegions and
54+
// resolveOneShotClusterHost in repo_mirror_create_wizard.go); this stays as
55+
// the fixed fallback for non-interactive invocations, so scripts keep a
56+
// stable, offline-resolvable default.
5557
const defaultClusterHost = "aws-us-east-2.entire.io"
5658

5759
// clusterArg returns the cluster host from the optional second positional
@@ -144,9 +146,9 @@ func newRepoMirrorCreateCmd() *cobra.Command {
144146
"the target cluster, then waits for the initial GitHub→EntireDB clone " +
145147
"to finish so `git clone` works on return. Pass --no-wait to return " +
146148
"as soon as the placement is registered. Idempotent on " +
147-
"(upstream, cluster). The cluster-host defaults to " +
148-
defaultClusterHost + " when omitted (the interactive wizard, with " +
149-
"no args, instead lets you pick clusters).",
149+
"(upstream, cluster). When the cluster-host is omitted, an " +
150+
"interactive terminal offers the available clusters as a picker; " +
151+
"non-interactive runs default to " + defaultClusterHost + ".",
150152
Example: " entire repo mirror create\n" +
151153
" entire repo mirror create github.com/octocat/hello-world\n" +
152154
" entire repo mirror create github.com/octocat/hello-world aws-us-east-2.entire.io",
@@ -160,11 +162,19 @@ func newRepoMirrorCreateCmd() *cobra.Command {
160162
cmd.SilenceUsage = true
161163
return fmt.Errorf("invalid <github-url>: %w", err)
162164
}
163-
// The non-interactive one-shot keeps a fixed default cluster
164-
// (defaultClusterHost) when [cluster-host] is omitted — catalog-based
165-
// cluster guessing is intentionally limited to the interactive
166-
// wizard (the no-args path above), so scripts get stable behavior.
167-
clusterHost := clusterArg(args)
165+
// [cluster-host] omitted: on an interactive terminal, offer the
166+
// catalog's clusters as a picker (the same prompt-only-when-there-
167+
// is-a-choice shape as `repo clone`); non-interactive invocations
168+
// keep the fixed defaultClusterHost so scripts get stable behavior.
169+
var clusterHost string
170+
if len(args) > 1 {
171+
clusterHost = args[1]
172+
} else {
173+
var rerr error
174+
if clusterHost, rerr = resolveOneShotClusterHost(cmd); rerr != nil {
175+
return rerr
176+
}
177+
}
168178
if err := validateClusterHost(clusterHost); err != nil {
169179
cmd.SilenceUsage = true
170180
return fmt.Errorf("invalid [cluster-host]: %w", err)

cmd/entire/cli/repo_mirror_create_wizard.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,85 @@ func regionLabel(r regionChoice) string {
195195
}
196196
}
197197

198+
// resolveOneShotClusterHost picks the cluster `repo mirror create
199+
// <github-url>` targets when [cluster-host] is omitted. Non-interactive
200+
// callers keep the fixed defaultClusterHost so scripts stay stable and
201+
// offline-resolvable; on a terminal the control plane's cluster catalog is
202+
// offered as a single-select (skipped when only one cluster exists),
203+
// pre-selecting the caller's jurisdiction default — the same
204+
// prompt-only-when-there-is-a-choice shape `repo clone` uses for
205+
// multi-cluster placements.
206+
func resolveOneShotClusterHost(cmd *cobra.Command) (string, error) {
207+
if !interactive.CanPromptInteractively() {
208+
return defaultClusterHost, nil
209+
}
210+
errW := cmd.ErrOrStderr()
211+
var (
212+
regions []regionChoice
213+
jurisdiction string
214+
)
215+
if err := runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
216+
stop := startSpinner(errW, "Fetching clusters")
217+
var err error
218+
if regions, err = availableRegions(ctx, c); err != nil {
219+
stop(false)
220+
return err
221+
}
222+
// The jurisdiction only pre-selects the picker's default; a /me
223+
// hiccup shouldn't sink the create, so fall back to no pre-selection.
224+
if me, merr := c.GetMe(ctx); merr == nil {
225+
jurisdiction, _ = me.Jurisdiction.Get()
226+
}
227+
stop(true)
228+
return nil
229+
}); err != nil {
230+
return "", err
231+
}
232+
if len(regions) == 0 {
233+
return "", errors.New("no clusters available to mirror into; pass [cluster-host] explicitly")
234+
}
235+
if len(regions) == 1 {
236+
fmt.Fprintf(errW, "Using cluster %s\n", regions[0].host)
237+
return regions[0].host, nil
238+
}
239+
return pickOneCluster(cmd.Context(), errW, regions, jurisdiction)
240+
}
241+
242+
// pickOneCluster runs the one-shot create's cluster single-select,
243+
// pre-selecting the default cluster for the caller's jurisdiction. A clean
244+
// cancel (Ctrl+C / cancelled ctx) surfaces as a SilentError so the create
245+
// stops instead of falling through to a cluster the user didn't choose.
246+
func pickOneCluster(ctx context.Context, w io.Writer, regions []regionChoice, jurisdiction string) (string, error) {
247+
opts, defaults := clusterChoices(regions, jurisdiction)
248+
var selected string
249+
if len(defaults) > 0 {
250+
selected = defaults[0]
251+
}
252+
form := NewAccessibleForm(
253+
huh.NewGroup(
254+
huh.NewSelect[string]().
255+
Title("Select the cluster to mirror into").
256+
Options(opts...).
257+
Value(&selected),
258+
),
259+
)
260+
if err := form.RunWithContext(ctx); err != nil {
261+
if cerr := handleFormCancellation(w, "Mirror create", err); cerr != nil {
262+
return "", cerr
263+
}
264+
return "", NewSilentError(errors.New("mirror create cancelled"))
265+
}
266+
// Guard the selection against the offered hosts (like repo clone's
267+
// picker) so a zero-value fall-through can't reach the caller as a
268+
// misleading "invalid [cluster-host]" error.
269+
for _, r := range regions {
270+
if r.host == selected {
271+
return selected, nil
272+
}
273+
}
274+
return "", NewSilentError(errors.New("mirror create cancelled"))
275+
}
276+
198277
// mirrorTarget is one unit of work: a selected repo to be mirrored into a
199278
// selected region. The wizard creates the cross-product of repos × regions.
200279
type mirrorTarget struct {

cmd/entire/cli/repo_mirror_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"testing"
1212
"time"
1313

14+
"github.com/spf13/cobra"
1415
"github.com/stretchr/testify/assert"
1516
"github.com/stretchr/testify/require"
1617

@@ -747,6 +748,24 @@ func TestClusterArg(t *testing.T) {
747748
}
748749
}
749750

751+
// TestResolveOneShotClusterHost_NonInteractive locks in that a non-interactive
752+
// `repo mirror create <github-url>` keeps the fixed defaultClusterHost without
753+
// dialing the control plane — scripts must get a stable, offline default. Under
754+
// `go test`, CanPromptInteractively() is false, so this exercises exactly the
755+
// script path; no server is running, so any catalog fetch would error.
756+
func TestResolveOneShotClusterHost_NonInteractive(t *testing.T) {
757+
t.Parallel()
758+
cmd := &cobra.Command{}
759+
cmd.SetContext(t.Context())
760+
got, err := resolveOneShotClusterHost(cmd)
761+
if err != nil {
762+
t.Fatalf("resolveOneShotClusterHost() error = %v", err)
763+
}
764+
if got != defaultClusterHost {
765+
t.Errorf("resolveOneShotClusterHost() = %q, want default %q", got, defaultClusterHost)
766+
}
767+
}
768+
750769
func TestClusterArgAt(t *testing.T) {
751770
t.Parallel()
752771
// clusterArgAt reads the cluster from the optional positional at an

0 commit comments

Comments
 (0)