Skip to content

Commit

Permalink
add subresources for custom resources
Browse files Browse the repository at this point in the history
  • Loading branch information
nikhita committed Feb 11, 2018
1 parent c61e25e commit c961ac4
Show file tree
Hide file tree
Showing 28 changed files with 2,428 additions and 215 deletions.
2 changes: 1 addition & 1 deletion cmd/kube-controller-manager/app/autoscaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion hack/.golint_failures
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkg/features/kube_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,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},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -146,3 +150,38 @@ 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.
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.
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.
LabelSelectorPath string
// ScaleGroupVersion denotes the GroupVersion in the form "group/version"
// of the Scale object sent as the payload for /scale.
// It allows transition to future versions easily.
// Today only autoscaling/v1 is allowed.
ScaleGroupVersion string
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -147,3 +154,39 @@ 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.
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.
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.
// +optional
LabelSelectorPath string `json:"labelSelectorPath,omitempty" protobuf:"bytes,3,opt,name=labelSelectorPath"`
// ScaleGroupVersion denotes the GroupVersion in the form "group/version"
// of the Scale object sent as the payload for /scale.
// It allows transition to future versions easily.
// Today only autoscaling/v1 is allowed.
ScaleGroupVersion string `json:"scaleGroupVersion" protobuf:"bytes,4,name=scaleGroupVersion"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package validation

import (
"fmt"
"reflect"
"strings"

genericvalidation "k8s.io/apimachinery/pkg/api/validation"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -326,3 +351,42 @@ 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.Invalid(fldPath.Child("scale.specReplicasPath"), subResources.Scale.SpecReplicasPath, "specReplicasPath cannot be empty"))
}

// should be constrained json path
specReplicasPath := strings.TrimPrefix(subResources.Scale.SpecReplicasPath, ".")
splitSpecReplicasPath := strings.Split(specReplicasPath, ".")
if len(splitSpecReplicasPath) <= 1 || splitSpecReplicasPath[0] != "spec" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.specReplicasPath"), subResources.Scale.SpecReplicasPath, "specReplicasPath should be a json path under .spec"))
}

if len(subResources.Scale.StatusReplicasPath) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.statusReplicasPath"), subResources.Scale.StatusReplicasPath, "statusReplicasPath cannot be empty"))
}

// should be constrained json path
statusReplicasPath := strings.TrimPrefix(subResources.Scale.StatusReplicasPath, ".")
splitStatusReplicasPath := strings.Split(statusReplicasPath, ".")
if len(splitStatusReplicasPath) <= 1 || splitStatusReplicasPath[0] != "status" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.statusReplicasPath"), subResources.Scale.StatusReplicasPath, "statusReplicasPath should be a json path under .status"))
}

if subResources.Scale.ScaleGroupVersion != "autoscaling/v1" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.scaleGroupVersion"), subResources.Scale.ScaleGroupVersion, "scaleGroupVersion must be autoscaling/v1"))
}
}

return allErrs
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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, // TODO: use crd.Spec.SubResources.Scale.ScaleGroupVersion
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 {
Expand Down

0 comments on commit c961ac4

Please sign in to comment.