Merge pull request #55168 from nikhita/customresources-subresources

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

apiextensions: add subresources for custom resources

Fixes #38113
Fixes #58778

**Related**:
- Proposal: https://github.com/kubernetes/community/pull/913
- For custom resources to work with `kubectl scale`: https://github.com/kubernetes/kubernetes/pull/58283

**Add types**:

- Add `CustomResourceSubResources` type to CRD.
    - Fix proto generation for `CustomResourceSubResourceStatus`: https://github.com/kubernetes/kubernetes/pull/55970.
- Add feature gate for `CustomResourceSubResources`.
    - Update CRD strategy: if feature gate is disabled, this feature is dropped (i.e. set to `nil`).
- Add validation for `CustomResourceSubResources`:
    - `SpecReplicasPath` should not be empty and should be a valid json path under `.spec`. If there is no value under the given path in the CustomResource, the `/scale` subresource will return an error on GET.
    - `StatusReplicasPath` should not be empty and should be a valid json path under `.status`. If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource will default to 0.
    - If present, `LabelSelectorPath` should be a valid json path. If there is no value under `LabelSelectorPath` in the CustomResource, the status label selector value in the `/scale` subresource will default to the empty string.
    - `ScaleGroupVersion` should be `autoscaling/v1`.
    - If `CustomResourceSubResources` is enabled, only `properties` is allowed under the root schema for CRD validation.

**Add status and scale subresources**:

- Use helper functions from `apimachinery/pkg/apis/meta/v1/unstructured/helpers.go`.
    - Improve error handling: https://github.com/kubernetes/kubernetes/pull/56563, https://github.com/kubernetes/kubernetes/pull/58215.
- Introduce Registry interface for storage.
- Update storage:
    - Introduce `CustomResourceStorage` which acts as storage for the custom resource and its status and scale subresources. Note: storage for status and scale is only enabled when the feature gate is enabled _and_ the respective fields are enabled in the CRD.
    - Introduce `StatusREST` and its `New()`, `Get()` and `Update()` methods.
    - Introduce `ScaleREST` and its `New()`, `Get()` and `Update()` methods.
        - Get and Update use the json paths from the CRD and use it to return an `autoscaling/v1.Scale` object.
- Update strategy:
    - In `PrepareForCreate`,
         - Clear `.status`.
         - Set `.metadata.generation` = 1
    - In `PrepareForUpdate`,
         - Do not update `.status`.
             - If both the old and new objects have `.status` and it is changed, set it back to its old value.
             - If the old object has a `.status` but the new object doesn't, set it to the old value.
             - If old object did not have a `.status` but the new object does, delete it.
         - Increment generation if spec changes i.e. in the following cases:
             - If both the old and new objects had `.spec` and it changed.
             - If the old object did not have `.spec` but the new object does.
             - If the old object had a `.spec` but the new object doesn't.
     - In `Validate` and `ValidateUpdate`,
        - ensure that values at `specReplicasPath` and `statusReplicasPath` are >=0 and < maxInt32.
        - make sure there are no errors in getting the value at all the paths.
    - Introduce `statusStrategy` with its methods.
        - In `PrepareForUpdate`:
            - Do not update `.spec`.
                - If both the old and new objects have `.spec` and it is changed, set it back to its old value.
                - If the old object has a `.spec` but the new object doesn't, set it to the old value.
                - If old object did not have a `.spec` but the new object does, delete it.
             - Do not update `.metadata`.
        - In `ValidateStatusUpdate`:
            - For CRD validation, validate only under `.status`.
            - Validate value at `statusReplicasPath` as above. If `labelSelectorPath` is a path under `.status`, then validate it as well.
- Plug into the custom resource handler:
    - Store all three storage - customResource, status and scale in `crdInfo`.
    - Use the storage as per the subresource in the request.
    - Use the validator as per the subresource (for status, only use the schema for `status`, if present).
    - Serve the endpoint as per the subresource - see `serveResource`, `serveStatus` and `serveScale`.
- Update discovery by adding the `/status` and `/scale` resources, if enabled.

**Add tests**:

- Add unit tests in `etcd_test.go`.
- Add integration tests.
    - In `subresources_test.go`, use the [polymporphic scale client](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/client-go/scale) to get and update `Scale`.
    -  Add a test to check everything works fine with yaml in `yaml_test.go`.

**Release note**:

```release-note
`/status` and `/scale` subresources are added for custom resources.
```
pull/6/head
Kubernetes Submit Queue 2018-02-22 13:37:35 -08:00 committed by GitHub
commit 6e856480c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 4366 additions and 497 deletions

2
Godeps/Godeps.json generated
View File

@ -1263,7 +1263,7 @@
},
{
"ImportPath": "github.com/go-openapi/validate",
"Rev": "deaf2c9013bc1a7f4c774662259a506ba874d80f"
"Rev": "d509235108fcf6ab4913d2dcb3a2260c0db2108e"
},
{
"ImportPath": "github.com/godbus/dbus",

View File

@ -83905,6 +83905,10 @@
"description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced",
"type": "string"
},
"subresources": {
"description": "Subresources describes the subresources for CustomResources This field is alpha-level and should only be sent to servers that enable subresources via the CustomResourceSubresources feature gate.",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources"
},
"validation": {
"description": "Validation describes the validation methods for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation"
@ -83935,6 +83939,43 @@
}
}
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceScale": {
"description": "CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.",
"required": [
"specReplicasPath",
"statusReplicasPath"
],
"properties": {
"labelSelectorPath": {
"description": "LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector. Only JSON paths without the array notation are allowed. Must be a JSON Path under .status. Must be set to work with HPA. If there is no value under the given path in the CustomResource, the status label selector value in the /scale subresource will default to the empty string.",
"type": "string"
},
"specReplicasPath": {
"description": "SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas. Only JSON paths without the array notation are allowed. Must be a JSON Path under .spec. If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.",
"type": "string"
},
"statusReplicasPath": {
"description": "StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas. Only JSON paths without the array notation are allowed. Must be a JSON Path under .status. If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource will default to 0.",
"type": "string"
}
}
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceStatus": {
"description": "CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources. Status is represented by the `.status` JSON path inside of a CustomResource. When set, * exposes a /status subresource for the custom resource * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza"
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources": {
"description": "CustomResourceSubresources defines the status and scale subresources for CustomResources.",
"properties": {
"scale": {
"description": "Scale denotes the scale subresource for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceScale"
},
"status": {
"description": "Status denotes the status subresource for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceStatus"
}
}
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation": {
"description": "CustomResourceValidation is a list of validation methods for CustomResources.",
"properties": {

View File

@ -24,7 +24,7 @@ import (
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
discocache "k8s.io/client-go/discovery/cached" // Saturday Night Fever
discocache "k8s.io/client-go/discovery/cached"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/scale"
"k8s.io/kubernetes/pkg/controller/podautoscaler"

View File

@ -477,7 +477,6 @@ staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server
staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer
staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status
staging/src/k8s.io/apiextensions-apiserver/pkg/features
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition
staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver
staging/src/k8s.io/apimachinery/pkg/api/meta

View File

@ -304,7 +304,8 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
// inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side:
apiextensionsfeatures.CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
apiextensionsfeatures.CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
apiextensionsfeatures.CustomResourceSubresources: {Default: false, PreRelease: utilfeature.Alpha},
// features that enable backwards compatibility but are scheduled to be removed
ServiceProxyAllowExternalIPs: {Default: false, PreRelease: utilfeature.Deprecated},

View File

@ -30,6 +30,22 @@
"ImportPath": "github.com/beorn7/perks/quantile",
"Rev": "3ac7bf7a47d159a033b107610db8a1b6575507a4"
},
{
"ImportPath": "github.com/cockroachdb/cmux",
"Rev": "112f0506e7743d64a6eb8fedbcff13d9979bbf92"
},
{
"ImportPath": "github.com/coreos/bbolt",
"Rev": "48ea1b39c25fc1bab3506fbc712ecbaa842c4d2d"
},
{
"ImportPath": "github.com/coreos/etcd/alarm",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/auth",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/auth/authpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
@ -42,26 +58,214 @@
"ImportPath": "github.com/coreos/etcd/clientv3",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/clientv3/concurrency",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/clientv3/namespace",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/clientv3/naming",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/compactor",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/discovery",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/embed",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/error",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/etcdhttp",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v2http",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v2http/httptypes",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3client",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3election",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb/gw",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3lock",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3lock/v3lockpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3lock/v3lockpb/gw",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3rpc",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/auth",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/etcdserverpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/etcdserverpb/gw",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/membership",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/stats",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/integration",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/lease",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/lease/leasehttp",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/lease/leasepb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/mvcc",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/mvcc/backend",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/mvcc/mvccpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/adt",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/contention",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/cors",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/cpuutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/crc",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/debugutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/fileutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/httputil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/idutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/ioutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/logutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/monotime",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/netutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/pathutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/pbutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/runtime",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/schedule",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/srv",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/testutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/tlsutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
@ -74,10 +278,58 @@
"ImportPath": "github.com/coreos/etcd/pkg/types",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/wait",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/proxy/grpcproxy",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/proxy/grpcproxy/adapter",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/proxy/grpcproxy/cache",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/raft",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/raft/raftpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/rafthttp",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/snap",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/snap/snappb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/store",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/version",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/wal",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/wal/walpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/go-semver/semver",
"Rev": "568e959cd89871e61434c1143528d9162da89ef2"
@ -86,10 +338,22 @@
"ImportPath": "github.com/coreos/go-systemd/daemon",
"Rev": "48702e0da86bd25e76cfef347e2adeb434a0d0a6"
},
{
"ImportPath": "github.com/coreos/go-systemd/journal",
"Rev": "48702e0da86bd25e76cfef347e2adeb434a0d0a6"
},
{
"ImportPath": "github.com/coreos/pkg/capnslog",
"Rev": "fa29b1d70f0beaddd4c7021607cc3c3be8ce94b8"
},
{
"ImportPath": "github.com/davecgh/go-spew/spew",
"Rev": "782f4967f2dc4564575ca782fe2d04090b5faca8"
},
{
"ImportPath": "github.com/dgrijalva/jwt-go",
"Rev": "01aeca54ebda6e0fbfafd0a524d234159c05ec20"
},
{
"ImportPath": "github.com/elazarl/go-bindata-assetfs",
"Rev": "3dcc96556217539f50599357fb481ac0dc7439b9"
@ -152,7 +416,7 @@
},
{
"ImportPath": "github.com/go-openapi/validate",
"Rev": "deaf2c9013bc1a7f4c774662259a506ba874d80f"
"Rev": "d509235108fcf6ab4913d2dcb3a2260c0db2108e"
},
{
"ImportPath": "github.com/gogo/protobuf/proto",
@ -166,6 +430,14 @@
"ImportPath": "github.com/golang/glog",
"Rev": "44145f04b68cf362d9c4df2182967c2275eaefed"
},
{
"ImportPath": "github.com/golang/groupcache/lru",
"Rev": "02826c3e79038b59d737d3b1c0a1d937f71a4433"
},
{
"ImportPath": "github.com/golang/protobuf/jsonpb",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/proto",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
@ -186,10 +458,18 @@
"ImportPath": "github.com/golang/protobuf/ptypes/duration",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes/struct",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes/timestamp",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/google/btree",
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
},
{
"ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
@ -206,6 +486,22 @@
"ImportPath": "github.com/googleapis/gnostic/extensions",
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus",
"Rev": "2500245aa6110c562d17020fb31a2c133d737799"
},
{
"ImportPath": "github.com/grpc-ecosystem/grpc-gateway/runtime",
"Rev": "8cc3a55af3bcf171a1c23a90c4df9cf591706104"
},
{
"ImportPath": "github.com/grpc-ecosystem/grpc-gateway/runtime/internal",
"Rev": "8cc3a55af3bcf171a1c23a90c4df9cf591706104"
},
{
"ImportPath": "github.com/grpc-ecosystem/grpc-gateway/utilities",
"Rev": "8cc3a55af3bcf171a1c23a90c4df9cf591706104"
},
{
"ImportPath": "github.com/hashicorp/golang-lru",
"Rev": "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4"
@ -226,6 +522,10 @@
"ImportPath": "github.com/inconshreveable/mousetrap",
"Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
},
{
"ImportPath": "github.com/jonboulle/clockwork",
"Rev": "72f9bd7c4e0c2a40055ab3d0f09654f730cce982"
},
{
"ImportPath": "github.com/json-iterator/go",
"Rev": "13f86432b882000a51c6e610c620974462691a97"
@ -302,6 +602,18 @@
"ImportPath": "github.com/ugorji/go/codec",
"Rev": "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74"
},
{
"ImportPath": "github.com/xiang90/probing",
"Rev": "07dd2e8dfe18522e9c447ba95f2fe95262f63bb2"
},
{
"ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "81e90905daefcd6fd217b62423c0908922eadb30"
},
{
"ImportPath": "golang.org/x/crypto/blowfish",
"Rev": "81e90905daefcd6fd217b62423c0908922eadb30"
},
{
"ImportPath": "golang.org/x/crypto/ssh/terminal",
"Rev": "81e90905daefcd6fd217b62423c0908922eadb30"
@ -1034,6 +1346,10 @@
"ImportPath": "k8s.io/apiserver/pkg/registry/rest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/registry/rest/resttest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/server",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1082,10 +1398,18 @@
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/etcdtest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/metrics",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/testing/testingcert",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/util",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1110,6 +1434,10 @@
"ImportPath": "k8s.io/apiserver/pkg/storage/storagebackend/factory",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/value",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1158,6 +1486,10 @@
"ImportPath": "k8s.io/client-go/discovery",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/dynamic",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/informers",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1546,6 +1878,34 @@
"ImportPath": "k8s.io/client-go/rest/watch",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale/scheme",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale/scheme/appsint",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale/scheme/appsv1beta1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale/scheme/appsv1beta2",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale/scheme/autoscalingv1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale/scheme/extensionsint",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale/scheme/extensionsv1beta1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1646,6 +2006,10 @@
"ImportPath": "k8s.io/apimachinery/pkg/api/meta",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/resource",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/testing/fuzzer",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1666,6 +2030,10 @@
"ImportPath": "k8s.io/apimachinery/pkg/apimachinery/registered",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/internalversion",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1710,10 +2078,18 @@
"ImportPath": "k8s.io/apimachinery/pkg/types",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/diff",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/errors",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/intstr",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/json",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1778,6 +2154,10 @@
"ImportPath": "k8s.io/apiserver/pkg/registry/generic/registry",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/registry/generic/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/registry/rest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1802,6 +2182,10 @@
"ImportPath": "k8s.io/apiserver/pkg/storage/errors",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/names",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1814,6 +2198,10 @@
"ImportPath": "k8s.io/apiserver/pkg/util/feature",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/util/feature/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/util/logs",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@ -1834,6 +2222,14 @@
"ImportPath": "k8s.io/client-go/rest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/scale/scheme/autoscalingv1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

View File

@ -16,7 +16,9 @@ limitations under the License.
package apiextensions
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CustomResourceDefinitionSpec describes how a user wants their resource to appear
type CustomResourceDefinitionSpec struct {
@ -30,6 +32,8 @@ type CustomResourceDefinitionSpec struct {
Scope ResourceScope
// Validation describes the validation methods for CustomResources
Validation *CustomResourceValidation
// Subresources describes the subresources for CustomResources
Subresources *CustomResourceSubresources
}
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
@ -146,3 +150,41 @@ type CustomResourceValidation struct {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
OpenAPIV3Schema *JSONSchemaProps
}
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
type CustomResourceSubresources struct {
// Status denotes the status subresource for CustomResources
Status *CustomResourceSubresourceStatus
// Scale denotes the scale subresource for CustomResources
Scale *CustomResourceSubresourceScale
}
// CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources.
// Status is represented by the `.status` JSON path inside of a CustomResource. When set,
// * exposes a /status subresource for the custom resource
// * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza
// * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza
type CustomResourceSubresourceStatus struct{}
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
type CustomResourceSubresourceScale struct {
// SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .spec.
// If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.
SpecReplicasPath string
// StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource
// will default to 0.
StatusReplicasPath string
// LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// Must be set to work with HPA.
// If there is no value under the given path in the CustomResource, the status label selector value in the /scale
// subresource will default to the empty string.
// +optional
LabelSelectorPath *string
}

View File

@ -107,6 +107,12 @@ message CustomResourceDefinitionSpec {
// Validation describes the validation methods for CustomResources
// +optional
optional CustomResourceValidation validation = 5;
// Subresources describes the subresources for CustomResources
// This field is alpha-level and should only be sent to servers that enable
// subresources via the CustomResourceSubresources feature gate.
// +optional
optional CustomResourceSubresources subresources = 6;
}
// CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition
@ -119,6 +125,48 @@ message CustomResourceDefinitionStatus {
optional CustomResourceDefinitionNames acceptedNames = 2;
}
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
message CustomResourceSubresourceScale {
// SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .spec.
// If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.
optional string specReplicasPath = 1;
// StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource
// will default to 0.
optional string statusReplicasPath = 2;
// LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// Must be set to work with HPA.
// If there is no value under the given path in the CustomResource, the status label selector value in the /scale
// subresource will default to the empty string.
// +optional
optional string labelSelectorPath = 3;
}
// CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources.
// Status is represented by the `.status` JSON path inside of a CustomResource. When set,
// * exposes a /status subresource for the custom resource
// * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza
// * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza
message CustomResourceSubresourceStatus {
}
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
message CustomResourceSubresources {
// Status denotes the status subresource for CustomResources
optional CustomResourceSubresourceStatus status = 1;
// Scale denotes the scale subresource for CustomResources
optional CustomResourceSubresourceScale scale = 2;
}
// CustomResourceValidation is a list of validation methods for CustomResources.
message CustomResourceValidation {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.

View File

@ -16,7 +16,9 @@ limitations under the License.
package v1beta1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CustomResourceDefinitionSpec describes how a user wants their resource to appear
type CustomResourceDefinitionSpec struct {
@ -31,6 +33,11 @@ type CustomResourceDefinitionSpec struct {
// Validation describes the validation methods for CustomResources
// +optional
Validation *CustomResourceValidation `json:"validation,omitempty" protobuf:"bytes,5,opt,name=validation"`
// Subresources describes the subresources for CustomResources
// This field is alpha-level and should only be sent to servers that enable
// subresources via the CustomResourceSubresources feature gate.
// +optional
Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,6,opt,name=subresources"`
}
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
@ -147,3 +154,41 @@ type CustomResourceValidation struct {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
OpenAPIV3Schema *JSONSchemaProps `json:"openAPIV3Schema,omitempty" protobuf:"bytes,1,opt,name=openAPIV3Schema"`
}
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
type CustomResourceSubresources struct {
// Status denotes the status subresource for CustomResources
Status *CustomResourceSubresourceStatus `json:"status,omitempty" protobuf:"bytes,1,opt,name=status"`
// Scale denotes the scale subresource for CustomResources
Scale *CustomResourceSubresourceScale `json:"scale,omitempty" protobuf:"bytes,2,opt,name=scale"`
}
// CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources.
// Status is represented by the `.status` JSON path inside of a CustomResource. When set,
// * exposes a /status subresource for the custom resource
// * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza
// * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza
type CustomResourceSubresourceStatus struct{}
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
type CustomResourceSubresourceScale struct {
// SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .spec.
// If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.
SpecReplicasPath string `json:"specReplicasPath" protobuf:"bytes,1,name=specReplicasPath"`
// StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource
// will default to 0.
StatusReplicasPath string `json:"statusReplicasPath" protobuf:"bytes,2,opt,name=statusReplicasPath"`
// LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// Must be set to work with HPA.
// If there is no value under the given path in the CustomResource, the status label selector value in the /scale
// subresource will default to the empty string.
// +optional
LabelSelectorPath *string `json:"labelSelectorPath,omitempty" protobuf:"bytes,3,opt,name=labelSelectorPath"`
}

View File

@ -48,6 +48,12 @@ func RegisterConversions(scheme *runtime.Scheme) error {
Convert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomResourceDefinitionSpec,
Convert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomResourceDefinitionStatus,
Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus,
Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale,
Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale,
Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus,
Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus,
Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources,
Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources,
Convert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation,
Convert_apiextensions_CustomResourceValidation_To_v1beta1_CustomResourceValidation,
Convert_v1beta1_ExternalDocumentation_To_apiextensions_ExternalDocumentation,
@ -211,6 +217,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes
} else {
out.Validation = nil
}
out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
return nil
}
@ -235,6 +242,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes
} else {
out.Validation = nil
}
out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
return nil
}
@ -269,6 +277,70 @@ func Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResou
return autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus(in, out, s)
}
func autoConvert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in *CustomResourceSubresourceScale, out *apiextensions.CustomResourceSubresourceScale, s conversion.Scope) error {
out.SpecReplicasPath = in.SpecReplicasPath
out.StatusReplicasPath = in.StatusReplicasPath
out.LabelSelectorPath = (*string)(unsafe.Pointer(in.LabelSelectorPath))
return nil
}
// Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in *CustomResourceSubresourceScale, out *apiextensions.CustomResourceSubresourceScale, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in *apiextensions.CustomResourceSubresourceScale, out *CustomResourceSubresourceScale, s conversion.Scope) error {
out.SpecReplicasPath = in.SpecReplicasPath
out.StatusReplicasPath = in.StatusReplicasPath
out.LabelSelectorPath = (*string)(unsafe.Pointer(in.LabelSelectorPath))
return nil
}
// Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in *apiextensions.CustomResourceSubresourceScale, out *CustomResourceSubresourceScale, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in, out, s)
}
func autoConvert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in *CustomResourceSubresourceStatus, out *apiextensions.CustomResourceSubresourceStatus, s conversion.Scope) error {
return nil
}
// Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in *CustomResourceSubresourceStatus, out *apiextensions.CustomResourceSubresourceStatus, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in *apiextensions.CustomResourceSubresourceStatus, out *CustomResourceSubresourceStatus, s conversion.Scope) error {
return nil
}
// Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in *apiextensions.CustomResourceSubresourceStatus, out *CustomResourceSubresourceStatus, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in, out, s)
}
func autoConvert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in *CustomResourceSubresources, out *apiextensions.CustomResourceSubresources, s conversion.Scope) error {
out.Status = (*apiextensions.CustomResourceSubresourceStatus)(unsafe.Pointer(in.Status))
out.Scale = (*apiextensions.CustomResourceSubresourceScale)(unsafe.Pointer(in.Scale))
return nil
}
// Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in *CustomResourceSubresources, out *apiextensions.CustomResourceSubresources, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in *apiextensions.CustomResourceSubresources, out *CustomResourceSubresources, s conversion.Scope) error {
out.Status = (*CustomResourceSubresourceStatus)(unsafe.Pointer(in.Status))
out.Scale = (*CustomResourceSubresourceScale)(unsafe.Pointer(in.Scale))
return nil
}
// Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in *apiextensions.CustomResourceSubresources, out *CustomResourceSubresources, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in, out, s)
}
func autoConvert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(in *CustomResourceValidation, out *apiextensions.CustomResourceValidation, s conversion.Scope) error {
if in.OpenAPIV3Schema != nil {
in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema

View File

@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
(*in).DeepCopyInto(*out)
}
}
if in.Subresources != nil {
in, out := &in.Subresources, &out.Subresources
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresources)
(*in).DeepCopyInto(*out)
}
}
return
}
@ -175,6 +184,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) {
*out = *in
if in.LabelSelectorPath != nil {
in, out := &in.LabelSelectorPath, &out.LabelSelectorPath
if *in == nil {
*out = nil
} else {
*out = new(string)
**out = **in
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceScale.
func (in *CustomResourceSubresourceScale) DeepCopy() *CustomResourceSubresourceScale {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceScale)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceStatus) DeepCopyInto(out *CustomResourceSubresourceStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceStatus.
func (in *CustomResourceSubresourceStatus) DeepCopy() *CustomResourceSubresourceStatus {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresources) DeepCopyInto(out *CustomResourceSubresources) {
*out = *in
if in.Status != nil {
in, out := &in.Status, &out.Status
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceStatus)
**out = **in
}
}
if in.Scale != nil {
in, out := &in.Scale, &out.Scale
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceScale)
(*in).DeepCopyInto(*out)
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresources.
func (in *CustomResourceSubresources) DeepCopy() *CustomResourceSubresources {
if in == nil {
return nil
}
out := new(CustomResourceSubresources)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) {
*out = *in

View File

@ -18,6 +18,7 @@ package validation
import (
"fmt"
"reflect"
"strings"
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
@ -107,7 +108,13 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, fldPath.Child("validation"))...)
} else if spec.Validation != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate"))
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate CustomResourceValidation"))
}
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...)
} else if spec.Subresources != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("subresources"), "disabled by feature-gate CustomResourceSubresources"))
}
return allErrs
@ -182,9 +189,27 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
return allErrs
}
if customResourceValidation.OpenAPIV3Schema != nil {
if schema := customResourceValidation.OpenAPIV3Schema; schema != nil {
// if subresources are enabled, only properties is allowed inside the root schema
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
v := reflect.ValueOf(schema).Elem()
fieldsPresent := 0
for i := 0; i < v.NumField(); i++ {
field := v.Field(i).Interface()
if !reflect.DeepEqual(field, reflect.Zero(reflect.TypeOf(field)).Interface()) {
fieldsPresent++
}
}
if fieldsPresent > 1 || (fieldsPresent == 1 && v.FieldByName("Properties").IsNil()) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), *schema, fmt.Sprintf("if subresources for custom resources are enabled, only properties can be used at the root of the schema")))
return allErrs
}
}
openAPIV3Schema := &specStandardValidatorV3{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(customResourceValidation.OpenAPIV3Schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
}
// if validation passed otherwise, make sure we can actually construct a schema validator from this custom resource validation.
@ -326,3 +351,64 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
return allErrs
}
// ValidateCustomResourceDefinitionSubresources statically validates
func ValidateCustomResourceDefinitionSubresources(subresources *apiextensions.CustomResourceSubresources, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if subresources == nil {
return allErrs
}
if subresources.Scale != nil {
if len(subresources.Scale.SpecReplicasPath) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("scale.specReplicasPath"), ""))
} else {
// should be constrained json path under .spec
if errs := validateSimpleJSONPath(subresources.Scale.SpecReplicasPath, fldPath.Child("scale.specReplicasPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(subresources.Scale.SpecReplicasPath, ".spec.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.specReplicasPath"), subresources.Scale.SpecReplicasPath, "should be a json path under .spec"))
}
}
if len(subresources.Scale.StatusReplicasPath) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("scale.statusReplicasPath"), ""))
} else {
// should be constrained json path under .status
if errs := validateSimpleJSONPath(subresources.Scale.StatusReplicasPath, fldPath.Child("scale.statusReplicasPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(subresources.Scale.StatusReplicasPath, ".status.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.statusReplicasPath"), subresources.Scale.StatusReplicasPath, "should be a json path under .status"))
}
}
// if labelSelectorPath is present, it should be a constrained json path under .status
if subresources.Scale.LabelSelectorPath != nil && len(*subresources.Scale.LabelSelectorPath) > 0 {
if errs := validateSimpleJSONPath(*subresources.Scale.LabelSelectorPath, fldPath.Child("scale.labelSelectorPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(*subresources.Scale.LabelSelectorPath, ".status.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.labelSelectorPath"), subresources.Scale.LabelSelectorPath, "should be a json path under .status"))
}
}
}
return allErrs
}
func validateSimpleJSONPath(s string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
switch {
case len(s) == 0:
allErrs = append(allErrs, field.Invalid(fldPath, s, "must not be empty"))
case s[0] != '.':
allErrs = append(allErrs, field.Invalid(fldPath, s, "must be a simple json path starting with ."))
case s != ".":
if cs := strings.Split(s[1:], "."); len(cs) < 1 {
allErrs = append(allErrs, field.Invalid(fldPath, s, "must be a json path in the dot notation"))
}
}
return allErrs
}

View File

@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
(*in).DeepCopyInto(*out)
}
}
if in.Subresources != nil {
in, out := &in.Subresources, &out.Subresources
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresources)
(*in).DeepCopyInto(*out)
}
}
return
}
@ -175,6 +184,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) {
*out = *in
if in.LabelSelectorPath != nil {
in, out := &in.LabelSelectorPath, &out.LabelSelectorPath
if *in == nil {
*out = nil
} else {
*out = new(string)
**out = **in
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceScale.
func (in *CustomResourceSubresourceScale) DeepCopy() *CustomResourceSubresourceScale {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceScale)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceStatus) DeepCopyInto(out *CustomResourceSubresourceStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceStatus.
func (in *CustomResourceSubresourceStatus) DeepCopy() *CustomResourceSubresourceStatus {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresources) DeepCopyInto(out *CustomResourceSubresources) {
*out = *in
if in.Status != nil {
in, out := &in.Status, &out.Status
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceStatus)
**out = **in
}
}
if in.Scale != nil {
in, out := &in.Scale, &out.Scale
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceScale)
(*in).DeepCopyInto(*out)
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresources.
func (in *CustomResourceSubresources) DeepCopy() *CustomResourceSubresources {
if in == nil {
return nil
}
out := new(CustomResourceSubresources)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) {
*out = *in

View File

@ -16,7 +16,11 @@ go_library(
],
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver",
deps = [
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/go-openapi/strfmt:go_default_library",
"//vendor/github.com/go-openapi/validate:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
@ -29,6 +33,7 @@ go_library(
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/controller/status:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
@ -60,7 +65,10 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/storage:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/scale:go_default_library",
"//vendor/k8s.io/client-go/scale/scheme/autoscalingv1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/util/workqueue:go_default_library",
],

View File

@ -22,6 +22,7 @@ import (
"github.com/golang/glog"
autoscaling "k8s.io/api/autoscaling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -117,6 +118,26 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
Verbs: verbs,
ShortNames: crd.Status.AcceptedNames.ShortNames,
})
if crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Name: crd.Status.AcceptedNames.Plural + "/status",
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
Kind: crd.Status.AcceptedNames.Kind,
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
})
}
if crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Group: autoscaling.GroupName,
Version: "v1",
Kind: "Scale",
Name: crd.Status.AcceptedNames.Plural + "/scale",
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
})
}
}
if !foundGroup {

View File

@ -25,6 +25,9 @@ import (
"sync/atomic"
"time"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/golang/glog"
apiequality "k8s.io/apimachinery/pkg/api/equality"
@ -35,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
"k8s.io/apimachinery/pkg/types"
@ -47,7 +51,10 @@ import (
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/storage/storagebackend"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/discovery"
"k8s.io/client-go/scale"
"k8s.io/client-go/scale/scheme/autoscalingv1"
"k8s.io/client-go/tools/cache"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
@ -55,6 +62,7 @@ import (
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
)
@ -87,8 +95,11 @@ type crdInfo struct {
spec *apiextensions.CustomResourceDefinitionSpec
acceptedNames *apiextensions.CustomResourceDefinitionNames
storage *customresource.REST
requestScope handlers.RequestScope
storage customresource.CustomResourceStorage
requestScope handlers.RequestScope
scaleRequestScope handlers.RequestScope
statusRequestScope handlers.RequestScope
}
// crdStorageMap goes from customresourcedefinition to its storage
@ -172,10 +183,6 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.delegate.ServeHTTP(w, req)
return
}
if len(requestInfo.Subresource) > 0 {
http.NotFound(w, req)
return
}
terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating)
@ -185,61 +192,126 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
storage := crdInfo.storage
requestScope := crdInfo.requestScope
minRequestTimeout := 1 * time.Minute
verb := strings.ToUpper(requestInfo.Verb)
resource := requestInfo.Resource
subresource := requestInfo.Subresource
scope := metrics.CleanScope(requestInfo)
supportedTypes := []string{
string(types.JSONPatchType),
string(types.MergePatchType),
}
var handler http.HandlerFunc
switch {
case subresource == "status" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil:
handler = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case subresource == "scale" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil:
handler = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case len(subresource) == 0:
handler = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
default:
http.Error(w, "the server could not find the requested resource", http.StatusNotFound)
}
if handler != nil {
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
handler(w, req)
return
}
}
func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.requestScope
storage := crdInfo.storage.CustomResource
minRequestTimeout := 1 * time.Minute
switch requestInfo.Verb {
case "get":
handler = handlers.GetResource(storage, storage, requestScope)
return handlers.GetResource(storage, storage, requestScope)
case "list":
forceWatch := false
handler = handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
case "watch":
forceWatch := true
handler = handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
case "create":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return
return nil
}
handler = handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
return handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "update":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return
return nil
}
handler = handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return
return nil
}
supportedTypes := []string{
string(types.JSONPatchType),
string(types.MergePatchType),
}
handler = handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
case "delete":
allowsOptions := true
handler = handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
return handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
case "deletecollection":
checkBody := true
handler = handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
return handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return
return nil
}
}
func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.statusRequestScope
storage := crdInfo.storage.Status
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, nil, requestScope)
case "update":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
}
func (r *crdHandler) serveScale(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.scaleRequestScope
storage := crdInfo.storage.Scale
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, nil, requestScope)
case "update":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
handler(w, req)
return
}
func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) {
@ -265,7 +337,8 @@ func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{})
// as it is used without locking elsewhere.
storageMap2 := storageMap.clone()
if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok {
oldInfo.storage.DestroyFunc()
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
oldInfo.storage.CustomResource.DestroyFunc()
delete(storageMap2, types.UID(oldCRD.UID))
}
@ -297,7 +370,8 @@ func (r *crdHandler) removeDeadStorage() {
}
if !found {
glog.V(4).Infof("Removing dead CRD storage for %v", s.requestScope.Resource)
s.storage.DestroyFunc()
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
s.storage.CustomResource.DestroyFunc()
delete(storageMap2, uid)
}
}
@ -311,7 +385,7 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions
if err != nil {
return nil, err
}
return info.storage, nil
return info.storage.CustomResource, nil
}
func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) {
@ -340,9 +414,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
parameterCodec := runtime.NewParameterCodec(parameterScheme)
kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.Kind}
typer := unstructuredObjectTyper{
delegate: parameterScheme,
unstructuredTyper: discovery.NewUnstructuredObjectTyper(nil),
typer := UnstructuredObjectTyper{
Delegate: parameterScheme,
UnstructuredTyper: discovery.NewUnstructuredObjectTyper(nil),
}
creator := unstructuredCreator{}
@ -351,7 +425,29 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
return nil, err
}
storage := customresource.NewREST(
var statusSpec *apiextensions.CustomResourceSubresourceStatus
var statusValidator *validate.SchemaValidator
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
statusSpec = crd.Spec.Subresources.Status
// for the status subresource, validate only against the status schema
if crd.Spec.Validation != nil && crd.Spec.Validation.OpenAPIV3Schema != nil && crd.Spec.Validation.OpenAPIV3Schema.Properties != nil {
if statusSchema, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["status"]; ok {
openapiSchema := &spec.Schema{}
if err := apiservervalidation.ConvertJSONSchemaProps(&statusSchema, openapiSchema); err != nil {
return nil, err
}
statusValidator = validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)
}
}
}
var scaleSpec *apiextensions.CustomResourceSubresourceScale
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
scaleSpec = crd.Spec.Subresources.Scale
}
customResourceStorage := customresource.NewStorage(
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural},
schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.ListKind},
customresource.NewStrategy(
@ -359,6 +455,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
crd.Spec.Scope == apiextensions.NamespaceScoped,
kind,
validator,
statusValidator,
statusSpec,
scaleSpec,
),
r.restOptionsGetter,
)
@ -373,12 +472,15 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
var ctxFn handlers.ContextFunc
ctxFn = func(req *http.Request) apirequest.Context {
ret, _ := r.requestContextMapper.Get(req)
return ret
}
requestScope := handlers.RequestScope{
Namer: handlers.ContextBasedNaming{
GetContext: func(req *http.Request) apirequest.Context {
ret, _ := r.requestContextMapper.Get(req)
return ret
},
GetContext: ctxFn,
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
@ -400,9 +502,8 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
Typer: typer,
UnsafeConvertor: unstructured.UnstructuredObjectConverter{},
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural},
Kind: kind,
Subresource: "",
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural},
Kind: kind,
MetaGroupVersion: metav1.SchemeGroupVersion,
}
@ -411,8 +512,33 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
spec: &crd.Spec,
acceptedNames: &crd.Status.AcceptedNames,
storage: storage,
requestScope: requestScope,
storage: customResourceStorage,
requestScope: requestScope,
scaleRequestScope: requestScope, // shallow copy
statusRequestScope: requestScope, // shallow copy
}
// override scaleSpec subresource values
scaleConverter := scale.NewScaleConverter()
ret.scaleRequestScope.Subresource = "scale"
ret.scaleRequestScope.Serializer = serializer.NewCodecFactory(scaleConverter.Scheme())
ret.scaleRequestScope.Kind = autoscalingv1.SchemeGroupVersion.WithKind("Scale")
ret.scaleRequestScope.Namer = handlers.ContextBasedNaming{
GetContext: ctxFn,
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
SelfLinkPathSuffix: "/scale",
}
// override status subresource values
ret.statusRequestScope.Subresource = "status"
ret.statusRequestScope.Namer = handlers.ContextBasedNaming{
GetContext: ctxFn,
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
SelfLinkPathSuffix: "/status",
}
// Copy because we cannot write to storageMap without a race
@ -477,21 +603,21 @@ func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decod
return versioning.NewDefaultingCodecForScheme(Scheme, nil, decoder, nil, gv)
}
type unstructuredObjectTyper struct {
delegate runtime.ObjectTyper
unstructuredTyper runtime.ObjectTyper
type UnstructuredObjectTyper struct {
Delegate runtime.ObjectTyper
UnstructuredTyper runtime.ObjectTyper
}
func (t unstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
func (t UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
// Delegate for things other than Unstructured.
if _, ok := obj.(runtime.Unstructured); !ok {
return t.delegate.ObjectKinds(obj)
return t.Delegate.ObjectKinds(obj)
}
return t.unstructuredTyper.ObjectKinds(obj)
return t.UnstructuredTyper.ObjectKinds(obj)
}
func (t unstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
return t.delegate.Recognizes(gvk) || t.unstructuredTyper.Recognizes(gvk)
func (t UnstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
return t.Delegate.Recognizes(gvk) || t.UnstructuredTyper.Recognizes(gvk)
}
type unstructuredCreator struct{}

View File

@ -29,7 +29,7 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
// Convert CRD schema to openapi schema
openapiSchema := &spec.Schema{}
if customResourceValidation != nil {
if err := convertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
if err := ConvertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
return nil, err
}
}
@ -39,6 +39,10 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
// CustomResource is a JSON data structure.
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
if validator == nil {
return nil
}
result := validator.Validate(customResource)
if result.AsError() != nil {
return result.AsError()
@ -46,7 +50,8 @@ func ValidateCustomResource(customResource interface{}, validator *validate.Sche
return nil
}
func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
// ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema
func ConvertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
if in == nil {
return nil
}
@ -99,7 +104,7 @@ func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema)
if in.Not != nil {
in, out := &in.Not, &out.Not
*out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil {
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
@ -176,7 +181,7 @@ func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]s
if in != nil {
for _, jsonSchemaProps := range *in {
schema := spec.Schema{}
if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return err
}
*out = append(*out, schema)
@ -190,7 +195,7 @@ func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (m
if len(in) != 0 {
for k, jsonSchemaProps := range in {
schema := spec.Schema{}
if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return nil, err
}
out[k] = schema
@ -203,7 +208,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil {
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
@ -211,7 +216,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
in, out := &in.JSONSchemas, &out.Schemas
*out = make([]spec.Schema, len(*in))
for i := range *in {
if err := convertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
if err := ConvertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
return err
}
}
@ -224,7 +229,7 @@ func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil {
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
@ -236,7 +241,7 @@ func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStri
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil {
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}

View File

@ -58,7 +58,7 @@ func TestRoundTrip(t *testing.T) {
// internal -> go-openapi
openAPITypes := &spec.Schema{}
if err := convertJSONSchemaProps(internal, openAPITypes); err != nil {
if err := ConvertJSONSchemaProps(internal, openAPITypes); err != nil {
t.Fatal(err)
}

View File

@ -33,6 +33,12 @@ const (
//
// CustomResourceValidation is a list of validation methods for CustomResources
CustomResourceValidation utilfeature.Feature = "CustomResourceValidation"
// owner: @sttts, @nikhita
// alpha: v1.10
//
// CustomResourceSubresources defines the subresources for CustomResources
CustomResourceSubresources utilfeature.Feature = "CustomResourceSubresources"
)
func init() {
@ -43,5 +49,6 @@ func init() {
// To add a new feature, define a key for it above and add it here. The features will be
// available throughout Kubernetes binaries.
var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{
CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
CustomResourceSubresources: {Default: false, PreRelease: utilfeature.Alpha},
}

View File

@ -3,20 +3,30 @@ package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"etcd.go",
"registry.go",
"status_strategy.go",
"strategy.go",
"validator.go",
],
importpath = "k8s.io/apiextensions-apiserver/pkg/registry/customresource",
deps = [
"//vendor/github.com/go-openapi/validate:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
@ -24,11 +34,14 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
],
)
@ -44,3 +57,27 @@ filegroup(
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_xtest",
srcs = ["etcd_test.go"],
deps = [
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic/testing:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/etcd/testing:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
],
)

View File

@ -17,20 +17,64 @@ limitations under the License.
package customresource
import (
"fmt"
"strings"
autoscalingv1 "k8s.io/api/autoscaling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
)
// CustomResourceStorage includes dummy storage for CustomResources, and their Status and Scale subresources.
type CustomResourceStorage struct {
CustomResource *REST
Status *StatusREST
Scale *ScaleREST
}
func NewStorage(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter) CustomResourceStorage {
customResourceREST, customResourceStatusREST := newREST(resource, listKind, strategy, optsGetter)
customResourceRegistry := NewRegistry(customResourceREST)
s := CustomResourceStorage{
CustomResource: customResourceREST,
}
if strategy.status != nil {
s.Status = customResourceStatusREST
}
if scale := strategy.scale; scale != nil {
var labelSelectorPath string
if scale.LabelSelectorPath != nil {
labelSelectorPath = *scale.LabelSelectorPath
}
s.Scale = &ScaleREST{
registry: customResourceRegistry,
specReplicasPath: scale.SpecReplicasPath,
statusReplicasPath: scale.StatusReplicasPath,
labelSelectorPath: labelSelectorPath,
}
}
return s
}
// REST implements a RESTStorage for API services against etcd
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against API services.
func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceDefinitionStorageStrategy, optsGetter generic.RESTOptionsGetter) *REST {
// newREST returns a RESTStorage object that will work against API services.
func newREST(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &unstructured.Unstructured{} },
NewListFunc: func() runtime.Object {
@ -50,5 +94,161 @@ func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, st
if err := store.CompleteWithOptions(options); err != nil {
panic(err) // TODO: Propagate error up
}
return &REST{store}
statusStore := *store
statusStore.UpdateStrategy = NewStatusStrategy(strategy)
return &REST{store}, &StatusREST{store: &statusStore}
}
// StatusREST implements the REST endpoint for changing the status of a CustomResource
type StatusREST struct {
store *genericregistry.Store
}
func (r *StatusREST) New() runtime.Object {
return &unstructured.Unstructured{}
}
// Get retrieves the object from the storage. It is required to support Patch.
func (r *StatusREST) Get(ctx genericapirequest.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return r.store.Get(ctx, name, options)
}
// Update alters the status subset of an object.
func (r *StatusREST) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) {
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation)
}
type ScaleREST struct {
registry Registry
specReplicasPath string
statusReplicasPath string
labelSelectorPath string
}
// ScaleREST implements Patcher
var _ = rest.Patcher(&ScaleREST{})
var _ = rest.GroupVersionKindProvider(&ScaleREST{})
func (r *ScaleREST) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind {
return autoscalingv1.SchemeGroupVersion.WithKind("Scale")
}
// New creates a new Scale object
func (r *ScaleREST) New() runtime.Object {
return &autoscalingv1.Scale{}
}
func (r *ScaleREST) Get(ctx genericapirequest.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
cr, err := r.registry.GetCustomResource(ctx, name, options)
if err != nil {
return nil, err
}
scaleObject, replicasFound, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, err
}
if !replicasFound {
return nil, apierrors.NewInternalError(fmt.Errorf("the spec replicas field %q does not exist", r.specReplicasPath))
}
return scaleObject, err
}
func (r *ScaleREST) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) {
cr, err := r.registry.GetCustomResource(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
const invalidSpecReplicas = -2147483648 // smallest int32
oldScale, replicasFound, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, false, err
}
if !replicasFound {
oldScale.Spec.Replicas = invalidSpecReplicas // signal that this was not set before
}
obj, err := objInfo.UpdatedObject(ctx, oldScale)
if err != nil {
return nil, false, err
}
if obj == nil {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("nil update passed to Scale"))
}
scale, ok := obj.(*autoscalingv1.Scale)
if !ok {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj))
}
if scale.Spec.Replicas == invalidSpecReplicas {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("the spec replicas field %q cannot be empty", r.specReplicasPath))
}
specReplicasPath := strings.TrimPrefix(r.specReplicasPath, ".") // ignore leading period
if err = unstructured.SetNestedField(cr.Object, int64(scale.Spec.Replicas), strings.Split(specReplicasPath, ".")...); err != nil {
return nil, false, err
}
cr.SetResourceVersion(scale.ResourceVersion)
cr, err = r.registry.UpdateCustomResource(ctx, cr, createValidation, updateValidation)
if err != nil {
return nil, false, err
}
newScale, _, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, false, apierrors.NewBadRequest(err.Error())
}
return newScale, false, err
}
// scaleFromCustomResource returns a scale subresource for a customresource and a bool signalling wether
// the specReplicas value was found.
func scaleFromCustomResource(cr *unstructured.Unstructured, specReplicasPath, statusReplicasPath, labelSelectorPath string) (*autoscalingv1.Scale, bool, error) {
specReplicasPath = strings.TrimPrefix(specReplicasPath, ".") // ignore leading period
specReplicas, foundSpecReplicas, err := unstructured.NestedInt64(cr.UnstructuredContent(), strings.Split(specReplicasPath, ".")...)
if err != nil {
return nil, false, err
} else if !foundSpecReplicas {
specReplicas = 0
}
statusReplicasPath = strings.TrimPrefix(statusReplicasPath, ".") // ignore leading period
statusReplicas, found, err := unstructured.NestedInt64(cr.UnstructuredContent(), strings.Split(statusReplicasPath, ".")...)
if err != nil {
return nil, false, err
} else if !found {
statusReplicas = 0
}
var labelSelector string
if len(labelSelectorPath) > 0 {
labelSelectorPath = strings.TrimPrefix(labelSelectorPath, ".") // ignore leading period
labelSelector, found, err = unstructured.NestedString(cr.UnstructuredContent(), strings.Split(labelSelectorPath, ".")...)
if err != nil {
return nil, false, err
}
}
scale := &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: cr.GetName(),
Namespace: cr.GetNamespace(),
UID: cr.GetUID(),
ResourceVersion: cr.GetResourceVersion(),
CreationTimestamp: cr.GetCreationTimestamp(),
},
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(specReplicas),
},
Status: autoscalingv1.ScaleStatus{
Replicas: int32(statusReplicas),
Selector: labelSelector,
},
}
return scale, foundSpecReplicas, nil
}

View File

@ -0,0 +1,380 @@
/*
Copyright 2018 The Kubernetes 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 customresource_test
import (
"io"
"strings"
"testing"
autoscalingv1 "k8s.io/api/autoscaling/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
registrytest "k8s.io/apiserver/pkg/registry/generic/testing"
"k8s.io/apiserver/pkg/registry/rest"
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
"k8s.io/client-go/discovery"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
)
func newStorage(t *testing.T) (customresource.CustomResourceStorage, *etcdtesting.EtcdTestServer) {
server, etcdStorage := etcdtesting.NewUnsecuredEtcd3TestClientServer(t)
etcdStorage.Codec = unstructuredJsonCodec{}
restOptions := generic.RESTOptions{StorageConfig: etcdStorage, Decorator: generic.UndecoratedStorage, DeleteCollectionWorkers: 1, ResourcePrefix: "noxus"}
parameterScheme := runtime.NewScheme()
parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: "mygroup.example.com", Version: "v1beta1"},
&metav1.ListOptions{},
&metav1.ExportOptions{},
&metav1.GetOptions{},
&metav1.DeleteOptions{},
)
typer := apiserver.UnstructuredObjectTyper{
Delegate: parameterScheme,
UnstructuredTyper: discovery.NewUnstructuredObjectTyper(nil),
}
kind := schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "Noxu"}
labelSelectorPath := ".status.labelSelector"
scale := &apiextensions.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: &labelSelectorPath,
}
status := &apiextensions.CustomResourceSubresourceStatus{}
storage := customresource.NewStorage(
schema.GroupResource{Group: "mygroup.example.com", Resource: "noxus"},
schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "NoxuItemList"},
customresource.NewStrategy(
typer,
true,
kind,
nil,
nil,
status,
scale,
),
restOptions,
)
return storage, server
}
// createCustomResource is a helper function that returns a CustomResource with the updated resource version.
func createCustomResource(storage *customresource.REST, cr unstructured.Unstructured, t *testing.T) (unstructured.Unstructured, error) {
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), cr.GetNamespace())
obj, err := storage.Create(ctx, &cr, rest.ValidateAllObjectFunc, false)
if err != nil {
t.Errorf("Failed to create CustomResource, %v", err)
}
newCR := obj.(*unstructured.Unstructured)
return *newCR, nil
}
func validNewCustomResource() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "Noxu",
"metadata": map[string]interface{}{
"namespace": "default",
"name": "foo",
},
"spec": map[string]interface{}{
"replicas": int64(7),
},
},
}
}
var validCustomResource = *validNewCustomResource()
func TestCreate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
test := registrytest.New(t, storage.CustomResource.Store)
cr := validNewCustomResource()
cr.SetNamespace("")
test.TestCreate(
cr,
)
}
func TestGet(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
test := registrytest.New(t, storage.CustomResource.Store)
test.TestGet(validNewCustomResource())
}
func TestList(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
test := registrytest.New(t, storage.CustomResource.Store)
test.TestList(validNewCustomResource())
}
func TestDelete(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
test := registrytest.New(t, storage.CustomResource.Store)
test.TestDelete(validNewCustomResource())
}
func TestStatusUpdate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/foo"
validCustomResource := validNewCustomResource()
if err := storage.CustomResource.Storage.Create(ctx, key, validCustomResource, nil, 0); err != nil {
t.Fatalf("unexpected error: %v", err)
}
gottenObj, err := storage.CustomResource.Get(ctx, "foo", &metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
update := gottenObj.(*unstructured.Unstructured)
updateContent := update.Object
updateContent["status"] = map[string]interface{}{
"replicas": int64(7),
}
if _, _, err := storage.Status.Update(ctx, update.GetName(), rest.DefaultUpdatedObjectInfo(update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc); err != nil {
t.Fatalf("unexpected error: %v", err)
}
obj, err := storage.CustomResource.Get(ctx, "foo", &metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cr, ok := obj.(*unstructured.Unstructured)
if !ok {
t.Fatal("unexpected error: custom resource should be of type Unstructured")
}
content := cr.UnstructuredContent()
spec := content["spec"].(map[string]interface{})
status := content["status"].(map[string]interface{})
if spec["replicas"].(int64) != 7 {
t.Errorf("we expected .spec.replicas to not be updated but it was updated to %v", spec["replicas"].(int64))
}
if status["replicas"].(int64) != 7 {
t.Errorf("we expected .status.replicas to be updated to %d but it was %v", 7, status["replicas"].(int64))
}
}
func TestScaleGet(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
name := "foo"
var cr unstructured.Unstructured
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0); err != nil {
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
}
want := &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: cr.GetName(),
Namespace: metav1.NamespaceDefault,
UID: cr.GetUID(),
ResourceVersion: cr.GetResourceVersion(),
CreationTimestamp: cr.GetCreationTimestamp(),
},
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(7),
},
}
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
got := obj.(*autoscalingv1.Scale)
if !apiequality.Semantic.DeepEqual(got, want) {
t.Errorf("unexpected scale: %s", diff.ObjectDiff(got, want))
}
}
func TestScaleGetWithoutSpecReplicas(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
name := "foo"
var cr unstructured.Unstructured
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
withoutSpecReplicas := validCustomResource.DeepCopy()
unstructured.RemoveNestedField(withoutSpecReplicas.Object, "spec", "replicas")
if err := storage.CustomResource.Storage.Create(ctx, key, withoutSpecReplicas, &cr, 0); err != nil {
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, withoutSpecReplicas, err)
}
_, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err == nil {
t.Fatalf("error expected for %s", name)
}
if expected := `the spec replicas field ".spec.replicas" does not exist`; !strings.Contains(err.Error(), expected) {
t.Fatalf("expected error string %q, got: %v", expected, err)
}
}
func TestScaleUpdate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
name := "foo"
var cr unstructured.Unstructured
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0); err != nil {
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
}
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
scale, ok := obj.(*autoscalingv1.Scale)
if !ok {
t.Fatalf("%v is not of the type autoscalingv1.Scale", scale)
}
replicas := 12
update := autoscalingv1.Scale{
ObjectMeta: scale.ObjectMeta,
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(replicas),
},
}
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc); err != nil {
t.Fatalf("error updating scale %v: %v", update, err)
}
obj, err = storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
scale = obj.(*autoscalingv1.Scale)
if scale.Spec.Replicas != int32(replicas) {
t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
}
update.ResourceVersion = scale.ResourceVersion
update.Spec.Replicas = 15
if _, _, err = storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc); err != nil && !errors.IsConflict(err) {
t.Fatalf("unexpected error, expecting an update conflict but got %v", err)
}
}
func TestScaleUpdateWithoutSpecReplicas(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
name := "foo"
var cr unstructured.Unstructured
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
withoutSpecReplicas := validCustomResource.DeepCopy()
unstructured.RemoveNestedField(withoutSpecReplicas.Object, "spec", "replicas")
if err := storage.CustomResource.Storage.Create(ctx, key, withoutSpecReplicas, &cr, 0); err != nil {
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, withoutSpecReplicas, err)
}
replicas := 12
update := autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: name,
ResourceVersion: cr.GetResourceVersion(),
},
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(replicas),
},
}
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc); err != nil {
t.Fatalf("error updating scale %v: %v", update, err)
}
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
scale := obj.(*autoscalingv1.Scale)
if scale.Spec.Replicas != int32(replicas) {
t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
}
}
type unstructuredJsonCodec struct{}
func (c unstructuredJsonCodec) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
obj := into.(*unstructured.Unstructured)
err := obj.UnmarshalJSON(data)
if err != nil {
return nil, nil, err
}
gvk := obj.GroupVersionKind()
return obj, &gvk, nil
}
func (c unstructuredJsonCodec) Encode(obj runtime.Object, w io.Writer) error {
u := obj.(*unstructured.Unstructured)
bs, err := u.MarshalJSON()
if err != nil {
return err
}
w.Write(bs)
return nil
}

View File

@ -0,0 +1,104 @@
/*
Copyright 2018 The Kubernetes 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 customresource
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
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/watch"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
)
// Registry is an interface for things that know how to store CustomResources.
type Registry interface {
ListCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*unstructured.UnstructuredList, error)
WatchCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error)
GetCustomResource(ctx genericapirequest.Context, customResourceID string, options *metav1.GetOptions) (*unstructured.Unstructured, error)
CreateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc) (*unstructured.Unstructured, error)
UpdateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (*unstructured.Unstructured, error)
DeleteCustomResource(ctx genericapirequest.Context, customResourceID string) error
}
// storage puts strong typing around storage calls
type storage struct {
rest.StandardStorage
}
// NewRegistry returns a new Registry interface for the given Storage. Any mismatched
// types will panic.
func NewRegistry(s rest.StandardStorage) Registry {
return &storage{s}
}
func (s *storage) ListCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*unstructured.UnstructuredList, error) {
if options != nil && options.FieldSelector != nil && !options.FieldSelector.Empty() {
return nil, fmt.Errorf("field selector not supported yet")
}
obj, err := s.List(ctx, options)
if err != nil {
return nil, err
}
return obj.(*unstructured.UnstructuredList), err
}
func (s *storage) WatchCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
return s.Watch(ctx, options)
}
func (s *storage) GetCustomResource(ctx genericapirequest.Context, customResourceID string, options *metav1.GetOptions) (*unstructured.Unstructured, error) {
obj, err := s.Get(ctx, customResourceID, options)
customResource, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("custom resource must be of type Unstructured")
}
if err != nil {
apiVersion := customResource.GetAPIVersion()
groupVersion := strings.Split(apiVersion, "/")
group := groupVersion[0]
return nil, errors.NewNotFound(schema.GroupResource{Group: group, Resource: "scale"}, customResourceID)
}
return customResource, nil
}
func (s *storage) CreateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc) (*unstructured.Unstructured, error) {
obj, err := s.Create(ctx, customResource, rest.ValidateAllObjectFunc, false)
if err != nil {
return nil, err
}
return obj.(*unstructured.Unstructured), nil
}
func (s *storage) UpdateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (*unstructured.Unstructured, error) {
obj, _, err := s.Update(ctx, customResource.GetName(), rest.DefaultUpdatedObjectInfo(customResource), createValidation, updateValidation)
if err != nil {
return nil, err
}
return obj.(*unstructured.Unstructured), nil
}
func (s *storage) DeleteCustomResource(ctx genericapirequest.Context, customResourceID string) error {
_, _, err := s.Delete(ctx, customResourceID, nil)
return err
}

View File

@ -0,0 +1,62 @@
/*
Copyright 2018 The Kubernetes 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 customresource
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
)
type statusStrategy struct {
customResourceStrategy
}
func NewStatusStrategy(strategy customResourceStrategy) statusStrategy {
return statusStrategy{strategy}
}
func (a statusStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
newCustomResourceObject := obj.(*unstructured.Unstructured)
oldCustomResourceObject := old.(*unstructured.Unstructured)
newCustomResource := newCustomResourceObject.UnstructuredContent()
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
// update is not allowed to set spec and metadata
_, ok1 := newCustomResource["spec"]
_, ok2 := oldCustomResource["spec"]
switch {
case ok2:
newCustomResource["spec"] = oldCustomResource["spec"]
case ok1:
delete(newCustomResource, "spec")
}
newCustomResourceObject.SetAnnotations(oldCustomResourceObject.GetAnnotations())
newCustomResourceObject.SetFinalizers(oldCustomResourceObject.GetFinalizers())
newCustomResourceObject.SetGeneration(oldCustomResourceObject.GetGeneration())
newCustomResourceObject.SetLabels(oldCustomResourceObject.GetLabels())
newCustomResourceObject.SetOwnerReferences(oldCustomResourceObject.GetOwnerReferences())
newCustomResourceObject.SetSelfLink(oldCustomResourceObject.GetSelfLink())
}
// ValidateUpdate is the default update validation for an end user updating status.
func (a statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return a.customResourceStrategy.validator.ValidateStatusUpdate(ctx, obj, old, a.scale)
}

View File

@ -17,12 +17,10 @@ limitations under the License.
package customresource
import (
"fmt"
"github.com/go-openapi/validate"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
@ -31,63 +29,129 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage"
apiserverstorage "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
)
type customResourceDefinitionStorageStrategy struct {
// customResourceStrategy implements behavior for CustomResources.
type customResourceStrategy struct {
runtime.ObjectTyper
names.NameGenerator
namespaceScoped bool
validator customResourceValidator
status *apiextensions.CustomResourceSubresourceStatus
scale *apiextensions.CustomResourceSubresourceScale
}
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, validator *validate.SchemaValidator) customResourceDefinitionStorageStrategy {
return customResourceDefinitionStorageStrategy{
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator *validate.SchemaValidator, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
return customResourceStrategy{
ObjectTyper: typer,
NameGenerator: names.SimpleNameGenerator,
namespaceScoped: namespaceScoped,
status: status,
scale: scale,
validator: customResourceValidator{
namespaceScoped: namespaceScoped,
kind: kind,
validator: validator,
namespaceScoped: namespaceScoped,
kind: kind,
schemaValidator: schemaValidator,
statusSchemaValidator: statusSchemaValidator,
},
}
}
func (a customResourceDefinitionStorageStrategy) NamespaceScoped() bool {
func (a customResourceStrategy) NamespaceScoped() bool {
return a.namespaceScoped
}
func (customResourceDefinitionStorageStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
// PrepareForCreate clears the status of a CustomResource before creation.
func (a customResourceStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && a.status != nil {
customResourceObject := obj.(*unstructured.Unstructured)
customResource := customResourceObject.UnstructuredContent()
// create cannot set status
if _, ok := customResource["status"]; ok {
delete(customResource, "status")
}
}
accessor, _ := meta.Accessor(obj)
accessor.SetGeneration(1)
}
func (customResourceDefinitionStorageStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (a customResourceStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) || a.status == nil {
return
}
newCustomResourceObject := obj.(*unstructured.Unstructured)
oldCustomResourceObject := old.(*unstructured.Unstructured)
newCustomResource := newCustomResourceObject.UnstructuredContent()
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
// update is not allowed to set status
_, ok1 := newCustomResource["status"]
_, ok2 := oldCustomResource["status"]
switch {
case ok2:
newCustomResource["status"] = oldCustomResource["status"]
case ok1:
delete(newCustomResource, "status")
}
// Any changes to the spec increment the generation number, any changes to the
// status should reflect the generation number of the corresponding object. We push
// the burden of managing the status onto the clients because we can't (in general)
// know here what version of spec the writer of the status has seen. It may seem like
// we can at first -- since obj contains spec -- but in the future we will probably make
// status its own object, and even if we don't, writes may be the result of a
// read-update-write loop, so the contents of spec may not actually be the spec that
// the CustomResource has *seen*.
newSpec, ok1 := newCustomResource["spec"]
oldSpec, ok2 := oldCustomResource["spec"]
// spec is changed, created or deleted
if (ok1 && ok2 && !apiequality.Semantic.DeepEqual(oldSpec, newSpec)) || (ok1 && !ok2) || (!ok1 && ok2) {
oldAccessor, _ := meta.Accessor(oldCustomResourceObject)
newAccessor, _ := meta.Accessor(newCustomResourceObject)
newAccessor.SetGeneration(oldAccessor.GetGeneration() + 1)
}
}
func (a customResourceDefinitionStorageStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
return a.validator.Validate(ctx, obj)
// Validate validates a new CustomResource.
func (a customResourceStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
return a.validator.Validate(ctx, obj, a.scale)
}
func (customResourceDefinitionStorageStrategy) AllowCreateOnUpdate() bool {
// Canonicalize normalizes the object after validation.
func (customResourceStrategy) Canonicalize(obj runtime.Object) {
}
// AllowCreateOnUpdate is false for CustomResources; this means a POST is
// needed to create one.
func (customResourceStrategy) AllowCreateOnUpdate() bool {
return false
}
func (customResourceDefinitionStorageStrategy) AllowUnconditionalUpdate() bool {
// AllowUnconditionalUpdate is the default update policy for CustomResource objects.
func (customResourceStrategy) AllowUnconditionalUpdate() bool {
return false
}
func (customResourceDefinitionStorageStrategy) Canonicalize(obj runtime.Object) {
// ValidateUpdate is the default update validation for an end user updating status.
func (a customResourceStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return a.validator.ValidateUpdate(ctx, obj, old, a.scale)
}
func (a customResourceDefinitionStorageStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return a.validator.ValidateUpdate(ctx, obj, old)
}
func (a customResourceDefinitionStorageStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
// GetAttrs returns labels and fields of a given object for filtering purposes.
func (a customResourceStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return nil, nil, false, err
@ -108,80 +172,13 @@ func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields.
}
}
func (a customResourceDefinitionStorageStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
return storage.SelectionPredicate{
// MatchCustomResourceDefinitionStorage is the filter used by the generic etcd backend to route
// watch events from etcd to clients of the apiserver only interested in specific
// labels/fields.
func (a customResourceStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) apiserverstorage.SelectionPredicate {
return apiserverstorage.SelectionPredicate{
Label: label,
Field: field,
GetAttrs: a.GetAttrs,
}
}
type customResourceValidator struct {
namespaceScoped bool
kind schema.GroupVersionKind
validator *validate.SchemaValidator
}
func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
accessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
}
func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
objAccessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
oldAccessor, err := meta.Accessor(old)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
}

View File

@ -0,0 +1,241 @@
/*
Copyright 2018 The Kubernetes 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 customresource
import (
"fmt"
"math"
"strings"
"github.com/go-openapi/validate"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
)
type customResourceValidator struct {
namespaceScoped bool
kind schema.GroupVersionKind
schemaValidator *validate.SchemaValidator
statusSchemaValidator *validate.SchemaValidator
}
func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
accessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.schemaValidator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
if scale != nil {
// validate specReplicas
specReplicasPath := strings.TrimPrefix(scale.SpecReplicasPath, ".") // ignore leading period
specReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(specReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, err.Error())}
}
if specReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, "should be a non-negative integer")}
}
if specReplicas > math.MaxInt32 {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
}
// validate statusReplicas
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
}
if statusReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
}
if statusReplicas > math.MaxInt32 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
}
// validate labelSelector
if scale.LabelSelectorPath != nil {
labelSelectorPath := strings.TrimPrefix(*scale.LabelSelectorPath, ".") // ignore leading period
labelSelector, _, err := unstructured.NestedString(customResource, strings.Split(labelSelectorPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
}
}
}
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
}
func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
objAccessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
oldAccessor, err := meta.Accessor(old)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.schemaValidator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
if scale != nil {
// validate specReplicas
specReplicasPath := strings.TrimPrefix(scale.SpecReplicasPath, ".") // ignore leading period
specReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(specReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, err.Error())}
}
if specReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, "should be a non-negative integer")}
}
if specReplicas > math.MaxInt32 {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
}
// validate statusReplicas
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
}
if statusReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
}
if statusReplicas > math.MaxInt32 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
}
// validate labelSelector
if scale.LabelSelectorPath != nil {
labelSelectorPath := strings.TrimPrefix(*scale.LabelSelectorPath, ".") // ignore leading period
labelSelector, _, err := unstructured.NestedString(customResource, strings.Split(labelSelectorPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
}
}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
}
func (a customResourceValidator) ValidateStatusUpdate(ctx genericapirequest.Context, obj, old runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
objAccessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
oldAccessor, err := meta.Accessor(old)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
// validate only the status
customResourceStatus := customResource["status"]
if err = apiservervalidation.ValidateCustomResource(customResourceStatus, a.statusSchemaValidator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath("status"), customResourceStatus, err.Error())}
}
if scale != nil {
// validate statusReplicas
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
}
if statusReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
}
if statusReplicas > math.MaxInt32 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
}
// validate labelSelector
if scale.LabelSelectorPath != nil {
labelSelectorPath := strings.TrimPrefix(*scale.LabelSelectorPath, ".") // ignore leading period
labelSelector, _, err := unstructured.NestedString(customResource, strings.Split(labelSelectorPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
}
}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
}

View File

@ -35,6 +35,7 @@ import (
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
)
// strategy implements behavior for CustomResources.
type strategy struct {
runtime.ObjectTyper
names.NameGenerator
@ -48,6 +49,7 @@ func (strategy) NamespaceScoped() bool {
return false
}
// PrepareForCreate clears the status of a CustomResourceDefinition before creation.
func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
crd := obj.(*apiextensions.CustomResourceDefinition)
crd.Status = apiextensions.CustomResourceDefinitionStatus{}
@ -57,8 +59,12 @@ func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Obje
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
crd.Spec.Validation = nil
}
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
crd.Spec.Subresources = nil
}
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
newCRD := obj.(*apiextensions.CustomResourceDefinition)
oldCRD := old.(*apiextensions.CustomResourceDefinition)
@ -80,23 +86,33 @@ func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime
newCRD.Spec.Validation = nil
oldCRD.Spec.Validation = nil
}
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
newCRD.Spec.Subresources = nil
oldCRD.Spec.Subresources = nil
}
}
// Validate validates a new CustomResourceDefinition.
func (strategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition))
}
// AllowCreateOnUpdate is false for CustomResourceDefinition; this means a POST is
// needed to create one.
func (strategy) AllowCreateOnUpdate() bool {
return false
}
// AllowUnconditionalUpdate is the default update policy for CustomResourceDefinition objects.
func (strategy) AllowUnconditionalUpdate() bool {
return false
}
// Canonicalize normalizes the object after validation.
func (strategy) Canonicalize(obj runtime.Object) {
}
// ValidateUpdate is the default update validation for an end user updating status.
func (strategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
}
@ -143,10 +159,11 @@ func (statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old run
return validation.ValidateUpdateCustomResourceDefinitionStatus(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
}
// GetAttrs returns labels and fields of a given object for filtering purposes.
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
apiserver, ok := obj.(*apiextensions.CustomResourceDefinition)
if !ok {
return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition.")
return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition")
}
return labels.Set(apiserver.ObjectMeta.Labels), CustomResourceDefinitionToSelectableFields(apiserver), apiserver.Initializers != nil, nil
}

View File

@ -11,6 +11,7 @@ go_test(
"basic_test.go",
"finalization_test.go",
"registration_test.go",
"subresources_test.go",
"validation_test.go",
"yaml_test.go",
],
@ -19,17 +20,23 @@ go_test(
"//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/test/integration/testserver:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
],
)

View File

@ -35,7 +35,7 @@ import (
)
func TestServerUp(t *testing.T) {
stopCh, _, _, err := testserver.StartDefaultServer()
stopCh, _, _, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -43,7 +43,7 @@ func TestServerUp(t *testing.T) {
}
func TestNamespaceScopedCRUD(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -61,7 +61,7 @@ func TestNamespaceScopedCRUD(t *testing.T) {
}
func TestClusterScopedCRUD(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -347,7 +347,7 @@ func TestDiscovery(t *testing.T) {
group := "mygroup.example.com"
version := "v1beta1"
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -391,7 +391,7 @@ func TestDiscovery(t *testing.T) {
}
func TestNoNamespaceReject(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -430,7 +430,7 @@ func TestNoNamespaceReject(t *testing.T) {
}
func TestSameNameDiffNamespace(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -450,7 +450,7 @@ func TestSameNameDiffNamespace(t *testing.T) {
}
func TestSelfLink(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -503,7 +503,7 @@ func TestSelfLink(t *testing.T) {
}
func TestPreserveInt(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -548,7 +548,7 @@ func TestPreserveInt(t *testing.T) {
}
func TestPatch(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -622,7 +622,7 @@ func TestPatch(t *testing.T) {
}
func TestCrossNamespaceListWatch(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -758,7 +758,7 @@ func checkNamespacesWatchHelper(t *testing.T, ns string, namespacedwatch watch.I
}
func TestNameConflict(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}

View File

@ -28,7 +28,7 @@ import (
)
func TestFinalization(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
require.NoError(t, err)
defer close(stopCh)

View File

@ -30,6 +30,7 @@ import (
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
@ -73,8 +74,22 @@ func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, defi
}, ns)
}
func NewNamespacedCustomResourceStatusClient(ns string, client dynamic.Interface, definition *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
return client.Resource(&metav1.APIResource{
Name: definition.Spec.Names.Plural + "/status",
Namespaced: definition.Spec.Scope == apiextensionsv1beta1.NamespaceScoped,
}, ns)
}
func NewNamespacedCustomResourceScaleClient(ns string, client dynamic.Interface, definition *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
return client.Resource(&metav1.APIResource{
Name: definition.Spec.Names.Plural + "/scale",
Namespaced: definition.Spec.Scope == apiextensionsv1beta1.NamespaceScoped,
}, ns)
}
func TestMultipleResourceInstances(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -198,7 +213,7 @@ func TestMultipleResourceInstances(t *testing.T) {
}
func TestMultipleRegistration(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -254,7 +269,7 @@ func TestMultipleRegistration(t *testing.T) {
}
func TestDeRegistrationAndReRegistration(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -347,12 +362,18 @@ func TestEtcdStorage(t *testing.T) {
if err != nil {
t.Fatal(err)
}
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config)
stopCh, clientConfig, err := testserver.StartServer(config)
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := apiextensionsclientset.NewForConfig(clientConfig)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(clientConfig)
etcdPrefix := getPrefixFromConfig(t, config)
ns1 := "another-default-is-possible"

View File

@ -0,0 +1,787 @@
/*
Copyright 2018 The Kubernetes 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 integration
import (
"math"
"reflect"
"sort"
"strings"
"testing"
"time"
autoscaling "k8s.io/api/autoscaling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
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/types"
"k8s.io/apimachinery/pkg/util/wait"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
"k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
)
var labelSelectorPath = ".status.labelSelector"
func NewNoxuSubresourcesCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "mygroup.example.com",
Version: "v1beta1",
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "noxus",
Singular: "nonenglishnoxu",
Kind: "WishIHadChosenNoxu",
ShortNames: []string{"foo", "bar", "abc", "def"},
ListKind: "NoxuItemList",
},
Scope: scope,
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: &labelSelectorPath,
},
},
},
}
}
func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
"spec": map[string]interface{}{
"num": int64(10),
"replicas": int64(3),
},
"status": map[string]interface{}{
"replicas": int64(7),
},
},
}
}
func TestStatusSubresource(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// status should not be set after creation
if val, ok := gottenNoxuInstance.Object["status"]; ok {
t.Fatalf("status should not be set after creation, got %v", val)
}
// .status.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// UpdateStatus should not update spec.
// Check that .spec.num = 10 and .status.num = 20
updatedStatusInstance, err := noxuStatusResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != int64(10) {
t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum)
}
statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != int64(20) {
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
}
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// .status.num = 40
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num = 40
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Update should not update status.
// Check that .spec.num = 40 and .status.num = 20
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update instance: %v", err)
}
specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != int64(40) {
t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum)
}
statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != int64(20) {
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
}
}
func TestScaleSubresource(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
groupResource := schema.GroupResource{
Group: "mygroup.example.com",
Resource: "noxus",
}
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
// set invalid json path for specReplicasPath
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = "foo,bar"
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err == nil {
t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec")
}
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = ".spec.replicas"
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
scaleClient, err := testserver.CreateNewScaleClient(noxuDefinition, config)
if err != nil {
t.Fatal(err)
}
// set .status.labelSelector = bar
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", "status", "labelSelector")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = noxuStatusResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
// get the scale object
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
if err != nil {
t.Fatal(err)
}
if gottenScale.Spec.Replicas != 3 {
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas)
}
if gottenScale.Status.Selector != "bar" {
t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector)
}
// check self link
expectedSelfLink := "/apis/mygroup.example.com/v1beta1/namespaces/not-the-default/noxus/foo/scale"
if gottenScale.GetSelfLink() != expectedSelfLink {
t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink())
}
// update the scale object
// check that spec is updated, but status is not
gottenScale.Spec.Replicas = 5
gottenScale.Status.Selector = "baz"
updatedScale, err := scaleClient.Scales("not-the-default").Update(groupResource, gottenScale)
if err != nil {
t.Fatal(err)
}
if updatedScale.Spec.Replicas != 5 {
t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas)
}
if updatedScale.Status.Selector != "bar" {
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector)
}
// check that .spec.replicas = 5, but status is not updated
updatedNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas")
if !found || err != nil {
t.Fatalf("unable to get .spec.replicas")
}
if specReplicas != 5 {
t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas)
}
statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, "status", "labelSelector")
if !found || err != nil {
t.Fatalf("unable to get .status.labelSelector")
}
if statusLabelSelector != "bar" {
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector)
}
// validate maximum value
// set .spec.replicas = math.MaxInt64
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = noxuResourceClient.Update(gottenNoxuInstance)
if err == nil {
t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647")
}
}
func TestValidationSchema(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
// fields other than properties in root schema are not allowed
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err == nil {
t.Fatalf("unexpected non-error: if subresources for custom resources are enabled, only properties can be used at the root of the schema")
}
// make sure we are not restricting fields to properties even in subschemas
noxuDefinition.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"spec": {
Description: "Validation for spec",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"replicas": {
Type: "integer",
},
},
},
},
}
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatalf("unable to created crd %v: %v", noxuDefinition.Name, err)
}
}
func TestValidateOnlyStatus(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
// UpdateStatus should validate only status
// 1. create a crd with max value of .spec.num = 10 and .status.num = 10
// 2. create a cr with .spec.num = 10 and .status.num = 10 (valid)
// 3. update the crd so that max value of .spec.num = 5 and .status.num = 10
// 4. update the status of the cr with .status.num = 5 (spec is invalid)
// validation passes becauses spec is not validated
// max value of spec.num = 10 and status.num = 10
schema := &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"spec": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(10),
},
},
},
"status": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(10),
},
},
},
},
}
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: schema,
}
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
// set .spec.num = 10 and .status.num = 10
noxuInstance := NewNoxuSubresourceInstance(ns, "foo")
err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
createdNoxuInstance, err := instantiateCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
gottenCRD, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get("noxus.mygroup.example.com", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// update the crd so that max value of spec.num = 5 and status.num = 10
gottenCRD.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"spec": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(5),
},
},
},
"status": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(10),
},
},
},
},
}
if _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(gottenCRD); err != nil {
t.Fatal(err)
}
// update the status with .status.num = 5
err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(5), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// cr is updated even though spec is invalid
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
_, err := noxuStatusResourceClient.Update(createdNoxuInstance)
if statusError, isStatus := err.(*apierrors.StatusError); isStatus {
if strings.Contains(statusError.Error(), "is invalid") {
return false, nil
}
}
if err != nil {
return false, err
}
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
func TestSubresourcesDiscovery(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
group := "mygroup.example.com"
version := "v1beta1"
resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version)
if err != nil {
t.Fatal(err)
}
if len(resources.APIResources) != 3 {
t.Fatalf("Expected exactly the resources \"noxus\", \"noxus/status\" and \"noxus/scale\" in group version %v/%v via discovery, got: %v", group, version, resources.APIResources)
}
// check discovery info for status
status := resources.APIResources[1]
if status.Name != "noxus/status" {
t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name)
}
if status.Namespaced != true {
t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced)
}
if status.Kind != "WishIHadChosenNoxu" {
t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind)
}
expectedVerbs := []string{"get", "patch", "update"}
sort.Strings(status.Verbs)
if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) {
t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs)
}
// check discovery info for scale
scale := resources.APIResources[2]
if scale.Group != autoscaling.GroupName {
t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group)
}
if scale.Version != "v1" {
t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version)
}
if scale.Name != "noxus/scale" {
t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name)
}
if scale.Namespaced != true {
t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced)
}
if scale.Kind != "Scale" {
t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind)
}
sort.Strings(scale.Verbs)
if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) {
t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs)
}
}
func TestGeneration(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
// .metadata.generation = 1
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if gottenNoxuInstance.GetGeneration() != 1 {
t.Fatalf(".metadata.generation should be 1 after creation")
}
// .status.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// UpdateStatus does not increment generation
updatedStatusInstance, err := noxuStatusResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
if updatedStatusInstance.GetGeneration() != 1 {
t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration())
}
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// .spec.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Update increments generation
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update instance: %v", err)
}
if updatedInstance.GetGeneration() != 2 {
t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration())
}
}
func TestSubresourcePatch(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
groupResource := schema.GroupResource{
Group: "mygroup.example.com",
Resource: "noxus",
}
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
noxuScaleResourceClient := NewNamespacedCustomResourceScaleClient(ns, noxuVersionClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
scaleClient, err := testserver.CreateNewScaleClient(noxuDefinition, config)
if err != nil {
t.Fatal(err)
}
patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`)
patchedNoxuInstance, err := noxuStatusResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num should remain 10
specNum, found, err := unstructured.NestedInt64(patchedNoxuInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != 10 {
t.Fatalf(".spec.num: expected: %v, got: %v", 10, specNum)
}
// .status.num should be 999
statusNum, found, err := unstructured.NestedInt64(patchedNoxuInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != 999 {
t.Fatalf(".status.num: expected: %v, got: %v", 999, statusNum)
}
// this call waits for the resourceVersion to be reached in the cache before returning.
// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
// and then the updated object shows a conflicting diff, which permanently fails the patch.
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
// See https://issue.k8s.io/42644
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// no-op patch
_, err = noxuStatusResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// empty patch
_, err = noxuStatusResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`)
patchedNoxuInstance, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// this call waits for the resourceVersion to be reached in the cache before returning.
// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
// and then the updated object shows a conflicting diff, which permanently fails the patch.
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
// See https://issue.k8s.io/42644
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 7
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
if err != nil {
t.Fatal(err)
}
if gottenScale.Spec.Replicas != 7 {
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas)
}
if gottenScale.Status.Replicas != 0 {
t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas)
}
// no-op patch
_, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// empty patch
_, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// make sure strategic merge patch is not supported for both status and scale
_, err = noxuStatusResourceClient.Patch("foo", types.StrategicMergePatchType, patch)
if err == nil {
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
}
_, err = noxuScaleResourceClient.Patch("foo", types.StrategicMergePatchType, patch)
if err == nil {
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
}
}

View File

@ -28,7 +28,10 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/options:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/scale:go_default_library",
],
)

View File

@ -30,7 +30,10 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/scale"
)
const (
@ -293,3 +296,34 @@ func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefi
func GetCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1beta1.CustomResourceDefinition, error) {
return apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
}
func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, config *rest.Config) (scale.ScalesGetter, error) {
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
groupResource, err := discoveryClient.ServerResourcesForGroupVersion(crd.Spec.Group + "/" + crd.Spec.Version)
if err != nil {
return nil, err
}
resources := []*discovery.APIGroupResources{
{
Group: metav1.APIGroup{
Name: crd.Spec.Group,
Versions: []metav1.GroupVersionForDiscovery{
{Version: crd.Spec.Version},
},
PreferredVersion: metav1.GroupVersionForDiscovery{Version: crd.Spec.Version},
},
VersionedResources: map[string][]metav1.APIResource{
crd.Spec.Version: groupResource.APIResources,
},
},
}
restMapper := discovery.NewRESTMapper(resources, nil)
resolver := scale.NewDiscoveryScaleKindResolver(discoveryClient)
return scale.NewForConfig(config, restMapper, dynamic.LegacyAPIPathResolverFunc, resolver)
}

View File

@ -32,6 +32,7 @@ import (
genericapiserver "k8s.io/apiserver/pkg/server"
genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
)
func DefaultServerConfig() (*extensionsapiserver.Config, error) {
@ -87,11 +88,11 @@ func DefaultServerConfig() (*extensionsapiserver.Config, error) {
return config, nil
}
func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
func StartServer(config *extensionsapiserver.Config) (chan struct{}, *rest.Config, error) {
stopCh := make(chan struct{})
server, err := config.Complete().New(genericapiserver.EmptyDelegate)
if err != nil {
return nil, nil, nil, err
return nil, nil, err
}
go func() {
err := server.GenericAPIServer.PrepareRun().Run(stopCh)
@ -123,26 +124,32 @@ func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.I
})
if err != nil {
close(stopCh)
return nil, nil, err
}
return stopCh, config.GenericConfig.LoopbackClientConfig, nil
}
func StartDefaultServer() (chan struct{}, *rest.Config, error) {
config, err := DefaultServerConfig()
if err != nil {
return nil, nil, err
}
return StartServer(config)
}
func StartDefaultServerWithClients() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
stopCh, config, err := StartDefaultServer()
if err != nil {
return nil, nil, nil, err
}
apiExtensionsClient, err := clientset.NewForConfig(server.GenericAPIServer.LoopbackClientConfig)
apiExtensionsClient, err := clientset.NewForConfig(config)
if err != nil {
close(stopCh)
return nil, nil, nil, err
}
bytes, _ := apiExtensionsClient.Discovery().RESTClient().Get().AbsPath("/apis/apiextensions.k8s.io/v1beta1").DoRaw()
fmt.Print(string(bytes))
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(server.GenericAPIServer.LoopbackClientConfig), nil
}
func StartDefaultServer() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
config, err := DefaultServerConfig()
if err != nil {
return nil, nil, nil, err
}
return StartServer(config)
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(config), nil
}

View File

@ -31,7 +31,7 @@ import (
)
func TestForProperValidationErrors(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -169,7 +169,7 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure
}
func TestCustomResourceValidation(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -190,7 +190,7 @@ func TestCustomResourceValidation(t *testing.T) {
}
func TestCustomResourceUpdateValidation(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -233,7 +233,7 @@ func TestCustomResourceUpdateValidation(t *testing.T) {
}
func TestCustomResourceValidationErrors(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -324,7 +324,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
}
func TestCRValidationOnCRDUpdate(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
@ -378,7 +378,7 @@ func TestCRValidationOnCRDUpdate(t *testing.T) {
}
func TestForbiddenFieldsInSchema(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}

View File

@ -24,25 +24,32 @@ import (
"github.com/ghodss/yaml"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
"k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
)
func TestYAML(t *testing.T) {
config, err := testserver.DefaultServerConfig()
if err != nil {
t.Fatal(err)
}
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config)
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
@ -232,7 +239,7 @@ values:
Param("watch", "true").
DoRaw()
if !errors.IsNotAcceptable(err) {
t.Fatal("expected not acceptable error, got %v (%s)", err, string(result))
t.Fatalf("expected not acceptable error, got %v (%s)", err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
@ -294,7 +301,7 @@ values:
t.Fatal(v, ok, err, string(result))
}
if obj.GetUID() != uid {
t.Fatal("uid changed: %v vs %v", uid, obj.GetUID())
t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
}
}
@ -346,6 +353,179 @@ values:
}
}
func TestYAMLSubresource(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.ClusterScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
rest := apiExtensionClient.Discovery().RESTClient()
uid := types.UID("")
resourceVersion := ""
// Create
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
spec:
replicas: 3`, apiVersion, kind))
result, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
uid = obj.GetUID()
resourceVersion = obj.GetResourceVersion()
}
// Get at /status
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// Update at /status
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
uid: %s
resourceVersion: "%s"
spec:
replicas: 5
status:
replicas: 3`, apiVersion, kind, uid, resourceVersion))
result, err := rest.Put().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if obj.GetUID() != uid {
t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
}
}
// Get at /scale
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "scale").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != "autoscaling/v1" {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != "Scale" {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
}
func decodeYAML(data []byte) (*unstructured.Unstructured, error) {
retval := &unstructured.Unstructured{Object: map[string]interface{}{}}
// ensure this isn't JSON

View File

@ -280,6 +280,7 @@ func (rc *ResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error
Watch()
}
// Patch applies the patch and returns the patched resource.
func (rc *ResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) {
result := new(unstructured.Unstructured)
resourceName, subresourceName := rc.parseResourceSubresourceName()

View File

@ -99,7 +99,7 @@ func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) {
restMapperRes, err := discovery.GetAPIGroupResources(fakeDiscoveryClient)
if err != nil {
t.Fatalf("unexpected error while constructing resource list from fake discovery client: %v")
t.Fatalf("unexpected error while constructing resource list from fake discovery client: %v", err)
}
restMapper := discovery.NewRESTMapper(restMapperRes, apimeta.InterfacesForUnstructured)

View File

@ -1 +0,0 @@
eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.lisG21ATunZSCBP6vaqK_AZCIR18tN563RdqkAb6PGOipqTeg8R7VwZQIeDqS-2Vond6NX_KSC_D_uxxv0hBf2DiGXmwMUmP4nRXrsmbzT2qQKKIHYRDC_6jb2-FSfK14ezIe1Q07UiiJecDsN3CFEccS8E68Tdnp78p7yDwbTvpumnZmwYfyhlImtjFQv2YpyFVsjEHWK0R4e9T3ONQWcx6D2rSoxABbutrS03QwsJhHCeD9joL_gxfkFKm3CW8yWPSk2QYtx_Q1hu-tZR4IPb2tQPXPX3mtyhwBqziWgmJRDFCEjlCO5aCobiMm_9K5X05gue_DcgW163zh1P9jg.nleER2An8CUn_OuR.b77RFEFp0gC8j5yCAoARNKYmQIvWq99ibmf5ffJgdhQBF3sRYJLt_XflJ_2lsaiFOxvc45T2fnkMVy2lHFcri7F9f1BRiT_0AcDthxsecGzG8BZ9QvaM6b4Dn0rhjrOq8rsF0m3ZnbPBkkg3LV5EkbHWstMo2fgJPJhJswlGWhqJPJBDecG1nMBC8SMH32X-zVlSM-BLiaghvOGNxyb_RLZJZ3CLczIdQ2JO2UeYkOGCPGzernvkHDMpqQXc-8cmulDdHgCy87qFLy5ttGFgYbnTm92h_ChOGKZixeX0PL0pQY5wXd2xTO7Tg_Ov5E5FoVwIkwOextedVsF9iz_b_mwtCY3LXrvbJTW7zWrwBVsVyAXxT5iu0HyQ3tBVxT2GxS-yM5ApqLozcZCQg9flMyfSgThu82FfzEr0fI5vKw8zo0GdO4GBuVSppM9m6ToG6hlwyHD9g2YTZw9068hyq1_kZQhugJRjgGbpa2gyGqzx16fg0zVoupVIiq5KfvRlAQFeOVVjQwb0BWf25tJUj5tV3O9ge6dbKSXizEca33FJJwJWoXhd7DCREXUU9pBz06NCCf495BGoVbq3oLPDQc2mpcuy0XAPxSwXcc5Ts8DNs7MrxBlYdw81wMXuztIpOY4.XjKlMWl_H40XszToi2VU5g

View File

@ -1,40 +0,0 @@
clone:
path: github.com/go-openapi/validate
matrix:
GO_VERSION:
- "1.6"
build:
integration:
image: golang:$$GO_VERSION
pull: true
commands:
- go get -u github.com/axw/gocov/gocov
- go get -u gopkg.in/matm/v1/gocov-html
- go get -u github.com/cee-dub/go-junit-report
- go get -u github.com/stretchr/testify/assert
- go get -u gopkg.in/yaml.v2
- go get -u github.com/go-openapi/analysis
- go get -u github.com/go-openapi/errors
- go get -u github.com/go-openapi/loads
- go get -u github.com/go-openapi/strfmt
- go get -u github.com/go-openapi/runtime
- go test -race
- go test -v -cover -coverprofile=coverage.out -covermode=count
notify:
slack:
channel: bots
webhook_url: $$SLACK_URL
username: drone
publish:
coverage:
server: https://coverage.vmware.run
token: $$GITHUB_TOKEN
# threshold: 70
# must_increase: true
when:
matrix:
GO_VERSION: "1.6"

View File

@ -1,13 +0,0 @@
approve_by_comment: true
approve_regex: '^(:shipit:|:\+1:|\+1|LGTM|lgtm|Approved)'
reject_regex: ^[Rr]ejected
reset_on_push: false
reviewers:
members:
- casualjim
- chancez
- frapposelli
- vburenin
- pytlesk4
name: pullapprove
required: 1

22
vendor/github.com/go-openapi/validate/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,22 @@
language: go
go:
- 1.7
install:
- go get -u github.com/axw/gocov/gocov
- go get -u gopkg.in/matm/v1/gocov-html
- go get -u github.com/cee-dub/go-junit-report
- go get -u github.com/stretchr/testify/assert
- go get -u github.com/kr/pretty
- go get -u gopkg.in/yaml.v2
- go get -u github.com/go-openapi/analysis
- go get -u github.com/go-openapi/errors
- go get -u github.com/go-openapi/loads
- go get -u github.com/go-openapi/strfmt
- go get -u github.com/go-openapi/runtime
script:
- go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
slack:
secure: EmObnQuM9Mw8J9vpFaKKHqSMN4Wsr/A9+v7ewAD5cEhA0T1P4m7MbJMiJOhxUhj/X+BFh2DamW+P2lT8mybj5wg8wnkQ2BteKA8Tawi6f9PRw2NRheO8tAi8o/npLnlmet0kc93mn+oLuqHw36w4+j5mkOl2FghkfGiUVhwrhkCP7KXQN+3TU87e+/HzQumlJ3nsE+6terVxkH3PmaUTsS5ONaODZfuxFpfb7RsoEl3skHf6d+tr+1nViLxxly7558Nc33C+W1mr0qiEvMLZ+kJ/CpGWBJ6CUJM3jm6hNe2eMuIPwEK2hxZob8c7n22VPap4K6a0bBRoydoDXaba+2sD7Ym6ivDO/DVyL44VeBBLyIiIBylDGQdZH+6SoWm90Qe/i7tnY/T5Ao5igT8f3cfQY1c3EsTfqmlDfrhmACBmwSlgkdVBLTprHL63JMY24LWmh4jhxsmMRZhCL4dze8su1w6pLN/pD1pGHtKYCEVbdTmaM3PblNRFf12XB7qosmQsgUndH4Vq3bTbU0s1pKjeDhRyLvFzvR0TBbo0pDLEoF1A/i5GVFWa7yLZNUDudQERRh7qv/xBl2excIaQ1sV4DSVm7bAE9l6Kp+yeHQJW2uN6Y3X8wu9gB9nv9l5HBze7wh8KE6PyWAOLYYqZg9/sAtsv/2GcQqXcKFF1zcA=

View File

@ -1,3 +1,3 @@
# Validation helpers [![Build Status](https://ci.vmware.run/api/badges/go-openapi/validate/status.svg)](https://ci.vmware.run/go-openapi/validate) [![Coverage](https://coverage.vmware.run/badges/go-openapi/validate/coverage.svg)](https://coverage.vmware.run/go-openapi/validate) [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io)
# Validation helpers [![Build Status](https://travis-ci.org/go-openapi/validate.svg?branch=master)](https://travis-ci.org/go-openapi/validate) [![codecov](https://codecov.io/gh/go-openapi/validate/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/validate) [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io)
[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/validate/master/LICENSE) [![GoDoc](https://godoc.org/github.com/go-openapi/validate?status.svg)](http://godoc.org/github.com/go-openapi/validate)

View File

@ -15,6 +15,7 @@
package validate
import (
"log"
"reflect"
"github.com/go-openapi/spec"
@ -51,7 +52,9 @@ func (f *formatValidator) Applies(source interface{}, kind reflect.Kind) bool {
return false
}
r := doit()
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", f.Path, r, source, kind)
if Debug {
log.Printf("format validator for %q applies %t for %T (kind: %v)\n", f.Path, r, source, kind)
}
return r
}

View File

@ -15,8 +15,10 @@
package validate
import (
"log"
"reflect"
"regexp"
"strings"
"github.com/go-openapi/errors"
"github.com/go-openapi/spec"
@ -45,10 +47,45 @@ func (o *objectValidator) Applies(source interface{}, kind reflect.Kind) bool {
// there is a problem in the type validator where it will be unhappy about null values
// so that requires more testing
r := reflect.TypeOf(source) == specSchemaType && (kind == reflect.Map || kind == reflect.Struct)
//fmt.Printf("object validator for %q applies %t for %T (kind: %v)\n", o.Path, r, source, kind)
if Debug {
log.Printf("object validator for %q applies %t for %T (kind: %v)\n", o.Path, r, source, kind)
}
return r
}
func (o *objectValidator) isPropertyName() bool {
p := strings.Split(o.Path, ".")
return p[len(p)-1] == "properties" && p[len(p)-2] != "properties"
}
func (o *objectValidator) checkArrayMustHaveItems(res *Result, val map[string]interface{}) {
if t, typeFound := val["type"]; typeFound {
if tpe, ok := t.(string); ok && tpe == "array" {
if _, itemsKeyFound := val["items"]; !itemsKeyFound {
res.AddErrors(errors.Required("items", o.Path))
}
}
}
}
func (o *objectValidator) checkItemsMustBeTypeArray(res *Result, val map[string]interface{}) {
if !o.isPropertyName() {
if _, itemsKeyFound := val["items"]; itemsKeyFound {
t, typeFound := val["type"]
if typeFound {
if tpe, ok := t.(string); !ok || tpe != "array" {
res.AddErrors(errors.InvalidType(o.Path, o.In, "array", nil))
}
} else {
// there is no type
res.AddErrors(errors.Required("type", o.Path))
}
}
}
}
func (o *objectValidator) precheck(res *Result, val map[string]interface{}) {
o.checkArrayMustHaveItems(res, val)
o.checkItemsMustBeTypeArray(res, val)
}
func (o *objectValidator) Validate(data interface{}) *Result {
val := data.(map[string]interface{})
numKeys := int64(len(val))
@ -61,14 +98,8 @@ func (o *objectValidator) Validate(data interface{}) *Result {
}
res := new(Result)
if len(o.Required) > 0 {
for _, k := range o.Required {
if _, ok := val[k]; !ok {
res.AddErrors(errors.Required(o.Path+"."+k, o.In))
continue
}
}
}
o.precheck(res, val)
if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows {
for k := range val {
@ -99,6 +130,8 @@ func (o *objectValidator) Validate(data interface{}) *Result {
}
}
createdFromDefaults := map[string]bool{}
for pName, pSchema := range o.Properties {
rName := pName
if o.Path != "" {
@ -106,7 +139,24 @@ func (o *objectValidator) Validate(data interface{}) *Result {
}
if v, ok := val[pName]; ok {
res.Merge(NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats).Validate(v))
r := NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats).Validate(v)
res.Merge(r)
} else if pSchema.Default != nil {
createdFromDefaults[pName] = true
pName := pName // shaddow
def := pSchema.Default
res.Defaulters = append(res.Defaulters, DefaulterFunc(func() {
val[pName] = def
}))
}
}
if len(o.Required) > 0 {
for _, k := range o.Required {
if _, ok := val[k]; !ok && !createdFromDefaults[k] {
res.AddErrors(errors.Required(o.Path+"."+k, o.In))
continue
}
}
}
@ -137,9 +187,6 @@ func (o *objectValidator) validatePatternProperty(key string, value interface{},
res := validator.Validate(value)
result.Merge(res)
if res.IsValid() {
succeededOnce = true
}
}
}

View File

@ -14,12 +14,32 @@
package validate
import "github.com/go-openapi/errors"
import (
"os"
"github.com/go-openapi/errors"
)
var (
// Debug is true when the SWAGGER_DEBUG env var is not empty
Debug = os.Getenv("SWAGGER_DEBUG") != ""
)
type Defaulter interface {
Apply()
}
type DefaulterFunc func()
func (f DefaulterFunc) Apply() {
f()
}
// Result represents a validation result
type Result struct {
Errors []error
MatchCount int
Defaulters []Defaulter
}
// Merge merges this result with the other one, preserving match counts etc
@ -29,11 +49,13 @@ func (r *Result) Merge(other *Result) *Result {
}
r.AddErrors(other.Errors...)
r.MatchCount += other.MatchCount
r.Defaulters = append(r.Defaulters, other.Defaulters...)
return r
}
// AddErrors adds errors to this validation result
func (r *Result) AddErrors(errors ...error) {
// TODO: filter already existing errors
r.Errors = append(r.Errors, errors...)
}
@ -59,3 +81,9 @@ func (r *Result) AsError() error {
}
return errors.CompositeValidationError(r.Errors...)
}
func (r *Result) ApplyDefaults() {
for _, d := range r.Defaulters {
d.Apply()
}
}

View File

@ -15,6 +15,8 @@
package validate
import (
"encoding/json"
"log"
"reflect"
"github.com/go-openapi/spec"
@ -80,12 +82,16 @@ func (s *SchemaValidator) Applies(source interface{}, kind reflect.Kind) bool {
// Validate validates the data against the schema
func (s *SchemaValidator) Validate(data interface{}) *Result {
result := new(Result)
if s == nil {
return result
}
if data == nil {
v := s.validators[0].Validate(data)
v.Merge(s.validators[6].Validate(data))
return v
}
result := new(Result)
tpe := reflect.TypeOf(data)
kind := tpe.Kind()
@ -98,8 +104,35 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
d = swag.ToDynamicJSON(data)
}
isnumber := s.Schema.Type.Contains("number") || s.Schema.Type.Contains("integer")
if num, ok := data.(json.Number); ok && isnumber {
if s.Schema.Type.Contains("integer") { // avoid lossy conversion
in, erri := num.Int64()
if erri != nil {
result.AddErrors(erri)
result.Inc()
return result
}
d = in
} else {
nf, errf := num.Float64()
if errf != nil {
result.AddErrors(errf)
result.Inc()
return result
}
d = nf
}
tpe = reflect.TypeOf(d)
kind = tpe.Kind()
}
for _, v := range s.validators {
if !v.Applies(s.Schema, kind) {
if Debug {
log.Printf("%T does not apply for %v", v, kind)
}
continue
}
@ -117,10 +150,9 @@ func (s *SchemaValidator) typeValidator() valueValidator {
func (s *SchemaValidator) commonValidator() valueValidator {
return &basicCommonValidator{
Path: s.Path,
In: s.in,
Default: s.Schema.Default,
Enum: s.Schema.Enum,
Path: s.Path,
In: s.in,
Enum: s.Schema.Enum,
}
}
@ -155,7 +187,6 @@ func (s *SchemaValidator) stringValidator() valueValidator {
return &stringValidator{
Path: s.Path,
In: s.in,
Default: s.Schema.Default,
MaxLength: s.Schema.MaxLength,
MinLength: s.Schema.MinLength,
Pattern: s.Schema.Pattern,
@ -164,9 +195,8 @@ func (s *SchemaValidator) stringValidator() valueValidator {
func (s *SchemaValidator) formatValidator() valueValidator {
return &formatValidator{
Path: s.Path,
In: s.in,
//Default: s.Schema.Default,
Path: s.Path,
In: s.in,
Format: s.Schema.Format,
KnownFormats: s.KnownFormats,
}

View File

@ -15,6 +15,7 @@
package validate
import (
"log"
"reflect"
"github.com/go-openapi/errors"
@ -80,12 +81,15 @@ func newSchemaPropsValidator(path string, in string, allOf, oneOf, anyOf []spec.
func (s *schemaPropsValidator) Applies(source interface{}, kind reflect.Kind) bool {
r := reflect.TypeOf(source) == specSchemaType
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
}
return r
}
func (s *schemaPropsValidator) Validate(data interface{}) *Result {
mainResult := new(Result)
var firstSuccess *Result
if len(s.anyOfValidators) > 0 {
var bestFailures *Result
succeededOnce := false
@ -94,6 +98,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if result.IsValid() {
bestFailures = nil
succeededOnce = true
if firstSuccess == nil {
firstSuccess = result
}
break
}
if bestFailures == nil || result.MatchCount > bestFailures.MatchCount {
@ -106,11 +113,14 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
}
if bestFailures != nil {
mainResult.Merge(bestFailures)
} else if firstSuccess != nil {
mainResult.Merge(firstSuccess)
}
}
if len(s.oneOfValidators) > 0 {
var bestFailures *Result
var firstSuccess *Result
validated := 0
for _, oneOfSchema := range s.oneOfValidators {
@ -118,6 +128,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if result.IsValid() {
validated++
bestFailures = nil
if firstSuccess == nil {
firstSuccess = result
}
continue
}
if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) {
@ -130,6 +143,8 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if bestFailures != nil {
mainResult.Merge(bestFailures)
}
} else if firstSuccess != nil {
mainResult.Merge(firstSuccess)
}
}

View File

@ -67,6 +67,9 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
itemsSize = int64(len(s.Items.Schemas))
for i := int64(0); i < itemsSize; i++ {
validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats)
if val.Len() <= int(i) {
break
}
result.Merge(validator.Validate(val.Index(int(i)).Interface()))
}

View File

@ -19,6 +19,7 @@ import (
"fmt"
"log"
"regexp"
"strconv"
"strings"
"github.com/go-openapi/analysis"
@ -193,6 +194,13 @@ func (s *SpecValidator) validateDuplicatePropertyNames() *Result {
return res
}
func (s *SpecValidator) resolveRef(ref *spec.Ref) (*spec.Schema, error) {
if s.spec.SpecFilePath() != "" {
return spec.ResolveRefWithBase(s.spec.Spec(), ref, &spec.ExpandOptions{RelativeBase: s.spec.SpecFilePath()})
}
return spec.ResolveRef(s.spec.Spec(), ref)
}
func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, knowns map[string]struct{}) []dupProp {
var dups []dupProp
@ -200,7 +208,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema,
schc := &sch
for schc.Ref.String() != "" {
// gather property names
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref)
reso, err := s.resolveRef(&schc.Ref)
if err != nil {
panic(err)
}
@ -236,7 +244,7 @@ func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, kno
schn := nm
schc := &sch
for schc.Ref.String() != "" {
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref)
reso, err := s.resolveRef(&schc.Ref)
if err != nil {
panic(err)
}
@ -335,15 +343,15 @@ func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID str
return errors.New(422, "%s for %q is a collection without an element type", prefix, opID)
}
schemas := schema.Items.Schemas
if schema.Items.Schema != nil {
schemas = []spec.Schema{*schema.Items.Schema}
}
for _, sch := range schemas {
if err := s.validateSchemaItems(sch, prefix, opID); err != nil {
return err
schema = *schema.Items.Schema
if _, err := regexp.Compile(schema.Pattern); err != nil {
return errors.New(422, "%s for %q has invalid items pattern: %q", prefix, opID, schema.Pattern)
}
return s.validateSchemaItems(schema, prefix, opID)
}
return nil
}
@ -524,8 +532,15 @@ func (s *SpecValidator) validateParameters() *Result {
}
var fromPath []string
for _, i := range params {
fromPath = append(fromPath, knowns[i])
knowns[i] = "!"
knownsi := knowns[i]
iparams := extractPathParams(knownsi)
if len(iparams) > 0 {
fromPath = append(fromPath, iparams...)
for _, iparam := range iparams {
knownsi = strings.Replace(knownsi, iparam, "!", 1)
}
knowns[i] = knownsi
}
}
knownPath := strings.Join(knowns, "/")
if orig, ok := knownPaths[knownPath]; ok {
@ -544,7 +559,9 @@ func (s *SpecValidator) validateParameters() *Result {
for pr.Ref.String() != "" {
obj, _, err := pr.Ref.GetPointer().Get(sw)
if err != nil {
log.Println(err)
if Debug {
log.Println(err)
}
res.AddErrors(err)
break PARAMETERS
}
@ -575,6 +592,10 @@ func (s *SpecValidator) validateParameters() *Result {
pr = obj.(spec.Parameter)
}
if _, err := regexp.Compile(pr.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in param %q: %q", op.ID, pr.Name, pr.Pattern))
}
if pr.In == "body" {
if firstBodyParam != "" {
res.AddErrors(errors.New(422, "operation %q has more than 1 body param (accepted: %q, dropped: %q)", op.ID, firstBodyParam, pr.Name))
@ -595,18 +616,35 @@ func (s *SpecValidator) validateParameters() *Result {
func parsePath(path string) (segments []string, params []int) {
for i, p := range strings.Split(path, "/") {
segments = append(segments, p)
if len(p) > 0 && p[0] == '{' && p[len(p)-1] == '}' {
if d0 := strings.Index(p, "{"); d0 >= 0 && d0 < strings.Index(p, "}") {
params = append(params, i)
}
}
return
}
func extractPathParams(segment string) (params []string) {
for {
d0 := strings.IndexByte(segment, '{')
if d0 < 0 {
break
}
d1 := strings.IndexByte(segment[d0:], '}')
if d1 > 0 {
params = append(params, segment[d0:d0+d1+1])
} else {
break
}
segment = segment[d1:]
}
return params
}
func (s *SpecValidator) validateReferencesValid() *Result {
// each reference must point to a valid object
res := new(Result)
for _, r := range s.analyzer.AllRefs() {
if !r.IsValidURI() {
if !r.IsValidURI(s.spec.SpecFilePath()) {
res.AddErrors(errors.New(404, "invalid ref %q", r.String()))
}
}
@ -698,7 +736,9 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
}
// check simple paramters first
if param.Default != nil && param.Schema == nil {
//fmt.Println(param.Name, "in", param.In, "has a default without a schema")
if Debug {
log.Println(param.Name, "in", param.In, "has a default without a schema")
}
// check param valid
res.Merge(NewParamValidator(&param, s.KnownFormats).Validate(param.Default))
}
@ -721,9 +761,15 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
if h.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items))
}
if _, err := regexp.Compile(h.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in default header %q: %q", op.ID, nm, h.Pattern))
}
}
if dr.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema("default", "response", dr.Schema))
}
}
for _, r := range op.Responses.StatusCodeResponses {
for code, r := range op.Responses.StatusCodeResponses {
for nm, h := range r.Headers {
if h.Default != nil {
res.Merge(NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default))
@ -731,6 +777,12 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
if h.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items))
}
if _, err := regexp.Compile(h.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in %v's header %q: %q", op.ID, code, nm, h.Pattern))
}
}
if r.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(strconv.Itoa(code), "response", r.Schema))
}
}
@ -758,6 +810,9 @@ func (s *SpecValidator) validateDefaultValueSchemaAgainstSchema(path, in string,
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.items[%d]", path, i), in, &sch))
}
}
if _, err := regexp.Compile(schema.Pattern); err != nil {
res.AddErrors(errors.New(422, "%s in %s has invalid pattern: %q", path, in, schema.Pattern))
}
if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema))
}
@ -787,6 +842,9 @@ func (s *SpecValidator) validateDefaultValueItemsAgainstSchema(path, in string,
if items.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(path+"[0]", in, root, items.Items))
}
if _, err := regexp.Compile(items.Pattern); err != nil {
res.AddErrors(errors.New(422, "%s in %s has invalid pattern: %q", path, in, items.Pattern))
}
}
return res
}

View File

@ -15,6 +15,7 @@
package validate
import (
"log"
"reflect"
"strings"
@ -32,16 +33,6 @@ type typeValidator struct {
Path string
}
var jsonTypeNames = map[string]struct{}{
"array": struct{}{},
"boolean": struct{}{},
"integer": struct{}{},
"null": struct{}{},
"number": struct{}{},
"object": struct{}{},
"string": struct{}{},
}
func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) {
switch data.(type) {
case []byte:
@ -121,7 +112,9 @@ func (t *typeValidator) SetPath(path string) {
func (t *typeValidator) Applies(source interface{}, kind reflect.Kind) bool {
stpe := reflect.TypeOf(source)
r := (len(t.Type) > 0 || t.Format != "") && (stpe == specSchemaType || stpe == specParameterType || stpe == specHeaderType)
//fmt.Printf("type validator for %q applies %t for %T (kind: %v)\n", t.Path, r, source, kind)
if Debug {
log.Printf("type validator for %q applies %t for %T (kind: %v)\n", t.Path, r, source, kind)
}
return r
}
@ -140,7 +133,9 @@ func (t *typeValidator) Validate(data interface{}) *Result {
kind := val.Kind()
schType, format := t.schemaInfoForType(data)
//fmt.Println("path:", t.Path, "schType:", schType, "format:", format, "expType:", t.Type, "expFmt:", t.Format, "kind:", val.Kind().String())
if Debug {
log.Println("path:", t.Path, "schType:", schType, "format:", format, "expType:", t.Type, "expFmt:", t.Format, "kind:", val.Kind().String())
}
isLowerInt := t.Format == "int64" && format == "int32"
isLowerFloat := t.Format == "float64" && format == "float32"
isFloatInt := schType == "number" && swag.IsFloat64AJSONInteger(val.Float()) && t.Type.Contains("integer")

15
vendor/github.com/go-openapi/validate/update-fixtures.sh generated vendored Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
set -eu -o pipefail
dir=$(git rev-parse --show-toplevel)
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
function finish {
rm -rf "$scratch"
}
trap finish EXIT SIGHUP SIGINT SIGTERM
cd "$scratch"
git clone https://github.com/json-schema-org/JSON-Schema-Test-Suite Suite
cp -r Suite/tests/draft4/* "$dir/fixtures/jsonschema_suite"
cp -a Suite/remotes "$dir/fixtures/jsonschema_suite"

View File

@ -16,6 +16,7 @@ package validate
import (
"fmt"
"log"
"reflect"
"github.com/go-openapi/errors"
@ -156,8 +157,15 @@ func (b *basicCommonValidator) Applies(source interface{}, kind reflect.Kind) bo
func (b *basicCommonValidator) Validate(data interface{}) (res *Result) {
if len(b.Enum) > 0 {
for _, enumValue := range b.Enum {
if data != nil && reflect.DeepEqual(enumValue, data) {
return nil
actualType := reflect.TypeOf(enumValue)
if actualType == nil {
continue
}
expectedValue := reflect.ValueOf(data)
if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
if reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), enumValue) {
return nil
}
}
}
return sErr(errors.EnumFail(b.Path, b.In, data, b.Enum))
@ -474,10 +482,14 @@ func (n *numberValidator) Applies(source interface{}, kind reflect.Kind) bool {
isInt := kind >= reflect.Int && kind <= reflect.Uint64
isFloat := kind == reflect.Float32 || kind == reflect.Float64
r := isInt || isFloat
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, r, source, kind)
if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, r, source, kind)
}
return r
}
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, false, source, kind)
if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, false, source, kind)
}
return false
}
@ -536,15 +548,22 @@ func (s *stringValidator) Applies(source interface{}, kind reflect.Kind) bool {
switch source.(type) {
case *spec.Parameter, *spec.Schema, *spec.Items, *spec.Header:
r := kind == reflect.String
// fmt.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
if Debug {
log.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
}
return r
}
// fmt.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, false, source, kind)
if Debug {
log.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, false, source, kind)
}
return false
}
func (s *stringValidator) Validate(val interface{}) *Result {
data := val.(string)
data, ok := val.(string)
if !ok {
return sErr(errors.InvalidType(s.Path, s.In, "string", val))
}
if s.Required && !s.AllowEmptyValue && (s.Default == nil || s.Default == "") {
if err := RequiredString(s.Path, s.In, data); err != nil {

View File

@ -196,7 +196,11 @@ func MinimumUint(path, in string, data, min uint64, exclusive bool) *errors.Vali
// MultipleOf validates if the provided number is a multiple of the factor
func MultipleOf(path, in string, data, factor float64) *errors.Validation {
if !swag.IsFloat64AJSONInteger(data / factor) {
mult := data / factor
if factor < 1 {
mult = 1 / factor * data
}
if !swag.IsFloat64AJSONInteger(mult) {
return errors.NotMultipleOf(path, in, factor)
}
return nil