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