v2.31.0-dev20 020cc00

remote_inference.go RawHistoryFolder

1package cli
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"net/http"
8	"strings"
9
10	"github.com/bnema/gordon/internal/adapters/in/cli/remote"
11	"github.com/bnema/gordon/internal/domain"
12	"github.com/bnema/gordon/pkg/validation"
13)
14
15type remoteInferenceProbe func(context.Context, *remote.Client) (bool, error)
16
17func inferPushRemote(ctx context.Context, imageArg, domainFlag, dockerfile string) (*remote.ResolvedRemote, error) {
18	if domainFlag != "" {
19		return inferRemoteForRouteDomain(ctx, domainFlag)
20	}
21
22	if imageArg == "" {
23		detected, err := detectImageName(dockerfile)
24		if err != nil {
25			return nil, err
26		}
27		imageArg = detected
28	}
29
30	if looksLikeLegacyDomain(imageArg) {
31		lookupImage := imageArg
32		domainLookupArg := imageArg
33		if parsedImage, ref := validation.ParseImageReference(imageArg); parsedImage != imageArg {
34			domainLookupArg = parsedImage
35			if !strings.HasPrefix(ref, "sha256:") {
36				lookupImage = parsedImage
37			}
38		}
39
40		resolved, err := inferRemoteForImage(ctx, lookupImage)
41		if err != nil || resolved != nil {
42			return resolved, err
43		}
44
45		return inferRemoteForRouteDomain(ctx, domainLookupArg)
46	}
47
48	classified := classifyPushArgument(imageArg)
49	return inferRemoteForImage(ctx, classified.lookupImage)
50}
51
52func inferRemoteForImage(ctx context.Context, imageName string) (*remote.ResolvedRemote, error) {
53	return inferSavedRemote(ctx, "image", imageName, func(ctx context.Context, client *remote.Client) (bool, error) {
54		routes, err := client.FindRoutesByImage(ctx, imageName)
55		if err != nil {
56			return false, err
57		}
58		return len(filterPreviewRoutes(routes)) > 0, nil
59	})
60}
61
62func inferRemoteForRouteDomain(ctx context.Context, routeDomain string) (*remote.ResolvedRemote, error) {
63	return inferSavedRemote(ctx, "route", routeDomain, func(ctx context.Context, client *remote.Client) (bool, error) {
64		_, err := client.GetRoute(ctx, routeDomain)
65		if err != nil {
66			if isRemoteNotFoundError(err) {
67				return false, nil
68			}
69			return false, err
70		}
71		return true, nil
72	})
73}
74
75func inferRemoteForRouteCleanupDomain(ctx context.Context, routeDomain string) (*remote.ResolvedRemote, error) {
76	return inferSavedRemote(ctx, "route cleanup", routeDomain, func(ctx context.Context, client *remote.Client) (bool, error) {
77		_, err := client.GetRoute(ctx, routeDomain)
78		if err == nil {
79			return true, nil
80		}
81		if !isRemoteNotFoundError(err) {
82			return false, err
83		}
84
85		preview, err := client.GetRouteCleanupPreview(ctx, routeDomain)
86		if err != nil {
87			return false, err
88		}
89		return routeCleanupReportHasEvidence(preview), nil
90	})
91}
92
93func routeCleanupReportHasEvidence(report *domain.CleanupReport) bool {
94	if report == nil {
95		return false
96	}
97	return len(report.PreservedVolumes) > 0 || len(report.PreservedAttachments) > 0 || len(report.OrphanedEntities) > 0
98}
99
100func inferRemoteForAttachmentImage(ctx context.Context, imageName string) (*remote.ResolvedRemote, error) {
101	return inferSavedRemote(ctx, "attachment image", imageName, func(ctx context.Context, client *remote.Client) (bool, error) {
102		targets, err := client.FindAttachmentTargetsByImage(ctx, imageName)
103		if err != nil {
104			return false, err
105		}
106		return len(targets) > 0, nil
107	})
108}
109
110func inferRemoteForAttachmentTarget(ctx context.Context, target string) (*remote.ResolvedRemote, error) {
111	return inferSavedRemote(ctx, "attachment target", target, func(ctx context.Context, client *remote.Client) (bool, error) {
112		images, err := client.GetAttachmentsConfig(ctx, target)
113		if err == nil {
114			return len(images) > 0, nil
115		}
116		if !isRemoteNotFoundError(err) {
117			return false, err
118		}
119
120		_, err = client.GetRoute(ctx, target)
121		if err != nil {
122			if isRemoteNotFoundError(err) {
123				return false, nil
124			}
125			return false, err
126		}
127		return true, nil
128	})
129}
130
131func inferRemoteForRepository(ctx context.Context, repository string) (*remote.ResolvedRemote, error) {
132	return inferSavedRemote(ctx, "repository", repository, func(ctx context.Context, client *remote.Client) (bool, error) {
133		tags, err := client.ListTags(ctx, repository)
134		if err != nil {
135			if isRemoteNotFoundError(err) {
136				return false, nil
137			}
138			return false, err
139		}
140		return len(tags) > 0, nil
141	})
142}
143
144func inferSavedRemote(ctx context.Context, targetKind, target string, probe remoteInferenceProbe) (*remote.ResolvedRemote, error) {
145	remotes, ok := loadInferenceCandidateRemotes()
146	if !ok {
147		return nil, nil
148	}
149
150	names := sortedRemoteNames(remotes)
151	matches := make([]*remote.ResolvedRemote, 0, len(names))
152	probeFailures := make([]string, 0)
153
154	for _, name := range names {
155		entry := remotes[name]
156		matched, err := probe(ctx, newRoutesTargetClient(name, entry))
157		if err != nil {
158			probeFailures = append(probeFailures, fmt.Sprintf("%s (%v)", name, err))
159			continue
160		}
161		if matched {
162			matches = append(matches, resolvedRemoteFromEntry(name, entry))
163		}
164	}
165
166	if len(probeFailures) > 0 {
167		return nil, fmt.Errorf("could not safely infer remote for %s %q because probing failed for: %s; use --remote", targetKind, target, strings.Join(probeFailures, ", "))
168	}
169
170	if len(matches) > 1 {
171		names := make([]string, 0, len(matches))
172		for _, match := range matches {
173			names = append(names, match.DisplayName())
174		}
175		return nil, fmt.Errorf("multiple saved remotes match %s %q: %s; use --remote", targetKind, target, strings.Join(names, ", "))
176	}
177
178	if len(matches) == 1 {
179		return matches[0], nil
180	}
181
182	return nil, nil
183}
184
185func loadInferenceCandidateRemotes() (map[string]remote.RemoteEntry, bool) {
186	if _, ok := resolveRoutesExplicitTarget(); ok {
187		return nil, false
188	}
189
190	remotes, err := remote.LoadRemotes("")
191	if err != nil || remotes == nil {
192		return nil, false
193	}
194	if remotes.Active != "" {
195		return nil, false
196	}
197	if len(remotes.Remotes) == 0 {
198		return nil, false
199	}
200
201	return remotes.Remotes, true
202}
203
204func resolvedRemoteFromEntry(name string, entry remote.RemoteEntry) *remote.ResolvedRemote {
205	return &remote.ResolvedRemote{
206		Name:        name,
207		URL:         entry.URL,
208		Token:       resolveRoutesTokenForTarget(name, entry),
209		InsecureTLS: resolveRoutesInsecureForTarget(name, entry),
210	}
211}
212
213func isRemoteNotFoundError(err error) bool {
214	if httpErr, ok := errors.AsType[*remote.HTTPError](err); ok {
215		return httpErr.StatusCode == http.StatusNotFound
216	}
217	return errors.Is(err, domain.ErrRouteNotFound)
218}
219
220func filterPreviewRoutes(routes []domain.Route) []domain.Route {
221	filtered := make([]domain.Route, 0, len(routes))
222	for _, route := range routes {
223		if !strings.Contains(route.Domain, domain.DefaultPreviewSeparator) {
224			filtered = append(filtered, route)
225		}
226	}
227	return filtered
228}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close