1package remote
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strconv"
8
9 "github.com/pelletier/go-toml/v2"
10)
11
12// ClientConfig represents the client-mode configuration.
13type ClientConfig struct {
14 Client ClientSettings `toml:"client"`
15 Remotes map[string]RemoteEntry `toml:"remotes"`
16 Active string `toml:"active"` // Active remote name
17}
18
19// ClientSettings represents the [client] section.
20type ClientSettings struct {
21 Mode string `toml:"mode"` // "local" (default) or "remote"
22 Remote string `toml:"remote"` // Remote Gordon URL
23 Token string `toml:"token"` // Auth token
24 TokenEnv string `toml:"token_env"` // Env var name for token
25 InsecureTLS bool `toml:"insecure_tls"` // Skip TLS verification for remote admin API
26}
27
28// RemoteEntry represents a saved remote in [remotes.*].
29type RemoteEntry struct {
30 URL string `toml:"url"`
31 Token string `toml:"token,omitempty"`
32 TokenEnv string `toml:"token_env,omitempty"`
33 InsecureTLS bool `toml:"insecure_tls,omitempty"`
34}
35
36// DefaultClientConfigPath returns the default client config path.
37func DefaultClientConfigPath() string {
38 configDir, err := os.UserConfigDir()
39 if err != nil {
40 configDir = os.Getenv("HOME")
41 }
42 return filepath.Join(configDir, "gordon", "gordon.toml")
43}
44
45// DefaultRemotesPath returns the default remotes config path.
46func DefaultRemotesPath() string {
47 configDir, err := os.UserConfigDir()
48 if err != nil {
49 configDir = os.Getenv("HOME")
50 }
51 return filepath.Join(configDir, "gordon", "remotes.toml")
52}
53
54// LoadClientConfig loads the client configuration.
55func LoadClientConfig(path string) (*ClientConfig, error) {
56 if path == "" {
57 path = DefaultClientConfigPath()
58 }
59
60 data, err := os.ReadFile(path)
61 if err != nil {
62 if os.IsNotExist(err) {
63 // Return default config
64 return &ClientConfig{
65 Client: ClientSettings{
66 Mode: "local",
67 },
68 Remotes: make(map[string]RemoteEntry),
69 }, nil
70 }
71 return nil, fmt.Errorf("failed to read config: %w", err)
72 }
73
74 var config ClientConfig
75 if err := toml.Unmarshal(data, &config); err != nil {
76 return nil, fmt.Errorf("failed to parse config: %w", err)
77 }
78
79 if config.Remotes == nil {
80 config.Remotes = make(map[string]RemoteEntry)
81 }
82
83 return &config, nil
84}
85
86// LoadRemotes loads the remotes configuration.
87func LoadRemotes(path string) (*ClientConfig, error) {
88 if path == "" {
89 path = DefaultRemotesPath()
90 }
91
92 data, err := os.ReadFile(path)
93 if err != nil {
94 if os.IsNotExist(err) {
95 return &ClientConfig{
96 Remotes: make(map[string]RemoteEntry),
97 }, nil
98 }
99 return nil, fmt.Errorf("failed to read remotes: %w", err)
100 }
101
102 var config ClientConfig
103 if err := toml.Unmarshal(data, &config); err != nil {
104 return nil, fmt.Errorf("failed to parse remotes: %w", err)
105 }
106
107 if config.Remotes == nil {
108 config.Remotes = make(map[string]RemoteEntry)
109 }
110
111 return &config, nil
112}
113
114// SaveRemotes saves the remotes configuration.
115func SaveRemotes(path string, config *ClientConfig) error {
116 if path == "" {
117 path = DefaultRemotesPath()
118 }
119
120 // Ensure directory exists
121 dir := filepath.Dir(path)
122 if err := os.MkdirAll(dir, 0700); err != nil {
123 return fmt.Errorf("failed to create config directory: %w", err)
124 }
125
126 data, err := toml.Marshal(config)
127 if err != nil {
128 return fmt.Errorf("failed to marshal remotes: %w", err)
129 }
130
131 if err := os.WriteFile(path, data, 0600); err != nil {
132 return fmt.Errorf("failed to write remotes: %w", err)
133 }
134
135 return nil
136}
137
138// ResolveRemote resolves the remote URL and token from configuration.
139// Precedence: flag > env > config > active remote
140func ResolveRemote(flagRemote, flagToken string, flagInsecure bool) (url, token string, insecureTLS, isRemote bool) {
141 url, token, insecureTLS, _, isRemote = ResolveRemoteFull(flagRemote, flagToken, flagInsecure)
142 return url, token, insecureTLS, isRemote
143}
144
145// ResolveRemoteFull resolves the remote URL, token, and named remote from configuration.
146// It also returns the remote name for use in token persistence callbacks.
147// Precedence: flag > env > config > active remote
148func ResolveRemoteFull(flagRemote, flagToken string, flagInsecure bool) (url, token string, insecureTLS bool, remoteName string, isRemote bool) {
149 config, _ := LoadClientConfig("")
150 remotes, _ := LoadRemotes("")
151
152 var name string
153 url, name, isRemote = resolveRemoteURL(flagRemote, config, remotes)
154 if isRemote {
155 // resolveToken looks up remotes.Active internally; name == remotes.Active here
156 // because resolveRemoteURL only sets name when it finds the active remote entry.
157 token = resolveToken(flagToken, config, remotes)
158 insecureTLS = resolveInsecureTLS(flagInsecure, config, remotes, name)
159 remoteName = name
160 }
161
162 return url, token, insecureTLS, remoteName, isRemote
163}
164
165// resolveRemoteURL resolves the remote URL from various sources.
166func resolveRemoteURL(flagRemote string, config *ClientConfig, remotes *ClientConfig) (url, remoteName string, isRemote bool) {
167 // 1. Check flag
168 if flagRemote != "" {
169 return flagRemote, "", true
170 }
171
172 // 2. Check environment variable
173 if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
174 return envRemote, "", true
175 }
176
177 // 3. Check client config
178 if config != nil && config.Client.Mode == "remote" && config.Client.Remote != "" {
179 return config.Client.Remote, "", true
180 }
181
182 // 4. Check active remote
183 if remotes != nil && remotes.Active != "" {
184 if remote, ok := remotes.Remotes[remotes.Active]; ok {
185 return remote.URL, remotes.Active, true
186 }
187 }
188
189 return "", "", false
190}
191
192// resolveToken resolves the authentication token from various sources.
193func resolveToken(flagToken string, config *ClientConfig, remotes *ClientConfig) string {
194 activeRemote := activeRemoteEntry(remotes)
195
196 // 1. Flag token takes precedence
197 if flagToken != "" {
198 return flagToken
199 }
200
201 // 2. Environment variable
202 if envToken := os.Getenv("GORDON_TOKEN"); envToken != "" {
203 return envToken
204 }
205
206 // 3. Config token
207 if config != nil {
208 if config.Client.Token != "" {
209 return config.Client.Token
210 }
211 if config.Client.TokenEnv != "" {
212 if envToken := os.Getenv(config.Client.TokenEnv); envToken != "" {
213 return envToken
214 }
215 }
216 }
217
218 // 4. Try pass store for active remote
219 if remotes != nil && remotes.Active != "" && passAvailable() {
220 if token, err := passReadToken(remotes.Active); err == nil && token != "" {
221 return token
222 }
223 }
224
225 // 5. Active remote token
226 if token := resolveRemoteEntryToken(activeRemote); token != "" {
227 return token
228 }
229
230 return ""
231}
232
233func activeRemoteEntry(remotes *ClientConfig) *RemoteEntry {
234 if remotes == nil || remotes.Active == "" {
235 return nil
236 }
237
238 remote, ok := remotes.Remotes[remotes.Active]
239 if !ok {
240 return nil
241 }
242
243 return &remote
244}
245
246func resolveRemoteEntryToken(remote *RemoteEntry) string {
247 if remote == nil {
248 return ""
249 }
250 if remote.Token != "" {
251 return remote.Token
252 }
253 if remote.TokenEnv != "" {
254 return os.Getenv(remote.TokenEnv)
255 }
256 return ""
257}
258
259// ResolveInsecureTLSForRemote resolves insecure TLS behavior for a named remote.
260// Precedence: flag > env > client config > specific remote config.
261func ResolveInsecureTLSForRemote(flagInsecure bool, remoteName string) bool {
262 config, _ := LoadClientConfig("")
263 remotes, _ := LoadRemotes("")
264 return resolveInsecureTLS(flagInsecure, config, remotes, remoteName)
265}
266
267func resolveInsecureTLS(flagInsecure bool, config *ClientConfig, remotes *ClientConfig, remoteName string) bool {
268 if flagInsecure {
269 return true
270 }
271
272 if env := os.Getenv("GORDON_INSECURE"); env != "" {
273 if value, err := strconv.ParseBool(env); err == nil {
274 return value
275 }
276 fmt.Fprintf(os.Stderr, "WARNING: invalid GORDON_INSECURE value %q, ignoring\n", env)
277 }
278
279 if config != nil && config.Client.InsecureTLS {
280 return true
281 }
282
283 if remoteName != "" && remotes != nil {
284 if remote, ok := remotes.Remotes[remoteName]; ok && remote.InsecureTLS {
285 return true
286 }
287 }
288
289 return false
290}
291
292// AddRemote adds a new remote to the remotes configuration.
293func AddRemote(name, url, token string, insecureTLS bool) error {
294 config, err := LoadRemotes("")
295 if err != nil {
296 return err
297 }
298
299 config.Remotes[name] = RemoteEntry{
300 URL: url,
301 Token: token,
302 InsecureTLS: insecureTLS,
303 }
304
305 return SaveRemotes("", config)
306}
307
308// RemoveRemote removes a remote from the remotes configuration.
309func RemoveRemote(name string) error {
310 config, err := LoadRemotes("")
311 if err != nil {
312 return err
313 }
314
315 delete(config.Remotes, name)
316
317 // Clear active if it was the removed remote
318 if config.Active == name {
319 config.Active = ""
320 }
321
322 return SaveRemotes("", config)
323}
324
325// SetActiveRemote sets the active remote.
326func SetActiveRemote(name string) error {
327 config, err := LoadRemotes("")
328 if err != nil {
329 return err
330 }
331
332 // Verify remote exists
333 if _, ok := config.Remotes[name]; !ok {
334 return fmt.Errorf("remote '%s' not found", name)
335 }
336
337 config.Active = name
338
339 return SaveRemotes("", config)
340}
341
342// ListRemotes returns all saved remotes.
343func ListRemotes() (map[string]RemoteEntry, string, error) {
344 config, err := LoadRemotes("")
345 if err != nil {
346 return nil, "", err
347 }
348
349 return config.Remotes, config.Active, nil
350}