From 507555795f41088ef0af97ad1f15c8d65c1d7da5 Mon Sep 17 00:00:00 2001 From: olalekan odukoya Date: Fri, 30 Jan 2026 01:05:07 +0100 Subject: [PATCH] fix bug preventing the creation of CRD in virtual workspace Signed-off-by: olalekan odukoya --- .prow.yaml | 8 +- .../dynamic/apiserver/crd_helpers.go | 44 ++++ .../framework/dynamic/apiserver/openapi.go | 95 +++++++- .../dynamic/apiserver/serving_info.go | 38 ++++ .../kcp-dev/sdk/testing/workspaces.go | 2 +- .../crd_apiexport_test.go | 207 ++++++++++++++++++ .../customresourcedefinition/shirts_crd.yaml | 36 +++ .../replication/virtualworkspace_test.go | 27 ++- .../vr_cachedresources_test.go | 13 +- 9 files changed, 445 insertions(+), 25 deletions(-) create mode 100644 pkg/virtual/framework/dynamic/apiserver/crd_helpers.go create mode 100644 test/e2e/customresourcedefinition/crd_apiexport_test.go create mode 100644 test/e2e/customresourcedefinition/shirts_crd.yaml diff --git a/.prow.yaml b/.prow.yaml index 6d848036296..3657488ec7a 100644 --- a/.prow.yaml +++ b/.prow.yaml @@ -210,8 +210,8 @@ presubmits: value: "3" resources: requests: - memory: 6Gi - cpu: 4 + memory: 8Gi + cpu: 6 - name: pull-kcp-test-e2e-sharded decorate: true @@ -239,5 +239,5 @@ presubmits: value: "3" resources: requests: - memory: 6Gi - cpu: 4 + memory: 8Gi + cpu: 6 diff --git a/pkg/virtual/framework/dynamic/apiserver/crd_helpers.go b/pkg/virtual/framework/dynamic/apiserver/crd_helpers.go new file mode 100644 index 00000000000..650a5065a49 --- /dev/null +++ b/pkg/virtual/framework/dynamic/apiserver/crd_helpers.go @@ -0,0 +1,44 @@ +/* +Copyright 2026 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apiserver + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" +) + +// isCRDResource checks if a CustomResourceDefinition is for the CRD type itself +// (apiextensions.k8s.io/v1.CustomResourceDefinition). +// +// This is used to trigger special handling for the deeply nested, recursive schema structure +// of a CRD. When generating the OpenAPI spec, the builder would otherwise truncate +// spec.versions[].schema.openAPIV3Schema to . +// +// When this returns true, we explicitly patch the OpenAPI output to include +// x-kubernetes-preserve-unknown-fields: true on openAPIV3Schema. This tells clients +// (like kubectl) that the field accepts arbitrary objects without forcing the OpenAPI +// builder to recursively unroll the massive JSONSchemaProps tree which cpuld crush performance. +// +// See: https://github.com/kcp-dev/kcp/issues/3389 +func isCRDResource(crd *apiextensionsv1.CustomResourceDefinition) bool { + return crd.Spec.Group == "apiextensions.k8s.io" && crd.Spec.Names.Kind == "CustomResourceDefinition" +} + +func isCRDAPIResourceSchema(apiResourceSchema *apisv1alpha1.APIResourceSchema) bool { + return apiResourceSchema.Spec.Group == "apiextensions.k8s.io" && apiResourceSchema.Spec.Names.Kind == "CustomResourceDefinition" +} diff --git a/pkg/virtual/framework/dynamic/apiserver/openapi.go b/pkg/virtual/framework/dynamic/apiserver/openapi.go index 0c82996bd70..f280f8f7195 100644 --- a/pkg/virtual/framework/dynamic/apiserver/openapi.go +++ b/pkg/virtual/framework/dynamic/apiserver/openapi.go @@ -23,6 +23,7 @@ import ( "fmt" "net/http" "sort" + "strings" "time" "github.com/emicklei/go-restful/v3" @@ -42,6 +43,7 @@ import ( "k8s.io/kube-openapi/pkg/common/restfuladapter" "k8s.io/kube-openapi/pkg/handler3" "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" "k8s.io/utils/keymutex" "k8s.io/utils/lru" @@ -305,12 +307,16 @@ func apiResourceSchemaToSpec(apiResourceSchema *apisv1alpha1.APIResourceSchema) }, } - for _, ver := range versions { + for i, ver := range versions { spec, err := builder.BuildOpenAPIV3(crd, ver.Name, builder.Options{V2: false}) if err != nil { return nil, err } + if isCRDResource(crd) { + patchCRDSchemaForOpenAPI(spec, &apiResourceSchema.Spec.Versions[i]) + } + gv := schema.GroupVersion{Group: crd.Spec.Group, Version: ver.Name} gvPath := groupVersionToOpenAPIV3Path(gv) specs[gvPath] = append(specs[gvPath], spec) @@ -319,6 +325,93 @@ func apiResourceSchemaToSpec(apiResourceSchema *apisv1alpha1.APIResourceSchema) return specs, nil } +func patchCRDSchemaForOpenAPI(spec *spec3.OpenAPI, ver *apisv1alpha1.APIResourceVersion) { + if spec.Components == nil || spec.Components.Schemas == nil { + return + } + + for name, s := range spec.Components.Schemas { + if !strings.HasSuffix(name, "CustomResourceDefinition") { + continue + } + if _, hasSpec := s.Properties["spec"]; hasSpec { + continue + } + + sourceSchema, err := ver.GetSchema() + if err != nil || sourceSchema == nil { + continue + } + + patchedSchema, err := convertToOpenAPISchema(sourceSchema) + if err != nil { + continue + } + + patchedSchema.VendorExtensible.Extensions = s.VendorExtensible.Extensions + enhanceOpenAPIV3SchemaProperty(&patchedSchema) + spec.Components.Schemas[name] = &patchedSchema + } +} + +func convertToOpenAPISchema(source *apiextensionsv1.JSONSchemaProps) (spec.Schema, error) { + bs, err := json.Marshal(source) + if err != nil { + return spec.Schema{}, err + } + + var result spec.Schema + if err := json.Unmarshal(bs, &result); err != nil { + return spec.Schema{}, err + } + + return result, nil +} + +func enhanceOpenAPIV3SchemaProperty(schema *spec.Schema) { + specProp, ok := schema.Properties["spec"] + if !ok { + return + } + + versionsProp, ok := specProp.Properties["versions"] + if !ok { + return + } + + if versionsProp.Items == nil || versionsProp.Items.Schema == nil { + return + } + + itemSchema := versionsProp.Items.Schema + schemaProp, ok := itemSchema.Properties["schema"] + if !ok { + return + } + + oaProp, ok := schemaProp.Properties["openAPIV3Schema"] + if !ok { + return + } + + // We apply x-kubernetes-preserve-unknown-fields so OpenAPI consumers (like kubectl) + // know that openAPIV3Schema can contain arbitrary objects, without requiring us to + // recursively unroll the massively nested JSONSchemaProps type which exhausts the API server. + if oaProp.VendorExtensible.Extensions == nil { + oaProp.VendorExtensible.Extensions = spec.Extensions{} + } + oaProp.VendorExtensible.Extensions["x-kubernetes-preserve-unknown-fields"] = true + + oaProp.Properties = nil + oaProp.Type = []string{"object"} + + schemaProp.Properties["openAPIV3Schema"] = oaProp + itemSchema.Properties["schema"] = schemaProp + versionsProp.Items.Schema = itemSchema + specProp.Properties["versions"] = versionsProp + schema.Properties["spec"] = specProp +} + func groupVersionToOpenAPIV3Path(gv schema.GroupVersion) string { if gv.Group == "" { return "api/" + gv.Version diff --git a/pkg/virtual/framework/dynamic/apiserver/serving_info.go b/pkg/virtual/framework/dynamic/apiserver/serving_info.go index 0b6eee8a691..23e4827369e 100644 --- a/pkg/virtual/framework/dynamic/apiserver/serving_info.go +++ b/pkg/virtual/framework/dynamic/apiserver/serving_info.go @@ -73,6 +73,11 @@ func CreateServingInfoFor(genericConfig genericapiserver.CompletedConfig, apiRes if err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(openapiSchema, internalSchema, nil); err != nil { return nil, fmt.Errorf("failed converting CRD validation to internal version: %w", err) } + + if isCRDAPIResourceSchema(apiResourceSchema) { + patchCRDValidationSchema(internalSchema) + } + structuralSchema, err := structuralschema.NewStructural(internalSchema) if err != nil { // This should never happen. If it does, it is a programming error. @@ -404,3 +409,36 @@ func (c unstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object, ret.SetGroupVersionKind(kind) return ret, nil } + +func patchCRDValidationSchema(schema *apiextensionsinternal.JSONSchemaProps) { + trueVal := true + + specProp, ok := schema.Properties["spec"] + if !ok { + return + } + + versionsProp, ok := specProp.Properties["versions"] + if !ok { + return + } + + if versionsProp.Items == nil || versionsProp.Items.Schema == nil { + return + } + + itemSchema := versionsProp.Items.Schema + schemaProp, ok := itemSchema.Properties["schema"] + if !ok { + return + } + + oaProp, ok := schemaProp.Properties["openAPIV3Schema"] + if !ok { + return + } + + oaProp.XPreserveUnknownFields = &trueVal + schemaProp.Properties["openAPIV3Schema"] = oaProp + itemSchema.Properties["schema"] = schemaProp +} diff --git a/staging/src/github.com/kcp-dev/sdk/testing/workspaces.go b/staging/src/github.com/kcp-dev/sdk/testing/workspaces.go index c37a7ab4173..f90ccbe81da 100644 --- a/staging/src/github.com/kcp-dev/sdk/testing/workspaces.go +++ b/staging/src/github.com/kcp-dev/sdk/testing/workspaces.go @@ -136,7 +136,7 @@ func NewLowLevelWorkspaceFixture[O WorkspaceOption](t TestingT, createClusterCli var err error ws, err = createClusterClient.Cluster(parent).TenancyV1alpha1().Workspaces().Create(ctx, tmpl, metav1.CreateOptions{}) return err == nil, fmt.Sprintf("error creating workspace under %s: %v", parent, err) - }, wait.ForeverTestTimeout, time.Millisecond*100, "failed to create %s workspace under %s", tmpl.Spec.Type.Name, parent) + }, wait.ForeverTestTimeout*2, time.Millisecond*500, "failed to create %s workspace under %s", tmpl.Spec.Type.Name, parent) wsName := ws.Name t.Cleanup(func() { diff --git a/test/e2e/customresourcedefinition/crd_apiexport_test.go b/test/e2e/customresourcedefinition/crd_apiexport_test.go new file mode 100644 index 00000000000..146d1caeb9e --- /dev/null +++ b/test/e2e/customresourcedefinition/crd_apiexport_test.go @@ -0,0 +1,207 @@ +/* +Copyright 2026 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package customresourcedefinition + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" + + rbacv1 "k8s.io/api/rbac/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + + kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client" + kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes" + "github.com/kcp-dev/logicalcluster/v3" + apisv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2" + "github.com/kcp-dev/sdk/apis/core" + kcpclientset "github.com/kcp-dev/sdk/client/clientset/versioned/cluster" + kcptesting "github.com/kcp-dev/sdk/testing" + + "github.com/kcp-dev/kcp/test/e2e/fixtures/apifixtures" + "github.com/kcp-dev/kcp/test/e2e/framework" +) + +func TestCRDVirtualWorkspace(t *testing.T) { + t.Parallel() + framework.Suite(t, "control-plane") + + server := kcptesting.SharedKcpServer(t) + cfg := server.BaseConfig(t) + + orgPath, _ := kcptesting.NewWorkspaceFixture(t, server, core.RootCluster.Path(), kcptesting.WithType(core.RootCluster.Path(), "organization")) + + providerPath, _ := kcptesting.NewWorkspaceFixture(t, server, orgPath) + + consumerPath, consumerWorkspace := kcptesting.NewWorkspaceFixture(t, server, orgPath) + + kcpClient, err := kcpclientset.NewForConfig(cfg) + require.NoError(t, err) + + ctx := context.Background() + + t.Log("Creating cowboys APIExport in provider workspace") + apifixtures.CreateSheriffsSchemaAndExport(ctx, t, providerPath, kcpClient, "wildwest.dev", "Wild West API") + + t.Log("Adding CRD permission claim to APIExport") + require.Eventually(t, func() bool { + export, err := kcpClient.Cluster(providerPath).ApisV1alpha2().APIExports().Get(ctx, "wildwest.dev", metav1.GetOptions{}) + if err != nil { + return false + } + + export.Spec.PermissionClaims = []apisv1alpha2.PermissionClaim{ + { + GroupResource: apisv1alpha2.GroupResource{ + Group: "apiextensions.k8s.io", + Resource: "customresourcedefinitions", + }, + Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"}, + }, + } + export.Spec.MaximalPermissionPolicy = &apisv1alpha2.MaximalPermissionPolicy{ + Local: &apisv1alpha2.LocalAPIExportPolicy{}, + } + + _, err = kcpClient.Cluster(providerPath).ApisV1alpha2().APIExports().Update(ctx, export, metav1.UpdateOptions{}) + return err == nil + }, wait.ForeverTestTimeout, 100*time.Millisecond, "Failed to add CRD permission claim to APIExport") + + t.Log("Creating RBAC rules in provider workspace for maximal permission policy") + kubeClient, err := kcpkubernetesclientset.NewForConfig(cfg) + require.NoError(t, err) + + crdRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Name: "kcp-admin-crd-permissions"}, + Rules: []rbacv1.PolicyRule{ + {APIGroups: []string{"apiextensions.k8s.io"}, Resources: []string{"customresourcedefinitions"}, Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"}}, + }, + } + _, err = kubeClient.Cluster(providerPath).RbacV1().ClusterRoles().Create(ctx, crdRole, metav1.CreateOptions{}) + require.NoError(t, err) + + crdRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: "kcp-admin-crd-permissions"}, + Subjects: []rbacv1.Subject{{Kind: "User", Name: "apis.kcp.io:binding:kcp-admin"}}, + RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.SchemeGroupVersion.Group, Kind: "ClusterRole", Name: "kcp-admin-crd-permissions"}, + } + _, err = kubeClient.Cluster(providerPath).RbacV1().ClusterRoleBindings().Create(ctx, crdRoleBinding, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Log("Binding to cowboys export in consumer workspace with accepted CRD permission claims") + binding := &apisv1alpha2.APIBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wildwest.dev", + }, + Spec: apisv1alpha2.APIBindingSpec{ + Reference: apisv1alpha2.BindingReference{ + Export: &apisv1alpha2.ExportBindingReference{ + Path: providerPath.String(), + Name: "wildwest.dev", + }, + }, + PermissionClaims: []apisv1alpha2.AcceptablePermissionClaim{ + { + ScopedPermissionClaim: apisv1alpha2.ScopedPermissionClaim{ + PermissionClaim: apisv1alpha2.PermissionClaim{ + GroupResource: apisv1alpha2.GroupResource{ + Group: "apiextensions.k8s.io", + Resource: "customresourcedefinitions", + }, + Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"}, + }, + Selector: apisv1alpha2.PermissionClaimSelector{ + MatchAll: true, + }, + }, + State: apisv1alpha2.ClaimAccepted, + }, + }, + }, + } + + require.Eventually(t, func() bool { + _, err := kcpClient.Cluster(consumerPath).ApisV1alpha2().APIBindings().Create(ctx, binding, metav1.CreateOptions{}) + return err == nil + }, wait.ForeverTestTimeout, 100*time.Millisecond, "Failed to create APIBinding") + + t.Log("Waiting for APIBinding to be bound") + require.Eventually(t, func() bool { + binding, err := kcpClient.Cluster(consumerPath).ApisV1alpha2().APIBindings().Get(ctx, "wildwest.dev", metav1.GetOptions{}) + if err != nil { + return false + } + return binding.Status.Phase == apisv1alpha2.APIBindingPhaseBound + }, wait.ForeverTestTimeout, 100*time.Millisecond, "APIBinding never reached Bound phase") + + t.Log("Waiting for RBAC to propagate") + time.Sleep(2 * time.Second) + + t.Log("Getting virtual workspace URL from APIExportEndpointSlice") + var vwURL string + require.Eventually(t, func() bool { + endpointSlice, err := kcpClient.Cluster(providerPath).ApisV1alpha1().APIExportEndpointSlices().Get(ctx, "wildwest.dev", metav1.GetOptions{}) + if err != nil { + t.Logf("Failed to get APIExportEndpointSlice: %v", err) + return false + } + var found bool + vwURL, found, err = framework.VirtualWorkspaceURL(ctx, kcpClient, consumerWorkspace, framework.ExportVirtualWorkspaceURLs(endpointSlice)) + if err != nil { + t.Logf("Error getting virtual workspace URL: %v", err) + return false + } + return found + }, wait.ForeverTestTimeout, 100*time.Millisecond, "APIExportEndpointSlice never got virtual workspace URL") + + t.Logf("Virtual workspace URL: %s", vwURL) + + t.Log("Creating apiextensions client for virtual workspace") + vwCfg := rest.CopyConfig(cfg) + vwCfg.Host = vwURL + vwAPIExtensionsClient, err := kcpapiextensionsclientset.NewForConfig(vwCfg) + require.NoError(t, err) + + t.Log("Loading shirts CRD from YAML file") + shirtsCRDBytes, err := testFiles.ReadFile("shirts_crd.yaml") + require.NoError(t, err) + + shirtsCRD := &apiextensionsv1.CustomResourceDefinition{} + err = yaml.Unmarshal(shirtsCRDBytes, shirtsCRD) + require.NoError(t, err) + + t.Log("Creating shirts CRD via virtual workspace URL") + + created, err := vwAPIExtensionsClient.Cluster(logicalcluster.Name(consumerWorkspace.Spec.Cluster).Path()).ApiextensionsV1().CustomResourceDefinitions().Create(ctx, shirtsCRD, metav1.CreateOptions{}) + require.NoError(t, err, "Failed to create CRD via virtual workspace - OpenAPI schema may be incomplete") + require.NotNil(t, created) + t.Logf("Successfully created CRD: %s", created.Name) + + retrieved, err := vwAPIExtensionsClient.Cluster(logicalcluster.Name(consumerWorkspace.Spec.Cluster).Path()).ApiextensionsV1().CustomResourceDefinitions().Get(ctx, "shirts.stable.example.com", metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "shirts.stable.example.com", retrieved.Name) + require.Equal(t, "stable.example.com", retrieved.Spec.Group) + require.Equal(t, "v1", retrieved.Spec.Versions[0].Name) + require.Equal(t, ".spec.color", retrieved.Spec.Versions[0].SelectableFields[0].JSONPath) + require.Equal(t, ".spec.size", retrieved.Spec.Versions[0].SelectableFields[1].JSONPath) +} diff --git a/test/e2e/customresourcedefinition/shirts_crd.yaml b/test/e2e/customresourcedefinition/shirts_crd.yaml new file mode 100644 index 00000000000..3aab92e5378 --- /dev/null +++ b/test/e2e/customresourcedefinition/shirts_crd.yaml @@ -0,0 +1,36 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: shirts.stable.example.com +spec: + group: stable.example.com + scope: Namespaced + names: + plural: shirts + singular: shirt + kind: Shirt + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + color: + type: string + size: + type: string + selectableFields: + - jsonPath: .spec.color + - jsonPath: .spec.size + additionalPrinterColumns: + - jsonPath: .spec.color + name: Color + type: string + - jsonPath: .spec.size + name: Size + type: string diff --git a/test/e2e/virtual/replication/virtualworkspace_test.go b/test/e2e/virtual/replication/virtualworkspace_test.go index 8a3eb0afff5..f8e3e39f519 100644 --- a/test/e2e/virtual/replication/virtualworkspace_test.go +++ b/test/e2e/virtual/replication/virtualworkspace_test.go @@ -245,15 +245,12 @@ func TestCachedResourceVirtualWorkspace(t *testing.T) { kcptestinghelpers.Eventually(t, func() (bool, string) { var err error _, err = getSheriff(t.Context(), user1CachedResourceDynClient, consumerClusterName, sheriffOne.Name) - if apierrors.IsForbidden(err) { - return false, fmt.Sprintf("waiting until rbac cache is primed: %v", err) - } - if apierrors.IsNotFound(err) { - return false, fmt.Sprintf("waiting until the sheriff is in cache: %v", err) + if err != nil { + t.Logf("waiting until the sheriff is in cache: %v", err) + return false, err.Error() } - require.NoError(t, err) return true, "" - }, wait.ForeverTestTimeout, time.Millisecond*100, "expected user-1 to list sheriffs") + }, wait.ForeverTestTimeout*5, time.Millisecond*500, "expected user-1 to get sheriff") } sheriffLabels := map[string]string{"hello": "world"} @@ -284,12 +281,15 @@ func TestCachedResourceVirtualWorkspace(t *testing.T) { if apierrors.IsForbidden(err) { return false, fmt.Sprintf("waiting until rbac cache is primed: %v", err) } + if err != nil { + t.Logf("waiting until the sheriff is in cache: %v", err) + return false, err.Error() + } if len(sherrifList.Items) < 2 { return false, fmt.Sprintf("waiting until there are two items in list, have %d", len(sherrifList.Items)) } - require.NoError(t, err) return true, "" - }, wait.ForeverTestTimeout, time.Millisecond*100, "expected user-1 to list sheriffs") + }, wait.ForeverTestTimeout*5, time.Millisecond*500, "expected user-1 to list sheriffs") require.Len(t, sherrifList.Items, 2, "expected to find exactly two sheriffs") t.Logf("### got LIST sheriffs resourceVersion=%s", sherrifList.ResourceVersion) @@ -324,9 +324,12 @@ func TestCachedResourceVirtualWorkspace(t *testing.T) { if apierrors.IsForbidden(err) { return false, fmt.Sprintf("waiting until rbac cache is primed: %v", err) } - require.NoError(t, err) + if err != nil { + t.Logf("waiting until the sheriff is in cache: %v", err) + return false, err.Error() + } return true, "" - }, wait.ForeverTestTimeout, time.Millisecond*100, "expected user-1 to watch sheriffs") + }, wait.ForeverTestTimeout*5, time.Millisecond*500, "expected user-1 to watch sheriffs") sheriffWatchCh := sheriffWatch.ResultChan() waitForEvent := func() (watch.Event, bool) { @@ -335,7 +338,7 @@ func TestCachedResourceVirtualWorkspace(t *testing.T) { kcptestinghelpers.Eventually(t, func() (bool, string) { event, more = <-sheriffWatchCh return true, "" - }, wait.ForeverTestTimeout, time.Millisecond*100, "expected to get a watch event") + }, wait.ForeverTestTimeout*5, time.Millisecond*500, "expected to get a watch event") return event, more } checkEvent := func(actualEvent watch.Event, diff --git a/test/e2e/virtualresources/cachedresources/vr_cachedresources_test.go b/test/e2e/virtualresources/cachedresources/vr_cachedresources_test.go index bc1bd81f7ed..85631fa7806 100644 --- a/test/e2e/virtualresources/cachedresources/vr_cachedresources_test.go +++ b/test/e2e/virtualresources/cachedresources/vr_cachedresources_test.go @@ -200,8 +200,7 @@ func TestCachedResources(t *testing.T) { return false, fmt.Sprintf("failed to create APIBinding: %v", err) } return true, "" - }, wait.ForeverTestTimeout, time.Second*1, "waiting to create apibinding") - + }, wait.ForeverTestTimeout*5, time.Second*1, "waiting to create apibinding") for resourceName := range resourceNames { t.Logf("Waiting for %s.v1alpha1.wildwest.dev API to appear in %q", resourceName, consumerPath) kcptestinghelpers.Eventually(t, func() (bool, string) { @@ -212,7 +211,7 @@ func TestCachedResources(t *testing.T) { return slices.ContainsFunc(groupList.Groups, func(e metav1.APIGroup) bool { return e.Name == wildwestv1alpha1.SchemeGroupVersion.Group }), fmt.Sprintf("wildwest.dev group not found in %q", consumerPath) - }, wait.ForeverTestTimeout, time.Second*1, "waiting for wildwest.dev group in %q", consumerPath) + }, wait.ForeverTestTimeout*5, time.Second*1, "waiting for wildwest.dev group in %q", consumerPath) kcptestinghelpers.Eventually(t, func() (bool, string) { resourceList, err := kcpClusterClient.Cluster(consumerPath).Discovery().ServerResourcesForGroupVersion("wildwest.dev/v1alpha1") if err != nil { @@ -221,7 +220,7 @@ func TestCachedResources(t *testing.T) { return slices.ContainsFunc(resourceList.APIResources, func(e metav1.APIResource) bool { return e.Name == resourceName }), fmt.Sprintf("%s.v1alpha1.wildwest.dev API not found in %q", resourceName, consumerPath) - }, wait.ForeverTestTimeout, time.Second*1, "waiting for wildwest.dev group in %q", resourceName, consumerPath) + }, wait.ForeverTestTimeout*5, time.Second*1, "waiting for wildwest.dev group in %q", resourceName, consumerPath) t.Logf("Ensure %s.v1alpha1.wildwest.dev API is available in OpenAPIv3 endpoint in %q", resourceName, consumerPath) paths, err := kcpClusterClient.Cluster(consumerPath).Discovery().OpenAPIV3().Paths() @@ -262,7 +261,7 @@ func TestCachedResources(t *testing.T) { return false, err.Error() } return found, fmt.Sprintf("URL for workspace %q not found in APIExportEndpointSlice %s|%s", consumerPath, providerPath, apiExport.Name) - }, wait.ForeverTestTimeout, time.Second*1, "waiting for workspace URL in APIExportEndpointSlice") + }, wait.ForeverTestTimeout*5, time.Second*1, "waiting for workspace URL in APIExportEndpointSlice") if _, ok := apiExportVWClientConfigs[vwURL]; !ok { vwCfg := rest.CopyConfig(cfg) @@ -364,7 +363,7 @@ func TestCachedResources(t *testing.T) { } } return true, "" - }, wait.ForeverTestTimeout*2, time.Millisecond*500, "waiting for two sheriffs") + }, wait.ForeverTestTimeout*5, time.Millisecond*500, "waiting for two sheriffs") t.Log("Stopping watches") for _, stop := range watchStopFuncs { @@ -463,7 +462,7 @@ func TestCachedResources(t *testing.T) { return false, fmt.Sprintf("failed to get CRD: %v", err) } return apiextensionshelpers.IsCRDConditionFalse(sheriffsCRDConflicting, apiextensionsv1.NamesAccepted), "the CRD should not be accepted because of names collision" - }, wait.ForeverTestTimeout, time.Second*1, "waiting to create apibinding") + }, wait.ForeverTestTimeout*5, time.Second*1, "waiting to create apibinding") } func normalizeUnstructuredMap(origObj map[string]interface{}) map[string]interface{} {