Skip to content

Commit

Permalink
add support for azure system assigned identities
Browse files Browse the repository at this point in the history
 - add new field for IdentityType  in AzureMachineSpec
 - add new flavor for VMs with system assigned identity
 - add a role assognment to the system generated identity
  • Loading branch information
nader-ziada committed Apr 30, 2020
1 parent 6a820e0 commit 45cafc9
Show file tree
Hide file tree
Showing 20 changed files with 665 additions and 18 deletions.
3 changes: 3 additions & 0 deletions api/v1alpha2/azuremachine_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { // nolint
if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
return err
}
if restored.Spec.Identity != "" {
dst.Spec.Identity = restored.Spec.Identity
}

return nil
}
Expand Down
3 changes: 3 additions & 0 deletions api/v1alpha2/azuremachinetemplate_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { // nol
if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
return err
}
if restored.Spec.Template.Spec.Identity != "" {
dst.Spec.Template.Spec.Identity = restored.Spec.Template.Spec.Identity
}

return nil
}
Expand Down
1 change: 1 addition & 0 deletions api/v1alpha2/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions api/v1alpha3/azuremachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ type AzureMachineSpec struct {
// +optional
Image *Image `json:"image,omitempty"`

// Identity is the type of identity used for the virtual machine.
// The type 'SystemAssigned' is an implicitly created identity
// The generated identity will be assigned a Subscription contributor role
// +kubebuilder:default=None
// +optional
Identity VMIdentity `json:"identity,omitempty"`

OSDisk OSDisk `json:"osDisk"`

Location string `json:"location"`
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,16 @@ type AvailabilityZone struct {
}

// VMIdentity defines the identity of the virtual machine, if configured.
// +kubebuilder:validation:Enum=None;SystemAssigned
type VMIdentity string

const (
// VMIdentityNone ...
VMIdentityNone VMIdentity = "None"
// VMIdentitySystemAssigned ...
VMIdentitySystemAssigned VMIdentity = "SystemAssigned"
)

type OSDisk struct {
OSType string `json:"osType"`
DiskSizeGB int32 `json:"diskSizeGB"`
Expand Down
64 changes: 64 additions & 0 deletions cloud/services/roleassignments/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package roleassignments

import (
"context"

"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/authorization/mgmt/authorization"
"github.com/Azure/go-autorest/autorest"
azure "sigs.k8s.io/cluster-api-provider-azure/cloud"
)

// Client wraps go-sdk
type Client interface {
Create(context.Context, string, string, authorization.RoleAssignmentCreateParameters) (authorization.RoleAssignment, error)
}

// AzureClient contains the Azure go-sdk Client
type AzureClient struct {
roleassignments authorization.RoleAssignmentsClient
}

var _ Client = &AzureClient{}

// NewClient creates a new role assignment client from subscription ID.
func NewClient(subscriptionID string, authorizer autorest.Authorizer) *AzureClient {
c := newRoleAssignmentClient(subscriptionID, authorizer)
return &AzureClient{c}
}

// newRoleAssignmentClient creates a role assignments client from subscription ID.
func newRoleAssignmentClient(subscriptionID string, authorizer autorest.Authorizer) authorization.RoleAssignmentsClient {
roleClient := authorization.NewRoleAssignmentsClient(subscriptionID)
roleClient.Authorizer = authorizer
roleClient.AddToUserAgent(azure.UserAgent)
return roleClient
}

// Create creates a role assignment.
// Parameters:
// scope - the scope of the role assignment to create. The scope can be any REST resource instance. For
// example, use '/subscriptions/{subscription-id}/' for a subscription,
// '/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}' for a resource group, and
// '/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/{resource-provider}/{resource-type}/{resource-name}'
// for a resource.
// roleAssignmentName - the name of the role assignment to create. It can be any valid GUID.
// parameters - parameters for the role assignment.
func (ac *AzureClient) Create(ctx context.Context, scope string, roleAssignmentName string, parameters authorization.RoleAssignmentCreateParameters) (authorization.RoleAssignment, error) {
return ac.roleassignments.Create(ctx, scope, roleAssignmentName, parameters)
}
20 changes: 20 additions & 0 deletions cloud/services/roleassignments/mock_roleassignments/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Run go generate to regenerate this mock.
//go:generate ../../../../hack/tools/bin/mockgen -destination roleassignments_mock.go -package mock_roleassignments -source ../client.go Client
//go:generate /usr/bin/env bash -c "cat ../../../../hack/boilerplate/boilerplate.generatego.txt roleassignments_mock.go > _roleassignments_mock.go && mv _roleassignments_mock.go roleassignments_mock.go"
package mock_roleassignments //nolint

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 10 additions & 7 deletions cloud/services/virtualmachines/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,27 @@ import (
"sigs.k8s.io/cluster-api-provider-azure/cloud/scope"
"sigs.k8s.io/cluster-api-provider-azure/cloud/services/networkinterfaces"
"sigs.k8s.io/cluster-api-provider-azure/cloud/services/publicips"
"sigs.k8s.io/cluster-api-provider-azure/cloud/services/roleassignments"
)

// Service provides operations on azure resources
type Service struct {
Scope *scope.ClusterScope
MachineScope *scope.MachineScope
Client
InterfacesClient networkinterfaces.Client
PublicIPsClient publicips.Client
InterfacesClient networkinterfaces.Client
PublicIPsClient publicips.Client
RoleAssignmentsClient roleassignments.Client
}

// NewService creates a new service.
func NewService(scope *scope.ClusterScope, machineScope *scope.MachineScope) *Service {
return &Service{
Scope: scope,
MachineScope: machineScope,
Client: NewClient(scope.SubscriptionID, scope.Authorizer),
InterfacesClient: networkinterfaces.NewClient(scope.SubscriptionID, scope.Authorizer),
PublicIPsClient: publicips.NewClient(scope.SubscriptionID, scope.Authorizer),
Scope: scope,
MachineScope: machineScope,
Client: NewClient(scope.SubscriptionID, scope.Authorizer),
InterfacesClient: networkinterfaces.NewClient(scope.SubscriptionID, scope.Authorizer),
PublicIPsClient: publicips.NewClient(scope.SubscriptionID, scope.Authorizer),
RoleAssignmentsClient: roleassignments.NewClient(scope.SubscriptionID, scope.Authorizer),
}
}
44 changes: 43 additions & 1 deletion cloud/services/virtualmachines/virtualmachines.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ import (
"fmt"
"strings"

"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
"github.com/Azure/go-autorest/autorest/to"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/klog"
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha3"
azure "sigs.k8s.io/cluster-api-provider-azure/cloud"
"sigs.k8s.io/cluster-api-provider-azure/cloud/converters"
)

const azureBuiltInContributorID = "b24988ac-6180-42a0-ab88-20f7382dd24c"

// Spec input specification for Get/CreateOrUpdate/Delete calls
type Spec struct {
Name string
Expand All @@ -41,6 +45,7 @@ type Spec struct {
Size string
Zone string
Image *infrav1.Image
Identity infrav1.VMIdentity
OSDisk infrav1.OSDisk
CustomData string
}
Expand Down Expand Up @@ -149,19 +154,56 @@ func (s *Service) Reconcile(ctx context.Context, spec interface{}) error {
virtualMachine.Zones = &zones
}

if vmSpec.Identity == infrav1.VMIdentitySystemAssigned {
virtualMachine.Identity = &compute.VirtualMachineIdentity{
Type: compute.ResourceIdentityTypeSystemAssigned,
}
}

err = s.Client.CreateOrUpdate(
ctx,
s.Scope.ResourceGroup(),
vmSpec.Name,
virtualMachine)
if err != nil {
return errors.Wrapf(err, "cannot create vm")
return errors.Wrapf(err, "cannot create VM")
}

if vmSpec.Identity == infrav1.VMIdentitySystemAssigned {
err = s.createRoleAssignmentForIdentity(ctx, vmSpec.Name)
if err != nil {
return errors.Wrapf(err, "cannot create VM")
}
}

klog.V(2).Infof("successfully created VM %s ", vmSpec.Name)
return nil
}

func (s *Service) createRoleAssignmentForIdentity(ctx context.Context, vmName string) error {
resultVM, err := s.Client.Get(ctx, s.Scope.ResourceGroup(), vmName)
if err != nil {
return errors.Wrapf(err, "cannot get VM to assign role to system assigned identity")
}

scope := fmt.Sprintf("/subscriptions/%s/", s.Scope.SubscriptionID)
// Azure built-in roles https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
contributorRoleDefinitionID := fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s", s.Scope.SubscriptionID, azureBuiltInContributorID)
params := authorization.RoleAssignmentCreateParameters{
Properties: &authorization.RoleAssignmentProperties{
RoleDefinitionID: to.StringPtr(contributorRoleDefinitionID),
PrincipalID: to.StringPtr(*resultVM.Identity.PrincipalID),
},
}
_, err = s.RoleAssignmentsClient.Create(ctx, scope, string(uuid.NewUUID()), params)
if err != nil {
return errors.Wrapf(err, "cannot assign role to VM system assigned identity")
}

klog.V(2).Infof("successfully created Role assignment for generated Identity for VM %s ", vmName)
return nil
}

// Delete deletes the virtual machine with the provided name.
func (s *Service) Delete(ctx context.Context, spec interface{}) error {
vmSpec, ok := spec.(*Spec)
Expand Down
Loading

0 comments on commit 45cafc9

Please sign in to comment.