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}