Skip to content

Commit

Permalink
using aad-pod-identity for multi-tenancy
Browse files Browse the repository at this point in the history
 - support SP identity only
 - add flavor + topic doc
 - add identity reconciler
  • Loading branch information
nader-ziada committed Dec 13, 2020
1 parent 700eeee commit 97f1988
Show file tree
Hide file tree
Showing 42 changed files with 1,543 additions and 15 deletions.
1 change: 1 addition & 0 deletions api/v1alpha2/azurecluster_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint

dst.Status.FailureDomains = restored.Status.FailureDomains
dst.Spec.NetworkSpec.Vnet.CIDRBlocks = restored.Spec.NetworkSpec.Vnet.CIDRBlocks
dst.Spec.IdentityName = restored.Spec.IdentityName

for _, restoredSubnet := range restored.Spec.NetworkSpec.Subnets {
if restoredSubnet != 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/azurecluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const (
// ClusterFinalizer allows ReconcileAzureCluster to clean up Azure resources associated with AzureCluster before
// removing it from the apiserver.
ClusterFinalizer = "azurecluster.infrastructure.cluster.x-k8s.io"

// ClusterLabelNamespace indicates the namespace of the cluster
ClusterLabelNamespace = "azurecluster.infrastructure.cluster.x-k8s.io/cluster-namespace"
)

// AzureClusterSpec defines the desired state of AzureCluster
Expand All @@ -48,6 +51,10 @@ type AzureClusterSpec struct {
// ones added by default.
// +optional
AdditionalTags Tags `json:"additionalTags,omitempty"`

// IdentityName is a reference to a AzureIdentity to be used when reconciling this cluster
// +optional
IdentityName *string `json:"identityName,omitempty"`
}

// AzureClusterStatus defines the observed state of AzureCluster
Expand Down
9 changes: 9 additions & 0 deletions api/v1alpha3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
)

const (
// ControllerNamespace is the namespace where controller manager will run
ControllerNamespace = "capz-system"
// ControlPlane machine label
ControlPlane string = "control-plane"
// Node machine label
Expand Down Expand Up @@ -321,6 +323,13 @@ type UserAssignedIdentity struct {
ProviderID string `json:"providerID"`
}

const (
// AzureIdentityBindingSelector is the label used to match with the AzureIdentityBinding
// For the controller to match an identity binding, it needs a [label] with the key `aadpodidbinding`
// whose value is that of the `selector:` field in the `AzureIdentityBinding`.
AzureIdentityBindingSelector = "capz-controller-aadpodidentity-selector"
)

// OSDisk defines the operating system disk for a VM.
type OSDisk struct {
OSType string `json:"osType"`
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha3/zz_generated.deepcopy.go

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

27 changes: 27 additions & 0 deletions cloud/scope/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package scope

import (
"context"
"fmt"
"strings"

Expand Down Expand Up @@ -83,3 +84,29 @@ func (c *AzureClients) setCredentials(subscriptionID string) error {
c.Authorizer, err = c.GetAuthorizer()
return err
}

func (c *AzureClients) setCredentialsWithProvider(ctx context.Context, subscriptionID string, credentialsProvider *AzureCredentialsProvider) error {
if credentialsProvider == nil {
return fmt.Errorf("Credentials provider cannot have an empty value")
}

settings, err := auth.GetSettingsFromEnvironment()
if err != nil {
return err
}

if subscriptionID == "" {
subscriptionID = settings.GetSubscriptionID()
if subscriptionID == "" {
return fmt.Errorf("error creating azure services. subscriptionID is not set in cluster or AZURE_SUBSCRIPTION_ID env var")
}
}

c.EnvironmentSettings = settings
c.ResourceManagerEndpoint = settings.Environment.ResourceManagerEndpoint
c.ResourceManagerVMDNSSuffix = settings.Environment.ResourceManagerVMDNSSuffix
c.Values[auth.SubscriptionID] = strings.TrimSuffix(subscriptionID, "\n")

c.Authorizer, err = credentialsProvider.GetAuthorizer(ctx, c.ResourceManagerEndpoint)
return err
}
19 changes: 15 additions & 4 deletions cloud/scope/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type ClusterScopeParams struct {

// NewClusterScope creates a new Scope from the supplied parameters.
// This is meant to be called for each reconcile iteration.
func NewClusterScope(params ClusterScopeParams) (*ClusterScope, error) {
func NewClusterScope(ctx context.Context, params ClusterScopeParams) (*ClusterScope, error) {
if params.Cluster == nil {
return nil, errors.New("failed to generate new scope from nil Cluster")
}
Expand All @@ -61,9 +61,20 @@ func NewClusterScope(params ClusterScopeParams) (*ClusterScope, error) {
params.Logger = klogr.New()
}

err := params.AzureClients.setCredentials(params.AzureCluster.Spec.SubscriptionID)
if err != nil {
return nil, err
if params.AzureCluster.Spec.IdentityName == nil {
err := params.AzureClients.setCredentials(params.AzureCluster.Spec.SubscriptionID)
if err != nil {
return nil, errors.Wrap(err, "failed to configure azure settings and credentials from environment")
}
} else {
credentailsProvider, err := NewAzureCredentialsProvider(ctx, params.Client, params.AzureCluster, to.String(params.AzureCluster.Spec.IdentityName), params.AzureCluster.Namespace)
if err != nil {
return nil, errors.Wrap(err, "failed to init credentials provider")
}
err = params.AzureClients.setCredentialsWithProvider(ctx, params.AzureCluster.Spec.SubscriptionID, credentailsProvider)
if err != nil {
return nil, errors.Wrap(err, "failed to configure azure settings and credentials for Identity")
}
}

helper, err := patch.NewHelper(params.AzureCluster, params.Client)
Expand Down
129 changes: 129 additions & 0 deletions cloud/scope/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
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 scope

import (
"context"
"fmt"

"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/pkg/errors"
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha3"
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
"sigs.k8s.io/controller-runtime/pkg/client"

aadpodv1 "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// AzureCredentialsProvider provides
type AzureCredentialsProvider struct {
Client client.Client
AzureCluster *infrav1.AzureCluster
Identity *aadpodv1.AzureIdentity
}

// NewAzureCredentialsProvider creates a new AzureCredentialsProvider from the supplied inputs.
func NewAzureCredentialsProvider(ctx context.Context, kubeClient client.Client, azureCluster *infrav1.AzureCluster, identityName, namespace string) (*AzureCredentialsProvider, error) {
if identityName == "" {
return nil, errors.New("failed to generate new AzureCredentialsProvider from empty identityName")
}

azureIdentity := &aadpodv1.AzureIdentity{}
err := kubeClient.Get(ctx, client.ObjectKey{Name: identityName, Namespace: namespace}, azureIdentity)
if err != nil {
return nil, errors.Wrap(err, "failed to get AzureIdentity")
}
if azureIdentity.Spec.Type != aadpodv1.ServicePrincipal {
return nil, errors.New("AzureIdentity is not of type Service Principal")
}

return &AzureCredentialsProvider{
Client: kubeClient,
AzureCluster: azureCluster,
Identity: azureIdentity,
}, nil
}

// GetAuthorizer returns Azure authorizer based on the provided azure identity
func (p *AzureCredentialsProvider) GetAuthorizer(ctx context.Context, resourceManagerEndpoint string) (autorest.Authorizer, error) {
copiedIdentity := &aadpodv1.AzureIdentity{
TypeMeta: metav1.TypeMeta{
Kind: "AzureIdentity",
APIVersion: "aadpodidentity.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s", p.AzureCluster.Namespace, p.Identity.Name),
Namespace: infrav1.ControllerNamespace,
Annotations: map[string]string{
aadpodv1.BehaviorKey: "namespaced",
},
Labels: map[string]string{
clusterv1.ClusterLabelName: p.AzureCluster.Name,
infrav1.ClusterLabelNamespace: p.AzureCluster.Namespace,
},
OwnerReferences: p.AzureCluster.OwnerReferences,
},
Spec: p.Identity.Spec,
}
err := p.Client.Create(ctx, copiedIdentity)
if err != nil && !apierrors.IsAlreadyExists(err) {
return nil, errors.Wrapf(err, "failed to create copied AzureIdentity %s in %s", copiedIdentity.Name, infrav1.ControllerNamespace)
}

azureIdentityBinding := &aadpodv1.AzureIdentityBinding{
TypeMeta: metav1.TypeMeta{
Kind: "AzureIdentityBinding",
APIVersion: "aadpodidentity.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-binding", copiedIdentity.Name),
Namespace: copiedIdentity.Namespace,
Labels: map[string]string{
clusterv1.ClusterLabelName: p.AzureCluster.Name,
infrav1.ClusterLabelNamespace: p.AzureCluster.Namespace,
},
OwnerReferences: p.AzureCluster.OwnerReferences,
},
Spec: aadpodv1.AzureIdentityBindingSpec{
AzureIdentity: copiedIdentity.Name,
Selector: infrav1.AzureIdentityBindingSelector, //should be same as selector added on controller
},
}
err = p.Client.Create(ctx, azureIdentityBinding)
if err != nil && !apierrors.IsAlreadyExists(err) {
return nil, errors.Wrapf(err, "failed to create AzureIdentityBinding %s in %s", copiedIdentity.Name, infrav1.ControllerNamespace)
}

var spt *adal.ServicePrincipalToken
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, errors.Wrap(err, "failed to get MSI endpoint")
}
if p.Identity.Spec.Type == aadpodv1.ServicePrincipal {
spt, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, resourceManagerEndpoint, p.Identity.Spec.ClientID)
if err != nil {
return nil, errors.Wrap(err, "failed to get token from service principal identity")
}
} else if p.Identity.Spec.Type == aadpodv1.UserAssignedMSI {
return nil, errors.Wrap(err, "UserAssignedMSI not supported")
}

return autorest.NewBearerAuthorizer(spt), nil
}
2 changes: 1 addition & 1 deletion cloud/services/disks/disks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func TestDiskSpecs(t *testing.T) {
azureMachine,
}
client := fake.NewFakeClientWithScheme(scheme, initObjects...)
clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{
clusterScope, err := scope.NewClusterScope(context.Background(), scope.ClusterScopeParams{
AzureClients: scope.AzureClients{
Authorizer: autorest.NullAuthorizer{},
},
Expand Down
2 changes: 1 addition & 1 deletion cloud/services/scalesets/scalesets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestNewService(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
}
client := fake.NewFakeClientWithScheme(scheme.Scheme, cluster)
s, err := scope.NewClusterScope(scope.ClusterScopeParams{
s, err := scope.NewClusterScope(context.Background(), scope.ClusterScopeParams{
AzureClients: scope.AzureClients{
Authorizer: autorest.NullAuthorizer{},
},
Expand Down
Loading

0 comments on commit 97f1988

Please sign in to comment.