|
| 1 | +/* |
| 2 | +Copyright 2024 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 clusterctl |
| 18 | + |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "fmt" |
| 22 | + "strings" |
| 23 | + "time" |
| 24 | + |
| 25 | + "github.com/pkg/errors" |
| 26 | + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" |
| 27 | + corev1 "k8s.io/api/core/v1" |
| 28 | + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" |
| 29 | + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| 30 | + "k8s.io/apimachinery/pkg/types" |
| 31 | + kerrors "k8s.io/apimachinery/pkg/util/errors" |
| 32 | + "k8s.io/apimachinery/pkg/util/rand" |
| 33 | + "k8s.io/apimachinery/pkg/util/wait" |
| 34 | + "sigs.k8s.io/controller-runtime/pkg/client" |
| 35 | + |
| 36 | + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" |
| 37 | +) |
| 38 | + |
| 39 | +const certManagerCAAnnotation = "cert-manager.io/inject-ca-from" |
| 40 | + |
| 41 | +func verifyCAInjection(ctx context.Context, c client.Client) error { |
| 42 | + v := newCAInjectionVerifier(c) |
| 43 | + |
| 44 | + errs := []error{} |
| 45 | + errs = append(errs, v.verifyCustomResourceDefinitions(ctx)...) |
| 46 | + errs = append(errs, v.verifyMutatingWebhookConfigurations(ctx)...) |
| 47 | + errs = append(errs, v.verifyValidatingWebhookConfigurations(ctx)...) |
| 48 | + |
| 49 | + return kerrors.NewAggregate(errs) |
| 50 | +} |
| 51 | + |
| 52 | +// certificateInjectionVerifier waits for cert-managers ca-injector to inject the |
| 53 | +// referred CA certificate to all CRDs, MutatingWebhookConfigurations and |
| 54 | +// ValidatingWebhookConfigurations. |
| 55 | +// As long as the correct CA certificates are not injected the kube-apiserver will |
| 56 | +// reject the requests due to certificate verification errors. |
| 57 | +type certificateInjectionVerifier struct { |
| 58 | + Client client.Client |
| 59 | +} |
| 60 | + |
| 61 | +// newCAInjectionVerifier creates a new CRD migrator. |
| 62 | +func newCAInjectionVerifier(client client.Client) *certificateInjectionVerifier { |
| 63 | + return &certificateInjectionVerifier{ |
| 64 | + Client: client, |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +func (c *certificateInjectionVerifier) verifyCustomResourceDefinitions(ctx context.Context) []error { |
| 69 | + crds := &apiextensionsv1.CustomResourceDefinitionList{} |
| 70 | + if err := c.Client.List(ctx, crds, client.HasLabels{clusterv1.ProviderNameLabel}); err != nil { |
| 71 | + return []error{err} |
| 72 | + } |
| 73 | + |
| 74 | + errs := []error{} |
| 75 | + for i := range crds.Items { |
| 76 | + crd := crds.Items[i] |
| 77 | + ca, err := c.getCACertificateFor(ctx, &crd) |
| 78 | + if err != nil { |
| 79 | + errs = append(errs, err) |
| 80 | + continue |
| 81 | + } |
| 82 | + |
| 83 | + if crd.Spec.Conversion.Webhook == nil || crd.Spec.Conversion.Webhook.ClientConfig == nil { |
| 84 | + continue |
| 85 | + } |
| 86 | + |
| 87 | + if string(crd.Spec.Conversion.Webhook.ClientConfig.CABundle) != ca { |
| 88 | + errs = append(errs, fmt.Errorf("injected CA for CustomResourceDefinition %s does not match", crd.Name)) |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + return errs |
| 93 | +} |
| 94 | + |
| 95 | +func (c *certificateInjectionVerifier) verifyMutatingWebhookConfigurations(ctx context.Context) []error { |
| 96 | + mutateHooks := &admissionregistrationv1.MutatingWebhookConfigurationList{} |
| 97 | + if err := c.Client.List(ctx, mutateHooks, client.HasLabels{clusterv1.ProviderNameLabel}); err != nil { |
| 98 | + return []error{err} |
| 99 | + } |
| 100 | + |
| 101 | + errs := []error{} |
| 102 | + for i := range mutateHooks.Items { |
| 103 | + mutateHook := mutateHooks.Items[i] |
| 104 | + ca, err := c.getCACertificateFor(ctx, &mutateHook) |
| 105 | + if err != nil { |
| 106 | + errs = append(errs, err) |
| 107 | + continue |
| 108 | + } |
| 109 | + var changed bool |
| 110 | + for _, webhook := range mutateHook.Webhooks { |
| 111 | + if string(webhook.ClientConfig.CABundle) != ca { |
| 112 | + changed = true |
| 113 | + errs = append(errs, fmt.Errorf("injected CA for MutatingWebhookConfiguration %s does not match", webhook.Name)) |
| 114 | + } |
| 115 | + } |
| 116 | + if changed { |
| 117 | + annotatedHook := mutateHook.DeepCopy() |
| 118 | + annotatedHook.Annotations["cluster-api-force-ca-injection"] = rand.String(10) |
| 119 | + if err := c.Client.Patch(ctx, annotatedHook, client.StrategicMergeFrom(&mutateHook)); err != nil { |
| 120 | + errs = append(errs, err) |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + return errs |
| 126 | +} |
| 127 | + |
| 128 | +func (c *certificateInjectionVerifier) verifyValidatingWebhookConfigurations(ctx context.Context) []error { |
| 129 | + validateHooks := &admissionregistrationv1.ValidatingWebhookConfigurationList{} |
| 130 | + if err := c.Client.List(ctx, validateHooks, client.HasLabels{clusterv1.ProviderNameLabel}); err != nil { |
| 131 | + return []error{err} |
| 132 | + } |
| 133 | + |
| 134 | + errs := []error{} |
| 135 | + for i := range validateHooks.Items { |
| 136 | + validateHook := validateHooks.Items[i] |
| 137 | + ca, err := c.getCACertificateFor(ctx, &validateHook) |
| 138 | + if err != nil { |
| 139 | + errs = append(errs, err) |
| 140 | + continue |
| 141 | + } |
| 142 | + var changed bool |
| 143 | + for _, webhook := range validateHook.Webhooks { |
| 144 | + if string(webhook.ClientConfig.CABundle) != ca { |
| 145 | + changed = true |
| 146 | + errs = append(errs, fmt.Errorf("injected CA for ValidatingWebhookConfiguration %s does not match", webhook.Name)) |
| 147 | + } |
| 148 | + } |
| 149 | + if changed { |
| 150 | + annotatedHook := validateHook.DeepCopy() |
| 151 | + annotatedHook.Annotations["cluster-api-force-ca-injection"] = rand.String(10) |
| 152 | + if err := c.Client.Patch(ctx, annotatedHook, client.StrategicMergeFrom(&validateHook)); err != nil { |
| 153 | + errs = append(errs, err) |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + return errs |
| 159 | +} |
| 160 | + |
| 161 | +// getCACertificateFor returns the ca certificate from the secret referred by the |
| 162 | +// Certificate object. It reads the namespaced name of the Certificate from the |
| 163 | +// injection annotation of the passed object. |
| 164 | +func (c *certificateInjectionVerifier) getCACertificateFor(ctx context.Context, obj client.Object) (string, error) { |
| 165 | + annotationValue, ok := obj.GetAnnotations()[certManagerCAAnnotation] |
| 166 | + if !ok || annotationValue == "" { |
| 167 | + return "", fmt.Errorf("getting value for injection annotation") |
| 168 | + } |
| 169 | + |
| 170 | + certificateObjKey := splitObjectKey(annotationValue) |
| 171 | + |
| 172 | + certificate := &unstructured.Unstructured{} |
| 173 | + certificate.SetKind("Certificate") |
| 174 | + certificate.SetAPIVersion("cert-manager.io/v1") |
| 175 | + |
| 176 | + if err := c.Client.Get(ctx, certificateObjKey, certificate); err != nil { |
| 177 | + return "", errors.Wrapf(err, "getting certificate %s", certificateObjKey) |
| 178 | + } |
| 179 | + |
| 180 | + secretName, _, err := unstructured.NestedString(certificate.Object, "spec", "secretName") |
| 181 | + if err != nil || secretName == "" { |
| 182 | + return "", errors.Wrapf(err, "reading .spec.secretName name from certificate %s", certificateObjKey) |
| 183 | + } |
| 184 | + |
| 185 | + secretObjKey := client.ObjectKey{Namespace: certificate.GetNamespace(), Name: secretName} |
| 186 | + certificateSecret := &corev1.Secret{} |
| 187 | + if err := c.Client.Get(ctx, secretObjKey, certificateSecret); err != nil { |
| 188 | + return "", errors.Wrapf(err, "getting secret %s for certificate %s", secretObjKey, certificateObjKey) |
| 189 | + } |
| 190 | + |
| 191 | + ca, ok := certificateSecret.Data["ca.crt"] |
| 192 | + if !ok { |
| 193 | + return "", errors.Errorf("data for \"ca.crt\" not found in secret %s", secretObjKey) |
| 194 | + } |
| 195 | + |
| 196 | + return string(ca), nil |
| 197 | +} |
| 198 | + |
| 199 | +// splitObjectKey splits the string by the name separator and returns it as client.ObjectKey. |
| 200 | +func splitObjectKey(nameStr string) client.ObjectKey { |
| 201 | + splitPoint := strings.IndexRune(nameStr, types.Separator) |
| 202 | + if splitPoint == -1 { |
| 203 | + return client.ObjectKey{Name: nameStr} |
| 204 | + } |
| 205 | + return client.ObjectKey{Namespace: nameStr[:splitPoint], Name: nameStr[splitPoint+1:]} |
| 206 | +} |
| 207 | + |
| 208 | +// newVerifyBackoff creates a new API Machinery backoff parameter set suitable for use with clusterctl verify operations. |
| 209 | +func newVerifyBackoff() wait.Backoff { |
| 210 | + // Return a exponential backoff configuration which returns durations for a total time of ~5m. |
| 211 | + // Example: 0, .5s, 1.2s, 2s, 3.1s, 4.5s, 6.4s, 9s, 12s, 16s, 21s, 28s, ... |
| 212 | + // Jitter is added as a random fraction of the duration multiplied by the jitter factor. |
| 213 | + return wait.Backoff{ |
| 214 | + Duration: 500 * time.Millisecond, |
| 215 | + Factor: 1.2, |
| 216 | + Steps: 30, |
| 217 | + Jitter: 0.4, |
| 218 | + } |
| 219 | +} |
0 commit comments