@@ -12,15 +12,29 @@ import (
12
12
"net/http"
13
13
"net/url"
14
14
"os"
15
+ "path/filepath"
15
16
"strings"
16
17
"time"
18
+
19
+ "github.com/pelletier/go-toml"
17
20
)
18
21
19
- // TokenEnvVar is the environment variable that contains the token.
20
- const TokenEnvVar = "OXIDE_TOKEN"
22
+ const (
23
+ // TokenEnvVar is the environment variable that contains the token.
24
+ TokenEnvVar = "OXIDE_TOKEN"
25
+
26
+ // HostEnvVar is the environment variable that contains the host.
27
+ HostEnvVar = "OXIDE_HOST"
28
+
29
+ // credentialsFile is the name of the file the Oxide CLI stores credentials in.
30
+ credentialsFile = "credentials.toml"
21
31
22
- // HostEnvVar is the environment variable that contains the host.
23
- const HostEnvVar = "OXIDE_HOST"
32
+ // configFile is the name of the file the Oxide CLI stores its config in.
33
+ configFile = "config.toml"
34
+
35
+ // defaultConfigDir is the default path used by the Oxide CLI for configuration files.
36
+ defaultConfigDir = ".config" + string (filepath .Separator ) + "oxide"
37
+ )
24
38
25
39
// Config is the configuration that can be set on a Client.
26
40
type Config struct {
@@ -37,6 +51,20 @@ type Config struct {
37
51
// A custom user agent string to add to every request instead of the
38
52
// default.
39
53
UserAgent string
54
+
55
+ // The directory to look for Oxide CLI configuration files. Defaults
56
+ // to $HOME/.config/oxide if unset.
57
+ ConfigDir string
58
+
59
+ // The name of the Oxide CLI profile to use for authentication.
60
+ // The Host and Token fields will override their respective values
61
+ // provided by the profile.
62
+ Profile string
63
+
64
+ // Whether to use the default profile listed in the Oxide CLI
65
+ // config.toml file for authentication. Will be overridden by
66
+ // the Profile field.
67
+ UseDefaultProfile bool
40
68
}
41
69
42
70
// Client which conforms to the OpenAPI3 specification for this service.
@@ -55,11 +83,19 @@ type Client struct {
55
83
userAgent string
56
84
}
57
85
86
+ type authCredentials struct {
87
+ host string
88
+ token string
89
+ }
90
+
58
91
// NewClient creates a new client for the Oxide API. To authenticate with
59
92
// environment variables, set OXIDE_HOST and OXIDE_TOKEN accordingly. Pass in a
60
93
// non-nil *Config to set the various configuration options on a Client. When
61
94
// setting the host and token through the *Config, these will override any set
62
- // environment variables.
95
+ // environment variables. The Profile and UseDefaultProfile fields will pull
96
+ // authentication information from the credentials.toml file generated by
97
+ // the Oxide CLI. These are mutally exclusive with each other and the Host and
98
+ // Token fields.
63
99
func NewClient (cfg * Config ) (* Client , error ) {
64
100
token := os .Getenv (TokenEnvVar )
65
101
host := os .Getenv (HostEnvVar )
@@ -70,6 +106,24 @@ func NewClient(cfg *Config) (*Client, error) {
70
106
71
107
// Layer in the user-provided configuration if provided.
72
108
if cfg != nil {
109
+ if cfg .Profile != "" || cfg .UseDefaultProfile {
110
+ if cfg .Profile != "" && cfg .UseDefaultProfile {
111
+ return nil , errors .New ("cannot authenticate with both default profile and a defined profile" )
112
+ }
113
+
114
+ if cfg .Host != "" || cfg .Token != "" {
115
+ return nil , errors .New ("cannot authenticate with both a profile and host/token" )
116
+ }
117
+
118
+ fileCreds , err := getProfile (* cfg )
119
+ if err != nil {
120
+ return nil , fmt .Errorf ("unable to retrieve profile: %w" , err )
121
+ }
122
+
123
+ token = fileCreds .token
124
+ host = fileCreds .host
125
+ }
126
+
73
127
if cfg .Host != "" {
74
128
host = cfg .Host
75
129
}
@@ -87,19 +141,18 @@ func NewClient(cfg *Config) (*Client, error) {
87
141
}
88
142
}
89
143
90
- var errHost error
144
+ errs := make ([] error , 0 )
91
145
host , err := parseBaseURL (host )
92
146
if err != nil {
93
- errHost = fmt .Errorf ("failed parsing host address: %w" , err )
147
+ errs = append ( errs , fmt .Errorf ("failed parsing host address: %w" , err ) )
94
148
}
95
149
96
- var errToken error
97
150
if token == "" {
98
- errToken = errors .New ("token is required" )
151
+ errs = append ( errs , errors .New ("token is required" ) )
99
152
}
100
153
101
154
// To aggregate the validation errors above.
102
- if err := errors .Join (errHost , errToken ); err != nil {
155
+ if err := errors .Join (errs ... ); err != nil {
103
156
return nil , fmt .Errorf ("invalid client configuration:\n %w" , err )
104
157
}
105
158
@@ -118,6 +171,85 @@ func defaultUserAgent() string {
118
171
return fmt .Sprintf ("oxide.go/%s" , version )
119
172
}
120
173
174
+ // getProfile determines the path of the user's credentials file
175
+ // and returns the host and token for the requested profile.
176
+ func getProfile (cfg Config ) (* authCredentials , error ) {
177
+ configDir := cfg .ConfigDir
178
+ if configDir == "" {
179
+ homeDir , err := os .UserHomeDir ()
180
+ if err != nil {
181
+ return nil , fmt .Errorf ("unable to find user's home directory: %w" , err )
182
+ }
183
+ configDir = filepath .Join (homeDir , defaultConfigDir )
184
+ }
185
+
186
+ profile := cfg .Profile
187
+
188
+ // Use explicitly configured profile over default when both are set.
189
+ if cfg .UseDefaultProfile && profile == "" {
190
+ configPath := filepath .Join (configDir , configFile )
191
+
192
+ var err error
193
+ profile , err = defaultProfile (configPath )
194
+ if err != nil {
195
+ return nil , fmt .Errorf ("failed to get default profile from %q: %w" , configPath , err )
196
+ }
197
+ }
198
+
199
+ credentialsPath := filepath .Join (configDir , credentialsFile )
200
+ fileCreds , err := parseCredentialsFile (credentialsPath , profile )
201
+ if err != nil {
202
+ return nil , fmt .Errorf ("failed to get credentials for profile %q from %q: %w" , profile , credentialsPath , err )
203
+ }
204
+
205
+ return fileCreds , nil
206
+ }
207
+
208
+ // defaultProfile returns the default profile from config.toml, if present.
209
+ func defaultProfile (configPath string ) (string , error ) {
210
+ configFile , err := toml .LoadFile (configPath )
211
+ if err != nil {
212
+ return "" , fmt .Errorf ("failed to open config: %w" , err )
213
+ }
214
+
215
+ if profileName := configFile .Get ("default-profile" ); profileName != nil {
216
+ return profileName .(string ), nil
217
+ }
218
+
219
+ return "" , errors .New ("no default profile set" )
220
+ }
221
+
222
+ // parseCredentialsFile parses a credentials.toml and returns the token and host
223
+ // associated with the requested profile.
224
+ func parseCredentialsFile (credentialsPath , profileName string ) (* authCredentials , error ) {
225
+ if profileName == "" {
226
+ return nil , errors .New ("no profile name provided" )
227
+ }
228
+
229
+ credentialsFile , err := toml .LoadFile (credentialsPath )
230
+ if err != nil {
231
+ return nil , fmt .Errorf ("failed to open %q: %v" , credentialsPath , err )
232
+ }
233
+
234
+ profile , ok := credentialsFile .Get ("profile." + profileName ).(* toml.Tree )
235
+ if ! ok {
236
+ return nil , errors .New ("profile not found" )
237
+ }
238
+
239
+ var hostTokenErr error
240
+ token , ok := profile .Get ("token" ).(string )
241
+ if ! ok {
242
+ hostTokenErr = errors .New ("token not found" )
243
+ }
244
+
245
+ host , ok := profile .Get ("host" ).(string )
246
+ if ! ok {
247
+ hostTokenErr = errors .Join (errors .New ("host not found" ))
248
+ }
249
+
250
+ return & authCredentials {host : host , token : token }, hostTokenErr
251
+ }
252
+
121
253
// parseBaseURL parses the base URL from the host URL.
122
254
func parseBaseURL (baseURL string ) (string , error ) {
123
255
if baseURL == "" {
0 commit comments