Skip to content

Commit eb25d54

Browse files
mxm-trmaxime.hubert
authored and
maxime.hubert
committed
feat(ovhcloud): allow openstack application credentials to authenticate
1 parent 743ecba commit eb25d54

File tree

5 files changed

+141
-24
lines changed

5 files changed

+141
-24
lines changed

cluster-autoscaler/cloudprovider/ovhcloud/ovh_cloud_manager.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"errors"
2323
"fmt"
2424
"io"
25-
"io/ioutil"
2625
"slices"
2726
"sync"
2827
"time"
@@ -89,6 +88,10 @@ type Config struct {
8988
OpenStackPassword string `json:"openstack_password"`
9089
OpenStackDomain string `json:"openstack_domain"`
9190

91+
// OpenStack application credentials if authentication type is set to openstack_application.
92+
OpenStackApplicationCredentialID string `json:"openstack_application_credential_id"`
93+
OpenStackApplicationCredentialSecret string `json:"openstack_application_credential_secret"`
94+
9295
// Application credentials if CA is run as API consumer without using OpenStack keystone.
9396
// Tokens can be created here: https://api.ovh.com/createToken/
9497
ApplicationEndpoint string `json:"application_endpoint"`
@@ -102,6 +105,9 @@ const (
102105
// OpenStackAuthenticationType to request a keystone token credentials.
103106
OpenStackAuthenticationType = "openstack"
104107

108+
// OpenStackApplicationCredentialsAuthenticationType to request a keystone token credentials using keystone application credentials.
109+
OpenStackApplicationCredentialsAuthenticationType = "openstack_application"
110+
105111
// ApplicationConsumerAuthenticationType to consume an application key credentials.
106112
ApplicationConsumerAuthenticationType = "consumer"
107113
)
@@ -131,6 +137,13 @@ func NewManager(configFile io.Reader) (*OvhCloudManager, error) {
131137
return nil, fmt.Errorf("failed to create OpenStack provider: %w", err)
132138
}
133139

140+
client, err = sdk.NewDefaultClientWithToken(openStackProvider.AuthUrl, openStackProvider.Token)
141+
case OpenStackApplicationCredentialsAuthenticationType:
142+
openStackProvider, err = sdk.NewOpenstackApplicationProvider(cfg.OpenStackAuthUrl, cfg.OpenStackApplicationCredentialID, cfg.OpenStackApplicationCredentialSecret, cfg.OpenStackDomain)
143+
if err != nil {
144+
return nil, fmt.Errorf("failed to create OpenStack provider: %w", err)
145+
}
146+
134147
client, err = sdk.NewDefaultClientWithToken(openStackProvider.AuthUrl, openStackProvider.Token)
135148
case ApplicationConsumerAuthenticationType:
136149
client, err = sdk.NewClient(cfg.ApplicationEndpoint, cfg.ApplicationKey, cfg.ApplicationSecret, cfg.ApplicationConsumerKey)
@@ -271,7 +284,7 @@ func (m *OvhCloudManager) setNodePoolsState(pools []sdk.NodePool) {
271284
func readConfig(configFile io.Reader) (*Config, error) {
272285
cfg := &Config{}
273286
if configFile != nil {
274-
body, err := ioutil.ReadAll(configFile)
287+
body, err := io.ReadAll(configFile)
275288
if err != nil {
276289
return nil, fmt.Errorf("failed to read content: %w", err)
277290
}
@@ -295,8 +308,8 @@ func validatePayload(cfg *Config) error {
295308
return fmt.Errorf("`project_id` not found in config file")
296309
}
297310

298-
if cfg.AuthenticationType != OpenStackAuthenticationType && cfg.AuthenticationType != ApplicationConsumerAuthenticationType {
299-
return fmt.Errorf("`authentication_type` should only be `openstack` or `consumer`")
311+
if cfg.AuthenticationType != OpenStackAuthenticationType && cfg.AuthenticationType != OpenStackApplicationCredentialsAuthenticationType && cfg.AuthenticationType != ApplicationConsumerAuthenticationType {
312+
return fmt.Errorf("`authentication_type` should only be `openstack`, `openstack_application` or `consumer`")
300313
}
301314

302315
if cfg.AuthenticationType == OpenStackAuthenticationType {
@@ -317,6 +330,20 @@ func validatePayload(cfg *Config) error {
317330
}
318331
}
319332

333+
if cfg.AuthenticationType == OpenStackApplicationCredentialsAuthenticationType {
334+
if cfg.OpenStackAuthUrl == "" {
335+
return fmt.Errorf("`openstack_auth_url` not found in config file")
336+
}
337+
338+
if cfg.OpenStackApplicationCredentialID == "" {
339+
return fmt.Errorf("`openstack_application_credential_id` not found in config file")
340+
}
341+
342+
if cfg.OpenStackApplicationCredentialSecret == "" {
343+
return fmt.Errorf("`openstack_application_credential_secret` not found in config file")
344+
}
345+
}
346+
320347
if cfg.AuthenticationType == ApplicationConsumerAuthenticationType {
321348
if cfg.ApplicationEndpoint == "" {
322349
return fmt.Errorf("`application_endpoint` not found in config file")

cluster-autoscaler/cloudprovider/ovhcloud/ovh_cloud_manager_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,26 @@ func newTestManager(t *testing.T) *OvhCloudManager {
7878
return manager
7979
}
8080

81+
func TestOvhCloudManager_validateConfig(t *testing.T) {
82+
tests := []struct {
83+
name string
84+
configContent string
85+
expectedErrorMessage string
86+
}{
87+
{
88+
name: "New entry",
89+
configContent: "{}",
90+
expectedErrorMessage: "config content validation failed: `cluster_id` not found in config file",
91+
},
92+
}
93+
for _, tt := range tests {
94+
t.Run(tt.name, func(t *testing.T) {
95+
_, err := NewManager(bytes.NewBufferString(tt.configContent))
96+
assert.ErrorContains(t, err, tt.expectedErrorMessage)
97+
})
98+
}
99+
}
100+
81101
func TestOvhCloudManager_getFlavorsByName(t *testing.T) {
82102
expectedFlavorsByNameFromAPICall := map[string]sdk.Flavor{
83103
"b2-7": {

cluster-autoscaler/cloudprovider/ovhcloud/ovh_cloud_provider_test.go

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import (
2828
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/ovhcloud/sdk"
2929
)
3030

31-
func newTestProvider(t *testing.T) *OVHCloudProvider {
32-
cfg := `{
31+
const (
32+
ovhConsumerConfiguration = `{
3333
"project_id": "projectID",
3434
"cluster_id": "clusterID",
3535
"authentication_type": "consumer",
@@ -38,10 +38,30 @@ func newTestProvider(t *testing.T) *OVHCloudProvider {
3838
"application_secret": "secret",
3939
"application_consumer_key": "consumer_key"
4040
}`
41+
openstackUserPasswordConfiguration = `{
42+
"project_id": "projectID",
43+
"cluster_id": "clusterID",
44+
"authentication_type": "openstack",
45+
"openstack_auth_url": "https://auth.local",
46+
"openstack_domain": "Default",
47+
"openstack_username": "user",
48+
"openstack_password": "password"
49+
}`
50+
openstackApplicationCredentialsConfiguration = `{
51+
"project_id": "projectID",
52+
"cluster_id": "clusterID",
53+
"authentication_type": "openstack_application",
54+
"openstack_auth_url": "https://auth.local",
55+
"openstack_domain": "Default",
56+
"openstack_application_credential_id": "credential_id",
57+
"openstack_application_credential_secret": "credential_secret"
58+
}`
59+
)
4160

61+
func newTestProvider(t *testing.T, cfg string) (*OVHCloudProvider, error) {
4262
manager, err := NewManager(bytes.NewBufferString(cfg))
4363
if err != nil {
44-
assert.FailNow(t, "failed to create manager", err)
64+
return nil, err
4565
}
4666

4767
client := &sdk.ClientMock{}
@@ -110,19 +130,38 @@ func newTestProvider(t *testing.T) *OVHCloudProvider {
110130
}
111131

112132
err = provider.Refresh()
113-
assert.NoError(t, err)
133+
if err != nil {
134+
return provider, err
135+
}
114136

115-
return provider
137+
return provider, nil
116138
}
117139

118140
func TestOVHCloudProvider_BuildOVHcloud(t *testing.T) {
119141
t.Run("create new OVHcloud provider", func(t *testing.T) {
120-
_ = newTestProvider(t)
142+
_, err := newTestProvider(t, ovhConsumerConfiguration)
143+
assert.NoError(t, err)
144+
})
145+
}
146+
147+
// TestOVHCloudProvider_BuildOVHcloudOpenstackConfig validates that the configuration file is correct and the auth server is being resolved.
148+
func TestOVHCloudProvider_BuildOVHcloudOpenstackConfig(t *testing.T) {
149+
t.Run("create new OVHcloud provider", func(t *testing.T) {
150+
_, err := newTestProvider(t, openstackUserPasswordConfiguration)
151+
assert.ErrorContains(t, err, "lookup auth.local")
152+
})
153+
}
154+
155+
func TestOVHCloudProvider_BuildOVHcloudOpenstackApplicationConfig(t *testing.T) {
156+
t.Run("create new OVHcloud provider", func(t *testing.T) {
157+
_, err := newTestProvider(t, openstackApplicationCredentialsConfiguration)
158+
assert.ErrorContains(t, err, "lookup auth.local")
121159
})
122160
}
123161

124162
func TestOVHCloudProvider_Name(t *testing.T) {
125-
provider := newTestProvider(t)
163+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
164+
assert.NoError(t, err)
126165

127166
t.Run("check OVHcloud provider name", func(t *testing.T) {
128167
name := provider.Name()
@@ -132,7 +171,8 @@ func TestOVHCloudProvider_Name(t *testing.T) {
132171
}
133172

134173
func TestOVHCloudProvider_NodeGroups(t *testing.T) {
135-
provider := newTestProvider(t)
174+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
175+
assert.NoError(t, err)
136176

137177
t.Run("check default node groups length", func(t *testing.T) {
138178
groups := provider.NodeGroups()
@@ -149,7 +189,8 @@ func TestOVHCloudProvider_NodeGroups(t *testing.T) {
149189
}
150190

151191
func TestOVHCloudProvider_NodeGroupForNode(t *testing.T) {
152-
provider := newTestProvider(t)
192+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
193+
assert.NoError(t, err)
153194

154195
ListNodePoolNodesCall1 := provider.manager.Client.(*sdk.ClientMock).On(
155196
"ListNodePoolNodes",
@@ -317,7 +358,8 @@ func TestOVHCloudProvider_NodeGroupForNode(t *testing.T) {
317358
}
318359

319360
func TestOVHCloudProvider_Pricing(t *testing.T) {
320-
provider := newTestProvider(t)
361+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
362+
assert.NoError(t, err)
321363

322364
t.Run("not implemented", func(t *testing.T) {
323365
_, err := provider.Pricing()
@@ -326,7 +368,8 @@ func TestOVHCloudProvider_Pricing(t *testing.T) {
326368
}
327369

328370
func TestOVHCloudProvider_GetAvailableMachineTypes(t *testing.T) {
329-
provider := newTestProvider(t)
371+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
372+
assert.NoError(t, err)
330373

331374
t.Run("check available machine types", func(t *testing.T) {
332375
flavors, err := provider.GetAvailableMachineTypes()
@@ -337,7 +380,8 @@ func TestOVHCloudProvider_GetAvailableMachineTypes(t *testing.T) {
337380
}
338381

339382
func TestOVHCloudProvider_NewNodeGroup(t *testing.T) {
340-
provider := newTestProvider(t)
383+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
384+
assert.NoError(t, err)
341385

342386
t.Run("check new node group default values", func(t *testing.T) {
343387
group, err := provider.NewNodeGroup("b2-7", nil, nil, nil, nil)
@@ -350,7 +394,8 @@ func TestOVHCloudProvider_NewNodeGroup(t *testing.T) {
350394
}
351395

352396
func TestOVHCloudProvider_GetResourceLimiter(t *testing.T) {
353-
provider := newTestProvider(t)
397+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
398+
assert.NoError(t, err)
354399

355400
t.Run("check default resource limiter values", func(t *testing.T) {
356401
rl, err := provider.GetResourceLimiter()
@@ -370,7 +415,8 @@ func TestOVHCloudProvider_GetResourceLimiter(t *testing.T) {
370415
}
371416

372417
func TestOVHCloudProvider_GPULabel(t *testing.T) {
373-
provider := newTestProvider(t)
418+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
419+
assert.NoError(t, err)
374420

375421
t.Run("check gpu label annotation", func(t *testing.T) {
376422
label := provider.GPULabel()
@@ -380,7 +426,8 @@ func TestOVHCloudProvider_GPULabel(t *testing.T) {
380426
}
381427

382428
func TestOVHCloudProvider_GetAvailableGPUTypes(t *testing.T) {
383-
provider := newTestProvider(t)
429+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
430+
assert.NoError(t, err)
384431

385432
t.Run("check available gpu machine types", func(t *testing.T) {
386433
flavors := provider.GetAvailableGPUTypes()
@@ -391,7 +438,8 @@ func TestOVHCloudProvider_GetAvailableGPUTypes(t *testing.T) {
391438
}
392439

393440
func TestOVHCloudProvider_Cleanup(t *testing.T) {
394-
provider := newTestProvider(t)
441+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
442+
assert.NoError(t, err)
395443

396444
t.Run("check return nil", func(t *testing.T) {
397445
err := provider.Cleanup()
@@ -400,7 +448,8 @@ func TestOVHCloudProvider_Cleanup(t *testing.T) {
400448
}
401449

402450
func TestOVHCloudProvider_Refresh(t *testing.T) {
403-
provider := newTestProvider(t)
451+
provider, err := newTestProvider(t, ovhConsumerConfiguration)
452+
assert.NoError(t, err)
404453

405454
t.Run("check refresh reset node groups correctly", func(t *testing.T) {
406455
provider.manager.NodePoolsPerID = map[string]*sdk.NodePool{}

cluster-autoscaler/cloudprovider/ovhcloud/sdk/openstack.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type OpenStackProvider struct {
3636
tokenExpirationTime time.Time
3737
}
3838

39-
// NewOpenStackProvider initializes a client/token pair to interact with OpenStack
39+
// NewOpenStackProvider initializes a client/token pair to interact with OpenStack from a user/password.
4040
func NewOpenStackProvider(authUrl string, username string, password string, domain string, tenant string) (*OpenStackProvider, error) {
4141
provider, err := openstack.AuthenticatedClient(gophercloud.AuthOptions{
4242
IdentityEndpoint: authUrl,
@@ -58,6 +58,27 @@ func NewOpenStackProvider(authUrl string, username string, password string, doma
5858
}, nil
5959
}
6060

61+
// NewOpenstackApplicationProvider initializes a client/token pair to interact with OpenStack from application credentials.
62+
func NewOpenstackApplicationProvider(authUrl string, applicationCredentialID string, applicationCredentialSecret string, domain string) (*OpenStackProvider, error) {
63+
provider, err := openstack.AuthenticatedClient(gophercloud.AuthOptions{
64+
IdentityEndpoint: authUrl,
65+
ApplicationCredentialID: applicationCredentialID,
66+
ApplicationCredentialSecret: applicationCredentialSecret,
67+
DomainName: domain,
68+
AllowReauth: true,
69+
})
70+
if err != nil {
71+
return nil, fmt.Errorf("failed to create OpenStack authenticated client: %w", err)
72+
}
73+
74+
return &OpenStackProvider{
75+
provider: provider,
76+
AuthUrl: authUrl,
77+
Token: provider.Token(),
78+
tokenExpirationTime: time.Now().Add(DefaultExpirationTime),
79+
}, nil
80+
}
81+
6182
// ReauthenticateToken revoke the current provider token and re-create a new one
6283
func (p *OpenStackProvider) ReauthenticateToken() error {
6384
err := p.provider.Reauthenticate(p.Token)

cluster-autoscaler/cloudprovider/ovhcloud/sdk/ovh.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"encoding/json"
2424
"errors"
2525
"fmt"
26-
"io/ioutil"
26+
"io"
2727
"net/http"
2828
"net/url"
2929
"strconv"
@@ -469,7 +469,7 @@ func (c *Client) CallAPIWithContext(ctx context.Context, method, path string, re
469469
func (c *Client) UnmarshalResponse(response *http.Response, result interface{}) error {
470470
// Read all the response body
471471
defer response.Body.Close()
472-
body, err := ioutil.ReadAll(response.Body)
472+
body, err := io.ReadAll(response.Body)
473473
if err != nil {
474474
return err
475475
}

0 commit comments

Comments
 (0)