• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gophercloud / gophercloud / 15559696175

10 Jun 2025 12:38PM UTC coverage: 63.628% (-0.1%) from 63.748%
15559696175

push

github

web-flow
Merge pull request #3351 from stephenfin/endpoint-discovery

Add versioned endpoint discovery

65 of 166 new or added lines in 5 files covered. (39.16%)

23408 of 36789 relevant lines covered (63.63%)

98.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.89
/openstack/client.go
1
package openstack
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "reflect"
8
        "strings"
9

10
        "github.com/gophercloud/gophercloud/v2"
11
        tokens2 "github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens"
12
        "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens"
13
        "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1"
14
        tokens3 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens"
15
        "github.com/gophercloud/gophercloud/v2/openstack/utils"
16
)
17

18
const (
19
        // v2 represents Keystone v2.
20
        // It should never increase beyond 2.0.
21
        v2 = "v2.0"
22

23
        // v3 represents Keystone v3.
24
        // The version can be anything from v3 to v3.x.
25
        v3 = "v3"
26
)
27

28
// NewClient prepares an unauthenticated ProviderClient instance.
29
// Most users will probably prefer using the AuthenticatedClient function
30
// instead.
31
//
32
// This is useful if you wish to explicitly control the version of the identity
33
// service that's used for authentication explicitly, for example.
34
//
35
// A basic example of using this would be:
36
//
37
//        ao, err := openstack.AuthOptionsFromEnv()
38
//        provider, err := openstack.NewClient(ao.IdentityEndpoint)
39
//        client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{})
40
func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
12✔
41
        base, err := utils.BaseEndpoint(endpoint)
12✔
42
        if err != nil {
12✔
43
                return nil, err
×
44
        }
×
45

46
        endpoint = gophercloud.NormalizeURL(endpoint)
12✔
47
        base = gophercloud.NormalizeURL(base)
12✔
48

12✔
49
        p := new(gophercloud.ProviderClient)
12✔
50
        p.IdentityBase = base
12✔
51
        p.IdentityEndpoint = endpoint
12✔
52
        p.UseTokenLock()
12✔
53

12✔
54
        return p, nil
12✔
55
}
56

57
// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint
58
// specified by the options, acquires a token, and returns a Provider Client
59
// instance that's ready to operate.
60
//
61
// If the full path to a versioned identity endpoint was specified  (example:
62
// http://example.com:5000/v3), that path will be used as the endpoint to query.
63
//
64
// If a versionless endpoint was specified (example: http://example.com:5000/),
65
// the endpoint will be queried to determine which versions of the identity service
66
// are available, then chooses the most recent or most supported version.
67
//
68
// Example:
69
//
70
//        ao, err := openstack.AuthOptionsFromEnv()
71
//        provider, err := openstack.AuthenticatedClient(ctx, ao)
72
//        client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
73
//                Region: os.Getenv("OS_REGION_NAME"),
74
//        })
75
func AuthenticatedClient(ctx context.Context, options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
10✔
76
        client, err := NewClient(options.IdentityEndpoint)
10✔
77
        if err != nil {
10✔
78
                return nil, err
×
79
        }
×
80

81
        err = Authenticate(ctx, client, options)
10✔
82
        if err != nil {
14✔
83
                return nil, err
4✔
84
        }
4✔
85
        return client, nil
6✔
86
}
87

88
// Authenticate authenticates or re-authenticates against the most
89
// recent identity service supported at the provided endpoint.
90
func Authenticate(ctx context.Context, client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
10✔
91
        versions := []*utils.Version{
10✔
92
                {ID: v2, Priority: 20, Suffix: "/v2.0/"},
10✔
93
                {ID: v3, Priority: 30, Suffix: "/v3/"},
10✔
94
        }
10✔
95

10✔
96
        chosen, endpoint, err := utils.ChooseVersion(ctx, client, versions)
10✔
97
        if err != nil {
10✔
98
                return err
×
99
        }
×
100

101
        switch chosen.ID {
10✔
102
        case v2:
4✔
103
                return v2auth(ctx, client, endpoint, &options, gophercloud.EndpointOpts{})
4✔
104
        case v3:
6✔
105
                return v3auth(ctx, client, endpoint, &options, gophercloud.EndpointOpts{})
6✔
106
        default:
×
107
                // The switch statement must be out of date from the versions list.
×
108
                return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
×
109
        }
110
}
111

112
// AuthenticateV2 explicitly authenticates against the identity v2 endpoint.
113
func AuthenticateV2(ctx context.Context, client *gophercloud.ProviderClient, options tokens2.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
×
114
        return v2auth(ctx, client, "", options, eo)
×
115
}
×
116

117
type v2TokenNoReauth struct {
118
        tokens2.AuthOptionsBuilder
119
}
120

121
func (v2TokenNoReauth) CanReauth() bool { return false }
×
122

123
func v2auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint string, options tokens2.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
4✔
124
        v2Client, err := NewIdentityV2(client, eo)
4✔
125
        if err != nil {
4✔
126
                return err
×
127
        }
×
128

129
        if endpoint != "" {
8✔
130
                v2Client.Endpoint = endpoint
4✔
131
        }
4✔
132

133
        result := tokens2.Create(ctx, v2Client, options)
4✔
134

4✔
135
        err = client.SetTokenAndAuthResult(result)
4✔
136
        if err != nil {
6✔
137
                return err
2✔
138
        }
2✔
139

140
        catalog, err := result.ExtractServiceCatalog()
2✔
141
        if err != nil {
2✔
142
                return err
×
143
        }
×
144

145
        if options.CanReauth() {
2✔
146
                // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
×
147
                // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
×
148
                // this should retry authentication only once
×
149
                tac := *client
×
150
                tac.SetThrowaway(true)
×
151
                tac.ReauthFunc = nil
×
152
                err := tac.SetTokenAndAuthResult(nil)
×
153
                if err != nil {
×
154
                        return err
×
155
                }
×
156
                client.ReauthFunc = func(ctx context.Context) error {
×
157
                        err := v2auth(ctx, &tac, endpoint, &v2TokenNoReauth{options}, eo)
×
158
                        if err != nil {
×
159
                                return err
×
160
                        }
×
161
                        client.CopyTokenFrom(&tac)
×
162
                        return nil
×
163
                }
164
        }
165
        client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
2✔
NEW
166
                return V2Endpoint(ctx, client, catalog, opts)
×
167
        }
×
168

169
        return nil
2✔
170
}
171

172
// AuthenticateV3 explicitly authenticates against the identity v3 service.
173
func AuthenticateV3(ctx context.Context, client *gophercloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
×
174
        return v3auth(ctx, client, "", options, eo)
×
175
}
×
176

177
func v3auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
6✔
178
        // Override the generated service endpoint with the one returned by the version endpoint.
6✔
179
        v3Client, err := NewIdentityV3(client, eo)
6✔
180
        if err != nil {
6✔
181
                return err
×
182
        }
×
183

184
        if endpoint != "" {
12✔
185
                v3Client.Endpoint = endpoint
6✔
186
        }
6✔
187

188
        var catalog *tokens3.ServiceCatalog
6✔
189

6✔
190
        var tokenID string
6✔
191
        // passthroughToken allows to passthrough the token without a scope
6✔
192
        var passthroughToken bool
6✔
193
        switch v := opts.(type) {
6✔
194
        case *gophercloud.AuthOptions:
6✔
195
                tokenID = v.TokenID
6✔
196
                passthroughToken = (v.Scope == nil || *v.Scope == gophercloud.AuthScope{})
6✔
197
        case *tokens3.AuthOptions:
×
198
                tokenID = v.TokenID
×
199
                passthroughToken = (v.Scope == tokens3.Scope{})
×
200
        }
201

202
        if tokenID != "" && passthroughToken {
6✔
203
                // passing through the token ID without requesting a new scope
×
204
                if opts.CanReauth() {
×
205
                        return fmt.Errorf("cannot use AllowReauth, when the token ID is defined and auth scope is not set")
×
206
                }
×
207

208
                v3Client.SetToken(tokenID)
×
209
                result := tokens3.Get(ctx, v3Client, tokenID)
×
210
                if result.Err != nil {
×
211
                        return result.Err
×
212
                }
×
213

214
                err = client.SetTokenAndAuthResult(result)
×
215
                if err != nil {
×
216
                        return err
×
217
                }
×
218

219
                catalog, err = result.ExtractServiceCatalog()
×
220
                if err != nil {
×
221
                        return err
×
222
                }
×
223
        } else {
6✔
224
                var result tokens3.CreateResult
6✔
225
                switch opts.(type) {
6✔
226
                case *ec2tokens.AuthOptions:
×
227
                        result = ec2tokens.Create(ctx, v3Client, opts)
×
228
                case *oauth1.AuthOptions:
×
229
                        result = oauth1.Create(ctx, v3Client, opts)
×
230
                default:
6✔
231
                        result = tokens3.Create(ctx, v3Client, opts)
6✔
232
                }
233

234
                err = client.SetTokenAndAuthResult(result)
6✔
235
                if err != nil {
8✔
236
                        return err
2✔
237
                }
2✔
238

239
                catalog, err = result.ExtractServiceCatalog()
4✔
240
                if err != nil {
4✔
241
                        return err
×
242
                }
×
243
        }
244

245
        if opts.CanReauth() {
4✔
246
                // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
×
247
                // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
×
248
                // this should retry authentication only once
×
249
                tac := *client
×
250
                tac.SetThrowaway(true)
×
251
                tac.ReauthFunc = nil
×
252
                err = tac.SetTokenAndAuthResult(nil)
×
253
                if err != nil {
×
254
                        return err
×
255
                }
×
256
                var tao tokens3.AuthOptionsBuilder
×
257
                switch ot := opts.(type) {
×
258
                case *gophercloud.AuthOptions:
×
259
                        o := *ot
×
260
                        o.AllowReauth = false
×
261
                        tao = &o
×
262
                case *tokens3.AuthOptions:
×
263
                        o := *ot
×
264
                        o.AllowReauth = false
×
265
                        tao = &o
×
266
                case *ec2tokens.AuthOptions:
×
267
                        o := *ot
×
268
                        o.AllowReauth = false
×
269
                        tao = &o
×
270
                case *oauth1.AuthOptions:
×
271
                        o := *ot
×
272
                        o.AllowReauth = false
×
273
                        tao = &o
×
274
                default:
×
275
                        tao = opts
×
276
                }
277
                client.ReauthFunc = func(ctx context.Context) error {
×
278
                        err := v3auth(ctx, &tac, endpoint, tao, eo)
×
279
                        if err != nil {
×
280
                                return err
×
281
                        }
×
282
                        client.CopyTokenFrom(&tac)
×
283
                        return nil
×
284
                }
285
        }
286
        client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
6✔
287
                return V3Endpoint(ctx, client, catalog, opts)
2✔
288
        }
2✔
289

290
        return nil
4✔
291
}
292

293
// NewIdentityV2 creates a ServiceClient that may be used to interact with the
294
// v2 identity service.
295
func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
4✔
296
        endpoint := client.IdentityBase + "v2.0/"
4✔
297
        clientType := "identity"
4✔
298
        var err error
4✔
299
        if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
4✔
300
                eo.ApplyDefaults(clientType)
×
301
                endpoint, err = client.EndpointLocator(eo)
×
302
                if err != nil {
×
303
                        return nil, err
×
304
                }
×
305
        }
306

307
        return &gophercloud.ServiceClient{
4✔
308
                ProviderClient: client,
4✔
309
                Endpoint:       endpoint,
4✔
310
                Type:           clientType,
4✔
311
        }, nil
4✔
312
}
313

314
// NewIdentityV3 creates a ServiceClient that may be used to access the v3
315
// identity service.
316
func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
8✔
317
        endpoint := client.IdentityBase + "v3/"
8✔
318
        clientType := "identity"
8✔
319
        var err error
8✔
320
        if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
10✔
321
                eo.ApplyDefaults(clientType)
2✔
322
                endpoint, err = client.EndpointLocator(eo)
2✔
323
                if err != nil {
2✔
324
                        return nil, err
×
325
                }
×
326
        }
327

328
        // Ensure endpoint still has a suffix of v3.
329
        // This is because EndpointLocator might have found a versionless
330
        // endpoint or the published endpoint is still /v2.0. In both
331
        // cases, we need to fix the endpoint to point to /v3.
332
        base, err := utils.BaseEndpoint(endpoint)
8✔
333
        if err != nil {
8✔
334
                return nil, err
×
335
        }
×
336

337
        base = gophercloud.NormalizeURL(base)
8✔
338

8✔
339
        endpoint = base + "v3/"
8✔
340

8✔
341
        return &gophercloud.ServiceClient{
8✔
342
                ProviderClient: client,
8✔
343
                Endpoint:       endpoint,
8✔
344
                Type:           clientType,
8✔
345
        }, nil
8✔
346
}
347

348
// TODO(stephenfin): Allow passing aliases to all New${SERVICE}V${VERSION} methods in v3
NEW
349
func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string, version int) (*gophercloud.ServiceClient, error) {
×
350
        sc := new(gophercloud.ServiceClient)
×
NEW
351

×
352
        eo.ApplyDefaults(clientType)
×
NEW
353
        if eo.Version != 0 && eo.Version != version {
×
NEW
354
                return sc, errors.New("Conflict between requested service major version and manually set version")
×
NEW
355
        }
×
NEW
356
        eo.Version = version
×
NEW
357

×
358
        url, err := client.EndpointLocator(eo)
×
359
        if err != nil {
×
360
                return sc, err
×
361
        }
×
362

363
        sc.ProviderClient = client
×
364
        sc.Endpoint = url
×
365
        sc.Type = clientType
×
366
        return sc, nil
×
367
}
368

369
// NewBareMetalV1 creates a ServiceClient that may be used with the v1
370
// bare metal package.
371
func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
372
        sc, err := initClientOpts(client, eo, "baremetal", 1)
×
373
        if !strings.HasSuffix(strings.TrimSuffix(sc.Endpoint, "/"), "v1") {
×
374
                sc.ResourceBase = sc.Endpoint + "v1/"
×
375
        }
×
376
        return sc, err
×
377
}
378

379
// NewBareMetalIntrospectionV1 creates a ServiceClient that may be used with the v1
380
// bare metal introspection package.
381
func NewBareMetalIntrospectionV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
382
        return initClientOpts(client, eo, "baremetal-introspection", 1)
×
383
}
×
384

385
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1
386
// object storage package.
387
func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
388
        return initClientOpts(client, eo, "object-store", 1)
×
389
}
×
390

391
// NewComputeV2 creates a ServiceClient that may be used with the v2 compute
392
// package.
393
func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
394
        return initClientOpts(client, eo, "compute", 2)
×
395
}
×
396

397
// NewNetworkV2 creates a ServiceClient that may be used with the v2 network
398
// package.
399
func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
400
        sc, err := initClientOpts(client, eo, "network", 2)
×
401
        sc.ResourceBase = sc.Endpoint + "v2.0/"
×
402
        return sc, err
×
403
}
×
404

405
// TODO(stephenfin): Remove this in v3. We no longer support the V1 Block Storage service.
406
// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1
407
// block storage service.
408
func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
409
        return initClientOpts(client, eo, "volume", 1)
×
410
}
×
411

412
// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2
413
// block storage service.
414
func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
415
        return initClientOpts(client, eo, "block-storage", 2)
×
416
}
×
417

418
// NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service.
419
func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
420
        return initClientOpts(client, eo, "block-storage", 3)
×
421
}
×
422

423
// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service.
424
func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
425
        return initClientOpts(client, eo, "shared-file-system", 2)
×
426
}
×
427

428
// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1
429
// orchestration service.
430
func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
431
        return initClientOpts(client, eo, "orchestration", 1)
×
432
}
×
433

434
// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
435
func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
436
        return initClientOpts(client, eo, "database", 1)
×
437
}
×
438

439
// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS
440
// service.
441
func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
442
        sc, err := initClientOpts(client, eo, "dns", 2)
×
443
        sc.ResourceBase = sc.Endpoint + "v2/"
×
444
        return sc, err
×
445
}
×
446

447
// NewImageV2 creates a ServiceClient that may be used to access the v2 image
448
// service.
449
func NewImageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
450
        sc, err := initClientOpts(client, eo, "image", 2)
×
451
        sc.ResourceBase = sc.Endpoint + "v2/"
×
452
        return sc, err
×
453
}
×
454

455
// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2
456
// load balancer service.
457
func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
458
        sc, err := initClientOpts(client, eo, "load-balancer", 2)
×
459

×
460
        // Fixes edge case having an OpenStack lb endpoint with trailing version number.
×
461
        endpoint := strings.Replace(sc.Endpoint, "v2.0/", "", -1)
×
462

×
463
        sc.ResourceBase = endpoint + "v2.0/"
×
464
        return sc, err
×
465
}
×
466

467
// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging
468
// service.
469
func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
470
        sc, err := initClientOpts(client, eo, "message", 2)
×
471
        sc.MoreHeaders = map[string]string{"Client-ID": clientID}
×
472
        return sc, err
×
473
}
×
474

475
// NewContainerV1 creates a ServiceClient that may be used with v1 container package
476
func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
477
        return initClientOpts(client, eo, "application-container", 1)
×
478
}
×
479

480
// NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key
481
// manager service.
482
func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
483
        sc, err := initClientOpts(client, eo, "key-manager", 1)
×
484
        sc.ResourceBase = sc.Endpoint + "v1/"
×
485
        return sc, err
×
486
}
×
487

488
// NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management
489
// package.
490
func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
491
        return initClientOpts(client, eo, "container-infrastructure-management", 1)
×
492
}
×
493

494
// NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package.
495
func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
496
        return initClientOpts(client, eo, "workflow", 2)
×
497
}
×
498

499
// NewPlacementV1 creates a ServiceClient that may be used with the placement package.
500
func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
×
NEW
501
        return initClientOpts(client, eo, "placement", 1)
×
502
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc