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 config, _ := LoadClientConfig("")
142 remotes, _ := LoadRemotes("")
143
144 url, remoteName, isRemote := resolveRemoteURL(flagRemote, config, remotes)
145 if isRemote {
146 token = resolveToken(flagToken, config, remotes)
147 insecureTLS = resolveInsecureTLS(flagInsecure, config, remotes, remoteName)
148 }
149
150 return url, token, insecureTLS, isRemote
151}
152
153// resolveRemoteURL resolves the remote URL from various sources.
154func resolveRemoteURL(flagRemote string, config *ClientConfig, remotes *ClientConfig) (url, remoteName string, isRemote bool) {
155 // 1. Check flag
156 if flagRemote != "" {
157 return flagRemote, "", true
158 }
159
160 // 2. Check environment variable
161 if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
162 return envRemote, "", true
163 }
164
165 // 3. Check client config
166 if config != nil && config.Client.Mode == "remote" && config.Client.Remote != "" {
167 return config.Client.Remote, "", true
168 }
169
170 // 4. Check active remote
171 if remotes != nil && remotes.Active != "" {
172 if remote, ok := remotes.Remotes[remotes.Active]; ok {
173 return remote.URL, remotes.Active, true
174 }
175 }
176
177 return "", "", false
178}
179
180// resolveToken resolves the authentication token from various sources.
181func resolveToken(flagToken string, config *ClientConfig, remotes *ClientConfig) string {
182 // 1. Flag token takes precedence
183 if flagToken != "" {
184 return flagToken
185 }
186
187 // 2. Environment variable
188 if envToken := os.Getenv("GORDON_TOKEN"); envToken != "" {
189 return envToken
190 }
191
192 // 3. Config token
193 if config != nil {
194 if config.Client.Token != "" {
195 return config.Client.Token
196 }
197 if config.Client.TokenEnv != "" {
198 return os.Getenv(config.Client.TokenEnv)
199 }
200 }
201
202 // 4. Active remote token
203 if remotes != nil && remotes.Active != "" {
204 if remote, ok := remotes.Remotes[remotes.Active]; ok {
205 if remote.Token != "" {
206 return remote.Token
207 }
208 if remote.TokenEnv != "" {
209 return os.Getenv(remote.TokenEnv)
210 }
211 }
212 }
213
214 return ""
215}
216
217// ResolveInsecureTLSForRemote resolves insecure TLS behavior for a named remote.
218// Precedence: flag > env > client config > specific remote config.
219func ResolveInsecureTLSForRemote(flagInsecure bool, remoteName string) bool {
220 config, _ := LoadClientConfig("")
221 remotes, _ := LoadRemotes("")
222 return resolveInsecureTLS(flagInsecure, config, remotes, remoteName)
223}
224
225func resolveInsecureTLS(flagInsecure bool, config *ClientConfig, remotes *ClientConfig, remoteName string) bool {
226 if flagInsecure {
227 return true
228 }
229
230 if env := os.Getenv("GORDON_INSECURE"); env != "" {
231 if value, err := strconv.ParseBool(env); err == nil {
232 return value
233 }
234 fmt.Fprintf(os.Stderr, "WARNING: invalid GORDON_INSECURE value %q, ignoring\n", env)
235 }
236
237 if config != nil && config.Client.InsecureTLS {
238 return true
239 }
240
241 if remoteName != "" && remotes != nil {
242 if remote, ok := remotes.Remotes[remoteName]; ok && remote.InsecureTLS {
243 return true
244 }
245 }
246
247 return false
248}
249
250// AddRemote adds a new remote to the remotes configuration.
251func AddRemote(name, url, token string) error {
252 config, err := LoadRemotes("")
253 if err != nil {
254 return err
255 }
256
257 config.Remotes[name] = RemoteEntry{
258 URL: url,
259 Token: token,
260 }
261
262 return SaveRemotes("", config)
263}
264
265// RemoveRemote removes a remote from the remotes configuration.
266func RemoveRemote(name string) error {
267 config, err := LoadRemotes("")
268 if err != nil {
269 return err
270 }
271
272 delete(config.Remotes, name)
273
274 // Clear active if it was the removed remote
275 if config.Active == name {
276 config.Active = ""
277 }
278
279 return SaveRemotes("", config)
280}
281
282// SetActiveRemote sets the active remote.
283func SetActiveRemote(name string) error {
284 config, err := LoadRemotes("")
285 if err != nil {
286 return err
287 }
288
289 // Verify remote exists
290 if _, ok := config.Remotes[name]; !ok {
291 return fmt.Errorf("remote '%s' not found", name)
292 }
293
294 config.Active = name
295
296 return SaveRemotes("", config)
297}
298
299// ListRemotes returns all saved remotes.
300func ListRemotes() (map[string]RemoteEntry, string, error) {
301 config, err := LoadRemotes("")
302 if err != nil {
303 return nil, "", err
304 }
305
306 return config.Remotes, config.Active, nil
307}