sigs.k8s.io/cluster-api-provider-aws@v1.5.5/bootstrap/eks/controllers/eksconfig_controller.go (about) 1 /* 2 Copyright 2020 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 "bytes" 21 "context" 22 "fmt" 23 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/runtime" 29 "k8s.io/utils/pointer" 30 ctrl "sigs.k8s.io/controller-runtime" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/controller-runtime/pkg/controller" 33 "sigs.k8s.io/controller-runtime/pkg/handler" 34 "sigs.k8s.io/controller-runtime/pkg/source" 35 36 eksbootstrapv1 "sigs.k8s.io/cluster-api-provider-aws/bootstrap/eks/api/v1beta1" 37 "sigs.k8s.io/cluster-api-provider-aws/bootstrap/eks/internal/userdata" 38 ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/controlplane/eks/api/v1beta1" 39 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 40 bsutil "sigs.k8s.io/cluster-api/bootstrap/util" 41 expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 42 "sigs.k8s.io/cluster-api/feature" 43 "sigs.k8s.io/cluster-api/util" 44 "sigs.k8s.io/cluster-api/util/annotations" 45 "sigs.k8s.io/cluster-api/util/conditions" 46 "sigs.k8s.io/cluster-api/util/patch" 47 "sigs.k8s.io/cluster-api/util/predicates" 48 ) 49 50 // EKSConfigReconciler reconciles a EKSConfig object. 51 type EKSConfigReconciler struct { 52 client.Client 53 Scheme *runtime.Scheme 54 WatchFilterValue string 55 } 56 57 // +kubebuilder:rbac:groups=bootstrap.cluster.x-k8s.io,resources=eksconfigs,verbs=get;list;watch;create;update;patch;delete 58 // +kubebuilder:rbac:groups=bootstrap.cluster.x-k8s.io,resources=eksconfigs/status,verbs=get;update;patch 59 // +kubebuilder:rbac:groups=controlplane.cluster.x-k8s.io,resources=awsmanagedcontrolplanes,verbs=get;list;watch 60 // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines;machinepools;clusters,verbs=get;list;watch 61 // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinepools,verbs=get;list;watch 62 // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete; 63 64 func (r *EKSConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, rerr error) { 65 log := ctrl.LoggerFrom(ctx) 66 67 // get EKSConfig 68 config := &eksbootstrapv1.EKSConfig{} 69 if err := r.Client.Get(ctx, req.NamespacedName, config); err != nil { 70 if apierrors.IsNotFound(err) { 71 return ctrl.Result{}, nil 72 } 73 log.Error(err, "Failed to get config") 74 return ctrl.Result{}, err 75 } 76 77 // check owner references and look up owning Machine object 78 configOwner, err := bsutil.GetConfigOwner(ctx, r.Client, config) 79 if apierrors.IsNotFound(err) { 80 // no error here, requeue until we find an owner 81 return ctrl.Result{}, nil 82 } 83 if err != nil { 84 log.Error(err, "Failed to get owner") 85 return ctrl.Result{}, err 86 } 87 if configOwner == nil { 88 // no error, requeue until we find an owner 89 return ctrl.Result{}, nil 90 } 91 92 log = log.WithValues(configOwner.GetKind(), configOwner.GetName()) 93 94 cluster, err := util.GetClusterByName(ctx, r.Client, configOwner.GetNamespace(), configOwner.ClusterName()) 95 if err != nil { 96 if errors.Is(err, util.ErrNoCluster) { 97 log.Info("EKSConfig does not belong to a cluster yet, re-queuing until it's partof a cluster") 98 return ctrl.Result{}, nil 99 } 100 if apierrors.IsNotFound(err) { 101 log.Info("Cluster does not exist yet, re-queueing until it is created") 102 return ctrl.Result{}, nil 103 } 104 log.Error(err, "Could not get cluster with metadata") 105 return ctrl.Result{}, err 106 } 107 log = log.WithValues("cluster", cluster.Name) 108 109 if annotations.IsPaused(cluster, config) { 110 log.Info("Reconciliation is paused for this object") 111 return ctrl.Result{}, nil 112 } 113 114 patchHelper, err := patch.NewHelper(config, r.Client) 115 if err != nil { 116 return ctrl.Result{}, err 117 } 118 119 // set up defer block for updating config 120 defer func() { 121 conditions.SetSummary(config, 122 conditions.WithConditions( 123 eksbootstrapv1.DataSecretAvailableCondition, 124 ), 125 conditions.WithStepCounter(), 126 ) 127 128 patchOpts := []patch.Option{} 129 if rerr == nil { 130 patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{}) 131 } 132 if err := patchHelper.Patch(ctx, config, patchOpts...); err != nil { 133 log.Error(rerr, "Failed to patch config") 134 if rerr == nil { 135 rerr = err 136 } 137 } 138 }() 139 140 return r.joinWorker(ctx, cluster, config) 141 } 142 143 func (r *EKSConfigReconciler) joinWorker(ctx context.Context, cluster *clusterv1.Cluster, config *eksbootstrapv1.EKSConfig) (ctrl.Result, error) { 144 log := ctrl.LoggerFrom(ctx) 145 146 if config.Status.DataSecretName != nil { 147 secretKey := client.ObjectKey{Namespace: config.Namespace, Name: *config.Status.DataSecretName} 148 log = log.WithValues("data-secret-name", secretKey.Name) 149 existingSecret := &corev1.Secret{} 150 151 // No error here means the Secret exists and we have no 152 // reason to proceed. 153 err := r.Client.Get(ctx, secretKey, existingSecret) 154 switch { 155 case err == nil: 156 return ctrl.Result{}, nil 157 case !apierrors.IsNotFound(err): 158 log.Error(err, "unable to check for existing bootstrap secret") 159 return ctrl.Result{}, err 160 } 161 } 162 163 if cluster.Spec.ControlPlaneRef == nil || cluster.Spec.ControlPlaneRef.Kind != "AWSManagedControlPlane" { 164 return ctrl.Result{}, errors.New("Cluster's controlPlaneRef needs to be an AWSManagedControlPlane in order to use the EKS bootstrap provider") 165 } 166 167 if !cluster.Status.InfrastructureReady { 168 log.Info("Cluster infrastructure is not ready") 169 conditions.MarkFalse(config, 170 eksbootstrapv1.DataSecretAvailableCondition, 171 eksbootstrapv1.WaitingForClusterInfrastructureReason, 172 clusterv1.ConditionSeverityInfo, "") 173 return ctrl.Result{}, nil 174 } 175 176 if !conditions.IsTrue(cluster, clusterv1.ControlPlaneInitializedCondition) { 177 log.Info("Control Plane has not yet been initialized") 178 conditions.MarkFalse(config, eksbootstrapv1.DataSecretAvailableCondition, eksbootstrapv1.WaitingForControlPlaneInitializationReason, clusterv1.ConditionSeverityInfo, "") 179 return ctrl.Result{}, nil 180 } 181 182 controlPlane := &ekscontrolplanev1.AWSManagedControlPlane{} 183 if err := r.Get(ctx, client.ObjectKey{Name: cluster.Spec.ControlPlaneRef.Name, Namespace: cluster.Spec.ControlPlaneRef.Namespace}, controlPlane); err != nil { 184 return ctrl.Result{}, err 185 } 186 187 log.Info("Generating userdata") 188 189 nodeInput := &userdata.NodeInput{ 190 // AWSManagedControlPlane webhooks default and validate EKSClusterName 191 ClusterName: controlPlane.Spec.EKSClusterName, 192 KubeletExtraArgs: config.Spec.KubeletExtraArgs, 193 ContainerRuntime: config.Spec.ContainerRuntime, 194 DNSClusterIP: config.Spec.DNSClusterIP, 195 DockerConfigJSON: config.Spec.DockerConfigJSON, 196 APIRetryAttempts: config.Spec.APIRetryAttempts, 197 UseMaxPods: config.Spec.UseMaxPods, 198 } 199 if config.Spec.PauseContainer != nil { 200 nodeInput.PauseContainerAccount = &config.Spec.PauseContainer.AccountNumber 201 nodeInput.PauseContainerVersion = &config.Spec.PauseContainer.Version 202 } 203 // TODO(richardcase): uncomment when we support ipv6 / dual stack 204 /*if config.Spec.ServiceIPV6Cidr != nil && *config.Spec.ServiceIPV6Cidr != "" { 205 nodeInput.ServiceIPV6Cidr = config.Spec.ServiceIPV6Cidr 206 nodeInput.IPFamily = pointer.String("ipv6") 207 }*/ 208 209 // generate userdata 210 userDataScript, err := userdata.NewNode(nodeInput) 211 if err != nil { 212 log.Error(err, "Failed to create a worker join configuration") 213 conditions.MarkFalse(config, eksbootstrapv1.DataSecretAvailableCondition, eksbootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, "") 214 return ctrl.Result{}, err 215 } 216 217 // store userdata as secret 218 if err := r.storeBootstrapData(ctx, cluster, config, userDataScript); err != nil { 219 log.Error(err, "Failed to store bootstrap data") 220 conditions.MarkFalse(config, eksbootstrapv1.DataSecretAvailableCondition, eksbootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, "") 221 return ctrl.Result{}, err 222 } 223 224 return ctrl.Result{}, nil 225 } 226 227 func (r *EKSConfigReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, option controller.Options) error { 228 b := ctrl.NewControllerManagedBy(mgr). 229 For(&eksbootstrapv1.EKSConfig{}). 230 WithOptions(option). 231 WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)). 232 Watches( 233 &source.Kind{Type: &clusterv1.Machine{}}, 234 handler.EnqueueRequestsFromMapFunc(r.MachineToBootstrapMapFunc), 235 ) 236 237 if feature.Gates.Enabled(feature.MachinePool) { 238 b = b.Watches( 239 &source.Kind{Type: &expclusterv1.MachinePool{}}, 240 handler.EnqueueRequestsFromMapFunc(r.MachinePoolToBootstrapMapFunc), 241 ) 242 } 243 244 c, err := b.Build(r) 245 if err != nil { 246 return errors.Wrap(err, "failed setting up with a controller manager") 247 } 248 249 err = c.Watch( 250 &source.Kind{Type: &clusterv1.Cluster{}}, 251 handler.EnqueueRequestsFromMapFunc((r.ClusterToEKSConfigs)), 252 predicates.ClusterUnpausedAndInfrastructureReady(ctrl.LoggerFrom(ctx)), 253 ) 254 if err != nil { 255 return errors.Wrap(err, "failed adding watch for Clusters to controller manager") 256 } 257 258 return nil 259 } 260 261 // storeBootstrapData creates a new secret with the data passed in as input, 262 // sets the reference in the configuration status and ready to true. 263 func (r *EKSConfigReconciler) storeBootstrapData(ctx context.Context, cluster *clusterv1.Cluster, config *eksbootstrapv1.EKSConfig, data []byte) error { 264 log := ctrl.LoggerFrom(ctx) 265 266 // as secret creation and scope.Config status patch are not atomic operations 267 // it is possible that secret creation happens but the config.Status patches are not applied 268 secret := &corev1.Secret{} 269 if err := r.Client.Get(ctx, client.ObjectKey{ 270 Name: config.Name, 271 Namespace: config.Namespace, 272 }, secret); err != nil { 273 if apierrors.IsNotFound(err) { 274 if err := r.createBootstrapSecret(ctx, cluster, config, data); err != nil { 275 return errors.Wrap(err, "failed to create bootstrap data secret for EKSConfig") 276 } 277 log.Info("created bootstrap data secret for EKSConfig", "secret", secret.Name) 278 } else { 279 return errors.Wrap(err, "failed to get data secret for EKSConfig") 280 } 281 } else { 282 updated, err := r.updateBootstrapSecret(ctx, secret, data) 283 if err != nil { 284 return errors.Wrap(err, "failed to update data secret for EKSConfig") 285 } 286 if updated { 287 log.Info("updated bootstrap data secret for EKSConfig", "secret", secret.Name) 288 } else { 289 log.V(4).Info("no change in bootstrap data secret for EKSConfig", "secret", secret.Name) 290 } 291 } 292 293 config.Status.DataSecretName = pointer.StringPtr(secret.Name) 294 config.Status.Ready = true 295 conditions.MarkTrue(config, eksbootstrapv1.DataSecretAvailableCondition) 296 return nil 297 } 298 299 // MachineToBootstrapMapFunc is a handler.ToRequestsFunc to be used to enqueue requests 300 // for EKSConfig reconciliation. 301 func (r *EKSConfigReconciler) MachineToBootstrapMapFunc(o client.Object) []ctrl.Request { 302 result := []ctrl.Request{} 303 304 m, ok := o.(*clusterv1.Machine) 305 if !ok { 306 panic(fmt.Sprintf("Expected a Machine but got a %T", o)) 307 } 308 if m.Spec.Bootstrap.ConfigRef != nil && m.Spec.Bootstrap.ConfigRef.GroupVersionKind() == eksbootstrapv1.GroupVersion.WithKind("EKSConfig") { 309 name := client.ObjectKey{Namespace: m.Namespace, Name: m.Spec.Bootstrap.ConfigRef.Name} 310 result = append(result, ctrl.Request{NamespacedName: name}) 311 } 312 return result 313 } 314 315 // MachinePoolToBootstrapMapFunc is a handler.ToRequestsFunc to be uses to enqueue requests 316 // for EKSConfig reconciliation. 317 func (r *EKSConfigReconciler) MachinePoolToBootstrapMapFunc(o client.Object) []ctrl.Request { 318 result := []ctrl.Request{} 319 320 m, ok := o.(*expclusterv1.MachinePool) 321 if !ok { 322 panic(fmt.Sprintf("Expected a MachinePool but got a %T", o)) 323 } 324 configRef := m.Spec.Template.Spec.Bootstrap.ConfigRef 325 if configRef != nil && configRef.GroupVersionKind().GroupKind() == eksbootstrapv1.GroupVersion.WithKind("EKSConfig").GroupKind() { 326 name := client.ObjectKey{Namespace: m.Namespace, Name: configRef.Name} 327 result = append(result, ctrl.Request{NamespacedName: name}) 328 } 329 330 return result 331 } 332 333 // ClusterToEKSConfigs is a handler.ToRequestsFunc to be used to enqueue requests for 334 // EKSConfig reconciliation. 335 func (r *EKSConfigReconciler) ClusterToEKSConfigs(o client.Object) []ctrl.Request { 336 result := []ctrl.Request{} 337 338 c, ok := o.(*clusterv1.Cluster) 339 if !ok { 340 panic(fmt.Sprintf("Expected a Cluster but got a %T", o)) 341 } 342 343 selectors := []client.ListOption{ 344 client.InNamespace(c.Namespace), 345 client.MatchingLabels{ 346 clusterv1.ClusterLabelName: c.Name, 347 }, 348 } 349 350 machineList := &clusterv1.MachineList{} 351 if err := r.Client.List(context.Background(), machineList, selectors...); err != nil { 352 return nil 353 } 354 355 for _, m := range machineList.Items { 356 if m.Spec.Bootstrap.ConfigRef != nil && 357 m.Spec.Bootstrap.ConfigRef.GroupVersionKind().GroupKind() == eksbootstrapv1.GroupVersion.WithKind("EKSConfig").GroupKind() { 358 name := client.ObjectKey{Namespace: m.Namespace, Name: m.Spec.Bootstrap.ConfigRef.Name} 359 result = append(result, ctrl.Request{NamespacedName: name}) 360 } 361 } 362 363 return result 364 } 365 366 // Create the Secret containing bootstrap userdata. 367 func (r *EKSConfigReconciler) createBootstrapSecret(ctx context.Context, cluster *clusterv1.Cluster, config *eksbootstrapv1.EKSConfig, data []byte) error { 368 secret := &corev1.Secret{ 369 ObjectMeta: metav1.ObjectMeta{ 370 Name: config.Name, 371 Namespace: config.Namespace, 372 Labels: map[string]string{ 373 clusterv1.ClusterLabelName: cluster.Name, 374 }, 375 OwnerReferences: []metav1.OwnerReference{ 376 { 377 APIVersion: eksbootstrapv1.GroupVersion.String(), 378 Kind: "EKSConfig", 379 Name: config.Name, 380 UID: config.UID, 381 Controller: pointer.BoolPtr(true), 382 }, 383 }, 384 }, 385 Data: map[string][]byte{ 386 "value": data, 387 }, 388 Type: clusterv1.ClusterSecretType, 389 } 390 return r.Client.Create(ctx, secret) 391 } 392 393 // Update the userdata in the bootstrap Secret. 394 func (r *EKSConfigReconciler) updateBootstrapSecret(ctx context.Context, secret *corev1.Secret, data []byte) (bool, error) { 395 if !bytes.Equal(secret.Data["value"], data) { 396 secret.Data["value"] = data 397 return true, r.Client.Update(ctx, secret) 398 } 399 return false, nil 400 }