1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
// Package cloud provides functionality to parse information
5
// describing clouds, including regions, supported auth types etc.
16
"github.com/juju/errors"
17
"github.com/juju/utils"
20
"github.com/juju/juju/juju/osenv"
21
"github.com/juju/juju/provider/lxd/lxdnames"
24
//go:generate go run ../generate/filetoconst/filetoconst.go fallbackPublicCloudInfo fallback-public-cloud.yaml fallback_public_cloud.go 2015 cloud
26
// AuthType is the type of authentication used by the cloud.
29
// AuthTypes is defined to allow sorting AuthType slices.
30
type AuthTypes []AuthType
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] }
37
// AccessKeyAuthType is an authentication type using a key and secret.
38
AccessKeyAuthType AuthType = "access-key"
40
// UserPassAuthType is an authentication type using a username and password.
41
UserPassAuthType AuthType = "userpass"
43
// OAuth1AuthType is an authentication type using oauth1.
44
OAuth1AuthType AuthType = "oauth1"
46
// OAuth2AuthType is an authentication type using oauth2.
47
OAuth2AuthType AuthType = "oauth2"
49
// JSONFileAuthType is an authentication type that takes a path to
51
JSONFileAuthType AuthType = "jsonfile"
53
// CertificateAuthType is an authentication type using certificates.
54
CertificateAuthType AuthType = "certificate"
56
// EmptyAuthType is the authentication type used for providers
57
// that require no credentials, e.g. "lxd", and "manual".
58
EmptyAuthType AuthType = "empty"
61
// Cloud is a cloud definition.
63
// Type is the type of cloud, eg ec2, openstack etc.
64
// This is one of the provider names registered with
65
// environs.RegisterProvider.
68
// AuthTypes are the authentication modes supported by the cloud.
71
// Endpoint is the default endpoint for the cloud regions, may be
72
// overridden by a region.
75
// StorageEndpoint is the default storage endpoint for the cloud
76
// regions, may be overridden by a region.
77
StorageEndpoint string
79
// Regions are the regions available in the cloud.
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
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{}
93
// Region is a cloud region.
95
// Name is the name of the region.
98
// Endpoint is the region's primary endpoint URL.
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
107
// cloudSet contains cloud definitions, used for marshalling and
109
type cloudSet struct {
110
// Clouds is a map of cloud definitions, keyed on cloud name.
111
Clouds map[string]*cloud `yaml:"clouds"`
114
// cloud is equivalent to Cloud, for marshalling and unmarshalling.
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"`
124
// regions is a collection of regions, either as a map and/or
125
// as a yaml.MapSlice.
127
// When marshalling, we populate the Slice field only. This is
128
// necessary for us to control the order of map items.
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
139
// region is equivalent to Region, for marshalling and unmarshalling.
141
Endpoint string `yaml:"endpoint,omitempty"`
142
StorageEndpoint string `yaml:"storage-endpoint,omitempty"`
145
//DefaultLXD is the name of the default lxd cloud.
146
const DefaultLXD = "localhost"
148
// BuiltInClouds work out of the box.
149
var BuiltInClouds = map[string]Cloud{
151
Type: lxdnames.ProviderType,
152
AuthTypes: []AuthType{EmptyAuthType},
153
Regions: []Region{{Name: lxdnames.DefaultRegion}},
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.
161
// TODO(axw) write unit tests for this.
162
func CloudByName(name string) (*Cloud, error) {
163
// Personal clouds take precedence.
164
personalClouds, err := PersonalCloudMetadata()
166
return nil, errors.Trace(err)
168
if cloud, ok := personalClouds[name]; ok {
171
clouds, _, err := PublicCloudMetadata(JujuPublicCloudsPath())
173
return nil, errors.Trace(err)
175
if cloud, ok := clouds[name]; ok {
178
if cloud, ok := BuiltInClouds[name]; ok {
181
return nil, errors.NotFoundf("cloud %s", name)
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) {
193
return nil, errors.NewNotFound(nil, fmt.Sprintf(
194
"region %q not found (expected one of %q)",
195
name, RegionNames(regions),
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
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")
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) {
224
return nil, false, errors.Trace(err)
226
clouds, err := ParseCloudMetadata(data)
228
return nil, false, errors.Trace(err)
230
return clouds, false, err
232
clouds, err := ParseCloudMetadata([]byte(fallbackPublicCloudInfo))
233
return clouds, true, err
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")
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)
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)
257
return errors.Trace(err)
259
return utils.AtomicWriteFile(JujuPublicCloudsPath(), data, 0600)
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)
270
yaml2, err := marshalCloudMetadata(meta2)
274
return string(yaml1) == string(yaml2), nil
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)
283
data, err := yaml.Marshal(clouds)
285
return nil, errors.Annotate(err, "cannot marshal cloud metadata")
290
// MarshalCloud marshals a Cloud to an opaque byte array.
291
func MarshalCloud(cloud Cloud) ([]byte, error) {
292
return yaml.Marshal(cloudToInternal(cloud))
295
// UnmarshalCloud unmarshals a Cloud from a byte array produced by MarshalCloud.
296
func UnmarshalCloud(in []byte) (Cloud, error) {
298
if err := yaml.Unmarshal(in, &internal); err != nil {
299
return Cloud{}, errors.Annotate(err, "cannot unmarshal yaml cloud metadata")
301
return cloudFromInternal(&internal), nil
304
func cloudToInternal(in Cloud) *cloud {
306
for _, r := range in.Regions {
307
regions.Slice = append(regions.Slice, yaml.MapItem{
308
r.Name, region{r.Endpoint, r.StorageEndpoint},
313
AuthTypes: in.AuthTypes,
314
Endpoint: in.Endpoint,
315
StorageEndpoint: in.StorageEndpoint,
321
func cloudFromInternal(in *cloud) Cloud {
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]
328
// r will be nil if none of the fields in
330
regions = append(regions, Region{Name: name})
332
regions = append(regions, Region{
333
name, r.Endpoint, r.StorageEndpoint,
340
AuthTypes: in.AuthTypes,
341
Endpoint: in.Endpoint,
342
StorageEndpoint: in.StorageEndpoint,
346
meta.denormaliseMetadata()
350
// MarshalYAML implements the yaml.Marshaler interface.
351
func (r regions) MarshalYAML() (interface{}, error) {
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 {
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 {
371
cloud.Regions[name] = r
375
type structTags map[reflect.Type]map[string]int
377
var tagsForType structTags = make(structTags)
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 {
389
RegisterStructTags(Cloud{}, Region{})
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)
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))
406
tags := make(map[string]int)
407
for i := 0; i < t.NumField(); i++ {
409
if f.Type != reflect.TypeOf("") {
412
if tag := f.Tag.Get("yaml"); tag != "" {
413
if i := strings.Index(tag, ","); i >= 0 {
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)
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 {
443
if t.Kind() != reflect.Struct {
444
panic(errors.Errorf("expected struct, not %s", t))
447
if tagm := tagsForType[t]; tagm != nil {
450
panic(errors.Errorf("%s not found in type table", t))
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 {
456
v := reflect.ValueOf(x)
457
if v.Kind() == reflect.Ptr {
460
if i, ok := tagm[tag]; ok {
461
return v.Field(i).Interface().(string)
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]
473
v := reflect.ValueOf(x).Elem()
475
if override || f.Interface().(string) == "" {
476
f.Set(reflect.ValueOf(val))