sigs.k8s.io/cluster-api@v1.7.1/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 err 163 } 164 165 options = append(options, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ 166 runtimev1.RuntimeExtensionDiscoveredCondition, 167 }}) 168 return patchHelper.Patch(ctx, modified, options...) 169 } 170 171 // reconcileDelete will remove the ExtensionConfig from the registry on deletion of the object. Note this is a best 172 // effort deletion that may not catch all cases. 173 func (r *Reconciler) reconcileDelete(ctx context.Context, extensionConfig *runtimev1.ExtensionConfig) (ctrl.Result, error) { 174 log := ctrl.LoggerFrom(ctx) 175 log.Info("Unregistering ExtensionConfig information from registry") 176 if err := r.RuntimeClient.Unregister(extensionConfig); err != nil { 177 return ctrl.Result{}, errors.Wrapf(err, "failed to unregister %s", tlog.KObj{Obj: extensionConfig}) 178 } 179 return ctrl.Result{}, nil 180 } 181 182 // secretToExtensionConfig maps a secret to ExtensionConfigs with the corresponding InjectCAFromSecretAnnotation 183 // to reconcile them on updates of the secrets. 184 func (r *Reconciler) secretToExtensionConfig(ctx context.Context, secret client.Object) []reconcile.Request { 185 result := []ctrl.Request{} 186 187 extensionConfigs := runtimev1.ExtensionConfigList{} 188 indexKey := secret.GetNamespace() + "/" + secret.GetName() 189 190 if err := r.Client.List( 191 ctx, 192 &extensionConfigs, 193 client.MatchingFields{injectCAFromSecretAnnotationField: indexKey}, 194 ); err != nil { 195 return nil 196 } 197 198 for _, ext := range extensionConfigs.Items { 199 result = append(result, ctrl.Request{NamespacedName: client.ObjectKey{Name: ext.Name}}) 200 } 201 202 return result 203 } 204 205 // discoverExtensionConfig attempts to discover the Handlers for an ExtensionConfig. 206 // If discovery succeeds it returns the ExtensionConfig with Handlers updated in Status and an updated Condition. 207 // If discovery fails it returns the ExtensionConfig with no update to Handlers and a Failed Condition. 208 func discoverExtensionConfig(ctx context.Context, runtimeClient runtimeclient.Client, extensionConfig *runtimev1.ExtensionConfig) (*runtimev1.ExtensionConfig, error) { 209 discoveredExtension, err := runtimeClient.Discover(ctx, extensionConfig.DeepCopy()) 210 if err != nil { 211 modifiedExtensionConfig := extensionConfig.DeepCopy() 212 conditions.MarkFalse(modifiedExtensionConfig, runtimev1.RuntimeExtensionDiscoveredCondition, runtimev1.DiscoveryFailedReason, clusterv1.ConditionSeverityError, "error in discovery: %v", err) 213 return modifiedExtensionConfig, errors.Wrapf(err, "failed to discover %s", tlog.KObj{Obj: extensionConfig}) 214 } 215 216 conditions.MarkTrue(discoveredExtension, runtimev1.RuntimeExtensionDiscoveredCondition) 217 return discoveredExtension, nil 218 } 219 220 // reconcileCABundle reconciles the CA bundle for the ExtensionConfig. 221 // Note: This was implemented to behave similar to the cert-manager cainjector. 222 // We couldn't use the cert-manager cainjector because it doesn't work with CustomResources. 223 func reconcileCABundle(ctx context.Context, client client.Client, config *runtimev1.ExtensionConfig) error { 224 log := ctrl.LoggerFrom(ctx) 225 226 secretNameRaw, ok := config.Annotations[runtimev1.InjectCAFromSecretAnnotation] 227 if !ok { 228 return nil 229 } 230 secretName := splitNamespacedName(secretNameRaw) 231 232 log.Info(fmt.Sprintf("Injecting CA Bundle into ExtensionConfig from secret %q", secretNameRaw)) 233 234 if secretName.Namespace == "" || secretName.Name == "" { 235 return errors.Errorf("failed to reconcile caBundle: secret name %q must be in the form <namespace>/<name>", secretNameRaw) 236 } 237 238 var secret corev1.Secret 239 // Note: this is an expensive API call because secrets are explicitly not cached. 240 if err := client.Get(ctx, secretName, &secret); err != nil { 241 return errors.Wrapf(err, "failed to reconcile caBundle: failed to get secret %q", secretNameRaw) 242 } 243 244 caData, hasCAData := secret.Data[tlsCAKey] 245 if !hasCAData { 246 return errors.Errorf("failed to reconcile caBundle: secret %s does not contain a %q entry", secretNameRaw, tlsCAKey) 247 } 248 249 config.Spec.ClientConfig.CABundle = caData 250 return nil 251 } 252 253 // splitNamespacedName turns the string form of a namespaced name 254 // (<namespace>/<name>) into a types.NamespacedName. 255 func splitNamespacedName(nameStr string) types.NamespacedName { 256 splitPoint := strings.IndexRune(nameStr, types.Separator) 257 if splitPoint == -1 { 258 return types.NamespacedName{Name: nameStr} 259 } 260 return types.NamespacedName{Namespace: nameStr[:splitPoint], Name: nameStr[splitPoint+1:]} 261 }