-
Notifications
You must be signed in to change notification settings - Fork 525
/
clouds.go
271 lines (252 loc) · 9.75 KB
/
clouds.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
// package clouds provides a parser for OpenStack credentials stored in a clouds.yaml file.
//
// Example use:
//
// ctx := context.Background()
// ao, eo, tlsConfig, err := clouds.Parse()
// if err != nil {
// panic(err)
// }
//
// providerClient, err := config.NewProviderClient(ctx, ao, config.WithTLSConfig(tlsConfig))
// if err != nil {
// panic(err)
// }
//
// networkClient, err := openstack.NewNetworkV2(providerClient, eo)
// if err != nil {
// panic(err)
// }
package clouds
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"os"
"path"
"reflect"
"github.com/gophercloud/gophercloud"
"gopkg.in/yaml.v2"
)
// Parse fetches a clouds.yaml file from disk and returns the parsed
// credentials.
//
// By default this function mimics the behaviour of python-openstackclient, which is:
//
// - if the environment variable `OS_CLIENT_CONFIG_FILE` is set and points to a
// clouds.yaml, use that location as the only search location for `clouds.yaml` and `secure.yaml`;
// - otherwise, the search locations for `clouds.yaml` and `secure.yaml` are:
// 1. the current working directory (on Linux: `./`)
// 2. the directory `openstack` under the standatd user config location for
// the operating system (on Linux: `${XDG_CONFIG_HOME:-$HOME/.config}/openstack/`)
// 3. on Linux, `/etc/openstack/`
//
// Once `clouds.yaml` is found in a search location, the same location is used to search for `secure.yaml`.
//
// Like in python-openstackclient, relative paths in the `clouds.yaml` section
// `cacert` are interpreted as relative the the current directory, and not to
// the `clouds.yaml` location.
//
// Search locations, as well as individual `clouds.yaml` properties, can be
// overwritten with functional options.
func Parse(opts ...ParseOption) (gophercloud.AuthOptions, gophercloud.EndpointOpts, *tls.Config, error) {
options := cloudOpts{
cloudName: os.Getenv("OS_CLOUD"),
region: os.Getenv("OS_REGION_NAME"),
endpointType: os.Getenv("OS_INTERFACE"),
locations: func() []string {
if path := os.Getenv("OS_CLIENT_CONFIG_FILE"); path != "" {
return []string{path}
}
return nil
}(),
}
for _, apply := range opts {
apply(&options)
}
if options.cloudName == "" {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("the empty string \"\" is not a valid cloud name")
}
// Set the defaults and open the files for reading. This code only runs
// if no override has been set, because it is fallible.
if options.cloudsyamlReader == nil {
if len(options.locations) < 1 {
cwd, err := os.Getwd()
if err != nil {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to get the current working directory: %w", err)
}
userConfig, err := os.UserConfigDir()
if err != nil {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to get the user config directory: %w", err)
}
options.locations = []string{path.Join(cwd, "clouds.yaml"), path.Join(userConfig, "openstack", "clouds.yaml"), path.Join("/etc", "openstack", "clouds.yaml")}
}
for _, cloudsPath := range options.locations {
var errNotFound *os.PathError
f, err := os.Open(cloudsPath)
if err != nil && !errors.As(err, &errNotFound) {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to open %q: %w", cloudsPath, err)
}
if err == nil {
defer f.Close()
options.cloudsyamlReader = f
if options.secureyamlReader == nil {
securePath := path.Join(path.Base(cloudsPath), "secure.yaml")
secureF, err := os.Open(securePath)
if err != nil && !errors.As(err, &errNotFound) {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to open %q: %w", securePath, err)
}
if err == nil {
defer secureF.Close()
options.secureyamlReader = secureF
}
}
}
}
if options.cloudsyamlReader == nil {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("clouds file not found. Search locations were: %v", options.locations)
}
}
// Parse the YAML payloads.
var clouds Clouds
if err := yaml.NewDecoder(options.cloudsyamlReader).Decode(&clouds); err != nil {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, err
}
cloud, ok := clouds.Clouds[options.cloudName]
if !ok {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("cloud %q not found in clouds.yaml", options.cloudName)
}
if options.secureyamlReader != nil {
var secureClouds Clouds
if err := yaml.NewDecoder(options.secureyamlReader).Decode(&secureClouds); err != nil {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to parse secure.yaml: %w", err)
}
if secureCloud, ok := secureClouds.Clouds[options.cloudName]; ok {
// If secureCloud has content and it differs from the cloud entry,
// merge the two together.
if !reflect.DeepEqual((gophercloud.AuthOptions{}), secureClouds) && !reflect.DeepEqual(clouds, secureClouds) {
var err error
cloud, err = mergeClouds(secureCloud, cloud)
if err != nil {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml")
}
}
}
}
tlsConfig, err := computeTLSConfig(cloud, options)
if err != nil {
return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("unable to compute TLS configuration: %w", err)
}
endpointType := coalesce(options.endpointType, cloud.EndpointType, cloud.Interface)
return gophercloud.AuthOptions{
IdentityEndpoint: coalesce(options.authURL, cloud.AuthInfo.AuthURL),
Username: coalesce(options.username, cloud.AuthInfo.Username),
UserID: coalesce(options.userID, cloud.AuthInfo.UserID),
Password: coalesce(options.password, cloud.AuthInfo.Password),
DomainID: coalesce(options.domainID, cloud.AuthInfo.UserDomainID, cloud.AuthInfo.ProjectDomainID, cloud.AuthInfo.DomainID),
DomainName: coalesce(options.domainName, cloud.AuthInfo.UserDomainName, cloud.AuthInfo.ProjectDomainName, cloud.AuthInfo.DomainName),
TenantID: coalesce(options.projectID, cloud.AuthInfo.ProjectID),
TenantName: coalesce(options.projectName, cloud.AuthInfo.ProjectName),
TokenID: coalesce(options.token, cloud.AuthInfo.Token),
Scope: options.scope,
ApplicationCredentialID: coalesce(options.applicationCredentialID, cloud.AuthInfo.ApplicationCredentialID),
ApplicationCredentialName: coalesce(options.applicationCredentialName, cloud.AuthInfo.ApplicationCredentialName),
ApplicationCredentialSecret: coalesce(options.applicationCredentialSecret, cloud.AuthInfo.ApplicationCredentialSecret),
}, gophercloud.EndpointOpts{
Region: coalesce(options.region, cloud.RegionName),
Availability: computeAvailability(endpointType),
},
tlsConfig,
nil
}
// computeAvailability is a helper method to determine the endpoint type
// requested by the user.
func computeAvailability(endpointType string) gophercloud.Availability {
if endpointType == "internal" || endpointType == "internalURL" {
return gophercloud.AvailabilityInternal
}
if endpointType == "admin" || endpointType == "adminURL" {
return gophercloud.AvailabilityAdmin
}
return gophercloud.AvailabilityPublic
}
// coalesce returns the first argument that is not the empty string, or the
// empty string.
func coalesce(items ...string) string {
for _, item := range items {
if item != "" {
return item
}
}
return ""
}
// mergeClouds merges two Clouds recursively (the AuthInfo also gets merged).
// In case both Clouds define a value, the value in the 'override' cloud takes precedence
func mergeClouds(override, cloud Cloud) (Cloud, error) {
overrideJson, err := json.Marshal(override)
if err != nil {
return Cloud{}, err
}
cloudJson, err := json.Marshal(cloud)
if err != nil {
return Cloud{}, err
}
var overrideInterface interface{}
err = json.Unmarshal(overrideJson, &overrideInterface)
if err != nil {
return Cloud{}, err
}
var cloudInterface interface{}
err = json.Unmarshal(cloudJson, &cloudInterface)
if err != nil {
return Cloud{}, err
}
var mergedCloud Cloud
mergedInterface := mergeInterfaces(overrideInterface, cloudInterface)
mergedJson, err := json.Marshal(mergedInterface)
err = json.Unmarshal(mergedJson, &mergedCloud)
if err != nil {
return Cloud{}, err
}
return mergedCloud, nil
}
// merges two interfaces. In cases where a value is defined for both 'overridingInterface' and
// 'inferiorInterface' the value in 'overridingInterface' will take precedence.
func mergeInterfaces(overridingInterface, inferiorInterface interface{}) interface{} {
switch overriding := overridingInterface.(type) {
case map[string]interface{}:
interfaceMap, ok := inferiorInterface.(map[string]interface{})
if !ok {
return overriding
}
for k, v := range interfaceMap {
if overridingValue, ok := overriding[k]; ok {
overriding[k] = mergeInterfaces(overridingValue, v)
} else {
overriding[k] = v
}
}
case []interface{}:
list, ok := inferiorInterface.([]interface{})
if !ok {
return overriding
}
for i := range list {
overriding = append(overriding, list[i])
}
return overriding
case nil:
// mergeClouds(nil, map[string]interface{...}) -> map[string]interface{...}
v, ok := inferiorInterface.(map[string]interface{})
if ok {
return v
}
}
// We don't want to override with empty values
if reflect.DeepEqual(overridingInterface, nil) || reflect.DeepEqual(reflect.Zero(reflect.TypeOf(overridingInterface)).Interface(), overridingInterface) {
return inferiorInterface
} else {
return overridingInterface
}
}