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 }