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}