sigs.k8s.io/cluster-api-provider-azure@v1.14.3/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/client"
    42  	"sigs.k8s.io/controller-runtime/pkg/controller"
    43  	"sigs.k8s.io/controller-runtime/pkg/handler"
    44  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    45  	"sigs.k8s.io/controller-runtime/pkg/source"
    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  	c, err := 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  		Build(asos)
    72  	if err != nil {
    73  		return errors.Wrap(err, "error creating controller")
    74  	}
    75  
    76  	// Add a watch on infrav1.AzureManagedControlPlane.
    77  	if err = c.Watch(
    78  		source.Kind(mgr.GetCache(), &infrav1.AzureManagedControlPlane{}),
    79  		&handler.EnqueueRequestForObject{},
    80  		predicates.ResourceNotPausedAndHasFilterLabel(log, asos.WatchFilterValue),
    81  	); err != nil {
    82  		return errors.Wrap(err, "failed adding a watch for ready AzureManagedControlPlanes")
    83  	}
    84  
    85  	// Add a watch on ASO secrets owned by an AzureManagedControlPlane
    86  	if err = c.Watch(
    87  		source.Kind(mgr.GetCache(), &corev1.Secret{}),
    88  		handler.EnqueueRequestForOwner(asos.Scheme(), asos.RESTMapper(), &infrav1.AzureManagedControlPlane{}, handler.OnlyControllerOwner()),
    89  	); err != nil {
    90  		return errors.Wrap(err, "failed adding a watch for secrets")
    91  	}
    92  
    93  	// Add a watch on clusterv1.Cluster object for unpause notifications.
    94  	if err = c.Watch(
    95  		source.Kind(mgr.GetCache(), &clusterv1.Cluster{}),
    96  		handler.EnqueueRequestsFromMapFunc(util.ClusterToInfrastructureMapFunc(ctx, infrav1.GroupVersion.WithKind(infrav1.AzureClusterKind), mgr.GetClient(), &infrav1.AzureCluster{})),
    97  		predicates.ClusterUnpaused(log),
    98  		predicates.ResourceNotPausedAndHasFilterLabel(log, asos.WatchFilterValue),
    99  	); err != nil {
   100  		return errors.Wrap(err, "failed adding a watch for ready clusters")
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  // Reconcile reconciles the ASO secrets associated with AzureCluster objects.
   107  func (asos *ASOSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
   108  	ctx, cancel := context.WithTimeout(ctx, asos.Timeouts.DefaultedLoopTimeout())
   109  	defer cancel()
   110  
   111  	ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ASOSecret.Reconcile",
   112  		tele.KVP("namespace", req.Namespace),
   113  		tele.KVP("name", req.Name),
   114  		tele.KVP("kind", infrav1.AzureClusterKind),
   115  	)
   116  	defer done()
   117  
   118  	log = log.WithValues("namespace", req.Namespace)
   119  
   120  	// asoSecretOwner is the resource that created the identity. This could be either an AzureCluster or AzureManagedControlPlane (if AKS is enabled).
   121  	// check for AzureCluster first and if it is not found, check for AzureManagedControlPlane.
   122  	var asoSecretOwner client.Object
   123  
   124  	azureCluster := &infrav1.AzureCluster{}
   125  	checkForManagedControlPlane := false
   126  	// Fetch the AzureCluster or AzureManagedControlPlane instance
   127  	asoSecretOwner = azureCluster
   128  	err := asos.Get(ctx, req.NamespacedName, azureCluster)
   129  	if err != nil {
   130  		if apierrors.IsNotFound(err) {
   131  			checkForManagedControlPlane = true
   132  		} else {
   133  			return reconcile.Result{}, err
   134  		}
   135  	} else {
   136  		log = log.WithValues("AzureCluster", req.Name)
   137  	}
   138  
   139  	if checkForManagedControlPlane {
   140  		// Fetch the AzureManagedControlPlane instance instead
   141  		azureManagedControlPlane := &infrav1.AzureManagedControlPlane{}
   142  		asoSecretOwner = azureManagedControlPlane
   143  		err = asos.Get(ctx, req.NamespacedName, azureManagedControlPlane)
   144  		if err != nil {
   145  			if apierrors.IsNotFound(err) {
   146  				asos.Recorder.Eventf(azureCluster, corev1.EventTypeNormal, "AzureClusterObjectNotFound",
   147  					fmt.Sprintf("AzureCluster object %s/%s not found", req.Namespace, req.Name))
   148  				asos.Recorder.Eventf(azureManagedControlPlane, corev1.EventTypeNormal, "AzureManagedControlPlaneObjectNotFound",
   149  					fmt.Sprintf("AzureManagedControlPlane object %s/%s not found", req.Namespace, req.Name))
   150  				log.Info("object was not found")
   151  				return reconcile.Result{}, nil
   152  			} else {
   153  				return reconcile.Result{}, err
   154  			}
   155  		} else {
   156  			log = log.WithValues("AzureManagedControlPlane", req.Name)
   157  		}
   158  	}
   159  
   160  	var clusterIdentity *corev1.ObjectReference
   161  	var cluster *clusterv1.Cluster
   162  	var azureClient scope.AzureClients
   163  
   164  	switch ownerType := asoSecretOwner.(type) {
   165  	case *infrav1.AzureCluster:
   166  		clusterIdentity = ownerType.Spec.IdentityRef
   167  
   168  		// Fetch the Cluster.
   169  		cluster, err = util.GetOwnerCluster(ctx, asos.Client, ownerType.ObjectMeta)
   170  		if err != nil {
   171  			return reconcile.Result{}, err
   172  		}
   173  		if cluster == nil {
   174  			log.Info("Cluster Controller has not yet set OwnerRef")
   175  			return reconcile.Result{}, nil
   176  		}
   177  
   178  		// Create the scope.
   179  		clusterScope, err := scope.NewClusterScope(ctx, scope.ClusterScopeParams{
   180  			Client:       asos.Client,
   181  			Cluster:      cluster,
   182  			AzureCluster: ownerType,
   183  			Timeouts:     asos.Timeouts,
   184  		})
   185  		if err != nil {
   186  			return reconcile.Result{}, errors.Wrap(err, "failed to create scope")
   187  		}
   188  
   189  		azureClient = clusterScope.AzureClients
   190  
   191  	case *infrav1.AzureManagedControlPlane:
   192  		clusterIdentity = ownerType.Spec.IdentityRef
   193  
   194  		// Fetch the Cluster.
   195  		cluster, err = util.GetOwnerCluster(ctx, asos.Client, ownerType.ObjectMeta)
   196  		if err != nil {
   197  			return reconcile.Result{}, err
   198  		}
   199  		if cluster == nil {
   200  			log.Info("Cluster Controller has not yet set OwnerRef")
   201  			return reconcile.Result{}, nil
   202  		}
   203  
   204  		// Create the scope.
   205  		clusterScope, err := scope.NewManagedControlPlaneScope(ctx, scope.ManagedControlPlaneScopeParams{
   206  			Client:       asos.Client,
   207  			Cluster:      cluster,
   208  			ControlPlane: ownerType,
   209  			Timeouts:     asos.Timeouts,
   210  		})
   211  		if err != nil {
   212  			return reconcile.Result{}, errors.Wrap(err, "failed to create scope")
   213  		}
   214  
   215  		azureClient = clusterScope.AzureClients
   216  	}
   217  
   218  	if cluster == nil {
   219  		log.Info("Cluster Controller has not yet set OwnerRef")
   220  		asos.Recorder.Eventf(asoSecretOwner, corev1.EventTypeNormal, "OwnerRefNotFound",
   221  			fmt.Sprintf("Cluster Controller has not yet set OwnerRef for object %s/%s", req.Namespace, req.Name))
   222  		return reconcile.Result{}, nil
   223  	}
   224  
   225  	log = log.WithValues("cluster", cluster.Name)
   226  
   227  	// Return early if the ASO Secret Owner(AzureCluster or AzureManagedControlPlane) or Cluster is paused.
   228  	if annotations.IsPaused(cluster, asoSecretOwner) {
   229  		log.Info(fmt.Sprintf("%s or linked Cluster is marked as paused. Won't reconcile", asoSecretOwner.GetObjectKind()))
   230  		asos.Recorder.Eventf(asoSecretOwner, corev1.EventTypeNormal, "ClusterPaused",
   231  			fmt.Sprintf("%s or linked Cluster is marked as paused. Won't reconcile", asoSecretOwner.GetObjectKind().GroupVersionKind().Kind))
   232  		return ctrl.Result{}, nil
   233  	}
   234  
   235  	// Construct the ASO secret for this Cluster
   236  	newASOSecret, err := asos.createSecretFromClusterIdentity(ctx, clusterIdentity, cluster, azureClient)
   237  	if err != nil {
   238  		return reconcile.Result{}, err
   239  	}
   240  
   241  	gvk := asoSecretOwner.GetObjectKind().GroupVersionKind()
   242  	owner := metav1.OwnerReference{
   243  		APIVersion: gvk.GroupVersion().String(),
   244  		Kind:       gvk.Kind,
   245  		Name:       asoSecretOwner.GetName(),
   246  		UID:        asoSecretOwner.GetUID(),
   247  		Controller: ptr.To(true),
   248  	}
   249  
   250  	newASOSecret.OwnerReferences = []metav1.OwnerReference{owner}
   251  
   252  	if err := reconcileAzureSecret(ctx, asos.Client, owner, newASOSecret, cluster.GetName()); err != nil {
   253  		asos.Recorder.Eventf(cluster, corev1.EventTypeWarning, "Error reconciling ASO secret", err.Error())
   254  		return ctrl.Result{}, errors.Wrap(err, "failed to reconcile ASO secret")
   255  	}
   256  
   257  	return ctrl.Result{}, nil
   258  }
   259  
   260  func (asos *ASOSecretReconciler) createSecretFromClusterIdentity(ctx context.Context, clusterIdentity *corev1.ObjectReference, cluster *clusterv1.Cluster, azureClient scope.AzureClients) (*corev1.Secret, error) {
   261  	newASOSecret := &corev1.Secret{
   262  		ObjectMeta: metav1.ObjectMeta{
   263  			Name:      aso.GetASOSecretName(cluster.GetName()),
   264  			Namespace: cluster.GetNamespace(),
   265  			Labels: map[string]string{
   266  				cluster.GetName(): string(infrav1.ResourceLifecycleOwned),
   267  			},
   268  		},
   269  		Data: map[string][]byte{
   270  			asoconfig.AzureSubscriptionID: []byte(azureClient.SubscriptionID()),
   271  		},
   272  	}
   273  
   274  	// if the namespace isn't specified then assume it's in the same namespace as the Cluster's one
   275  	namespace := clusterIdentity.Namespace
   276  	if namespace == "" {
   277  		namespace = cluster.GetNamespace()
   278  	}
   279  	identity := &infrav1.AzureClusterIdentity{}
   280  	key := client.ObjectKey{
   281  		Name:      clusterIdentity.Name,
   282  		Namespace: namespace,
   283  	}
   284  	if err := asos.Get(ctx, key, identity); err != nil {
   285  		return nil, errors.Wrap(err, "failed to retrieve AzureClusterIdentity")
   286  	}
   287  
   288  	newASOSecret.Data[asoconfig.AzureTenantID] = []byte(identity.Spec.TenantID)
   289  	newASOSecret.Data[asoconfig.AzureClientID] = []byte(identity.Spec.ClientID)
   290  
   291  	// If the identity type is WorkloadIdentity or UserAssignedMSI, then we don't need to fetch the secret so return early
   292  	if identity.Spec.Type == infrav1.WorkloadIdentity {
   293  		newASOSecret.Data[asoconfig.AuthMode] = []byte(asoconfig.WorkloadIdentityAuthMode)
   294  		return newASOSecret, nil
   295  	}
   296  	if identity.Spec.Type == infrav1.UserAssignedMSI {
   297  		newASOSecret.Data[asoconfig.AuthMode] = []byte(asoconfig.PodIdentityAuthMode)
   298  		return newASOSecret, nil
   299  	}
   300  
   301  	// Fetch identity secret, if it exists
   302  	key = types.NamespacedName{
   303  		Namespace: identity.Spec.ClientSecret.Namespace,
   304  		Name:      identity.Spec.ClientSecret.Name,
   305  	}
   306  	identitySecret := &corev1.Secret{}
   307  	err := asos.Get(ctx, key, identitySecret)
   308  	if err != nil {
   309  		return nil, errors.Wrap(err, "failed to fetch AzureClusterIdentity secret")
   310  	}
   311  
   312  	switch identity.Spec.Type {
   313  	case infrav1.ServicePrincipal, infrav1.ManualServicePrincipal:
   314  		newASOSecret.Data[asoconfig.AzureClientSecret] = identitySecret.Data[scope.AzureSecretKey]
   315  	case infrav1.ServicePrincipalCertificate:
   316  		newASOSecret.Data[asoconfig.AzureClientCertificate] = identitySecret.Data["certificate"]
   317  		newASOSecret.Data[asoconfig.AzureClientCertificatePassword] = identitySecret.Data["password"]
   318  	}
   319  	return newASOSecret, nil
   320  }