-
Notifications
You must be signed in to change notification settings - Fork 0
/
load.go
293 lines (256 loc) · 7.39 KB
/
load.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package config
import (
"context"
"errors"
"fmt"
"io/fs"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"github.com/common-fate/clio"
"github.com/common-fate/sdk/tokenstore"
)
// LoadDefault is a shorthand function which loads the default SDK configuration by calling New()
// with default values.
//
// By default, config is loaded from environment variables, and then falling back to a TOML configuration
// file for any config values which are not set.
//
// The file is ~/.cf/config by default, but this can be overridden with the CF_CONFIG_PATH environment variable.
//
// Environment variables follow the pattern 'CF_CONFIG_VARIABLE_NAME', where CONFIG_VARIABLE_NAME
// is the name of the configuration value in upper snake-case.
// For example, 'CF_API_URL'.
func LoadDefault(ctx context.Context) (*Context, error) {
return New(ctx, Opts{})
}
type Opts struct {
APIURL string
// AccessURL is the base URL of the Access Handler.
// If empty, the API URL is used.
AccessURL string
// AccessURL is the base URL of the authz service.
// If empty, the API URL is used.
AuthzURL string
ClientID string
ClientSecret string
OIDCIssuer string
// The token storage backend to use for OIDC tokens.
// If not provided, will use the keychain backend.
TokenStore TokenStore
// the config sources to load config from.
// Must be 'env', 'file', or may be a URL to fetch remote config from.
// Defaults to ['env', 'file'] if not provided.
//
// Can be overridden by providing the 'CF_CONFIG_SOURCES' environment variable,
// for example: CF_CONFIG_SOURCES=file,env,https://commonfate.example.com/config.json
ConfigSources []string
}
type configSource interface {
Load(key Key) (string, error)
}
func loadFromSources(value *string, key Key, sources []configSource) error {
if value == nil {
return nil
}
if *value != "" {
return nil
}
for _, source := range sources {
loadedValue, err := source.Load(key)
if err != nil {
return err
}
if loadedValue != "" {
*value = loadedValue
return nil
}
}
return nil
}
// New creates an initialised SDK context to be used for creating SDK clients.
// For example:
//
// import "github.com/common-fate/sdk/config"
// import "github.com/common-fate/sdk/service/access"
//
// cfg, err := config.New(ctx, opts{})
// if err != nil {
// return err
// }
// client := access.NewClient(cfg)
//
// Configuration values, such as the API URL and OIDC Client ID to use,
// can be provided via the Opts{} argument.
//
// The New() method will look configuration values up from environment variables
// and the config file (~/.cf/config by default) if they are not provided in Opts{}.
// By default, the order of priority is:
//
// 1. Static value, set in Opts{}
//
// 2. Environment variable ('env')
//
// 3. Config file ('file')
//
// This behaviour can be customised by setting opts.ConfigSources or by providing the
// 'CF_CONFIG_SOURCES' environment variable, for example: CF_CONFIG_SOURCES=file,env
//
// Environment variables follow the pattern 'CF_CONFIG_VARIABLE_NAME', where CONFIG_VARIABLE_NAME
// is the name of the configuration value in upper snake-case.
// For example, 'CF_API_URL'.
func New(ctx context.Context, opts Opts) (*Context, error) {
// set up a default config
cfg := Context{
APIURL: opts.APIURL,
AccessURL: opts.AccessURL,
AuthzURL: opts.AuthzURL,
OIDCClientID: opts.ClientID,
OIDCIssuer: opts.OIDCIssuer,
OIDCClientSecret: opts.ClientSecret,
}
configSourcesFromEnv := os.Getenv("CF_CONFIG_SOURCES")
if opts.ConfigSources == nil && configSourcesFromEnv == "" {
opts.ConfigSources = []string{"env", "file"}
}
// if no static config sources were provided and CF_CONFIG_SOURCES is set,
// read the config sources from the environment.
if opts.ConfigSources == nil && configSourcesFromEnv != "" {
var newSources []string
sources := strings.Split(configSourcesFromEnv, ",")
for _, s := range sources {
newSources = append(newSources, strings.TrimSpace(s))
}
opts.ConfigSources = newSources
}
var sources []configSource
for _, loaderType := range opts.ConfigSources {
switch loaderType {
case "env":
sources = append(sources, EnvSource{})
case "file":
sources = append(sources, &FileSource{})
default:
// try and parse as a URL
loaderURL, err := url.Parse(loaderType)
if err == nil {
sources = append(sources, &URLSource{DeploymentURL: loaderURL})
} else {
return nil, fmt.Errorf("invalid config loader: %s (valid types are 'env', 'file', or a URL)", loaderType)
}
}
}
clio.Debugw("configured config sources", "sources", opts.ConfigSources)
err := loadFromSources(&cfg.APIURL, APIURLKey, sources)
if err != nil {
return nil, err
}
err = loadFromSources(&cfg.AuthzURL, AuthzURLKey, sources)
if err != nil {
return nil, err
}
err = loadFromSources(&cfg.AccessURL, AccessURLKey, sources)
if err != nil {
return nil, err
}
err = loadFromSources(&cfg.OIDCClientID, OIDCClientIDKey, sources)
if err != nil {
return nil, err
}
err = loadFromSources(&cfg.OIDCClientSecret, OIDCClientSecretKey, sources)
if err != nil {
return nil, err
}
err = loadFromSources(&cfg.OIDCIssuer, OIDCIssuerKey, sources)
if err != nil {
return nil, err
}
err = loadFromSources(&cfg.name, NameKey, sources)
if err != nil {
return nil, err
}
err = cfg.Initialize(ctx, InitializeOpts{TokenStore: opts.TokenStore})
if err != nil {
return nil, err
}
return &cfg, nil
}
// NewServerContext requires all option to be passed and does not attempt to read from the local config file
// it also uses an in memory token store to avoid keychain access
func NewServerContext(ctx context.Context, opts Opts) (*Context, error) {
if opts.ConfigSources == nil {
// don't source from from file
opts.ConfigSources = []string{"env"}
}
if opts.TokenStore == nil {
// default to an in-memory token store
opts.TokenStore = tokenstore.NewInMemoryTokenStore()
}
return New(ctx, opts)
}
func load() (*Config, error) {
// if CF_CONFIG_FILE is set, use a custom file path
// for the config file location.
// the file specified must exist.
customPath := os.Getenv("CF_CONFIG_FILE")
if customPath != "" {
return openConfigFile(customPath)
}
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
fp := filepath.Join(home, ".cf", "config")
cfg, err := openConfigFile(fp)
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrConfigFileNotFound
}
if err != nil {
// otherwise if we get an error, return it
return nil, err
}
return cfg, nil
}
func ListContexts() ([]string, error) {
cfg, err := load()
if err != nil {
return nil, err
}
contexts := []string{}
for k := range cfg.Contexts {
contexts = append(contexts, k)
}
return contexts, nil
}
func SwitchContext(contextName string) error {
cfg, err := load()
if err != nil {
return err
}
if _, ok := cfg.Contexts[contextName]; ok {
cfg.CurrentContext = contextName
return Save(cfg)
}
return errors.New("context not found in config file")
}
func openConfigFile(filepath string) (*Config, error) {
clio.Debugw("loading config", "path", filepath)
file, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer file.Close()
var cfg Config
_, err = toml.NewDecoder(file).Decode(&cfg)
if err != nil {
return nil, err
}
for k, context := range cfg.Contexts {
context.name = k
cfg.Contexts[k] = context
}
clio.Debugw("loaded config", "cfg", cfg)
return &cfg, nil
}