Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for application credentials #300

Merged
merged 2 commits into from
Jul 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
############# builder
FROM golang:1.16.3 AS builder
FROM golang:1.16.5 AS builder

WORKDIR /go/src/github.com/gardener/gardener-extension-provider-openstack
COPY . .
RUN make install

############# base
FROM alpine:3.13.4 AS base
FROM alpine:3.13.5 AS base

############# gardener-extension-provider-openstack
FROM base AS gardener-extension-provider-openstack
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ auth-url="{{ .Values.authUrl }}"
domain-name="{{ .Values.domainName }}"
tenant-name="{{ .Values.tenantName }}"
username="{{ .Values.username }}"
{{- if .Values.password }}
password="{{ .Values.password }}"
{{- end }}
{{- if .Values.applicationCredentialSecret }}
application-credential-id="{{ .Values.applicationCredentialID }}"
application-credential-name="{{ .Values.applicationCredentialName }}"
application-credential-secret="{{ .Values.applicationCredentialSecret }}"
{{- end }}
region="{{ .Values.region }}"
{{- end -}}
3 changes: 3 additions & 0 deletions charts/internal/cloud-provider-config/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ domainName: fooDomain
tenantName: fooTenant
username: barUser
password: barPass
# applicationCredentialID: barID
# applicationCredentialName: barName
# applicationCredentialSecret: barSecret
region: eu
# [LoadBalancer]
lbProvider: foobar
Expand Down
12 changes: 12 additions & 0 deletions docs/usage-as-end-user.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,24 @@ type: Opaque
data:
domainName: base64(domain-name)
tenantName: base64(tenant-name)

# either use username/password
username: base64(user-name)
password: base64(password)

# or application credentials
#applicationCredentialID: base64(app-credential-id)
#applicationCredentialName: base64(app-credential-name) # optional
#applicationCredentialSecret: base64(app-credential-secret)
```

Please look up https://docs.openstack.org/keystone/pike/admin/identity-concepts.html as well.

For authentication with username/password see [Keystone username/password](https://docs.openstack.org/keystone/latest/user/supported_clients.html)

Alternatively, for authentication with application credentials see [Keystone Application Credentials](https://docs.openstack.org/keystone/latest/user/application_credentials.html)


⚠️ Depending on your API usage it can be problematic to reuse the same provider credentials for different Shoot clusters due to rate limits.
Please consider spreading your Shoots over multiple credentials from different tenants if you are hitting those limits.

Expand Down
9 changes: 6 additions & 3 deletions pkg/apis/openstack/validation/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ func ValidateCloudProviderSecret(secret *corev1.Secret) error {

// domainName, tenantName, and userName must not contain leading or trailing whitespace
for key, value := range map[string]string{
openstack.DomainName: credentials.DomainName,
openstack.TenantName: credentials.TenantName,
openstack.UserName: credentials.Username,
openstack.DomainName: credentials.DomainName,
openstack.TenantName: credentials.TenantName,
openstack.UserName: credentials.Username,
openstack.ApplicationCredentialID: credentials.ApplicationCredentialID,
openstack.ApplicationCredentialName: credentials.ApplicationCredentialName,
openstack.ApplicationCredentialSecret: credentials.ApplicationCredentialSecret,
} {
if strings.TrimSpace(value) != value {
dkistner marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("field %q in secret %s must not contain leading or traling whitespace", key, secretKey)
Expand Down
104 changes: 104 additions & 0 deletions pkg/apis/openstack/validation/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,109 @@ var _ = Describe("Secret validation", func() {
},
BeNil(),
),

Entry("should return error when the application credential id contains a trailing new line",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.ApplicationCredentialID: []byte("app-id\n"),
openstack.ApplicationCredentialSecret: []byte("app-secret"),
},
HaveOccurred(),
),

Entry("should return error when the application credential secret contains a trailing new line",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.ApplicationCredentialID: []byte("app-id"),
openstack.ApplicationCredentialSecret: []byte("app-secret\n"),
},
HaveOccurred(),
),

Entry("should return error when the application credential name contains a trailing new line",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.UserName: []byte("user"),
openstack.ApplicationCredentialID: []byte("app-id"),
openstack.ApplicationCredentialName: []byte("app-name\n"),
openstack.ApplicationCredentialSecret: []byte("app-secret"),
},
HaveOccurred(),
),

Entry("should return error when neither username nor application credential id is given",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
},
HaveOccurred(),
),

Entry("should return error when both password and application credential secret is given",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.Password: []byte("password"),
openstack.ApplicationCredentialSecret: []byte("app-secret"),
},
HaveOccurred(),
),

Entry("should return error when application credential secret is missing",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.ApplicationCredentialID: []byte("app-id"),
},
HaveOccurred(),
),

Entry("should return error when application credential name is given, but without user name",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.ApplicationCredentialName: []byte("app-name"),
openstack.ApplicationCredentialSecret: []byte("app-secret"),
},
HaveOccurred(),
),

Entry("should succeed when the client application credentials are valid (id + secret)",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.ApplicationCredentialID: []byte("app-id"),
openstack.ApplicationCredentialSecret: []byte("app-secret"),
openstack.AuthURL: []byte("https://foo.bar"),
},
BeNil(),
),

Entry("should succeed when the client application credentials are valid (id + name + secret)",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.ApplicationCredentialID: []byte("app-id"),
openstack.ApplicationCredentialName: []byte("app-name"),
openstack.ApplicationCredentialSecret: []byte("app-secret"),
openstack.AuthURL: []byte("https://foo.bar"),
},
BeNil(),
),

Entry("should succeed when the client application credentials are valid (username + name + secret)",
map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.UserName: []byte("user"),
openstack.ApplicationCredentialName: []byte("app-name"),
openstack.ApplicationCredentialSecret: []byte("app-secret"),
openstack.AuthURL: []byte("https://foo.bar"),
},
BeNil(),
),
)
})
33 changes: 18 additions & 15 deletions pkg/controller/controlplane/valuesprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,21 +482,24 @@ func getConfigChartValues(
}

values := map[string]interface{}{
"kubernetesVersion": cluster.Shoot.Spec.Kubernetes.Version,
"domainName": c.DomainName,
"tenantName": c.TenantName,
"username": c.Username,
"password": c.Password,
"region": cp.Spec.Region,
"lbProvider": cpConfig.LoadBalancerProvider,
"floatingNetworkID": infraStatus.Networks.FloatingPool.ID,
"subnetID": subnet.ID,
"authUrl": keyStoneURL,
"dhcpDomain": cloudProfileConfig.DHCPDomain,
"requestTimeout": cloudProfileConfig.RequestTimeout,
"useOctavia": cloudProfileConfig.UseOctavia != nil && *cloudProfileConfig.UseOctavia,
"rescanBlockStorageOnResize": cloudProfileConfig.RescanBlockStorageOnResize != nil && *cloudProfileConfig.RescanBlockStorageOnResize,
"nodeVolumeAttachLimit": cloudProfileConfig.NodeVolumeAttachLimit,
"kubernetesVersion": cluster.Shoot.Spec.Kubernetes.Version,
"domainName": c.DomainName,
"tenantName": c.TenantName,
"username": c.Username,
"password": c.Password,
"applicationCredentialID": c.ApplicationCredentialID,
"applicationCredentialName": c.ApplicationCredentialName,
"applicationCredentialSecret": c.ApplicationCredentialSecret,
"region": cp.Spec.Region,
"lbProvider": cpConfig.LoadBalancerProvider,
"floatingNetworkID": infraStatus.Networks.FloatingPool.ID,
"subnetID": subnet.ID,
"authUrl": keyStoneURL,
"dhcpDomain": cloudProfileConfig.DHCPDomain,
"requestTimeout": cloudProfileConfig.RequestTimeout,
"useOctavia": cloudProfileConfig.UseOctavia != nil && *cloudProfileConfig.UseOctavia,
"rescanBlockStorageOnResize": cloudProfileConfig.RescanBlockStorageOnResize != nil && *cloudProfileConfig.RescanBlockStorageOnResize,
"nodeVolumeAttachLimit": cloudProfileConfig.NodeVolumeAttachLimit,
}

var loadBalancerClassesFromCloudProfile = []api.LoadBalancerClass{}
Expand Down
56 changes: 41 additions & 15 deletions pkg/controller/controlplane/valuesprovider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,21 +256,24 @@ var _ = Describe("ValuesProvider", func() {

Describe("#GetConfigChartValues", func() {
configChartValues := map[string]interface{}{
"kubernetesVersion": "1.13.4",
"domainName": "domain-name",
"tenantName": "tenant-name",
"username": "username",
"password": "password",
"region": region,
"subnetID": "subnet-acbd1234",
"lbProvider": "load-balancer-provider",
"floatingNetworkID": "floating-network-id",
"authUrl": authURL,
"dhcpDomain": dhcpDomain,
"requestTimeout": requestTimeout,
"useOctavia": useOctavia,
"rescanBlockStorageOnResize": rescanBlockStorageOnResize,
"nodeVolumeAttachLimit": pointer.Int32Ptr(nodeVoluemAttachLimit),
"kubernetesVersion": "1.13.4",
"domainName": "domain-name",
"tenantName": "tenant-name",
"username": "username",
"password": "password",
"region": region,
"subnetID": "subnet-acbd1234",
"lbProvider": "load-balancer-provider",
"floatingNetworkID": "floating-network-id",
"authUrl": authURL,
"dhcpDomain": dhcpDomain,
"requestTimeout": requestTimeout,
"useOctavia": useOctavia,
"rescanBlockStorageOnResize": rescanBlockStorageOnResize,
"nodeVolumeAttachLimit": pointer.Int32Ptr(nodeVoluemAttachLimit),
"applicationCredentialID": "",
"applicationCredentialSecret": "",
"applicationCredentialName": "",
}

It("should return correct config chart values", func() {
Expand Down Expand Up @@ -420,6 +423,29 @@ var _ = Describe("ValuesProvider", func() {
Expect(err).NotTo(HaveOccurred())
Expect(values).To(Equal(expectedValues))
})

It("should return correct config chart values with application credentials", func() {
secret2 := *cpSecret
secret2.Data = map[string][]byte{
"domainName": []byte(domainName),
"tenantName": []byte(tenantName),
"applicationCredentialID": []byte(`app-id`),
"applicationCredentialSecret": []byte(`app-secret`),
}

c.EXPECT().Get(ctx, cpSecretKey, &corev1.Secret{}).DoAndReturn(clientGet(&secret2))

expectedValues := utils.MergeMaps(configChartValues, map[string]interface{}{
"username": "",
"password": "",
"applicationCredentialID": "app-id",
"applicationCredentialSecret": "app-secret",
})
values, err := vp.GetConfigChartValues(ctx, cp, clusterK8sLessThan119)
Expect(err).NotTo(HaveOccurred())
Expect(values).To(Equal(expectedValues))
})

})

Describe("#GetControlPlaneChartValues", func() {
Expand Down
9 changes: 8 additions & 1 deletion pkg/controller/infrastructure/actuator_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/gardener/gardener-extension-provider-openstack/pkg/internal"
"github.com/gardener/gardener-extension-provider-openstack/pkg/internal/infrastructure"
"github.com/gardener/gardener-extension-provider-openstack/pkg/openstack"
"sigs.k8s.io/controller-runtime/pkg/client"

extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller"
Expand All @@ -46,7 +47,13 @@ func (a *actuator) Delete(ctx context.Context, infra *extensionsv1alpha1.Infrast
return tf.CleanupConfiguration(ctx)
}

// need to known if application credentials are used
credentials, err := openstack.GetCredentials(ctx, a.Client(), infra.Spec.SecretRef)
if err != nil {
return err
}

return tf.
SetEnvVars(internal.TerraformerEnvVars(infra.Spec.SecretRef)...).
SetEnvVars(internal.TerraformerEnvVars(infra.Spec.SecretRef, credentials)...).
Destroy(ctx)
}
9 changes: 8 additions & 1 deletion pkg/controller/infrastructure/actuator_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/gardener/gardener-extension-provider-openstack/pkg/apis/openstack/helper"
"github.com/gardener/gardener-extension-provider-openstack/pkg/internal"
"github.com/gardener/gardener-extension-provider-openstack/pkg/internal/infrastructure"
"github.com/gardener/gardener-extension-provider-openstack/pkg/openstack"
"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand All @@ -45,7 +46,13 @@ func (a *actuator) reconcile(ctx context.Context, logger logr.Logger, infra *ext
return err
}

tf, err := internal.NewTerraformerWithAuth(logger, a.RESTConfig(), infrastructure.TerraformerPurpose, infra)
// need to known if application credentials are used
credentials, err := openstack.GetCredentials(ctx, a.Client(), infra.Spec.SecretRef)
if err != nil {
return err
}

tf, err := internal.NewTerraformerWithAuth(logger, a.RESTConfig(), infrastructure.TerraformerPurpose, infra, credentials)
if err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/internal/infrastructure/templates/main.tpl.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ provider "openstack" {
region = "{{ .openstack.region }}"
user_name = var.USER_NAME
password = var.PASSWORD
application_credential_id = var.APPLICATION_CREDENTIAL_ID
application_credential_name = var.APPLICATION_CREDENTIAL_NAME
application_credential_secret = var.APPLICATION_CREDENTIAL_SECRET
insecure = true
max_retries = "{{ .openstack.maxApiCallRetries }}"
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/internal/infrastructure/templates/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,29 @@ variable "TENANT_NAME" {
variable "USER_NAME" {
description = "OpenStack user name"
type = string
default = "" # not needed if application credentials are used with APPLICATION_CREDENTIAL_ID
}

variable "PASSWORD" {
description = "OpenStack password"
type = string
default = "" # not needed if application credentials are used
}

variable "APPLICATION_CREDENTIAL_ID" {
description = "OpenStack application credential id"
type = string
default = "" # not needed if username/password are used
}

variable "APPLICATION_CREDENTIAL_NAME" {
description = "OpenStack application credential name"
type = string
default = "" # not needed if username/password are used or APPLICATION_CREDENTIAL_ID is given
}

variable "APPLICATION_CREDENTIAL_SECRET" {
description = "OpenStack application credential secret"
type = string
default = "" # not needed if username/password are used
}
Loading