~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/cloud/clouds.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2015 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
// Package cloud provides functionality to parse information
 
5
// describing clouds, including regions, supported auth types etc.
 
6
package cloud
 
7
 
 
8
import (
 
9
        "fmt"
 
10
        "io/ioutil"
 
11
        "os"
 
12
        "reflect"
 
13
        "sort"
 
14
        "strings"
 
15
 
 
16
        "github.com/juju/errors"
 
17
        "github.com/juju/utils"
 
18
        "gopkg.in/yaml.v2"
 
19
 
 
20
        "github.com/juju/juju/juju/osenv"
 
21
        "github.com/juju/juju/provider/lxd/lxdnames"
 
22
)
 
23
 
 
24
//go:generate go run ../generate/filetoconst/filetoconst.go fallbackPublicCloudInfo fallback-public-cloud.yaml fallback_public_cloud.go 2015 cloud
 
25
 
 
26
// AuthType is the type of authentication used by the cloud.
 
27
type AuthType string
 
28
 
 
29
// AuthTypes is defined to allow sorting AuthType slices.
 
30
type AuthTypes []AuthType
 
31
 
 
32
func (a AuthTypes) Len() int           { return len(a) }
 
33
func (a AuthTypes) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
 
34
func (a AuthTypes) Less(i, j int) bool { return a[i] < a[j] }
 
35
 
 
36
const (
 
37
        // AccessKeyAuthType is an authentication type using a key and secret.
 
38
        AccessKeyAuthType AuthType = "access-key"
 
39
 
 
40
        // UserPassAuthType is an authentication type using a username and password.
 
41
        UserPassAuthType AuthType = "userpass"
 
42
 
 
43
        // OAuth1AuthType is an authentication type using oauth1.
 
44
        OAuth1AuthType AuthType = "oauth1"
 
45
 
 
46
        // OAuth2AuthType is an authentication type using oauth2.
 
47
        OAuth2AuthType AuthType = "oauth2"
 
48
 
 
49
        // JSONFileAuthType is an authentication type that takes a path to
 
50
        // a JSON file.
 
51
        JSONFileAuthType AuthType = "jsonfile"
 
52
 
 
53
        // CertificateAuthType is an authentication type using certificates.
 
54
        CertificateAuthType AuthType = "certificate"
 
55
 
 
56
        // EmptyAuthType is the authentication type used for providers
 
57
        // that require no credentials, e.g. "lxd", and "manual".
 
58
        EmptyAuthType AuthType = "empty"
 
59
)
 
60
 
 
61
// Cloud is a cloud definition.
 
62
type Cloud struct {
 
63
        // Type is the type of cloud, eg ec2, openstack etc.
 
64
        // This is one of the provider names registered with
 
65
        // environs.RegisterProvider.
 
66
        Type string
 
67
 
 
68
        // AuthTypes are the authentication modes supported by the cloud.
 
69
        AuthTypes AuthTypes
 
70
 
 
71
        // Endpoint is the default endpoint for the cloud regions, may be
 
72
        // overridden by a region.
 
73
        Endpoint string
 
74
 
 
75
        // StorageEndpoint is the default storage endpoint for the cloud
 
76
        // regions, may be overridden by a region.
 
77
        StorageEndpoint string
 
78
 
 
79
        // Regions are the regions available in the cloud.
 
80
        //
 
81
        // Regions is a slice, and not a map, because order is important.
 
82
        // The first region in the slice is the default region for the
 
83
        // cloud.
 
84
        Regions []Region
 
85
 
 
86
        // Config contains optional cloud-specific configuration to use
 
87
        // when bootstrapping Juju in this cloud. The cloud configuration
 
88
        // will be combined with Juju-generated, and user-supplied values;
 
89
        // user-supplied values taking precedence.
 
90
        Config map[string]interface{}
 
91
}
 
92
 
 
93
// Region is a cloud region.
 
94
type Region struct {
 
95
        // Name is the name of the region.
 
96
        Name string
 
97
 
 
98
        // Endpoint is the region's primary endpoint URL.
 
99
        Endpoint string
 
100
 
 
101
        // StorageEndpoint is the region's storage endpoint URL.
 
102
        // If the cloud/region does not have a storage-specific
 
103
        // endpoint URL, this will be empty.
 
104
        StorageEndpoint string
 
105
}
 
106
 
 
107
// cloudSet contains cloud definitions, used for marshalling and
 
108
// unmarshalling.
 
109
type cloudSet struct {
 
110
        // Clouds is a map of cloud definitions, keyed on cloud name.
 
111
        Clouds map[string]*cloud `yaml:"clouds"`
 
112
}
 
113
 
 
114
// cloud is equivalent to Cloud, for marshalling and unmarshalling.
 
115
type cloud struct {
 
116
        Type            string                 `yaml:"type"`
 
117
        AuthTypes       []AuthType             `yaml:"auth-types,omitempty,flow"`
 
118
        Endpoint        string                 `yaml:"endpoint,omitempty"`
 
119
        StorageEndpoint string                 `yaml:"storage-endpoint,omitempty"`
 
120
        Regions         regions                `yaml:"regions,omitempty"`
 
121
        Config          map[string]interface{} `yaml:"config,omitempty"`
 
122
}
 
123
 
 
124
// regions is a collection of regions, either as a map and/or
 
125
// as a yaml.MapSlice.
 
126
//
 
127
// When marshalling, we populate the Slice field only. This is
 
128
// necessary for us to control the order of map items.
 
129
//
 
130
// When unmarshalling, we populate both Map and Slice. Map is
 
131
// populated to simplify conversion to Region objects. Slice
 
132
// is populated so we can identify the first map item, which
 
133
// becomes the default region for the cloud.
 
134
type regions struct {
 
135
        Map   map[string]*region
 
136
        Slice yaml.MapSlice
 
137
}
 
138
 
 
139
// region is equivalent to Region, for marshalling and unmarshalling.
 
140
type region struct {
 
141
        Endpoint        string `yaml:"endpoint,omitempty"`
 
142
        StorageEndpoint string `yaml:"storage-endpoint,omitempty"`
 
143
}
 
144
 
 
145
//DefaultLXD is the name of the default lxd cloud.
 
146
const DefaultLXD = "localhost"
 
147
 
 
148
// BuiltInClouds work out of the box.
 
149
var BuiltInClouds = map[string]Cloud{
 
150
        DefaultLXD: {
 
151
                Type:      lxdnames.ProviderType,
 
152
                AuthTypes: []AuthType{EmptyAuthType},
 
153
                Regions:   []Region{{Name: lxdnames.DefaultRegion}},
 
154
        },
 
155
}
 
156
 
 
157
// CloudByName returns the cloud with the specified name.
 
158
// If there exists no cloud with the specified name, an
 
159
// error satisfying errors.IsNotFound will be returned.
 
160
//
 
161
// TODO(axw) write unit tests for this.
 
162
func CloudByName(name string) (*Cloud, error) {
 
163
        // Personal clouds take precedence.
 
164
        personalClouds, err := PersonalCloudMetadata()
 
165
        if err != nil {
 
166
                return nil, errors.Trace(err)
 
167
        }
 
168
        if cloud, ok := personalClouds[name]; ok {
 
169
                return &cloud, nil
 
170
        }
 
171
        clouds, _, err := PublicCloudMetadata(JujuPublicCloudsPath())
 
172
        if err != nil {
 
173
                return nil, errors.Trace(err)
 
174
        }
 
175
        if cloud, ok := clouds[name]; ok {
 
176
                return &cloud, nil
 
177
        }
 
178
        if cloud, ok := BuiltInClouds[name]; ok {
 
179
                return &cloud, nil
 
180
        }
 
181
        return nil, errors.NotFoundf("cloud %s", name)
 
182
}
 
183
 
 
184
// RegionByName finds the region in the given slice with the
 
185
// specified name, with case folding.
 
186
func RegionByName(regions []Region, name string) (*Region, error) {
 
187
        for _, region := range regions {
 
188
                if !strings.EqualFold(region.Name, name) {
 
189
                        continue
 
190
                }
 
191
                return &region, nil
 
192
        }
 
193
        return nil, errors.NewNotFound(nil, fmt.Sprintf(
 
194
                "region %q not found (expected one of %q)",
 
195
                name, RegionNames(regions),
 
196
        ))
 
197
}
 
198
 
 
199
// RegionNames returns a sorted list of the names of the given regions.
 
200
func RegionNames(regions []Region) []string {
 
201
        names := make([]string, len(regions))
 
202
        for i, region := range regions {
 
203
                names[i] = region.Name
 
204
        }
 
205
        sort.Strings(names)
 
206
        return names
 
207
}
 
208
 
 
209
// JujuPublicCloudsPath is the location where public cloud information is
 
210
// expected to be found. Requires JUJU_HOME to be set.
 
211
func JujuPublicCloudsPath() string {
 
212
        return osenv.JujuXDGDataHomePath("public-clouds.yaml")
 
213
}
 
214
 
 
215
// PublicCloudMetadata looks in searchPath for cloud metadata files and if none
 
216
// are found, returns the fallback public cloud metadata.
 
217
func PublicCloudMetadata(searchPath ...string) (result map[string]Cloud, fallbackUsed bool, err error) {
 
218
        for _, file := range searchPath {
 
219
                data, err := ioutil.ReadFile(file)
 
220
                if err != nil && os.IsNotExist(err) {
 
221
                        continue
 
222
                }
 
223
                if err != nil {
 
224
                        return nil, false, errors.Trace(err)
 
225
                }
 
226
                clouds, err := ParseCloudMetadata(data)
 
227
                if err != nil {
 
228
                        return nil, false, errors.Trace(err)
 
229
                }
 
230
                return clouds, false, err
 
231
        }
 
232
        clouds, err := ParseCloudMetadata([]byte(fallbackPublicCloudInfo))
 
233
        return clouds, true, err
 
234
}
 
235
 
 
236
// ParseCloudMetadata parses the given yaml bytes into Clouds metadata.
 
237
func ParseCloudMetadata(data []byte) (map[string]Cloud, error) {
 
238
        var metadata cloudSet
 
239
        if err := yaml.Unmarshal(data, &metadata); err != nil {
 
240
                return nil, errors.Annotate(err, "cannot unmarshal yaml cloud metadata")
 
241
        }
 
242
 
 
243
        // Translate to the exported type. For each cloud, we store
 
244
        // the first region for the cloud as its default region.
 
245
        clouds := make(map[string]Cloud)
 
246
        for name, cloud := range metadata.Clouds {
 
247
                clouds[name] = cloudFromInternal(cloud)
 
248
        }
 
249
        return clouds, nil
 
250
}
 
251
 
 
252
// WritePublicCloudMetadata marshals to YAML and writes the cloud metadata
 
253
// to the public cloud file.
 
254
func WritePublicCloudMetadata(cloudsMap map[string]Cloud) error {
 
255
        data, err := marshalCloudMetadata(cloudsMap)
 
256
        if err != nil {
 
257
                return errors.Trace(err)
 
258
        }
 
259
        return utils.AtomicWriteFile(JujuPublicCloudsPath(), data, 0600)
 
260
}
 
261
 
 
262
// IsSameCloudMetadata returns true if both meta and meta2 contain the
 
263
// same cloud metadata.
 
264
func IsSameCloudMetadata(meta1, meta2 map[string]Cloud) (bool, error) {
 
265
        // The easiest approach is to simply marshall to YAML and compare.
 
266
        yaml1, err := marshalCloudMetadata(meta1)
 
267
        if err != nil {
 
268
                return false, err
 
269
        }
 
270
        yaml2, err := marshalCloudMetadata(meta2)
 
271
        if err != nil {
 
272
                return false, err
 
273
        }
 
274
        return string(yaml1) == string(yaml2), nil
 
275
}
 
276
 
 
277
// marshalCloudMetadata marshals the given clouds to YAML.
 
278
func marshalCloudMetadata(cloudsMap map[string]Cloud) ([]byte, error) {
 
279
        clouds := cloudSet{make(map[string]*cloud)}
 
280
        for name, metadata := range cloudsMap {
 
281
                clouds.Clouds[name] = cloudToInternal(metadata)
 
282
        }
 
283
        data, err := yaml.Marshal(clouds)
 
284
        if err != nil {
 
285
                return nil, errors.Annotate(err, "cannot marshal cloud metadata")
 
286
        }
 
287
        return data, nil
 
288
}
 
289
 
 
290
// MarshalCloud marshals a Cloud to an opaque byte array.
 
291
func MarshalCloud(cloud Cloud) ([]byte, error) {
 
292
        return yaml.Marshal(cloudToInternal(cloud))
 
293
}
 
294
 
 
295
// UnmarshalCloud unmarshals a Cloud from a byte array produced by MarshalCloud.
 
296
func UnmarshalCloud(in []byte) (Cloud, error) {
 
297
        var internal cloud
 
298
        if err := yaml.Unmarshal(in, &internal); err != nil {
 
299
                return Cloud{}, errors.Annotate(err, "cannot unmarshal yaml cloud metadata")
 
300
        }
 
301
        return cloudFromInternal(&internal), nil
 
302
}
 
303
 
 
304
func cloudToInternal(in Cloud) *cloud {
 
305
        var regions regions
 
306
        for _, r := range in.Regions {
 
307
                regions.Slice = append(regions.Slice, yaml.MapItem{
 
308
                        r.Name, region{r.Endpoint, r.StorageEndpoint},
 
309
                })
 
310
        }
 
311
        return &cloud{
 
312
                Type:            in.Type,
 
313
                AuthTypes:       in.AuthTypes,
 
314
                Endpoint:        in.Endpoint,
 
315
                StorageEndpoint: in.StorageEndpoint,
 
316
                Regions:         regions,
 
317
                Config:          in.Config,
 
318
        }
 
319
}
 
320
 
 
321
func cloudFromInternal(in *cloud) Cloud {
 
322
        var regions []Region
 
323
        if len(in.Regions.Map) > 0 {
 
324
                for _, item := range in.Regions.Slice {
 
325
                        name := fmt.Sprint(item.Key)
 
326
                        r := in.Regions.Map[name]
 
327
                        if r == nil {
 
328
                                // r will be nil if none of the fields in
 
329
                                // the YAML are set.
 
330
                                regions = append(regions, Region{Name: name})
 
331
                        } else {
 
332
                                regions = append(regions, Region{
 
333
                                        name, r.Endpoint, r.StorageEndpoint,
 
334
                                })
 
335
                        }
 
336
                }
 
337
        }
 
338
        meta := Cloud{
 
339
                Type:            in.Type,
 
340
                AuthTypes:       in.AuthTypes,
 
341
                Endpoint:        in.Endpoint,
 
342
                StorageEndpoint: in.StorageEndpoint,
 
343
                Regions:         regions,
 
344
                Config:          in.Config,
 
345
        }
 
346
        meta.denormaliseMetadata()
 
347
        return meta
 
348
}
 
349
 
 
350
// MarshalYAML implements the yaml.Marshaler interface.
 
351
func (r regions) MarshalYAML() (interface{}, error) {
 
352
        return r.Slice, nil
 
353
}
 
354
 
 
355
// UnmarshalYAML implements the yaml.Unmarshaler interface.
 
356
func (r *regions) UnmarshalYAML(f func(interface{}) error) error {
 
357
        if err := f(&r.Map); err != nil {
 
358
                return err
 
359
        }
 
360
        return f(&r.Slice)
 
361
}
 
362
 
 
363
// To keep the metadata concise, attributes on the metadata struct which
 
364
// have the same value for each item may be moved up to a higher level in
 
365
// the tree. denormaliseMetadata descends the tree and fills in any missing
 
366
// attributes with values from a higher level.
 
367
func (cloud Cloud) denormaliseMetadata() {
 
368
        for name, region := range cloud.Regions {
 
369
                r := region
 
370
                inherit(&r, &cloud)
 
371
                cloud.Regions[name] = r
 
372
        }
 
373
}
 
374
 
 
375
type structTags map[reflect.Type]map[string]int
 
376
 
 
377
var tagsForType structTags = make(structTags)
 
378
 
 
379
// RegisterStructTags ensures the yaml tags for the given structs are able to be used
 
380
// when parsing cloud metadata.
 
381
func RegisterStructTags(vals ...interface{}) {
 
382
        tags := mkTags(vals...)
 
383
        for k, v := range tags {
 
384
                tagsForType[k] = v
 
385
        }
 
386
}
 
387
 
 
388
func init() {
 
389
        RegisterStructTags(Cloud{}, Region{})
 
390
}
 
391
 
 
392
func mkTags(vals ...interface{}) map[reflect.Type]map[string]int {
 
393
        typeMap := make(map[reflect.Type]map[string]int)
 
394
        for _, v := range vals {
 
395
                t := reflect.TypeOf(v)
 
396
                typeMap[t] = yamlTags(t)
 
397
        }
 
398
        return typeMap
 
399
}
 
400
 
 
401
// yamlTags returns a map from yaml tag to the field index for the string fields in the given type.
 
402
func yamlTags(t reflect.Type) map[string]int {
 
403
        if t.Kind() != reflect.Struct {
 
404
                panic(errors.Errorf("cannot get yaml tags on type %s", t))
 
405
        }
 
406
        tags := make(map[string]int)
 
407
        for i := 0; i < t.NumField(); i++ {
 
408
                f := t.Field(i)
 
409
                if f.Type != reflect.TypeOf("") {
 
410
                        continue
 
411
                }
 
412
                if tag := f.Tag.Get("yaml"); tag != "" {
 
413
                        if i := strings.Index(tag, ","); i >= 0 {
 
414
                                tag = tag[0:i]
 
415
                        }
 
416
                        if tag == "-" {
 
417
                                continue
 
418
                        }
 
419
                        if tag != "" {
 
420
                                f.Name = tag
 
421
                        }
 
422
                }
 
423
                tags[f.Name] = i
 
424
        }
 
425
        return tags
 
426
}
 
427
 
 
428
// inherit sets any blank fields in dst to their equivalent values in fields in src that have matching json tags.
 
429
// The dst parameter must be a pointer to a struct.
 
430
func inherit(dst, src interface{}) {
 
431
        for tag := range tags(dst) {
 
432
                setFieldByTag(dst, tag, fieldByTag(src, tag), false)
 
433
        }
 
434
}
 
435
 
 
436
// tags returns the field offsets for the JSON tags defined by the given value, which must be
 
437
// a struct or a pointer to a struct.
 
438
func tags(x interface{}) map[string]int {
 
439
        t := reflect.TypeOf(x)
 
440
        if t.Kind() == reflect.Ptr {
 
441
                t = t.Elem()
 
442
        }
 
443
        if t.Kind() != reflect.Struct {
 
444
                panic(errors.Errorf("expected struct, not %s", t))
 
445
        }
 
446
 
 
447
        if tagm := tagsForType[t]; tagm != nil {
 
448
                return tagm
 
449
        }
 
450
        panic(errors.Errorf("%s not found in type table", t))
 
451
}
 
452
 
 
453
// fieldByTag returns the value for the field in x with the given JSON tag, or "" if there is no such field.
 
454
func fieldByTag(x interface{}, tag string) string {
 
455
        tagm := tags(x)
 
456
        v := reflect.ValueOf(x)
 
457
        if v.Kind() == reflect.Ptr {
 
458
                v = v.Elem()
 
459
        }
 
460
        if i, ok := tagm[tag]; ok {
 
461
                return v.Field(i).Interface().(string)
 
462
        }
 
463
        return ""
 
464
}
 
465
 
 
466
// setFieldByTag sets the value for the field in x with the given JSON tag to val.
 
467
// The override parameter specifies whether the value will be set even if the original value is non-empty.
 
468
func setFieldByTag(x interface{}, tag, val string, override bool) {
 
469
        i, ok := tags(x)[tag]
 
470
        if !ok {
 
471
                return
 
472
        }
 
473
        v := reflect.ValueOf(x).Elem()
 
474
        f := v.Field(i)
 
475
        if override || f.Interface().(string) == "" {
 
476
                f.Set(reflect.ValueOf(val))
 
477
        }
 
478
}