Skip to content

Commit 13abf22

Browse files
authored
fixes merging of clouds.yaml & secure.yaml (#143)
* Support for clouds.yaml with per-region overrides (#68) A "cloud" returned from clouds.yaml will not have per-region overrides applied when defined, and when a specific region is requested. This allows defining a cloud with separate Keystone URLs per region. Fixes #68 * added test cases showing cloud without common auth_url
1 parent eca7831 commit 13abf22

File tree

7 files changed

+222
-12
lines changed

7 files changed

+222
-12
lines changed

openstack/clientconfig/requests.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,16 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
299299
cloud.Verify = &iTrue
300300
}
301301

302+
// merging per-region value overrides
303+
if opts.RegionName != "" {
304+
for _, v := range cloud.Regions {
305+
if opts.RegionName == v.Name {
306+
cloud, err = mergeClouds(v.Values, cloud)
307+
break
308+
}
309+
}
310+
}
311+
302312
// TODO: this is where reading vendor files should go be considered when not found in
303313
// clouds-public.yml
304314
// https://github.com/openstack/openstacksdk/tree/master/openstack/config/vendors

openstack/clientconfig/results.go

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package clientconfig
22

3+
import "encoding/json"
4+
35
// PublicClouds represents a collection of PublicCloud entries in clouds-public.yaml file.
46
// The format of the clouds-public.yml is documented at
57
// https://docs.openstack.org/python-openstackclient/latest/configuration/
@@ -16,12 +18,12 @@ type Clouds struct {
1618

1719
// Cloud represents an entry in a clouds.yaml/public-clouds.yaml/secure.yaml file.
1820
type Cloud struct {
19-
Cloud string `yaml:"cloud,omitempty" json:"cloud,omitempty"`
20-
Profile string `yaml:"profile,omitempty" json:"profile,omitempty"`
21-
AuthInfo *AuthInfo `yaml:"auth,omitempty" json:"auth,omitempty"`
22-
AuthType AuthType `yaml:"auth_type,omitempty" json:"auth_type,omitempty"`
23-
RegionName string `yaml:"region_name,omitempty" json:"region_name,omitempty"`
24-
Regions []interface{} `yaml:"regions,omitempty" json:"regions,omitempty"`
21+
Cloud string `yaml:"cloud,omitempty" json:"cloud,omitempty"`
22+
Profile string `yaml:"profile,omitempty" json:"profile,omitempty"`
23+
AuthInfo *AuthInfo `yaml:"auth,omitempty" json:"auth,omitempty"`
24+
AuthType AuthType `yaml:"auth_type,omitempty" json:"auth_type,omitempty"`
25+
RegionName string `yaml:"region_name,omitempty" json:"region_name,omitempty"`
26+
Regions []Region `yaml:"regions,omitempty" json:"regions,omitempty"`
2527

2628
// EndpointType and Interface both specify whether to use the public, internal,
2729
// or admin interface of a service. They should be considered synonymous, but
@@ -125,3 +127,51 @@ type AuthInfo struct {
125127
// been specified and a domain is required for scope.
126128
DefaultDomain string `yaml:"default_domain,omitempty" json:"default_domain,omitempty"`
127129
}
130+
131+
// Region represents a region included as part of cloud in clouds.yaml
132+
// According to Python-based openstacksdk, this can be either a struct (as defined)
133+
// or a plain string. Custom unmarshallers handle both cases.
134+
type Region struct {
135+
Name string `yaml:"name,omitempty" json:"name,omitempty"`
136+
Values Cloud `yaml:"values,omitempty" json:"values,omitempty"`
137+
}
138+
139+
// UnmarshalJSON handles either a plain string acting as the Name property or
140+
// a struct, mimicking the Python-based openstacksdk.
141+
func (r *Region) UnmarshalJSON(data []byte) error {
142+
var name string
143+
if err := json.Unmarshal(data, &name); err == nil {
144+
r.Name = name
145+
return nil
146+
}
147+
148+
type region Region
149+
var tmp region
150+
if err := json.Unmarshal(data, &tmp); err != nil {
151+
return err
152+
}
153+
r.Name = tmp.Name
154+
r.Values = tmp.Values
155+
156+
return nil
157+
}
158+
159+
// UnmarshalYAML handles either a plain string acting as the Name property or
160+
// a struct, mimicking the Python-based openstacksdk.
161+
func (r *Region) UnmarshalYAML(unmarshal func(interface{}) error) error {
162+
var name string
163+
if err := unmarshal(&name); err == nil {
164+
r.Name = name
165+
return nil
166+
}
167+
168+
type region Region
169+
var tmp region
170+
if err := unmarshal(&tmp); err != nil {
171+
return err
172+
}
173+
r.Name = tmp.Name
174+
r.Values = tmp.Values
175+
176+
return nil
177+
}

openstack/clientconfig/testing/clouds.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,40 @@ clouds:
133133
password: "this should be overwritten by secure.yaml"
134134
project_name: "Some Project"
135135
region_name: "PHL"
136+
philadelphia_complex:
137+
auth:
138+
auth_url: "https://phl.example.com:5000/v3"
139+
username: "jdoe"
140+
password: "password"
141+
project_name: "Some Project"
142+
regions:
143+
- name: PHL1
144+
values:
145+
auth:
146+
auth_url: "https://phl1.example.com:5000/v3"
147+
- PHL2
136148
virginia:
137149
auth_type: "v3applicationcredential"
138150
auth:
139151
auth_url: "https://va.example.com:5000/v3"
140152
application_credential_id: "app-cred-id"
141153
application_credential_secret: "secret"
142154
region_name: "VA"
155+
disconnected_clouds:
156+
auth:
157+
username: "jdoe"
158+
password: "password"
159+
project_name: "Some Project"
160+
regions:
161+
- name: SOMEWHERE
162+
values:
163+
auth:
164+
auth_url: "https://somewhere.example.com:5000/v3"
165+
- name: ANYWHERE
166+
values:
167+
auth:
168+
auth_url: "https://anywhere.example.com:5000/v3"
169+
- name: NOWHERE
170+
values:
171+
auth:
172+
auth_url: "https://nowhere.example.com:5000/v3"

openstack/clientconfig/testing/fixtures.go

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,94 @@ var PhiladelphiaCloudYAML = clientconfig.Cloud{
4444
AuthType: "password",
4545
}
4646

47+
var PhiladelphiaComplexPhl1CloudYAML = clientconfig.Cloud{
48+
AuthInfo: &clientconfig.AuthInfo{
49+
AuthURL: "https://phl1.example.com:5000/v3",
50+
Username: "jdoe",
51+
Password: "password",
52+
ProjectName: "Some Project",
53+
},
54+
Regions: []clientconfig.Region{
55+
{
56+
Name: "PHL1",
57+
Values: clientconfig.Cloud{AuthInfo: &clientconfig.AuthInfo{AuthURL: "https://phl1.example.com:5000/v3"}},
58+
},
59+
{
60+
Name: "PHL2",
61+
Values: clientconfig.Cloud{},
62+
},
63+
},
64+
Verify: &iTrue,
65+
}
66+
67+
var PhiladelphiaComplexPhl2CloudYAML = clientconfig.Cloud{
68+
AuthInfo: &clientconfig.AuthInfo{
69+
AuthURL: "https://phl.example.com:5000/v3",
70+
Username: "jdoe",
71+
Password: "password",
72+
ProjectName: "Some Project",
73+
},
74+
Regions: []clientconfig.Region{
75+
{
76+
Name: "PHL1",
77+
Values: clientconfig.Cloud{AuthInfo: &clientconfig.AuthInfo{AuthURL: "https://phl1.example.com:5000/v3"}},
78+
},
79+
{
80+
Name: "PHL2",
81+
Values: clientconfig.Cloud{},
82+
},
83+
},
84+
Verify: &iTrue,
85+
}
86+
87+
var disconnected_regions = []clientconfig.Region{
88+
{
89+
Name: "SOMEWHERE",
90+
Values: clientconfig.Cloud{AuthInfo: &clientconfig.AuthInfo{AuthURL: "https://somewhere.example.com:5000/v3"}},
91+
},
92+
{
93+
Name: "ANYWHERE",
94+
Values: clientconfig.Cloud{AuthInfo: &clientconfig.AuthInfo{AuthURL: "https://anywhere.example.com:5000/v3"}},
95+
},
96+
{
97+
Name: "NOWHERE",
98+
Values: clientconfig.Cloud{AuthInfo: &clientconfig.AuthInfo{AuthURL: "https://nowhere.example.com:5000/v3"}},
99+
},
100+
}
101+
102+
var DisconnectedSomewhereCloudYAML = clientconfig.Cloud{
103+
AuthInfo: &clientconfig.AuthInfo{
104+
AuthURL: "https://somewhere.example.com:5000/v3",
105+
Username: "jdoe",
106+
Password: "password",
107+
ProjectName: "Some Project",
108+
},
109+
Regions: disconnected_regions,
110+
Verify: &iTrue,
111+
}
112+
113+
var DisconnectedAnywhereCloudYAML = clientconfig.Cloud{
114+
AuthInfo: &clientconfig.AuthInfo{
115+
AuthURL: "https://anywhere.example.com:5000/v3",
116+
Username: "jdoe",
117+
Password: "password",
118+
ProjectName: "Some Project",
119+
},
120+
Regions: disconnected_regions,
121+
Verify: &iTrue,
122+
}
123+
124+
var DisconnectedNowhereCloudYAML = clientconfig.Cloud{
125+
AuthInfo: &clientconfig.AuthInfo{
126+
AuthURL: "https://nowhere.example.com:5000/v3",
127+
Username: "jdoe",
128+
Password: "password",
129+
ProjectName: "Some Project",
130+
},
131+
Regions: disconnected_regions,
132+
Verify: &iTrue,
133+
}
134+
47135
var ChicagoCloudYAML = clientconfig.Cloud{
48136
Profile: "rackspace",
49137
RegionName: "ORD",
@@ -197,10 +285,11 @@ var FloridaAuthOpts = &gophercloud.AuthOptions{
197285

198286
var CaliforniaCloudYAML = clientconfig.Cloud{
199287
EndpointType: "internal",
200-
Regions: []interface{}{
201-
"SAN",
202-
"LAX",
203-
},
288+
Regions: []clientconfig.Region{{
289+
Name: "SAN",
290+
}, {
291+
Name: "LAX",
292+
}},
204293
AuthInfo: &clientconfig.AuthInfo{
205294
AuthURL: "https://ca.example.com:5000/v3",
206295
Username: "jdoe",

openstack/clientconfig/testing/requests_test.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,27 @@ func TestGetCloudFromYAML(t *testing.T) {
3333
"chicago_legacy": {Cloud: "chicago_legacy"},
3434
"chicago_useprofile": {Cloud: "chicago_useprofile"},
3535
"philadelphia": {Cloud: "philadelphia"},
36-
"virginia": {Cloud: "virginia"},
36+
"philadelphia_phl1": {
37+
Cloud: "philadelphia_complex",
38+
RegionName: "PHL1",
39+
},
40+
"philadelphia_phl2": {
41+
Cloud: "philadelphia_complex",
42+
RegionName: "PHL2",
43+
},
44+
"virginia": {Cloud: "virginia"},
45+
"disconnected_smw": {
46+
Cloud: "disconnected_clouds",
47+
RegionName: "SOMEWHERE",
48+
},
49+
"disconnected_anw": {
50+
Cloud: "disconnected_clouds",
51+
RegionName: "ANYWHERE",
52+
},
53+
"disconnected_now": {
54+
Cloud: "disconnected_clouds",
55+
RegionName: "NOWHERE",
56+
},
3757
}
3858

3959
expectedClouds := map[string]*clientconfig.Cloud{
@@ -49,7 +69,12 @@ func TestGetCloudFromYAML(t *testing.T) {
4969
"chicago_legacy": &ChicagoCloudLegacyYAML,
5070
"chicago_useprofile": &ChicagoCloudUseProfileYAML,
5171
"philadelphia": &PhiladelphiaCloudYAML,
72+
"philadelphia_phl1": &PhiladelphiaComplexPhl1CloudYAML,
73+
"philadelphia_phl2": &PhiladelphiaComplexPhl2CloudYAML,
5274
"virginia": &VirginiaCloudYAML,
75+
"disconnected_smw": &DisconnectedSomewhereCloudYAML,
76+
"disconnected_anw": &DisconnectedAnywhereCloudYAML,
77+
"disconnected_now": &DisconnectedNowhereCloudYAML,
5378
}
5479

5580
for cloud, clientOpts := range allClientOpts {

openstack/clientconfig/utils.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ func mergeClouds(override, cloud interface{}) (*Cloud, error) {
4646
var mergedCloud Cloud
4747
mergedInterface := mergeInterfaces(overrideInterface, cloudInterface)
4848
mergedJson, err := json.Marshal(mergedInterface)
49-
json.Unmarshal(mergedJson, &mergedCloud)
49+
err = json.Unmarshal(mergedJson, &mergedCloud)
50+
if err != nil {
51+
return nil, err
52+
}
5053
return &mergedCloud, nil
5154
}
5255

terraform/auth/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ func (c *Config) LoadAndValidate() error {
102102
if c.Cloud != "" {
103103
clientOpts.Cloud = c.Cloud
104104

105+
// Passing region allows GetCloudFromYAML to apply per-region overrides
106+
clientOpts.RegionName = c.Region
107+
105108
cloud, err := clientconfig.GetCloudFromYAML(clientOpts)
106109
if err != nil {
107110
return err

0 commit comments

Comments
 (0)