/
config.go
378 lines (329 loc) · 10.6 KB
/
config.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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package gce
import (
"os"
"github.com/juju/errors"
"github.com/juju/schema"
"github.com/juju/juju/environs/config"
"github.com/juju/juju/provider/gce/google"
)
// TODO(ericsnow) While not strictly config-related, we could use some
// mechanism by which we can validate the values we've hard-coded in
// this provider match up with the external authoritative sources. One
// example of this is the data stored in instancetypes.go. Similarly
// we should also ensure the cloud-images metadata is correct and
// up-to-date, though that is more the responsibility of that team.
// Regardless, it may be useful to include a tool somewhere in juju
// that we can use to validate this provider's potentially out-of-date
// data.
// The GCE-specific config keys.
const (
cfgAuthFile = "auth-file"
cfgPrivateKey = "private-key"
cfgClientID = "client-id"
cfgClientEmail = "client-email"
cfgRegion = "region"
cfgProjectID = "project-id"
cfgImageEndpoint = "image-endpoint"
)
// boilerplateConfig will be shown in help output, so please keep it up to
// date when you change environment configuration below.
var boilerplateConfig = `
gce:
type: gce
# Google Auth Info
# The GCE provider uses OAuth to authenticate. This requires that
# you set it up and get the relevant credentials. For more information
# see https://cloud.google.com/compute/docs/api/how-tos/authorization.
# The key information can be downloaded as a JSON file, or copied, from:
# https://console.developers.google.com/project/<projet>/apiui/credential
# Either set the path to the downloaded JSON file here:
auth-file:
# ...or set the individual fields for the credentials. Either way, all
# three of these are required and have specific meaning to GCE.
# private-key:
# client-email:
# client-id:
# Google instance info
# To provision instances and perform related operations, the provider
# will need to know which GCE project to use and into which region to
# provision. While the region has a default, the project ID is
# required. For information on the project ID, see
# https://cloud.google.com/compute/docs/projects and regarding regions
# see https://cloud.google.com/compute/docs/zones.
project-id:
# region: us-central1
# The GCE provider uses pre-built images when provisioning instances.
# You can customize the location in which to find them with the
# image-endpoint setting. The default value is the a location within
# GCE, so it will give you the best speed when bootstrapping or adding
# machines. For more information on the image cache see
# https://cloud-images.ubuntu.com/.
# image-endpoint: https://www.googleapis.com
`[1:]
// configFields is the spec for each GCE config value's type.
var configFields = schema.Fields{
cfgAuthFile: schema.String(),
cfgPrivateKey: schema.String(),
cfgClientID: schema.String(),
cfgClientEmail: schema.String(),
cfgRegion: schema.String(),
cfgProjectID: schema.String(),
cfgImageEndpoint: schema.String(),
}
// TODO(ericsnow) Do we need custom defaults for "image-metadata-url" or
// "agent-metadata-url"? The defaults are the official ones (e.g.
// cloud-images).
var configDefaults = schema.Defaults{
cfgAuthFile: "",
// See http://cloud-images.ubuntu.com/releases/streams/v1/com.ubuntu.cloud:released:gce.json
cfgImageEndpoint: "https://www.googleapis.com",
cfgRegion: "us-central1",
}
var configSecretFields = []string{
cfgPrivateKey,
}
var configImmutableFields = []string{
cfgAuthFile,
cfgPrivateKey,
cfgClientID,
cfgClientEmail,
cfgRegion,
cfgProjectID,
cfgImageEndpoint,
}
var configAuthFields = []string{
cfgPrivateKey,
cfgClientID,
cfgClientEmail,
}
// osEnvFields is the mapping from GCE env vars to config keys.
var osEnvFields = map[string]string{
google.OSEnvPrivateKey: cfgPrivateKey,
google.OSEnvClientID: cfgClientID,
google.OSEnvClientEmail: cfgClientEmail,
google.OSEnvRegion: cfgRegion,
google.OSEnvProjectID: cfgProjectID,
google.OSEnvImageEndpoint: cfgImageEndpoint,
}
func parseOSEnv() (map[string]interface{}, error) {
// TODO(ericsnow) Support pulling ID/PK from shell environment variables.
return nil, nil
}
// handleInvalidField converts a google.InvalidConfigValue into a new
// error, translating a {provider/gce/google}.OSEnvVar* value into a
// GCE config key in the new error.
func handleInvalidField(err error) error {
vErr := err.(*google.InvalidConfigValue)
if strValue, ok := vErr.Value.(string); ok && strValue == "" {
key := osEnvFields[vErr.Key]
return errors.Errorf("%s: must not be empty", key)
}
return err
}
type environConfig struct {
*config.Config
attrs map[string]interface{}
credentials *google.Credentials
}
// newConfig builds a new environConfig from the provided Config and
// returns it.
func newConfig(cfg *config.Config) *environConfig {
return &environConfig{
Config: cfg,
attrs: cfg.UnknownAttrs(),
}
}
// newValidConfig builds a new environConfig from the provided Config
// and returns it. This includes applying the provided defaults
// values, if any. The resulting config values are validated.
func newValidConfig(cfg *config.Config, defaults map[string]interface{}) (*environConfig, error) {
credentials, err := parseCredentials(cfg)
if err != nil {
return nil, errors.Trace(err)
}
handled, err := applyCredentials(cfg, credentials)
if err != nil {
return nil, errors.Trace(err)
}
cfg = handled
// Ensure that the provided config is valid.
if err := config.Validate(cfg, nil); err != nil {
return nil, errors.Trace(err)
}
// Apply the defaults and coerce/validate the custom config attrs.
validated, err := cfg.ValidateUnknownAttrs(configFields, defaults)
if err != nil {
return nil, errors.Trace(err)
}
validCfg, err := cfg.Apply(validated)
if err != nil {
return nil, errors.Trace(err)
}
// Build the config.
ecfg := newConfig(validCfg)
ecfg.credentials = credentials
// Do final validation.
if err := ecfg.validate(); err != nil {
return nil, errors.Trace(err)
}
return ecfg, nil
}
func (c *environConfig) authFile() string {
if c.attrs[cfgAuthFile] == nil {
return ""
}
return c.attrs[cfgAuthFile].(string)
}
func (c *environConfig) privateKey() string {
return c.attrs[cfgPrivateKey].(string)
}
func (c *environConfig) clientID() string {
return c.attrs[cfgClientID].(string)
}
func (c *environConfig) clientEmail() string {
return c.attrs[cfgClientEmail].(string)
}
func (c *environConfig) region() string {
return c.attrs[cfgRegion].(string)
}
func (c *environConfig) projectID() string {
return c.attrs[cfgProjectID].(string)
}
// imageEndpoint identifies where the provider should look for
// cloud images (i.e. for simplestreams).
func (c *environConfig) imageEndpoint() string {
return c.attrs[cfgImageEndpoint].(string)
}
// auth build a new Credentials based on the config and returns it.
func (c *environConfig) auth() *google.Credentials {
if c.credentials == nil {
c.credentials = &google.Credentials{
ClientID: c.clientID(),
ClientEmail: c.clientEmail(),
PrivateKey: []byte(c.privateKey()),
}
}
return c.credentials
}
// newConnection build a ConnectionConfig based on the config and returns it.
func (c *environConfig) newConnection() google.ConnectionConfig {
return google.ConnectionConfig{
Region: c.region(),
ProjectID: c.projectID(),
}
}
// secret gathers the "secret" config values and returns them.
func (c *environConfig) secret() map[string]string {
secretAttrs := make(map[string]string, len(configSecretFields))
for _, key := range configSecretFields {
secretAttrs[key] = c.attrs[key].(string)
}
return secretAttrs
}
// validate checks GCE-specific config values.
func (c environConfig) validate() error {
// All fields must be populated, even with just the default.
for field := range configFields {
if dflt, ok := configDefaults[field]; ok && dflt == "" {
continue
}
if c.attrs[field].(string) == "" {
return errors.Errorf("%s: must not be empty", field)
}
}
// Check sanity of GCE fields.
if err := c.auth().Validate(); err != nil {
return errors.Trace(handleInvalidField(err))
}
if err := c.newConnection().Validate(); err != nil {
return errors.Trace(handleInvalidField(err))
}
return nil
}
// update applies changes from the provided config to the env config.
// Changes to any immutable attributes result in an error.
func (c *environConfig) update(cfg *config.Config) error {
// Validate the updates. newValidConfig does not modify the "known"
// config attributes so it is safe to call Validate here first.
if err := config.Validate(cfg, c.Config); err != nil {
return errors.Trace(err)
}
updates, err := newValidConfig(cfg, configDefaults)
if err != nil {
return errors.Trace(err)
}
// Check that no immutable fields have changed.
attrs := updates.UnknownAttrs()
for _, field := range configImmutableFields {
if attrs[field] != c.attrs[field] {
return errors.Errorf("%s: cannot change from %v to %v", field, c.attrs[field], attrs[field])
}
}
// Apply the updates.
c.Config = cfg
c.attrs = cfg.UnknownAttrs()
return nil
}
// parseCredentials extracts the OAuth2 info from the config from the
// individual fields (falling back on the JSON file).
func parseCredentials(cfg *config.Config) (*google.Credentials, error) {
attrs := cfg.UnknownAttrs()
// Try the auth fields first.
values := make(map[string]string)
for _, field := range configAuthFields {
if existing, ok := attrs[field].(string); ok && existing != "" {
for key, candidate := range osEnvFields {
if field == candidate {
values[key] = existing
break
}
}
}
}
if len(values) > 0 {
creds, err := google.NewCredentials(values)
if err != nil {
return nil, errors.Trace(err)
}
return creds, nil
}
// Fall back to the auth file.
filename, ok := attrs[cfgAuthFile].(string)
if !ok || filename == "" {
// The missing credentials will be caught later.
return nil, nil
}
authFile, err := os.Open(filename)
if err != nil {
return nil, errors.Trace(err)
}
defer authFile.Close()
creds, err := google.ParseJSONKey(authFile)
if err != nil {
return nil, errors.Trace(err)
}
return creds, nil
}
func applyCredentials(cfg *config.Config, creds *google.Credentials) (*config.Config, error) {
updates := make(map[string]interface{})
for k, v := range creds.Values() {
if v == "" {
continue
}
if field, ok := osEnvFields[k]; ok {
for _, authField := range configAuthFields {
if field == authField {
updates[field] = v
break
}
}
}
}
updated, err := cfg.Apply(updates)
if err != nil {
return nil, errors.Trace(err)
}
return updated, nil
}