From e564b4f8d8a0ce706b10d3a7fe00a4387d0ca725 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Fri, 14 Oct 2022 17:29:17 -0400 Subject: [PATCH] Add APIConversion support Signed-off-by: Andy Goldstein --- config/crds/apis.kcp.dev_apiconversions.yaml | 122 +++++++ config/system-crds/bootstrap.go | 1 + go.mod | 56 +-- go.sum | 88 ++--- hack/logcheck.out | 14 +- .../apiconversion/apiconversion_admission.go | 153 +++++++++ pkg/admission/plugins.go | 4 + pkg/apis/apis/v1alpha1/register.go | 3 + .../apis/v1alpha1/types_apiresourceschema.go | 104 +++++- .../apis/v1alpha1/zz_generated.deepcopy.go | 125 +++++++ pkg/cache/server/config.go | 15 - .../typed/apis/v1alpha1/apiconversion.go | 181 ++++++++++ .../typed/apis/v1alpha1/apis_client.go | 5 + .../apis/v1alpha1/fake/fake_apiconversion.go | 123 +++++++ .../apis/v1alpha1/fake/fake_apis_client.go | 4 + .../apis/v1alpha1/generated_expansion.go | 2 + .../apis/v1alpha1/apiconversion.go | 103 ++++++ .../apis/v1alpha1/interface.go | 7 + .../informers/externalversions/generic.go | 2 + .../listers/apis/v1alpha1/apiconversion.go | 69 ++++ .../apis/v1alpha1/expansion_generated.go | 4 + pkg/conversion/conversion.go | 176 ++++++++++ pkg/openapi/zz_generated.openapi.go | 225 ++++++++++++ .../apis/apibinding/apibinding_controller.go | 82 ++++- .../apis/apibinding/apibinding_reconcile.go | 86 +++++ pkg/server/api_conversion.go | 325 ++++++++++++++++++ pkg/server/config.go | 19 +- pkg/server/controllers.go | 2 + pkg/server/handler.go | 12 - test/e2e/conversion/conversion_test.go | 150 ++++++++ test/e2e/conversion/resources.yaml | 121 +++++++ test/e2e/conversion/v1-widget.yaml | 7 + test/e2e/conversion/v2-widget.yaml | 10 + 33 files changed, 2265 insertions(+), 135 deletions(-) create mode 100644 config/crds/apis.kcp.dev_apiconversions.yaml create mode 100644 pkg/admission/apiconversion/apiconversion_admission.go create mode 100644 pkg/client/clientset/versioned/typed/apis/v1alpha1/apiconversion.go create mode 100644 pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apiconversion.go create mode 100644 pkg/client/informers/externalversions/apis/v1alpha1/apiconversion.go create mode 100644 pkg/client/listers/apis/v1alpha1/apiconversion.go create mode 100644 pkg/conversion/conversion.go create mode 100644 pkg/server/api_conversion.go create mode 100644 test/e2e/conversion/conversion_test.go create mode 100644 test/e2e/conversion/resources.yaml create mode 100644 test/e2e/conversion/v1-widget.yaml create mode 100644 test/e2e/conversion/v2-widget.yaml diff --git a/config/crds/apis.kcp.dev_apiconversions.yaml b/config/crds/apis.kcp.dev_apiconversions.yaml new file mode 100644 index 000000000000..ce7f8203eee4 --- /dev/null +++ b/config/crds/apis.kcp.dev_apiconversions.yaml @@ -0,0 +1,122 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: apiconversions.apis.kcp.dev +spec: + group: apis.kcp.dev + names: + categories: + - kcp + kind: APIConversion + listKind: APIConversionList + plural: apiconversions + singular: apiconversion + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: APIConversion contains rules to convert between different API + versions in an APIResourceSchema. The name must match the name of the APIResourceSchema + for the conversions to take effect. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec holds the desired state. + properties: + conversions: + description: conversions specify rules to convert between different + API versions in an APIResourceSchema. + items: + description: APIVersionConversion contains rules to convert between + two specific API versions in an APIResourceSchema. Additionally, + to avoid data loss when round-tripping from a version that contains + a new field to one that doesn't and back again, you can specify + a list of fields to preserve (these are stored in annotations). + properties: + from: + description: from is the source version. + minLength: 1 + pattern: ^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$ + type: string + preserve: + description: preserve contains a list of JSONPath expressions + to fields to preserve in the originating version of the object, + relative to its, such as '.spec.name.first'. + items: + type: string + type: array + rules: + description: rules contains field-specific conversion expressions. + items: + description: APIConversionRule specifies how to convert a + single field. + properties: + destination: + description: destination is a JSONPath expression to the + field in the target version of the object, relative + to its root, such as '.spec.name.first'. + type: string + field: + description: field is a JSONPath expression to the field + in the originating version of the object, relative to + its root, such as '.spec.name.first'. + type: string + transformation: + description: transformation is an optional CEL expression + used to execute user-specified rules to transform the + originating field -- identified by 'self' -- to the + destination field. + type: string + required: + - destination + - field + type: object + type: array + x-kubernetes-list-map-keys: + - destination + x-kubernetes-list-type: map + to: + description: to is the target version. + minLength: 1 + pattern: ^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$ + type: string + required: + - from + - rules + - to + type: object + type: array + x-kubernetes-list-map-keys: + - from + - to + x-kubernetes-list-type: map + required: + - conversions + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/config/system-crds/bootstrap.go b/config/system-crds/bootstrap.go index ed8f0c6721bd..b4b41087e2d2 100644 --- a/config/system-crds/bootstrap.go +++ b/config/system-crds/bootstrap.go @@ -51,6 +51,7 @@ func Bootstrap(ctx context.Context, crdClient apiextensionsclient.Interface, dis {Group: apis.GroupName, Resource: "apiexports"}, {Group: apis.GroupName, Resource: "apibindings"}, {Group: apis.GroupName, Resource: "apiresourceschemas"}, + {Group: apis.GroupName, Resource: "apiconversions"}, } if err := wait.PollImmediateInfiniteWithContext(ctx, time.Second, func(ctx context.Context) (bool, error) { diff --git a/go.mod b/go.mod index ecb67c900d69..66524f299c0a 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/evanphx/json-patch v5.6.0+incompatible github.com/fatih/color v1.12.0 github.com/go-logr/logr v1.2.3 + github.com/google/cel-go v0.10.1 github.com/google/go-cmp v0.5.8 github.com/google/uuid v1.3.0 github.com/kcp-dev/apimachinery v0.0.0-20220912132244-efe716c18e43 @@ -32,6 +33,7 @@ require ( go.etcd.io/etcd/server/v3 v3.5.0 go.uber.org/multierr v1.7.0 golang.org/x/net v0.0.0-20220722155237-a158d28d115b + google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd gopkg.in/square/go-jose.v2 v2.2.2 k8s.io/api v0.24.3 k8s.io/apiextensions-apiserver v0.24.3 @@ -92,7 +94,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.1 // indirect - github.com/google/cel-go v0.10.1 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect @@ -167,7 +168,6 @@ require ( golang.org/x/tools v0.1.12 // indirect gonum.org/v1/gonum v0.6.2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect google.golang.org/grpc v1.46.2 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -191,30 +191,30 @@ require ( replace ( github.com/kcp-dev/kcp/pkg/apis => ./pkg/apis - k8s.io/api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/apiextensions-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/apimachinery => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/cli-runtime => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/client-go => github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/cloud-provider => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/cluster-bootstrap => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/code-generator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/component-base => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/component-helpers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/cri-api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/csi-translation-lib => github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/kube-aggregator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/kube-controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/kube-proxy => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/kube-scheduler => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/kubectl => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/kubelet => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/kubernetes => github.com/kcp-dev/kubernetes v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/legacy-cloud-providers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/metrics => github.com/kcp-dev/kubernetes/staging/src/k8s.io/metrics v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/mount-utils => github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/pod-security-admission => github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20221005071841-6cfb7d485cbf - k8s.io/sample-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20221005071841-6cfb7d485cbf + k8s.io/api => github.com/ncdc/kubernetes/staging/src/k8s.io/api v0.0.0-20221014211755-cf066b1323e1 + k8s.io/apiextensions-apiserver => github.com/ncdc/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20221014211755-cf066b1323e1 + k8s.io/apimachinery => github.com/ncdc/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20221014211755-cf066b1323e1 + k8s.io/apiserver => github.com/ncdc/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20221014211755-cf066b1323e1 + k8s.io/cli-runtime => github.com/ncdc/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20221014211755-cf066b1323e1 + k8s.io/client-go => github.com/ncdc/kubernetes/staging/src/k8s.io/client-go v0.0.0-20221014211755-cf066b1323e1 + k8s.io/cloud-provider => github.com/ncdc/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20221014211755-cf066b1323e1 + k8s.io/cluster-bootstrap => github.com/ncdc/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20221014211755-cf066b1323e1 + k8s.io/code-generator => github.com/ncdc/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20221014211755-cf066b1323e1 + k8s.io/component-base => github.com/ncdc/kubernetes/staging/src/k8s.io/component-base v0.0.0-20221014211755-cf066b1323e1 + k8s.io/component-helpers => github.com/ncdc/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20221014211755-cf066b1323e1 + k8s.io/controller-manager => github.com/ncdc/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20221014211755-cf066b1323e1 + k8s.io/cri-api => github.com/ncdc/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20221014211755-cf066b1323e1 + k8s.io/csi-translation-lib => github.com/ncdc/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20221014211755-cf066b1323e1 + k8s.io/kube-aggregator => github.com/ncdc/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20221014211755-cf066b1323e1 + k8s.io/kube-controller-manager => github.com/ncdc/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20221014211755-cf066b1323e1 + k8s.io/kube-proxy => github.com/ncdc/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20221014211755-cf066b1323e1 + k8s.io/kube-scheduler => github.com/ncdc/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20221014211755-cf066b1323e1 + k8s.io/kubectl => github.com/ncdc/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20221014211755-cf066b1323e1 + k8s.io/kubelet => github.com/ncdc/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20221014211755-cf066b1323e1 + k8s.io/kubernetes => github.com/ncdc/kubernetes v0.0.0-20221014211755-cf066b1323e1 + k8s.io/legacy-cloud-providers => github.com/ncdc/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20221014211755-cf066b1323e1 + k8s.io/metrics => github.com/ncdc/kubernetes/staging/src/k8s.io/metrics v0.0.0-20221014211755-cf066b1323e1 + k8s.io/mount-utils => github.com/ncdc/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20221014211755-cf066b1323e1 + k8s.io/pod-security-admission => github.com/ncdc/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20221014211755-cf066b1323e1 + k8s.io/sample-apiserver => github.com/ncdc/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20221014211755-cf066b1323e1 ) diff --git a/go.sum b/go.sum index 908d5ac1da5e..77e8d4589312 100644 --- a/go.sum +++ b/go.sum @@ -461,50 +461,6 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kcp-dev/apimachinery v0.0.0-20220912132244-efe716c18e43 h1:vPv81j3mT5VYQ6YnCXrnKJQPeRNHwPcGJNsQNQfIG9Q= github.com/kcp-dev/apimachinery v0.0.0-20220912132244-efe716c18e43/go.mod h1:qnvUHkdxOrNzX17yX+z8r81CZEBuFdveNzWqFlwZ55w= -github.com/kcp-dev/kubernetes v0.0.0-20221005071841-6cfb7d485cbf h1:IEBrvL6I0eMYYUQ6gYAR5/6fDxu2nW486XkmwF6rd7Q= -github.com/kcp-dev/kubernetes v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:x3RxHGS2ZEGxxiakIQx3KJJJ9T5Q0DqFKWjIjIRSGCY= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20221005071841-6cfb7d485cbf h1:x8CiJUPnoSQL4dPleEBq1bMzC32IchRg0ZakFfTFYmk= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:IpPnJRE5t3olVaut5p67N16cZkWwwU5KVFM35xCKyxM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20221005071841-6cfb7d485cbf h1:Aoh1k/LE1vlG/AQ4rvj7q0TxoR4XC89dAFD0l0KJ/gk= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:6oSWzzGWMkE8w0yGHadnWyAxSgfv4KxMFZTrBWPGw9E= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20221005071841-6cfb7d485cbf h1:PjxeesT9bo8Fx6UQHCl68z5MBl3xWeoPGnDDkx06Xw8= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:9BXCsgESAOaJVjextCdJRvSGzzGQnC/sepABcOQuICQ= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20221005071841-6cfb7d485cbf h1:0Z7fNGi6yoEFLxjjKCQYh6UFJXjpCfHD43gA0S3Wj1w= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:UZcl2eMhBznRKRpxUm33RrFip03hlsTb4mVQJt4Eu9E= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20221005071841-6cfb7d485cbf h1:C7eutAFSXtNPcyNuokOFE+e2fM8/8IXDskPiScVFE1M= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:+MNCNcmO36uOM9bS7HPhag9fE0CQExmmQiMc5v/gcZs= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20221005071841-6cfb7d485cbf h1:OvZPVxpP0O/WR5kmM80UIk6EpdYKT3VK5k734q4LFpU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:GDxzAPoZYD5r6ga5H9++PuYseRuib8TwLrCAOggxgMg= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20221005071841-6cfb7d485cbf h1:7c5jENKWF+fZQITOHusjia0GSW9k2KWSiXT6bVWdmeA= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:90kId2LxyyslN28OpOxNPzAcuVhzSMvKOq78GSDWOcQ= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20221005071841-6cfb7d485cbf h1:MALIn6n+PnU2B86foUiETzvxwzUT0+16zBlbwqHktHM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:n9eB8ECEtaq1CBjOSeb4aHV8lZbzYPGT06l2uKY2ICc= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20221005071841-6cfb7d485cbf h1:WSjhSWVdHuxKIc4C4b/5za6FadvQzd5CVaabXVoTSiY= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:+TJKHME55JimWzqz1d+2bQxHqSo4bofDuzO2tdE1MCM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20221005071841-6cfb7d485cbf h1:KFwtI8BOjr1FZe7BntR1dYPQOfpOYZzNdWxUpRYy3rM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:lU2mhvadf8dTfE0i9Cm6JRz8ZE7gB8UnUbmG+NWeMAg= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20221005071841-6cfb7d485cbf h1:c13Vs2J3mZfP2n0czy3sbekCIxC4/P4X7uNakM+LOLg= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:JW1F/7H1Vd7HcD3A42iRZkLY2HTzceb1clY4fNd1LMU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20221005071841-6cfb7d485cbf h1:x3xbZjAD0tKU5hQQlIG5zJSRCUm+jxHYZ+xKht2O3SM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:agj885OpxzGI5Gbg5Ouaj25RyjKDsi/icMfDyqXbRwQ= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:VXzy1lqXPZW/hmxjBxZJUJbSMuVVlYz1y4mFPlV0jPc= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:qbfH+aRKr2Otv6Ing9vpaN7Sn2EYxr0kc++Gf0vnorc= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20221005071841-6cfb7d485cbf h1:KyeRgzqsx9k4+IbHHlZFeBQalS0DbBoiGurcqW36I4M= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:BNy4rh6giPrjqIE9w8eJ8flUPf5HYeN8yPfDvsoQvSU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20221005071841-6cfb7d485cbf h1:VNuWCRCzdzvJDw6qYia7VUz2j/lnHOyWrq6Ttp6m1SA= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:U3g0FGkoClR+edPeq1zcTKT3eNK/ZFzZALOcy9VsGMo= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:6mEp02ABsuOeeBuUrrol78v9LYysX7Z8CZOMFlkPOOI= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:xZnfOrGTta6rB9IWNKl82yzWKpMSUXVmyGHRilQ9kzM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:E3VGIQ1oVi7/DGsFJI3LEyLJJpMfRfg2EGdAr2Icef0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20221005071841-6cfb7d485cbf h1:SAI7XYjvMVDrEpqAALSF6Uxa4M/bMyDlSnEZxKfF3BY= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:qAKTVSBMo7gQhI/b+XhwJiACAC13pezoyyiQ3aqlctU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:zK6FFFrw2zrr22NEqDhAmMmgNSi8yWKbAbggTw2BxCc= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/metrics v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:0eE+ZenIbUy0MJH/T71Q4CSYKzAi1eatC7Y7cuzTiQA= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20221005071841-6cfb7d485cbf h1:0nV/xBAXN81MtlQOkHOGATsiobjFbj9u/+9dtJ22G24= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:ZbNHTLq12/PZiOToER9eSg3JyaV2f/aPRmcaQNsX4vw= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20221005071841-6cfb7d485cbf h1:f9/leSSf9ohKCczbYgOsEoj/p9tl80wNaPV4XdG18NU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:Iim5xqRnknXtHPXyZjZdfxEdASa/l/nHShGazinoYbQ= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20221005071841-6cfb7d485cbf/go.mod h1:7+QSUfC8FyVELcrtSpeAUGrgUoXlUQ+ZT32QCeoR91s= github.com/kcp-dev/logicalcluster/v2 v2.0.0-alpha.1/go.mod h1:lfWJL764jKFJxZWOGuFuT3PCCLPo6lV5Cl8P7u9T05g= github.com/kcp-dev/logicalcluster/v2 v2.0.0-alpha.3 h1:+DwIG/loh2nDB9c/FqNvLzFFq/YtBliLxAfw/uWNzyE= github.com/kcp-dev/logicalcluster/v2 v2.0.0-alpha.3/go.mod h1:lfWJL764jKFJxZWOGuFuT3PCCLPo6lV5Cl8P7u9T05g= @@ -595,6 +551,50 @@ github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq4 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncdc/kubernetes v0.0.0-20221014211755-cf066b1323e1 h1:F/qreHi7lkWSDy9K0q9mxiixTnZ2Vq1o2u4rn9HacXs= +github.com/ncdc/kubernetes v0.0.0-20221014211755-cf066b1323e1/go.mod h1:x3RxHGS2ZEGxxiakIQx3KJJJ9T5Q0DqFKWjIjIRSGCY= +github.com/ncdc/kubernetes/staging/src/k8s.io/api v0.0.0-20221014211755-cf066b1323e1 h1:aL46ASstDEJ1cBptMpQ7mv2ttLoGJ1hGL6WLEs11kto= +github.com/ncdc/kubernetes/staging/src/k8s.io/api v0.0.0-20221014211755-cf066b1323e1/go.mod h1:IpPnJRE5t3olVaut5p67N16cZkWwwU5KVFM35xCKyxM= +github.com/ncdc/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20221014211755-cf066b1323e1 h1:Vo2UNOnWMlZ4i6xQwEf8V1+QPtIiT20o/+xDQelB0RY= +github.com/ncdc/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20221014211755-cf066b1323e1/go.mod h1:6oSWzzGWMkE8w0yGHadnWyAxSgfv4KxMFZTrBWPGw9E= +github.com/ncdc/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20221014211755-cf066b1323e1 h1:8284hZGToPkv7mOC9LKPZdHjcAkG2g5NpWAMBF5AR+4= +github.com/ncdc/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20221014211755-cf066b1323e1/go.mod h1:9BXCsgESAOaJVjextCdJRvSGzzGQnC/sepABcOQuICQ= +github.com/ncdc/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20221014211755-cf066b1323e1 h1:FiQUoW6+6oggCNskBTuKiD9CGSO8KsF9UR5lunDDQlc= +github.com/ncdc/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20221014211755-cf066b1323e1/go.mod h1:UZcl2eMhBznRKRpxUm33RrFip03hlsTb4mVQJt4Eu9E= +github.com/ncdc/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20221014211755-cf066b1323e1 h1:PPX+hu4LqhxWGEal16ISoPo9C+Jtz0V/wPrfTHnJPTM= +github.com/ncdc/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20221014211755-cf066b1323e1/go.mod h1:+MNCNcmO36uOM9bS7HPhag9fE0CQExmmQiMc5v/gcZs= +github.com/ncdc/kubernetes/staging/src/k8s.io/client-go v0.0.0-20221014211755-cf066b1323e1 h1:nUYZInISOXlMbvwGaPMnlHwfMb7DbACoKyY5PYtQby8= +github.com/ncdc/kubernetes/staging/src/k8s.io/client-go v0.0.0-20221014211755-cf066b1323e1/go.mod h1:GDxzAPoZYD5r6ga5H9++PuYseRuib8TwLrCAOggxgMg= +github.com/ncdc/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20221014211755-cf066b1323e1 h1:8gLmgBdHgpz1J356kIIJoz/QMoHjZoJoP/wk02/7LCU= +github.com/ncdc/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20221014211755-cf066b1323e1/go.mod h1:90kId2LxyyslN28OpOxNPzAcuVhzSMvKOq78GSDWOcQ= +github.com/ncdc/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20221014211755-cf066b1323e1 h1:PHPxnqD/RGl0LTFn8jeOe+KrcxZQi23EvSLIwg/IPi0= +github.com/ncdc/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20221014211755-cf066b1323e1/go.mod h1:n9eB8ECEtaq1CBjOSeb4aHV8lZbzYPGT06l2uKY2ICc= +github.com/ncdc/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20221014211755-cf066b1323e1 h1:G97WeARXzMKltaI81SWCvPwan2QQjHy917l+TETiBlg= +github.com/ncdc/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20221014211755-cf066b1323e1/go.mod h1:+TJKHME55JimWzqz1d+2bQxHqSo4bofDuzO2tdE1MCM= +github.com/ncdc/kubernetes/staging/src/k8s.io/component-base v0.0.0-20221014211755-cf066b1323e1 h1:+TkYxWvBvFvwOx9Z9SYfYh9H8CfC9CDUUyRQxKtfxv0= +github.com/ncdc/kubernetes/staging/src/k8s.io/component-base v0.0.0-20221014211755-cf066b1323e1/go.mod h1:lU2mhvadf8dTfE0i9Cm6JRz8ZE7gB8UnUbmG+NWeMAg= +github.com/ncdc/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20221014211755-cf066b1323e1 h1:NdJU7MOEArZqUAKyCKknxhv92ECUPr9rgmzWAfb9oAQ= +github.com/ncdc/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20221014211755-cf066b1323e1/go.mod h1:JW1F/7H1Vd7HcD3A42iRZkLY2HTzceb1clY4fNd1LMU= +github.com/ncdc/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20221014211755-cf066b1323e1 h1:u8zTcrOMjLOPJlt241LRZNV2Hb9dyeos6HKTWBy7otY= +github.com/ncdc/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20221014211755-cf066b1323e1/go.mod h1:agj885OpxzGI5Gbg5Ouaj25RyjKDsi/icMfDyqXbRwQ= +github.com/ncdc/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20221014211755-cf066b1323e1/go.mod h1:VXzy1lqXPZW/hmxjBxZJUJbSMuVVlYz1y4mFPlV0jPc= +github.com/ncdc/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20221014211755-cf066b1323e1/go.mod h1:qbfH+aRKr2Otv6Ing9vpaN7Sn2EYxr0kc++Gf0vnorc= +github.com/ncdc/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20221014211755-cf066b1323e1 h1:Ux+dRqfvjAAiMy4kGYNUPbq1n4CQiWWWhQxc6kCNh+w= +github.com/ncdc/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20221014211755-cf066b1323e1/go.mod h1:BNy4rh6giPrjqIE9w8eJ8flUPf5HYeN8yPfDvsoQvSU= +github.com/ncdc/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20221014211755-cf066b1323e1 h1:JVOnQ563p3IGoEzl2NMeVjVLCpSDeZfSuUjeQFBy40Q= +github.com/ncdc/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20221014211755-cf066b1323e1/go.mod h1:U3g0FGkoClR+edPeq1zcTKT3eNK/ZFzZALOcy9VsGMo= +github.com/ncdc/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20221014211755-cf066b1323e1/go.mod h1:6mEp02ABsuOeeBuUrrol78v9LYysX7Z8CZOMFlkPOOI= +github.com/ncdc/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20221014211755-cf066b1323e1/go.mod h1:xZnfOrGTta6rB9IWNKl82yzWKpMSUXVmyGHRilQ9kzM= +github.com/ncdc/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20221014211755-cf066b1323e1/go.mod h1:E3VGIQ1oVi7/DGsFJI3LEyLJJpMfRfg2EGdAr2Icef0= +github.com/ncdc/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20221014211755-cf066b1323e1 h1:4PbPAN6d9X4eSQntYhZobiMzd4Nv6ZZ7OxFVQ4apwUY= +github.com/ncdc/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20221014211755-cf066b1323e1/go.mod h1:qAKTVSBMo7gQhI/b+XhwJiACAC13pezoyyiQ3aqlctU= +github.com/ncdc/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20221014211755-cf066b1323e1/go.mod h1:zK6FFFrw2zrr22NEqDhAmMmgNSi8yWKbAbggTw2BxCc= +github.com/ncdc/kubernetes/staging/src/k8s.io/metrics v0.0.0-20221014211755-cf066b1323e1/go.mod h1:0eE+ZenIbUy0MJH/T71Q4CSYKzAi1eatC7Y7cuzTiQA= +github.com/ncdc/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20221014211755-cf066b1323e1 h1:PMqPNS8od14UIFma3vemyL1fyjsgu3WcTwmIug/zUm8= +github.com/ncdc/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20221014211755-cf066b1323e1/go.mod h1:ZbNHTLq12/PZiOToER9eSg3JyaV2f/aPRmcaQNsX4vw= +github.com/ncdc/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20221014211755-cf066b1323e1 h1:2+u0vGaPxnEIk+eyec0LocSOGqec6K+astZ7QsVaBe8= +github.com/ncdc/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20221014211755-cf066b1323e1/go.mod h1:Iim5xqRnknXtHPXyZjZdfxEdASa/l/nHShGazinoYbQ= +github.com/ncdc/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20221014211755-cf066b1323e1/go.mod h1:7+QSUfC8FyVELcrtSpeAUGrgUoXlUQ+ZT32QCeoR91s= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= diff --git a/hack/logcheck.out b/hack/logcheck.out index 08906668ddf4..4ee331e28be6 100644 --- a/hack/logcheck.out +++ b/hack/logcheck.out @@ -38,14 +38,14 @@ /config/helpers/bootstrap.go:273:3: function "Infof" should not be used, convert to contextual logging /config/helpers/bootstrap.go:273:3: function "V" should not be used, convert to contextual logging /config/helpers/bootstrap.go:92:4: function "Infof" should not be used, convert to contextual logging -/config/system-crds/bootstrap.go:58:4: function "Errorf" should not be used, convert to contextual logging +/config/system-crds/bootstrap.go:59:4: function "Errorf" should not be used, convert to contextual logging /pkg/admission/kubequota/kubequota_admission.go:217:3: function "InfoS" should not be used, convert to contextual logging /pkg/admission/kubequota/kubequota_admission.go:217:3: function "V" should not be used, convert to contextual logging /pkg/admission/kubequota/kubequota_admission.go:221:2: function "InfoS" should not be used, convert to contextual logging /pkg/admission/kubequota/kubequota_admission.go:221:2: function "V" should not be used, convert to contextual logging /pkg/admission/kubequota/kubequota_clusterworkspace_monitor.go:76:2: function "Infof" should not be used, convert to contextual logging /pkg/admission/kubequota/kubequota_clusterworkspace_monitor.go:77:8: function "Infof" should not be used, convert to contextual logging -/pkg/admission/webhook/generic_webhook.go:101:5: function "Errorf" should not be used, convert to contextual logging +/pkg/admission/webhook/generic_webhook.go:102:5: function "Errorf" should not be used, convert to contextual logging /pkg/admission/webhook/generic_webhook.go:139:4: function "Errorf" should not be used, convert to contextual logging /pkg/admission/webhook/generic_webhook.go:82:3: function "Infof" should not be used, convert to contextual logging /pkg/admission/webhook/generic_webhook.go:82:3: function "V" should not be used, convert to contextual logging @@ -112,7 +112,13 @@ /pkg/reconciler/workload/synctargetexports/synctargetexports_controller.go:331:2: function "V" should not be used, convert to contextual logging /pkg/reconciler/workload/synctargetexports/synctargetexports_controller.go:333:3: function "Errorf" should not be used, convert to contextual logging /pkg/reconciler/workload/synctargetexports/synctargetexports_reconcile.go:58:5: function "Warningf" should not be used, convert to contextual logging -/pkg/server/controllers.go:997:4: function "Errorf" should not be used, convert to contextual logging +/pkg/server/api_conversion.go:269:5: function "Errorf" should not be used, convert to contextual logging +/pkg/server/api_conversion.go:274:7: function "Errorf" should not be used, convert to contextual logging +/pkg/server/api_conversion.go:276:7: function "Errorf" should not be used, convert to contextual logging +/pkg/server/api_conversion.go:294:7: function "Errorf" should not be used, convert to contextual logging +/pkg/server/api_conversion.go:302:7: function "Errorf" should not be used, convert to contextual logging +/pkg/server/api_conversion.go:312:6: function "Errorf" should not be used, convert to contextual logging +/pkg/server/controllers.go:1000:4: function "Errorf" should not be used, convert to contextual logging /pkg/server/home_workspaces.go:352:5: Additional arguments to WithValues should always be Key Value pairs. Please check if there is any key or value missing. /pkg/server/home_workspaces.go:638:6: Additional arguments to WithValues should always be Key Value pairs. Please check if there is any key or value missing. /pkg/server/options/controllers.go:54:3: function "Fatal" should not be used, convert to contextual logging @@ -129,7 +135,7 @@ /pkg/syncer/spec/spec_process.go:116:11: Key positional arguments are expected to be inlined constant strings. Please replace &{logging WorkspaceKey} provided with string value. /pkg/syncer/spec/spec_process.go:134:4: Key positional arguments are expected to be inlined constant strings. Please replace &{logging DownstreamNameKey} provided with string value. /pkg/syncer/spec/spec_process.go:152:11: Key positional arguments are expected to be inlined constant strings. Please replace &{logging DownstreamNamespaceKey} provided with string value. -/pkg/syncer/spec/spec_process.go:366:11: Key positional arguments are expected to be inlined constant strings. Please replace &{logging DownstreamNameKey} provided with string value. +/pkg/syncer/spec/spec_process.go:367:11: Key positional arguments are expected to be inlined constant strings. Please replace &{logging DownstreamNameKey} provided with string value. /pkg/syncer/status/status_process.go:138:11: Key positional arguments are expected to be inlined constant strings. Please replace &{logging NameKey} provided with string value. /pkg/syncer/status/status_process.go:138:11: Key positional arguments are expected to be inlined constant strings. Please replace &{logging NamespaceKey} provided with string value. /pkg/syncer/status/status_process.go:138:11: Key positional arguments are expected to be inlined constant strings. Please replace &{logging WorkspaceKey} provided with string value. diff --git a/pkg/admission/apiconversion/apiconversion_admission.go b/pkg/admission/apiconversion/apiconversion_admission.go new file mode 100644 index 000000000000..38dc80845dc6 --- /dev/null +++ b/pkg/admission/apiconversion/apiconversion_admission.go @@ -0,0 +1,153 @@ +/* +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 apiconversion + +import ( + "context" + "fmt" + "io" + + "github.com/kcp-dev/apimachinery/pkg/cache" + "github.com/kcp-dev/logicalcluster/v2" + + apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + + "github.com/kcp-dev/kcp/pkg/admission/initializers" + apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + kcpinformers "github.com/kcp-dev/kcp/pkg/client/informers/externalversions" + "github.com/kcp-dev/kcp/pkg/conversion" + "github.com/kcp-dev/kcp/pkg/reconciler/apis/apibinding" +) + +const ( + PluginName = "apis.kcp.dev/APIConversion" +) + +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, + func(_ io.Reader) (admission.Interface, error) { + return &apiConversionAdmission{ + Handler: admission.NewHandler(admission.Create, admission.Update), + }, nil + }) +} + +type apiConversionAdmission struct { + *admission.Handler + + getAPIResourceSchema func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIResourceSchema, error) +} + +// Ensure that the required admission interfaces are implemented. +var ( + _ = admission.ValidationInterface(&apiConversionAdmission{}) + _ = admission.InitializationValidator(&apiConversionAdmission{}) + _ = initializers.WantsKcpInformers(&apiConversionAdmission{}) +) + +// Validate validates the creation and updating of APIBinding resources. It also performs a SubjectAccessReview +// making sure the user is allowed to use the 'bind' verb with the referenced APIExport. +func (o *apiConversionAdmission) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error { + if a.GetResource().GroupResource() != apisv1alpha1.Resource("apiconversions") { + return nil + } + + cluster, err := genericapirequest.ValidClusterFrom(ctx) + if err != nil { + return admission.NewForbidden(a, fmt.Errorf("error determining workspace: %w", err)) + } + + if cluster.Name == apibinding.ShadowWorkspaceName { + // TODO(ncdc): do we also want to validate these conversions? They've already been validated once (in their + // original logical cluster). + return nil + } + + u, ok := a.GetObject().(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("unexpected type %T", a.GetObject()) + } + + apiResourceSchema, err := o.getAPIResourceSchema(cluster.Name, u.GetName()) + if err != nil { + return admission.NewForbidden(a, fmt.Errorf("error getting APIResourceSchema %s: %w", u.GetName(), err)) + } + + apiConversion := &apisv1alpha1.APIConversion{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, apiConversion); err != nil { + return fmt.Errorf("failed to convert unstructured to APIConversion: %w", err) + } + + structuralSchemas := map[string]*structuralschema.Structural{} + for i, v := range apiResourceSchema.Spec.Versions { + schema, err := v.GetSchema() + if err != nil { + return admission.NewForbidden(a, field.Required(field.NewPath("spec", "versions").Index(i).Child("schema"), "is required")) + } + + internalJSONSchemaProps := &apiextensionsinternal.JSONSchemaProps{} + if err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(schema, internalJSONSchemaProps, nil); err != nil { + return fmt.Errorf("failed converting version %s validation to internal version: %w", v.Name, err) + } + + structuralSchema, err := structuralschema.NewStructural(internalJSONSchemaProps) + if err != nil { + return fmt.Errorf("error getting structural schema for version %s: %w", v.Name, err) + } + + structuralSchemas[v.Name] = structuralSchema + } + + compiledRules, err := conversion.CompileConversions(apiConversion, structuralSchemas) + if err != nil { + return fmt.Errorf("error compiling conversion rules: %w", err) + } + + for version, rules := range compiledRules { + for _, rule := range rules { + if rule.Err != nil { + // TODO aggregate and return multiple, up to some max truncated length? + // TODO use proper field path + return admission.NewForbidden(a, fmt.Errorf("error compiling conversion rules for version %s: %w", version, rule.Err)) + } + } + } + + return nil +} + +// ValidateInitialization ensures the required injected fields are set. +func (o *apiConversionAdmission) ValidateInitialization() error { + if o.getAPIResourceSchema == nil { + return fmt.Errorf("getAPIResourceSchema is unset") + } + + return nil +} + +func (o *apiConversionAdmission) SetKcpInformers(f kcpinformers.SharedInformerFactory) { + o.getAPIResourceSchema = func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIResourceSchema, error) { + return f.Apis().V1alpha1().APIResourceSchemas().Lister().Get(cache.ToClusterAwareKey(clusterName.String(), "", name)) + } +} diff --git a/pkg/admission/plugins.go b/pkg/admission/plugins.go index ed5d893b3ca3..740b1ebcbe0c 100644 --- a/pkg/admission/plugins.go +++ b/pkg/admission/plugins.go @@ -41,6 +41,7 @@ import ( "github.com/kcp-dev/kcp/pkg/admission/apibinding" "github.com/kcp-dev/kcp/pkg/admission/apibindingfinalizer" + "github.com/kcp-dev/kcp/pkg/admission/apiconversion" "github.com/kcp-dev/kcp/pkg/admission/apiresourceschema" "github.com/kcp-dev/kcp/pkg/admission/clusterworkspace" "github.com/kcp-dev/kcp/pkg/admission/clusterworkspacefinalizer" @@ -63,6 +64,7 @@ import ( var AllOrderedPlugins = beforeWebhooks(kubeapiserveroptions.AllOrderedPlugins, workspacenamespacelifecycle.PluginName, apiresourceschema.PluginName, + apiconversion.PluginName, clusterworkspace.PluginName, clusterworkspacefinalizer.PluginName, clusterworkspaceshard.PluginName, @@ -102,6 +104,7 @@ func RegisterAllKcpAdmissionPlugins(plugins *admission.Plugins) { clusterworkspacetype.Register(plugins) clusterworkspacetypeexists.Register(plugins) apiresourceschema.Register(plugins) + apiconversion.Register(plugins) apibinding.Register(plugins) apibindingfinalizer.Register(plugins) workspacenamespacelifecycle.Register(plugins) @@ -130,6 +133,7 @@ var defaultOnPluginsInKcp = sets.NewString( clusterworkspacetype.PluginName, clusterworkspacetypeexists.PluginName, apiresourceschema.PluginName, + apiconversion.PluginName, apibinding.PluginName, apibindingfinalizer.PluginName, kcpvalidatingwebhook.PluginName, diff --git a/pkg/apis/apis/v1alpha1/register.go b/pkg/apis/apis/v1alpha1/register.go index a5006ae99b61..178c5b6fe00e 100644 --- a/pkg/apis/apis/v1alpha1/register.go +++ b/pkg/apis/apis/v1alpha1/register.go @@ -53,6 +53,9 @@ func addKnownTypes(scheme *runtime.Scheme) error { &APIResourceSchema{}, &APIResourceSchemaList{}, + + &APIConversion{}, + &APIConversionList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/apis/v1alpha1/types_apiresourceschema.go b/pkg/apis/apis/v1alpha1/types_apiresourceschema.go index d4b4d30efab0..23074248d43f 100644 --- a/pkg/apis/apis/v1alpha1/types_apiresourceschema.go +++ b/pkg/apis/apis/v1alpha1/types_apiresourceschema.go @@ -24,17 +24,17 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -// APIResourceSchema describes a resource, identified by (group, version, resource, schema). -// -// A APIResourceSchema is immutable and cannot be deleted if they are referenced by -// an APIExport in the same workspace. -// // +crd // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:scope=Cluster,categories=kcp // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// APIResourceSchema describes a resource, identified by (group, version, resource, schema). +// +// A APIResourceSchema is immutable and cannot be deleted if they are referenced by +// an APIExport in the same workspace. type APIResourceSchema struct { metav1.TypeMeta `json:",inline"` // +optional @@ -130,9 +130,9 @@ type APIResourceVersion struct { AdditionalPrinterColumns []apiextensionsv1.CustomResourceColumnDefinition `json:"additionalPrinterColumns,omitempty"` } -// APIResourceSchemaList is a list of APIResourceSchema resources -// // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// APIResourceSchemaList is a list of APIResourceSchema resources type APIResourceSchemaList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` @@ -163,3 +163,93 @@ func (v *APIResourceVersion) SetSchema(schema *apiextensionsv1.JSONSchemaProps) v.Schema.Raw = raw return nil } + +// +crd +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,categories=kcp +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// APIConversion contains rules to convert between different API versions in an APIResourceSchema. The name must match +// the name of the APIResourceSchema for the conversions to take effect. +type APIConversion struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + // Spec holds the desired state. + Spec APIConversionSpec `json:"spec"` +} + +// APIConversionSpec contains rules to convert between different API versions in an APIResourceSchema. +type APIConversionSpec struct { + // +required + // +listType=map + // +listMapKey=from + // +listMapKey=to + + // conversions specify rules to convert between different API versions in an APIResourceSchema. + Conversions []APIVersionConversion `json:"conversions"` +} + +// APIVersionConversion contains rules to convert between two specific API versions in an +// APIResourceSchema. Additionally, to avoid data loss when round-tripping from a version that +// contains a new field to one that doesn't and back again, you can specify a list of fields to +// preserve (these are stored in annotations). +type APIVersionConversion struct { + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$ + + // from is the source version. + From string `json:"from"` + + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=^v[1-9][0-9]*([a-z]+[1-9][0-9]*)?$ + + // to is the target version. + To string `json:"to"` + + // +required + // +listType=map + // +listMapKey=destination + + // rules contains field-specific conversion expressions. + Rules []APIConversionRule `json:"rules"` + + // +optional + + // preserve contains a list of JSONPath expressions to fields to preserve in the originating version of the object, relative to its, such as + // '.spec.name.first'. + Preserve []string `json:"preserve,omitempty"` +} + +// APIConversionRule specifies how to convert a single field. +type APIConversionRule struct { + // +required + + // field is a JSONPath expression to the field in the originating version of the object, relative to its root, such + // as '.spec.name.first'. + Field string `json:"field"` + + // +required + + // destination is a JSONPath expression to the field in the target version of the object, relative to its root, such as '.spec.name.first'. + Destination string `json:"destination"` + + // +optional + + // transformation is an optional CEL expression used to execute user-specified rules to transform the originating field -- identified by 'self' -- to the destination field. + Transformation string `json:"transformation,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// APIConversionList is a list of APIConversion resources. +type APIConversionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []APIConversion `json:"items"` +} diff --git a/pkg/apis/apis/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apis/v1alpha1/zz_generated.deepcopy.go index 7b4a90ab96d1..2b890e0433d8 100644 --- a/pkg/apis/apis/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/apis/v1alpha1/zz_generated.deepcopy.go @@ -152,6 +152,105 @@ func (in *APIBindingStatus) DeepCopy() *APIBindingStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIConversion) DeepCopyInto(out *APIConversion) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIConversion. +func (in *APIConversion) DeepCopy() *APIConversion { + if in == nil { + return nil + } + out := new(APIConversion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIConversion) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIConversionList) DeepCopyInto(out *APIConversionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]APIConversion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIConversionList. +func (in *APIConversionList) DeepCopy() *APIConversionList { + if in == nil { + return nil + } + out := new(APIConversionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIConversionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIConversionRule) DeepCopyInto(out *APIConversionRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIConversionRule. +func (in *APIConversionRule) DeepCopy() *APIConversionRule { + if in == nil { + return nil + } + out := new(APIConversionRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIConversionSpec) DeepCopyInto(out *APIConversionSpec) { + *out = *in + if in.Conversions != nil { + in, out := &in.Conversions, &out.Conversions + *out = make([]APIVersionConversion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIConversionSpec. +func (in *APIConversionSpec) DeepCopy() *APIConversionSpec { + if in == nil { + return nil + } + out := new(APIConversionSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *APIExport) DeepCopyInto(out *APIExport) { *out = *in @@ -389,6 +488,32 @@ func (in *APIResourceVersion) DeepCopy() *APIResourceVersion { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIVersionConversion) DeepCopyInto(out *APIVersionConversion) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]APIConversionRule, len(*in)) + copy(*out, *in) + } + if in.Preserve != nil { + in, out := &in.Preserve, &out.Preserve + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIVersionConversion. +func (in *APIVersionConversion) DeepCopy() *APIVersionConversion { + if in == nil { + return nil + } + out := new(APIVersionConversion) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AcceptablePermissionClaim) DeepCopyInto(out *AcceptablePermissionClaim) { *out = *in diff --git a/pkg/cache/server/config.go b/pkg/cache/server/config.go index 02910d79e08d..fe36d7e7af95 100644 --- a/pkg/cache/server/config.go +++ b/pkg/cache/server/config.go @@ -17,10 +17,8 @@ limitations under the License. package server import ( - "errors" "fmt" "net/http" - "net/url" "time" kcpclienthelper "github.com/kcp-dev/apimachinery/pkg/client" @@ -38,7 +36,6 @@ import ( genericapiserver "k8s.io/apiserver/pkg/server" genericoptions "k8s.io/apiserver/pkg/server/options" utilfeature "k8s.io/apiserver/pkg/util/feature" - "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/rest" "k8s.io/kubernetes/pkg/genericcontrolplane/clientutils" @@ -212,22 +209,10 @@ func NewConfig(opts *cacheserveroptions.CompletedOptions, optionalLocalShardRest // Wire in a ServiceResolver that always returns an error that ResolveEndpoint is not yet // supported. The effect is that CRD webhook conversions are not supported and will always get an // error. - ServiceResolver: &unimplementedServiceResolver{}, MasterCount: 1, - AuthResolverWrapper: webhook.NewDefaultAuthenticationInfoResolverWrapper(nil, nil, rt, nil), ClusterAwareCRDLister: &crdLister{lister: c.ApiExtensionsSharedInformerFactory.Apiextensions().V1().CustomResourceDefinitions().Lister()}, }, } return c, nil } - -// unimplementedServiceResolver is a webhook.ServiceResolver that always returns an error, because -// we have not implemented support for this yet. As a result, CRD webhook conversions are not -// supported. -type unimplementedServiceResolver struct{} - -// ResolveEndpoint always returns an error that this is not yet supported. -func (r *unimplementedServiceResolver) ResolveEndpoint(namespace string, name string, port int32) (*url.URL, error) { - return nil, errors.New("CRD webhook conversions are not supported") -} diff --git a/pkg/client/clientset/versioned/typed/apis/v1alpha1/apiconversion.go b/pkg/client/clientset/versioned/typed/apis/v1alpha1/apiconversion.go new file mode 100644 index 000000000000..4ee2efc998e0 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/apis/v1alpha1/apiconversion.go @@ -0,0 +1,181 @@ +/* +Copyright 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v2 "github.com/kcp-dev/logicalcluster/v2" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" + + v1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + scheme "github.com/kcp-dev/kcp/pkg/client/clientset/versioned/scheme" +) + +// APIConversionsGetter has a method to return a APIConversionInterface. +// A group's client should implement this interface. +type APIConversionsGetter interface { + APIConversions() APIConversionInterface +} + +// APIConversionInterface has methods to work with APIConversion resources. +type APIConversionInterface interface { + Create(ctx context.Context, aPIConversion *v1alpha1.APIConversion, opts v1.CreateOptions) (*v1alpha1.APIConversion, error) + Update(ctx context.Context, aPIConversion *v1alpha1.APIConversion, opts v1.UpdateOptions) (*v1alpha1.APIConversion, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.APIConversion, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.APIConversionList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.APIConversion, err error) + APIConversionExpansion +} + +// aPIConversions implements APIConversionInterface +type aPIConversions struct { + client rest.Interface + cluster v2.Name +} + +// newAPIConversions returns a APIConversions +func newAPIConversions(c *ApisV1alpha1Client) *aPIConversions { + return &aPIConversions{ + client: c.RESTClient(), + cluster: c.cluster, + } +} + +// Get takes name of the aPIConversion, and returns the corresponding aPIConversion object, and an error if there is any. +func (c *aPIConversions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.APIConversion, err error) { + result = &v1alpha1.APIConversion{} + err = c.client.Get(). + Cluster(c.cluster). + Resource("apiconversions"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of APIConversions that match those selectors. +func (c *aPIConversions) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.APIConversionList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.APIConversionList{} + err = c.client.Get(). + Cluster(c.cluster). + Resource("apiconversions"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested aPIConversions. +func (c *aPIConversions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Cluster(c.cluster). + Resource("apiconversions"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a aPIConversion and creates it. Returns the server's representation of the aPIConversion, and an error, if there is any. +func (c *aPIConversions) Create(ctx context.Context, aPIConversion *v1alpha1.APIConversion, opts v1.CreateOptions) (result *v1alpha1.APIConversion, err error) { + result = &v1alpha1.APIConversion{} + err = c.client.Post(). + Cluster(c.cluster). + Resource("apiconversions"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(aPIConversion). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a aPIConversion and updates it. Returns the server's representation of the aPIConversion, and an error, if there is any. +func (c *aPIConversions) Update(ctx context.Context, aPIConversion *v1alpha1.APIConversion, opts v1.UpdateOptions) (result *v1alpha1.APIConversion, err error) { + result = &v1alpha1.APIConversion{} + err = c.client.Put(). + Cluster(c.cluster). + Resource("apiconversions"). + Name(aPIConversion.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(aPIConversion). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the aPIConversion and deletes it. Returns an error if one occurs. +func (c *aPIConversions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Cluster(c.cluster). + Resource("apiconversions"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *aPIConversions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Cluster(c.cluster). + Resource("apiconversions"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched aPIConversion. +func (c *aPIConversions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.APIConversion, err error) { + result = &v1alpha1.APIConversion{} + err = c.client.Patch(pt). + Cluster(c.cluster). + Resource("apiconversions"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go b/pkg/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go index e16418c70c53..5372ca3048d1 100644 --- a/pkg/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go +++ b/pkg/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go @@ -32,6 +32,7 @@ import ( type ApisV1alpha1Interface interface { RESTClient() rest.Interface APIBindingsGetter + APIConversionsGetter APIExportsGetter APIResourceSchemasGetter } @@ -46,6 +47,10 @@ func (c *ApisV1alpha1Client) APIBindings() APIBindingInterface { return newAPIBindings(c) } +func (c *ApisV1alpha1Client) APIConversions() APIConversionInterface { + return newAPIConversions(c) +} + func (c *ApisV1alpha1Client) APIExports() APIExportInterface { return newAPIExports(c) } diff --git a/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apiconversion.go b/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apiconversion.go new file mode 100644 index 000000000000..49f6739b97da --- /dev/null +++ b/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apiconversion.go @@ -0,0 +1,123 @@ +/* +Copyright 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" + + v1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" +) + +// FakeAPIConversions implements APIConversionInterface +type FakeAPIConversions struct { + Fake *FakeApisV1alpha1 +} + +var apiconversionsResource = schema.GroupVersionResource{Group: "apis.kcp.dev", Version: "v1alpha1", Resource: "apiconversions"} + +var apiconversionsKind = schema.GroupVersionKind{Group: "apis.kcp.dev", Version: "v1alpha1", Kind: "APIConversion"} + +// Get takes name of the aPIConversion, and returns the corresponding aPIConversion object, and an error if there is any. +func (c *FakeAPIConversions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.APIConversion, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(apiconversionsResource, name), &v1alpha1.APIConversion{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIConversion), err +} + +// List takes label and field selectors, and returns the list of APIConversions that match those selectors. +func (c *FakeAPIConversions) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.APIConversionList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(apiconversionsResource, apiconversionsKind, opts), &v1alpha1.APIConversionList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.APIConversionList{ListMeta: obj.(*v1alpha1.APIConversionList).ListMeta} + for _, item := range obj.(*v1alpha1.APIConversionList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested aPIConversions. +func (c *FakeAPIConversions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(apiconversionsResource, opts)) +} + +// Create takes the representation of a aPIConversion and creates it. Returns the server's representation of the aPIConversion, and an error, if there is any. +func (c *FakeAPIConversions) Create(ctx context.Context, aPIConversion *v1alpha1.APIConversion, opts v1.CreateOptions) (result *v1alpha1.APIConversion, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(apiconversionsResource, aPIConversion), &v1alpha1.APIConversion{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIConversion), err +} + +// Update takes the representation of a aPIConversion and updates it. Returns the server's representation of the aPIConversion, and an error, if there is any. +func (c *FakeAPIConversions) Update(ctx context.Context, aPIConversion *v1alpha1.APIConversion, opts v1.UpdateOptions) (result *v1alpha1.APIConversion, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(apiconversionsResource, aPIConversion), &v1alpha1.APIConversion{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIConversion), err +} + +// Delete takes name of the aPIConversion and deletes it. Returns an error if one occurs. +func (c *FakeAPIConversions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(apiconversionsResource, name, opts), &v1alpha1.APIConversion{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeAPIConversions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(apiconversionsResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.APIConversionList{}) + return err +} + +// Patch applies the patch and returns the patched aPIConversion. +func (c *FakeAPIConversions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.APIConversion, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(apiconversionsResource, name, pt, data, subresources...), &v1alpha1.APIConversion{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.APIConversion), err +} diff --git a/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go b/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go index f94b663ebc2d..41a99f6c0096 100644 --- a/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go +++ b/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go @@ -33,6 +33,10 @@ func (c *FakeApisV1alpha1) APIBindings() v1alpha1.APIBindingInterface { return &FakeAPIBindings{c} } +func (c *FakeApisV1alpha1) APIConversions() v1alpha1.APIConversionInterface { + return &FakeAPIConversions{c} +} + func (c *FakeApisV1alpha1) APIExports() v1alpha1.APIExportInterface { return &FakeAPIExports{c} } diff --git a/pkg/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go index 131c6426d487..efda09acdafe 100644 --- a/pkg/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go @@ -20,6 +20,8 @@ package v1alpha1 type APIBindingExpansion interface{} +type APIConversionExpansion interface{} + type APIExportExpansion interface{} type APIResourceSchemaExpansion interface{} diff --git a/pkg/client/informers/externalversions/apis/v1alpha1/apiconversion.go b/pkg/client/informers/externalversions/apis/v1alpha1/apiconversion.go new file mode 100644 index 000000000000..2f0335ba9e84 --- /dev/null +++ b/pkg/client/informers/externalversions/apis/v1alpha1/apiconversion.go @@ -0,0 +1,103 @@ +/* +Copyright 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" + + apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + versioned "github.com/kcp-dev/kcp/pkg/client/clientset/versioned" + internalinterfaces "github.com/kcp-dev/kcp/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/kcp-dev/kcp/pkg/client/listers/apis/v1alpha1" +) + +// APIConversionInformer provides access to a shared informer and lister for +// APIConversions. +type APIConversionInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.APIConversionLister +} + +type aPIConversionInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewAPIConversionInformer constructs a new informer for APIConversion type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewAPIConversionInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredAPIConversionInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredAPIConversionInformer constructs a new informer for APIConversion type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredAPIConversionInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return NewFilteredAPIConversionInformerWithOptions(client, tweakListOptions, cache.WithResyncPeriod(resyncPeriod), cache.WithIndexers(indexers)) +} + +func NewFilteredAPIConversionInformerWithOptions(client versioned.Interface, tweakListOptions internalinterfaces.TweakListOptionsFunc, opts ...cache.SharedInformerOption) cache.SharedIndexInformer { + return cache.NewSharedIndexInformerWithOptions( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ApisV1alpha1().APIConversions().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ApisV1alpha1().APIConversions().Watch(context.TODO(), options) + }, + }, + &apisv1alpha1.APIConversion{}, + opts..., + ) +} + +func (f *aPIConversionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + indexers := cache.Indexers{} + for k, v := range f.factory.ExtraClusterScopedIndexers() { + indexers[k] = v + } + + return NewFilteredAPIConversionInformerWithOptions(client, + f.tweakListOptions, + cache.WithResyncPeriod(resyncPeriod), + cache.WithIndexers(indexers), + cache.WithKeyFunction(f.factory.KeyFunction()), + ) +} + +func (f *aPIConversionInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisv1alpha1.APIConversion{}, f.defaultInformer) +} + +func (f *aPIConversionInformer) Lister() v1alpha1.APIConversionLister { + return v1alpha1.NewAPIConversionLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/apis/v1alpha1/interface.go b/pkg/client/informers/externalversions/apis/v1alpha1/interface.go index b679e31ac064..ff63593536aa 100644 --- a/pkg/client/informers/externalversions/apis/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/apis/v1alpha1/interface.go @@ -26,6 +26,8 @@ import ( type Interface interface { // APIBindings returns a APIBindingInformer. APIBindings() APIBindingInformer + // APIConversions returns a APIConversionInformer. + APIConversions() APIConversionInformer // APIExports returns a APIExportInformer. APIExports() APIExportInformer // APIResourceSchemas returns a APIResourceSchemaInformer. @@ -48,6 +50,11 @@ func (v *version) APIBindings() APIBindingInformer { return &aPIBindingInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } +// APIConversions returns a APIConversionInformer. +func (v *version) APIConversions() APIConversionInformer { + return &aPIConversionInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + // APIExports returns a APIExportInformer. func (v *version) APIExports() APIExportInformer { return &aPIExportInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 01ab395d934a..e53307747dea 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -67,6 +67,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=apis.kcp.dev, Version=v1alpha1 case apisv1alpha1.SchemeGroupVersion.WithResource("apibindings"): return &genericInformer{resource: resource.GroupResource(), informer: f.Apis().V1alpha1().APIBindings().Informer()}, nil + case apisv1alpha1.SchemeGroupVersion.WithResource("apiconversions"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Apis().V1alpha1().APIConversions().Informer()}, nil case apisv1alpha1.SchemeGroupVersion.WithResource("apiexports"): return &genericInformer{resource: resource.GroupResource(), informer: f.Apis().V1alpha1().APIExports().Informer()}, nil case apisv1alpha1.SchemeGroupVersion.WithResource("apiresourceschemas"): diff --git a/pkg/client/listers/apis/v1alpha1/apiconversion.go b/pkg/client/listers/apis/v1alpha1/apiconversion.go new file mode 100644 index 000000000000..1100d1672cf0 --- /dev/null +++ b/pkg/client/listers/apis/v1alpha1/apiconversion.go @@ -0,0 +1,69 @@ +/* +Copyright 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" + + v1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" +) + +// APIConversionLister helps list APIConversions. +// All objects returned here must be treated as read-only. +type APIConversionLister interface { + // List lists all APIConversions in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.APIConversion, err error) + // Get retrieves the APIConversion from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.APIConversion, error) + APIConversionListerExpansion +} + +// aPIConversionLister implements the APIConversionLister interface. +type aPIConversionLister struct { + indexer cache.Indexer +} + +// NewAPIConversionLister returns a new APIConversionLister. +func NewAPIConversionLister(indexer cache.Indexer) APIConversionLister { + return &aPIConversionLister{indexer: indexer} +} + +// List lists all APIConversions in the indexer. +func (s *aPIConversionLister) List(selector labels.Selector) (ret []*v1alpha1.APIConversion, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.APIConversion)) + }) + return ret, err +} + +// Get retrieves the APIConversion from the index for a given name. +func (s *aPIConversionLister) Get(name string) (*v1alpha1.APIConversion, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("apiconversion"), name) + } + return obj.(*v1alpha1.APIConversion), nil +} diff --git a/pkg/client/listers/apis/v1alpha1/expansion_generated.go b/pkg/client/listers/apis/v1alpha1/expansion_generated.go index 3a0c352e843f..67f75b056295 100644 --- a/pkg/client/listers/apis/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/apis/v1alpha1/expansion_generated.go @@ -22,6 +22,10 @@ package v1alpha1 // APIBindingLister. type APIBindingListerExpansion interface{} +// APIConversionListerExpansion allows custom methods to be added to +// APIConversionLister. +type APIConversionListerExpansion interface{} + // APIExportListerExpansion allows custom methods to be added to // APIExportLister. type APIExportListerExpansion interface{} diff --git a/pkg/conversion/conversion.go b/pkg/conversion/conversion.go new file mode 100644 index 000000000000..e0f651b70fd8 --- /dev/null +++ b/pkg/conversion/conversion.go @@ -0,0 +1,176 @@ +/* +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 conversion + +import ( + "fmt" + "strings" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" + "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + crdcel "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/library" + "k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model" + + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" +) + +const celCheckFrequency = 100 + +// CompiledRule contains the compiled cel.Program to convert a single field. If there was a compilation error, that is +// stored in Err. +type CompiledRule struct { + FromPath string + ToPath string + Program cel.Program + Err error +} + +// CompileConversions compiles conversion rules. +func CompileConversions( + apiConversion *v1alpha1.APIConversion, + structuralSchemas map[string]*schema.Structural, +) (map[string][]*CompiledRule, error) { + compiledRules := make(map[string][]*CompiledRule) + + for i := range apiConversion.Spec.Conversions { + c := apiConversion.Spec.Conversions[i] + var rulesForVersion []*CompiledRule + for _, rule := range c.Rules { + cr := &CompiledRule{ + FromPath: rule.Field, + ToPath: rule.Destination, + } + rulesForVersion = append(rulesForVersion, cr) + + if rule.Transformation == "" { + continue + } + + schema, exists := structuralSchemas[c.From] + if !exists { + return nil, fmt.Errorf("unable to find structural schema for version %s", c.From) + } + + field := rule.Field + if field[0] == '.' { + field = field[1:] + } + schemaForField, err := schemaIndexByJSONPath(schema, field) + if err != nil { + return nil, fmt.Errorf("error getting schema for version %s, field path expressoin %s: %w", c.From, rule.Field, err) + } + + env, err := getCELEnv(schemaForField) + if err != nil { + return nil, fmt.Errorf("error creating CEL environment for version %s, field path expression %s: %w", c.From, rule.Field, err) + } + if env == nil { + return nil, fmt.Errorf("nil CEL environment for version %s, field path expression %s", c.From, rule.Field) + } + + ast, issues := env.Compile(rule.Transformation) + if issues != nil { + cr.Err = issues.Err() + continue + } + + program, err := env.Program(ast, + cel.EvalOptions(cel.OptOptimize), + cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...), + cel.InterruptCheckFrequency(celCheckFrequency), + ) + if err != nil { + cr.Err = err + continue + } + + cr.Program = program + } + + compiledRules[c.From] = rulesForVersion + } + + return compiledRules, nil +} + +func getCELEnv(structuralSchema *schema.Structural) (*cel.Env, error) { + env, err := cel.NewEnv(cel.HomogeneousAggregateLiterals()) + if err != nil { + return nil, fmt.Errorf("error creating CEL environment: %w", err) + } + + registry := model.NewRegistry(env) + scopedTypeName := crdcel.GenerateUniqueSelfTypeName() + + ruleTypes, err := model.NewRuleTypes(scopedTypeName, structuralSchema, true, registry) + if err != nil { + return nil, fmt.Errorf("error creating rule types: %w", err) + } + if ruleTypes == nil { + return nil, nil + } + + opts, err := ruleTypes.EnvOptions(env.TypeProvider()) + if err != nil { + return nil, fmt.Errorf("error getting CEL environment options: %w", err) + } + + root, ok := ruleTypes.FindDeclType(scopedTypeName) + if !ok { + rootDecl := model.SchemaDeclType(structuralSchema, true) + if rootDecl == nil { + return nil, fmt.Errorf("unable to find CEL decl type for %s", structuralSchema.Type) + } + root = rootDecl.MaybeAssignTypeName(scopedTypeName) + } + + var propDecls []*expr.Decl + propDecls = append(propDecls, decls.NewVar("self", root.ExprType())) + + opts = append(opts, cel.Declarations(propDecls...), cel.HomogeneousAggregateLiterals()) + opts = append(opts, library.ExtensionLibs...) + return env.Extend(opts...) +} + +func schemaIndexByJSONPath(s *schema.Structural, path string) (*schema.Structural, error) { + cursor := s + + fields := strings.Split(path, ".") + var visited []string + + // spec.name.first ==> 1 + // 0 1 2 + lastParentIndex := len(fields) - 2 + + for i, field := range fields { + visited = append(visited, field) + if lastParentIndex >= 0 && i <= lastParentIndex && cursor.Type != "object" { + return nil, fmt.Errorf("expected field %q to be an object", strings.Join(visited, ".")) + } + property, exists := cursor.Properties[field] + if !exists { + return nil, fmt.Errorf("field %q doesn't exist", strings.Join(visited, ".")) + } + cursor = &property + } + + return cursor, nil +} diff --git a/pkg/openapi/zz_generated.openapi.go b/pkg/openapi/zz_generated.openapi.go index 9d41c913527a..78a25b37c103 100644 --- a/pkg/openapi/zz_generated.openapi.go +++ b/pkg/openapi/zz_generated.openapi.go @@ -49,6 +49,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIBindingList": schema_pkg_apis_apis_v1alpha1_APIBindingList(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIBindingSpec": schema_pkg_apis_apis_v1alpha1_APIBindingSpec(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIBindingStatus": schema_pkg_apis_apis_v1alpha1_APIBindingStatus(ref), + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversion": schema_pkg_apis_apis_v1alpha1_APIConversion(ref), + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversionList": schema_pkg_apis_apis_v1alpha1_APIConversionList(ref), + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversionRule": schema_pkg_apis_apis_v1alpha1_APIConversionRule(ref), + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversionSpec": schema_pkg_apis_apis_v1alpha1_APIConversionSpec(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIExport": schema_pkg_apis_apis_v1alpha1_APIExport(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIExportList": schema_pkg_apis_apis_v1alpha1_APIExportList(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIExportSpec": schema_pkg_apis_apis_v1alpha1_APIExportSpec(ref), @@ -57,6 +61,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIResourceSchemaList": schema_pkg_apis_apis_v1alpha1_APIResourceSchemaList(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIResourceSchemaSpec": schema_pkg_apis_apis_v1alpha1_APIResourceSchemaSpec(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIResourceVersion": schema_pkg_apis_apis_v1alpha1_APIResourceVersion(ref), + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIVersionConversion": schema_pkg_apis_apis_v1alpha1_APIVersionConversion(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.AcceptablePermissionClaim": schema_pkg_apis_apis_v1alpha1_AcceptablePermissionClaim(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.BoundAPIResource": schema_pkg_apis_apis_v1alpha1_BoundAPIResource(ref), "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.BoundAPIResourceSchema": schema_pkg_apis_apis_v1alpha1_BoundAPIResourceSchema(ref), @@ -1280,6 +1285,165 @@ func schema_pkg_apis_apis_v1alpha1_APIBindingStatus(ref common.ReferenceCallback } } +func schema_pkg_apis_apis_v1alpha1_APIConversion(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIConversion contains rules to convert between different API versions in an APIResourceSchema. The name must match the name of the APIResourceSchema for the conversions to take effect.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec holds the desired state.", + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversionSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversionSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_apis_v1alpha1_APIConversionList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIConversionList is a list of APIConversion resources.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversion"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversion", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_apis_v1alpha1_APIConversionRule(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIConversionRule specifies how to convert a single field.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "field": { + SchemaProps: spec.SchemaProps{ + Description: "field is a JSONPath expression to the field in the originating version of the object, relative to its root, such as '.spec.name.first'.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "destination": { + SchemaProps: spec.SchemaProps{ + Description: "destination is a JSONPath expression to the field in the target version of the object, relative to its root, such as '.spec.name.first'.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "transformation": { + SchemaProps: spec.SchemaProps{ + Description: "transformation is an optional CEL expression used to execute user-specified rules to transform the originating field -- identified by 'self' -- to the destination field.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"field", "destination"}, + }, + }, + } +} + +func schema_pkg_apis_apis_v1alpha1_APIConversionSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIConversionSpec contains rules to convert between different API versions in an APIResourceSchema.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conversions": { + SchemaProps: spec.SchemaProps{ + Description: "conversions specify rules to convert between different API versions in an APIResourceSchema.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIVersionConversion"), + }, + }, + }, + }, + }, + }, + Required: []string{"conversions"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIVersionConversion"}, + } +} + func schema_pkg_apis_apis_v1alpha1_APIExport(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1745,6 +1909,67 @@ func schema_pkg_apis_apis_v1alpha1_APIResourceVersion(ref common.ReferenceCallba } } +func schema_pkg_apis_apis_v1alpha1_APIVersionConversion(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "APIVersionConversion contains rules to convert between two specific API versions in an APIResourceSchema. Additionally, to avoid data loss when round-tripping from a version that contains a new field to one that doesn't and back again, you can specify a list of fields to preserve (these are stored in annotations).", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "from": { + SchemaProps: spec.SchemaProps{ + Description: "from is the source version.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "to": { + SchemaProps: spec.SchemaProps{ + Description: "to is the target version.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "rules": { + SchemaProps: spec.SchemaProps{ + Description: "rules contains field-specific conversion expressions.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversionRule"), + }, + }, + }, + }, + }, + "preserve": { + SchemaProps: spec.SchemaProps{ + Description: "preserve contains a list of JSONPath expressions to fields to preserve in the originating version of the object, relative to its, such as '.spec.name.first'.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"from", "to", "rules"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1.APIConversionRule"}, + } +} + func schema_pkg_apis_apis_v1alpha1_AcceptablePermissionClaim(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/reconciler/apis/apibinding/apibinding_controller.go b/pkg/reconciler/apis/apibinding/apibinding_controller.go index e9562249072e..e919c3f4f55f 100644 --- a/pkg/reconciler/apis/apibinding/apibinding_controller.go +++ b/pkg/reconciler/apis/apibinding/apibinding_controller.go @@ -67,8 +67,10 @@ func NewController( apiBindingInformer apisinformers.APIBindingInformer, apiExportInformer apisinformers.APIExportInformer, apiResourceSchemaInformer apisinformers.APIResourceSchemaInformer, + apiConversionInformer apisinformers.APIConversionInformer, temporaryRemoteShardApiExportInformer apisinformers.APIExportInformer, /*TODO(p0lyn0mial): replace with multi-shard informers*/ temporaryRemoteShardApiResourceSchemaInformer apisinformers.APIResourceSchemaInformer, /*TODO(p0lyn0mial): replace with multi-shard informers*/ + temporaryRemoteShardApiConversionInformer apisinformers.APIConversionInformer, /*TODO(p0lyn0mial): replace with multi-shard informers*/ crdInformer apiextensionsinformers.CustomResourceDefinitionInformer, ) (*controller, error) { queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName) @@ -119,6 +121,17 @@ func NewController( return apiResourceSchema, err }, + getAPIConversion: func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIConversion, error) { + apiConversion, err := apiConversionInformer.Lister().Get(clusters.ToClusterAwareKey(clusterName, name)) + if errors.IsNotFound(err) { + return temporaryRemoteShardApiConversionInformer.Lister().Get(clusters.ToClusterAwareKey(clusterName, name)) + } + return apiConversion, err + }, + createAPIConversion: func(ctx context.Context, clusterName logicalcluster.Name, apiConversion *apisv1alpha1.APIConversion) (*apisv1alpha1.APIConversion, error) { + return kcpClusterClient.ApisV1alpha1().APIConversions().Create(logicalcluster.WithCluster(ctx, clusterName), apiConversion, metav1.CreateOptions{}) + }, + createCRD: func(ctx context.Context, clusterName logicalcluster.Name, crd *apiextensionsv1.CustomResourceDefinition) (*apiextensionsv1.CustomResourceDefinition, error) { return crdClusterClient.ApiextensionsV1().CustomResourceDefinitions().Create(logicalcluster.WithCluster(ctx, clusterName), crd, metav1.CreateOptions{}) }, @@ -178,12 +191,7 @@ func NewController( return nil, err } - apiResourceSchemaInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, - UpdateFunc: func(_, obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, - DeleteFunc: func(obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, - }) - + // APIExport-related handlers apiExportInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { c.enqueueAPIExport(obj, logger, "") }, UpdateFunc: func(_, obj interface{}) { c.enqueueAPIExport(obj, logger, "") }, @@ -194,12 +202,8 @@ func NewController( UpdateFunc: func(_, obj interface{}) { c.enqueueAPIExport(obj, logger, "") }, DeleteFunc: func(obj interface{}) { c.enqueueAPIExport(obj, logger, "") }, }) - temporaryRemoteShardApiResourceSchemaInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, - UpdateFunc: func(_, obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, - DeleteFunc: func(obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, - }) + // APIExport-related indexers if err := c.apiExportsIndexer.AddIndexers(cache.Indexers{ indexAPIExportsByAPIResourceSchema: indexAPIExportsByAPIResourceSchemasFunc, }); err != nil { @@ -211,6 +215,30 @@ func NewController( return nil, fmt.Errorf("error adding ApiExport indexes for the root shard: %w", err) } + // APIResourceSchema-related handlers + apiResourceSchemaInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, + UpdateFunc: func(_, obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, + DeleteFunc: func(obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, + }) + temporaryRemoteShardApiResourceSchemaInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, + UpdateFunc: func(_, obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, + DeleteFunc: func(obj interface{}) { c.enqueueAPIResourceSchema(obj, logger, "") }, + }) + + // APIConversion-related handlers + apiConversionInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { c.enqueueAPIConversion(obj, logger) }, + UpdateFunc: func(_, obj interface{}) { c.enqueueAPIConversion(obj, logger) }, + DeleteFunc: func(obj interface{}) { c.enqueueAPIConversion(obj, logger) }, + }) + temporaryRemoteShardApiConversionInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { c.enqueueAPIConversion(obj, logger) }, + UpdateFunc: func(_, obj interface{}) { c.enqueueAPIConversion(obj, logger) }, + DeleteFunc: func(obj interface{}) { c.enqueueAPIConversion(obj, logger) }, + }) + return c, nil } @@ -241,6 +269,9 @@ type controller struct { getAPIResourceSchema func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIResourceSchema, error) + getAPIConversion func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIConversion, error) + createAPIConversion func(ctx context.Context, clusterName logicalcluster.Name, apiConversion *apisv1alpha1.APIConversion) (*apisv1alpha1.APIConversion, error) + createCRD func(ctx context.Context, clusterName logicalcluster.Name, crd *apiextensionsv1.CustomResourceDefinition) (*apiextensionsv1.CustomResourceDefinition, error) getCRD func(clusterName logicalcluster.Name, name string) (*apiextensionsv1.CustomResourceDefinition, error) crdIndexer cache.Indexer @@ -337,6 +368,35 @@ func (c *controller) enqueueAPIResourceSchema(obj interface{}, logger logr.Logge } } +// enqueueAPIConversion maps an APIConversion to APIExports for enqueuing. +func (c *controller) enqueueAPIConversion(obj interface{}, logger logr.Logger) { + key, err := kcpcache.DeletionHandlingMetaClusterNamespaceKeyFunc(obj) + if err != nil { + runtime.HandleError(err) + return + } + + logger = logging.WithQueueKey(logger, key) + + // APIConversions and APIResourceSchemas have matching names, so we can reuse this index + apiExports, err := c.apiExportsIndexer.ByIndex(indexAPIExportsByAPIResourceSchema, key) + if err != nil { + runtime.HandleError(err) + return + } + if len(apiExports) == 0 { + apiExports, err = c.temporaryRemoteShardApiExportsIndexer.ByIndex(indexAPIExportsByAPIResourceSchema, key) + if err != nil { + runtime.HandleError(err) + return + } + } + + for _, export := range apiExports { + c.enqueueAPIExport(export, logging.WithObject(logger, obj.(*apisv1alpha1.APIConversion)), "because of APIConversion") + } +} + // Start starts the controller, which stops when ctx.Done() is closed. func (c *controller) Start(ctx context.Context, numThreads int) { defer runtime.HandleCrash() diff --git a/pkg/reconciler/apis/apibinding/apibinding_reconcile.go b/pkg/reconciler/apis/apibinding/apibinding_reconcile.go index ded06a787fe1..fb81254c798f 100644 --- a/pkg/reconciler/apis/apibinding/apibinding_reconcile.go +++ b/pkg/reconciler/apis/apibinding/apibinding_reconcile.go @@ -332,6 +332,75 @@ func (c *controller) reconcileBinding(ctx context.Context, apiBinding *apisv1alp } } + // TODO(ncdc): should we add a spec field to APIResourceSchema to indicate it has conversions, so we + // know we need to wait for them to show up in the informer cache? And to account for it in the + // conditions? + apiConversion, err := c.getAPIConversion(apiExportClusterName, schema.Name) + if err == nil { + logger := logging.WithObject(logger, apiConversion) + + if _, err := c.getAPIConversion(ShadowWorkspaceName, crd.Name); err == nil { + // all good + } else if !apierrors.IsNotFound(err) { + // indexer error + logger.Error(err, "error checking if APIConversion already exists in system:bound-crds") + + conditions.MarkFalse( + apiBinding, + apisv1alpha1.BindingUpToDate, + apisv1alpha1.InternalErrorReason, + conditionsv1alpha1.ConditionSeverityError, + "An internal error prevented the APIBinding process from completing. Please contact your system administrator for assistance", + ) + // Only change InitialBindingCompleted if it's false + if conditions.IsFalse(apiBinding, apisv1alpha1.InitialBindingCompleted) { + conditions.MarkFalse( + apiBinding, + apisv1alpha1.InitialBindingCompleted, + apisv1alpha1.InternalErrorReason, + conditionsv1alpha1.ConditionSeverityError, + "An internal error prevented the APIBinding process from completing. Please contact your system administrator for assistance", + ) + } + } else { + // need to create + apiConversion = apiConversion.DeepCopy() + apiConversion.Name = crd.Name + apiConversion.ResourceVersion = "" + if _, err := c.createAPIConversion(ctx, ShadowWorkspaceName, apiConversion); err != nil { + logger.Error(err, "error creating APIConversion") + + conditions.MarkFalse( + apiBinding, + apisv1alpha1.BindingUpToDate, + apisv1alpha1.InternalErrorReason, + conditionsv1alpha1.ConditionSeverityError, + "An internal error prevented the APIBinding process from completing. Please contact your system administrator for assistance", + ) + // Only change InitialBindingCompleted if it's false + if conditions.IsFalse(apiBinding, apisv1alpha1.InitialBindingCompleted) { + conditions.MarkFalse( + apiBinding, + apisv1alpha1.InitialBindingCompleted, + apisv1alpha1.InternalErrorReason, + conditionsv1alpha1.ConditionSeverityError, + "An internal error prevented the APIBinding process from completing. Please contact your system administrator for assistance", + ) + } + } + } + } else if !apierrors.IsNotFound(err) { + logger.Error(err, "error getting APIConversion") + + conditions.MarkFalse( + apiBinding, + apisv1alpha1.APIExportValid, + apisv1alpha1.InternalErrorReason, + conditionsv1alpha1.ConditionSeverityError, + "Invalid APIExport. Please contact the APIExport owner to resolve", + ) + } + // Merge any current storage versions with new ones storageVersions := sets.NewString() if existingCRD != nil { @@ -476,6 +545,23 @@ func (c *controller) reconcileBound(ctx context.Context, apiBinding *apisv1alpha } exportedSchemas = append(exportedSchemas, apiResourceSchema) + + if _, err := c.getAPIConversion(apiExportClusterName, schemaName); err != nil { + if apierrors.IsNotFound(err) { + continue + } + + // TODO(ncdc): log, set condition, return error? + return false, nil + } + + if _, err := c.getAPIConversion(ShadowWorkspaceName, string(apiResourceSchema.UID)); err != nil { + if apierrors.IsNotFound(err) { + return true, nil + } + + // TODO(ncdc): log, set condition, return error? + } } if apiExportLatestResourceSchemasChanged(apiBinding, exportedSchemas) { diff --git a/pkg/server/api_conversion.go b/pkg/server/api_conversion.go new file mode 100644 index 000000000000..4948be110c5d --- /dev/null +++ b/pkg/server/api_conversion.go @@ -0,0 +1,325 @@ +/* +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 server + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + + "github.com/google/cel-go/interpreter" + "github.com/kcp-dev/logicalcluster/v2" + + apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion" + structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/clusters" + "k8s.io/klog/v2" + + apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + apisinformers "github.com/kcp-dev/kcp/pkg/client/informers/externalversions/apis/v1alpha1" + kcpconversion "github.com/kcp-dev/kcp/pkg/conversion" +) + +// CRConverterFactory instantiates converters that are capable of converting custom resources between different API +// versions. It supports CEL-based conversion rules from APIConversion resources and the "none" conversion strategy. +type CRConverterFactory struct { + nopConverter conversion.Converter + getAPIConversion func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIConversion, error) +} + +var _ conversion.Factory = &CRConverterFactory{} + +// NewCRConverterFactory returns a CRConverterFactory that supports APIConversion-based conversions and the "none" +// conversion strategy. +func NewCRConverterFactory(apiConversionInformer apisinformers.APIConversionInformer) *CRConverterFactory { + return &CRConverterFactory{ + nopConverter: conversion.NewNOPConverter(), + getAPIConversion: func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIConversion, error) { + return apiConversionInformer.Lister().Get(clusters.ToClusterAwareKey(clusterName, name)) + }, + } +} + +// NewConverter returns the appropriate conversion.Converter based on the CRD. If the CRD identifies as for a wildcard +// partial metadata request, the nop converter is used. Otherwise, it returns a CEL-based converter if there is an +// associated APIConversion, a nop converter if the strategy is "none", or an error otherwise. +func (f *CRConverterFactory) NewConverter(crd *apiextensionsv1.CustomResourceDefinition) (conversion.Converter, error) { + // Wildcard, partial metadata requests never need conversion + if strings.HasSuffix(string(crd.UID), ".wildcard.partial-metadata") { + return f.nopConverter, nil + } + + // We have to return a deferredConverter here instead of an actual one because apiextensions-apiserver creates the + // converter once, when it creates serving info for a CRD. Because we aren't currently guaranteeing order (or cache + // freshness), there is always a chance that serving info is created for a CRD before an associated APIConversion + // exists and is present in the informer cache. + return &deferredConverter{ + crd: crd, + nopConverter: conversion.NewNOPConverter(), + + celConverters: map[string]*celConverter{}, + + getAPIConversion: f.getAPIConversion, + }, nil +} + +// deferredConverter implements conversion.Converter and determines on the fly what type of converter to use. +type deferredConverter struct { + crd *apiextensionsv1.CustomResourceDefinition + nopConverter conversion.Converter + + // lock guards celConverters + lock sync.Mutex + // celConverters is a map from CRD UID to a celConverter. + celConverters map[string]*celConverter + + getAPIConversion func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIConversion, error) +} + +// Convert converts in to targetGV. If there is an APIConversion for this CRD, a CEL-based converter is used. Otherwise, +// a nop converter is used. +func (f *deferredConverter) Convert(in *unstructured.UnstructuredList, targetGV schema.GroupVersion) (*unstructured.UnstructuredList, error) { + converter, err := f.getConverter() + if err != nil { + return nil, fmt.Errorf("error getting converter: %w", err) + } + + return converter.Convert(in, targetGV) +} + +func (f *deferredConverter) getConverter() (conversion.Converter, error) { + f.lock.Lock() + defer f.lock.Unlock() + + converter, ok := f.celConverters[string(f.crd.UID)] + if ok { + return converter, nil + } + + clusterName := logicalcluster.From(f.crd) + apiConversion, err := f.getAPIConversion(clusterName, f.crd.Name) + if err != nil && !errors.IsNotFound(err) { + return nil, fmt.Errorf("error checking for APIConversion for CRD %s|%s: %w", clusterName, f.crd.Name, err) + } + if errors.IsNotFound(err) { + switch f.crd.Spec.Conversion.Strategy { + case apiextensionsv1.NoneConverter: + return f.nopConverter, nil + case apiextensionsv1.WebhookConverter: + return nil, fmt.Errorf("conversion strategy %q is not supported for CRD %s", f.crd.Spec.Conversion.Strategy, f.crd.Name) + default: + return nil, fmt.Errorf("unknown conversion strategy %q for CRD %s", f.crd.Spec.Conversion.Strategy, f.crd.Name) + } + } + + converter, err = f.newCELConverter(f.crd, apiConversion) + if err != nil { + return nil, fmt.Errorf("error creating CEL converter for CRD %s|%s: %w", clusterName, f.crd.Name, err) + } + f.celConverters[string(f.crd.UID)] = converter + + return converter, nil +} + +func (f *deferredConverter) newCELConverter( + crd *apiextensionsv1.CustomResourceDefinition, + apiConversion *apisv1alpha1.APIConversion, +) (*celConverter, error) { + structuralSchemas := make(map[string]*structuralschema.Structural) + + // Gather all the structural schemas for easy map-based indexing + for _, v := range crd.Spec.Versions { + if v.Schema == nil { + continue + } + + internalJSONSchemaProps := &apiextensionsinternal.JSONSchemaProps{} + if err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v.Schema.OpenAPIV3Schema, internalJSONSchemaProps, nil); err != nil { + return nil, fmt.Errorf("failed converting version %s validation to internal version: %w", v.Name, err) + } + + structuralSchema, err := structuralschema.NewStructural(internalJSONSchemaProps) + if err != nil { + return nil, fmt.Errorf("error getting structural schema for version %s: %w", v.Name, err) + } + + // Deep copy because PruneDefaults is mutating + structuralSchema = structuralSchema.DeepCopy() + + if err := structuraldefaulting.PruneDefaults(structuralSchema); err != nil { + return nil, fmt.Errorf("error pruning defaults for version %s: %w", v.Name, err) + } + + structuralSchemas[v.Name] = structuralSchema + } + + compiledRules, err := kcpconversion.CompileConversions(apiConversion, structuralSchemas) + if err != nil { + return nil, fmt.Errorf("error compiling conversion rules: %w", err) + } + + return &celConverter{ + crd: crd, + compiledRules: compiledRules, + structuralSchemas: structuralSchemas, + apiConversion: apiConversion, + }, nil +} + +type celConverter struct { + crd *apiextensionsv1.CustomResourceDefinition + compiledRules map[string][]*kcpconversion.CompiledRule + structuralSchemas map[string]*structuralschema.Structural + apiConversion *apisv1alpha1.APIConversion +} + +func (c *celConverter) Convert(list *unstructured.UnstructuredList, targetGV schema.GroupVersion) (*unstructured.UnstructuredList, error) { + convertedList := &unstructured.UnstructuredList{} + for i := range list.Items { + original := &list.Items[i] + converted := original.DeepCopy() + converted.SetAPIVersion(targetGV.String()) + + originalVersion := original.GetObjectKind().GroupVersionKind().Version + compiledRules := c.compiledRules[originalVersion] + for _, rule := range compiledRules { + if rule.Err != nil { + // TODO(ncdc): what to do here? + continue + } + + fromPath := rule.FromPath + if fromPath[0] == '.' { + fromPath = fromPath[1:] + } + fromFields := strings.Split(fromPath, ".") + fromValue, exists, err := unstructured.NestedFieldNoCopy(original.Object, fromFields...) + if err != nil { + return nil, fmt.Errorf("error getting source field %q: %w", rule.FromPath, err) + } + if !exists { + return nil, fmt.Errorf("source field %q does not exist", rule.FromPath) + } + + toPath := rule.ToPath + if toPath[0] == '.' { + toPath = toPath[1:] + } + fields := strings.Split(toPath, ".") + if err := unstructured.SetNestedField(converted.Object, fromValue, fields...); err != nil { + return nil, fmt.Errorf("error setting target field %q: %w", rule.ToPath, err) + } + + if rule.Program == nil { + continue + } + + // structuralSchema := c.structuralSchemas[originalVersion] + bindings := map[string]interface{}{ + "self": fromValue, // crdcel.UnstructuredToVal(original.Object, structuralSchema), + } + activation, err := interpreter.NewActivation(bindings) + if err != nil { + return nil, fmt.Errorf("error creating CEL interpreter activation: %w", err) + } + + evalResult, evalDetails, err := rule.Program.ContextEval(context.TODO(), activation) + _ = evalDetails + if err != nil { + return nil, fmt.Errorf("error executing transformation: %w", err) + } + + if err := unstructured.SetNestedField(converted.Object, evalResult.Value(), fields...); err != nil { + return nil, fmt.Errorf("error setting target field %q: %w", rule.ToPath, err) + } + } + + annotations := converted.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + preserveAnnotation := annotations[targetGV.Version+".conversion.apis.kcp.dev/preserve"] + if preserveAnnotation != "" { + // If we're going to a version that already has the preserve annotation, restore from it + m := map[string]string{} + if err := json.Unmarshal([]byte(preserveAnnotation), &m); err != nil { + klog.Errorf("ANDY error unmarshaling preserve annotation %q: %v", preserveAnnotation, err) + } else { + for field, val := range m { + var decodedVal interface{} + if err := json.Unmarshal([]byte(val), &decodedVal); err != nil { + klog.Errorf("ANDY error decoding field %q val %q: %v", field, val, err) + } else if err := unstructured.SetNestedField(converted.Object, decodedVal, strings.Split(field, ".")...); err != nil { + klog.Errorf("ANDY error setting nested field %q val %q: %v", field, val, err) + } + } + } + } else { + // Otherwise, store it + + m := map[string]string{} + for _, conversion := range c.apiConversion.Spec.Conversions { + if conversion.From != originalVersion { + continue + } + for _, field := range conversion.Preserve { + if field[0] == '.' { + field = field[1:] + } + v, exists, err := unstructured.NestedFieldNoCopy(original.Object, strings.Split(field, ".")...) + if err != nil { + klog.Errorf("ANDY error getting field %q to preserve: %v", field, err) + continue + } + if !exists { + continue + } + encoded, err := json.Marshal(v) + if err != nil { + klog.Errorf("ANDY error encoding field %q value %q: %v", field, v, err) + continue + } + m[field] = string(encoded) + } + } + + if len(m) > 0 { + encoded, err := json.Marshal(m) + if err != nil { + klog.Errorf("ANDY error encoding preserve map %#v: %v", m, err) + } else { + annotations[originalVersion+".conversion.apis.kcp.dev/preserve"] = string(encoded) + converted.SetAnnotations(annotations) + } + } + } + + convertedList.Items = append(convertedList.Items, *converted) + break + } + + return convertedList, nil +} diff --git a/pkg/server/config.go b/pkg/server/config.go index 7620ec9dc52f..137bc2c43f48 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -33,7 +33,6 @@ import ( "k8s.io/apiserver/pkg/quota/v1/generic" genericapiserver "k8s.io/apiserver/pkg/server" serverstorage "k8s.io/apiserver/pkg/server/storage" - "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/dynamic" kubernetesinformers "k8s.io/client-go/informers" kubernetesclient "k8s.io/client-go/kubernetes" @@ -390,23 +389,15 @@ func NewConfig(opts *kcpserveroptions.CompletedOptions) (*Config, error) { c.Apis.ExtraConfig.VersionedInformers, admissionPluginInitializers, opts.GenericControlPlane, - - // Wire in a ServiceResolver that always returns an error that ResolveEndpoint is not yet - // supported. The effect is that CRD webhook conversions are not supported and will always get an - // error. - &unimplementedServiceResolver{}, - - webhook.NewDefaultAuthenticationInfoResolverWrapper( - nil, - c.Apis.GenericConfig.EgressSelector, - c.Apis.GenericConfig.LoopbackClientConfig, - c.Apis.GenericConfig.TracerProvider, - ), ) if err != nil { - return nil, fmt.Errorf("configure api extensions: %w", err) + return nil, fmt.Errorf("error configuring api extensions: %w", err) } + c.ApiExtensions.ExtraConfig.ConversionFactory = NewCRConverterFactory(c.KcpSharedInformerFactory.Apis().V1alpha1().APIConversions()) + // make sure the informer gets started + _ = c.KcpSharedInformerFactory.Apis().V1alpha1().APIConversions().Informer() + c.KcpSharedInformerFactory.Apis().V1alpha1().APIBindings().Informer().GetIndexer().AddIndexers(cache.Indexers{byWorkspace: indexByWorkspace}) //nolint:errcheck c.ApiExtensionsSharedInformerFactory.Apiextensions().V1().CustomResourceDefinitions().Informer().GetIndexer().AddIndexers(cache.Indexers{byWorkspace: indexByWorkspace}) //nolint:errcheck c.ApiExtensionsSharedInformerFactory.Apiextensions().V1().CustomResourceDefinitions().Informer().GetIndexer().AddIndexers(cache.Indexers{byGroupResourceName: indexCRDByGroupResourceName}) //nolint:errcheck diff --git a/pkg/server/controllers.go b/pkg/server/controllers.go index f6a67a96d47e..e5dee37d3ecc 100644 --- a/pkg/server/controllers.go +++ b/pkg/server/controllers.go @@ -568,8 +568,10 @@ func (s *Server) installAPIBindingController(ctx context.Context, config *rest.C s.KcpSharedInformerFactory.Apis().V1alpha1().APIBindings(), s.KcpSharedInformerFactory.Apis().V1alpha1().APIExports(), s.KcpSharedInformerFactory.Apis().V1alpha1().APIResourceSchemas(), + s.KcpSharedInformerFactory.Apis().V1alpha1().APIConversions(), s.TemporaryRootShardKcpSharedInformerFactory.Apis().V1alpha1().APIExports(), s.TemporaryRootShardKcpSharedInformerFactory.Apis().V1alpha1().APIResourceSchemas(), + s.TemporaryRootShardKcpSharedInformerFactory.Apis().V1alpha1().APIConversions(), s.ApiExtensionsSharedInformerFactory.Apiextensions().V1().CustomResourceDefinitions(), ) if err != nil { diff --git a/pkg/server/handler.go b/pkg/server/handler.go index bba954bb5a5b..14e32f0b519e 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -18,11 +18,9 @@ package server import ( "context" - "errors" "fmt" "net/http" _ "net/http/pprof" - "net/url" "path" "sort" "strings" @@ -456,13 +454,3 @@ func (r *inMemoryResponseWriter) String() string { } return s } - -// unimplementedServiceResolver is a webhook.ServiceResolver that always returns an error, because -// we have not implemented support for this yet. As a result, CRD webhook conversions are not -// supported. -type unimplementedServiceResolver struct{} - -// ResolveEndpoint always returns an error that this is not yet supported. -func (r *unimplementedServiceResolver) ResolveEndpoint(namespace string, name string, port int32) (*url.URL, error) { - return nil, errors.New("CRD webhook conversions are not yet supported in kcp") -} diff --git a/test/e2e/conversion/conversion_test.go b/test/e2e/conversion/conversion_test.go new file mode 100644 index 000000000000..b1baf0ad8623 --- /dev/null +++ b/test/e2e/conversion/conversion_test.go @@ -0,0 +1,150 @@ +/* +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 conversion + +import ( + "context" + "embed" + "fmt" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/kcp-dev/apimachinery/pkg/dynamic" + "github.com/stretchr/testify/require" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/discovery/cached/memory" + "k8s.io/client-go/restmapper" + + "github.com/kcp-dev/kcp/config/helpers" + apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + "github.com/kcp-dev/kcp/pkg/apis/third_party/conditions/util/conditions" + kcpclientset "github.com/kcp-dev/kcp/pkg/client/clientset/versioned" + "github.com/kcp-dev/kcp/test/e2e/framework" +) + +//go:embed *.yaml +var embeddedResources embed.FS + +func TestAPIConversion(t *testing.T) { + t.Parallel() + + server := framework.SharedKcpServer(t) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + cfg := server.BaseConfig(t) + + kcpClusterClient, err := kcpclientset.NewClusterForConfig(cfg) + require.NoError(t, err, "error creating kube cluster client") + + orgClusterName := framework.NewOrganizationFixture(t, server) + + cache := memory.NewMemCacheClient(kcpClusterClient.Cluster(orgClusterName).Discovery()) + mapper := restmapper.NewDeferredDiscoveryRESTMapper(cache) + + dynamicClusterClient, err := dynamic.NewClusterDynamicClientForConfig(cfg) + require.NoError(t, err, "error creating dynamic cluster client") + + t.Logf("Setting up APIResourceSchema, APIConversion, APIExport, and APIBinding for widgets") + err = helpers.CreateResourceFromFS(ctx, dynamicClusterClient.Cluster(orgClusterName), mapper, nil, "resources.yaml", embeddedResources) + require.NoError(t, err, "error creating embedded resources") + + t.Logf("Waiting for initial binding to complete") + framework.Eventually(t, func() (bool, string) { + apiBinding, err := kcpClusterClient.Cluster(orgClusterName).ApisV1alpha1().APIBindings().Get(ctx, "widgets.example.io", metav1.GetOptions{}) + require.NoError(t, err, "error getting APIBinding") + + if conditions.IsTrue(apiBinding, apisv1alpha1.InitialBindingCompleted) { + return true, "" + } + + return false, fmt.Sprintf("%v", apiBinding.Status) + }, wait.ForeverTestTimeout, 100*time.Millisecond, "APIBinding never completed its initial binding") + + t.Logf("Resetting the RESTMapper so it can pick up widgets") + mapper.Reset() + + t.Logf("Creating v1 widget") + err = helpers.CreateResourceFromFS(ctx, dynamicClusterClient.Cluster(orgClusterName), mapper, nil, "v1-widget.yaml", embeddedResources) + require.NoError(t, err, "error creating v1 widget") + + v2GVR := schema.GroupVersionResource{Group: "example.io", Version: "v2", Resource: "widgets"} + + t.Logf("Retrieving the widget as v2") + v2WidgetClient := dynamicClusterClient.Cluster(orgClusterName).Resource(v2GVR) + v2Widget, err := v2WidgetClient.Get(ctx, "bob", metav1.GetOptions{}) + require.NoError(t, err, "error getting v2 widget") + + t.Logf("Ensuring field conversions work") + requireUnstructuredFieldEqual(t, v2Widget, "Bob", "spec", "name", "first") + requireUnstructuredFieldEqual(t, v2Widget, "Jones", "spec", "name", "last") + requireUnstructuredFieldEqual(t, v2Widget, "JONES", "spec", "name", "lastUpper") + + t.Logf("Setting and storing a v2-only field") + err = unstructured.SetNestedField(v2Widget.Object, "someNewValue", "spec", "someNewField", "hello") + require.NoError(t, err, "error setting spec.someNewField.hello") + _, err = v2WidgetClient.Update(ctx, v2Widget, metav1.UpdateOptions{}) + require.NoError(t, err, "error updating v2 widget") + + t.Logf("Getting the v2 widget again to make sure the field was preserved") + v2Widget, err = v2WidgetClient.Get(ctx, v2Widget.GetName(), metav1.GetOptions{}) + require.NoError(t, err, "error getting v2 widget") + requireUnstructuredFieldEqual(t, v2Widget, "someNewValue", "spec", "someNewField", "hello") + + t.Logf("Make sure we can create a v2 widget") + err = helpers.CreateResourceFromFS(ctx, dynamicClusterClient.Cluster(orgClusterName), mapper, nil, "v2-widget.yaml", embeddedResources) + require.NoError(t, err, "error creating v2 widget") + + v1GVR := schema.GroupVersionResource{Group: "example.io", Version: "v1", Resource: "widgets"} + t.Logf("Retrieving the widget as v2") + v1WidgetClient := dynamicClusterClient.Cluster(orgClusterName).Resource(v1GVR) + v1Widget, err := v1WidgetClient.Get(ctx, "alice", metav1.GetOptions{}) + require.NoError(t, err, "error getting v1 widget") + + t.Logf("Ensuring field conversions work") + requireUnstructuredFieldEqual(t, v1Widget, "Alice", "spec", "firstName") + requireUnstructuredFieldEqual(t, v1Widget, "Smith", "spec", "lastName") + + t.Logf("Updating v1 names") + err = unstructured.SetNestedField(v1Widget.Object, "Robot", "spec", "firstName") + require.NoError(t, err, "error setting spec.firstName") + err = unstructured.SetNestedField(v1Widget.Object, "Dragon", "spec", "lastName") + require.NoError(t, err, "error setting spec.lastName") + _, err = v1WidgetClient.Update(ctx, v1Widget, metav1.UpdateOptions{}) + require.NoError(t, err, "error updating v1 widget") + + t.Logf("Getting the widget as v2 to make sure the names were changed and the v2-only field was preserved") + v2Widget, err = v2WidgetClient.Get(ctx, v1Widget.GetName(), metav1.GetOptions{}) + require.NoError(t, err, "error getting v2 widget") + requireUnstructuredFieldEqual(t, v2Widget, "world", "spec", "someNewField", "hello") + requireUnstructuredFieldEqual(t, v2Widget, "Robot", "spec", "name", "first") + requireUnstructuredFieldEqual(t, v2Widget, "Dragon", "spec", "name", "last") +} + +func requireUnstructuredFieldEqual(t *testing.T, u *unstructured.Unstructured, expected string, fields ...string) { + actual, exists, err := unstructured.NestedFieldNoCopy(u.Object, fields...) + require.NoError(t, err, "error getting %s", strings.Join(fields, ".")) + require.True(t, exists, "field %s does not exist", strings.Join(fields, ".")) + require.Empty(t, cmp.Diff(expected, actual), "unexpected value for %s", strings.Join(fields, ".")) +} diff --git a/test/e2e/conversion/resources.yaml b/test/e2e/conversion/resources.yaml new file mode 100644 index 000000000000..58e9cd44f116 --- /dev/null +++ b/test/e2e/conversion/resources.yaml @@ -0,0 +1,121 @@ +apiVersion: apis.kcp.dev/v1alpha1 +kind: APIResourceSchema +metadata: + name: rev0002.widgets.example.io +spec: + group: example.io + names: + kind: Widget + listKind: WidgetList + plural: widgets + singular: widget + scope: Cluster + versions: + - name: v1 + schema: + description: Widgets do things + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + firstName: + type: string + lastName: + type: string + type: object + status: + properties: + phase: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} + - name: v2 + schema: + description: Widgets do things + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + someNewField: + type: object + properties: + hello: + type: string + name: + type: object + properties: + first: + type: string + last: + type: string + lastUpper: + type: string + type: object + status: + properties: + phase: + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} +--- +apiVersion: apis.kcp.dev/v1alpha1 +kind: APIConversion +metadata: + name: rev0002.widgets.example.io +spec: + conversions: + - from: v1 + to: v2 + rules: + - field: .spec.firstName + destination: .spec.name.first + - field: .spec.lastName + destination: .spec.name.last + transformation: self + - field: .spec.lastName + destination: .spec.name.lastUpper + transformation: self.upperAscii() + - from: v2 + to: v1 + rules: + - field: .spec.name.first + destination: .spec.firstName + - field: .spec.name.last + destination: .spec.lastName + preserve: + - .spec.someNewField +--- +apiVersion: apis.kcp.dev/v1alpha1 +kind: APIExport +metadata: + name: widgets.example.io +spec: + latestResourceSchemas: + - rev0002.widgets.example.io +--- +apiVersion: apis.kcp.dev/v1alpha1 +kind: APIBinding +metadata: + name: widgets.example.io +spec: + reference: + workspace: + exportName: widgets.example.io diff --git a/test/e2e/conversion/v1-widget.yaml b/test/e2e/conversion/v1-widget.yaml new file mode 100644 index 000000000000..1a7cd1a41949 --- /dev/null +++ b/test/e2e/conversion/v1-widget.yaml @@ -0,0 +1,7 @@ +apiVersion: example.io/v1 +kind: Widget +metadata: + name: bob +spec: + firstName: Bob + lastName: Jones diff --git a/test/e2e/conversion/v2-widget.yaml b/test/e2e/conversion/v2-widget.yaml new file mode 100644 index 000000000000..0e3796ac8d01 --- /dev/null +++ b/test/e2e/conversion/v2-widget.yaml @@ -0,0 +1,10 @@ +apiVersion: example.io/v2 +kind: Widget +metadata: + name: alice +spec: + name: + first: Alice + last: Smith + someNewField: + hello: world