Skip to content

Commit 7755426

Browse files
authored
Merge pull request #10469 from chrischdi/pr-cert-manager-fix-x509-errors
🐛 clusterctl: ensure cert-manager objects get applied before other provider objects
2 parents 63f1482 + b239b4c commit 7755426

File tree

4 files changed

+269
-2
lines changed

4 files changed

+269
-2
lines changed

cmd/clusterctl/client/repository/components.go

+17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package repository
1818

1919
import (
2020
"fmt"
21+
"sort"
2122
"strings"
2223

2324
"github.com/pkg/errors"
@@ -259,6 +260,22 @@ func NewComponents(input ComponentsInput) (Components, error) {
259260
// Add common labels.
260261
objs = addCommonLabels(objs, input.Provider)
261262

263+
// Deploying cert-manager objects and especially Certificates before Mutating-
264+
// ValidatingWebhookConfigurations and CRDs ensures cert-manager's ca-injector
265+
// receives the event for the objects at the right time to inject the new CA.
266+
sort.SliceStable(objs, func(i, j int) bool {
267+
// First prioritize Namespaces over everything.
268+
if objs[i].GetKind() == "Namespace" {
269+
return true
270+
}
271+
if objs[j].GetKind() == "Namespace" {
272+
return false
273+
}
274+
275+
// Second prioritize cert-manager objects.
276+
return objs[i].GroupVersionKind().Group == "cert-manager.io"
277+
})
278+
262279
return &components{
263280
Provider: input.Provider,
264281
version: input.Options.Version,
+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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+
}

test/framework/clusterctl/clusterctl_helpers.go

+23-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"path/filepath"
2424
"time"
2525

26+
"github.com/blang/semver/v4"
2627
. "github.com/onsi/gomega"
2728
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829
"k8s.io/klog/v2"
@@ -95,6 +96,16 @@ func InitManagementClusterAndWatchControllerLogs(ctx context.Context, input Init
9596

9697
if input.ClusterctlBinaryPath != "" {
9798
InitWithBinary(ctx, input.ClusterctlBinaryPath, initInput)
99+
// Old versions of clusterctl may deploy CRDs, Mutating- and/or ValidatingWebhookConfigurations
100+
// before creating the new Certificate objects. This check ensures the CA's are up to date before
101+
// continuing.
102+
clusterctlVersion, err := getClusterCtlVersion(input.ClusterctlBinaryPath)
103+
Expect(err).ToNot(HaveOccurred())
104+
if clusterctlVersion.LT(semver.MustParse("1.7.2")) {
105+
Eventually(func() error {
106+
return verifyCAInjection(ctx, client)
107+
}, time.Minute*5, time.Second*10).Should(Succeed(), "Failed to verify CA injection")
108+
}
98109
} else {
99110
Init(ctx, initInput)
100111
}
@@ -183,14 +194,24 @@ func UpgradeManagementClusterAndWait(ctx context.Context, input UpgradeManagemen
183194
LogFolder: input.LogFolder,
184195
}
185196

197+
client := input.ClusterProxy.GetClient()
198+
186199
if input.ClusterctlBinaryPath != "" {
187200
UpgradeWithBinary(ctx, input.ClusterctlBinaryPath, upgradeInput)
201+
// Old versions of clusterctl may deploy CRDs, Mutating- and/or ValidatingWebhookConfigurations
202+
// before creating the new Certificate objects. This check ensures the CA's are up to date before
203+
// continuing.
204+
clusterctlVersion, err := getClusterCtlVersion(input.ClusterctlBinaryPath)
205+
Expect(err).ToNot(HaveOccurred())
206+
if clusterctlVersion.LT(semver.MustParse("1.7.2")) {
207+
Eventually(func() error {
208+
return verifyCAInjection(ctx, client)
209+
}, time.Minute*5, time.Second*10).Should(Succeed(), "Failed to verify CA injection")
210+
}
188211
} else {
189212
Upgrade(ctx, upgradeInput)
190213
}
191214

192-
client := input.ClusterProxy.GetClient()
193-
194215
log.Logf("Waiting for provider controllers to be running")
195216
controllersDeployments := framework.GetControllerDeployments(ctx, framework.GetControllerDeploymentsInput{
196217
Lister: client,

test/framework/convenience.go

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package framework
1919
import (
2020
"reflect"
2121

22+
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
2223
appsv1 "k8s.io/api/apps/v1"
2324
coordinationv1 "k8s.io/api/coordination/v1"
2425
corev1 "k8s.io/api/core/v1"
@@ -71,6 +72,9 @@ func TryAddDefaultSchemes(scheme *runtime.Scheme) {
7172
_ = apiextensionsv1beta.AddToScheme(scheme)
7273
_ = apiextensionsv1.AddToScheme(scheme)
7374

75+
// Add the admission registration scheme (Mutating-, ValidatingWebhookConfiguration).
76+
_ = admissionregistrationv1.AddToScheme(scheme)
77+
7478
// Add RuntimeSDK to the scheme.
7579
_ = runtimev1.AddToScheme(scheme)
7680

0 commit comments

Comments
 (0)