Skip to content

Commit

Permalink
Rejig Neutron VLAN Provisioning
Browse files Browse the repository at this point in the history
So it transpires we were trying to piggy back on the stellar work by SCS
for identity and allow a domain admin for provider networks, but alas
Neutron has zero visibility of domains, and secondly only "admin" and
"advsvc" can provision in a different project (hard coded, not a
policy).  Out one remaining option is to create a context that is for
the "manager" user, but scoped to the user's project, and that can allow
the provider network to be provisioned.
  • Loading branch information
spjmurray committed Aug 7, 2024
1 parent b4b82f1 commit d8a9a31
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 28 deletions.
4 changes: 2 additions & 2 deletions charts/region/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ description: A Helm chart for deploying Unikorn's Region Controller

type: application

version: v0.1.32
appVersion: v0.1.32
version: v0.1.33
appVersion: v0.1.33

icon: https://raw.githubusercontent.com/unikorn-cloud/assets/main/images/logos/dark-on-light/icon.png

Expand Down
28 changes: 24 additions & 4 deletions pkg/providers/openstack/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,20 @@ var (

// NetworkClient wraps the generic client because gophercloud is unsafe.
type NetworkClient struct {
// client is a network client scoped as per the provider given
// during initialization.
client *gophercloud.ServiceClient

// options are optional configuration about the network service.
options *unikornv1.RegionOpenstackNetworkSpec

// credentials gives access to the region's login information.
credentials *providerCredentials
// externalNetworkCache provides caching to avoid having to talk to
// OpenStack.
externalNetworkCache *cache.TimeoutCache[[]networks.Network]
}

// NewNetworkClient provides a simple one-liner to start networking.
func NewNetworkClient(ctx context.Context, provider CredentialProvider, options *unikornv1.RegionOpenstackNetworkSpec) (*NetworkClient, error) {
func NewNetworkClient(ctx context.Context, provider CredentialProvider, credentials *providerCredentials, options *unikornv1.RegionOpenstackNetworkSpec) (*NetworkClient, error) {
providerClient, err := provider.Client(ctx)
if err != nil {
return nil, err
Expand All @@ -69,6 +74,7 @@ func NewNetworkClient(ctx context.Context, provider CredentialProvider, options
c := &NetworkClient{
client: client,
options: options,
credentials: credentials,
externalNetworkCache: cache.New[[]networks.Network](time.Hour),
}

Expand Down Expand Up @@ -176,6 +182,8 @@ func (c *NetworkClient) AllocateVLAN(ctx context.Context) (int, error) {
}

// CreateVLANProviderNetwork creates a VLAN provider network for a project.
// This requires https://github.com/unikorn-cloud/python-unikorn-openstack-policy
// to be installed, see the README for further details on how this has to work.
func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name string, projectID string) (int, *networks.Network, error) {
if c.options == nil || c.options.ProviderNetworks == nil || c.options.ProviderNetworks.PhysicalNetwork == nil {
return -1, nil, ErrConfiguration
Expand Down Expand Up @@ -206,7 +214,19 @@ func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name stri
},
}

network, err := networks.Create(ctx, c.client, opts).Extract()
// Create a project scoped client that has the "manager" role as defined
// by the receiver comment, and can actually securely create provider networks.
providerClient, err := NewPasswordProvider(c.credentials.endpoint, c.credentials.userID, c.credentials.password, projectID).Client(ctx)
if err != nil {
return -1, nil, err
}

client, err := openstack.NewNetworkV2(providerClient, gophercloud.EndpointOpts{})
if err != nil {
return -1, nil, err
}

network, err := networks.Create(ctx, client, opts).Extract()
if err != nil {
return -1, nil, err
}
Expand Down
73 changes: 51 additions & 22 deletions pkg/providers/openstack/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ var (
ErrKeyUndefined = errors.New("a required key was not defined")
)

type providerCredentials struct {
endpoint string
domainID string
projectID string
userID string
password string
}

type Provider struct {
// client is Kubernetes client.
client client.Client
Expand All @@ -61,10 +69,8 @@ type Provider struct {
// secret is the current region secret.
secret *corev1.Secret

domainID string
projectID string
userID string
password string
// credentials hold cloud identity information.
credentials *providerCredentials

// DO NOT USE DIRECTLY, CALL AN ACCESSOR.
_identity *IdentityClient
Expand Down Expand Up @@ -147,15 +153,25 @@ func (p *Provider) serviceClientRefresh(ctx context.Context) error {
return fmt.Errorf("%w: project-id", ErrKeyUndefined)
}

// '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))
credentials := &providerCredentials{
endpoint: region.Spec.Openstack.Endpoint,
domainID: string(domainID),
projectID: string(projectID),
userID: string(userID),
password: string(password),
}

// Identity client is scoped to a domain to use the manager role
// The identity client needs to have "manager" powers, so it create projects and
// users within a domain without full admin.
identity, err := NewIdentityClient(ctx, NewDomainScopedPasswordProvider(region.Spec.Openstack.Endpoint, string(userID), string(password), string(domainID)))
if err != nil {
return err
}

// Everything else gets a default view when bound to a project as a "member".
// Sadly, domain scoped accesses do not work by default any longer.
providerClient := NewPasswordProvider(region.Spec.Openstack.Endpoint, string(userID), string(password), string(projectID))

compute, err := NewComputeClient(ctx, providerClient, region.Spec.Openstack.Compute)
if err != nil {
return err
Expand All @@ -166,20 +182,15 @@ func (p *Provider) serviceClientRefresh(ctx context.Context) error {
return err
}

network, err := NewNetworkClient(ctx, providerClient, region.Spec.Openstack.Network)
network, err := NewNetworkClient(ctx, providerClient, credentials, region.Spec.Openstack.Network)
if err != nil {
return err
}

// Save the current configuration for checking next time.
p.region = region
p.secret = secret

p.domainID = string(domainID)
p.projectID = string(projectID)

p.userID = string(userID)
p.password = string(password)
p.credentials = credentials

// Seve the clients
p._identity = identity
Expand Down Expand Up @@ -374,7 +385,7 @@ func projectTags(organizationID, projectID string) []string {
func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityClient, project *projects.Project) (*users.User, string, error) {
password := string(uuid.NewUUID())

user, err := identityService.CreateUser(ctx, p.domainID, project.Name, password)
user, err := identityService.CreateUser(ctx, p.credentials.domainID, project.Name, password)
if err != nil {
return nil, "", err
}
Expand All @@ -388,7 +399,7 @@ func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityC
func (p *Provider) provisionProject(ctx context.Context, identityService *IdentityClient, organizationID, projectID string) (*projects.Project, error) {
name := "unikorn-" + rand.String(8)

project, err := identityService.CreateProject(ctx, p.domainID, name, projectTags(organizationID, projectID))
project, err := identityService.CreateProject(ctx, p.credentials.domainID, name, projectTags(organizationID, projectID))
if err != nil {
return nil, err
}
Expand All @@ -408,9 +419,19 @@ func roleNameToID(roles []roles.Role, name string) (string, error) {
return "", fmt.Errorf("%w: role %s", ErrResourceNotFound, name)
}

// getRequiredRoles returns the roles required for a user to create, manage and delete
// getRequiredProjectManagerRoles returns the roles required for a manager to create, manager
// and delete things like provider networks to support baremetal.
func (p *Provider) getRequiredProjectManagerRoles() []string {
defaultRoles := []string{
"member",
}

return defaultRoles
}

// getRequiredProjectUserRoles returns the roles required for a user to create, manage and delete
// a cluster.
func (p *Provider) getRequiredRoles() []string {
func (p *Provider) getRequiredProjectUserRoles() []string {
if p.region.Spec.Openstack.Identity != nil && len(p.region.Spec.Openstack.Identity.ClusterRoles) > 0 {
return p.region.Spec.Openstack.Identity.ClusterRoles
}
Expand All @@ -426,13 +447,13 @@ func (p *Provider) getRequiredRoles() []string {
// provisionProjectRoles creates a binding between our service account and the project
// with the required roles to provision an application credential that will allow cluster
// creation, deletion and life-cycle management.
func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *IdentityClient, userID string, project *projects.Project) error {
func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *IdentityClient, userID string, project *projects.Project, rolesGetter func() []string) error {
allRoles, err := identityService.ListRoles(ctx)
if err != nil {
return err
}

for _, name := range p.getRequiredRoles() {
for _, name := range rolesGetter() {
roleID, err := roleNameToID(allRoles, name)
if err != nil {
return err
Expand All @@ -457,7 +478,7 @@ func (p *Provider) provisionApplicationCredential(ctx context.Context, userID, p

// Application crdentials are scoped to the user, not the project, so the name needs
// to be unique, so just use the project name.
return identityService.CreateApplicationCredential(ctx, userID, project.Name, "IaaS lifecycle management", p.getRequiredRoles())
return identityService.CreateApplicationCredential(ctx, userID, project.Name, "IaaS lifecycle management", p.getRequiredProjectUserRoles())
}

func (p *Provider) createClientConfig(applicationCredential *applicationcredentials.ApplicationCredential) ([]byte, string, error) {
Expand Down Expand Up @@ -542,6 +563,14 @@ func (p *Provider) CreateIdentity(ctx context.Context, organizationID, projectID
return nil, err
}

// Grant the "manager" role on the project for unikorn's user. Sadly when provisioning
// resources, most services can only infer the project ID from the token, and not any
// of the heirarchy, so we cannot define policy rules for a domain manager in the same
// way as can be done for the identity service.
if err := p.provisionProjectRoles(ctx, identityService, p.credentials.userID, project, p.getRequiredProjectManagerRoles); err != nil {
return nil, err
}

// You MUST provision a new user, if we rotate a password, any application credentials
// hanging off it will stop working, i.e. doing that to the unikorn management user
// will be pretty catastrophic for all clusters in the region.
Expand All @@ -552,7 +581,7 @@ func (p *Provider) CreateIdentity(ctx context.Context, organizationID, projectID

// Give the user only what permissions they need to provision a cluster and
// manage it during its lifetime.
if err := p.provisionProjectRoles(ctx, identityService, user.ID, project); err != nil {
if err := p.provisionProjectRoles(ctx, identityService, user.ID, project, p.getRequiredProjectUserRoles); err != nil {
return nil, err
}

Expand Down

0 comments on commit d8a9a31

Please sign in to comment.