diff --git a/pkg/informer/informer.go b/pkg/informer/informer.go index ce5c6f6930b3..3b95e15466da 100644 --- a/pkg/informer/informer.go +++ b/pkg/informer/informer.go @@ -852,6 +852,7 @@ var builtInInformableTypes map[schema.GroupVersionResource]GVRPartialMetadata = gvrFor("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): withGVRPartialMetadata(apiextensionsv1.ClusterScoped, "ValidatingAdmissionPolicy", "validatingadmissionpolicy"), gvrFor("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"): withGVRPartialMetadata(apiextensionsv1.ClusterScoped, "ValidatingAdmissionPolicyBinding", "validatingadmissionpolicybinding"), gvrFor("apiextensions.k8s.io", "v1", "customresourcedefinitions"): withGVRPartialMetadata(apiextensionsv1.ClusterScoped, "CustomResourceDefinition", "customresourcedefinition"), + gvrFor("core.kcp.io", "v1alpha1", "logicalclusters"): withGVRPartialMetadata(apiextensionsv1.ClusterScoped, "LogicalCluster", "logicalcluster"), } func (s *crdGVRSource) GVRs() map[schema.GroupVersionResource]GVRPartialMetadata { diff --git a/pkg/informer/informer_test.go b/pkg/informer/informer_test.go index c058480d6164..00c6d314dfa2 100644 --- a/pkg/informer/informer_test.go +++ b/pkg/informer/informer_test.go @@ -84,6 +84,7 @@ func TestBuiltInInformableTypes(t *testing.T) { {Group: "authorization.k8s.io", Version: "v1", Kind: "SelfSubjectRulesReview"}: {}, {Group: "authorization.k8s.io", Version: "v1", Kind: "SubjectAccessReview"}: {}, {Group: "apiextensions.k8s.io", Version: "v1", Kind: "ConversionReview"}: {}, + {Group: "core.kcp.io", Version: "v1alpha1", Kind: "Shard"}: {}, } gvsToIgnore := map[schema.GroupVersion]struct{}{ diff --git a/pkg/server/scheme/scheme.go b/pkg/server/scheme/scheme.go index 0526a45331a9..c6b52a73374d 100644 --- a/pkg/server/scheme/scheme.go +++ b/pkg/server/scheme/scheme.go @@ -28,10 +28,13 @@ import ( "k8s.io/kubernetes/pkg/apis/core/install" eventsinstall "k8s.io/kubernetes/pkg/apis/events/install" rbacinstall "k8s.io/kubernetes/pkg/apis/rbac/install" + + installkcpcore "github.com/kcp-dev/kcp/sdk/apis/core/install" ) func init() { install.Install(Scheme) + installkcpcore.Install(Scheme) authenticationinstall.Install(Scheme) authorizationinstall.Install(Scheme) apiextensionsinstall.Install(Scheme) diff --git a/pkg/virtual/apiexport/schemas/builtin/builtin.go b/pkg/virtual/apiexport/schemas/builtin/builtin.go index 84b248dd8c58..b7cfbbc4812d 100644 --- a/pkg/virtual/apiexport/schemas/builtin/builtin.go +++ b/pkg/virtual/apiexport/schemas/builtin/builtin.go @@ -32,9 +32,11 @@ import ( "k8s.io/kube-openapi/pkg/common" generatedopenapi "k8s.io/kubernetes/pkg/generated/openapi" + generatedkcpopenapi "github.com/kcp-dev/kcp/pkg/openapi" kcpscheme "github.com/kcp-dev/kcp/pkg/server/scheme" "github.com/kcp-dev/kcp/pkg/virtual/framework/internalapis" apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" ) // Create APIResourceSchemas for built-in APIs available as permission claims @@ -42,7 +44,10 @@ import ( // TODO(hasheddan): this could be handled via code generation. func init() { schemes := []*runtime.Scheme{kcpscheme.Scheme} - openAPIDefinitionsGetters := []common.GetOpenAPIDefinitions{generatedopenapi.GetOpenAPIDefinitions} + openAPIDefinitionsGetters := []common.GetOpenAPIDefinitions{ + generatedopenapi.GetOpenAPIDefinitions, // core types + generatedkcpopenapi.GetOpenAPIDefinitions, // KCP core types for LogicalCluster + } apis, err := internalapis.CreateAPIResourceSchemas(schemes, openAPIDefinitionsGetters, BuiltInAPIs...) if err != nil { @@ -266,4 +271,15 @@ var BuiltInAPIs = []internalapis.InternalAPI{ ResourceScope: apiextensionsv1.ClusterScoped, HasStatus: true, }, + { + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "logicalclusters", + Singular: "logicalcluster", + Kind: "LogicalCluster", + }, + GroupVersion: schema.GroupVersion{Group: "core.kcp.io", Version: "v1alpha1"}, + Instance: &corev1alpha1.LogicalCluster{}, + ResourceScope: apiextensionsv1.ClusterScoped, + HasStatus: true, + }, } diff --git a/sdk/apis/apis/v1alpha1/types_apiexport.go b/sdk/apis/apis/v1alpha1/types_apiexport.go index 5b0e0cbf7e76..e6415f92c678 100644 --- a/sdk/apis/apis/v1alpha1/types_apiexport.go +++ b/sdk/apis/apis/v1alpha1/types_apiexport.go @@ -198,7 +198,6 @@ const ( // request and that a consumer may accept and allow the service provider access to. // // +kubebuilder:validation:XValidation:rule="(has(self.all) && self.all) != (has(self.resourceSelector) && size(self.resourceSelector) > 0)",message="either \"all\" or \"resourceSelector\" must be set" -// +kubebuilder:validation:XValidation:rule="!has(self.group) || self.group != \"core.kcp.io\" || self.resource != \"logicalclusters\" || (has(self.identityHash) && self.identityHash != \"\")",message="logicalclusters cannot be claimed" type PermissionClaim struct { GroupResource `json:","` diff --git a/sdk/apis/apis/v1alpha1/types_apiexport_test.go b/sdk/apis/apis/v1alpha1/types_apiexport_test.go index 7fe9e1244e87..15e13d1e18d0 100644 --- a/sdk/apis/apis/v1alpha1/types_apiexport_test.go +++ b/sdk/apis/apis/v1alpha1/types_apiexport_test.go @@ -115,29 +115,6 @@ func TestAPIExportPermissionClaimCELValidation(t *testing.T) { "openAPIV3Schema.properties.spec.properties.permissionClaims.items: Invalid value: \"object\": either \"all\" or \"resourceSelector\" must be set", }, }, - { - name: "logicalcluster invalid", - current: map[string]interface{}{ - "group": "core.kcp.io", - "resource": "logicalclusters", - "all": true, - }, - wantErrs: []string{ - "openAPIV3Schema.properties.spec.properties.permissionClaims.items: Invalid value: \"object\": logicalclusters cannot be claimed", - }, - }, - { - name: "logicalcluster invalid with empty identityHash", - current: map[string]interface{}{ - "group": "core.kcp.io", - "resource": "logicalclusters", - "identityHash": "", - "all": true, - }, - wantErrs: []string{ - "openAPIV3Schema.properties.spec.properties.permissionClaims.items: Invalid value: \"object\": logicalclusters cannot be claimed", - }, - }, { name: "logicalcluster fine with non-empty identityHash", current: map[string]interface{}{ diff --git a/sdk/apis/core/install/install.go b/sdk/apis/core/install/install.go new file mode 100644 index 000000000000..84119b65de65 --- /dev/null +++ b/sdk/apis/core/install/install.go @@ -0,0 +1,31 @@ +/* +Copyright 2022 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 install installs the v1alpha1 monolithic api, making it available as an +// option to all of the API encoding/decoding machinery. +package install + +import ( + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + v1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" +) + +// Install registers the API group and adds types to a scheme +func Install(scheme *runtime.Scheme) { + utilruntime.Must(v1alpha1.AddToScheme(scheme)) +} diff --git a/test/e2e/apibinding/apibinding_logicalcluster_test.go b/test/e2e/apibinding/apibinding_logicalcluster_test.go new file mode 100644 index 000000000000..af562e808857 --- /dev/null +++ b/test/e2e/apibinding/apibinding_logicalcluster_test.go @@ -0,0 +1,257 @@ +/* +Copyright 2022 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 apibinding + +import ( + "context" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" + + kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kcpdynamic "github.com/kcp-dev/client-go/dynamic" + apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" + "github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/util/conditions" + kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + "github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest" + "github.com/kcp-dev/kcp/test/e2e/framework" +) + +// Test that service provider can access logical cluster object from within the +// consumers cluster when consumer binds to the provider's APIExport. +func TestAPIBindingLogicalCluster(t *testing.T) { + t.Parallel() + framework.Suite(t, "control-plane") + + server := framework.SharedKcpServer(t) + + orgPath, _ := framework.NewOrganizationFixture(t, server) + providerPath, _ := framework.NewWorkspaceFixture(t, server, orgPath) + consumerPath, _ := framework.NewWorkspaceFixture(t, server, orgPath) + + t.Logf("providerPath: %v", providerPath) + t.Logf("consumerPath: %v", consumerPath) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + cfg := server.BaseConfig(t) + + kcpClusterClient, err := kcpclientset.NewForConfig(cfg) + require.NoError(t, err, "failed to construct kcp cluster client for server") + + _, err = kcpClusterClient.Cluster(providerPath).CoreV1alpha1().LogicalClusters().List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "failed to list logical clusters") + + exportName := "logical-clusters" + apiExport := apisv1alpha1.APIExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: exportName, + }, + Spec: apisv1alpha1.APIExportSpec{ + PermissionClaims: []apisv1alpha1.PermissionClaim{ + { + GroupResource: apisv1alpha1.GroupResource{ + Group: "core.kcp.io", + Resource: "logicalclusters", + }, + All: true, + }, + }, + }, + } + + _, err = kcpClusterClient.Cluster(providerPath).ApisV1alpha1().APIExports().Create(ctx, &apiExport, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create api export") + + // validate the valid claims condition occurs + t.Logf("validate that the permission claim's conditions true") + framework.EventuallyCondition(t, func() (conditions.Getter, error) { + return kcpClusterClient.Cluster(providerPath).ApisV1alpha1().APIExports().Get(ctx, exportName, metav1.GetOptions{}) + }, framework.Is(apisv1alpha1.APIExportIdentityValid), "could not wait for APIExport to be valid with identity hash") + + apiBinding := apisv1alpha1.APIBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: exportName, + }, + Spec: apisv1alpha1.APIBindingSpec{ + Reference: apisv1alpha1.BindingReference{ + Export: &apisv1alpha1.ExportBindingReference{ + Path: providerPath.String(), + Name: exportName, + }, + }, + PermissionClaims: []apisv1alpha1.AcceptablePermissionClaim{ + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + GroupResource: apisv1alpha1.GroupResource{ + Group: "core.kcp.io", + Resource: "logicalclusters", + }, + All: true, + }, + State: apisv1alpha1.ClaimAccepted, + }, + }, + }, + } + + _, err = kcpClusterClient.Cluster(consumerPath).ApisV1alpha1().APIBindings().Create(ctx, &apiBinding, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create api binding") + + t.Logf("Validate that the permission claims are valid") + framework.EventuallyCondition(t, func() (conditions.Getter, error) { + return kcpClusterClient.Cluster(consumerPath).ApisV1alpha1().APIBindings().Get(ctx, exportName, metav1.GetOptions{}) + }, framework.Is(apisv1alpha1.PermissionClaimsValid), "unable to see valid claims") + + export, err := kcpClusterClient.Cluster(providerPath).ApisV1alpha1().APIExports().Get(ctx, exportName, metav1.GetOptions{}) + require.NoError(t, err) + + virtualWorkspaceURL := export.Status.VirtualWorkspaces[0].URL + t.Logf("Found virtual workspace URL: %v", virtualWorkspaceURL) + + rawConfig, err := server.RawConfig() + require.NoError(t, err) + + vwClusterClient, err := kcpdynamic.NewForConfig(apiexportVWConfig(t, rawConfig, virtualWorkspaceURL)) + require.NoError(t, err) + gvr := corev1alpha1.SchemeGroupVersion.WithResource("logicalclusters") + spew.Dump(gvr) + + list, err := vwClusterClient.Resource(gvr).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, list.Items, 1, "expected to find one logical cluster") + + // Sorry :( Checking the owner to validate + name := list.Items[0].Object["spec"].(map[string]interface{})["owner"].(map[string]interface{})["name"].(string) + require.Equal(t, consumerPath.Base(), name) +} + +func TestAPIBindingCRDs(t *testing.T) { + t.Parallel() + framework.Suite(t, "control-plane") + + server := framework.SharedKcpServer(t) + + orgPath, _ := framework.NewOrganizationFixture(t, server) + providerPath, _ := framework.NewWorkspaceFixture(t, server, orgPath) + consumerPath, _ := framework.NewWorkspaceFixture(t, server, orgPath) + + t.Logf("providerPath: %v", providerPath) + t.Logf("consumerPath: %v", consumerPath) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + cfg := server.BaseConfig(t) + + kcpClusterClient, err := kcpclientset.NewForConfig(cfg) + require.NoError(t, err, "failed to construct kcp cluster client for server") + + // create test crd in the consumer cluster + cowBoysGR := metav1.GroupResource{Group: "wildwest.dev", Resource: "cowboys"} + t.Log("Creating wildwest.dev CRD") + kcpApiExtensionClusterClient, err := kcpapiextensionsclientset.NewForConfig(cfg) + require.NoError(t, err) + kcpCRDClusterClient := kcpApiExtensionClusterClient.ApiextensionsV1().CustomResourceDefinitions() + + t.Log("Creating wildwest.dev.cowboys CR") + wildwest.Create(t, consumerPath, kcpCRDClusterClient, cowBoysGR) + + exportName := "crds" + apiExport := apisv1alpha1.APIExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: exportName, + }, + Spec: apisv1alpha1.APIExportSpec{ + PermissionClaims: []apisv1alpha1.PermissionClaim{ + { + GroupResource: apisv1alpha1.GroupResource{ + Group: "apiextensions.k8s.io", + Resource: "customresourcedefinitions", + }, + All: true, + }, + }, + }, + } + + _, err = kcpClusterClient.Cluster(providerPath).ApisV1alpha1().APIExports().Create(ctx, &apiExport, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create api export") + + // validate the valid claims condition occurs + t.Logf("validate that the permission claim's conditions true") + framework.EventuallyCondition(t, func() (conditions.Getter, error) { + return kcpClusterClient.Cluster(providerPath).ApisV1alpha1().APIExports().Get(ctx, exportName, metav1.GetOptions{}) + }, framework.Is(apisv1alpha1.APIExportIdentityValid), "could not wait for APIExport to be valid with identity hash") + + apiBinding := apisv1alpha1.APIBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: exportName, + }, + Spec: apisv1alpha1.APIBindingSpec{ + Reference: apisv1alpha1.BindingReference{ + Export: &apisv1alpha1.ExportBindingReference{ + Path: providerPath.String(), + Name: exportName, + }, + }, + PermissionClaims: []apisv1alpha1.AcceptablePermissionClaim{ + { + PermissionClaim: apisv1alpha1.PermissionClaim{ + GroupResource: apisv1alpha1.GroupResource{ + Group: "apiextensions.k8s.io", + Resource: "customresourcedefinitions", + }, + All: true, + }, + State: apisv1alpha1.ClaimAccepted, + }, + }, + }, + } + + _, err = kcpClusterClient.Cluster(consumerPath).ApisV1alpha1().APIBindings().Create(ctx, &apiBinding, metav1.CreateOptions{}) + require.NoError(t, err, "failed to create api binding") + + t.Logf("Validate that the permission claims are valid") + framework.EventuallyCondition(t, func() (conditions.Getter, error) { + return kcpClusterClient.Cluster(consumerPath).ApisV1alpha1().APIBindings().Get(ctx, exportName, metav1.GetOptions{}) + }, framework.Is(apisv1alpha1.PermissionClaimsValid), "unable to see valid claims") + + export, err := kcpClusterClient.Cluster(providerPath).ApisV1alpha1().APIExports().Get(ctx, exportName, metav1.GetOptions{}) + require.NoError(t, err) + + virtualWorkspaceURL := export.Status.VirtualWorkspaces[0].URL + t.Logf("Found virtual workspace URL: %v", virtualWorkspaceURL) + + rawConfig, err := server.RawConfig() + require.NoError(t, err) + + vwClusterClient, err := kcpdynamic.NewForConfig(apiexportVWConfig(t, rawConfig, virtualWorkspaceURL)) + require.NoError(t, err) + gvr := apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions") + + list, err := vwClusterClient.Resource(gvr).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, list.Items, 1, "expected to find one crs instance") +} diff --git a/test/e2e/virtual/apiexport/virtualworkspace_test.go b/test/e2e/virtual/apiexport/virtualworkspace_test.go index 0565a051d0f1..63f5d5fa046a 100644 --- a/test/e2e/virtual/apiexport/virtualworkspace_test.go +++ b/test/e2e/virtual/apiexport/virtualworkspace_test.go @@ -825,8 +825,8 @@ func gatherInternalAPIs(t *testing.T, discoveryClient discovery.DiscoveryInterfa for _, apiResourcesList := range apiResourcesLists { gv, err := schema.ParseGroupVersion(apiResourcesList.GroupVersion) require.NoError(t, err) - // ignore kcp resources - if strings.HasSuffix(gv.Group, ".kcp.io") { + // ignore kcp resources expect for core.kcp.io + if strings.HasSuffix(gv.Group, ".kcp.io") && gv.Group != "core.kcp.io" { continue } // ignore authn/authz non-crud apis