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 }