Skip to content

Commit 15bde64

Browse files
authored
Merge pull request #1386 from shysank/aks_multitenancy
Multitenancy for managed clusters
2 parents 908cbfd + 5a6a864 commit 15bde64

29 files changed

+946
-69
lines changed

azure/scope/clients.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func (c *AzureClients) setCredentials(subscriptionID, environmentName string) er
9999
return err
100100
}
101101

102-
func (c *AzureClients) setCredentialsWithProvider(ctx context.Context, subscriptionID, environmentName string, credentialsProvider *AzureCredentialsProvider) error {
102+
func (c *AzureClients) setCredentialsWithProvider(ctx context.Context, subscriptionID, environmentName string, credentialsProvider CredentialsProvider) error {
103103
if credentialsProvider == nil {
104104
return fmt.Errorf("credentials provider cannot have an empty value")
105105
}

azure/scope/cluster.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func NewClusterScope(ctx context.Context, params ClusterScopeParams) (*ClusterSc
6767
return nil, errors.Wrap(err, "failed to configure azure settings and credentials from environment")
6868
}
6969
} else {
70-
credentailsProvider, err := NewAzureCredentialsProvider(ctx, params.Client, params.AzureCluster)
70+
credentailsProvider, err := NewAzureClusterCredentialsProvider(ctx, params.Client, params.AzureCluster)
7171
if err != nil {
7272
return nil, errors.Wrap(err, "failed to init credentials provider")
7373
}

azure/scope/identity.go

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/Azure/go-autorest/autorest/adal"
2626
"github.com/pkg/errors"
2727
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
28+
infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4"
2829
"sigs.k8s.io/cluster-api-provider-azure/util/identity"
2930
"sigs.k8s.io/cluster-api-provider-azure/util/system"
3031
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
@@ -37,17 +38,36 @@ import (
3738
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3839
)
3940

40-
// AzureCredentialsProvider provides credentials for an AzureCluster.
41+
// CredentialsProvider defines the behavior for azure identity based credential providers.
42+
type CredentialsProvider interface {
43+
GetAuthorizer(ctx context.Context, resourceManagerEndpoint string) (autorest.Authorizer, error)
44+
}
45+
46+
// AzureCredentialsProvider represents a credential provider with azure cluster identity.
4147
type AzureCredentialsProvider struct {
42-
Client client.Client
48+
Client client.Client
49+
Identity *infrav1.AzureClusterIdentity
50+
}
51+
52+
// AzureClusterCredentialsProvider wraps AzureCredentialsProvider with AzureCluster.
53+
type AzureClusterCredentialsProvider struct {
54+
AzureCredentialsProvider
4355
AzureCluster *infrav1.AzureCluster
44-
Identity *infrav1.AzureClusterIdentity
4556
}
4657

47-
// NewAzureCredentialsProvider creates a new AzureCredentialsProvider from the supplied inputs.
48-
func NewAzureCredentialsProvider(ctx context.Context, kubeClient client.Client, azureCluster *infrav1.AzureCluster) (*AzureCredentialsProvider, error) {
58+
// ManagedControlPlaneCredentialsProvider wraps AzureCredentialsProvider with AzureManagedControlPlane.
59+
type ManagedControlPlaneCredentialsProvider struct {
60+
AzureCredentialsProvider
61+
AzureManagedControlPlane *infrav1exp.AzureManagedControlPlane
62+
}
63+
64+
var _ CredentialsProvider = (*AzureClusterCredentialsProvider)(nil)
65+
var _ CredentialsProvider = (*ManagedControlPlaneCredentialsProvider)(nil)
66+
67+
// NewAzureClusterCredentialsProvider creates a new AzureClusterCredentialsProvider from the supplied inputs.
68+
func NewAzureClusterCredentialsProvider(ctx context.Context, kubeClient client.Client, azureCluster *infrav1.AzureCluster) (*AzureClusterCredentialsProvider, error) {
4969
if azureCluster.Spec.IdentityRef == nil {
50-
return nil, errors.New("failed to generate new AzureCredentialsProvider from empty identityName")
70+
return nil, errors.New("failed to generate new AzureClusterCredentialsProvider from empty identityName")
5171
}
5272

5373
ref := azureCluster.Spec.IdentityRef
@@ -66,15 +86,58 @@ func NewAzureCredentialsProvider(ctx context.Context, kubeClient client.Client,
6686
return nil, errors.New("AzureClusterIdentity is not of type Service Principal")
6787
}
6888

69-
return &AzureCredentialsProvider{
70-
Client: kubeClient,
71-
AzureCluster: azureCluster,
72-
Identity: identity,
89+
return &AzureClusterCredentialsProvider{
90+
AzureCredentialsProvider{
91+
Client: kubeClient,
92+
Identity: identity,
93+
},
94+
azureCluster,
7395
}, nil
7496
}
7597

76-
// GetAuthorizer returns an Azure authorizer based on the provided Azure identity.
77-
func (p *AzureCredentialsProvider) GetAuthorizer(ctx context.Context, resourceManagerEndpoint string) (autorest.Authorizer, error) {
98+
// GetAuthorizer returns an Azure authorizer based on the provided azure identity. It delegates to AzureCredentialsProvider with AzureCluster metadata.
99+
func (p *AzureClusterCredentialsProvider) GetAuthorizer(ctx context.Context, resourceManagerEndpoint string) (autorest.Authorizer, error) {
100+
return p.AzureCredentialsProvider.GetAuthorizer(ctx, resourceManagerEndpoint, p.AzureCluster.ObjectMeta)
101+
}
102+
103+
// NewManagedControlPlaneCredentialsProvider creates a new ManagedControlPlaneCredentialsProvider from the supplied inputs.
104+
func NewManagedControlPlaneCredentialsProvider(ctx context.Context, kubeClient client.Client, managedControlPlane *infrav1exp.AzureManagedControlPlane) (*ManagedControlPlaneCredentialsProvider, error) {
105+
if managedControlPlane.Spec.IdentityRef == nil {
106+
return nil, errors.New("failed to generate new ManagedControlPlaneCredentialsProvider from empty identityName")
107+
}
108+
109+
ref := managedControlPlane.Spec.IdentityRef
110+
// if the namespace isn't specified then assume it's in the same namespace as the AzureManagedControlPlane
111+
namespace := ref.Namespace
112+
if namespace == "" {
113+
namespace = managedControlPlane.Namespace
114+
}
115+
identity := &infrav1.AzureClusterIdentity{}
116+
key := client.ObjectKey{Name: ref.Name, Namespace: namespace}
117+
if err := kubeClient.Get(ctx, key, identity); err != nil {
118+
return nil, errors.Errorf("failed to retrieve AzureClusterIdentity external object %q/%q: %v", key.Namespace, key.Name, err)
119+
}
120+
121+
if identity.Spec.Type != infrav1.ServicePrincipal {
122+
return nil, errors.New("AzureClusterIdentity is not of type Service Principal")
123+
}
124+
125+
return &ManagedControlPlaneCredentialsProvider{
126+
AzureCredentialsProvider{
127+
Client: kubeClient,
128+
Identity: identity,
129+
},
130+
managedControlPlane,
131+
}, nil
132+
}
133+
134+
// GetAuthorizer returns an Azure authorizer based on the provided azure identity. It delegates to AzureCredentialsProvider with AzureManagedControlPlane metadata.
135+
func (p *ManagedControlPlaneCredentialsProvider) GetAuthorizer(ctx context.Context, resourceManagerEndpoint string) (autorest.Authorizer, error) {
136+
return p.AzureCredentialsProvider.GetAuthorizer(ctx, resourceManagerEndpoint, p.AzureManagedControlPlane.ObjectMeta)
137+
}
138+
139+
// GetAuthorizer returns an Azure authorizer based on the provided azure identity and cluster metadata.
140+
func (p *AzureCredentialsProvider) GetAuthorizer(ctx context.Context, resourceManagerEndpoint string, clusterMeta metav1.ObjectMeta) (autorest.Authorizer, error) {
78141
azureIdentityType, err := getAzureIdentityType(p.Identity)
79142
if err != nil {
80143
return nil, err
@@ -85,17 +148,17 @@ func (p *AzureCredentialsProvider) GetAuthorizer(ctx context.Context, resourceMa
85148
APIVersion: "aadpodidentity.k8s.io/v1",
86149
},
87150
ObjectMeta: metav1.ObjectMeta{
88-
Name: identity.GetAzureIdentityName(p.AzureCluster.Name, p.AzureCluster.Namespace, p.Identity.Name),
151+
Name: identity.GetAzureIdentityName(clusterMeta.Name, clusterMeta.Namespace, p.Identity.Name),
89152
Namespace: system.GetManagerNamespace(),
90153
Annotations: map[string]string{
91154
aadpodv1.BehaviorKey: "namespaced",
92155
},
93156
Labels: map[string]string{
94-
clusterv1.ClusterLabelName: p.AzureCluster.Name,
95-
infrav1.ClusterLabelNamespace: p.AzureCluster.Namespace,
157+
clusterv1.ClusterLabelName: clusterMeta.Name,
158+
infrav1.ClusterLabelNamespace: clusterMeta.Namespace,
96159
clusterctl.ClusterctlMoveLabelName: "true",
97160
},
98-
OwnerReferences: p.AzureCluster.OwnerReferences,
161+
OwnerReferences: clusterMeta.OwnerReferences,
99162
},
100163
Spec: aadpodv1.AzureIdentitySpec{
101164
Type: azureIdentityType,
@@ -119,11 +182,11 @@ func (p *AzureCredentialsProvider) GetAuthorizer(ctx context.Context, resourceMa
119182
Name: fmt.Sprintf("%s-binding", copiedIdentity.Name),
120183
Namespace: copiedIdentity.Namespace,
121184
Labels: map[string]string{
122-
clusterv1.ClusterLabelName: p.AzureCluster.Name,
123-
infrav1.ClusterLabelNamespace: p.AzureCluster.Namespace,
185+
clusterv1.ClusterLabelName: clusterMeta.Name,
186+
infrav1.ClusterLabelNamespace: clusterMeta.Namespace,
124187
clusterctl.ClusterctlMoveLabelName: "true",
125188
},
126-
OwnerReferences: p.AzureCluster.OwnerReferences,
189+
OwnerReferences: clusterMeta.OwnerReferences,
127190
},
128191
Spec: aadpodv1.AzureIdentityBindingSpec{
129192
AzureIdentity: copiedIdentity.Name,
@@ -169,7 +232,7 @@ func IsClusterNamespaceAllowed(ctx context.Context, k8sClient client.Client, all
169232
return false
170233
}
171234

172-
// empty value matches with all namespaces.
235+
// empty value matches with all namespaces
173236
if reflect.DeepEqual(*allowedNamespaces, infrav1.AllowedNamespaces{}) {
174237
return true
175238
}

azure/scope/managedcontrolplane.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type ManagedControlPlaneScopeParams struct {
4848

4949
// NewManagedControlPlaneScope creates a new Scope from the supplied parameters.
5050
// This is meant to be called for each reconcile iteration.
51-
func NewManagedControlPlaneScope(params ManagedControlPlaneScopeParams) (*ManagedControlPlaneScope, error) {
51+
func NewManagedControlPlaneScope(ctx context.Context, params ManagedControlPlaneScopeParams) (*ManagedControlPlaneScope, error) {
5252
if params.Cluster == nil {
5353
return nil, errors.New("failed to generate new scope from nil Cluster")
5454
}
@@ -61,8 +61,19 @@ func NewManagedControlPlaneScope(params ManagedControlPlaneScopeParams) (*Manage
6161
params.Logger = klogr.New()
6262
}
6363

64-
if err := params.AzureClients.setCredentials(params.ControlPlane.Spec.SubscriptionID, ""); err != nil {
65-
return nil, errors.Wrap(err, "failed to create Azure session")
64+
if params.ControlPlane.Spec.IdentityRef == nil {
65+
if err := params.AzureClients.setCredentials(params.ControlPlane.Spec.SubscriptionID, ""); err != nil {
66+
return nil, errors.Wrap(err, "failed to create Azure session")
67+
}
68+
} else {
69+
credentialsProvider, err := NewManagedControlPlaneCredentialsProvider(ctx, params.Client, params.ControlPlane)
70+
if err != nil {
71+
return nil, errors.Wrap(err, "failed to init credentials provider")
72+
}
73+
74+
if err := params.AzureClients.setCredentialsWithProvider(ctx, params.ControlPlane.Spec.SubscriptionID, "", credentialsProvider); err != nil {
75+
return nil, errors.Wrap(err, "failed to configure azure settings and credentials for Identity")
76+
}
6677
}
6778

6879
helper, err := patch.NewHelper(params.PatchTarget, params.Client)

config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,31 @@ spec:
192192
dnsServiceIP:
193193
description: DNSServiceIP is an IP address assigned to the Kubernetes DNS service. It must be within the Kubernetes service address range specified in serviceCidr.
194194
type: string
195+
identityRef:
196+
description: IdentityRef is a reference to a AzureClusterIdentity to be used when reconciling this cluster
197+
properties:
198+
apiVersion:
199+
description: API version of the referent.
200+
type: string
201+
fieldPath:
202+
description: 'If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object. TODO: this design is not final and this field is subject to change in the future.'
203+
type: string
204+
kind:
205+
description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
206+
type: string
207+
name:
208+
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
209+
type: string
210+
namespace:
211+
description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/'
212+
type: string
213+
resourceVersion:
214+
description: 'Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency'
215+
type: string
216+
uid:
217+
description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids'
218+
type: string
219+
type: object
195220
loadBalancerSKU:
196221
description: LoadBalancerSKU is the SKU of the loadBalancer to be provisioned.
197222
enum:

controllers/azureidentity_controller.go

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ package controllers
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"time"
2223

24+
infraexpv1 "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4"
25+
"sigs.k8s.io/cluster-api-provider-azure/feature"
26+
2327
aadpodv1 "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1"
2428
"github.com/go-logr/logr"
2529
"github.com/pkg/errors"
@@ -65,6 +69,17 @@ func (r *AzureIdentityReconciler) SetupWithManager(ctx context.Context, mgr ctrl
6569
return errors.Wrap(err, "error creating controller")
6670
}
6771

72+
// Add a watch on infraexpv1.AzureManagedControlPlane if aks is enabled.
73+
if feature.Gates.Enabled(feature.AKS) {
74+
if err = c.Watch(
75+
&source.Kind{Type: &infraexpv1.AzureManagedControlPlane{}},
76+
&handler.EnqueueRequestForObject{},
77+
predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue),
78+
); err != nil {
79+
return errors.Wrap(err, "failed adding a watch for ready clusters")
80+
}
81+
}
82+
6883
// Add a watch on clusterv1.Cluster object for unpause notifications.
6984
if err = c.Watch(
7085
&source.Kind{Type: &clusterv1.Cluster{}},
@@ -96,20 +111,38 @@ func (r *AzureIdentityReconciler) Reconcile(ctx context.Context, req ctrl.Reques
96111
))
97112
defer span.End()
98113

114+
// identityOwner is the resource that created the identity. This could be either an AzureCluster or AzureManagedControlPlane (if AKS is enabled).
115+
// check for AzureManagedControlPlane first and if it is not found, check for AzureManagedControlPlane.
116+
var identityOwner interface{}
117+
99118
// Fetch the AzureCluster instance
100119
azureCluster := &infrav1.AzureCluster{}
120+
identityOwner = azureCluster
101121
err := r.Get(ctx, req.NamespacedName, azureCluster)
102-
if err != nil {
103-
if apierrors.IsNotFound(err) {
122+
if err != nil && apierrors.IsNotFound(err) {
123+
if feature.Gates.Enabled(feature.AKS) {
124+
// Fetch the AzureManagedControlPlane instance
125+
azureManagedControlPlane := &infraexpv1.AzureManagedControlPlane{}
126+
identityOwner = azureManagedControlPlane
127+
err = r.Get(ctx, req.NamespacedName, azureManagedControlPlane)
128+
if err != nil && apierrors.IsNotFound(err) {
129+
r.Recorder.Eventf(azureCluster, corev1.EventTypeNormal, "AzureClusterObjectNotFound",
130+
fmt.Sprintf("AzureCluster object %s/%s not found", req.Namespace, req.Name))
131+
r.Recorder.Eventf(azureManagedControlPlane, corev1.EventTypeNormal, "AzureManagedControlPlaneObjectNotFound",
132+
fmt.Sprintf("AzureManagedControlPlane object %s/%s not found", req.Namespace, req.Name))
133+
log.Info("object was not found")
134+
return reconcile.Result{}, nil
135+
}
136+
} else {
104137
r.Recorder.Eventf(azureCluster, corev1.EventTypeNormal, "AzureClusterObjectNotFound", err.Error())
105138
log.Info("object was not found")
106139
return reconcile.Result{}, nil
107140
}
141+
}
142+
if err != nil {
108143
return reconcile.Result{}, err
109144
}
110145

111-
log = log.WithValues("azurecluster", azureCluster.Name)
112-
113146
// get all the bindings
114147
var bindings aadpodv1.AzureIdentityBindingList
115148
if err := r.List(ctx, &bindings, client.InNamespace(system.GetManagerNamespace())); err != nil {
@@ -125,16 +158,36 @@ func (r *AzureIdentityReconciler) Reconcile(ctx context.Context, req ctrl.Reques
125158
clusterNamespace := binding.ObjectMeta.Labels[infrav1.ClusterLabelNamespace]
126159

127160
key := client.ObjectKey{Name: clusterName, Namespace: clusterNamespace}
128-
azCluster := &infrav1.AzureCluster{}
129-
if err := r.Get(ctx, key, azCluster); err != nil {
130-
if apierrors.IsNotFound(err) {
131-
bindingsToDelete = append(bindingsToDelete, b)
132-
continue
133-
} else {
134-
return ctrl.Result{}, errors.Wrap(err, "failed to get AzureCluster")
161+
var expectedIdentityName string
162+
163+
// only delete bindings when the identity owner type is not found.
164+
// we should not delete an identity when azureCluster is not found because it could have been created by AzureManagedControlPlane.
165+
switch identityOwner.(type) {
166+
case infrav1.AzureCluster:
167+
azCluster := &infrav1.AzureCluster{}
168+
if err := r.Get(ctx, key, azCluster); err != nil {
169+
if apierrors.IsNotFound(err) {
170+
bindingsToDelete = append(bindingsToDelete, b)
171+
continue
172+
} else {
173+
return ctrl.Result{}, errors.Wrap(err, "failed to get AzureCluster")
174+
}
175+
}
176+
expectedIdentityName = identity.GetAzureIdentityName(azCluster.Name, azCluster.Namespace, azCluster.Spec.IdentityRef.Name)
177+
case infraexpv1.AzureManagedControlPlane:
178+
azManagedControlPlane := &infraexpv1.AzureManagedControlPlane{}
179+
if err := r.Get(ctx, key, azManagedControlPlane); err != nil {
180+
if apierrors.IsNotFound(err) {
181+
bindingsToDelete = append(bindingsToDelete, b)
182+
continue
183+
} else {
184+
return ctrl.Result{}, errors.Wrap(err, "failed to get AzureManagedControlPlane")
185+
}
135186
}
187+
expectedIdentityName = identity.GetAzureIdentityName(azManagedControlPlane.Name, azManagedControlPlane.Namespace,
188+
azManagedControlPlane.Spec.IdentityRef.Name)
136189
}
137-
expectedIdentityName := identity.GetAzureIdentityName(azCluster.Name, azCluster.Namespace, azCluster.Spec.IdentityRef.Name)
190+
138191
if binding.Spec.AzureIdentity != expectedIdentityName {
139192
bindingsToDelete = append(bindingsToDelete, b)
140193
}
@@ -145,7 +198,7 @@ func (r *AzureIdentityReconciler) Reconcile(ctx context.Context, req ctrl.Reques
145198
binding := bindingToDelete
146199
identityName := binding.Spec.AzureIdentity
147200
if err := r.Client.Delete(ctx, &binding); err != nil {
148-
r.Recorder.Eventf(azureCluster, corev1.EventTypeWarning, "Error reconciling AzureIdentity for AzureCluster", err.Error())
201+
r.Recorder.Eventf(azureCluster, corev1.EventTypeWarning, "Error reconciling AzureIdentity", err.Error())
149202
log.Error(err, "failed to delete AzureIdentityBinding")
150203
return ctrl.Result{}, err
151204
}
@@ -155,7 +208,7 @@ func (r *AzureIdentityReconciler) Reconcile(ctx context.Context, req ctrl.Reques
155208
return ctrl.Result{}, err
156209
}
157210
if err := r.Client.Delete(ctx, azureIdentity); err != nil {
158-
r.Recorder.Eventf(azureCluster, corev1.EventTypeWarning, "Error reconciling AzureIdentity for AzureCluster", err.Error())
211+
r.Recorder.Eventf(azureCluster, corev1.EventTypeWarning, "Error reconciling AzureIdentity", err.Error())
159212
log.Error(err, "failed to delete AzureIdentity")
160213
return ctrl.Result{}, err
161214
}

0 commit comments

Comments
 (0)