/
client.go
291 lines (248 loc) · 9.8 KB
/
client.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
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright(c) 2019 Wind River Systems, Inc. */
package manager
import (
"context"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/starlingx/inventory/v1/system"
perrors "github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"net/http"
"net/url"
"os"
"strconv"
"strings"
)
const (
// Expected Secret data and string data keys
AuthUrlKey = "OS_AUTH_URL"
UsernameKey = "OS_USERNAME"
UserIDKey = "OS_USERID"
PasswordKey = "OS_PASSWORD"
TenantIDKey = "OS_TENANT_ID"
TenantNameKey = "OS_TENANT_NAME"
DomainIDKey = "OS_PROJECT_DOMAIN_ID"
DomainNameKey = "OS_PROJECT_DOMAIN_NAME"
RegionNameKey = "OS_REGION_NAME"
KeystoneRegionNameKey = "OS_KEYSTONE_REGION_NAME"
ApplicationCredentialIDKey = "OS_APPLICATION_CREDENTIAL_ID"
ApplicationCredentialNameKey = "OS_APPLICATION_CREDENTIAL_NAME"
ApplicationCredentialSecretKey = "OS_APPLICATION_CREDENTIAL_SECRET"
ProjectIDKey = "OS_PROJECT_ID"
ProjectNameKey = "OS_PROJECT_NAME"
InterfaceKey = "OS_INTERFACE"
DebugKey = "OS_DEBUG"
)
const (
// Well-known openstack API attribute values for the system API
SystemEndpointName = "sysinv"
SystemEndpointType = "platform"
)
// Builds the client authentication options from a given secret which should
// contain environment variable like values. For example, OS_AUTH_URL,
// OS_USERNAME, etc...
func GetAuthOptionsFromSecret(endpointSecret *v1.Secret) ([]gophercloud.AuthOptions, error) {
username := string(endpointSecret.Data[UsernameKey])
password := string(endpointSecret.Data[PasswordKey])
authURL := string(endpointSecret.Data[AuthUrlKey])
userID := string(endpointSecret.Data[UserIDKey])
tenantID := string(endpointSecret.Data[TenantIDKey])
tenantName := string(endpointSecret.Data[TenantNameKey])
domainID := string(endpointSecret.Data[DomainIDKey])
domainName := string(endpointSecret.Data[DomainNameKey])
applicationCredentialID := string(endpointSecret.Data[ApplicationCredentialIDKey])
applicationCredentialName := string(endpointSecret.Data[ApplicationCredentialNameKey])
applicationCredentialSecret := string(endpointSecret.Data[ApplicationCredentialSecretKey])
projectID := string(endpointSecret.Data[ProjectIDKey])
projectName := string(endpointSecret.Data[ProjectNameKey])
if projectID != "" {
// If OS_PROJECT_ID is set, overwrite tenantID with the value.
tenantID = projectID
}
if projectName != "" {
// If OS_PROJECT_Name is set, overwrite tenantName with the value.
tenantName = projectName
}
if authURL == "" {
return nil, NewClientError("OS_AUTH_URL must be provided")
}
if userID == "" && username == "" {
return nil, NewClientError("OS_USERID or OS_USERNAME must be provided")
}
if password == "" && applicationCredentialID == "" && applicationCredentialName == "" {
return nil, NewClientError("OS_PASSWORD must be provided")
}
if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" {
return nil, NewClientError("OS_APPLICATION_CREDENTIAL_SECRET must be provided")
}
result := make([]gophercloud.AuthOptions, 0)
for _, entry := range strings.Split(authURL, ",") {
// The authURL may be specified as a comma separated list of URL therefore
// return a list of auth options to the caller.
ao := gophercloud.AuthOptions{
IdentityEndpoint: entry,
UserID: userID,
Username: username,
Password: password,
TenantID: tenantID,
TenantName: tenantName,
DomainID: domainID,
DomainName: domainName,
ApplicationCredentialID: applicationCredentialID,
ApplicationCredentialName: applicationCredentialName,
ApplicationCredentialSecret: applicationCredentialSecret,
}
result = append(result, ao)
}
return result, nil
}
func GetAuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
password := os.Getenv(PasswordKey)
username := os.Getenv(UsernameKey)
authURL := os.Getenv(AuthUrlKey)
userID := os.Getenv(UserIDKey)
tenantID := os.Getenv(TenantIDKey)
tenantName := os.Getenv(TenantNameKey)
domainID := os.Getenv(DomainIDKey)
domainName := os.Getenv(DomainNameKey)
applicationCredentialID := os.Getenv(ApplicationCredentialIDKey)
applicationCredentialName := os.Getenv(ApplicationCredentialNameKey)
applicationCredentialSecret := os.Getenv(ApplicationCredentialSecretKey)
projectID := os.Getenv(ProjectIDKey)
projectName := os.Getenv(ProjectNameKey)
if projectID != "" {
// If OS_PROJECT_ID is set, overwrite tenantID with the value.
tenantID = projectID
}
if projectName != "" {
// If OS_PROJECT_Name is set, overwrite tenantName with the value.
tenantName = projectName
}
if authURL == "" {
return gophercloud.AuthOptions{}, NewClientError("OS_AUTH_URL must be provided")
}
if userID == "" && username == "" {
return gophercloud.AuthOptions{}, NewClientError("OS_USERID or OS_USERNAME must be provided")
}
if password == "" && applicationCredentialID == "" && applicationCredentialName == "" {
return gophercloud.AuthOptions{}, NewClientError("OS_PASSWORD must be provided")
}
if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" {
return gophercloud.AuthOptions{}, NewClientError("OS_APPLICATION_CREDENTIAL_SECRET must be provided")
}
ao := gophercloud.AuthOptions{
IdentityEndpoint: authURL,
UserID: userID,
Username: username,
Password: password,
ApplicationCredentialID: applicationCredentialID,
ApplicationCredentialName: applicationCredentialName,
ApplicationCredentialSecret: applicationCredentialSecret,
}
if tenantID != "" {
ao.TenantID = tenantID
} else {
ao.TenantName = tenantName
}
if domainID != "" {
ao.DomainID = domainID
} else {
ao.DomainName = domainName
}
return ao, nil
}
func (m *PlatformManager) BuildPlatformClient(namespace string) (*gophercloud.ServiceClient, error) {
var provider *gophercloud.ProviderClient
secret := &v1.Secret{}
secretName := types.NamespacedName{Namespace: namespace, Name: SystemEndpointSecretName}
// Lookup the system endpoint secret for this namespace
err := m.GetClient().Get(context.TODO(), secretName, secret)
if err != nil {
err = perrors.Wrap(err, "failed to find system endpoint secret")
return nil, err
}
options, err := GetAuthOptionsFromSecret(secret)
if err != nil {
return nil, err
}
for _, authOptions := range options {
// Force re-authentication on failures.
authOptions.AllowReauth = true
retry:
// Authenticate against the openstack API
provider, err = openstack.AuthenticatedClient(authOptions)
if err != nil {
if urlError, ok := err.(*url.Error); ok {
if urlError.Err.Error() == "EOF" && strings.Contains(authOptions.IdentityEndpoint, HTTPPrefix) {
// The endpoint has been switched to HTTPS mode so automatically
// update our endpoint to HTTPS so that we can continue.
authOptions.IdentityEndpoint = strings.Replace(authOptions.IdentityEndpoint, HTTPPrefix, HTTPSPrefix, 1)
log.Info("retrying authentication request with HTTPS enabled")
goto retry
} else if strings.Contains(err.Error(), HTTPSNotEnabled) && strings.Contains(authOptions.IdentityEndpoint, HTTPSPrefix) {
// The endpoint has been switched to HTTP mode so automatically
// update our endpoint to HTTP so that we can continue.
authOptions.IdentityEndpoint = strings.Replace(authOptions.IdentityEndpoint, HTTPSPrefix, HTTPPrefix, 1)
log.Info("retrying authentication request with HTTPS disabled")
goto retry
}
}
authOptions.Password = "***REDACTED***" // redact for logging
log.Error(err, "failed to authenticate client", "url", authOptions.IdentityEndpoint, "options", authOptions)
} else {
// Use the first successful client
break
}
}
if provider == nil {
return nil, perrors.Wrap(err, "failed to authenticate against all available auth URL options")
}
availability := gophercloud.Availability(secret.Data[InterfaceKey])
if availability == "" {
availability = gophercloud.AvailabilityPublic
}
// Set the destination endpoint options to point to the system API
endpointOpts := gophercloud.EndpointOpts{
Name: SystemEndpointName,
Type: SystemEndpointType,
Availability: availability,
Region: string(secret.Data[RegionNameKey]),
}
// Get the system API URL
urlEndpoint, err := provider.EndpointLocator(endpointOpts)
if err != nil {
err = perrors.Wrapf(err, "failed to find endpoint location, options: %+v", endpointOpts)
return nil, err
}
c := &gophercloud.ServiceClient{
ProviderClient: provider,
Endpoint: urlEndpoint,
ResourceBase: urlEndpoint}
debug, err := strconv.ParseBool(string(secret.Data[DebugKey]))
if err == nil && debug {
// Debug is enabled so log all API requests/responses
t := c.HTTPClient.Transport
if t == nil {
t = http.DefaultTransport
}
c.HTTPClient.Transport = &clients.LogRoundTripper{Rt: t}
}
// Test the client because the authentication endpoint is different from
// the resource endpoint therefore there is no guarantee that it works.
_, err = system.GetDefaultSystem(c)
if err != nil {
err = perrors.Wrap(err, "failed to test client connection")
return nil, err
}
m.lock.Lock()
defer func() { m.lock.Unlock() }()
if obj, ok := m.systems[namespace]; !ok {
m.systems[namespace] = &SystemNamespace{client: c}
} else {
obj.client = c
}
return c, nil
}