Skip to content

Commit e3e6558

Browse files
committed
test: wait for ca injection in case we use an external clusterctl binary for upgrading
1 parent d79cfee commit e3e6558

File tree

2 files changed

+234
-2
lines changed

2 files changed

+234
-2
lines changed
+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
}

test/framework/clusterctl/clusterctl_helpers.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ func InitManagementClusterAndWatchControllerLogs(ctx context.Context, input Init
9595

9696
if input.ClusterctlBinaryPath != "" {
9797
InitWithBinary(ctx, input.ClusterctlBinaryPath, initInput)
98+
// Old versions of clusterctl may deploy CRDs, Mutating- and/or ValidatingWebhookConfigurations
99+
// before creating the new Certificate objects. This check ensures the CA's are up to date before
100+
// continuing.
101+
Eventually(func() error {
102+
return verifyCAInjection(ctx, client)
103+
}, time.Minute*5, time.Second*10).Should(Succeed(), "Failed to verify CA injection")
104+
98105
} else {
99106
Init(ctx, initInput)
100107
}
@@ -183,14 +190,20 @@ func UpgradeManagementClusterAndWait(ctx context.Context, input UpgradeManagemen
183190
LogFolder: input.LogFolder,
184191
}
185192

193+
client := input.ClusterProxy.GetClient()
194+
186195
if input.ClusterctlBinaryPath != "" {
187196
UpgradeWithBinary(ctx, input.ClusterctlBinaryPath, upgradeInput)
197+
// Old versions of clusterctl may deploy CRDs, Mutating- and/or ValidatingWebhookConfigurations
198+
// before creating the new Certificate objects. This check ensures the CA's are up to date before
199+
// continuing.
200+
Eventually(func() error {
201+
return verifyCAInjection(ctx, client)
202+
}, time.Minute*5, time.Second*10).Should(Succeed(), "Failed to verify CA injection")
188203
} else {
189204
Upgrade(ctx, upgradeInput)
190205
}
191206

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

0 commit comments

Comments
 (0)