Skip to content

Commit 6f8ce15

Browse files
tiffanny29631k8s-publishing-bot
authored andcommitted
Allow white-spaced CABundle during webhook client creation and validation (#132514)
* apiextensions: Treat whitespace-only caBundle as empty for webhook client config and validation - Updates webhookClientConfigForCRD to treat caBundle values containing only whitespace as empty, ensuring system trust roots are used in this case. - Updates ValidateCABundle to treat whitespace-only caBundle as valid, consistent with empty or nil values. - Adds/updates unit tests to verify that whitespace-only caBundle is handled equivalently to empty or nil. - Ensures consistent and user-friendly handling of caBundle across CRD conversion webhook configuration and validation. * Revert validation logic * Add integration test for webhook bypass * Fix linting Kubernetes-commit: a652896307ce8dd1412483ed18e61d6dd2ad36da
1 parent 69bd66b commit 6f8ce15

File tree

5 files changed

+125
-8
lines changed

5 files changed

+125
-8
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ require (
2626
google.golang.org/grpc v1.72.1
2727
google.golang.org/protobuf v1.36.5
2828
gopkg.in/evanphx/json-patch.v4 v4.12.0
29-
k8s.io/api v0.0.0-20250724224534-f2279712f874
29+
k8s.io/api v0.0.0-20250725024534-53dd5462e729
3030
k8s.io/apimachinery v0.0.0-20250724224258-50e39b11cd32
3131
k8s.io/apiserver v0.0.0-20250724230616-e08cc1978f1b
32-
k8s.io/client-go v0.0.0-20250724224906-d4f2d5b8ccf7
32+
k8s.io/client-go v0.0.0-20250725024915-bce2be2f20f3
3333
k8s.io/code-generator v0.0.0-20250724225710-fe7d3af27d90
3434
k8s.io/component-base v0.0.0-20250724225857-f959b0363667
3535
k8s.io/klog/v2 v2.130.1

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,14 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
291291
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
292292
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
293293
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
294-
k8s.io/api v0.0.0-20250724224534-f2279712f874 h1:AzVs/5TvK2r0BSsV33dVm4FLPgJEUybkjfLrEmiA7Ss=
295-
k8s.io/api v0.0.0-20250724224534-f2279712f874/go.mod h1:h7pQu2oCQ3ccFA95Gaxuu7JO+0gHm6uxdKA4dLNL3Y8=
294+
k8s.io/api v0.0.0-20250725024534-53dd5462e729 h1:raeWWJ+/+yxrpn2KAvZgxic537l2Ejpa8nnLaCO7+7Y=
295+
k8s.io/api v0.0.0-20250725024534-53dd5462e729/go.mod h1:h7pQu2oCQ3ccFA95Gaxuu7JO+0gHm6uxdKA4dLNL3Y8=
296296
k8s.io/apimachinery v0.0.0-20250724224258-50e39b11cd32 h1:XbDM37tJSNp0ga7QZkizY+qxKJEA0DFe+nqssKruths=
297297
k8s.io/apimachinery v0.0.0-20250724224258-50e39b11cd32/go.mod h1:mjSAX6740hY31nMwwbFVIcjVaXzV46iZzqDA+UfugZ8=
298298
k8s.io/apiserver v0.0.0-20250724230616-e08cc1978f1b h1:Q295T9ri9A24WwsnAmEf8k+12NVY8qhPUZjsrMqAn8E=
299299
k8s.io/apiserver v0.0.0-20250724230616-e08cc1978f1b/go.mod h1:td8qDjyqmlRNp/inElkMFDWBrHv+YFCYE536yMGqEno=
300-
k8s.io/client-go v0.0.0-20250724224906-d4f2d5b8ccf7 h1:C8WJw/YbIDqiA23gjDMRgBKsALuaO0i3iF2w574EpfU=
301-
k8s.io/client-go v0.0.0-20250724224906-d4f2d5b8ccf7/go.mod h1:zKs1Ap5k23bvGabDhvkL2sweYIp3KNomdmyrhOBwvzo=
300+
k8s.io/client-go v0.0.0-20250725024915-bce2be2f20f3 h1:FGZDzvT0js6Mp+uw3hIBjUB1MM7nZgCOSSTKHsoR1M8=
301+
k8s.io/client-go v0.0.0-20250725024915-bce2be2f20f3/go.mod h1:dSUbXIhtKRg5cEkqD3yeFAJ7ZZpVZw6dns/ikA9dMNE=
302302
k8s.io/code-generator v0.0.0-20250724225710-fe7d3af27d90 h1:l7oUGTSxEKfrSHv7eYupe8wGxvf3AQ3Xy63LWqlBFB0=
303303
k8s.io/code-generator v0.0.0-20250724225710-fe7d3af27d90/go.mod h1:9aIDufE+Ylxl6SGZilgTysYdMp/GoFcHLAF/c8gE2uQ=
304304
k8s.io/component-base v0.0.0-20250724225857-f959b0363667 h1:pvV8NZOWqqWnHImbEtFPHt7aFoftkCKdq4X51FyljYQ=

pkg/apiserver/conversion/webhook_converter.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package conversion
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"errors"
2223
"fmt"
@@ -76,9 +77,14 @@ type webhookConverter struct {
7677

7778
func webhookClientConfigForCRD(crd *v1.CustomResourceDefinition) *webhook.ClientConfig {
7879
apiConfig := crd.Spec.Conversion.Webhook.ClientConfig
80+
caBundle := apiConfig.CABundle
81+
if len(caBundle) > 0 && len(bytes.TrimSpace(caBundle)) == 0 {
82+
// treat whitespace-only caBundle as empty
83+
caBundle = nil
84+
}
7985
ret := webhook.ClientConfig{
8086
Name: fmt.Sprintf("conversion_webhook_for_%s", crd.Name),
81-
CABundle: apiConfig.CABundle,
87+
CABundle: caBundle,
8288
}
8389
if apiConfig.URL != nil {
8490
ret.URL = *apiConfig.URL

test/integration/conversion/conversion_test.go

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,6 @@ func (c *conversionTestContext) setConversionWebhook(t *testing.T, webhookClient
981981
t.Fatal(err)
982982
}
983983
c.crd = crd
984-
985984
}
986985

987986
func (c *conversionTestContext) removeConversionWebhook(t *testing.T) {
@@ -1337,3 +1336,102 @@ func jsonPtr(x interface{}) *apiextensionsv1.JSON {
13371336
ret := apiextensionsv1.JSON{Raw: bs}
13381337
return &ret
13391338
}
1339+
1340+
func TestWebhookConversion_WhitespaceCABundleEtcdBypass(t *testing.T) {
1341+
// Setup server and clients
1342+
tearDown, config, options, err := fixtures.StartDefaultServer(t)
1343+
if err != nil {
1344+
t.Fatal(err)
1345+
}
1346+
defer tearDown()
1347+
1348+
apiExtensionsClient, err := clientset.NewForConfig(config)
1349+
if err != nil {
1350+
t.Fatal(err)
1351+
}
1352+
dynamicClient, err := dynamic.NewForConfig(config)
1353+
if err != nil {
1354+
t.Fatal(err)
1355+
}
1356+
1357+
crd := multiVersionFixture.DeepCopy()
1358+
1359+
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd, nil, nil)
1360+
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural}, nil)
1361+
if err != nil {
1362+
t.Fatal(err)
1363+
}
1364+
etcdClient, _, err := storage.GetEtcdClients(restOptions.StorageConfig.Transport)
1365+
if err != nil {
1366+
t.Fatal(err)
1367+
}
1368+
defer func() {
1369+
err = etcdClient.Close()
1370+
if err != nil {
1371+
t.Fatal(err)
1372+
}
1373+
}()
1374+
etcdObjectReader := storage.NewEtcdObjectReader(etcdClient, &restOptions, crd)
1375+
1376+
ctcTearDown, ctc := newConversionTestContext(t, apiExtensionsClient, dynamicClient, etcdObjectReader, crd)
1377+
defer ctcTearDown()
1378+
1379+
ns := "whitespace-cabundle"
1380+
version := "v1beta1"
1381+
client := ctc.versionedClient(ns, version)
1382+
1383+
// Create a CR instance
1384+
name := "test"
1385+
obj, err := client.Create(context.TODO(), newConversionMultiVersionFixture(ns, name, version), metav1.CreateOptions{})
1386+
if err != nil {
1387+
t.Fatalf("failed to create CR instance: %v", err)
1388+
}
1389+
verifyMultiVersionObject(t, "v1beta1", obj)
1390+
1391+
// Set up webhook conversion
1392+
tearDown, webhookClientConfig, err := StartConversionWebhookServer(NewObjectConverterWebhookHandler(t, noopConverter))
1393+
if err != nil {
1394+
t.Fatal(err)
1395+
}
1396+
defer tearDown()
1397+
1398+
crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
1399+
Strategy: apiextensionsv1.WebhookConverter,
1400+
Webhook: &apiextensionsv1.WebhookConversion{
1401+
ClientConfig: webhookClientConfig,
1402+
ConversionReviewVersions: []string{"v1beta1"},
1403+
},
1404+
}
1405+
crd.TypeMeta = metav1.TypeMeta{
1406+
APIVersion: "apiextensions.k8s.io/v1",
1407+
Kind: "CustomResourceDefinition",
1408+
}
1409+
1410+
// Fetch the latest CRD from the API server to get the current resourceVersion
1411+
crdFromAPI, err := ctc.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(
1412+
context.TODO(), crd.Name, metav1.GetOptions{})
1413+
if err != nil {
1414+
t.Fatal(err)
1415+
}
1416+
crd.ResourceVersion = crdFromAPI.ResourceVersion
1417+
_, err = ctc.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Update(
1418+
context.TODO(), crd, metav1.UpdateOptions{})
1419+
if err != nil {
1420+
t.Fatal(err)
1421+
}
1422+
1423+
// Replace the CABundle with empty spaces
1424+
crd.Spec.Conversion.Webhook.ClientConfig.CABundle = []byte(" \n\t ")
1425+
err = etcdObjectReader.SetStoredCustomResourceDefinition(crd.Name, crd)
1426+
if err != nil {
1427+
t.Fatal(err)
1428+
}
1429+
1430+
// Try to read the CR instance (should succeed, as no conversion is needed)
1431+
obj, err = ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
1432+
if err != nil {
1433+
t.Fatal(err)
1434+
}
1435+
verifyMultiVersionObject(t, "v1beta1", obj)
1436+
1437+
}

test/integration/storage/objectreader.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,16 @@ func GetEtcdClients(config storagebackend.TransportConfig) (*clientv3.Client, cl
128128

129129
return c, clientv3.NewKV(c), nil
130130
}
131+
132+
// SetStoredCustomResourceDefinition writes the storage representation of a CRD to etcd.
133+
func (s *EtcdObjectReader) SetStoredCustomResourceDefinition(name string, crd *apiextensionsv1.CustomResourceDefinition) error {
134+
bs, err := json.Marshal(crd)
135+
if err != nil {
136+
return err
137+
}
138+
key := path.Join("/", s.storagePrefix, "apiextensions.k8s.io", "customresourcedefinitions", name)
139+
if _, err := s.etcdClient.Put(context.Background(), key, string(bs)); err != nil {
140+
return fmt.Errorf("error setting CRD %s in etcd at key %s: %w", name, key, err)
141+
}
142+
return nil
143+
}

0 commit comments

Comments
 (0)