Skip to content

Commit a9bbb37

Browse files
authored
client: add support for OXIDE_PROFILE (#332)
Allow reading the authentication profile from the `OXIDE_PROFILE` environment variable.
1 parent bd541f4 commit a9bbb37

File tree

2 files changed

+110
-38
lines changed

2 files changed

+110
-38
lines changed

oxide/lib.go

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const (
2626
// HostEnvVar is the environment variable that contains the host.
2727
HostEnvVar = "OXIDE_HOST"
2828

29+
// ProfileEnvVar is the environment variable that contains the credentials
30+
// profile to use.
31+
ProfileEnvVar = "OXIDE_PROFILE"
32+
2933
// credentialsFile is the name of the file the Oxide CLI stores credentials in.
3034
credentialsFile = "credentials.toml"
3135

@@ -89,47 +93,42 @@ type authCredentials struct {
8993
}
9094

9195
// NewClient creates a new client for the Oxide API. To authenticate with
92-
// environment variables, set OXIDE_HOST and OXIDE_TOKEN accordingly. Pass in a
93-
// non-nil *Config to set the various configuration options on a Client. When
94-
// setting the host and token through the *Config, these will override any set
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.
96+
// environment variables, set either OXIDE_HOST and OXIDE_TOKEN, or
97+
// OXIDE_PROFILE accordingly. Pass in a non-nil *Config to set the various
98+
// configuration options on a Client. When setting the host, token, or profile
99+
// through the *Config, these will override any set environment variables. The
100+
// Profile and UseDefaultProfile fields will pull authentication information
101+
// from the credentials.toml file generated by the Oxide CLI. These are
102+
// mutually exclusive with each other and the Host and Token fields.
99103
func NewClient(cfg *Config) (*Client, error) {
100104
token := os.Getenv(TokenEnvVar)
101105
host := os.Getenv(HostEnvVar)
106+
profile := os.Getenv(ProfileEnvVar)
107+
useDefaultProfile := false
102108
userAgent := defaultUserAgent()
103109
httpClient := &http.Client{
104110
Timeout: 600 * time.Second,
105111
}
106112

107-
// Layer in the user-provided configuration if provided.
113+
// Layer in the user-provided configuration if provided and override
114+
// environment variables when needed.
108115
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-
}
116+
useDefaultProfile = cfg.UseDefaultProfile
126117

127118
if cfg.Host != "" {
128119
host = cfg.Host
120+
profile = cfg.Profile // Ignore OXIDE_PROFILE.
129121
}
130122

131123
if cfg.Token != "" {
132124
token = cfg.Token
125+
profile = cfg.Profile // Ignore OXIDE_PROFILE.
126+
}
127+
128+
if cfg.Profile != "" {
129+
profile = cfg.Profile
130+
host = cfg.Host // Ignore OXIDE_HOST.
131+
token = cfg.Token // Ignore OXIDE_TOKEN.
133132
}
134133

135134
if cfg.UserAgent != "" {
@@ -141,6 +140,33 @@ func NewClient(cfg *Config) (*Client, error) {
141140
}
142141
}
143142

143+
if (profile != "" || useDefaultProfile) && (host != "" || token != "") {
144+
return nil, errors.New("cannot authenticate with both a profile and host/token")
145+
}
146+
if profile != "" && useDefaultProfile {
147+
return nil, errors.New("cannot authenticate with both default profile and a defined profile")
148+
}
149+
if profile != "" || useDefaultProfile {
150+
var configDir string
151+
if cfg != nil && cfg.ConfigDir != "" {
152+
configDir = cfg.ConfigDir
153+
} else {
154+
homeDir, err := os.UserHomeDir()
155+
if err != nil {
156+
return nil, fmt.Errorf("unable to find user's home directory: %w", err)
157+
}
158+
configDir = filepath.Join(homeDir, defaultConfigDir)
159+
}
160+
161+
authCredentials, err := getProfile(configDir, profile, useDefaultProfile)
162+
if err != nil {
163+
return nil, fmt.Errorf("unable to retrieve profile: %w", err)
164+
}
165+
166+
host = authCredentials.host
167+
token = authCredentials.token
168+
}
169+
144170
errs := make([]error, 0)
145171
host, err := parseBaseURL(host)
146172
if err != nil {
@@ -173,20 +199,9 @@ func defaultUserAgent() string {
173199

174200
// getProfile determines the path of the user's credentials file
175201
// 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-
202+
func getProfile(configDir string, profile string, useDefault bool) (*authCredentials, error) {
188203
// Use explicitly configured profile over default when both are set.
189-
if cfg.UseDefaultProfile && profile == "" {
204+
if useDefault && profile == "" {
190205
configPath := filepath.Join(configDir, configFile)
191206

192207
var err error

oxide/lib_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,26 @@ func Test_NewClient(t *testing.T) {
227227
userAgent: "bob",
228228
},
229229
},
230+
"succeeds with config, overrides env": {
231+
env: map[string]string{
232+
"OXIDE_PROFILE": "file",
233+
},
234+
config: func(string) *Config {
235+
return &Config{
236+
Host: "http://localhost",
237+
Token: "foo",
238+
}
239+
},
240+
setHome: true,
241+
expectedClient: &Client{
242+
host: "http://localhost/",
243+
token: "foo",
244+
client: &http.Client{
245+
Timeout: 600 * time.Second,
246+
},
247+
userAgent: defaultUserAgent(),
248+
},
249+
},
230250
"succeeds with profile": {
231251
config: func(string) *Config {
232252
return &Config{
@@ -243,6 +263,23 @@ func Test_NewClient(t *testing.T) {
243263
userAgent: defaultUserAgent(),
244264
},
245265
},
266+
"succeeds with profile from env": {
267+
env: map[string]string{
268+
"OXIDE_PROFILE": "file",
269+
},
270+
config: func(string) *Config {
271+
return &Config{}
272+
},
273+
setHome: true,
274+
expectedClient: &Client{
275+
host: "http://file-host/",
276+
token: "file-token",
277+
client: &http.Client{
278+
Timeout: 600 * time.Second,
279+
},
280+
userAgent: defaultUserAgent(),
281+
},
282+
},
246283
"succeeds with default profile": {
247284
config: func(string) *Config {
248285
return &Config{
@@ -311,6 +348,24 @@ func Test_NewClient(t *testing.T) {
311348
userAgent: defaultUserAgent(),
312349
},
313350
},
351+
"succeeds with host and token from different sources ": {
352+
env: map[string]string{
353+
"OXIDE_TOKEN": "foo",
354+
},
355+
config: func(string) *Config {
356+
return &Config{
357+
Host: "http://localhost",
358+
}
359+
},
360+
expectedClient: &Client{
361+
host: "http://localhost/",
362+
token: "foo",
363+
client: &http.Client{
364+
Timeout: 600 * time.Second,
365+
},
366+
userAgent: defaultUserAgent(),
367+
},
368+
},
314369
"fails with missing address using config": {
315370
env: map[string]string{
316371
"OXIDE_HOST": "",
@@ -441,6 +496,8 @@ func Test_NewClient(t *testing.T) {
441496

442497
if testCase.expectedError != "" {
443498
assert.EqualError(t, err, strings.ReplaceAll(testCase.expectedError, "<OXIDE_DIR>", oxideDir))
499+
} else {
500+
assert.NoError(t, err)
444501
}
445502

446503
assert.Equal(t, testCase.expectedClient, c)

0 commit comments

Comments
 (0)