Skip to content

Commit ffd5c80

Browse files
authored
Merge pull request #1357 from entireio/dont-poll-empty-mirrors
repo mirror create: skip clone polling when upstream is empty
2 parents b98014a + 499e9ab commit ffd5c80

6 files changed

Lines changed: 252 additions & 30 deletions

File tree

cmd/entire/cli/repo_mirror.go

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"io"
78
"net"
89
"net/url"
910
"regexp"
@@ -12,6 +13,7 @@ import (
1213

1314
"github.com/spf13/cobra"
1415

16+
"github.com/entireio/cli/cmd/entire/cli/auth"
1517
"github.com/entireio/cli/internal/coreapi"
1618
)
1719

@@ -145,25 +147,18 @@ func newRepoMirrorCreateCmd() *cobra.Command {
145147
return err
146148
}
147149
out := cmd.OutOrStdout()
148-
if created.Created {
149-
fmt.Fprintf(out, "Registered mirror %s\n", created.MirrorId)
150-
} else {
151-
fmt.Fprintf(out, "Mirror already exists (%s)\n", created.MirrorId)
152-
}
153-
fmt.Fprintf(out, " %s\n", created.MirrorUrl)
154-
if noWait {
155-
fmt.Fprintf(out, "Initial clone may still be in progress; `git clone %s` will work once it completes.\n", created.MirrorUrl)
156-
return nil
157-
}
158-
if err := waitForMirrorClone(ctx, out, clusterHost, owner, repo, waitTimeout); err != nil {
159-
if handled, serr := explainSuspendedMirror(cmd.ErrOrStderr(), created.MirrorId, created.Created, err); handled {
160-
cmd.SilenceUsage = true
161-
return serr
162-
}
163-
return err
164-
}
165-
fmt.Fprintf(out, "\nClone it:\n git clone %s\n", created.MirrorUrl)
166-
return nil
150+
repoSlug := "/gh/" + owner + "/" + repo
151+
return finishMirrorCreate(out, cmd.ErrOrStderr(), created, noWait,
152+
func() error {
153+
if _, terr := auth.RepoScopedToken(ctx, "https://"+clusterHost, repoSlug, "pull"); terr != nil {
154+
return fmt.Errorf("probe mirror for suspension: %w", terr)
155+
}
156+
return nil
157+
},
158+
func() error {
159+
return waitForMirrorClone(ctx, out, clusterHost, owner, repo, waitTimeout)
160+
},
161+
)
167162
})
168163
},
169164
}
@@ -172,6 +167,62 @@ func newRepoMirrorCreateCmd() *cobra.Command {
172167
return cmd
173168
}
174169

170+
// finishMirrorCreate prints the post-create status for `repo mirror create`
171+
// and, unless noWait, makes sure the mirror is usable before returning.
172+
//
173+
// Empty upstream and suspended placement interact. An empty upstream has no
174+
// clone to wait for, so the HEAD-poll loop is skipped — it could only spin to
175+
// the timeout, since an empty repo never advertises a HEAD. But an *existing*
176+
// placement can be suspended even when its upstream is empty, and the
177+
// repo-scoped token exchange is the only signal that surfaces that; a *fresh*
178+
// create can't be suspended (suspension follows upstream access loss), so it
179+
// needs neither the probe nor the wait. Non-empty mirrors take the normal
180+
// clone-wait path.
181+
//
182+
// probeSuspended mints a repo-scoped pull token and returns its error for
183+
// explainSuspendedMirror to classify; waitClone runs the HEAD-poll loop. Both
184+
// are injected so the branching is unit-testable without the auth and
185+
// control-plane stack the production caller wires up.
186+
func finishMirrorCreate(out, errW io.Writer, created *coreapi.CreatedMirror, noWait bool, probeSuspended, waitClone func() error) error {
187+
if created.Created {
188+
fmt.Fprintf(out, "Registered mirror %s\n", created.MirrorId)
189+
} else {
190+
fmt.Fprintf(out, "Mirror already exists (%s)\n", created.MirrorId)
191+
}
192+
fmt.Fprintf(out, " %s\n", created.MirrorUrl)
193+
194+
if created.Empty {
195+
// An existing placement can sit behind a suspension even with an empty
196+
// upstream, so probe the token exchange to surface it. A fresh create
197+
// can't be suspended, so skip the probe there.
198+
if !created.Created {
199+
if err := probeSuspended(); err != nil {
200+
if handled, serr := explainSuspendedMirror(errW, created.MirrorId, created.Created, err); handled {
201+
return serr
202+
}
203+
// A non-suspension probe error isn't fatal: the placement exists
204+
// and the upstream is genuinely empty, so report that rather than
205+
// failing the create on a transient token hiccup.
206+
}
207+
}
208+
fmt.Fprintln(out, "Upstream has no commits yet — nothing to clone. The mirror will pick up refs once the upstream is pushed to.")
209+
return nil
210+
}
211+
212+
if noWait {
213+
fmt.Fprintf(out, "Initial clone may still be in progress; `git clone %s` will work once it completes.\n", created.MirrorUrl)
214+
return nil
215+
}
216+
if err := waitClone(); err != nil {
217+
if handled, serr := explainSuspendedMirror(errW, created.MirrorId, created.Created, err); handled {
218+
return serr
219+
}
220+
return err
221+
}
222+
fmt.Fprintf(out, "\nClone it:\n git clone %s\n", created.MirrorUrl)
223+
return nil
224+
}
225+
175226
func newRepoMirrorListCmd() *cobra.Command {
176227
var cluster, provider, owner string
177228
cmd := &cobra.Command{

cmd/entire/cli/repo_mirror_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,143 @@ func TestExplainSuspendedMirror(t *testing.T) {
8181
})
8282
}
8383

84+
// TestFinishMirrorCreate exercises the post-create branching: when the
85+
// upstream is empty we must skip the HEAD-poll loop (an empty repo never
86+
// advertises a HEAD), yet an *existing* empty placement must still go through
87+
// the token exchange so a suspended mirror surfaces its resume guidance
88+
// instead of a success-style "nothing to clone" note.
89+
func TestFinishMirrorCreate(t *testing.T) {
90+
t.Parallel()
91+
92+
const id = "01KS6KFJR2XS6PZ188MVYE07AN"
93+
const mirrorURL = "entire://eu-west-1.entire.io/gh/octocat/hello-world"
94+
// The error shape RepoScopedToken/waitForMirrorClone produce for a
95+
// suspended (non-servable) placement.
96+
suspended := fmt.Errorf("repo-scoped token exchange: %w", auth.ErrRepoTargetUnknown)
97+
98+
// seen records whether each injected operation ran, so we can assert the
99+
// empty path never polls and a fresh create never probes.
100+
type call struct{ probed, waited bool }
101+
102+
t.Run("fresh empty create skips both probe and poll", func(t *testing.T) {
103+
t.Parallel()
104+
var seen call
105+
var out, errW bytes.Buffer
106+
created := &coreapi.CreatedMirror{Created: true, Empty: true, MirrorId: id, MirrorUrl: mirrorURL}
107+
err := finishMirrorCreate(&out, &errW, created, false,
108+
func() error { seen.probed = true; return nil },
109+
func() error { seen.waited = true; return nil },
110+
)
111+
require.NoError(t, err)
112+
require.False(t, seen.probed, "a fresh create can't be suspended; must not probe")
113+
require.False(t, seen.waited, "empty upstream has nothing to clone; must not poll")
114+
require.Contains(t, out.String(), "nothing to clone")
115+
require.Empty(t, errW.String())
116+
})
117+
118+
t.Run("existing empty healthy probes but does not poll", func(t *testing.T) {
119+
t.Parallel()
120+
var seen call
121+
var out, errW bytes.Buffer
122+
created := &coreapi.CreatedMirror{Created: false, Empty: true, MirrorId: id, MirrorUrl: mirrorURL}
123+
err := finishMirrorCreate(&out, &errW, created, false,
124+
func() error { seen.probed = true; return nil },
125+
func() error { seen.waited = true; return nil },
126+
)
127+
require.NoError(t, err)
128+
require.True(t, seen.probed, "existing empty placement must probe for suspension")
129+
require.False(t, seen.waited, "empty upstream has nothing to clone; must not poll")
130+
require.Contains(t, out.String(), "nothing to clone")
131+
})
132+
133+
t.Run("existing empty suspended surfaces resume guidance", func(t *testing.T) {
134+
t.Parallel()
135+
var seen call
136+
var out, errW bytes.Buffer
137+
created := &coreapi.CreatedMirror{Created: false, Empty: true, MirrorId: id, MirrorUrl: mirrorURL}
138+
err := finishMirrorCreate(&out, &errW, created, false,
139+
func() error { seen.probed = true; return suspended },
140+
func() error { seen.waited = true; return nil },
141+
)
142+
var silent *SilentError
143+
require.ErrorAs(t, err, &silent, "suspended mirror must return a SilentError")
144+
require.True(t, seen.probed)
145+
require.False(t, seen.waited, "must not poll a suspended empty mirror")
146+
require.Contains(t, errW.String(), "entire-core admin mirrors resume "+id)
147+
require.NotContains(t, out.String(), "nothing to clone",
148+
"a suspended mirror must not get the success-style empty note")
149+
})
150+
151+
t.Run("existing empty transient probe error is non-fatal", func(t *testing.T) {
152+
t.Parallel()
153+
var out, errW bytes.Buffer
154+
created := &coreapi.CreatedMirror{Created: false, Empty: true, MirrorId: id, MirrorUrl: mirrorURL}
155+
err := finishMirrorCreate(&out, &errW, created, false,
156+
func() error { return errors.New("dial tcp: connection refused") },
157+
func() error { t.Fatal("must not poll an empty mirror"); return nil },
158+
)
159+
require.NoError(t, err, "a non-suspension probe error must not fail a create whose placement exists")
160+
require.Contains(t, out.String(), "nothing to clone")
161+
})
162+
163+
t.Run("non-empty no-wait skips both probe and poll", func(t *testing.T) {
164+
t.Parallel()
165+
var seen call
166+
var out, errW bytes.Buffer
167+
created := &coreapi.CreatedMirror{Created: true, Empty: false, MirrorId: id, MirrorUrl: mirrorURL}
168+
err := finishMirrorCreate(&out, &errW, created, true,
169+
func() error { seen.probed = true; return nil },
170+
func() error { seen.waited = true; return nil },
171+
)
172+
require.NoError(t, err)
173+
require.False(t, seen.probed)
174+
require.False(t, seen.waited, "--no-wait must not poll")
175+
require.Contains(t, out.String(), "still be in progress")
176+
})
177+
178+
t.Run("non-empty waits for clone then prints clone hint", func(t *testing.T) {
179+
t.Parallel()
180+
var seen call
181+
var out, errW bytes.Buffer
182+
created := &coreapi.CreatedMirror{Created: true, Empty: false, MirrorId: id, MirrorUrl: mirrorURL}
183+
err := finishMirrorCreate(&out, &errW, created, false,
184+
func() error { seen.probed = true; return nil },
185+
func() error { seen.waited = true; return nil },
186+
)
187+
require.NoError(t, err)
188+
require.False(t, seen.probed, "non-empty path detects suspension through waitClone, not a separate probe")
189+
require.True(t, seen.waited)
190+
require.Contains(t, out.String(), "git clone "+mirrorURL)
191+
})
192+
193+
t.Run("non-empty existing suspended surfaces resume guidance", func(t *testing.T) {
194+
t.Parallel()
195+
var out, errW bytes.Buffer
196+
created := &coreapi.CreatedMirror{Created: false, Empty: false, MirrorId: id, MirrorUrl: mirrorURL}
197+
err := finishMirrorCreate(&out, &errW, created, false,
198+
func() error { return nil },
199+
func() error { return suspended },
200+
)
201+
var silent *SilentError
202+
require.ErrorAs(t, err, &silent)
203+
require.Contains(t, errW.String(), "entire-core admin mirrors resume "+id)
204+
require.NotContains(t, out.String(), "git clone")
205+
})
206+
207+
t.Run("non-empty wait error other than suspension propagates", func(t *testing.T) {
208+
t.Parallel()
209+
var out, errW bytes.Buffer
210+
created := &coreapi.CreatedMirror{Created: true, Empty: false, MirrorId: id, MirrorUrl: mirrorURL}
211+
wantErr := errors.New("timed out waiting for initial clone")
212+
err := finishMirrorCreate(&out, &errW, created, false,
213+
func() error { return nil },
214+
func() error { return wantErr },
215+
)
216+
require.ErrorIs(t, err, wantErr)
217+
require.Empty(t, errW.String())
218+
})
219+
}
220+
84221
// TestParseGitHubURL is ported from entiredb's cmd/entire-repo/cli
85222
// mirror_test.go, since parseGitHubURL was carried over verbatim.
86223
func TestParseGitHubURL(t *testing.T) {

internal/coreapi/oas_json_gen.go

Lines changed: 25 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/coreapi/oas_schemas_gen.go

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/coreapi/spec/core.gen.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,10 @@
371371
"description": "true on fresh creation; false when an existing mirror was returned.",
372372
"type": "boolean"
373373
},
374+
"empty": {
375+
"description": "true when the upstream has no refs to clone.",
376+
"type": "boolean"
377+
},
374378
"mirrorId": {
375379
"type": "string"
376380
},
@@ -385,7 +389,8 @@
385389
"mirrorId",
386390
"mirrorUrl",
387391
"publicUrl",
388-
"created"
392+
"created",
393+
"empty"
389394
],
390395
"type": "object"
391396
},

internal/coreapi/spec/core.openapi.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)