v2.31.0-dev19 35467d7

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 inferRemoteForAttachmentImage(ctx context.Context, imageName string) (*remote.ResolvedRemote, error) {
76	return inferSavedRemote(ctx, "attachment image", imageName, func(ctx context.Context, client *remote.Client) (bool, error) {
77		targets, err := client.FindAttachmentTargetsByImage(ctx, imageName)
78		if err != nil {
79			return false, err
80		}
81		return len(targets) > 0, nil
82	})
83}
84
85func inferRemoteForAttachmentTarget(ctx context.Context, target string) (*remote.ResolvedRemote, error) {
86	return inferSavedRemote(ctx, "attachment target", target, func(ctx context.Context, client *remote.Client) (bool, error) {
87		images, err := client.GetAttachmentsConfig(ctx, target)
88		if err == nil {
89			return len(images) > 0, nil
90		}
91		if !isRemoteNotFoundError(err) {
92			return false, err
93		}
94
95		_, err = client.GetRoute(ctx, target)
96		if err != nil {
97			if isRemoteNotFoundError(err) {
98				return false, nil
99			}
100			return false, err
101		}
102		return true, nil
103	})
104}
105
106func inferRemoteForRepository(ctx context.Context, repository string) (*remote.ResolvedRemote, error) {
107	return inferSavedRemote(ctx, "repository", repository, func(ctx context.Context, client *remote.Client) (bool, error) {
108		tags, err := client.ListTags(ctx, repository)
109		if err != nil {
110			if isRemoteNotFoundError(err) {
111				return false, nil
112			}
113			return false, err
114		}
115		return len(tags) > 0, nil
116	})
117}
118
119func inferSavedRemote(ctx context.Context, targetKind, target string, probe remoteInferenceProbe) (*remote.ResolvedRemote, error) {
120	remotes, ok := loadInferenceCandidateRemotes()
121	if !ok {
122		return nil, nil
123	}
124
125	names := sortedRemoteNames(remotes)
126	matches := make([]*remote.ResolvedRemote, 0, len(names))
127	probeFailures := make([]string, 0)
128
129	for _, name := range names {
130		entry := remotes[name]
131		matched, err := probe(ctx, newRoutesTargetClient(name, entry))
132		if err != nil {
133			probeFailures = append(probeFailures, fmt.Sprintf("%s (%v)", name, err))
134			continue
135		}
136		if matched {
137			matches = append(matches, resolvedRemoteFromEntry(name, entry))
138		}
139	}
140
141	if len(probeFailures) > 0 {
142		return nil, fmt.Errorf("could not safely infer remote for %s %q because probing failed for: %s; use --remote", targetKind, target, strings.Join(probeFailures, ", "))
143	}
144
145	if len(matches) > 1 {
146		names := make([]string, 0, len(matches))
147		for _, match := range matches {
148			names = append(names, match.DisplayName())
149		}
150		return nil, fmt.Errorf("multiple saved remotes match %s %q: %s; use --remote", targetKind, target, strings.Join(names, ", "))
151	}
152
153	if len(matches) == 1 {
154		return matches[0], nil
155	}
156
157	return nil, nil
158}
159
160func loadInferenceCandidateRemotes() (map[string]remote.RemoteEntry, bool) {
161	if _, ok := resolveRoutesExplicitTarget(); ok {
162		return nil, false
163	}
164
165	remotes, err := remote.LoadRemotes("")
166	if err != nil || remotes == nil {
167		return nil, false
168	}
169	if remotes.Active != "" {
170		return nil, false
171	}
172	if len(remotes.Remotes) == 0 {
173		return nil, false
174	}
175
176	return remotes.Remotes, true
177}
178
179func resolvedRemoteFromEntry(name string, entry remote.RemoteEntry) *remote.ResolvedRemote {
180	return &remote.ResolvedRemote{
181		Name:        name,
182		URL:         entry.URL,
183		Token:       resolveRoutesTokenForTarget(name, entry),
184		InsecureTLS: resolveRoutesInsecureForTarget(name, entry),
185	}
186}
187
188func isRemoteNotFoundError(err error) bool {
189	if httpErr, ok := errors.AsType[*remote.HTTPError](err); ok {
190		return httpErr.StatusCode == http.StatusNotFound
191	}
192	return errors.Is(err, domain.ErrRouteNotFound)
193}
194
195func filterPreviewRoutes(routes []domain.Route) []domain.Route {
196	filtered := make([]domain.Route, 0, len(routes))
197	for _, route := range routes {
198		if !strings.Contains(route.Domain, domain.DefaultPreviewSeparator) {
199			filtered = append(filtered, route)
200		}
201	}
202	return filtered
203}

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