1package cli
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "os"
9 "strconv"
10 "strings"
11
12 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
13 "github.com/bnema/gordon/internal/domain"
14)
15
16type remoteInferenceProbe func(context.Context, *remote.Client) (bool, error)
17
18func inferPushRemote(ctx context.Context, imageArg, _, dockerfile string) (*remote.ResolvedRemote, error) {
19 if imageArg == "" {
20 detected, err := detectImageName(dockerfile)
21 if err != nil {
22 return nil, err
23 }
24 imageArg = detected
25 }
26 classified := classifyPushArgument(imageArg)
27 return inferRemoteForImage(ctx, classified.repository)
28}
29
30func inferRemoteForImage(ctx context.Context, imageName string) (*remote.ResolvedRemote, error) {
31 return inferSavedRemote(ctx, "image", imageName, func(ctx context.Context, client *remote.Client) (bool, error) {
32 tags, err := client.ListTags(ctx, imageName)
33 if err != nil {
34 if isRemoteNotFoundError(err) {
35 return false, nil
36 }
37 return false, err
38 }
39 return len(tags) > 0, nil
40 })
41}
42
43func inferRemoteForRepository(ctx context.Context, repository string) (*remote.ResolvedRemote, error) {
44 return inferSavedRemote(ctx, "repository", repository, func(ctx context.Context, client *remote.Client) (bool, error) {
45 tags, err := client.ListTags(ctx, repository)
46 if err != nil {
47 if isRemoteNotFoundError(err) {
48 return false, nil
49 }
50 return false, err
51 }
52 return len(tags) > 0, nil
53 })
54}
55
56func inferSavedRemote(ctx context.Context, targetKind, target string, probe remoteInferenceProbe) (*remote.ResolvedRemote, error) {
57 remotes, ok := loadInferenceCandidateRemotes()
58 if !ok {
59 return nil, nil
60 }
61
62 names := sortedRemoteNames(remotes)
63 matches := make([]*remote.ResolvedRemote, 0, len(names))
64 probeFailures := make([]string, 0)
65
66 for _, name := range names {
67 entry := remotes[name]
68 matched, err := probe(ctx, newInferenceTargetClient(name, entry))
69 if err != nil {
70 probeFailures = append(probeFailures, fmt.Sprintf("%s (%v)", name, err))
71 continue
72 }
73 if matched {
74 matches = append(matches, resolvedRemoteFromEntry(name, entry))
75 }
76 }
77
78 if len(probeFailures) > 0 {
79 return nil, fmt.Errorf("could not safely infer remote for %s %q because probing failed for: %s; use --remote", targetKind, target, strings.Join(probeFailures, ", "))
80 }
81
82 if len(matches) > 1 {
83 names := make([]string, 0, len(matches))
84 for _, match := range matches {
85 names = append(names, match.DisplayName())
86 }
87 return nil, fmt.Errorf("multiple saved remotes match %s %q: %s; use --remote", targetKind, target, strings.Join(names, ", "))
88 }
89
90 if len(matches) == 1 {
91 return matches[0], nil
92 }
93
94 return nil, nil
95}
96
97func loadInferenceCandidateRemotes() (map[string]remote.RemoteEntry, bool) {
98 if _, ok := resolveExplicitTargetName(); ok {
99 return nil, false
100 }
101
102 remotes, err := remote.LoadRemotes("")
103 if err != nil || remotes == nil {
104 return nil, false
105 }
106 if remotes.Active != "" {
107 return nil, false
108 }
109 if len(remotes.Remotes) == 0 {
110 return nil, false
111 }
112
113 return remotes.Remotes, true
114}
115
116func resolvedRemoteFromEntry(name string, entry remote.RemoteEntry) *remote.ResolvedRemote {
117 return &remote.ResolvedRemote{
118 Name: name,
119 URL: entry.URL,
120 Token: resolveTokenForTarget(name, entry),
121 InsecureTLS: resolveInsecureForTarget(name, entry),
122 }
123}
124
125func isRemoteNotFoundError(err error) bool {
126 if httpErr, ok := errors.AsType[*remote.HTTPError](err); ok {
127 return httpErr.StatusCode == http.StatusNotFound
128 }
129 return errors.Is(err, domain.ErrRouteNotFound)
130}
131
132func resolveExplicitTargetName() (string, bool) {
133 if target := strings.TrimSpace(remoteFlag); target != "" {
134 return target, true
135 }
136 if target := strings.TrimSpace(os.Getenv("GORDON_REMOTE")); target != "" {
137 return target, true
138 }
139 return "", false
140}
141
142func resolveTokenForTarget(name string, entry remote.RemoteEntry) string {
143 if token := strings.TrimSpace(tokenFlag); token != "" {
144 return token
145 }
146 if token := strings.TrimSpace(os.Getenv("GORDON_TOKEN")); token != "" {
147 return token
148 }
149 if name != "" {
150 return remote.ResolveTokenForRemote(name, entry)
151 }
152 return ""
153}
154
155func resolveInsecureForTarget(name string, entry remote.RemoteEntry) bool {
156 if insecureTLSFlag {
157 return true
158 }
159 if env := strings.TrimSpace(os.Getenv("GORDON_INSECURE")); env != "" {
160 if value, err := strconv.ParseBool(env); err == nil {
161 return value
162 }
163 }
164 if name != "" {
165 return entry.InsecureTLS
166 }
167 return false
168}
169
170func newInferenceTargetClient(name string, entry remote.RemoteEntry) *remote.Client {
171 return remote.NewClient(entry.URL, remoteClientOptions(resolveTokenForTarget(name, entry), resolveInsecureForTarget(name, entry))...)
172}