diff --git a/charts/region/Chart.yaml b/charts/region/Chart.yaml index a116f9f..4afe2fe 100644 --- a/charts/region/Chart.yaml +++ b/charts/region/Chart.yaml @@ -4,8 +4,8 @@ description: A Helm chart for deploying Unikorn's Region Controller type: application -version: v0.1.31 -appVersion: v0.1.31 +version: v0.1.32 +appVersion: v0.1.32 icon: https://raw.githubusercontent.com/unikorn-cloud/assets/main/images/logos/dark-on-light/icon.png diff --git a/pkg/providers/openstack/README.md b/pkg/providers/openstack/README.md index 2ed7753..290df57 100644 --- a/pkg/providers/openstack/README.md +++ b/pkg/providers/openstack/README.md @@ -17,10 +17,11 @@ Start by selecting a unique name that will be used for the deployment's name, pr ```bash export USER=unikorn-staging export DOMAIN=unikorn-staging +export PROJECT=unikorn-default export PASSWORD=$(apg -n 1 -m 24) ``` -Create the domain. +#### Create the domain. The use of project domains for projects deployed to provision Kubernetes cluster achieves a few aims. First namespace isolation. Second is a security consideration. @@ -32,21 +33,34 @@ A domain may also aid in simplifying operations like auditing and capacity plann DOMAIN_ID=$(openstack domain create ${DOMAIN} -f json | jq -r .id) ``` -Crete the user. +#### Create the project. +As the OpenStack provider for the region controller also functions as a client in order to retrieve information such as available images, flavors, and so on it also needs to be associated with a project so that the default policy for various API requests is correctly satisfied: + +```bash +PROJECT_ID=$(openstack project create $PROJECT --domain $DOMAIN -f json | jq -r .id) +``` + +#### Create the user. ```bash USER_ID=$(openstack user create --domain ${DOMAIN_ID} --password ${PASSWORD} ${USER} -f json | jq -r .id) ``` -Grant any roles to the user. +### Grant any roles to the user. When a Kubernetes cluster is provisioned, it will be done using application credentials, so ensure any required application credentials as configured for the region are explicitly associated with the user here. ```bash -for role in _member_ member load-balancer_member manager; do +for role in member load-balancer_member manager; do openstack role add --user ${USER_ID} --domain ${DOMAIN_ID} ${role} done ``` +And also grant the `member` role on the project we created in a previous step: + +```bash +openstack role add --user ${USER_ID} --project ${PROJECT_ID} member +``` + ### Unikorn Configuration When we create a `Region` of type `openstack`, it will require a secret that contains credentials. @@ -54,9 +68,10 @@ This can be configured as follows. ```bash kubectl create secret generic -n unikorn-region gb-north-1-credentials \ - --from-literal=domain-id=${DOMAIN_ID} \ - --from-literal=user-id=${USER_ID} \ - --from-literal=password=${PASSWORD} + --from-literal=domain-id=${DOMAIN_ID} \ + --from-literal=project-id=${PROJECT_ID} \ + --from-literal=user-id=${USER_ID} \ + --from-literal=password=${PASSWORD} ``` Finally we can create the region itself, although this should be statically configured via Helm. @@ -83,9 +98,11 @@ spec: Cleanup actions. ```bash +unset DOMAIN unset DOMAIN_ID +unset USER unset USER_ID unset PASSWORD -unset DOMAIN -unset USER +unset PROJECT +unset PROJECT_ID ``` diff --git a/pkg/providers/openstack/client.go b/pkg/providers/openstack/client.go index 625ba60..c564e9f 100644 --- a/pkg/providers/openstack/client.go +++ b/pkg/providers/openstack/client.go @@ -103,8 +103,10 @@ func (p *PasswordProvider) Client(ctx context.Context) (*gophercloud.ProviderCli IdentityEndpoint: p.endpoint, UserID: p.userID, Password: p.password, - TenantID: p.projectID, AllowReauth: true, + Scope: &gophercloud.AuthScope{ + ProjectID: p.projectID, + }, } return authenticatedClient(ctx, options) @@ -120,8 +122,9 @@ type DomainScopedPasswordProvider struct { // Ensure the interface is implemented. var _ CredentialProvider = &DomainScopedPasswordProvider{} +var _ CredentialProvider = &PasswordProvider{} -// NewDomainScopedPasswordProvider creates a client that comsumes passwords +// NewDomainScopedPasswordProvider creates a client that consumes passwords // for authentication. func NewDomainScopedPasswordProvider(endpoint, userID, password, domainID string) *DomainScopedPasswordProvider { return &DomainScopedPasswordProvider{ diff --git a/pkg/providers/openstack/provider.go b/pkg/providers/openstack/provider.go index 512e9c9..671b0c8 100644 --- a/pkg/providers/openstack/provider.go +++ b/pkg/providers/openstack/provider.go @@ -61,9 +61,10 @@ type Provider struct { // secret is the current region secret. secret *corev1.Secret - domainID string - userID string - password string + domainID string + projectID string + userID string + password string // DO NOT USE DIRECTLY, CALL AN ACCESSOR. _identity *IdentityClient @@ -141,11 +142,16 @@ func (p *Provider) serviceClientRefresh(ctx context.Context) error { return fmt.Errorf("%w: password", ErrKeyUndefined) } - // Pass in an empty string to use the default project. - providerClient := NewDomainScopedPasswordProvider(region.Spec.Openstack.Endpoint, string(userID), string(password), string(domainID)) + projectID, ok := secret.Data["project-id"] + if !ok { + return fmt.Errorf("%w: project-id", ErrKeyUndefined) + } - // Create the clients. - identity, err := NewIdentityClient(ctx, providerClient) + // 'Regular' client calls to APIs for Nova, Glance etc. must to be project-scoped + providerClient := NewPasswordProvider(region.Spec.Openstack.Endpoint, string(userID), string(password), string(projectID)) + + // Identity client is scoped to a domain to use the manager role + identity, err := NewIdentityClient(ctx, NewDomainScopedPasswordProvider(region.Spec.Openstack.Endpoint, string(userID), string(password), string(domainID))) if err != nil { return err } @@ -170,6 +176,8 @@ func (p *Provider) serviceClientRefresh(ctx context.Context) error { p.secret = secret p.domainID = string(domainID) + p.projectID = string(projectID) + p.userID = string(userID) p.password = string(password)