sigs.k8s.io/cluster-api-provider-azure@v1.17.0/controllers/asosecret_controller.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package controllers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	asoconfig "github.com/Azure/azure-service-operator/v2/pkg/common/config"
    24  	"github.com/pkg/errors"
    25  	corev1 "k8s.io/api/core/v1"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/types"
    29  	"k8s.io/client-go/tools/record"
    30  	"k8s.io/utils/ptr"
    31  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    32  	"sigs.k8s.io/cluster-api-provider-azure/azure/scope"
    33  	"sigs.k8s.io/cluster-api-provider-azure/util/aso"
    34  	"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
    35  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    36  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    37  	"sigs.k8s.io/cluster-api/util"
    38  	"sigs.k8s.io/cluster-api/util/annotations"
    39  	"sigs.k8s.io/cluster-api/util/predicates"
    40  	ctrl "sigs.k8s.io/controller-runtime"
    41  	"sigs.k8s.io/controller-runtime/pkg/builder"
    42  	"sigs.k8s.io/controller-runtime/pkg/client"
    43  	"sigs.k8s.io/controller-runtime/pkg/controller"
    44  	"sigs.k8s.io/controller-runtime/pkg/handler"
    45  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    46  )
    47  
    48  // ASOSecretReconciler reconciles ASO secrets associated with AzureCluster objects.
    49  type ASOSecretReconciler struct {
    50  	client.Client
    51  	Recorder         record.EventRecorder
    52  	Timeouts         reconciler.Timeouts
    53  	WatchFilterValue string
    54  }
    55  
    56  // SetupWithManager initializes this controller with a manager.
    57  func (asos *ASOSecretReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
    58  	_, log, done := tele.StartSpanWithLogger(ctx,
    59  		"controllers.ASOSecretReconciler.SetupWithManager",
    60  		tele.KVP("controller", "ASOSecret"),
    61  	)
    62  	defer done()
    63  
    64  	return ctrl.NewControllerManagedBy(mgr).
    65  		WithOptions(options).
    66  		For(&infrav1.AzureCluster{}).
    67  		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(log, asos.WatchFilterValue)).
    68  		WithEventFilter(predicates.ResourceIsNotExternallyManaged(log)).
    69  		Named("ASOSecret").
    70  		Owns(&corev1.Secret{}).
    71  		// Add a watch on ASO secrets owned by an AzureManagedControlPlane
    72  		Watches(
    73  			&corev1.Secret{},
    74  			handler.EnqueueRequestForOwner(asos.Scheme(), asos.RESTMapper(), &infrav1.AzureManagedControlPlane{}, handler.OnlyControllerOwner()),
    75  		).
    76  		// Add a watch on infrav1.AzureManagedControlPlane.
    77  		Watches(
    78  			&infrav1.AzureManagedControlPlane{},
    79  			&handler.EnqueueRequestForObject{},
    80  			builder.WithPredicates(
    81  				predicates.ResourceNotPausedAndHasFilterLabel(log, asos.WatchFilterValue),
    82  			),
    83  		).
    84  		// Add a watch on clusterv1.Cluster object for unpause notifications.
    85  		Watches(
    86  			&clusterv1.Cluster{},
    87  			handler.EnqueueRequestsFromMapFunc(util.ClusterToInfrastructureMapFunc(ctx, infrav1.GroupVersion.WithKind(infrav1.AzureClusterKind), mgr.GetClient(), &infrav1.AzureCluster{})),
    88  			builder.WithPredicates(
    89  				predicates.ClusterUnpaused(log),
    90  				predicates.ResourceNotPausedAndHasFilterLabel(log, asos.WatchFilterValue),
    91  			),
    92  		).
    93  		Complete(asos)
    94  }
    95  
    96  // Reconcile reconciles the ASO secrets associated with AzureCluster objects.
    97  func (asos *ASOSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
    98  	ctx, cancel := context.WithTimeout(ctx, asos.Timeouts.DefaultedLoopTimeout())
    99  	defer cancel()
   100  
   101  	ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ASOSecret.Reconcile",
   102  		tele.KVP("namespace", req.Namespace),
   103  		tele.KVP("name", req.Name),
   104  		tele.KVP("kind", infrav1.AzureClusterKind),
   105  	)
   106  	defer done()
   107  
   108  	log = log.WithValues("namespace", req.Namespace)
   109  
   110  	// asoSecretOwner is the resource that created the identity. This could be either an AzureCluster or AzureManagedControlPlane (if AKS is enabled).
   111  	// check for AzureCluster first and if it is not found, check for AzureManagedControlPlane.
   112  	var asoSecretOwner client.Object
   113  
   114  	azureCluster := &infrav1.AzureCluster{}
   115  	checkForManagedControlPlane := false
   116  	// Fetch the AzureCluster or AzureManagedControlPlane instance
   117  	asoSecretOwner = azureCluster
   118  	err := asos.Get(ctx, req.NamespacedName, azureCluster)
   119  	if err != nil {
   120  		if apierrors.IsNotFound(err) {
   121  			checkForManagedControlPlane = true
   122  		} else {
   123  			return reconcile.Result{}, err
   124  		}
   125  	} else {
   126  		log = log.WithValues("AzureCluster", req.Name)
   127  	}
   128  
   129  	if checkForManagedControlPlane {
   130  		// Fetch the AzureManagedControlPlane instance instead
   131  		azureManagedControlPlane := &infrav1.AzureManagedControlPlane{}
   132  		asoSecretOwner = azureManagedControlPlane
   133  		err = asos.Get(ctx, req.NamespacedName, azureManagedControlPlane)
   134  		if err != nil {
   135  			if apierrors.IsNotFound(err) {
   136  				asos.Recorder.Eventf(azureCluster, corev1.EventTypeNormal, "AzureClusterObjectNotFound",
   137  					fmt.Sprintf("AzureCluster object %s/%s not found", req.Namespace, req.Name))
   138  				asos.Recorder.Eventf(azureManagedControlPlane, corev1.EventTypeNormal, "AzureManagedControlPlaneObjectNotFound",
   139  					fmt.Sprintf("AzureManagedControlPlane object %s/%s not found", req.Namespace, req.Name))
   140  				log.Info("object was not found")
   141  				return reconcile.Result{}, nil
   142  			} else {
   143  				return reconcile.Result{}, err
   144  			}
   145  		} else {
   146  			log = log.WithValues("AzureManagedControlPlane", req.Name)
   147  		}
   148  	}
   149  
   150  	var clusterIdentity *corev1.ObjectReference
   151  	var cluster *clusterv1.Cluster
   152  	var azureClient scope.AzureClients
   153  
   154  	switch ownerType := asoSecretOwner.(type) {
   155  	case *infrav1.AzureCluster:
   156  		clusterIdentity = ownerType.Spec.IdentityRef
   157  
   158  		// Fetch the Cluster.
   159  		cluster, err = util.GetOwnerCluster(ctx, asos.Client, ownerType.ObjectMeta)
   160  		if err != nil {
   161  			return reconcile.Result{}, err
   162  		}
   163  		if cluster == nil {
   164  			log.Info("Cluster Controller has not yet set OwnerRef")
   165  			return reconcile.Result{}, nil
   166  		}
   167  
   168  		// Create the scope.
   169  		clusterScope, err := scope.NewClusterScope(ctx, scope.ClusterScopeParams{
   170  			Client:       asos.Client,
   171  			Cluster:      cluster,
   172  			AzureCluster: ownerType,
   173  			Timeouts:     asos.Timeouts,
   174  		})
   175  		if err != nil {
   176  			return reconcile.Result{}, errors.Wrap(err, "failed to create scope")
   177  		}
   178  
   179  		azureClient = clusterScope.AzureClients
   180  
   181  	case *infrav1.AzureManagedControlPlane:
   182  		clusterIdentity = ownerType.Spec.IdentityRef
   183  
   184  		// Fetch the Cluster.
   185  		cluster, err = util.GetOwnerCluster(ctx, asos.Client, ownerType.ObjectMeta)
   186  		if err != nil {
   187  			return reconcile.Result{}, err
   188  		}
   189  		if cluster == nil {
   190  			log.Info("Cluster Controller has not yet set OwnerRef")
   191  			return reconcile.Result{}, nil
   192  		}
   193  
   194  		// Create the scope.
   195  		clusterScope, err := scope.NewManagedControlPlaneScope(ctx, scope.ManagedControlPlaneScopeParams{
   196  			Client:       asos.Client,
   197  			Cluster:      cluster,
   198  			ControlPlane: ownerType,
   199  			Timeouts:     asos.Timeouts,
   200  		})
   201  		if err != nil {
   202  			return reconcile.Result{}, errors.Wrap(err, "failed to create scope")
   203  		}
   204  
   205  		azureClient = clusterScope.AzureClients
   206  	}
   207  
   208  	if cluster == nil {
   209  		log.Info("Cluster Controller has not yet set OwnerRef")
   210  		asos.Recorder.Eventf(asoSecretOwner, corev1.EventTypeNormal, "OwnerRefNotFound",
   211  			fmt.Sprintf("Cluster Controller has not yet set OwnerRef for object %s/%s", req.Namespace, req.Name))
   212  		return reconcile.Result{}, nil
   213  	}
   214  
   215  	log = log.WithValues("cluster", cluster.Name)
   216  
   217  	// Return early if the ASO Secret Owner(AzureCluster or AzureManagedControlPlane) or Cluster is paused.
   218  	if annotations.IsPaused(cluster, asoSecretOwner) {
   219  		log.Info(fmt.Sprintf("%T or linked Cluster is marked as paused. Won't reconcile", asoSecretOwner))
   220  		asos.Recorder.Eventf(asoSecretOwner, corev1.EventTypeNormal, "ClusterPaused",
   221  			fmt.Sprintf("%T or linked Cluster is marked as paused. Won't reconcile", asoSecretOwner))
   222  		return ctrl.Result{}, nil
   223  	}
   224  
   225  	// Construct the ASO secret for this Cluster
   226  	newASOSecret, err := asos.createSecretFromClusterIdentity(ctx, clusterIdentity, cluster, azureClient)
   227  	if err != nil {
   228  		return reconcile.Result{}, err
   229  	}
   230  
   231  	gvk := asoSecretOwner.GetObjectKind().GroupVersionKind()
   232  	owner := metav1.OwnerReference{
   233  		APIVersion: gvk.GroupVersion().String(),
   234  		Kind:       gvk.Kind,
   235  		Name:       asoSecretOwner.GetName(),
   236  		UID:        asoSecretOwner.GetUID(),
   237  		Controller: ptr.To(true),
   238  	}
   239  
   240  	newASOSecret.OwnerReferences = []metav1.OwnerReference{owner}
   241  
   242  	if err := reconcileAzureSecret(ctx, asos.Client, owner, newASOSecret, cluster.GetName()); err != nil {
   243  		asos.Recorder.Eventf(cluster, corev1.EventTypeWarning, "Error reconciling ASO secret", err.Error())
   244  		return ctrl.Result{}, errors.Wrap(err, "failed to reconcile ASO secret")
   245  	}
   246  
   247  	return ctrl.Result{}, nil
   248  }
   249  
   250  func (asos *ASOSecretReconciler) createSecretFromClusterIdentity(ctx context.Context, clusterIdentity *corev1.ObjectReference, cluster *clusterv1.Cluster, azureClient scope.AzureClients) (*corev1.Secret, error) {
   251  	newASOSecret := &corev1.Secret{
   252  		ObjectMeta: metav1.ObjectMeta{
   253  			Name:      aso.GetASOSecretName(cluster.GetName()),
   254  			Namespace: cluster.GetNamespace(),
   255  			Labels: map[string]string{
   256  				cluster.GetName(): string(infrav1.ResourceLifecycleOwned),
   257  			},
   258  		},
   259  		Data: map[string][]byte{
   260  			asoconfig.AzureSubscriptionID: []byte(azureClient.SubscriptionID()),
   261  		},
   262  	}
   263  
   264  	// if the namespace isn't specified then assume it's in the same namespace as the Cluster's one
   265  	namespace := clusterIdentity.Namespace
   266  	if namespace == "" {
   267  		namespace = cluster.GetNamespace()
   268  	}
   269  	identity := &infrav1.AzureClusterIdentity{}
   270  	key := client.ObjectKey{
   271  		Name:      clusterIdentity.Name,
   272  		Namespace: namespace,
   273  	}
   274  	if err := asos.Get(ctx, key, identity); err != nil {
   275  		return nil, errors.Wrap(err, "failed to retrieve AzureClusterIdentity")
   276  	}
   277  
   278  	newASOSecret.Data[asoconfig.AzureTenantID] = []byte(identity.Spec.TenantID)
   279  	newASOSecret.Data[asoconfig.AzureClientID] = []byte(identity.Spec.ClientID)
   280  
   281  	// If the identity type is WorkloadIdentity or UserAssignedMSI, then we don't need to fetch the secret so return early
   282  	if identity.Spec.Type == infrav1.WorkloadIdentity {
   283  		newASOSecret.Data[asoconfig.AuthMode] = []byte(asoconfig.WorkloadIdentityAuthMode)
   284  		return newASOSecret, nil
   285  	}
   286  	if identity.Spec.Type == infrav1.UserAssignedMSI {
   287  		newASOSecret.Data[asoconfig.AuthMode] = []byte(asoconfig.PodIdentityAuthMode)
   288  		return newASOSecret, nil
   289  	}
   290  
   291  	// Fetch identity secret, if it exists
   292  	key = types.NamespacedName{
   293  		Namespace: identity.Spec.ClientSecret.Namespace,
   294  		Name:      identity.Spec.ClientSecret.Name,
   295  	}
   296  	identitySecret := &corev1.Secret{}
   297  	err := asos.Get(ctx, key, identitySecret)
   298  	if err != nil {
   299  		return nil, errors.Wrap(err, "failed to fetch AzureClusterIdentity secret")
   300  	}
   301  
   302  	switch identity.Spec.Type {
   303  	case infrav1.ServicePrincipal, infrav1.ManualServicePrincipal:
   304  		newASOSecret.Data[asoconfig.AzureClientSecret] = identitySecret.Data[scope.AzureSecretKey]
   305  	case infrav1.ServicePrincipalCertificate:
   306  		newASOSecret.Data[asoconfig.AzureClientCertificate] = identitySecret.Data["certificate"]
   307  		newASOSecret.Data[asoconfig.AzureClientCertificatePassword] = identitySecret.Data["password"]
   308  	}
   309  	return newASOSecret, nil
   310  }