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 azure system assigned identities #565

Merged
merged 1 commit into from
Apr 30, 2020
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
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
CecileRobertMichon marked this conversation as resolved.
Show resolved Hide resolved
// 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
CecileRobertMichon marked this conversation as resolved.
Show resolved Hide resolved

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