sigs.k8s.io/cluster-api@v1.6.3/exp/runtime/internal/controllers/extensionconfig_controller.go (about) 1 /* 2 Copyright 2022 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 "strings" 23 24 "github.com/pkg/errors" 25 corev1 "k8s.io/api/core/v1" 26 apierrors "k8s.io/apimachinery/pkg/api/errors" 27 "k8s.io/apimachinery/pkg/types" 28 kerrors "k8s.io/apimachinery/pkg/util/errors" 29 ctrl "sigs.k8s.io/controller-runtime" 30 "sigs.k8s.io/controller-runtime/pkg/client" 31 "sigs.k8s.io/controller-runtime/pkg/controller" 32 "sigs.k8s.io/controller-runtime/pkg/handler" 33 "sigs.k8s.io/controller-runtime/pkg/reconcile" 34 35 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 36 runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" 37 tlog "sigs.k8s.io/cluster-api/internal/log" 38 runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client" 39 "sigs.k8s.io/cluster-api/util/annotations" 40 "sigs.k8s.io/cluster-api/util/conditions" 41 "sigs.k8s.io/cluster-api/util/patch" 42 "sigs.k8s.io/cluster-api/util/predicates" 43 ) 44 45 const ( 46 // tlsCAKey is used as a data key in Secret resources to store a CA certificate. 47 tlsCAKey = "ca.crt" 48 ) 49 50 // +kubebuilder:rbac:groups=runtime.cluster.x-k8s.io,resources=extensionconfigs;extensionconfigs/status,verbs=get;list;watch;patch;update 51 // +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch 52 53 // Reconciler reconciles an ExtensionConfig object. 54 type Reconciler struct { 55 Client client.Client 56 APIReader client.Reader 57 RuntimeClient runtimeclient.Client 58 // WatchFilterValue is the label value used to filter events prior to reconciliation. 59 WatchFilterValue string 60 } 61 62 func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { 63 err := ctrl.NewControllerManagedBy(mgr). 64 For(&runtimev1.ExtensionConfig{}). 65 WatchesMetadata( 66 &corev1.Secret{}, 67 handler.EnqueueRequestsFromMapFunc(r.secretToExtensionConfig), 68 ). 69 WithOptions(options). 70 WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)). 71 Complete(r) 72 if err != nil { 73 return errors.Wrap(err, "failed setting up with a controller manager") 74 } 75 76 if err := indexByExtensionInjectCAFromSecretName(ctx, mgr); err != nil { 77 return errors.Wrap(err, "failed setting up with a controller manager") 78 } 79 80 // warmupRunnable will attempt to sync the RuntimeSDK registry with existing ExtensionConfig objects to ensure extensions 81 // are discovered before controllers begin reconciling. 82 err = mgr.Add(&warmupRunnable{ 83 Client: r.Client, 84 APIReader: r.APIReader, 85 RuntimeClient: r.RuntimeClient, 86 }) 87 if err != nil { 88 return errors.Wrap(err, "failed adding warmupRunnable to controller manager") 89 } 90 return nil 91 } 92 93 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 94 var errs []error 95 log := ctrl.LoggerFrom(ctx) 96 97 // Requeue events when the registry is not ready. 98 // The registry will become ready after it is 'warmed up' by warmupRunnable. 99 if !r.RuntimeClient.IsReady() { 100 return ctrl.Result{Requeue: true}, nil 101 } 102 103 extensionConfig := &runtimev1.ExtensionConfig{} 104 err := r.Client.Get(ctx, req.NamespacedName, extensionConfig) 105 if err != nil { 106 if apierrors.IsNotFound(err) { 107 // ExtensionConfig not found. Remove from registry. 108 // First we need to add Namespace/Name to empty ExtensionConfig object. 109 extensionConfig.Name = req.Name 110 extensionConfig.Namespace = req.Namespace 111 return r.reconcileDelete(ctx, extensionConfig) 112 } 113 // Error reading the object - requeue the request. 114 return ctrl.Result{}, err 115 } 116 117 // Return early if the ExtensionConfig is paused. 118 if annotations.HasPaused(extensionConfig) { 119 log.Info("Reconciliation is paused for this object") 120 return ctrl.Result{}, nil 121 } 122 123 // Handle deletion reconciliation loop. 124 if !extensionConfig.ObjectMeta.DeletionTimestamp.IsZero() { 125 return r.reconcileDelete(ctx, extensionConfig) 126 } 127 128 // Copy to avoid modifying the original extensionConfig. 129 original := extensionConfig.DeepCopy() 130 131 // Inject CABundle from secret if annotation is set. Otherwise https calls may fail. 132 if err := reconcileCABundle(ctx, r.Client, extensionConfig); err != nil { 133 return ctrl.Result{}, err 134 } 135 136 // discoverExtensionConfig will return a discovered ExtensionConfig with the appropriate conditions. 137 discoveredExtensionConfig, err := discoverExtensionConfig(ctx, r.RuntimeClient, extensionConfig) 138 if err != nil { 139 errs = append(errs, err) 140 } 141 142 // Always patch the ExtensionConfig as it may contain updates in conditions or clientConfig.caBundle. 143 if err = patchExtensionConfig(ctx, r.Client, original, discoveredExtensionConfig); err != nil { 144 errs = append(errs, err) 145 } 146 147 if len(errs) != 0 { 148 return ctrl.Result{}, kerrors.NewAggregate(errs) 149 } 150 151 // Register the ExtensionConfig if it was found and patched without error. 152 log.Info("Registering ExtensionConfig information into registry") 153 if err = r.RuntimeClient.Register(discoveredExtensionConfig); err != nil { 154 return ctrl.Result{}, errors.Wrapf(err, "failed to register ExtensionConfig %s/%s", extensionConfig.Namespace, extensionConfig.Name) 155 } 156 return ctrl.Result{}, nil 157 } 158 159 func patchExtensionConfig(ctx context.Context, client client.Client, original, modified *runtimev1.ExtensionConfig, options ...patch.Option) error { 160 patchHelper, err := patch.NewHelper(original, client) 161 if err != nil { 162 return errors.Wrapf(err, "failed to create patch helper for %s", tlog.KObj{Obj: modified}) 163 } 164 165 options = append(options, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ 166 runtimev1.RuntimeExtensionDiscoveredCondition, 167 }}) 168 err = patchHelper.Patch(ctx, modified, options...) 169 if err != nil { 170 return errors.Wrapf(err, "failed to patch %s", tlog.KObj{Obj: modified}) 171 } 172 return nil 173 } 174 175 // reconcileDelete will remove the ExtensionConfig from the registry on deletion of the object. Note this is a best 176 // effort deletion that may not catch all cases. 177 func (r *Reconciler) reconcileDelete(ctx context.Context, extensionConfig *runtimev1.ExtensionConfig) (ctrl.Result, error) { 178 log := ctrl.LoggerFrom(ctx) 179 log.Info("Unregistering ExtensionConfig information from registry") 180 if err := r.RuntimeClient.Unregister(extensionConfig); err != nil { 181 return ctrl.Result{}, errors.Wrapf(err, "failed to unregister %s", tlog.KObj{Obj: extensionConfig}) 182 } 183 return ctrl.Result{}, nil 184 } 185 186 // secretToExtensionConfig maps a secret to ExtensionConfigs with the corresponding InjectCAFromSecretAnnotation 187 // to reconcile them on updates of the secrets. 188 func (r *Reconciler) secretToExtensionConfig(ctx context.Context, secret client.Object) []reconcile.Request { 189 result := []ctrl.Request{} 190 191 extensionConfigs := runtimev1.ExtensionConfigList{} 192 indexKey := secret.GetNamespace() + "/" + secret.GetName() 193 194 if err := r.Client.List( 195 ctx, 196 &extensionConfigs, 197 client.MatchingFields{injectCAFromSecretAnnotationField: indexKey}, 198 ); err != nil { 199 return nil 200 } 201 202 for _, ext := range extensionConfigs.Items { 203 result = append(result, ctrl.Request{NamespacedName: client.ObjectKey{Name: ext.Name}}) 204 } 205 206 return result 207 } 208 209 // discoverExtensionConfig attempts to discover the Handlers for an ExtensionConfig. 210 // If discovery succeeds it returns the ExtensionConfig with Handlers updated in Status and an updated Condition. 211 // If discovery fails it returns the ExtensionConfig with no update to Handlers and a Failed Condition. 212 func discoverExtensionConfig(ctx context.Context, runtimeClient runtimeclient.Client, extensionConfig *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) { 213 discoveredExtension, err := runtimeClient.Discover(ctx, extensionConfig.DeepCopy()) 214 if err != nil { 215 modifiedExtensionConfig := extensionConfig.DeepCopy() 216 conditions.MarkFalse(modifiedExtensionConfig, runtimev1.RuntimeExtensionDiscoveredCondition, runtimev1.DiscoveryFailedReason, clusterv1.ConditionSeverityError, "error in discovery: %v", err) 217 return modifiedExtensionConfig, errors.Wrapf(err, "failed to discover %s", tlog.KObj{Obj: extensionConfig}) 218 } 219 220 conditions.MarkTrue(discoveredExtension, runtimev1.RuntimeExtensionDiscoveredCondition) 221 return discoveredExtension, nil 222 } 223 224 // reconcileCABundle reconciles the CA bundle for the ExtensionConfig. 225 // Note: This was implemented to behave similar to the cert-manager cainjector. 226 // We couldn't use the cert-manager cainjector because it doesn't work with CustomResources. 227 func reconcileCABundle(ctx context.Context, client client.Client, config *runtimev1.ExtensionConfig) error { 228 log := ctrl.LoggerFrom(ctx) 229 230 secretNameRaw, ok := config.Annotations[runtimev1.InjectCAFromSecretAnnotation] 231 if !ok { 232 return nil 233 } 234 secretName := splitNamespacedName(secretNameRaw) 235 236 log.Info(fmt.Sprintf("Injecting CA Bundle into ExtensionConfig from secret %q", secretNameRaw)) 237 238 if secretName.Namespace == "" || secretName.Name == "" { 239 return errors.Errorf("failed to reconcile caBundle: secret name %q must be in the form <namespace>/<name>", secretNameRaw) 240 } 241 242 var secret corev1.Secret 243 // Note: this is an expensive API call because secrets are explicitly not cached. 244 if err := client.Get(ctx, secretName, &secret); err != nil { 245 return errors.Wrapf(err, "failed to reconcile caBundle: failed to get secret %q", secretNameRaw) 246 } 247 248 caData, hasCAData := secret.Data[tlsCAKey] 249 if !hasCAData { 250 return errors.Errorf("failed to reconcile caBundle: secret %s does not contain a %q entry", secretNameRaw, tlsCAKey) 251 } 252 253 config.Spec.ClientConfig.CABundle = caData 254 return nil 255 } 256 257 // splitNamespacedName turns the string form of a namespaced name 258 // (<namespace>/<name>) into a types.NamespacedName. 259 func splitNamespacedName(nameStr string) types.NamespacedName { 260 splitPoint := strings.IndexRune(nameStr, types.Separator) 261 if splitPoint == -1 { 262 return types.NamespacedName{Name: nameStr} 263 } 264 return types.NamespacedName{Namespace: nameStr[:splitPoint], Name: nameStr[splitPoint+1:]} 265 }