Unverified Commit 7155b8a3 authored by k8s-ci-robot's avatar k8s-ci-robot Committed by GitHub

Merge pull request #70211 from roycaihw/crd-multi-version-x

CRD supports multi-version Schema, Subresources and AdditionalPrintColumns
parents 6f305d92 5b545bbc
...@@ -91983,7 +91983,7 @@ ...@@ -91983,7 +91983,7 @@
], ],
"properties": { "properties": {
"additionalPrinterColumns": { "additionalPrinterColumns": {
"description": "AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.", "description": "AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column. Optional, the global columns for all versions. Top-level and per-version columns are mutually exclusive.",
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceColumnDefinition" "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceColumnDefinition"
...@@ -92006,11 +92006,11 @@ ...@@ -92006,11 +92006,11 @@
"type": "string" "type": "string"
}, },
"subresources": { "subresources": {
"description": "Subresources describes the subresources for CustomResources", "description": "Subresources describes the subresources for CustomResource Optional, the global subresources for all versions. Top-level and per-version subresources are mutually exclusive.",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources" "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources"
}, },
"validation": { "validation": {
"description": "Validation describes the validation methods for CustomResources", "description": "Validation describes the validation methods for CustomResources Optional, the global validation schema for all versions. Top-level and per-version schemas are mutually exclusive.",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation" "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation"
}, },
"version": { "version": {
...@@ -92062,10 +92062,21 @@ ...@@ -92062,10 +92062,21 @@
"storage" "storage"
], ],
"properties": { "properties": {
"additionalPrinterColumns": {
"description": "AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column. Top-level and per-version columns are mutually exclusive. Per-version columns must not all be set to identical values (top-level columns should be used instead) This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature. NOTE: CRDs created prior to 1.13 populated the top-level additionalPrinterColumns field by default. To apply an update that changes to per-version additionalPrinterColumns, the top-level additionalPrinterColumns field must be explicitly set to null",
"type": "array",
"items": {
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceColumnDefinition"
}
},
"name": { "name": {
"description": "Name is the version name, e.g. “v1”, “v2beta1”, etc.", "description": "Name is the version name, e.g. “v1”, “v2beta1”, etc.",
"type": "string" "type": "string"
}, },
"schema": {
"description": "Schema describes the schema for CustomResource used in validation, pruning, and defaulting. Top-level and per-version schemas are mutually exclusive. Per-version schemas must not all be set to identical values (top-level validation schema should be used instead) This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation"
},
"served": { "served": {
"description": "Served is a flag enabling/disabling this version from being served via REST APIs", "description": "Served is a flag enabling/disabling this version from being served via REST APIs",
"type": "boolean" "type": "boolean"
...@@ -92073,6 +92084,10 @@ ...@@ -92073,6 +92084,10 @@
"storage": { "storage": {
"description": "Storage flags the version as storage version. There must be exactly one flagged as storage version.", "description": "Storage flags the version as storage version. There must be exactly one flagged as storage version.",
"type": "boolean" "type": "boolean"
},
"subresources": {
"description": "Subresources describes the subresources for CustomResource Top-level and per-version subresources are mutually exclusive. Per-version subresources must not all be set to identical values (top-level subresources should be used instead) This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources"
} }
} }
}, },
...@@ -2443,6 +2443,10 @@ ...@@ -2443,6 +2443,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apiserver/pkg/util/feature/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/util/logs", "ImportPath": "k8s.io/apiserver/pkg/util/logs",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
...@@ -44,8 +44,14 @@ type CustomResourceDefinitionSpec struct { ...@@ -44,8 +44,14 @@ type CustomResourceDefinitionSpec struct {
// Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced // Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced
Scope ResourceScope Scope ResourceScope
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
// Optional, the global validation schema for all versions.
// Top-level and per-version schemas are mutually exclusive.
// +optional
Validation *CustomResourceValidation Validation *CustomResourceValidation
// Subresources describes the subresources for CustomResources // Subresources describes the subresources for CustomResource
// Optional, the global subresources for all versions.
// Top-level and per-version subresources are mutually exclusive.
// +optional
Subresources *CustomResourceSubresources Subresources *CustomResourceSubresources
// Versions is the list of all supported versions for this resource. // Versions is the list of all supported versions for this resource.
// If Version field is provided, this field is optional. // If Version field is provided, this field is optional.
...@@ -60,6 +66,9 @@ type CustomResourceDefinitionSpec struct { ...@@ -60,6 +66,9 @@ type CustomResourceDefinitionSpec struct {
// v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10. // v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.
Versions []CustomResourceDefinitionVersion Versions []CustomResourceDefinitionVersion
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column. // AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
// Optional, the global columns for all versions.
// Top-level and per-version columns are mutually exclusive.
// +optional
AdditionalPrinterColumns []CustomResourceColumnDefinition AdditionalPrinterColumns []CustomResourceColumnDefinition
// `conversion` defines conversion settings for the CRD. // `conversion` defines conversion settings for the CRD.
...@@ -149,6 +158,27 @@ type CustomResourceDefinitionVersion struct { ...@@ -149,6 +158,27 @@ type CustomResourceDefinitionVersion struct {
// Storage flags the version as storage version. There must be exactly one flagged // Storage flags the version as storage version. There must be exactly one flagged
// as storage version. // as storage version.
Storage bool Storage bool
// Schema describes the schema for CustomResource used in validation, pruning, and defaulting.
// Top-level and per-version schemas are mutually exclusive.
// Per-version schemas must not all be set to identical values (top-level validation schema should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
Schema *CustomResourceValidation
// Subresources describes the subresources for CustomResource
// Top-level and per-version subresources are mutually exclusive.
// Per-version subresources must not all be set to identical values (top-level subresources should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
Subresources *CustomResourceSubresources
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
// Top-level and per-version columns are mutually exclusive.
// Per-version columns must not all be set to identical values (top-level columns should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// NOTE: CRDs created prior to 1.13 populated the top-level additionalPrinterColumns field by default. To apply an
// update that changes to per-version additionalPrinterColumns, the top-level additionalPrinterColumns field must
// be explicitly set to null
// +optional
AdditionalPrinterColumns []CustomResourceColumnDefinition
} }
// CustomResourceColumnDefinition specifies a column for server side printing. // CustomResourceColumnDefinition specifies a column for server side printing.
......
...@@ -19,12 +19,9 @@ package v1beta1 ...@@ -19,12 +19,9 @@ package v1beta1
import ( import (
"strings" "strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()
func addDefaultingFuncs(scheme *runtime.Scheme) error { func addDefaultingFuncs(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&CustomResourceDefinition{}, func(obj interface{}) { SetDefaults_CustomResourceDefinition(obj.(*CustomResourceDefinition)) }) scheme.AddTypeDefaultingFunc(&CustomResourceDefinition{}, func(obj interface{}) { SetDefaults_CustomResourceDefinition(obj.(*CustomResourceDefinition)) })
// TODO figure out why I can't seem to get my defaulter generated // TODO figure out why I can't seem to get my defaulter generated
...@@ -66,14 +63,19 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec) ...@@ -66,14 +63,19 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec)
if len(obj.Version) == 0 && len(obj.Versions) != 0 { if len(obj.Version) == 0 && len(obj.Versions) != 0 {
obj.Version = obj.Versions[0].Name obj.Version = obj.Versions[0].Name
} }
if len(obj.AdditionalPrinterColumns) == 0 {
obj.AdditionalPrinterColumns = []CustomResourceColumnDefinition{
{Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"},
}
}
if obj.Conversion == nil { if obj.Conversion == nil {
obj.Conversion = &CustomResourceConversion{ obj.Conversion = &CustomResourceConversion{
Strategy: NoneConverter, Strategy: NoneConverter,
} }
} }
} }
// hasPerVersionColumns returns true if a CRD uses per-version columns.
func hasPerVersionColumns(versions []CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if len(v.AdditionalPrinterColumns) > 0 {
return true
}
}
return false
}
...@@ -785,6 +785,38 @@ func (m *CustomResourceDefinitionVersion) MarshalTo(dAtA []byte) (int, error) { ...@@ -785,6 +785,38 @@ func (m *CustomResourceDefinitionVersion) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0 dAtA[i] = 0
} }
i++ i++
if m.Schema != nil {
dAtA[i] = 0x22
i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size()))
n15, err := m.Schema.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n15
}
if m.Subresources != nil {
dAtA[i] = 0x2a
i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Subresources.Size()))
n16, err := m.Subresources.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n16
}
if len(m.AdditionalPrinterColumns) > 0 {
for _, msg := range m.AdditionalPrinterColumns {
dAtA[i] = 0x32
i++
i = encodeVarintGenerated(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n
}
}
return i, nil return i, nil
} }
...@@ -857,21 +889,21 @@ func (m *CustomResourceSubresources) MarshalTo(dAtA []byte) (int, error) { ...@@ -857,21 +889,21 @@ func (m *CustomResourceSubresources) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0xa dAtA[i] = 0xa
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Status.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Status.Size()))
n15, err := m.Status.MarshalTo(dAtA[i:]) n17, err := m.Status.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n15 i += n17
} }
if m.Scale != nil { if m.Scale != nil {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Scale.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Scale.Size()))
n16, err := m.Scale.MarshalTo(dAtA[i:]) n18, err := m.Scale.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n16 i += n18
} }
return i, nil return i, nil
} }
...@@ -895,11 +927,11 @@ func (m *CustomResourceValidation) MarshalTo(dAtA []byte) (int, error) { ...@@ -895,11 +927,11 @@ func (m *CustomResourceValidation) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0xa dAtA[i] = 0xa
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.OpenAPIV3Schema.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.OpenAPIV3Schema.Size()))
n17, err := m.OpenAPIV3Schema.MarshalTo(dAtA[i:]) n19, err := m.OpenAPIV3Schema.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n17 i += n19
} }
return i, nil return i, nil
} }
...@@ -1003,11 +1035,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1003,11 +1035,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x42 dAtA[i] = 0x42
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Default.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Default.Size()))
n18, err := m.Default.MarshalTo(dAtA[i:]) n20, err := m.Default.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n18 i += n20
} }
if m.Maximum != nil { if m.Maximum != nil {
dAtA[i] = 0x49 dAtA[i] = 0x49
...@@ -1134,11 +1166,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1134,11 +1166,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x1 dAtA[i] = 0x1
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Items.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Items.Size()))
n19, err := m.Items.MarshalTo(dAtA[i:]) n21, err := m.Items.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n19 i += n21
} }
if len(m.AllOf) > 0 { if len(m.AllOf) > 0 {
for _, msg := range m.AllOf { for _, msg := range m.AllOf {
...@@ -1188,11 +1220,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1188,11 +1220,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x1 dAtA[i] = 0x1
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Not.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Not.Size()))
n20, err := m.Not.MarshalTo(dAtA[i:]) n22, err := m.Not.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n20 i += n22
} }
if len(m.Properties) > 0 { if len(m.Properties) > 0 {
keysForProperties := make([]string, 0, len(m.Properties)) keysForProperties := make([]string, 0, len(m.Properties))
...@@ -1220,11 +1252,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1220,11 +1252,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64((&v).Size())) i = encodeVarintGenerated(dAtA, i, uint64((&v).Size()))
n21, err := (&v).MarshalTo(dAtA[i:]) n23, err := (&v).MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n21 i += n23
} }
} }
if m.AdditionalProperties != nil { if m.AdditionalProperties != nil {
...@@ -1233,11 +1265,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1233,11 +1265,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x1 dAtA[i] = 0x1
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.AdditionalProperties.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.AdditionalProperties.Size()))
n22, err := m.AdditionalProperties.MarshalTo(dAtA[i:]) n24, err := m.AdditionalProperties.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n22 i += n24
} }
if len(m.PatternProperties) > 0 { if len(m.PatternProperties) > 0 {
keysForPatternProperties := make([]string, 0, len(m.PatternProperties)) keysForPatternProperties := make([]string, 0, len(m.PatternProperties))
...@@ -1265,11 +1297,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1265,11 +1297,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64((&v).Size())) i = encodeVarintGenerated(dAtA, i, uint64((&v).Size()))
n23, err := (&v).MarshalTo(dAtA[i:]) n25, err := (&v).MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n23 i += n25
} }
} }
if len(m.Dependencies) > 0 { if len(m.Dependencies) > 0 {
...@@ -1298,11 +1330,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1298,11 +1330,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64((&v).Size())) i = encodeVarintGenerated(dAtA, i, uint64((&v).Size()))
n24, err := (&v).MarshalTo(dAtA[i:]) n26, err := (&v).MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n24 i += n26
} }
} }
if m.AdditionalItems != nil { if m.AdditionalItems != nil {
...@@ -1311,11 +1343,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1311,11 +1343,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x2 dAtA[i] = 0x2
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.AdditionalItems.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.AdditionalItems.Size()))
n25, err := m.AdditionalItems.MarshalTo(dAtA[i:]) n27, err := m.AdditionalItems.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n25 i += n27
} }
if len(m.Definitions) > 0 { if len(m.Definitions) > 0 {
keysForDefinitions := make([]string, 0, len(m.Definitions)) keysForDefinitions := make([]string, 0, len(m.Definitions))
...@@ -1343,11 +1375,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1343,11 +1375,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64((&v).Size())) i = encodeVarintGenerated(dAtA, i, uint64((&v).Size()))
n26, err := (&v).MarshalTo(dAtA[i:]) n28, err := (&v).MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n26 i += n28
} }
} }
if m.ExternalDocs != nil { if m.ExternalDocs != nil {
...@@ -1356,11 +1388,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1356,11 +1388,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x2 dAtA[i] = 0x2
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.ExternalDocs.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.ExternalDocs.Size()))
n27, err := m.ExternalDocs.MarshalTo(dAtA[i:]) n29, err := m.ExternalDocs.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n27 i += n29
} }
if m.Example != nil { if m.Example != nil {
dAtA[i] = 0xa2 dAtA[i] = 0xa2
...@@ -1368,11 +1400,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -1368,11 +1400,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x2 dAtA[i] = 0x2
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Example.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Example.Size()))
n28, err := m.Example.MarshalTo(dAtA[i:]) n30, err := m.Example.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n28 i += n30
} }
return i, nil return i, nil
} }
...@@ -1396,11 +1428,11 @@ func (m *JSONSchemaPropsOrArray) MarshalTo(dAtA []byte) (int, error) { ...@@ -1396,11 +1428,11 @@ func (m *JSONSchemaPropsOrArray) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0xa dAtA[i] = 0xa
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size()))
n29, err := m.Schema.MarshalTo(dAtA[i:]) n31, err := m.Schema.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n29 i += n31
} }
if len(m.JSONSchemas) > 0 { if len(m.JSONSchemas) > 0 {
for _, msg := range m.JSONSchemas { for _, msg := range m.JSONSchemas {
...@@ -1444,11 +1476,11 @@ func (m *JSONSchemaPropsOrBool) MarshalTo(dAtA []byte) (int, error) { ...@@ -1444,11 +1476,11 @@ func (m *JSONSchemaPropsOrBool) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size()))
n30, err := m.Schema.MarshalTo(dAtA[i:]) n32, err := m.Schema.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n30 i += n32
} }
return i, nil return i, nil
} }
...@@ -1472,11 +1504,11 @@ func (m *JSONSchemaPropsOrStringArray) MarshalTo(dAtA []byte) (int, error) { ...@@ -1472,11 +1504,11 @@ func (m *JSONSchemaPropsOrStringArray) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0xa dAtA[i] = 0xa
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size()))
n31, err := m.Schema.MarshalTo(dAtA[i:]) n33, err := m.Schema.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n31 i += n33
} }
if len(m.Property) > 0 { if len(m.Property) > 0 {
for _, s := range m.Property { for _, s := range m.Property {
...@@ -1547,11 +1579,11 @@ func (m *WebhookClientConfig) MarshalTo(dAtA []byte) (int, error) { ...@@ -1547,11 +1579,11 @@ func (m *WebhookClientConfig) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0xa dAtA[i] = 0xa
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Service.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Service.Size()))
n32, err := m.Service.MarshalTo(dAtA[i:]) n34, err := m.Service.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n32 i += n34
} }
if m.CABundle != nil { if m.CABundle != nil {
dAtA[i] = 0x12 dAtA[i] = 0x12
...@@ -1785,6 +1817,20 @@ func (m *CustomResourceDefinitionVersion) Size() (n int) { ...@@ -1785,6 +1817,20 @@ func (m *CustomResourceDefinitionVersion) Size() (n int) {
n += 1 + l + sovGenerated(uint64(l)) n += 1 + l + sovGenerated(uint64(l))
n += 2 n += 2
n += 2 n += 2
if m.Schema != nil {
l = m.Schema.Size()
n += 1 + l + sovGenerated(uint64(l))
}
if m.Subresources != nil {
l = m.Subresources.Size()
n += 1 + l + sovGenerated(uint64(l))
}
if len(m.AdditionalPrinterColumns) > 0 {
for _, e := range m.AdditionalPrinterColumns {
l = e.Size()
n += 1 + l + sovGenerated(uint64(l))
}
}
return n return n
} }
...@@ -2239,6 +2285,9 @@ func (this *CustomResourceDefinitionVersion) String() string { ...@@ -2239,6 +2285,9 @@ func (this *CustomResourceDefinitionVersion) String() string {
`Name:` + fmt.Sprintf("%v", this.Name) + `,`, `Name:` + fmt.Sprintf("%v", this.Name) + `,`,
`Served:` + fmt.Sprintf("%v", this.Served) + `,`, `Served:` + fmt.Sprintf("%v", this.Served) + `,`,
`Storage:` + fmt.Sprintf("%v", this.Storage) + `,`, `Storage:` + fmt.Sprintf("%v", this.Storage) + `,`,
`Schema:` + strings.Replace(fmt.Sprintf("%v", this.Schema), "CustomResourceValidation", "CustomResourceValidation", 1) + `,`,
`Subresources:` + strings.Replace(fmt.Sprintf("%v", this.Subresources), "CustomResourceSubresources", "CustomResourceSubresources", 1) + `,`,
`AdditionalPrinterColumns:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.AdditionalPrinterColumns), "CustomResourceColumnDefinition", "CustomResourceColumnDefinition", 1), `&`, ``, 1) + `,`,
`}`, `}`,
}, "") }, "")
return s return s
...@@ -4414,6 +4463,103 @@ func (m *CustomResourceDefinitionVersion) Unmarshal(dAtA []byte) error { ...@@ -4414,6 +4463,103 @@ func (m *CustomResourceDefinitionVersion) Unmarshal(dAtA []byte) error {
} }
} }
m.Storage = bool(v != 0) m.Storage = bool(v != 0)
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Schema", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Schema == nil {
m.Schema = &CustomResourceValidation{}
}
if err := m.Schema.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 5:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Subresources", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Subresources == nil {
m.Subresources = &CustomResourceSubresources{}
}
if err := m.Subresources.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 6:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field AdditionalPrinterColumns", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.AdditionalPrinterColumns = append(m.AdditionalPrinterColumns, CustomResourceColumnDefinition{})
if err := m.AdditionalPrinterColumns[len(m.AdditionalPrinterColumns)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:]) skippy, err := skipGenerated(dAtA[iNdEx:])
...@@ -7107,175 +7253,177 @@ func init() { ...@@ -7107,175 +7253,177 @@ func init() {
} }
var fileDescriptorGenerated = []byte{ var fileDescriptorGenerated = []byte{
// 2711 bytes of a gzipped FileDescriptorProto // 2740 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x5a, 0xdd, 0x6f, 0x24, 0x47, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x5a, 0xcd, 0x73, 0x1c, 0x47,
0x11, 0xbf, 0xd9, 0xf5, 0xda, 0xeb, 0xb6, 0x7d, 0xb6, 0xfb, 0xe2, 0xcb, 0x9c, 0xb9, 0xec, 0xda, 0x15, 0xf7, 0xec, 0x6a, 0xa5, 0x55, 0x4b, 0xb2, 0xa4, 0xb6, 0xe5, 0x8c, 0x85, 0xbd, 0x2b, 0xad,
0x1b, 0x12, 0x99, 0x70, 0xb7, 0xce, 0x1d, 0x09, 0x09, 0x91, 0x78, 0xf0, 0xda, 0x4e, 0xe4, 0x70, 0x49, 0x4a, 0x04, 0x7b, 0x15, 0x9b, 0x84, 0x84, 0x54, 0x71, 0xd0, 0x4a, 0x4a, 0x4a, 0xc6, 0xfa,
0x3e, 0x9b, 0xde, 0xbb, 0x0b, 0x90, 0xcf, 0xf6, 0x4c, 0xef, 0x7a, 0xce, 0xf3, 0x75, 0xd3, 0x33, 0xa0, 0xd7, 0x76, 0x80, 0x7c, 0xb6, 0x66, 0x7b, 0x57, 0x63, 0xcd, 0x97, 0xa7, 0x67, 0x56, 0x52,
0x6b, 0x5b, 0x7c, 0x08, 0x12, 0x45, 0x20, 0x04, 0x04, 0x91, 0x13, 0x12, 0x02, 0x1e, 0x00, 0xf1, 0x05, 0x28, 0x48, 0x2a, 0x05, 0x45, 0x01, 0xa1, 0x88, 0x2f, 0x14, 0x70, 0x00, 0x8a, 0x0b, 0x45,
0x82, 0x10, 0x3c, 0xc0, 0x1b, 0xfc, 0x01, 0xf7, 0x18, 0xf1, 0x94, 0xa7, 0x15, 0xb7, 0xfc, 0x0b, 0xc1, 0x01, 0x6e, 0xf0, 0x07, 0xf8, 0x98, 0xe2, 0x94, 0xd3, 0x16, 0xde, 0xfc, 0x0b, 0x54, 0x51,
0x48, 0x48, 0x7e, 0x42, 0xfd, 0x31, 0x3d, 0xb3, 0xb3, 0xbb, 0x39, 0x2b, 0xb7, 0x9b, 0x7b, 0xf3, 0xa5, 0x13, 0xd5, 0x1f, 0xd3, 0x33, 0x3b, 0xbb, 0x6b, 0xab, 0xe2, 0xdd, 0x98, 0x9b, 0xe6, 0xbd,
0x54, 0x55, 0xd7, 0xaf, 0xba, 0xba, 0xaa, 0xba, 0xaa, 0xd7, 0xa0, 0x71, 0xf0, 0x22, 0xad, 0x5a, 0xd7, 0xef, 0xf7, 0xfa, 0xf5, 0x7b, 0xaf, 0x5f, 0xbf, 0x15, 0xa8, 0xef, 0xbf, 0x44, 0xcb, 0xa6,
0xde, 0xea, 0x41, 0xb4, 0x47, 0x02, 0x97, 0x84, 0x84, 0xae, 0xb6, 0x88, 0x6b, 0x7a, 0xc1, 0xaa, 0xbb, 0xbc, 0x1f, 0xee, 0x12, 0xdf, 0x21, 0x01, 0xa1, 0xcb, 0x4d, 0xe2, 0xd4, 0x5c, 0x7f, 0x59,
0x64, 0x60, 0xdf, 0x22, 0x47, 0x21, 0x71, 0xa9, 0xe5, 0xb9, 0xf4, 0x32, 0xf6, 0x2d, 0x4a, 0x82, 0x32, 0xb0, 0x67, 0x92, 0xc3, 0x80, 0x38, 0xd4, 0x74, 0x1d, 0x7a, 0x05, 0x7b, 0x26, 0x25, 0x7e,
0x16, 0x09, 0x56, 0xfd, 0x83, 0x26, 0xe3, 0xd1, 0x6e, 0x81, 0xd5, 0xd6, 0x95, 0x3d, 0x12, 0xe2, 0x93, 0xf8, 0xcb, 0xde, 0x7e, 0x83, 0xf1, 0x68, 0xa7, 0xc0, 0x72, 0xf3, 0xea, 0x2e, 0x09, 0xf0,
0x2b, 0xab, 0x4d, 0xe2, 0x92, 0x00, 0x87, 0xc4, 0xac, 0xfa, 0x81, 0x17, 0x7a, 0xf0, 0xab, 0x42, 0xd5, 0xe5, 0x06, 0x71, 0x88, 0x8f, 0x03, 0x52, 0x2b, 0x7b, 0xbe, 0x1b, 0xb8, 0xf0, 0xeb, 0x42,
0x5d, 0xb5, 0x4b, 0xfa, 0x6d, 0xa5, 0xae, 0xea, 0x1f, 0x34, 0x19, 0x8f, 0x76, 0x0b, 0x54, 0xa5, 0x5d, 0xb9, 0x43, 0xfa, 0x6d, 0xa5, 0xae, 0xec, 0xed, 0x37, 0x18, 0x8f, 0x76, 0x0a, 0x94, 0xa5,
0xba, 0xc5, 0xcb, 0x4d, 0x2b, 0xdc, 0x8f, 0xf6, 0xaa, 0x86, 0xe7, 0xac, 0x36, 0xbd, 0xa6, 0xb7, 0xba, 0xf9, 0x2b, 0x0d, 0x33, 0xd8, 0x0b, 0x77, 0xcb, 0x86, 0x6b, 0x2f, 0x37, 0xdc, 0x86, 0xbb,
0xca, 0xb5, 0xee, 0x45, 0x0d, 0xfe, 0xc5, 0x3f, 0xf8, 0x5f, 0x02, 0x6d, 0xf1, 0xb9, 0xc4, 0x78, 0xcc, 0xb5, 0xee, 0x86, 0x75, 0xfe, 0xc5, 0x3f, 0xf8, 0x5f, 0x02, 0x6d, 0xfe, 0xf9, 0xd8, 0x78,
0x07, 0x1b, 0xfb, 0x96, 0x4b, 0x82, 0xe3, 0xc4, 0x62, 0x87, 0x84, 0x78, 0xb5, 0xd5, 0x63, 0xe3, 0x1b, 0x1b, 0x7b, 0xa6, 0x43, 0xfc, 0xa3, 0xd8, 0x62, 0x9b, 0x04, 0x78, 0xb9, 0xd9, 0x65, 0xe3,
0xe2, 0xea, 0xa0, 0x55, 0x41, 0xe4, 0x86, 0x96, 0x43, 0x7a, 0x16, 0x7c, 0xf9, 0x41, 0x0b, 0xa8, 0xfc, 0x72, 0xbf, 0x55, 0x7e, 0xe8, 0x04, 0xa6, 0x4d, 0xba, 0x16, 0x7c, 0xf5, 0x51, 0x0b, 0xa8,
0xb1, 0x4f, 0x1c, 0x9c, 0x5d, 0x57, 0x39, 0xd1, 0xc0, 0xfc, 0xba, 0xe7, 0xb6, 0x48, 0xc0, 0x76, 0xb1, 0x47, 0x6c, 0x9c, 0x5e, 0x57, 0x3a, 0xd6, 0xc0, 0xec, 0xaa, 0xeb, 0x34, 0x89, 0xcf, 0x76,
0x89, 0xc8, 0x9d, 0x88, 0xd0, 0x10, 0xd6, 0x40, 0x3e, 0xb2, 0x4c, 0x5d, 0x5b, 0xd2, 0x56, 0x26, 0x89, 0xc8, 0xdd, 0x90, 0xd0, 0x00, 0x56, 0x40, 0x36, 0x34, 0x6b, 0xba, 0xb6, 0xa0, 0x2d, 0x8d,
0x6b, 0xcf, 0xde, 0x6b, 0x97, 0xcf, 0x74, 0xda, 0xe5, 0xfc, 0xcd, 0xad, 0x8d, 0x93, 0x76, 0x79, 0x57, 0x9e, 0xbb, 0xdf, 0x2a, 0x9e, 0x6a, 0xb7, 0x8a, 0xd9, 0x5b, 0x1b, 0x6b, 0xc7, 0xad, 0xe2,
0x79, 0x10, 0x52, 0x78, 0xec, 0x13, 0x5a, 0xbd, 0xb9, 0xb5, 0x81, 0xd8, 0x62, 0xf8, 0x0a, 0x98, 0x62, 0x3f, 0xa4, 0xe0, 0xc8, 0x23, 0xb4, 0x7c, 0x6b, 0x63, 0x0d, 0xb1, 0xc5, 0xf0, 0x55, 0x30,
0x37, 0x09, 0xb5, 0x02, 0x62, 0xae, 0xed, 0x6e, 0xdd, 0x12, 0xfa, 0xf5, 0x1c, 0xd7, 0x78, 0x41, 0x5b, 0x23, 0xd4, 0xf4, 0x49, 0x6d, 0x65, 0x67, 0xe3, 0xb6, 0xd0, 0xaf, 0x67, 0xb8, 0xc6, 0xf3,
0x6a, 0x9c, 0xdf, 0xc8, 0x0a, 0xa0, 0xde, 0x35, 0xf0, 0x1b, 0x60, 0xc2, 0xdb, 0xbb, 0x4d, 0x8c, 0x52, 0xe3, 0xec, 0x5a, 0x5a, 0x00, 0x75, 0xaf, 0x81, 0xdf, 0x02, 0x63, 0xee, 0xee, 0x1d, 0x62,
0x90, 0xea, 0xf9, 0xa5, 0xfc, 0xca, 0xd4, 0xd5, 0xcb, 0xd5, 0xe4, 0x04, 0x95, 0x09, 0xfc, 0xd8, 0x04, 0x54, 0xcf, 0x2e, 0x64, 0x97, 0x26, 0xae, 0x5d, 0x29, 0xc7, 0x27, 0xa8, 0x4c, 0xe0, 0xc7,
0xe4, 0x66, 0xab, 0x08, 0x1f, 0x6e, 0xc6, 0x27, 0x57, 0x9b, 0x95, 0x68, 0x13, 0x3b, 0x42, 0x0b, 0x26, 0x37, 0x5b, 0x46, 0xf8, 0x60, 0x3d, 0x3a, 0xb9, 0xca, 0xb4, 0x44, 0x1b, 0xdb, 0x16, 0x5a,
0x8a, 0xd5, 0x55, 0xfe, 0x90, 0x03, 0x30, 0xbd, 0x79, 0xea, 0x7b, 0x2e, 0x25, 0x43, 0xd9, 0x3d, 0x50, 0xa4, 0xae, 0xf4, 0x87, 0x0c, 0x80, 0xc9, 0xcd, 0x53, 0xcf, 0x75, 0x28, 0x19, 0xc8, 0xee,
0x05, 0x73, 0x06, 0xd7, 0x1c, 0x12, 0x53, 0xe2, 0xea, 0xb9, 0x4f, 0x63, 0xbd, 0x2e, 0xf1, 0xe7, 0x29, 0x98, 0x31, 0xb8, 0xe6, 0x80, 0xd4, 0x24, 0xae, 0x9e, 0xf9, 0x2c, 0xd6, 0xeb, 0x12, 0x7f,
0xd6, 0x33, 0xea, 0x50, 0x0f, 0x00, 0xbc, 0x01, 0xc6, 0x03, 0x42, 0x23, 0x3b, 0xd4, 0xf3, 0x4b, 0x66, 0x35, 0xa5, 0x0e, 0x75, 0x01, 0xc0, 0x9b, 0x60, 0xd4, 0x27, 0x34, 0xb4, 0x02, 0x3d, 0xbb,
0xda, 0xca, 0xd4, 0xd5, 0x4b, 0x03, 0xa1, 0x78, 0x7c, 0xb3, 0xe0, 0xab, 0xb6, 0xae, 0x54, 0xeb, 0xa0, 0x2d, 0x4d, 0x5c, 0xbb, 0xdc, 0x17, 0x8a, 0xc7, 0x37, 0x0b, 0xbe, 0x72, 0xf3, 0x6a, 0xb9,
0x21, 0x0e, 0x23, 0x5a, 0x3b, 0x2b, 0x91, 0xc6, 0x11, 0xd7, 0x81, 0xa4, 0xae, 0xca, 0x8f, 0x72, 0x1a, 0xe0, 0x20, 0xa4, 0x95, 0xd3, 0x12, 0x69, 0x14, 0x71, 0x1d, 0x48, 0xea, 0x2a, 0xfd, 0x38,
0x60, 0x2e, 0xed, 0xa5, 0x96, 0x45, 0x0e, 0xe1, 0x21, 0x98, 0x08, 0x44, 0xb0, 0x70, 0x3f, 0x4d, 0x03, 0x66, 0x92, 0x5e, 0x6a, 0x9a, 0xe4, 0x00, 0x1e, 0x80, 0x31, 0x5f, 0x04, 0x0b, 0xf7, 0xd3,
0x5d, 0xdd, 0xad, 0x3e, 0x54, 0x5a, 0x55, 0x7b, 0x82, 0xb0, 0x36, 0xc5, 0xce, 0x4c, 0x7e, 0xa0, 0xc4, 0xb5, 0x9d, 0xf2, 0x63, 0xa5, 0x55, 0xb9, 0x2b, 0x08, 0x2b, 0x13, 0xec, 0xcc, 0xe4, 0x07,
0x18, 0x0d, 0x7e, 0x1b, 0x14, 0x03, 0x79, 0x50, 0x3c, 0x9a, 0xa6, 0xae, 0x7e, 0x7d, 0x88, 0xc8, 0x8a, 0xd0, 0xe0, 0xbb, 0x20, 0xef, 0xcb, 0x83, 0xe2, 0xd1, 0x34, 0x71, 0xed, 0x9b, 0x03, 0x44,
0x42, 0x71, 0x6d, 0xba, 0xd3, 0x2e, 0x17, 0xe3, 0x2f, 0xa4, 0x00, 0x2b, 0x1f, 0xe6, 0x40, 0x69, 0x16, 0x8a, 0x2b, 0x93, 0xed, 0x56, 0x31, 0x1f, 0x7d, 0x21, 0x05, 0x58, 0xfa, 0x28, 0x03, 0x0a,
0x3d, 0xa2, 0xa1, 0xe7, 0x20, 0x42, 0xbd, 0x28, 0x30, 0xc8, 0xba, 0x67, 0x47, 0x8e, 0xbb, 0x41, 0xab, 0x21, 0x0d, 0x5c, 0x1b, 0x11, 0xea, 0x86, 0xbe, 0x41, 0x56, 0x5d, 0x2b, 0xb4, 0x9d, 0x35,
0x1a, 0x96, 0x6b, 0x85, 0x2c, 0x5a, 0x97, 0xc0, 0x98, 0x8b, 0x1d, 0x22, 0xa3, 0x67, 0x5a, 0xfa, 0x52, 0x37, 0x1d, 0x33, 0x60, 0xd1, 0xba, 0x00, 0x46, 0x1c, 0x6c, 0x13, 0x19, 0x3d, 0x93, 0xd2,
0x74, 0xec, 0x3a, 0x76, 0x08, 0xe2, 0x1c, 0x26, 0xc1, 0x82, 0x45, 0xe6, 0x82, 0x92, 0xb8, 0x71, 0xa7, 0x23, 0x5b, 0xd8, 0x26, 0x88, 0x73, 0x98, 0x04, 0x0b, 0x16, 0x99, 0x0b, 0x4a, 0xe2, 0xe6,
0xec, 0x13, 0xc4, 0x39, 0xf0, 0x69, 0x30, 0xde, 0xf0, 0x02, 0x07, 0x8b, 0x73, 0x9c, 0x4c, 0x4e, 0x91, 0x47, 0x10, 0xe7, 0xc0, 0x67, 0xc0, 0x68, 0xdd, 0xf5, 0x6d, 0x2c, 0xce, 0x71, 0x3c, 0x3e,
0xe6, 0x65, 0x4e, 0x45, 0x92, 0x0b, 0x9f, 0x07, 0x53, 0x26, 0xa1, 0x46, 0x60, 0xf9, 0x0c, 0x5a, 0x99, 0x57, 0x38, 0x15, 0x49, 0x2e, 0x7c, 0x01, 0x4c, 0xd4, 0x08, 0x35, 0x7c, 0xd3, 0x63, 0xd0,
0x1f, 0xe3, 0xc2, 0xe7, 0xa4, 0xf0, 0xd4, 0x46, 0xc2, 0x42, 0x69, 0x39, 0x78, 0x09, 0x14, 0xfd, 0xfa, 0x08, 0x17, 0x3e, 0x23, 0x85, 0x27, 0xd6, 0x62, 0x16, 0x4a, 0xca, 0xc1, 0xcb, 0x20, 0xef,
0xc0, 0xf2, 0x02, 0x2b, 0x3c, 0xd6, 0x0b, 0x4b, 0xda, 0x4a, 0xa1, 0x36, 0x27, 0xd7, 0x14, 0x77, 0xf9, 0xa6, 0xeb, 0x9b, 0xc1, 0x91, 0x9e, 0x5b, 0xd0, 0x96, 0x72, 0x95, 0x19, 0xb9, 0x26, 0xbf,
0x25, 0x1d, 0x29, 0x09, 0xb8, 0x04, 0x8a, 0xaf, 0xd6, 0x77, 0xae, 0xef, 0xe2, 0x70, 0x5f, 0x1f, 0x23, 0xe9, 0x48, 0x49, 0xc0, 0x05, 0x90, 0xbf, 0x5e, 0xdd, 0xde, 0xda, 0xc1, 0xc1, 0x9e, 0x3e,
0xe7, 0x08, 0x63, 0x4c, 0x1a, 0x15, 0x6f, 0x4b, 0x6a, 0xe5, 0xdd, 0x1c, 0xd0, 0xb3, 0x5e, 0x89, 0xca, 0x11, 0x46, 0x98, 0x34, 0xca, 0xdf, 0x91, 0xd4, 0xd2, 0x7b, 0x19, 0xa0, 0xa7, 0xbd, 0x12,
0x5d, 0x0a, 0x5f, 0x06, 0x45, 0x1a, 0xb2, 0x8a, 0xd3, 0x3c, 0x96, 0x3e, 0x79, 0x26, 0x06, 0xab, 0xb9, 0x14, 0xbe, 0x02, 0xf2, 0x34, 0x60, 0x15, 0xa7, 0x71, 0x24, 0x7d, 0xf2, 0x6c, 0x04, 0x56,
0x4b, 0xfa, 0x49, 0xbb, 0x7c, 0x3e, 0x59, 0x11, 0x53, 0xb9, 0x3f, 0xd4, 0x5a, 0xf8, 0x5b, 0x0d, 0x95, 0xf4, 0xe3, 0x56, 0xf1, 0x5c, 0xbc, 0x22, 0xa2, 0x72, 0x7f, 0xa8, 0xb5, 0xf0, 0xb7, 0x1a,
0x9c, 0x3b, 0x24, 0x7b, 0xfb, 0x9e, 0x77, 0xb0, 0x6e, 0x5b, 0xc4, 0x0d, 0xd7, 0x3d, 0xb7, 0x61, 0x38, 0x73, 0x40, 0x76, 0xf7, 0x5c, 0x77, 0x7f, 0xd5, 0x32, 0x89, 0x13, 0xac, 0xba, 0x4e, 0xdd,
0x35, 0x65, 0x0c, 0xa0, 0x87, 0x8c, 0x81, 0xd7, 0x7a, 0x35, 0xd7, 0x1e, 0xef, 0xb4, 0xcb, 0xe7, 0x6c, 0xc8, 0x18, 0x40, 0x8f, 0x19, 0x03, 0xaf, 0x75, 0x6b, 0xae, 0x3c, 0xd5, 0x6e, 0x15, 0xcf,
0xfa, 0x30, 0x50, 0x3f, 0x3b, 0x2a, 0xef, 0xe5, 0xb3, 0x4e, 0x48, 0x05, 0xc5, 0x3b, 0xa0, 0xc8, 0xf4, 0x60, 0xa0, 0x5e, 0x76, 0x94, 0xde, 0xcf, 0xa6, 0x9d, 0x90, 0x08, 0x8a, 0x77, 0x40, 0x9e,
0x92, 0xcd, 0xc4, 0x21, 0x96, 0xe9, 0xf2, 0xec, 0xe9, 0x52, 0x53, 0x64, 0xf6, 0x36, 0x09, 0x71, 0x25, 0x5b, 0x0d, 0x07, 0x58, 0xa6, 0xcb, 0x73, 0x27, 0x4b, 0x4d, 0x91, 0xd9, 0x9b, 0x24, 0xc0,
0x0d, 0x4a, 0xb7, 0x81, 0x84, 0x86, 0x94, 0x56, 0xf8, 0x5d, 0x30, 0x46, 0x7d, 0x62, 0x48, 0x77, 0x15, 0x28, 0xdd, 0x06, 0x62, 0x1a, 0x52, 0x5a, 0xe1, 0xf7, 0xc0, 0x08, 0xf5, 0x88, 0x21, 0xdd,
0xbc, 0xfe, 0xb0, 0x29, 0x31, 0x60, 0x23, 0x75, 0x9f, 0x18, 0x49, 0xc4, 0xb2, 0x2f, 0xc4, 0x61, 0xf1, 0xfa, 0xe3, 0xa6, 0x44, 0x9f, 0x8d, 0x54, 0x3d, 0x62, 0xc4, 0x11, 0xcb, 0xbe, 0x10, 0x87,
0xe1, 0xfb, 0x1a, 0x18, 0xa7, 0xbc, 0x8c, 0xc8, 0xd2, 0xf3, 0xe6, 0xa8, 0x2c, 0xc8, 0xd4, 0x2a, 0x85, 0x1f, 0x68, 0x60, 0x94, 0xf2, 0x32, 0x22, 0x4b, 0xcf, 0x9b, 0xc3, 0xb2, 0x20, 0x55, 0xab,
0xf1, 0x8d, 0x24, 0x78, 0xe5, 0xbf, 0x39, 0xb0, 0x3c, 0x68, 0xe9, 0xba, 0xe7, 0x9a, 0xe2, 0x38, 0xc4, 0x37, 0x92, 0xe0, 0xa5, 0xff, 0x64, 0xc0, 0x62, 0xbf, 0xa5, 0xab, 0xae, 0x53, 0x13, 0xc7,
0xb6, 0x64, 0x06, 0x8a, 0x78, 0x7c, 0x3e, 0x9d, 0x81, 0x27, 0xed, 0xf2, 0x53, 0x0f, 0x54, 0x90, 0xb1, 0x21, 0x33, 0x50, 0xc4, 0xe3, 0x0b, 0xc9, 0x0c, 0x3c, 0x6e, 0x15, 0x9f, 0x7e, 0xa4, 0x82,
0x4a, 0xd5, 0xaf, 0xa8, 0x7d, 0x8b, 0x74, 0x5e, 0xee, 0x36, 0xec, 0xa4, 0x5d, 0x9e, 0x55, 0xcb, 0x44, 0xaa, 0x7e, 0x4d, 0xed, 0x5b, 0xa4, 0xf3, 0x62, 0xa7, 0x61, 0xc7, 0xad, 0xe2, 0xb4, 0x5a,
0xba, 0x6d, 0x85, 0x2d, 0x00, 0x6d, 0x4c, 0xc3, 0x1b, 0x01, 0x76, 0xa9, 0x50, 0x6b, 0x39, 0x44, 0xd6, 0x69, 0x2b, 0x6c, 0x02, 0x68, 0x61, 0x1a, 0xdc, 0xf4, 0xb1, 0x43, 0x85, 0x5a, 0xd3, 0x26,
0xba, 0xef, 0x99, 0xd3, 0x85, 0x07, 0x5b, 0x51, 0x5b, 0x94, 0x90, 0xf0, 0x5a, 0x8f, 0x36, 0xd4, 0xd2, 0x7d, 0xcf, 0x9e, 0x2c, 0x3c, 0xd8, 0x8a, 0xca, 0xbc, 0x84, 0x84, 0x37, 0xba, 0xb4, 0xa1,
0x07, 0x81, 0x55, 0x97, 0x80, 0x60, 0xaa, 0x0a, 0x46, 0xaa, 0xee, 0x33, 0x2a, 0x92, 0x5c, 0xf8, 0x1e, 0x08, 0xac, 0xba, 0xf8, 0x04, 0x53, 0x55, 0x30, 0x12, 0x75, 0x9f, 0x51, 0x91, 0xe4, 0xc2,
0x05, 0x30, 0xe1, 0x10, 0x4a, 0x71, 0x93, 0xf0, 0x2a, 0x31, 0x99, 0x5c, 0xa4, 0xdb, 0x82, 0x8c, 0x2f, 0x81, 0x31, 0x9b, 0x50, 0x8a, 0x1b, 0x84, 0x57, 0x89, 0xf1, 0xf8, 0x22, 0xdd, 0x14, 0x64,
0x62, 0x3e, 0xeb, 0x22, 0x2e, 0x0e, 0xf2, 0xda, 0x35, 0x8b, 0x86, 0xf0, 0x8d, 0x9e, 0x04, 0xa8, 0x14, 0xf1, 0x59, 0x17, 0x71, 0xa1, 0x9f, 0xd7, 0x6e, 0x98, 0x34, 0x80, 0x6f, 0x74, 0x25, 0x40,
0x9e, 0x6e, 0x87, 0x6c, 0x35, 0x0f, 0x7f, 0x55, 0xa2, 0x62, 0x4a, 0x2a, 0xf8, 0xbf, 0x03, 0x0a, 0xf9, 0x64, 0x3b, 0x64, 0xab, 0x79, 0xf8, 0xab, 0x12, 0x15, 0x51, 0x12, 0xc1, 0xff, 0x5d, 0x90,
0x56, 0x48, 0x9c, 0xf8, 0x86, 0x7d, 0x6d, 0x44, 0xb1, 0x57, 0x9b, 0x91, 0x36, 0x14, 0xb6, 0x18, 0x33, 0x03, 0x62, 0x47, 0x37, 0xec, 0x6b, 0x43, 0x8a, 0xbd, 0xca, 0x94, 0xb4, 0x21, 0xb7, 0xc1,
0x1a, 0x12, 0xa0, 0x95, 0x3f, 0xe6, 0xc0, 0x13, 0x83, 0x96, 0xb0, 0xb2, 0x4f, 0x99, 0xc7, 0x7d, 0xd0, 0x90, 0x00, 0x2d, 0xfd, 0x31, 0x03, 0x2e, 0xf6, 0x5b, 0xc2, 0xca, 0x3e, 0x65, 0x1e, 0xf7,
0x3b, 0x0a, 0xb0, 0x2d, 0x23, 0x4e, 0x79, 0x7c, 0x97, 0x53, 0x91, 0xe4, 0xb2, 0xc2, 0x4c, 0x2d, 0xac, 0xd0, 0xc7, 0x96, 0x8c, 0x38, 0xe5, 0xf1, 0x1d, 0x4e, 0x45, 0x92, 0xcb, 0x0a, 0x33, 0x35,
0xb7, 0x19, 0xd9, 0x38, 0x90, 0xe1, 0xa4, 0x76, 0x5d, 0x97, 0x74, 0xa4, 0x24, 0x60, 0x15, 0x00, 0x9d, 0x46, 0x68, 0x61, 0x5f, 0x86, 0x93, 0xda, 0x75, 0x55, 0xd2, 0x91, 0x92, 0x80, 0x65, 0x00,
0xba, 0xef, 0x05, 0x21, 0xc7, 0xe0, 0xad, 0xd1, 0x64, 0xed, 0x2c, 0x2b, 0x10, 0x75, 0x45, 0x45, 0xe8, 0x9e, 0xeb, 0x07, 0x1c, 0x83, 0xb7, 0x46, 0xe3, 0x95, 0xd3, 0xac, 0x40, 0x54, 0x15, 0x15,
0x29, 0x09, 0x76, 0xef, 0x1c, 0x58, 0xae, 0x29, 0x4f, 0x5d, 0x65, 0xf1, 0xd7, 0x2c, 0xd7, 0x44, 0x25, 0x24, 0xd8, 0xbd, 0xb3, 0x6f, 0x3a, 0x35, 0x79, 0xea, 0x2a, 0x8b, 0xbf, 0x61, 0x3a, 0x35,
0x9c, 0xc3, 0xf0, 0x6d, 0x8b, 0x86, 0x8c, 0x22, 0x8f, 0xbc, 0xcb, 0xeb, 0x5c, 0x52, 0x49, 0x30, 0xc4, 0x39, 0x0c, 0xdf, 0x32, 0x69, 0xc0, 0x28, 0xf2, 0xc8, 0x3b, 0xbc, 0xce, 0x25, 0x95, 0x04,
0x7c, 0x83, 0xd5, 0x66, 0x2f, 0xb0, 0x08, 0xd5, 0xc7, 0x13, 0xfc, 0x75, 0x45, 0x45, 0x29, 0x89, 0xc3, 0x37, 0x58, 0x6d, 0x76, 0x7d, 0x93, 0x50, 0x7d, 0x34, 0xc6, 0x5f, 0x55, 0x54, 0x94, 0x90,
0xca, 0xaf, 0x8b, 0x83, 0x83, 0x84, 0x95, 0x12, 0xf8, 0x24, 0x28, 0x34, 0x03, 0x2f, 0xf2, 0xa5, 0x28, 0xfd, 0x3a, 0xdf, 0x3f, 0x48, 0x58, 0x29, 0x81, 0x97, 0x40, 0xae, 0xe1, 0xbb, 0xa1, 0x27,
0x97, 0x94, 0xb7, 0x5f, 0x61, 0x44, 0x24, 0x78, 0x2c, 0x2a, 0x5b, 0x5d, 0xcd, 0xa4, 0x8a, 0xca, 0xbd, 0xa4, 0xbc, 0xfd, 0x2a, 0x23, 0x22, 0xc1, 0x63, 0x51, 0xd9, 0xec, 0x68, 0x26, 0x55, 0x54,
0xb8, 0x85, 0x8c, 0xf9, 0xf0, 0x07, 0x1a, 0x28, 0xb8, 0xd2, 0x39, 0x2c, 0xe4, 0xde, 0x18, 0x51, 0x46, 0x2d, 0x64, 0xc4, 0x87, 0x3f, 0xd4, 0x40, 0xce, 0x91, 0xce, 0x61, 0x21, 0xf7, 0xc6, 0x90,
0x5c, 0x70, 0xf7, 0x26, 0xe6, 0x0a, 0xcf, 0x0b, 0x64, 0xf8, 0x1c, 0x28, 0x50, 0xc3, 0xf3, 0x89, 0xe2, 0x82, 0xbb, 0x37, 0x36, 0x57, 0x78, 0x5e, 0x20, 0xc3, 0xe7, 0x41, 0x8e, 0x1a, 0xae, 0x47,
0xf4, 0x7a, 0x29, 0x16, 0xaa, 0x33, 0xe2, 0x49, 0xbb, 0x3c, 0x13, 0xab, 0xe3, 0x04, 0x24, 0x84, 0xa4, 0xd7, 0x0b, 0x91, 0x50, 0x95, 0x11, 0x8f, 0x5b, 0xc5, 0xa9, 0x48, 0x1d, 0x27, 0x20, 0x21,
0xe1, 0x0f, 0x35, 0x00, 0x5a, 0xd8, 0xb6, 0x4c, 0xcc, 0x2f, 0xf6, 0x02, 0x37, 0x7f, 0xb8, 0x61, 0x0c, 0x7f, 0xa4, 0x01, 0xd0, 0xc4, 0x96, 0x59, 0xc3, 0xfc, 0x62, 0xcf, 0x71, 0xf3, 0x07, 0x1b,
0x7d, 0x4b, 0xa9, 0x17, 0x87, 0x96, 0x7c, 0xa3, 0x14, 0x34, 0xfc, 0x40, 0x03, 0xd3, 0x34, 0xda, 0xd6, 0xb7, 0x95, 0x7a, 0x71, 0x68, 0xf1, 0x37, 0x4a, 0x40, 0xc3, 0x0f, 0x35, 0x30, 0x49, 0xc3,
0x0b, 0xe4, 0x2a, 0xca, 0x5b, 0x80, 0xa9, 0xab, 0xdf, 0x1c, 0xaa, 0x2d, 0xf5, 0x14, 0x40, 0x6d, 0x5d, 0x5f, 0xae, 0xa2, 0xbc, 0x05, 0x98, 0xb8, 0xf6, 0xed, 0x81, 0xda, 0x52, 0x4d, 0x00, 0x54,
0xae, 0xd3, 0x2e, 0x4f, 0xa7, 0x29, 0xa8, 0xcb, 0x00, 0xf8, 0x13, 0x0d, 0x14, 0xe5, 0x09, 0x53, 0x66, 0xda, 0xad, 0xe2, 0x64, 0x92, 0x82, 0x3a, 0x0c, 0x80, 0x3f, 0xd5, 0x40, 0x5e, 0x9e, 0x30,
0x7d, 0x82, 0x27, 0xfc, 0x5b, 0x23, 0x3a, 0x58, 0x19, 0x51, 0x49, 0x16, 0x48, 0x02, 0x45, 0xca, 0xd5, 0xc7, 0x78, 0xc2, 0xbf, 0x35, 0xa4, 0x83, 0x95, 0x11, 0x15, 0x67, 0x81, 0x24, 0x50, 0xa4,
0x02, 0xf8, 0x0f, 0x0d, 0xe8, 0xd8, 0x14, 0x05, 0x1e, 0xdb, 0xbb, 0x81, 0xe5, 0x86, 0x24, 0x10, 0x2c, 0x80, 0xff, 0xd0, 0x80, 0x8e, 0x6b, 0xa2, 0xc0, 0x63, 0x6b, 0xc7, 0x37, 0x9d, 0x80, 0xf8,
0x5d, 0x21, 0xd5, 0x8b, 0xdc, 0xbc, 0xe1, 0xde, 0x85, 0xd9, 0x8e, 0xb3, 0xb6, 0x24, 0xad, 0xd3, 0xa2, 0x2b, 0xa4, 0x7a, 0x9e, 0x9b, 0x37, 0xd8, 0xbb, 0x30, 0xdd, 0x71, 0x56, 0x16, 0xa4, 0x75,
0xd7, 0x06, 0x98, 0x81, 0x06, 0x1a, 0xc8, 0x03, 0xcd, 0x50, 0xad, 0x97, 0x3e, 0x39, 0x82, 0x40, 0xfa, 0x4a, 0x1f, 0x33, 0x50, 0x5f, 0x03, 0x79, 0xa0, 0x19, 0xaa, 0xf5, 0xd2, 0xc7, 0x87, 0x10,
0x4b, 0x3a, 0x3b, 0x59, 0x1d, 0x92, 0x76, 0x3b, 0x05, 0x5d, 0xf9, 0x20, 0x9f, 0x6d, 0xad, 0xb3, 0x68, 0x71, 0x67, 0x27, 0xab, 0x43, 0xdc, 0x6e, 0x27, 0xa0, 0x4b, 0x1f, 0x66, 0xd3, 0xad, 0x75,
0x97, 0x3e, 0xbc, 0x2b, 0x8c, 0x15, 0x5b, 0xa1, 0xba, 0xc6, 0x9d, 0xfb, 0xce, 0x88, 0xce, 0x5e, 0xfa, 0xd2, 0x87, 0xf7, 0x84, 0xb1, 0x62, 0x2b, 0x54, 0xd7, 0xb8, 0x73, 0xdf, 0x19, 0xd2, 0xd9,
0xdd, 0xda, 0x49, 0xe3, 0xa5, 0x48, 0x14, 0xa5, 0xec, 0x80, 0xbf, 0xd2, 0xc0, 0x0c, 0x36, 0x0c, 0xab, 0x5b, 0x3b, 0x6e, 0xbc, 0x14, 0x89, 0xa2, 0x84, 0x1d, 0xf0, 0x57, 0x1a, 0x98, 0xc2, 0x86,
0xe2, 0x87, 0xc4, 0x14, 0xb5, 0x38, 0xf7, 0x19, 0x94, 0x9b, 0x05, 0x69, 0xd5, 0xcc, 0x5a, 0x1a, 0x41, 0xbc, 0x80, 0xd4, 0x44, 0x2d, 0xce, 0x7c, 0x0e, 0xe5, 0x66, 0x4e, 0x5a, 0x35, 0xb5, 0x92,
0x1a, 0x75, 0x5b, 0x02, 0x5f, 0x02, 0x67, 0x69, 0xe8, 0x05, 0xc4, 0x8c, 0x23, 0x57, 0xde, 0x13, 0x84, 0x46, 0x9d, 0x96, 0xc0, 0x97, 0xc1, 0x69, 0x1a, 0xb8, 0x3e, 0xa9, 0x45, 0x91, 0x2b, 0xef,
0xb0, 0xd3, 0x2e, 0x9f, 0xad, 0x77, 0x71, 0x50, 0x46, 0xb2, 0xf2, 0x4b, 0x0d, 0x94, 0x1f, 0x90, 0x09, 0xd8, 0x6e, 0x15, 0x4f, 0x57, 0x3b, 0x38, 0x28, 0x25, 0x59, 0xfa, 0x74, 0x04, 0x14, 0x1f,
0x19, 0xa7, 0x98, 0x76, 0x9e, 0x06, 0xe3, 0x7c, 0xbb, 0x26, 0xf7, 0x4a, 0x31, 0xd5, 0xb9, 0x71, 0x91, 0x19, 0x27, 0x78, 0xed, 0x3c, 0x03, 0x46, 0xf9, 0x76, 0x6b, 0xdc, 0x2b, 0xf9, 0x44, 0xe7,
0x2a, 0x92, 0x5c, 0x56, 0xd7, 0x19, 0x3e, 0xeb, 0x36, 0xf2, 0x5c, 0x50, 0xd5, 0xf5, 0xba, 0x20, 0xc6, 0xa9, 0x48, 0x72, 0x59, 0x5d, 0x67, 0xf8, 0xac, 0xdb, 0xc8, 0x72, 0x41, 0x55, 0xd7, 0xab,
0xa3, 0x98, 0x5f, 0xf9, 0x9f, 0x96, 0x0d, 0x95, 0x54, 0xb9, 0xa8, 0x1b, 0xd8, 0x26, 0x70, 0x03, 0x82, 0x8c, 0x22, 0x3e, 0x7c, 0x17, 0x8c, 0x8a, 0x69, 0x06, 0x2f, 0xaa, 0x43, 0x2c, 0x8c, 0x80,
0xcc, 0xb1, 0xbe, 0x14, 0x11, 0xdf, 0xb6, 0x0c, 0x4c, 0xf9, 0xf0, 0x22, 0x6c, 0x54, 0xf3, 0x74, 0xdb, 0xc9, 0xa1, 0x90, 0x84, 0xec, 0x2e, 0x88, 0xb9, 0x27, 0x5d, 0x10, 0x1f, 0x5a, 0x81, 0x46,
0x3d, 0xc3, 0x47, 0x3d, 0x2b, 0xe0, 0xab, 0x00, 0x8a, 0x5e, 0xad, 0x4b, 0x8f, 0xb8, 0x76, 0x54, 0xff, 0xcf, 0x2b, 0x50, 0xe9, 0xbf, 0x5a, 0x3a, 0xef, 0x13, 0x5b, 0xad, 0x1a, 0xd8, 0x22, 0x70,
0xd7, 0x55, 0xef, 0x91, 0x40, 0x7d, 0x56, 0xc1, 0x75, 0x30, 0x6f, 0xe3, 0x3d, 0x62, 0xd7, 0x89, 0x0d, 0xcc, 0xb0, 0x47, 0x06, 0x22, 0x9e, 0x65, 0x1a, 0x98, 0xf2, 0x97, 0xa8, 0x08, 0x38, 0x35,
0x4d, 0x8c, 0xd0, 0x0b, 0xb8, 0x2a, 0x31, 0xde, 0x2d, 0x74, 0xda, 0xe5, 0xf9, 0x6b, 0x59, 0x26, 0x1c, 0xa9, 0xa6, 0xf8, 0xa8, 0x6b, 0x05, 0xbc, 0x0e, 0xa0, 0x68, 0xbc, 0x3b, 0xf4, 0x88, 0x1e,
0xea, 0x95, 0xaf, 0x2c, 0x67, 0x4f, 0x24, 0xbd, 0x71, 0xd1, 0x01, 0xff, 0x2e, 0x07, 0x16, 0x07, 0x42, 0xb5, 0xd0, 0xd5, 0x2e, 0x09, 0xd4, 0x63, 0x15, 0x5c, 0x05, 0xb3, 0x16, 0xde, 0x25, 0x56,
0x57, 0x57, 0xf8, 0x6e, 0xd2, 0xa8, 0x8b, 0x3e, 0xec, 0xad, 0x51, 0x55, 0x72, 0xd9, 0xa9, 0x83, 0x95, 0x58, 0xc4, 0x08, 0x5c, 0x9f, 0xab, 0x12, 0x6f, 0xf5, 0xb9, 0x76, 0xab, 0x38, 0x7b, 0x23,
0xde, 0x2e, 0x1d, 0x7e, 0x8f, 0x5d, 0x8a, 0xd8, 0x8e, 0x07, 0xf8, 0x37, 0x47, 0x66, 0x02, 0x03, 0xcd, 0x44, 0xdd, 0xf2, 0xa5, 0xc5, 0x74, 0x7a, 0x25, 0x37, 0x2e, 0x9e, 0x33, 0xbf, 0xcb, 0x80,
0xa9, 0x4d, 0x8a, 0xfb, 0x16, 0xdb, 0xfc, 0x7a, 0xc5, 0x36, 0xa9, 0xfc, 0x49, 0xcb, 0xce, 0x6a, 0xf9, 0xfe, 0x91, 0x01, 0xdf, 0x8b, 0x5f, 0x5d, 0xa2, 0xa9, 0x7e, 0x6b, 0x58, 0x51, 0x28, 0x9f,
0xc9, 0xed, 0x07, 0x7f, 0xaa, 0x81, 0x59, 0xcf, 0x27, 0xee, 0xda, 0xee, 0xd6, 0xad, 0x2f, 0xd5, 0x5d, 0xa0, 0xfb, 0xc9, 0x05, 0xbf, 0xcf, 0x3a, 0x1c, 0x6c, 0x45, 0xd3, 0x98, 0x37, 0x87, 0x66,
0xf9, 0xab, 0x99, 0x74, 0xd5, 0xf5, 0x87, 0xb4, 0x93, 0x8d, 0xd1, 0x42, 0xe1, 0x6e, 0xe0, 0xf9, 0x02, 0x03, 0xa9, 0x8c, 0x8b, 0xe6, 0x09, 0x5b, 0xbc, 0x57, 0xc2, 0x16, 0x29, 0xfd, 0x49, 0x4b,
0xb4, 0x76, 0xae, 0xd3, 0x2e, 0xcf, 0xee, 0x74, 0x43, 0xa1, 0x2c, 0x76, 0xc5, 0x01, 0x0b, 0x9b, 0x3f, 0xbc, 0xe3, 0x0c, 0x86, 0x3f, 0xd3, 0xc0, 0xb4, 0xeb, 0x11, 0x67, 0x65, 0x67, 0xe3, 0xf6,
0x47, 0x21, 0x09, 0x5c, 0x6c, 0x6f, 0x78, 0x46, 0xe4, 0x10, 0x37, 0x14, 0x86, 0x66, 0xa6, 0x7f, 0x57, 0x44, 0x26, 0x4b, 0x57, 0x6d, 0x3d, 0xa6, 0x9d, 0xd7, 0xab, 0xdb, 0x5b, 0x42, 0xe1, 0x8e,
0xed, 0x94, 0xd3, 0xff, 0x13, 0x20, 0x1f, 0x05, 0xb6, 0x8c, 0xe2, 0x29, 0xf5, 0xba, 0x85, 0xae, 0xef, 0x7a, 0xb4, 0x72, 0xa6, 0xdd, 0x2a, 0x4e, 0x6f, 0x77, 0x42, 0xa1, 0x34, 0x76, 0xc9, 0x06,
0x21, 0x46, 0xaf, 0x2c, 0x83, 0x31, 0x66, 0x27, 0xbc, 0x00, 0xf2, 0x01, 0x3e, 0xe4, 0x5a, 0xa7, 0x73, 0xeb, 0x87, 0x01, 0xf1, 0x1d, 0x6c, 0xad, 0xb9, 0x46, 0x68, 0x13, 0x27, 0x10, 0x86, 0xa6,
0x6b, 0x13, 0x4c, 0x04, 0xe1, 0x43, 0xc4, 0x68, 0x95, 0x3f, 0x5f, 0x04, 0xb3, 0x99, 0xbd, 0xc0, 0x46, 0x39, 0xda, 0x09, 0x47, 0x39, 0x17, 0x41, 0x36, 0xf4, 0x2d, 0x19, 0xc5, 0x13, 0x6a, 0x54,
0x45, 0x90, 0x53, 0x4f, 0x66, 0x40, 0x2a, 0xcd, 0x6d, 0x6d, 0xa0, 0x9c, 0x65, 0xc2, 0x17, 0xc0, 0x89, 0x6e, 0x20, 0x46, 0x2f, 0x2d, 0x82, 0x11, 0x66, 0x27, 0x3c, 0x0f, 0xb2, 0x3e, 0x3e, 0xe0,
0xb8, 0x78, 0x7d, 0x94, 0xa0, 0x65, 0x55, 0x02, 0x38, 0x95, 0x75, 0x41, 0x89, 0x3a, 0x66, 0x88, 0x5a, 0x27, 0x2b, 0x63, 0x4c, 0x04, 0xe1, 0x03, 0xc4, 0x68, 0xa5, 0x3f, 0x5f, 0x00, 0xd3, 0xa9,
0x14, 0xe7, 0x36, 0x90, 0x86, 0xcc, 0x12, 0x61, 0x03, 0x69, 0x20, 0x46, 0xfb, 0xb4, 0x4f, 0x1f, 0xbd, 0xc0, 0x79, 0x90, 0x51, 0xf3, 0x4f, 0x20, 0x95, 0x66, 0x36, 0xd6, 0x50, 0xc6, 0xac, 0xc1,
0xf1, 0xdb, 0x4b, 0xe1, 0x14, 0x6f, 0x2f, 0xe3, 0x9f, 0xf8, 0xf6, 0xf2, 0x24, 0x28, 0x84, 0x56, 0x17, 0x55, 0xf1, 0x15, 0xa0, 0x45, 0x55, 0xcf, 0x39, 0x95, 0xb5, 0xb4, 0xb1, 0x3a, 0x66, 0x48,
0x68, 0x13, 0x7d, 0xa2, 0xbb, 0x59, 0xbd, 0xc1, 0x88, 0x48, 0xf0, 0xe0, 0x6d, 0x30, 0x61, 0x92, 0x54, 0x38, 0x99, 0x0d, 0xa4, 0x2e, 0xb3, 0x44, 0xd8, 0x40, 0xea, 0x88, 0xd1, 0x3e, 0xeb, 0x1c,
0x06, 0x8e, 0xec, 0x50, 0x2f, 0xf2, 0x10, 0x5a, 0x1f, 0x42, 0x08, 0x89, 0x87, 0xb1, 0x0d, 0xa1, 0x2b, 0x1a, 0xa4, 0xe5, 0x4e, 0x30, 0x48, 0x1b, 0x7d, 0xe8, 0x20, 0xed, 0x12, 0xc8, 0x05, 0x66,
0x17, 0xc5, 0x00, 0xf0, 0x29, 0x30, 0xe1, 0xe0, 0x23, 0xcb, 0x89, 0x1c, 0x7e, 0x8d, 0x6b, 0x42, 0x60, 0x11, 0x7d, 0xac, 0xf3, 0xe5, 0x71, 0x93, 0x11, 0x91, 0xe0, 0xc1, 0x3b, 0x60, 0xac, 0x46,
0x6c, 0x5b, 0x90, 0x50, 0xcc, 0x63, 0x95, 0x91, 0x1c, 0x19, 0x76, 0x44, 0xad, 0x16, 0x91, 0x4c, 0xea, 0x38, 0xb4, 0x02, 0x3d, 0xcf, 0x43, 0x68, 0x75, 0x00, 0x21, 0x24, 0xa6, 0x9c, 0x6b, 0x42,
0x1d, 0xf0, 0x82, 0xab, 0x2a, 0xe3, 0x66, 0x86, 0x8f, 0x7a, 0x56, 0x70, 0x30, 0xcb, 0xe5, 0x8b, 0x2f, 0x8a, 0x00, 0xe0, 0xd3, 0x60, 0xcc, 0xc6, 0x87, 0xa6, 0x1d, 0xda, 0xbc, 0x27, 0xd3, 0x84,
0xa7, 0x52, 0x60, 0x82, 0x84, 0x62, 0x5e, 0x37, 0x98, 0x94, 0x9f, 0x1e, 0x04, 0x26, 0x17, 0xf7, 0xd8, 0xa6, 0x20, 0xa1, 0x88, 0xc7, 0x2a, 0x23, 0x39, 0x34, 0xac, 0x90, 0x9a, 0x4d, 0x22, 0x99,
0xac, 0x80, 0x5f, 0x04, 0x93, 0x0e, 0x3e, 0xba, 0x46, 0xdc, 0x66, 0xb8, 0xaf, 0xcf, 0x2c, 0x69, 0x3a, 0xe0, 0xb7, 0xa7, 0xaa, 0x8c, 0xeb, 0x29, 0x3e, 0xea, 0x5a, 0xc1, 0xc1, 0x4c, 0x87, 0x2f,
0x2b, 0xf9, 0xda, 0x4c, 0xa7, 0x5d, 0x9e, 0xdc, 0x8e, 0x89, 0x28, 0xe1, 0x73, 0x61, 0xcb, 0x95, 0x9e, 0x48, 0x80, 0x09, 0x12, 0x8a, 0x78, 0x9d, 0x60, 0x52, 0x7e, 0xb2, 0x1f, 0x98, 0x5c, 0xdc,
0xc2, 0x67, 0x53, 0xc2, 0x31, 0x11, 0x25, 0x7c, 0x76, 0xe9, 0xf8, 0x38, 0x64, 0xc9, 0xa5, 0xcf, 0xb5, 0x02, 0x7e, 0x19, 0x8c, 0xdb, 0xf8, 0xf0, 0x06, 0x71, 0x1a, 0xc1, 0x9e, 0x3e, 0xb5, 0xa0,
0x76, 0x0f, 0x13, 0xbb, 0x82, 0x8c, 0x62, 0x3e, 0x5c, 0x01, 0x45, 0x07, 0x1f, 0xf1, 0xc1, 0x4f, 0x2d, 0x65, 0x2b, 0x53, 0xed, 0x56, 0x71, 0x7c, 0x33, 0x22, 0xa2, 0x98, 0xcf, 0x85, 0x4d, 0x47,
0x9f, 0xe3, 0x6a, 0xf9, 0x23, 0xe1, 0xb6, 0xa4, 0x21, 0xc5, 0xe5, 0x92, 0x96, 0x2b, 0x24, 0xe7, 0x0a, 0x9f, 0x4e, 0x08, 0x47, 0x44, 0x14, 0xf3, 0x59, 0x07, 0xe1, 0xe1, 0x80, 0x25, 0x97, 0x3e,
0x53, 0x92, 0x92, 0x86, 0x14, 0x97, 0x05, 0x71, 0xe4, 0x5a, 0x77, 0x22, 0x22, 0x84, 0x21, 0xf7, 0xdd, 0xf9, 0x32, 0xdc, 0x11, 0x64, 0x14, 0xf1, 0xe1, 0x12, 0xc8, 0xdb, 0xf8, 0x90, 0xbf, 0xe2,
0x8c, 0x0a, 0xe2, 0x9b, 0x09, 0x0b, 0xa5, 0xe5, 0xd8, 0xe0, 0xe5, 0x44, 0x76, 0x68, 0xf9, 0x36, 0xf5, 0x19, 0xae, 0x96, 0x4f, 0x7c, 0x37, 0x25, 0x0d, 0x29, 0x2e, 0x97, 0x34, 0x1d, 0x21, 0x39,
0xd9, 0x69, 0xe8, 0xe7, 0xb8, 0xff, 0x79, 0x6b, 0xb5, 0xad, 0xa8, 0x28, 0x25, 0x01, 0x09, 0x18, 0x9b, 0x90, 0x94, 0x34, 0xa4, 0xb8, 0x2c, 0x88, 0x43, 0xc7, 0xbc, 0x1b, 0x12, 0x21, 0x0c, 0xb9,
0x23, 0x6e, 0xe4, 0xe8, 0x8f, 0xf1, 0x86, 0x69, 0x28, 0x21, 0xa8, 0x32, 0x67, 0xd3, 0x8d, 0x1c, 0x67, 0x54, 0x10, 0xdf, 0x8a, 0x59, 0x28, 0x29, 0xc7, 0x5e, 0xd1, 0x76, 0x68, 0x05, 0xa6, 0x67,
0xc4, 0xd5, 0xc3, 0x17, 0xc0, 0x8c, 0x83, 0x8f, 0x58, 0x39, 0x20, 0x41, 0xc8, 0x46, 0xc2, 0x05, 0x91, 0xed, 0xba, 0x7e, 0x86, 0xfb, 0x9f, 0xf7, 0xc9, 0x9b, 0x8a, 0x8a, 0x12, 0x12, 0x90, 0x80,
0xbe, 0xf9, 0x79, 0xd6, 0xa4, 0x6c, 0xa7, 0x19, 0xa8, 0x5b, 0x8e, 0x2f, 0xb4, 0xdc, 0xd4, 0xc2, 0x11, 0xe2, 0x84, 0xb6, 0x7e, 0x96, 0x5f, 0xec, 0x03, 0x09, 0x41, 0x95, 0x39, 0xeb, 0x4e, 0x68,
0xf3, 0xa9, 0x85, 0x69, 0x06, 0xea, 0x96, 0x63, 0x9e, 0x0e, 0xc8, 0x9d, 0xc8, 0x0a, 0x88, 0xa9, 0x23, 0xae, 0x1e, 0xbe, 0x08, 0xa6, 0x6c, 0x7c, 0xc8, 0xca, 0x01, 0xf1, 0x03, 0xf6, 0xbe, 0x9f,
0x3f, 0xce, 0xfb, 0x1a, 0xf9, 0x70, 0x2b, 0x68, 0x48, 0x71, 0x61, 0x2b, 0x7e, 0x21, 0xd0, 0x79, 0xe3, 0x9b, 0x9f, 0x65, 0x1d, 0xe7, 0x66, 0x92, 0x81, 0x3a, 0xe5, 0xf8, 0x42, 0xd3, 0x49, 0x2c,
0x1a, 0xde, 0x1c, 0x6e, 0x25, 0xdf, 0x09, 0xd6, 0x82, 0x00, 0x1f, 0x8b, 0x9b, 0x26, 0xfd, 0x36, 0x3c, 0x97, 0x58, 0x98, 0x64, 0xa0, 0x4e, 0x39, 0xe6, 0x69, 0x9f, 0xdc, 0x0d, 0x4d, 0x9f, 0xd4,
0x00, 0x29, 0x28, 0x60, 0xdb, 0xde, 0x69, 0xe8, 0x17, 0xb8, 0xef, 0x87, 0x7d, 0x83, 0xa8, 0xaa, 0xf4, 0xa7, 0x78, 0x93, 0x2a, 0xa7, 0xf0, 0x82, 0x86, 0x14, 0x17, 0x36, 0xa3, 0x71, 0x8f, 0xce,
0xb3, 0xc6, 0x40, 0x90, 0xc0, 0x62, 0xa0, 0x9e, 0xcb, 0x42, 0x63, 0x71, 0xb4, 0xa0, 0x3b, 0x0c, 0xd3, 0xf0, 0xd6, 0x60, 0x2b, 0xf9, 0xb6, 0xbf, 0xe2, 0xfb, 0xf8, 0x48, 0xdc, 0x34, 0xc9, 0x41,
0x04, 0x09, 0x2c, 0xbe, 0x53, 0xf7, 0x78, 0xa7, 0xa1, 0x7f, 0x6e, 0xc4, 0x3b, 0x65, 0x20, 0x48, 0x0f, 0xa4, 0x20, 0x87, 0x2d, 0x6b, 0xbb, 0xae, 0x9f, 0xe7, 0xbe, 0x1f, 0xf4, 0x0d, 0xa2, 0xaa,
0x60, 0x41, 0x0b, 0xe4, 0x5d, 0x2f, 0xd4, 0x2f, 0x8e, 0xe4, 0x7a, 0xe6, 0x17, 0xce, 0x75, 0x2f, 0xce, 0x0a, 0x03, 0x41, 0x02, 0x8b, 0x81, 0xba, 0x0e, 0x0b, 0x8d, 0xf9, 0xe1, 0x82, 0x6e, 0x33,
0x44, 0x0c, 0x03, 0xfe, 0x42, 0x03, 0xc0, 0x4f, 0x42, 0xf4, 0x89, 0xa1, 0x0c, 0x9e, 0x19, 0xc8, 0x10, 0x24, 0xb0, 0xf8, 0x4e, 0x9d, 0xa3, 0xed, 0xba, 0xfe, 0x85, 0x21, 0xef, 0x94, 0x81, 0x20,
0x6a, 0x12, 0xdb, 0x9b, 0x6e, 0x18, 0x1c, 0x27, 0xa3, 0x47, 0x2a, 0x07, 0x52, 0x56, 0xc0, 0xdf, 0x81, 0x05, 0x4d, 0x90, 0x75, 0xdc, 0x40, 0xbf, 0x30, 0x94, 0xeb, 0x99, 0x5f, 0x38, 0x5b, 0x6e,
0x6b, 0xe0, 0xb1, 0xf4, 0x6c, 0xa7, 0xcc, 0x2b, 0x71, 0x8f, 0xdc, 0x18, 0x76, 0x98, 0xd7, 0x3c, 0x80, 0x18, 0x06, 0xfc, 0xa5, 0x06, 0x80, 0x17, 0x87, 0xe8, 0xc5, 0x81, 0x4c, 0x11, 0x52, 0x90,
0xcf, 0xae, 0xe9, 0x9d, 0x76, 0xf9, 0xb1, 0xb5, 0x3e, 0xa8, 0xa8, 0xaf, 0x2d, 0xf0, 0x2f, 0x1a, 0xe5, 0x38, 0xb6, 0xd7, 0x9d, 0xc0, 0x3f, 0x8a, 0xdf, 0x91, 0x89, 0x1c, 0x48, 0x58, 0x01, 0x7f,
0x98, 0x97, 0x55, 0x34, 0x65, 0x61, 0x99, 0x3b, 0x90, 0x0c, 0xdb, 0x81, 0x59, 0x1c, 0xe1, 0x47, 0xaf, 0x81, 0xb3, 0xc9, 0x36, 0x59, 0x99, 0x57, 0xe0, 0x1e, 0xb9, 0x39, 0xe8, 0x30, 0xaf, 0xb8,
0xf5, 0x83, 0x63, 0x0f, 0x1f, 0xf5, 0x9a, 0x06, 0xff, 0xae, 0x81, 0x69, 0x93, 0xf8, 0xc4, 0x35, 0xae, 0x55, 0xd1, 0xdb, 0xad, 0xe2, 0xd9, 0x95, 0x1e, 0xa8, 0xa8, 0xa7, 0x2d, 0xf0, 0x2f, 0x1a,
0x89, 0x6b, 0x30, 0x5b, 0x97, 0x86, 0x32, 0x69, 0x66, 0x6d, 0xdd, 0x48, 0x41, 0x08, 0x33, 0xab, 0x98, 0x95, 0x55, 0x34, 0x61, 0x61, 0x91, 0x3b, 0x90, 0x0c, 0xda, 0x81, 0x69, 0x1c, 0xe1, 0x47,
0xd2, 0xcc, 0xe9, 0x34, 0xeb, 0xa4, 0x5d, 0x3e, 0x9f, 0x2c, 0x4d, 0x73, 0x50, 0x97, 0x95, 0xf0, 0xf5, 0xeb, 0x71, 0x17, 0x1f, 0x75, 0x9b, 0x06, 0xff, 0xae, 0x81, 0xc9, 0x1a, 0xf1, 0x88, 0x53,
0x43, 0x0d, 0xcc, 0x26, 0x07, 0x20, 0xae, 0x94, 0xe5, 0x11, 0xc6, 0x01, 0x6f, 0x5f, 0xd7, 0xba, 0x23, 0x8e, 0xc1, 0x6c, 0x5d, 0x18, 0xc8, 0xd8, 0x20, 0x6d, 0xeb, 0x5a, 0x02, 0x42, 0x98, 0x59,
0x01, 0x51, 0xd6, 0x02, 0xf8, 0x57, 0x8d, 0x75, 0x6a, 0xf1, 0xdc, 0x48, 0xf5, 0x0a, 0xf7, 0xe5, 0x96, 0x66, 0x4e, 0x26, 0x59, 0xc7, 0xad, 0xe2, 0xb9, 0x78, 0x69, 0x92, 0x83, 0x3a, 0xac, 0x84,
0xdb, 0x43, 0xf7, 0xa5, 0x42, 0x10, 0xae, 0xbc, 0x94, 0xb4, 0x82, 0x8a, 0x73, 0xd2, 0x2e, 0x2f, 0x1f, 0x69, 0x60, 0x3a, 0x3e, 0x00, 0x71, 0xa5, 0x2c, 0x0e, 0x31, 0x0e, 0x78, 0xfb, 0xba, 0xd2,
0xa4, 0x3d, 0xa9, 0x18, 0x28, 0x6d, 0x21, 0xfc, 0xb1, 0x06, 0xa6, 0x49, 0xd2, 0x71, 0x53, 0xfd, 0x09, 0x88, 0xd2, 0x16, 0xc0, 0xbf, 0x6a, 0xac, 0x53, 0x8b, 0xde, 0x7d, 0x54, 0x2f, 0x71, 0x5f,
0xc9, 0xa1, 0x38, 0xb1, 0x6f, 0x13, 0x2f, 0x5e, 0xbb, 0x52, 0x2c, 0x8a, 0xba, 0xb0, 0x59, 0x07, 0xbe, 0x3d, 0x70, 0x5f, 0x2a, 0x04, 0xe1, 0xca, 0xcb, 0x71, 0x2b, 0xa8, 0x38, 0xc7, 0xad, 0xe2,
0x49, 0x8e, 0xb0, 0xe3, 0xdb, 0x44, 0xff, 0xfc, 0x90, 0x3b, 0xc8, 0x4d, 0xa1, 0x17, 0xc5, 0x00, 0x5c, 0xd2, 0x93, 0x8a, 0x81, 0x92, 0x16, 0xc2, 0x9f, 0x68, 0x60, 0x92, 0xc4, 0x1d, 0x37, 0xd5,
0x8b, 0x6c, 0xf2, 0xc9, 0x64, 0x0e, 0x9c, 0x03, 0xf9, 0x03, 0x22, 0x7f, 0xb9, 0x43, 0xec, 0x4f, 0x2f, 0x0d, 0xc4, 0x89, 0x3d, 0x9b, 0x78, 0xf1, 0x52, 0x4f, 0xb0, 0x28, 0xea, 0xc0, 0x66, 0x1d,
0x68, 0x82, 0x42, 0x0b, 0xdb, 0x51, 0x3c, 0xbc, 0x0d, 0xb9, 0xea, 0x22, 0xa1, 0xfc, 0xa5, 0xdc, 0x24, 0x39, 0xc4, 0xb6, 0x67, 0x11, 0xfd, 0x8b, 0x03, 0xee, 0x20, 0xd7, 0x85, 0x5e, 0x14, 0x01,
0x8b, 0xda, 0xe2, 0x5d, 0x0d, 0x9c, 0xef, 0x9f, 0xd0, 0x8f, 0xd4, 0xac, 0xdf, 0x68, 0x60, 0xbe, 0xcc, 0xb3, 0x97, 0x4f, 0x2a, 0x73, 0xe0, 0x0c, 0xc8, 0xee, 0x13, 0xf9, 0x33, 0x2c, 0x62, 0x7f,
0x27, 0x77, 0xfb, 0x58, 0x74, 0xa7, 0xdb, 0xa2, 0xd7, 0x87, 0x9d, 0x84, 0xf5, 0x30, 0xb0, 0xdc, 0xc2, 0x1a, 0xc8, 0x35, 0xb1, 0x15, 0x46, 0x8f, 0xb7, 0x01, 0x57, 0x5d, 0x24, 0x94, 0xbf, 0x9c,
0x26, 0xef, 0x3c, 0xd2, 0xe6, 0xfd, 0x4c, 0x03, 0x73, 0xd9, 0x74, 0x78, 0x94, 0xfe, 0xaa, 0xdc, 0x79, 0x49, 0x9b, 0xbf, 0xa7, 0x81, 0x73, 0xbd, 0x13, 0xfa, 0x89, 0x9a, 0xf5, 0x1b, 0x0d, 0xcc,
0xcd, 0x81, 0xf3, 0xfd, 0x1b, 0x26, 0x18, 0xa8, 0xc9, 0x70, 0x34, 0x13, 0x36, 0x48, 0xa6, 0x4c, 0x76, 0xe5, 0x6e, 0x0f, 0x8b, 0xee, 0x76, 0x5a, 0xf4, 0xfa, 0xa0, 0x93, 0xb0, 0x1a, 0xf8, 0xa6,
0x35, 0x54, 0xbe, 0xaf, 0x81, 0xa9, 0xdb, 0x4a, 0x2e, 0xfe, 0xcd, 0x68, 0xe8, 0xb3, 0x7d, 0x5c, 0xd3, 0xe0, 0x9d, 0x47, 0xd2, 0xbc, 0x9f, 0x6b, 0x60, 0x26, 0x9d, 0x0e, 0x4f, 0xd2, 0x5f, 0xa5,
0x7f, 0x12, 0x06, 0x45, 0x69, 0xdc, 0xca, 0xdf, 0x34, 0xb0, 0xd0, 0xb7, 0xb0, 0xb2, 0x11, 0x14, 0x7b, 0x19, 0x70, 0xae, 0x77, 0xc3, 0x04, 0x7d, 0xf5, 0x32, 0x1c, 0xce, 0x0b, 0xbb, 0xd7, 0x34,
0xdb, 0xb6, 0x77, 0x28, 0x9e, 0x68, 0x52, 0x4f, 0x66, 0x6b, 0x9c, 0x8a, 0x24, 0x37, 0xe5, 0xbd, 0xee, 0x03, 0x0d, 0x4c, 0xdc, 0x51, 0x72, 0xd1, 0x0f, 0x80, 0x03, 0x7f, 0xdb, 0x47, 0xf5, 0x27,
0xdc, 0x67, 0xe5, 0xbd, 0xca, 0x3f, 0x35, 0x70, 0xf1, 0x93, 0x22, 0xf1, 0x91, 0x1c, 0xe9, 0x0a, 0x66, 0x50, 0x94, 0xc4, 0x2d, 0xfd, 0x4d, 0x03, 0x73, 0x3d, 0x0b, 0x2b, 0x7b, 0x82, 0x62, 0xcb,
0x28, 0xca, 0xa6, 0xe8, 0x98, 0x1f, 0xa7, 0x9c, 0x03, 0x64, 0xd1, 0xe0, 0xff, 0xcc, 0x20, 0xfe, 0x72, 0x0f, 0xc4, 0x88, 0x26, 0x31, 0xff, 0x5c, 0xe1, 0x54, 0x24, 0xb9, 0x09, 0xef, 0x65, 0x3e,
0xaa, 0xbc, 0xa7, 0x81, 0xb9, 0x3a, 0x09, 0x5a, 0x96, 0x41, 0x10, 0x69, 0x90, 0x80, 0xb8, 0x06, 0x2f, 0xef, 0x95, 0xfe, 0xa9, 0x81, 0x0b, 0x0f, 0x8b, 0xc4, 0x27, 0x72, 0xa4, 0x4b, 0x20, 0x2f,
0x81, 0xab, 0x60, 0x92, 0xff, 0x58, 0xe3, 0x63, 0x23, 0x7e, 0xc9, 0x9c, 0x97, 0x2e, 0x9f, 0xbc, 0x9b, 0xa2, 0x23, 0x7e, 0x9c, 0xf2, 0x1d, 0x20, 0x8b, 0x06, 0xff, 0xcf, 0x14, 0xf1, 0x57, 0xe9,
0x1e, 0x33, 0x50, 0x22, 0xa3, 0x5e, 0x3d, 0x73, 0x03, 0x5f, 0x3d, 0x2f, 0x82, 0x31, 0x3f, 0x79, 0x7d, 0x0d, 0xcc, 0x54, 0x89, 0xdf, 0x34, 0x0d, 0x82, 0x48, 0x9d, 0xf8, 0xc4, 0x31, 0x08, 0x5c,
0xe0, 0x2b, 0x32, 0x2e, 0x7f, 0xd3, 0xe3, 0xd4, 0xca, 0xbf, 0x34, 0xd0, 0xef, 0x1f, 0x0b, 0x60, 0x06, 0xe3, 0xfc, 0x97, 0x37, 0x0f, 0x1b, 0xd1, 0x58, 0x7a, 0x56, 0xba, 0x7c, 0x7c, 0x2b, 0x62,
0x0b, 0x4c, 0x50, 0x61, 0x9c, 0x74, 0xde, 0xce, 0x43, 0x3a, 0x2f, 0xbb, 0x55, 0x51, 0xf8, 0x63, 0xa0, 0x58, 0x46, 0x8d, 0xb0, 0x33, 0x7d, 0x47, 0xd8, 0x17, 0xc0, 0x88, 0x17, 0x0f, 0xf8, 0xf2,
0x6a, 0x0c, 0xc6, 0xfc, 0x67, 0xe0, 0x5a, 0xe4, 0x9a, 0xf2, 0x49, 0x6e, 0x5a, 0xf8, 0x6f, 0x7d, 0x8c, 0xcb, 0x67, 0x7a, 0x9c, 0x5a, 0xfa, 0x97, 0x06, 0x7a, 0xfd, 0x97, 0x08, 0x6c, 0x82, 0x31,
0x4d, 0xd0, 0x90, 0xe2, 0xc2, 0x0b, 0xe2, 0xf1, 0x28, 0xf5, 0x22, 0x13, 0x3f, 0x1c, 0xd5, 0x2e, 0x2a, 0x8c, 0x93, 0xce, 0xdb, 0x7e, 0x4c, 0xe7, 0xa5, 0xb7, 0x2a, 0x0a, 0x7f, 0x44, 0x8d, 0xc0,
0xdf, 0xbb, 0x5f, 0x3a, 0xf3, 0xd1, 0xfd, 0xd2, 0x99, 0x8f, 0xef, 0x97, 0xce, 0x7c, 0xbf, 0x53, 0x98, 0xff, 0x0c, 0x5c, 0x09, 0x9d, 0x9a, 0x1c, 0xc9, 0x4d, 0x0a, 0xff, 0xad, 0xae, 0x08, 0x1a,
0xd2, 0xee, 0x75, 0x4a, 0xda, 0x47, 0x9d, 0x92, 0xf6, 0x71, 0xa7, 0xa4, 0xfd, 0xbb, 0x53, 0xd2, 0x52, 0x5c, 0x78, 0x5e, 0x0c, 0x8f, 0x12, 0x13, 0x99, 0x68, 0x70, 0x54, 0xb9, 0x72, 0xff, 0x41,
0x7e, 0xfe, 0x9f, 0xd2, 0x99, 0x6f, 0x4d, 0x48, 0xd3, 0xfe, 0x1f, 0x00, 0x00, 0xff, 0xff, 0xc9, 0xe1, 0xd4, 0xc7, 0x0f, 0x0a, 0xa7, 0x3e, 0x79, 0x50, 0x38, 0xf5, 0x83, 0x76, 0x41, 0xbb, 0xdf,
0x4a, 0x8d, 0x8a, 0xee, 0x27, 0x00, 0x00, 0x2e, 0x68, 0x1f, 0xb7, 0x0b, 0xda, 0x27, 0xed, 0x82, 0xf6, 0xef, 0x76, 0x41, 0xfb, 0xc5, 0xa7,
0x85, 0x53, 0xdf, 0x19, 0x93, 0xa6, 0xfd, 0x2f, 0x00, 0x00, 0xff, 0xff, 0xe7, 0xc5, 0xe4, 0x3a,
0xbb, 0x29, 0x00, 0x00,
} }
...@@ -203,10 +203,14 @@ message CustomResourceDefinitionSpec { ...@@ -203,10 +203,14 @@ message CustomResourceDefinitionSpec {
optional string scope = 4; optional string scope = 4;
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
// Optional, the global validation schema for all versions.
// Top-level and per-version schemas are mutually exclusive.
// +optional // +optional
optional CustomResourceValidation validation = 5; optional CustomResourceValidation validation = 5;
// Subresources describes the subresources for CustomResources // Subresources describes the subresources for CustomResource
// Optional, the global subresources for all versions.
// Top-level and per-version subresources are mutually exclusive.
// +optional // +optional
optional CustomResourceSubresources subresources = 6; optional CustomResourceSubresources subresources = 6;
...@@ -225,6 +229,8 @@ message CustomResourceDefinitionSpec { ...@@ -225,6 +229,8 @@ message CustomResourceDefinitionSpec {
repeated CustomResourceDefinitionVersion versions = 7; repeated CustomResourceDefinitionVersion versions = 7;
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column. // AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
// Optional, the global columns for all versions.
// Top-level and per-version columns are mutually exclusive.
// +optional // +optional
repeated CustomResourceColumnDefinition additionalPrinterColumns = 8; repeated CustomResourceColumnDefinition additionalPrinterColumns = 8;
...@@ -262,6 +268,30 @@ message CustomResourceDefinitionVersion { ...@@ -262,6 +268,30 @@ message CustomResourceDefinitionVersion {
// Storage flags the version as storage version. There must be exactly one // Storage flags the version as storage version. There must be exactly one
// flagged as storage version. // flagged as storage version.
optional bool storage = 3; optional bool storage = 3;
// Schema describes the schema for CustomResource used in validation, pruning, and defaulting.
// Top-level and per-version schemas are mutually exclusive.
// Per-version schemas must not all be set to identical values (top-level validation schema should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
optional CustomResourceValidation schema = 4;
// Subresources describes the subresources for CustomResource
// Top-level and per-version subresources are mutually exclusive.
// Per-version subresources must not all be set to identical values (top-level subresources should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
optional CustomResourceSubresources subresources = 5;
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
// Top-level and per-version columns are mutually exclusive.
// Per-version columns must not all be set to identical values (top-level columns should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// NOTE: CRDs created prior to 1.13 populated the top-level additionalPrinterColumns field by default. To apply an
// update that changes to per-version additionalPrinterColumns, the top-level additionalPrinterColumns field must
// be explicitly set to null
// +optional
repeated CustomResourceColumnDefinition additionalPrinterColumns = 6;
} }
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources. // CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
......
...@@ -47,9 +47,13 @@ type CustomResourceDefinitionSpec struct { ...@@ -47,9 +47,13 @@ type CustomResourceDefinitionSpec struct {
// Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced // Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced
Scope ResourceScope `json:"scope" protobuf:"bytes,4,opt,name=scope,casttype=ResourceScope"` Scope ResourceScope `json:"scope" protobuf:"bytes,4,opt,name=scope,casttype=ResourceScope"`
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
// Optional, the global validation schema for all versions.
// Top-level and per-version schemas are mutually exclusive.
// +optional // +optional
Validation *CustomResourceValidation `json:"validation,omitempty" protobuf:"bytes,5,opt,name=validation"` Validation *CustomResourceValidation `json:"validation,omitempty" protobuf:"bytes,5,opt,name=validation"`
// Subresources describes the subresources for CustomResources // Subresources describes the subresources for CustomResource
// Optional, the global subresources for all versions.
// Top-level and per-version subresources are mutually exclusive.
// +optional // +optional
Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,6,opt,name=subresources"` Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,6,opt,name=subresources"`
// Versions is the list of all supported versions for this resource. // Versions is the list of all supported versions for this resource.
...@@ -66,6 +70,8 @@ type CustomResourceDefinitionSpec struct { ...@@ -66,6 +70,8 @@ type CustomResourceDefinitionSpec struct {
// +optional // +optional
Versions []CustomResourceDefinitionVersion `json:"versions,omitempty" protobuf:"bytes,7,rep,name=versions"` Versions []CustomResourceDefinitionVersion `json:"versions,omitempty" protobuf:"bytes,7,rep,name=versions"`
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column. // AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
// Optional, the global columns for all versions.
// Top-level and per-version columns are mutually exclusive.
// +optional // +optional
AdditionalPrinterColumns []CustomResourceColumnDefinition `json:"additionalPrinterColumns,omitempty" protobuf:"bytes,8,rep,name=additionalPrinterColumns"` AdditionalPrinterColumns []CustomResourceColumnDefinition `json:"additionalPrinterColumns,omitempty" protobuf:"bytes,8,rep,name=additionalPrinterColumns"`
...@@ -159,6 +165,27 @@ type CustomResourceDefinitionVersion struct { ...@@ -159,6 +165,27 @@ type CustomResourceDefinitionVersion struct {
// Storage flags the version as storage version. There must be exactly one // Storage flags the version as storage version. There must be exactly one
// flagged as storage version. // flagged as storage version.
Storage bool `json:"storage" protobuf:"varint,3,opt,name=storage"` Storage bool `json:"storage" protobuf:"varint,3,opt,name=storage"`
// Schema describes the schema for CustomResource used in validation, pruning, and defaulting.
// Top-level and per-version schemas are mutually exclusive.
// Per-version schemas must not all be set to identical values (top-level validation schema should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
Schema *CustomResourceValidation `json:"schema,omitempty" protobuf:"bytes,4,opt,name=schema"`
// Subresources describes the subresources for CustomResource
// Top-level and per-version subresources are mutually exclusive.
// Per-version subresources must not all be set to identical values (top-level subresources should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,5,opt,name=subresources"`
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
// Top-level and per-version columns are mutually exclusive.
// Per-version columns must not all be set to identical values (top-level columns should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// NOTE: CRDs created prior to 1.13 populated the top-level additionalPrinterColumns field by default. To apply an
// update that changes to per-version additionalPrinterColumns, the top-level additionalPrinterColumns field must
// be explicitly set to null
// +optional
AdditionalPrinterColumns []CustomResourceColumnDefinition `json:"additionalPrinterColumns,omitempty" protobuf:"bytes,6,rep,name=additionalPrinterColumns"`
} }
// CustomResourceColumnDefinition specifies a column for server side printing. // CustomResourceColumnDefinition specifies a column for server side printing.
......
...@@ -464,7 +464,17 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes ...@@ -464,7 +464,17 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes
out.Validation = nil out.Validation = nil
} }
out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources)) out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
out.Versions = *(*[]apiextensions.CustomResourceDefinitionVersion)(unsafe.Pointer(&in.Versions)) if in.Versions != nil {
in, out := &in.Versions, &out.Versions
*out = make([]apiextensions.CustomResourceDefinitionVersion, len(*in))
for i := range *in {
if err := Convert_v1beta1_CustomResourceDefinitionVersion_To_apiextensions_CustomResourceDefinitionVersion(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Versions = nil
}
out.AdditionalPrinterColumns = *(*[]apiextensions.CustomResourceColumnDefinition)(unsafe.Pointer(&in.AdditionalPrinterColumns)) out.AdditionalPrinterColumns = *(*[]apiextensions.CustomResourceColumnDefinition)(unsafe.Pointer(&in.AdditionalPrinterColumns))
out.Conversion = (*apiextensions.CustomResourceConversion)(unsafe.Pointer(in.Conversion)) out.Conversion = (*apiextensions.CustomResourceConversion)(unsafe.Pointer(in.Conversion))
return nil return nil
...@@ -492,7 +502,17 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes ...@@ -492,7 +502,17 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes
out.Validation = nil out.Validation = nil
} }
out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources)) out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
out.Versions = *(*[]CustomResourceDefinitionVersion)(unsafe.Pointer(&in.Versions)) if in.Versions != nil {
in, out := &in.Versions, &out.Versions
*out = make([]CustomResourceDefinitionVersion, len(*in))
for i := range *in {
if err := Convert_apiextensions_CustomResourceDefinitionVersion_To_v1beta1_CustomResourceDefinitionVersion(&(*in)[i], &(*out)[i], s); err != nil {
return err
}
}
} else {
out.Versions = nil
}
out.AdditionalPrinterColumns = *(*[]CustomResourceColumnDefinition)(unsafe.Pointer(&in.AdditionalPrinterColumns)) out.AdditionalPrinterColumns = *(*[]CustomResourceColumnDefinition)(unsafe.Pointer(&in.AdditionalPrinterColumns))
out.Conversion = (*CustomResourceConversion)(unsafe.Pointer(in.Conversion)) out.Conversion = (*CustomResourceConversion)(unsafe.Pointer(in.Conversion))
return nil return nil
...@@ -535,6 +555,17 @@ func autoConvert_v1beta1_CustomResourceDefinitionVersion_To_apiextensions_Custom ...@@ -535,6 +555,17 @@ func autoConvert_v1beta1_CustomResourceDefinitionVersion_To_apiextensions_Custom
out.Name = in.Name out.Name = in.Name
out.Served = in.Served out.Served = in.Served
out.Storage = in.Storage out.Storage = in.Storage
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(apiextensions.CustomResourceValidation)
if err := Convert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(*in, *out, s); err != nil {
return err
}
} else {
out.Schema = nil
}
out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
out.AdditionalPrinterColumns = *(*[]apiextensions.CustomResourceColumnDefinition)(unsafe.Pointer(&in.AdditionalPrinterColumns))
return nil return nil
} }
...@@ -547,6 +578,17 @@ func autoConvert_apiextensions_CustomResourceDefinitionVersion_To_v1beta1_Custom ...@@ -547,6 +578,17 @@ func autoConvert_apiextensions_CustomResourceDefinitionVersion_To_v1beta1_Custom
out.Name = in.Name out.Name = in.Name
out.Served = in.Served out.Served = in.Served
out.Storage = in.Storage out.Storage = in.Storage
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(CustomResourceValidation)
if err := Convert_apiextensions_CustomResourceValidation_To_v1beta1_CustomResourceValidation(*in, *out, s); err != nil {
return err
}
} else {
out.Schema = nil
}
out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
out.AdditionalPrinterColumns = *(*[]CustomResourceColumnDefinition)(unsafe.Pointer(&in.AdditionalPrinterColumns))
return nil return nil
} }
......
...@@ -264,7 +264,9 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -264,7 +264,9 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
if in.Versions != nil { if in.Versions != nil {
in, out := &in.Versions, &out.Versions in, out := &in.Versions, &out.Versions
*out = make([]CustomResourceDefinitionVersion, len(*in)) *out = make([]CustomResourceDefinitionVersion, len(*in))
copy(*out, *in) for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
} }
if in.AdditionalPrinterColumns != nil { if in.AdditionalPrinterColumns != nil {
in, out := &in.AdditionalPrinterColumns, &out.AdditionalPrinterColumns in, out := &in.AdditionalPrinterColumns, &out.AdditionalPrinterColumns
...@@ -321,6 +323,21 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt ...@@ -321,6 +323,21 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceDefinitionVersion) DeepCopyInto(out *CustomResourceDefinitionVersion) { func (in *CustomResourceDefinitionVersion) DeepCopyInto(out *CustomResourceDefinitionVersion) {
*out = *in *out = *in
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(CustomResourceValidation)
(*in).DeepCopyInto(*out)
}
if in.Subresources != nil {
in, out := &in.Subresources, &out.Subresources
*out = new(CustomResourceSubresources)
(*in).DeepCopyInto(*out)
}
if in.AdditionalPrinterColumns != nil {
in, out := &in.AdditionalPrinterColumns, &out.AdditionalPrinterColumns
*out = make([]CustomResourceColumnDefinition, len(*in))
copy(*out, *in)
}
return return
} }
......
...@@ -15,6 +15,7 @@ go_library( ...@@ -15,6 +15,7 @@ go_library(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
......
...@@ -18,15 +18,16 @@ package validation ...@@ -18,15 +18,16 @@ package validation
import ( import (
"fmt" "fmt"
"k8s.io/apiserver/pkg/util/webhook"
"reflect" "reflect"
"strings" "strings"
apiequality "k8s.io/apimachinery/pkg/api/equality"
genericvalidation "k8s.io/apimachinery/pkg/api/validation" genericvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
validationutil "k8s.io/apimachinery/pkg/util/validation" validationutil "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
...@@ -99,6 +100,17 @@ func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.Cus ...@@ -99,6 +100,17 @@ func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.Cus
return allErrs return allErrs
} }
// ValidateCustomResourceDefinitionVersion statically validates.
func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, statusEnabled bool) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(version.Schema, statusEnabled, fldPath.Child("schema"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...)
for i := range version.AdditionalPrinterColumns {
allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...)
}
return allErrs
}
// ValidateCustomResourceDefinitionSpec statically validates // ValidateCustomResourceDefinitionSpec statically validates
func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList { func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
...@@ -128,7 +140,32 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -128,7 +140,32 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
if errs := validationutil.IsDNS1035Label(version.Name); len(errs) > 0 { if errs := validationutil.IsDNS1035Label(version.Name); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ","))) allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ",")))
} }
subresources := getSubresourcesForVersion(spec, version.Name)
allErrs = append(allErrs, ValidateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), hasStatusEnabled(subresources))...)
}
// The top-level and per-version fields are mutual exclusive
if spec.Validation != nil && hasPerVersionSchema(spec.Versions) {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "top-level and per-version schemas are mutually exclusive"))
} }
if spec.Subresources != nil && hasPerVersionSubresources(spec.Versions) {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("subresources"), "top-level and per-version subresources are mutually exclusive"))
}
if len(spec.AdditionalPrinterColumns) > 0 && hasPerVersionColumns(spec.Versions) {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalPrinterColumns"), "top-level and per-version additionalPrinterColumns are mutually exclusive"))
}
// Per-version fields may not all be set to identical values (top-level field should be used instead)
if hasIdenticalPerVersionSchema(spec.Versions) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version schemas may not all be set to identical values (top-level validation should be used instead)"))
}
if hasIdenticalPerVersionSubresources(spec.Versions) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version subresources may not all be set to identical values (top-level subresources should be used instead)"))
}
if hasIdenticalPerVersionColumns(spec.Versions) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version additionalPrinterColumns may not all be set to identical values (top-level additionalPrinterColumns should be used instead)"))
}
if !uniqueNames { if !uniqueNames {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must contain unique version names")) allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must contain unique version names"))
} }
...@@ -161,11 +198,7 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -161,11 +198,7 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...)
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
statusEnabled := false allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, hasAnyStatusEnabled(spec), fldPath.Child("validation"))...)
if spec.Subresources != nil && spec.Subresources.Status != nil {
statusEnabled = true
}
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, statusEnabled, fldPath.Child("validation"))...)
} else if spec.Validation != nil { } else if spec.Validation != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate CustomResourceValidation")) allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate CustomResourceValidation"))
} }
...@@ -177,7 +210,7 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -177,7 +210,7 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
} }
for i := range spec.AdditionalPrinterColumns { for i := range spec.AdditionalPrinterColumns {
if errs := ValidateCustomResourceColumnDefinition(&spec.AdditionalPrinterColumns[i], fldPath.Child("columns").Index(i)); len(errs) > 0 { if errs := ValidateCustomResourceColumnDefinition(&spec.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i)); len(errs) > 0 {
allErrs = append(allErrs, errs...) allErrs = append(allErrs, errs...)
} }
} }
...@@ -250,6 +283,118 @@ func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.Cus ...@@ -250,6 +283,118 @@ func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.Cus
return allErrs return allErrs
} }
// getSubresourcesForVersion returns the subresources for given version in given CRD spec.
// NOTE That this function assumes version always exist since it's used by the validation process
// that iterates through the existing versions.
func getSubresourcesForVersion(crd *apiextensions.CustomResourceDefinitionSpec, version string) *apiextensions.CustomResourceSubresources {
if !hasPerVersionSubresources(crd.Versions) {
return crd.Subresources
}
for _, v := range crd.Versions {
if version == v.Name {
return v.Subresources
}
}
return nil
}
// hasAnyStatusEnabled returns true if given CRD spec has at least one Status Subresource set
// among the top-level and per-version Subresources.
func hasAnyStatusEnabled(crd *apiextensions.CustomResourceDefinitionSpec) bool {
if hasStatusEnabled(crd.Subresources) {
return true
}
for _, v := range crd.Versions {
if hasStatusEnabled(v.Subresources) {
return true
}
}
return false
}
// hasStatusEnabled returns true if given CRD Subresources has non-nil Status set.
func hasStatusEnabled(subresources *apiextensions.CustomResourceSubresources) bool {
if subresources != nil && subresources.Status != nil {
return true
}
return false
}
// hasPerVersionSchema returns true if a CRD uses per-version schema.
func hasPerVersionSchema(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Schema != nil {
return true
}
}
return false
}
// hasPerVersionSubresources returns true if a CRD uses per-version subresources.
func hasPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Subresources != nil {
return true
}
}
return false
}
// hasPerVersionColumns returns true if a CRD uses per-version columns.
func hasPerVersionColumns(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if len(v.AdditionalPrinterColumns) > 0 {
return true
}
}
return false
}
// hasIdenticalPerVersionSchema returns true if a CRD sets identical non-nil values
// to all per-version schemas
func hasIdenticalPerVersionSchema(versions []apiextensions.CustomResourceDefinitionVersion) bool {
if len(versions) == 0 {
return false
}
value := versions[0].Schema
for _, v := range versions {
if v.Schema == nil || !apiequality.Semantic.DeepEqual(v.Schema, value) {
return false
}
}
return true
}
// hasIdenticalPerVersionSubresources returns true if a CRD sets identical non-nil values
// to all per-version subresources
func hasIdenticalPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
if len(versions) == 0 {
return false
}
value := versions[0].Subresources
for _, v := range versions {
if v.Subresources == nil || !apiequality.Semantic.DeepEqual(v.Subresources, value) {
return false
}
}
return true
}
// hasIdenticalPerVersionColumns returns true if a CRD sets identical non-nil values
// to all per-version columns
func hasIdenticalPerVersionColumns(versions []apiextensions.CustomResourceDefinitionVersion) bool {
if len(versions) == 0 {
return false
}
value := versions[0].AdditionalPrinterColumns
for _, v := range versions {
if len(v.AdditionalPrinterColumns) == 0 || !apiequality.Semantic.DeepEqual(v.AdditionalPrinterColumns, value) {
return false
}
}
return true
}
// ValidateCustomResourceDefinitionStatus statically validates // ValidateCustomResourceDefinitionStatus statically validates
func ValidateCustomResourceDefinitionStatus(status *apiextensions.CustomResourceDefinitionStatus, fldPath *field.Path) field.ErrorList { func ValidateCustomResourceDefinitionStatus(status *apiextensions.CustomResourceDefinitionStatus, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
......
...@@ -594,6 +594,55 @@ func TestValidateCustomResourceDefinition(t *testing.T) { ...@@ -594,6 +594,55 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
}, },
errors: []validationMatch{}, errors: []validationMatch{},
}, },
{
name: "per-version fields may not all be set to identical values (top-level field should be used instead)",
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
Schema: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: validValidationSchema,
},
Subresources: &apiextensions.CustomResourceSubresources{},
AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"}},
},
{
Name: "version2",
Served: true,
Storage: false,
Schema: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: validValidationSchema,
},
Subresources: &apiextensions.CustomResourceSubresources{},
AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"}},
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
errors: []validationMatch{
// Per-version schema/subresources/columns may not all be set to identical values.
// Note that the test will fail if we de-duplicate the expected errors below.
invalid("spec", "versions"),
invalid("spec", "versions"),
invalid("spec", "versions"),
},
},
} }
for _, tc := range tests { for _, tc := range tests {
...@@ -1003,6 +1052,87 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { ...@@ -1003,6 +1052,87 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
immutable("spec", "names", "plural"), immutable("spec", "names", "plural"),
}, },
}, },
{
name: "top-level and per-version fields are mutually exclusive",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
Subresources: &apiextensions.CustomResourceSubresources{},
},
{
Name: "version2",
Served: true,
Storage: false,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
{
Name: "version2",
Served: true,
Storage: false,
Schema: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: validValidationSchema,
},
Subresources: &apiextensions.CustomResourceSubresources{},
AdditionalPrinterColumns: []apiextensions.CustomResourceColumnDefinition{{Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"}},
},
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: validValidationSchema,
},
Subresources: &apiextensions.CustomResourceSubresources{},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
errors: []validationMatch{
forbidden("spec", "validation"),
forbidden("spec", "subresources"),
},
},
} }
for _, tc := range tests { for _, tc := range tests {
...@@ -1090,36 +1220,7 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) { ...@@ -1090,36 +1220,7 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
{ {
name: "all allowed fields at the root of the schema with status", name: "all allowed fields at the root of the schema with status",
input: apiextensions.CustomResourceValidation{ input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ OpenAPIV3Schema: validValidationSchema,
Description: "This is a description",
Type: "object",
Format: "date-time",
Title: "This is a title",
Maximum: float64Ptr(10),
ExclusiveMaximum: true,
Minimum: float64Ptr(5),
ExclusiveMinimum: true,
MaxLength: int64Ptr(10),
MinLength: int64Ptr(5),
Pattern: "^[a-z]$",
MaxItems: int64Ptr(10),
MinItems: int64Ptr(5),
MultipleOf: float64Ptr(3),
Required: []string{"spec", "status"},
Items: &apiextensions.JSONSchemaPropsOrArray{
Schema: &apiextensions.JSONSchemaProps{
Description: "This is a schema nested under Items",
},
},
Properties: map[string]apiextensions.JSONSchemaProps{
"spec": {},
"status": {},
},
ExternalDocs: &apiextensions.ExternalDocumentation{
Description: "This is an external documentation description",
},
Example: &example,
},
}, },
statusEnabled: true, statusEnabled: true,
wantError: false, wantError: false,
...@@ -1139,6 +1240,37 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) { ...@@ -1139,6 +1240,37 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
var example = apiextensions.JSON(`"This is an example"`) var example = apiextensions.JSON(`"This is an example"`)
var validValidationSchema = &apiextensions.JSONSchemaProps{
Description: "This is a description",
Type: "object",
Format: "date-time",
Title: "This is a title",
Maximum: float64Ptr(10),
ExclusiveMaximum: true,
Minimum: float64Ptr(5),
ExclusiveMinimum: true,
MaxLength: int64Ptr(10),
MinLength: int64Ptr(5),
Pattern: "^[a-z]$",
MaxItems: int64Ptr(10),
MinItems: int64Ptr(5),
MultipleOf: float64Ptr(3),
Required: []string{"spec", "status"},
Items: &apiextensions.JSONSchemaPropsOrArray{
Schema: &apiextensions.JSONSchemaProps{
Description: "This is a schema nested under Items",
},
},
Properties: map[string]apiextensions.JSONSchemaProps{
"spec": {},
"status": {},
},
ExternalDocs: &apiextensions.ExternalDocumentation{
Description: "This is an external documentation description",
},
Example: &example,
}
func float64Ptr(f float64) *float64 { func float64Ptr(f float64) *float64 {
return &f return &f
} }
......
...@@ -182,7 +182,9 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -182,7 +182,9 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
if in.Versions != nil { if in.Versions != nil {
in, out := &in.Versions, &out.Versions in, out := &in.Versions, &out.Versions
*out = make([]CustomResourceDefinitionVersion, len(*in)) *out = make([]CustomResourceDefinitionVersion, len(*in))
copy(*out, *in) for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
} }
if in.AdditionalPrinterColumns != nil { if in.AdditionalPrinterColumns != nil {
in, out := &in.AdditionalPrinterColumns, &out.AdditionalPrinterColumns in, out := &in.AdditionalPrinterColumns, &out.AdditionalPrinterColumns
...@@ -239,6 +241,21 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt ...@@ -239,6 +241,21 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceDefinitionVersion) DeepCopyInto(out *CustomResourceDefinitionVersion) { func (in *CustomResourceDefinitionVersion) DeepCopyInto(out *CustomResourceDefinitionVersion) {
*out = *in *out = *in
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(CustomResourceValidation)
(*in).DeepCopyInto(*out)
}
if in.Subresources != nil {
in, out := &in.Subresources, &out.Subresources
*out = new(CustomResourceSubresources)
(*in).DeepCopyInto(*out)
}
if in.AdditionalPrinterColumns != nil {
in, out := &in.AdditionalPrinterColumns, &out.AdditionalPrinterColumns
*out = make([]CustomResourceColumnDefinition, len(*in))
copy(*out, *in)
}
return return
} }
......
...@@ -13,6 +13,7 @@ go_library( ...@@ -13,6 +13,7 @@ go_library(
"customresource_discovery.go", "customresource_discovery.go",
"customresource_discovery_controller.go", "customresource_discovery_controller.go",
"customresource_handler.go", "customresource_handler.go",
"helpers.go",
], ],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver", importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver",
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver", importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver",
......
...@@ -136,7 +136,11 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { ...@@ -136,7 +136,11 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
Categories: crd.Status.AcceptedNames.Categories, Categories: crd.Status.AcceptedNames.Categories,
}) })
if crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil { subresources, err := getSubresourcesForVersion(crd, version.Version)
if err != nil {
return err
}
if subresources != nil && subresources.Status != nil {
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{ apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Name: crd.Status.AcceptedNames.Plural + "/status", Name: crd.Status.AcceptedNames.Plural + "/status",
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped, Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
...@@ -145,7 +149,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { ...@@ -145,7 +149,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
}) })
} }
if crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil { if subresources != nil && subresources.Scale != nil {
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{ apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Group: autoscaling.GroupName, Group: autoscaling.GroupName,
Version: "v1", Version: "v1",
......
...@@ -220,10 +220,16 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -220,10 +220,16 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
var handler http.HandlerFunc var handler http.HandlerFunc
subresources, err := getSubresourcesForVersion(crd, requestInfo.APIVersion)
if err != nil {
utilruntime.HandleError(err)
http.Error(w, "the server could not properly serve the CR subresources", http.StatusInternalServerError)
return
}
switch { switch {
case subresource == "status" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil: case subresource == "status" && subresources != nil && subresources.Status != nil:
handler = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes) handler = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case subresource == "scale" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil: case subresource == "scale" && subresources != nil && subresources.Scale != nil:
handler = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes) handler = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case len(subresource) == 0: case len(subresource) == 0:
handler = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes) handler = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
...@@ -443,19 +449,28 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -443,19 +449,28 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
typer := newUnstructuredObjectTyper(parameterScheme) typer := newUnstructuredObjectTyper(parameterScheme)
creator := unstructuredCreator{} creator := unstructuredCreator{}
validator, _, err := apiservervalidation.NewSchemaValidator(crd.Spec.Validation) validationSchema, err := getSchemaForVersion(crd, v.Name)
if err != nil {
utilruntime.HandleError(err)
return nil, fmt.Errorf("the server could not properly serve the CR schema")
}
validator, _, err := apiservervalidation.NewSchemaValidator(validationSchema)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var statusSpec *apiextensions.CustomResourceSubresourceStatus var statusSpec *apiextensions.CustomResourceSubresourceStatus
var statusValidator *validate.SchemaValidator var statusValidator *validate.SchemaValidator
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil { subresources, err := getSubresourcesForVersion(crd, v.Name)
statusSpec = crd.Spec.Subresources.Status if err != nil {
utilruntime.HandleError(err)
return nil, fmt.Errorf("the server could not properly serve the CR subresources")
}
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && subresources != nil && subresources.Status != nil {
statusSpec = subresources.Status
// for the status subresource, validate only against the status schema // 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 validationSchema != nil && validationSchema.OpenAPIV3Schema != nil && validationSchema.OpenAPIV3Schema.Properties != nil {
if statusSchema, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["status"]; ok { if statusSchema, ok := validationSchema.OpenAPIV3Schema.Properties["status"]; ok {
openapiSchema := &spec.Schema{} openapiSchema := &spec.Schema{}
if err := apiservervalidation.ConvertJSONSchemaProps(&statusSchema, openapiSchema); err != nil { if err := apiservervalidation.ConvertJSONSchemaProps(&statusSchema, openapiSchema); err != nil {
return nil, err return nil, err
...@@ -466,11 +481,16 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -466,11 +481,16 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
} }
var scaleSpec *apiextensions.CustomResourceSubresourceScale var scaleSpec *apiextensions.CustomResourceSubresourceScale
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil { if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && subresources != nil && subresources.Scale != nil {
scaleSpec = crd.Spec.Subresources.Scale scaleSpec = subresources.Scale
} }
table, err := tableconvertor.New(crd.Spec.AdditionalPrinterColumns) columns, err := getColumnsForVersion(crd, v.Name)
if err != nil {
utilruntime.HandleError(err)
return nil, fmt.Errorf("the server could not properly serve the CR columns")
}
table, err := tableconvertor.New(columns)
if err != nil { if err != nil {
glog.V(2).Infof("The CRD for %v has an invalid printer specification, falling back to default printing: %v", kind, err) glog.V(2).Infof("The CRD for %v has an invalid printer specification, falling back to default printing: %v", kind, err)
} }
......
/*
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 apiserver
import (
"fmt"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)
// getSchemaForVersion returns the validation schema for given version in given CRD.
func getSchemaForVersion(crd *apiextensions.CustomResourceDefinition, version string) (*apiextensions.CustomResourceValidation, error) {
if !hasPerVersionSchema(crd.Spec.Versions) {
return crd.Spec.Validation, nil
}
if crd.Spec.Validation != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version schemas must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return v.Schema, nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// getSubresourcesForVersion returns the subresources for given version in given CRD.
func getSubresourcesForVersion(crd *apiextensions.CustomResourceDefinition, version string) (*apiextensions.CustomResourceSubresources, error) {
if !hasPerVersionSubresources(crd.Spec.Versions) {
return crd.Spec.Subresources, nil
}
if crd.Spec.Subresources != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version subresources must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return v.Subresources, nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// getColumnsForVersion returns the columns for given version in given CRD.
// NOTE: the newly logically-defaulted columns is not pointing to the original CRD object.
// One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through
// the original CRD object instead.
func getColumnsForVersion(crd *apiextensions.CustomResourceDefinition, version string) ([]apiextensions.CustomResourceColumnDefinition, error) {
if !hasPerVersionColumns(crd.Spec.Versions) {
return serveDefaultColumnsIfEmpty(crd.Spec.AdditionalPrinterColumns), nil
}
if len(crd.Spec.AdditionalPrinterColumns) > 0 {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version additionalPrinterColumns must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return serveDefaultColumnsIfEmpty(v.AdditionalPrinterColumns), nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// serveDefaultColumnsIfEmpty applies logically defaulting to columns, if the input columns is empty.
// NOTE: in this way, the newly logically-defaulted columns is not pointing to the original CRD object.
// One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through
// the original CRD object instead.
func serveDefaultColumnsIfEmpty(columns []apiextensions.CustomResourceColumnDefinition) []apiextensions.CustomResourceColumnDefinition {
if len(columns) > 0 {
return columns
}
return []apiextensions.CustomResourceColumnDefinition{
{Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"},
}
}
// hasPerVersionSchema returns true if a CRD uses per-version schema.
func hasPerVersionSchema(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Schema != nil {
return true
}
}
return false
}
// hasPerVersionSubresources returns true if a CRD uses per-version subresources.
func hasPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Subresources != nil {
return true
}
}
return false
}
// hasPerVersionColumns returns true if a CRD uses per-version columns.
func hasPerVersionColumns(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if len(v.AdditionalPrinterColumns) > 0 {
return true
}
}
return false
}
...@@ -41,7 +41,7 @@ const ( ...@@ -41,7 +41,7 @@ const (
// CustomResourceSubresources defines the subresources for CustomResources // CustomResourceSubresources defines the subresources for CustomResources
CustomResourceSubresources utilfeature.Feature = "CustomResourceSubresources" CustomResourceSubresources utilfeature.Feature = "CustomResourceSubresources"
// owner: @mbohlool // owner: @mbohlool, @roycaihw
// alpha: v1.13 // alpha: v1.13
// //
// CustomResourceWebhookConversion defines the webhook conversion for Custom Resources. // CustomResourceWebhookConversion defines the webhook conversion for Custom Resources.
......
...@@ -57,9 +57,25 @@ func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { ...@@ -57,9 +57,25 @@ func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
// if the feature gate is disabled, drop the feature. // if the feature gate is disabled, drop the feature.
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
crd.Spec.Validation = nil crd.Spec.Validation = nil
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Schema = nil
}
} }
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
crd.Spec.Subresources = nil crd.Spec.Subresources = nil
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Subresources = nil
}
}
// On CREATE, if the CustomResourceWebhookConversion feature gate is off, we auto-clear
// the per-version fields. This is to be consistent with the other built-in types, as the
// apiserver drops unknown fields.
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) {
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Schema = nil
crd.Spec.Versions[i].Subresources = nil
crd.Spec.Versions[i].AdditionalPrinterColumns = nil
}
} }
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) && crd.Spec.Conversion != nil { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) && crd.Spec.Conversion != nil {
crd.Spec.Conversion.WebhookClientConfig = nil crd.Spec.Conversion.WebhookClientConfig = nil
...@@ -96,10 +112,36 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { ...@@ -96,10 +112,36 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
newCRD.Spec.Validation = nil newCRD.Spec.Validation = nil
oldCRD.Spec.Validation = nil oldCRD.Spec.Validation = nil
for i := range newCRD.Spec.Versions {
newCRD.Spec.Versions[i].Schema = nil
}
for i := range oldCRD.Spec.Versions {
oldCRD.Spec.Versions[i].Schema = nil
}
} }
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
newCRD.Spec.Subresources = nil newCRD.Spec.Subresources = nil
oldCRD.Spec.Subresources = nil oldCRD.Spec.Subresources = nil
for i := range newCRD.Spec.Versions {
newCRD.Spec.Versions[i].Subresources = nil
}
for i := range oldCRD.Spec.Versions {
oldCRD.Spec.Versions[i].Subresources = nil
}
}
// On UPDATE, if the CustomResourceWebhookConversion feature gate is off, we auto-clear
// the per-version fields if the old CRD doesn't use per-version fields already.
// This is to be consistent with the other built-in types, as the apiserver drops unknown
// fields. If the old CRD already uses per-version fields, the CRD is allowed to continue
// use per-version fields.
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) &&
!hasPerVersionField(oldCRD.Spec.Versions) {
for i := range newCRD.Spec.Versions {
newCRD.Spec.Versions[i].Schema = nil
newCRD.Spec.Versions[i].Subresources = nil
newCRD.Spec.Versions[i].AdditionalPrinterColumns = nil
}
} }
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) && newCRD.Spec.Conversion != nil { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) && newCRD.Spec.Conversion != nil {
if oldCRD.Spec.Conversion == nil || newCRD.Spec.Conversion.WebhookClientConfig == nil { if oldCRD.Spec.Conversion == nil || newCRD.Spec.Conversion.WebhookClientConfig == nil {
...@@ -117,6 +159,16 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { ...@@ -117,6 +159,16 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
} }
} }
// hasPerVersionField returns true if a CRD uses per-version schema/subresources/columns fields.
func hasPerVersionField(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Schema != nil || v.Subresources != nil || len(v.AdditionalPrinterColumns) > 0 {
return true
}
}
return false
}
// Validate validates a new CustomResourceDefinition. // Validate validates a new CustomResourceDefinition.
func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition)) return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition))
......
...@@ -26,6 +26,7 @@ go_test( ...@@ -26,6 +26,7 @@ go_test(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server/options:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server/options:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
...@@ -40,6 +41,8 @@ go_test( ...@@ -40,6 +41,8 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
"//staging/src/k8s.io/client-go/dynamic:go_default_library", "//staging/src/k8s.io/client-go/dynamic:go_default_library",
"//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library",
"//vendor/github.com/coreos/etcd/clientv3:go_default_library", "//vendor/github.com/coreos/etcd/clientv3:go_default_library",
......
...@@ -370,13 +370,13 @@ func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefi ...@@ -370,13 +370,13 @@ func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefi
return nil return nil
} }
// CreateNewScaleClient returns a scale client. // CreateNewVersionedScaleClient returns a scale client.
func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, config *rest.Config) (scale.ScalesGetter, error) { func CreateNewVersionedScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, config *rest.Config, version string) (scale.ScalesGetter, error) {
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
groupResource, err := discoveryClient.ServerResourcesForGroupVersion(crd.Spec.Group + "/" + crd.Spec.Version) groupResource, err := discoveryClient.ServerResourcesForGroupVersion(crd.Spec.Group + "/" + version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -386,12 +386,12 @@ func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, co ...@@ -386,12 +386,12 @@ func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, co
Group: metav1.APIGroup{ Group: metav1.APIGroup{
Name: crd.Spec.Group, Name: crd.Spec.Group,
Versions: []metav1.GroupVersionForDiscovery{ Versions: []metav1.GroupVersionForDiscovery{
{Version: crd.Spec.Version}, {Version: version},
}, },
PreferredVersion: metav1.GroupVersionForDiscovery{Version: crd.Spec.Version}, PreferredVersion: metav1.GroupVersionForDiscovery{Version: version},
}, },
VersionedResources: map[string][]metav1.APIResource{ VersionedResources: map[string][]metav1.APIResource{
crd.Spec.Version: groupResource.APIResources, version: groupResource.APIResources,
}, },
}, },
} }
......
...@@ -30,6 +30,8 @@ import ( ...@@ -30,6 +30,8 @@ import (
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
) )
var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()
func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unstructured, client dynamic.ResourceInterface, definition *apiextensionsv1beta1.CustomResourceDefinition) (*unstructured.Unstructured, error) { func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unstructured, client dynamic.ResourceInterface, definition *apiextensionsv1beta1.CustomResourceDefinition) (*unstructured.Unstructured, error) {
return instantiateVersionedCustomResource(t, instanceToCreate, client, definition, definition.Spec.Versions[0].Name) return instantiateVersionedCustomResource(t, instanceToCreate, client, definition, definition.Spec.Versions[0].Name)
} }
...@@ -92,3 +94,97 @@ func updateCustomResourceDefinitionWithRetry(client clientset.Interface, name st ...@@ -92,3 +94,97 @@ func updateCustomResourceDefinitionWithRetry(client clientset.Interface, name st
} }
return nil, fmt.Errorf("too many retries after conflicts updating CustomResourceDefinition %q", name) return nil, fmt.Errorf("too many retries after conflicts updating CustomResourceDefinition %q", name)
} }
// getSchemaForVersion returns the validation schema for given version in given CRD.
func getSchemaForVersion(crd *apiextensionsv1beta1.CustomResourceDefinition, version string) (*apiextensionsv1beta1.CustomResourceValidation, error) {
if !hasPerVersionSchema(crd.Spec.Versions) {
return crd.Spec.Validation, nil
}
if crd.Spec.Validation != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version schemas must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return v.Schema, nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// getSubresourcesForVersion returns the subresources for given version in given CRD.
func getSubresourcesForVersion(crd *apiextensionsv1beta1.CustomResourceDefinition, version string) (*apiextensionsv1beta1.CustomResourceSubresources, error) {
if !hasPerVersionSubresources(crd.Spec.Versions) {
return crd.Spec.Subresources, nil
}
if crd.Spec.Subresources != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version subresources must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return v.Subresources, nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// getColumnsForVersion returns the columns for given version in given CRD.
// NOTE: the newly logically-defaulted columns is not pointing to the original CRD object.
// One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through
// the original CRD object instead.
func getColumnsForVersion(crd *apiextensionsv1beta1.CustomResourceDefinition, version string) ([]apiextensionsv1beta1.CustomResourceColumnDefinition, error) {
if !hasPerVersionColumns(crd.Spec.Versions) {
return serveDefaultColumnsIfEmpty(crd.Spec.AdditionalPrinterColumns), nil
}
if len(crd.Spec.AdditionalPrinterColumns) > 0 {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version additionalPrinterColumns must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return serveDefaultColumnsIfEmpty(v.AdditionalPrinterColumns), nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// serveDefaultColumnsIfEmpty applies logically defaulting to columns, if the input columns is empty.
// NOTE: in this way, the newly logically-defaulted columns is not pointing to the original CRD object.
// One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through
// the original CRD object instead.
func serveDefaultColumnsIfEmpty(columns []apiextensionsv1beta1.CustomResourceColumnDefinition) []apiextensionsv1beta1.CustomResourceColumnDefinition {
if len(columns) > 0 {
return columns
}
return []apiextensionsv1beta1.CustomResourceColumnDefinition{
{Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"},
}
}
// hasPerVersionSchema returns true if a CRD uses per-version schema.
func hasPerVersionSchema(versions []apiextensionsv1beta1.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Schema != nil {
return true
}
}
return false
}
// hasPerVersionSubresources returns true if a CRD uses per-version subresources.
func hasPerVersionSubresources(versions []apiextensionsv1beta1.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Subresources != nil {
return true
}
}
return false
}
// hasPerVersionColumns returns true if a CRD uses per-version columns.
func hasPerVersionColumns(versions []apiextensionsv1beta1.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if len(v.AdditionalPrinterColumns) > 0 {
return true
}
}
return false
}
...@@ -17,6 +17,7 @@ limitations under the License. ...@@ -17,6 +17,7 @@ limitations under the License.
package integration package integration
import ( import (
"fmt"
"math" "math"
"reflect" "reflect"
"sort" "sort"
...@@ -29,49 +30,108 @@ import ( ...@@ -29,49 +30,108 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "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" "k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/fixtures" "k8s.io/apiextensions-apiserver/test/integration/fixtures"
) )
var labelSelectorPath = ".status.labelSelector" var labelSelectorPath = ".status.labelSelector"
var anotherLabelSelectorPath = ".status.anotherLabelSelector"
func NewNoxuSubresourcesCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{ func NewNoxuSubresourcesCRDs(scope apiextensionsv1beta1.ResourceScope) []*apiextensionsv1beta1.CustomResourceDefinition {
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, return []*apiextensionsv1beta1.CustomResourceDefinition{
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ // CRD that uses top-level subresources
Group: "mygroup.example.com", {
Version: "v1beta1", ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Plural: "noxus", Group: "mygroup.example.com",
Singular: "nonenglishnoxu", Version: "v1beta1",
Kind: "WishIHadChosenNoxu", Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
ShortNames: []string{"foo", "bar", "abc", "def"}, Plural: "noxus",
ListKind: "NoxuItemList", Singular: "nonenglishnoxu",
}, Kind: "WishIHadChosenNoxu",
Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ ShortNames: []string{"foo", "bar", "abc", "def"},
{Name: "v1beta1", Served: true, Storage: false}, ListKind: "NoxuItemList",
{Name: "v1", Served: true, Storage: true}, },
Scope: scope,
Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Served: true,
Storage: true,
},
{
Name: "v1",
Served: true,
Storage: false,
},
},
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: &labelSelectorPath,
},
},
}, },
Scope: scope, },
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ // CRD that uses per-version subresources
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, {
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
SpecReplicasPath: ".spec.replicas", Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
StatusReplicasPath: ".status.replicas", Group: "mygroup.example.com",
LabelSelectorPath: &labelSelectorPath, Version: "v1beta1",
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "noxus",
Singular: "nonenglishnoxu",
Kind: "WishIHadChosenNoxu",
ShortNames: []string{"foo", "bar", "abc", "def"},
ListKind: "NoxuItemList",
},
Scope: scope,
Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Served: true,
Storage: true,
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: &labelSelectorPath,
},
},
},
{
Name: "v1",
Served: true,
Storage: false,
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: &anotherLabelSelectorPath,
},
},
},
}, },
}, },
}, },
} }
} }
func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructured { func NewNoxuSubresourceInstance(namespace, name, version string) *unstructured.Unstructured {
return &unstructured.Unstructured{ return &unstructured.Unstructured{
Object: map[string]interface{}{ Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1", "apiVersion": fmt.Sprintf("mygroup.example.com/%s", version),
"kind": "WishIHadChosenNoxu", "kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{ "metadata": map[string]interface{}{
"namespace": namespace, "namespace": namespace,
...@@ -89,112 +149,120 @@ func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructur ...@@ -89,112 +149,120 @@ func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructur
} }
func TestStatusSubresource(t *testing.T) { func TestStatusSubresource(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDown() defer tearDown()
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) for _, noxuDefinition := range noxuDefinitions {
if err != nil { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) ns := "not-the-default"
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition) for _, v := range noxuDefinition.Spec.Versions {
if err != nil { noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
t.Fatalf("unable to create noxu instance: %v", err) _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
} if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) }
if err != nil { gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
// status should not be set after creation // status should not be set after creation
if val, ok := gottenNoxuInstance.Object["status"]; ok { if val, ok := gottenNoxuInstance.Object["status"]; ok {
t.Fatalf("status should not be set after creation, got %v", val) t.Fatalf("status should not be set after creation, got %v", val)
} }
// .status.num = 20 // .status.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num") err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
// .spec.num = 20 // .spec.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num") err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
// UpdateStatus should not update spec. // UpdateStatus should not update spec.
// Check that .spec.num = 10 and .status.num = 20 // Check that .spec.num = 10 and .status.num = 20
updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{}) updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{})
if err != nil { if err != nil {
t.Fatalf("unable to update status: %v", err) t.Fatalf("unable to update status: %v", err)
} }
specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num") specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num")
if !found || err != nil { if !found || err != nil {
t.Fatalf("unable to get .spec.num") t.Fatalf("unable to get .spec.num")
} }
if specNum != int64(10) { if specNum != int64(10) {
t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum) t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum)
} }
statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num") statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num")
if !found || err != nil { if !found || err != nil {
t.Fatalf("unable to get .status.num") t.Fatalf("unable to get .status.num")
} }
if statusNum != int64(20) { if statusNum != int64(20) {
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum) t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
} }
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{}) gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// .status.num = 40 // .status.num = 40
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num") err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
// .spec.num = 40 // .spec.num = 40
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num") err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
// Update should not update status. // Update should not update status.
// Check that .spec.num = 40 and .status.num = 20 // Check that .spec.num = 40 and .status.num = 20
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{})
if err != nil { if err != nil {
t.Fatalf("unable to update instance: %v", err) t.Fatalf("unable to update instance: %v", err)
} }
specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num") specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num")
if !found || err != nil { if !found || err != nil {
t.Fatalf("unable to get .spec.num") t.Fatalf("unable to get .spec.num")
} }
if specNum != int64(40) { if specNum != int64(40) {
t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum) t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum)
} }
statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num") statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num")
if !found || err != nil { if !found || err != nil {
t.Fatalf("unable to get .status.num") t.Fatalf("unable to get .status.num")
} }
if statusNum != int64(20) { if statusNum != int64(20) {
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum) t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
}
noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
} }
} }
func TestScaleSubresource(t *testing.T) { func TestScaleSubresource(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
groupResource := schema.GroupResource{ groupResource := schema.GroupResource{
Group: "mygroup.example.com", Group: "mygroup.example.com",
Resource: "noxus", Resource: "noxus",
...@@ -215,117 +283,132 @@ func TestScaleSubresource(t *testing.T) { ...@@ -215,117 +283,132 @@ func TestScaleSubresource(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
for _, noxuDefinition := range noxuDefinitions {
// set invalid json path for specReplicasPath for _, v := range noxuDefinition.Spec.Versions {
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = "foo,bar" // Start with a new CRD, so that the object doesn't have resourceVersion
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) noxuDefinition := noxuDefinition.DeepCopy()
if err == nil {
t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec") subresources, err := getSubresourcesForVersion(noxuDefinition, v.Name)
} if err != nil {
t.Fatal(err)
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = ".spec.replicas" }
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) // set invalid json path for specReplicasPath
if err != nil { subresources.Scale.SpecReplicasPath = "foo,bar"
t.Fatal(err) _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
} if err == nil {
t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec")
ns := "not-the-default" }
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition) subresources.Scale.SpecReplicasPath = ".spec.replicas"
if err != nil { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
t.Fatalf("unable to create noxu instance: %v", err) if err != nil {
} t.Fatal(err)
}
scaleClient, err := fixtures.CreateNewScaleClient(noxuDefinition, config)
if err != nil { ns := "not-the-default"
t.Fatal(err) noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
} _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
if err != nil {
// set .status.labelSelector = bar t.Fatalf("unable to create noxu instance: %v", err)
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) }
if err != nil {
t.Fatal(err) scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name)
} if err != nil {
err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", "status", "labelSelector") t.Fatal(err)
if err != nil { }
t.Fatalf("unexpected error: %v", err)
} // set .status.labelSelector = bar
_, err = noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{}) gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil { if err != nil {
t.Fatalf("unable to update status: %v", err) t.Fatal(err)
} }
err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...)
// get the scale object if err != nil {
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo") t.Fatalf("unexpected error: %v", err)
if err != nil { }
t.Fatal(err) _, err = noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{})
} if err != nil {
if gottenScale.Spec.Replicas != 3 { t.Fatalf("unable to update status: %v", err)
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas) }
}
if gottenScale.Status.Selector != "bar" { // get the scale object
t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector) gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
} if err != nil {
t.Fatal(err)
// check self link }
expectedSelfLink := "/apis/mygroup.example.com/v1beta1/namespaces/not-the-default/noxus/foo/scale" if gottenScale.Spec.Replicas != 3 {
if gottenScale.GetSelfLink() != expectedSelfLink { t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas)
t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink()) }
} if gottenScale.Status.Selector != "bar" {
t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector)
// update the scale object }
// check that spec is updated, but status is not
gottenScale.Spec.Replicas = 5 // check self link
gottenScale.Status.Selector = "baz" expectedSelfLink := fmt.Sprintf("/apis/mygroup.example.com/%s/namespaces/not-the-default/noxus/foo/scale", v.Name)
updatedScale, err := scaleClient.Scales("not-the-default").Update(groupResource, gottenScale) if gottenScale.GetSelfLink() != expectedSelfLink {
if err != nil { t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink())
t.Fatal(err) }
}
if updatedScale.Spec.Replicas != 5 { // update the scale object
t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas) // check that spec is updated, but status is not
} gottenScale.Spec.Replicas = 5
if updatedScale.Status.Selector != "bar" { gottenScale.Status.Selector = "baz"
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector) updatedScale, err := scaleClient.Scales("not-the-default").Update(groupResource, gottenScale)
} if err != nil {
t.Fatal(err)
// check that .spec.replicas = 5, but status is not updated }
updatedNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) if updatedScale.Spec.Replicas != 5 {
if err != nil { t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas)
t.Fatal(err) }
} if updatedScale.Status.Selector != "bar" {
specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas") t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector)
if !found || err != nil { }
t.Fatalf("unable to get .spec.replicas")
} // check that .spec.replicas = 5, but status is not updated
if specReplicas != 5 { updatedNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas) if err != nil {
} t.Fatal(err)
statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, "status", "labelSelector") }
if !found || err != nil { specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas")
t.Fatalf("unable to get .status.labelSelector") if !found || err != nil {
} t.Fatalf("unable to get .spec.replicas")
if statusLabelSelector != "bar" { }
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector) if specReplicas != 5 {
} t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas)
}
// validate maximum value statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...)
// set .spec.replicas = math.MaxInt64 if !found || err != nil {
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{}) t.Fatalf("unable to get %s", *subresources.Scale.LabelSelectorPath)
if err != nil { }
t.Fatal(err) if statusLabelSelector != "bar" {
} t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector)
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas") }
if err != nil {
t.Fatalf("unexpected error: %v", err) // validate maximum value
} // set .spec.replicas = math.MaxInt64
_, err = noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err == nil { if err != nil {
t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647") 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, metav1.UpdateOptions{})
if err == nil {
t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647")
}
noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
}
} }
} }
func TestValidationSchemaWithStatus(t *testing.T) { func TestValidationSchemaWithStatus(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, config, _, err := fixtures.StartDefaultServer(t) tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -342,7 +425,7 @@ func TestValidationSchemaWithStatus(t *testing.T) { ...@@ -342,7 +425,7 @@ func TestValidationSchemaWithStatus(t *testing.T) {
} }
// fields other than properties in root schema are not allowed // fields other than properties in root schema are not allowed
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[0]
noxuDefinition.Spec.Subresources = &apiextensionsv1beta1.CustomResourceSubresources{ noxuDefinition.Spec.Subresources = &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
} }
...@@ -373,6 +456,7 @@ func TestValidationSchemaWithStatus(t *testing.T) { ...@@ -373,6 +456,7 @@ func TestValidationSchemaWithStatus(t *testing.T) {
} }
func TestValidateOnlyStatus(t *testing.T) { func TestValidateOnlyStatus(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -407,59 +491,79 @@ func TestValidateOnlyStatus(t *testing.T) { ...@@ -407,59 +491,79 @@ func TestValidateOnlyStatus(t *testing.T) {
}, },
} }
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{ for i, noxuDefinition := range noxuDefinitions {
OpenAPIV3Schema: schema, if i == 0 {
} noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: schema,
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) }
if err != nil { } else {
t.Fatal(err) noxuDefinition.Spec.Versions[0].Schema = &apiextensionsv1beta1.CustomResourceValidation{
} OpenAPIV3Schema: schema,
ns := "not-the-default" }
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) schemaWithDescription := schema.DeepCopy()
schemaWithDescription.Description = "test"
// set .spec.num = 10 and .status.num = 10 noxuDefinition.Spec.Versions[1].Schema = &apiextensionsv1beta1.CustomResourceValidation{
noxuInstance := NewNoxuSubresourceInstance(ns, "foo") OpenAPIV3Schema: schemaWithDescription,
err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num") }
if err != nil { }
t.Fatalf("unexpected error: %v", err)
} noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
createdNoxuInstance, err := instantiateCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition) t.Fatal(err)
if err != nil { }
t.Fatalf("unable to create noxu instance: %v", err) ns := "not-the-default"
} for _, v := range noxuDefinition.Spec.Versions {
noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
// update the spec with .spec.num = 15, expecting no error
err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "spec", "num") // set .spec.num = 10 and .status.num = 10
if err != nil { noxuInstance := NewNoxuSubresourceInstance(ns, "foo", v.Name)
t.Fatalf("unexpected error setting .spec.num: %v", err) err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num")
} if err != nil {
createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance, metav1.UpdateOptions{}) t.Fatalf("unexpected error: %v", err)
if err != nil { }
t.Errorf("unexpected error: %v", err)
} createdNoxuInstance, err := instantiateVersionedCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition, v.Name)
if err != nil {
// update with .status.num = 15, expecting an error t.Fatalf("unable to create noxu instance: %v", err)
err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "status", "num") }
if err != nil {
t.Fatalf("unexpected error setting .status.num: %v", err) // update the spec with .spec.num = 15, expecting no error
} err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "spec", "num")
createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance, metav1.UpdateOptions{}) if err != nil {
if err == nil { t.Fatalf("unexpected error setting .spec.num: %v", err)
t.Fatal("expected error, but got none") }
} createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance, metav1.UpdateOptions{})
statusError, isStatus := err.(*apierrors.StatusError) if err != nil {
if !isStatus || statusError == nil { t.Errorf("unexpected error: %v", err)
t.Fatalf("expected status error, got %T: %v", err, err) }
}
if !strings.Contains(statusError.Error(), "Invalid value") { // update with .status.num = 15, expecting an error
t.Fatalf("expected 'Invalid value' in error, got: %v", err) err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "status", "num")
if err != nil {
t.Fatalf("unexpected error setting .status.num: %v", err)
}
createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance, metav1.UpdateOptions{})
if err == nil {
t.Fatal("expected error, but got none")
}
statusError, isStatus := err.(*apierrors.StatusError)
if !isStatus || statusError == nil {
t.Fatalf("expected status error, got %T: %v", err, err)
}
if !strings.Contains(statusError.Error(), "Invalid value") {
t.Fatalf("expected 'Invalid value' in error, got: %v", err)
}
noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
} }
} }
func TestSubresourcesDiscovery(t *testing.T) { func TestSubresourcesDiscovery(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, config, _, err := fixtures.StartDefaultServer(t) tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -475,140 +579,157 @@ func TestSubresourcesDiscovery(t *testing.T) { ...@@ -475,140 +579,157 @@ func TestSubresourcesDiscovery(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) for _, noxuDefinition := range noxuDefinitions {
if err != nil { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
group := "mygroup.example.com" for _, v := range noxuDefinition.Spec.Versions {
version := "v1beta1" group := "mygroup.example.com"
version := v.Name
resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version) resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(resources.APIResources) != 3 { 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) 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 // check discovery info for status
status := resources.APIResources[1] status := resources.APIResources[1]
if status.Name != "noxus/status" { if status.Name != "noxus/status" {
t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name) t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name)
} }
if status.Namespaced != true { if status.Namespaced != true {
t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced) t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced)
} }
if status.Kind != "WishIHadChosenNoxu" { if status.Kind != "WishIHadChosenNoxu" {
t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind) t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind)
} }
expectedVerbs := []string{"get", "patch", "update"} expectedVerbs := []string{"get", "patch", "update"}
sort.Strings(status.Verbs) sort.Strings(status.Verbs)
if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) { if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) {
t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs) t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs)
} }
// check discovery info for scale // check discovery info for scale
scale := resources.APIResources[2] scale := resources.APIResources[2]
if scale.Group != autoscaling.GroupName { if scale.Group != autoscaling.GroupName {
t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group) t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group)
} }
if scale.Version != "v1" { if scale.Version != "v1" {
t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version) t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version)
} }
if scale.Name != "noxus/scale" { if scale.Name != "noxus/scale" {
t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name) t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name)
} }
if scale.Namespaced != true { if scale.Namespaced != true {
t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced) t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced)
} }
if scale.Kind != "Scale" { if scale.Kind != "Scale" {
t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind) t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind)
} }
sort.Strings(scale.Verbs) sort.Strings(scale.Verbs)
if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) { if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) {
t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs) t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs)
}
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
} }
} }
func TestGeneration(t *testing.T) { func TestGeneration(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDown() defer tearDown()
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) for _, noxuDefinition := range noxuDefinitions {
if err != nil { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) ns := "not-the-default"
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition) for _, v := range noxuDefinition.Spec.Versions {
if err != nil { noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
t.Fatalf("unable to create noxu instance: %v", err) _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
} 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 { // .metadata.generation = 1
t.Fatal(err) gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
} if err != nil {
if gottenNoxuInstance.GetGeneration() != 1 { t.Fatal(err)
t.Fatalf(".metadata.generation should be 1 after creation") }
} 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 { // .status.num = 20
t.Fatalf("unexpected error: %v", err) 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 := noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{})
if err != nil { // UpdateStatus does not increment generation
t.Fatalf("unable to update status: %v", err) updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{})
} if err != nil {
if updatedStatusInstance.GetGeneration() != 1 { t.Fatalf("unable to update status: %v", err)
t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration()) }
} 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) 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 { // .spec.num = 20
t.Fatalf("unexpected error: %v", err) 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, metav1.UpdateOptions{})
if err != nil { // Update increments generation
t.Fatalf("unable to update instance: %v", err) updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{})
} if err != nil {
if updatedInstance.GetGeneration() != 2 { t.Fatalf("unable to update instance: %v", err)
t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration()) }
if updatedInstance.GetGeneration() != 2 {
t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration())
}
noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
} }
} }
func TestSubresourcePatch(t *testing.T) { func TestSubresourcePatch(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
groupResource := schema.GroupResource{ groupResource := schema.GroupResource{
Group: "mygroup.example.com", Group: "mygroup.example.com",
Resource: "noxus", Resource: "noxus",
...@@ -629,146 +750,155 @@ func TestSubresourcePatch(t *testing.T) { ...@@ -629,146 +750,155 @@ func TestSubresourcePatch(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) for _, noxuDefinition := range noxuDefinitions {
if err != nil { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) ns := "not-the-default"
for _, v := range noxuDefinition.Spec.Versions {
t.Logf("Creating foo") noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil { t.Logf("Creating foo")
t.Fatalf("unable to create noxu instance: %v", err) _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
} if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
scaleClient, err := fixtures.CreateNewScaleClient(noxuDefinition, config) }
if err != nil {
t.Fatal(err) scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name)
} if err != nil {
t.Fatal(err)
t.Logf("Patching .status.num to 999") }
patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`)
patchedNoxuInstance, err := noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status") t.Logf("Patching .status.num to 999")
if err != nil { patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`)
t.Fatalf("unexpected error: %v", err) patchedNoxuInstance, err := noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status")
} if err != nil {
t.Fatalf("unexpected error: %v", err)
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") // .status.num should be 999 }
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") // .spec.num should remain 10
rv, found, err := unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion") expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") // .status.num should be 999
if err != nil { expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") // .spec.num should remain 10
t.Fatal(err) rv, found, err := unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion")
} if err != nil {
if !found { t.Fatal(err)
t.Fatalf("metadata.resourceVersion not found") }
} if !found {
t.Fatalf("metadata.resourceVersion not found")
// 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 // this call waits for the resourceVersion to be reached in the cache before returning.
// and then the updated object shows a conflicting diff, which permanently fails the patch. // We need to do this because the patch gets its initial object from the storage, and the cache serves that.
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test. // If it is out of date, then our initial patch is applied to an old resource version, which conflicts
// See https://issue.k8s.io/42644 // and then the updated object shows a conflicting diff, which permanently fails the patch.
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()}) // This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
if err != nil { // See https://issue.k8s.io/42644
t.Fatalf("unexpected error: %v", err) _, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
} if err != nil {
t.Fatalf("unexpected error: %v", err)
// no-op patch }
t.Logf("Patching .status.num again to 999")
patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status") // no-op patch
if err != nil { t.Logf("Patching .status.num again to 999")
t.Fatalf("unexpected error: %v", err) patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status")
} if err != nil {
// make sure no-op patch does not increment resourceVersion t.Fatalf("unexpected error: %v", err)
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") }
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") // make sure no-op patch does not increment resourceVersion
expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num")
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")
// empty patch expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
t.Logf("Applying empty patch")
patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "status") // empty patch
if err != nil { t.Logf("Applying empty patch")
t.Fatalf("unexpected error: %v", err) patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "status")
} if err != nil {
// an empty patch is a no-op patch. make sure it does not increment resourceVersion t.Fatalf("unexpected error: %v", err)
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") }
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")
expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") // an empty patch is a no-op patch. make sure it does not increment resourceVersion
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num")
t.Logf("Patching .spec.replicas to 7") expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")
patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`) expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale")
if err != nil { t.Logf("Patching .spec.replicas to 7")
t.Fatalf("unexpected error: %v", err) patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`)
} patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale")
if err != nil {
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") t.Fatalf("unexpected error: %v", err)
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") // .status.replicas should remain 0 }
rv, found, err = unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion")
if err != nil { expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
t.Fatal(err) expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") // .status.replicas should remain 0
} rv, found, err = unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion")
if !found { if err != nil {
t.Fatalf("metadata.resourceVersion not found") t.Fatal(err)
} }
if !found {
// this call waits for the resourceVersion to be reached in the cache before returning. t.Fatalf("metadata.resourceVersion not found")
// 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 call waits for the resourceVersion to be reached in the cache before returning.
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test. // We need to do this because the patch gets its initial object from the storage, and the cache serves that.
// See https://issue.k8s.io/42644 // If it is out of date, then our initial patch is applied to an old resource version, which conflicts
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()}) // and then the updated object shows a conflicting diff, which permanently fails the patch.
if err != nil { // This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
t.Fatalf("unexpected error: %v", err) // See https://issue.k8s.io/42644
} _, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
if err != nil {
// Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 0 t.Fatalf("unexpected error: %v", err)
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo") }
if err != nil {
t.Fatal(err) // Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 0
} gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
if gottenScale.Spec.Replicas != 7 { if err != nil {
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas) t.Fatal(err)
} }
if gottenScale.Status.Replicas != 0 { if gottenScale.Spec.Replicas != 7 {
t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas) t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas)
} }
if gottenScale.Status.Replicas != 0 {
// no-op patch t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas)
t.Logf("Patching .spec.replicas again to 7") }
patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale")
if err != nil { // no-op patch
t.Fatalf("unexpected error: %v", err) t.Logf("Patching .spec.replicas again to 7")
} patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale")
// make sure no-op patch does not increment resourceVersion if err != nil {
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") t.Fatalf("unexpected error: %v", err)
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") }
expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") // make sure no-op patch does not increment resourceVersion
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
// empty patch expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas")
t.Logf("Applying empty patch") expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "scale")
if err != nil { // empty patch
t.Fatalf("unexpected error: %v", err) t.Logf("Applying empty patch")
} patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "scale")
// an empty patch is a no-op patch. make sure it does not increment resourceVersion if err != nil {
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") t.Fatalf("unexpected error: %v", err)
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") }
expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") // an empty patch is a no-op patch. make sure it does not increment resourceVersion
expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
// make sure strategic merge patch is not supported for both status and scale expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas")
_, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "status") expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
if err == nil {
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") // make sure strategic merge patch is not supported for both status and scale
} _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "status")
if err == nil {
_, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "scale") t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
if err == nil { }
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
_, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "scale")
if err == nil {
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
}
noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
} }
} }
...@@ -28,10 +28,14 @@ import ( ...@@ -28,10 +28,14 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"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" "k8s.io/client-go/dynamic"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/fixtures" "k8s.io/apiextensions-apiserver/test/integration/fixtures"
) )
...@@ -48,12 +52,32 @@ func newTableCRD() *apiextensionsv1beta1.CustomResourceDefinition { ...@@ -48,12 +52,32 @@ func newTableCRD() *apiextensionsv1beta1.CustomResourceDefinition {
ListKind: "TablemList", ListKind: "TablemList",
}, },
Scope: apiextensionsv1beta1.ClusterScoped, Scope: apiextensionsv1beta1.ClusterScoped,
AdditionalPrinterColumns: []apiextensionsv1beta1.CustomResourceColumnDefinition{ Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, {
{Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"}, Name: "v1beta1",
{Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"}, Served: true,
{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"}, Storage: false,
{Name: "Epsilon", Type: "string", Description: "an array of integers as string", JSONPath: ".spec.epsilon"}, AdditionalPrinterColumns: []apiextensionsv1beta1.CustomResourceColumnDefinition{
{Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
{Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"},
{Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"},
{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"},
{Name: "Epsilon", Type: "string", Description: "an array of integers as string", JSONPath: ".spec.epsilon"},
},
},
{
Name: "v1",
Served: true,
Storage: true,
AdditionalPrinterColumns: []apiextensionsv1beta1.CustomResourceColumnDefinition{
{Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
{Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"},
{Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"},
{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"},
{Name: "Epsilon", Type: "string", Description: "an array of integers as string", JSONPath: ".spec.epsilon"},
{Name: "Zeta", Type: "integer", Description: "the zeta field", Format: "int64", Priority: 42, JSONPath: ".spec.zeta"},
},
},
}, },
}, },
} }
...@@ -62,7 +86,7 @@ func newTableCRD() *apiextensionsv1beta1.CustomResourceDefinition { ...@@ -62,7 +86,7 @@ func newTableCRD() *apiextensionsv1beta1.CustomResourceDefinition {
func newTableInstance(name string) *unstructured.Unstructured { func newTableInstance(name string) *unstructured.Unstructured {
return &unstructured.Unstructured{ return &unstructured.Unstructured{
Object: map[string]interface{}{ Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1", "apiVersion": "mygroup.example.com/v1",
"kind": "Table", "kind": "Table",
"metadata": map[string]interface{}{ "metadata": map[string]interface{}{
"name": name, "name": name,
...@@ -73,12 +97,14 @@ func newTableInstance(name string) *unstructured.Unstructured { ...@@ -73,12 +97,14 @@ func newTableInstance(name string) *unstructured.Unstructured {
"gamma": "bar", "gamma": "bar",
"delta": "hello", "delta": "hello",
"epsilon": []int64{1, 2, 3}, "epsilon": []int64{1, 2, 3},
"zeta": 5,
}, },
}, },
} }
} }
func TestTableGet(t *testing.T) { func TestTableGet(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, config, _, err := fixtures.StartDefaultServer(t) tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -107,107 +133,219 @@ func TestTableGet(t *testing.T) { ...@@ -107,107 +133,219 @@ func TestTableGet(t *testing.T) {
} }
t.Logf("table crd created: %#v", crd) t.Logf("table crd created: %#v", crd)
crClient := newNamespacedCustomResourceClient("", dynamicClient, crd) crClient := newNamespacedCustomResourceVersionedClient("", dynamicClient, crd, "v1")
foo, err := crClient.Create(newTableInstance("foo"), metav1.CreateOptions{}) foo, err := crClient.Create(newTableInstance("foo"), metav1.CreateOptions{})
if err != nil { if err != nil {
t.Fatalf("unable to create noxu instance: %v", err) t.Fatalf("unable to create noxu instance: %v", err)
} }
t.Logf("foo created: %#v", foo.UnstructuredContent()) t.Logf("foo created: %#v", foo.UnstructuredContent())
gv := schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version} for i, v := range crd.Spec.Versions {
gvk := gv.WithKind(crd.Spec.Names.Kind) gv := schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name}
gvk := gv.WithKind(crd.Spec.Names.Kind)
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme) codecs := serializer.NewCodecFactory(scheme)
parameterCodec := runtime.NewParameterCodec(scheme) parameterCodec := runtime.NewParameterCodec(scheme)
metav1.AddToGroupVersion(scheme, gv) metav1.AddToGroupVersion(scheme, gv)
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
crConfig := *config crConfig := *config
crConfig.GroupVersion = &gv crConfig.GroupVersion = &gv
crConfig.APIPath = "/apis" crConfig.APIPath = "/apis"
crConfig.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs} crConfig.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs}
crRestClient, err := rest.RESTClientFor(&crConfig) crRestClient, err := rest.RESTClientFor(&crConfig)
if err != nil {
t.Fatal(err)
}
ret, err := crRestClient.Get().
Resource(crd.Spec.Names.Plural).
SetHeader("Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
VersionedParams(&metav1beta1.TableOptions{}, parameterCodec).
Do().
Get()
if err != nil {
t.Fatalf("failed to list %v resources: %v", gvk, err)
}
tbl, ok := ret.(*metav1beta1.Table)
if !ok {
t.Fatalf("expected metav1beta1.Table, got %T", ret)
}
t.Logf("%v table list: %#v", gvk, tbl)
columns, err := getColumnsForVersion(crd, v.Name)
if err != nil {
t.Fatal(err)
}
expectColumnNum := len(columns) + 1
if got, expected := len(tbl.ColumnDefinitions), expectColumnNum; got != expected {
t.Errorf("expected %d headers, got %d", expected, got)
} else {
age := metav1beta1.TableColumnDefinition{Name: "Age", Type: "date", Format: "", Description: "Custom resource definition column (in JSONPath format): .metadata.creationTimestamp", Priority: 0}
if got, expected := tbl.ColumnDefinitions[1], age; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
alpha := metav1beta1.TableColumnDefinition{Name: "Alpha", Type: "string", Format: "", Description: "Custom resource definition column (in JSONPath format): .spec.alpha", Priority: 0}
if got, expected := tbl.ColumnDefinitions[2], alpha; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
beta := metav1beta1.TableColumnDefinition{Name: "Beta", Type: "integer", Format: "int64", Description: "the beta field", Priority: 42}
if got, expected := tbl.ColumnDefinitions[3], beta; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
gamma := metav1beta1.TableColumnDefinition{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values"}
if got, expected := tbl.ColumnDefinitions[4], gamma; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
epsilon := metav1beta1.TableColumnDefinition{Name: "Epsilon", Type: "string", Description: "an array of integers as string"}
if got, expected := tbl.ColumnDefinitions[5], epsilon; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
// Validate extra column for v1
if i == 1 {
zeta := metav1beta1.TableColumnDefinition{Name: "Zeta", Type: "integer", Format: "int64", Description: "the zeta field", Priority: 42}
if got, expected := tbl.ColumnDefinitions[6], zeta; got != expected {
t.Errorf("expected column definition %#v, got %#v", expected, got)
}
}
}
if got, expected := len(tbl.Rows), 1; got != expected {
t.Errorf("expected %d rows, got %d", expected, got)
} else if got, expected := len(tbl.Rows[0].Cells), expectColumnNum; got != expected {
t.Errorf("expected %d cells, got %d", expected, got)
} else {
if got, expected := tbl.Rows[0].Cells[0], "foo"; got != expected {
t.Errorf("expected cell[0] to equal %q, got %q", expected, got)
}
if s, ok := tbl.Rows[0].Cells[1].(string); !ok {
t.Errorf("expected cell[1] to be a string, got: %#v", tbl.Rows[0].Cells[1])
} else {
dur, err := time.ParseDuration(s)
if err != nil {
t.Errorf("expected cell[1] to be a duration: %v", err)
} else if abs(dur.Seconds()) > 30.0 {
t.Errorf("expected cell[1] to be a small age, but got: %v", dur)
}
}
if got, expected := tbl.Rows[0].Cells[2], "foo_123"; got != expected {
t.Errorf("expected cell[2] to equal %q, got %q", expected, got)
}
if got, expected := tbl.Rows[0].Cells[3], int64(10); got != expected {
t.Errorf("expected cell[3] to equal %#v, got %#v", expected, got)
}
if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected {
t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got)
}
if got, expected := tbl.Rows[0].Cells[5], "[1 2 3]"; got != expected {
t.Errorf("expected cell[5] to equal %q, got %q", expected, got)
}
// Validate extra column for v1
if i == 1 {
if got, expected := tbl.Rows[0].Cells[6], int64(5); got != expected {
t.Errorf("expected cell[6] to equal %q, got %q", expected, got)
}
}
}
}
}
// TestColumnsPatch tests the case that a CRD was created with no top-level or
// per-version columns. One should be able to PATCH the CRD setting per-version columns.
func TestColumnsPatch(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDown()
ret, err := crRestClient.Get(). apiExtensionClient, err := clientset.NewForConfig(config)
Resource(crd.Spec.Names.Plural).
SetHeader("Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
VersionedParams(&metav1beta1.TableOptions{}, parameterCodec).
Do().
Get()
if err != nil { if err != nil {
t.Fatalf("failed to list %v resources: %v", gvk, err) t.Fatal(err)
} }
tbl, ok := ret.(*metav1beta1.Table) dynamicClient, err := dynamic.NewForConfig(config)
if !ok { if err != nil {
t.Fatalf("expected metav1beta1.Table, got %T", ret) t.Fatal(err)
} }
t.Logf("%v table list: %#v", gvk, tbl)
if got, expected := len(tbl.ColumnDefinitions), 6; got != expected { // CRD with no top-level and per-version columns should be created successfully
t.Errorf("expected %d headers, got %d", expected, got) crd := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)[0]
} else { crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
age := metav1beta1.TableColumnDefinition{Name: "Age", Type: "date", Format: "", Description: "Custom resource definition column (in JSONPath format): .metadata.creationTimestamp", Priority: 0} if err != nil {
if got, expected := tbl.ColumnDefinitions[1], age; got != expected { t.Fatal(err)
t.Errorf("expected column definition %#v, got %#v", expected, got) }
}
alpha := metav1beta1.TableColumnDefinition{Name: "Alpha", Type: "string", Format: "", Description: "Custom resource definition column (in JSONPath format): .spec.alpha", Priority: 0} // One should be able to patch the CRD to use per-version columns. The top-level columns
if got, expected := tbl.ColumnDefinitions[2], alpha; got != expected { // should not be defaulted during creation, and apiserver should not return validation
t.Errorf("expected column definition %#v, got %#v", expected, got) // error about top-level and per-version columns being mutual exclusive.
} patch := []byte(`{"spec":{"versions":[{"name":"v1beta1","served":true,"storage":true,"additionalPrinterColumns":[{"name":"Age","type":"date","JSONPath":".metadata.creationTimestamp"}]},{"name":"v1","served":true,"storage":false,"additionalPrinterColumns":[{"name":"Age2","type":"date","JSONPath":".metadata.creationTimestamp"}]}]}}`)
beta := metav1beta1.TableColumnDefinition{Name: "Beta", Type: "integer", Format: "int64", Description: "the beta field", Priority: 42} _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Patch(crd.Name, types.MergePatchType, patch)
if got, expected := tbl.ColumnDefinitions[3], beta; got != expected { if err != nil {
t.Errorf("expected column definition %#v, got %#v", expected, got) t.Fatal(err)
} }
gamma := metav1beta1.TableColumnDefinition{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values"} crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
if got, expected := tbl.ColumnDefinitions[4], gamma; got != expected { if err != nil {
t.Errorf("expected column definition %#v, got %#v", expected, got) t.Fatal(err)
} }
t.Logf("columns crd patched: %#v", crd)
}
epsilon := metav1beta1.TableColumnDefinition{Name: "Epsilon", Type: "string", Description: "an array of integers as string"} // TestPatchCleanTopLevelColumns tests the case that a CRD was created with top-level columns.
if got, expected := tbl.ColumnDefinitions[5], epsilon; got != expected { // One should be able to PATCH the CRD cleaning the top-level columns and setting per-version
t.Errorf("expected column definition %#v, got %#v", expected, got) // columns.
} func TestPatchCleanTopLevelColumns(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
} }
if got, expected := len(tbl.Rows), 1; got != expected { defer tearDown()
t.Errorf("expected %d rows, got %d", expected, got)
} else if got, expected := len(tbl.Rows[0].Cells), 6; got != expected { apiExtensionClient, err := clientset.NewForConfig(config)
t.Errorf("expected %d cells, got %d", expected, got) if err != nil {
} else { t.Fatal(err)
if got, expected := tbl.Rows[0].Cells[0], "foo"; got != expected { }
t.Errorf("expected cell[0] to equal %q, got %q", expected, got)
} dynamicClient, err := dynamic.NewForConfig(config)
if s, ok := tbl.Rows[0].Cells[1].(string); !ok { if err != nil {
t.Errorf("expected cell[1] to be a string, got: %#v", tbl.Rows[0].Cells[1]) t.Fatal(err)
} else { }
dur, err := time.ParseDuration(s)
if err != nil { crd := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)[0]
t.Errorf("expected cell[1] to be a duration: %v", err) crd.Spec.AdditionalPrinterColumns = []apiextensionsv1beta1.CustomResourceColumnDefinition{
} else if abs(dur.Seconds()) > 30.0 { {Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"},
t.Errorf("expected cell[1] to be a small age, but got: %v", dur) }
} crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
} if err != nil {
if got, expected := tbl.Rows[0].Cells[2], "foo_123"; got != expected { t.Fatal(err)
t.Errorf("expected cell[2] to equal %q, got %q", expected, got) }
} t.Logf("columns crd created: %#v", crd)
if got, expected := tbl.Rows[0].Cells[3], int64(10); got != expected {
t.Errorf("expected cell[3] to equal %#v, got %#v", expected, got) // One should be able to patch the CRD to use per-version columns by cleaning
} // the top-level columns.
if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected { patch := []byte(`{"spec":{"additionalPrinterColumns":null,"versions":[{"name":"v1beta1","served":true,"storage":true,"additionalPrinterColumns":[{"name":"Age","type":"date","JSONPath":".metadata.creationTimestamp"}]},{"name":"v1","served":true,"storage":false,"additionalPrinterColumns":[{"name":"Age2","type":"date","JSONPath":".metadata.creationTimestamp"}]}]}}`)
t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got)
} _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Patch(crd.Name, types.MergePatchType, patch)
if got, expected := tbl.Rows[0].Cells[5], "[1 2 3]"; got != expected { if err != nil {
t.Errorf("expected cell[5] to equal %q, got %q", expected, got) t.Fatal(err)
} }
crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
} }
t.Logf("columns crd patched: %#v", crd)
} }
func abs(x float64) float64 { func abs(x float64) float64 {
......
...@@ -17,6 +17,7 @@ limitations under the License. ...@@ -17,6 +17,7 @@ limitations under the License.
package integration package integration
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
"time" "time"
...@@ -25,8 +26,11 @@ import ( ...@@ -25,8 +26,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/fixtures" "k8s.io/apiextensions-apiserver/test/integration/fixtures"
) )
...@@ -84,64 +88,114 @@ func TestForProperValidationErrors(t *testing.T) { ...@@ -84,64 +88,114 @@ func TestForProperValidationErrors(t *testing.T) {
} }
} }
func newNoxuValidationCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition { func newNoxuValidationCRDs(scope apiextensionsv1beta1.ResourceScope) []*apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{ validationSchema := &apiextensionsv1beta1.JSONSchemaProps{
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, Required: []string{"alpha", "beta"},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ AdditionalProperties: &apiextensionsv1beta1.JSONSchemaPropsOrBool{
Group: "mygroup.example.com", Allows: true,
Version: "v1beta1", },
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
Plural: "noxus", "alpha": {
Singular: "nonenglishnoxu", Description: "Alpha is an alphanumeric string with underscores",
Kind: "WishIHadChosenNoxu", Type: "string",
ShortNames: []string{"foo", "bar", "abc", "def"}, Pattern: "^[a-zA-Z0-9_]*$",
ListKind: "NoxuItemList", },
"beta": {
Description: "Minimum value of beta is 10",
Type: "number",
Minimum: float64Ptr(10),
}, },
Scope: apiextensionsv1beta1.NamespaceScoped, "gamma": {
Validation: &apiextensionsv1beta1.CustomResourceValidation{ Description: "Gamma is restricted to foo, bar and baz",
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ Type: "string",
Required: []string{"alpha", "beta"}, Enum: []apiextensionsv1beta1.JSON{
AdditionalProperties: &apiextensionsv1beta1.JSONSchemaPropsOrBool{ {
Allows: true, Raw: []byte(`"foo"`),
}, },
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ {
"alpha": { Raw: []byte(`"bar"`),
Description: "Alpha is an alphanumeric string with underscores", },
Type: "string", {
Pattern: "^[a-zA-Z0-9_]*$", Raw: []byte(`"baz"`),
}, },
"beta": { },
Description: "Minimum value of beta is 10", },
Type: "number", "delta": {
Minimum: float64Ptr(10), Description: "Delta is a string with a maximum length of 5 or a number with a minimum value of 0",
}, AnyOf: []apiextensionsv1beta1.JSONSchemaProps{
"gamma": { {
Description: "Gamma is restricted to foo, bar and baz", Type: "string",
Type: "string", MaxLength: int64Ptr(5),
Enum: []apiextensionsv1beta1.JSON{ },
{ {
Raw: []byte(`"foo"`), Type: "number",
}, Minimum: float64Ptr(0),
{ },
Raw: []byte(`"bar"`), },
}, },
{ },
Raw: []byte(`"baz"`), }
}, validationSchemaWithDescription := validationSchema.DeepCopy()
}, validationSchemaWithDescription.Description = "test"
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: apiextensionsv1beta1.NamespaceScoped,
Validation: &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: validationSchema,
},
Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Served: true,
Storage: true,
},
{
Name: "v1",
Served: true,
Storage: false,
},
},
},
},
{
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: apiextensionsv1beta1.NamespaceScoped,
Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Served: true,
Storage: true,
Schema: &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: validationSchema,
}, },
"delta": { },
Description: "Delta is a string with a maximum length of 5 or a number with a minimum value of 0", {
AnyOf: []apiextensionsv1beta1.JSONSchemaProps{ Name: "v1",
{ Served: true,
Type: "string", Storage: false,
MaxLength: int64Ptr(5), Schema: &apiextensionsv1beta1.CustomResourceValidation{
}, OpenAPIV3Schema: validationSchemaWithDescription,
{
Type: "number",
Minimum: float64Ptr(0),
},
},
}, },
}, },
}, },
...@@ -168,253 +222,320 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure ...@@ -168,253 +222,320 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure
} }
func TestCustomResourceValidation(t *testing.T) { func TestCustomResourceValidation(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDown() defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) for _, noxuDefinition := range noxuDefinitions {
if err != nil { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
ns := "not-the-default" ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) for _, v := range noxuDefinition.Spec.Versions {
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition) noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
if err != nil { instanceToCreate := newNoxuValidationInstance(ns, "foo")
t.Fatalf("unable to create noxu instance: %v", err) instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
_, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
} }
} }
func TestCustomResourceUpdateValidation(t *testing.T) { func TestCustomResourceUpdateValidation(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDown() defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) for _, noxuDefinition := range noxuDefinitions {
if err != nil { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
ns := "not-the-default" ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) for _, v := range noxuDefinition.Spec.Versions {
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition) noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
if err != nil { instanceToCreate := newNoxuValidationInstance(ns, "foo")
t.Fatalf("unable to create noxu instance: %v", err) instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
} _, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// invalidate the instance // invalidate the instance
gottenNoxuInstance.Object = map[string]interface{}{ gottenNoxuInstance.Object = map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1", "apiVersion": "mygroup.example.com/v1beta1",
"kind": "WishIHadChosenNoxu", "kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{ "metadata": map[string]interface{}{
"namespace": "not-the-default", "namespace": "not-the-default",
"name": "foo", "name": "foo",
}, },
"gamma": "bar", "gamma": "bar",
"delta": "hello", "delta": "hello",
} }
_, err = noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) _, err = noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{})
if err == nil { if err == nil {
t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance) t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance)
}
noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
} }
} }
func TestCustomResourceValidationErrors(t *testing.T) { func TestCustomResourceValidationErrors(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDown() defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) for _, noxuDefinition := range noxuDefinitions {
if err != nil { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
tests := []struct { ns := "not-the-default"
name string
instanceFn func() *unstructured.Unstructured tests := []struct {
expectedError string name string
}{ instanceFn func() *unstructured.Unstructured
{ expectedError string
name: "bad alpha", }{
instanceFn: func() *unstructured.Unstructured { {
instance := newNoxuValidationInstance(ns, "foo") name: "bad alpha",
instance.Object["alpha"] = "foo_123!" instanceFn: func() *unstructured.Unstructured {
return instance instance := newNoxuValidationInstance(ns, "foo")
instance.Object["alpha"] = "foo_123!"
return instance
},
expectedError: "alpha in body should match '^[a-zA-Z0-9_]*$'",
}, },
expectedError: "alpha in body should match '^[a-zA-Z0-9_]*$'", {
}, name: "bad beta",
{ instanceFn: func() *unstructured.Unstructured {
name: "bad beta", instance := newNoxuValidationInstance(ns, "foo")
instanceFn: func() *unstructured.Unstructured { instance.Object["beta"] = 5
instance := newNoxuValidationInstance(ns, "foo") return instance
instance.Object["beta"] = 5 },
return instance expectedError: "beta in body should be greater than or equal to 10",
}, },
expectedError: "beta in body should be greater than or equal to 10", {
}, name: "bad gamma",
{ instanceFn: func() *unstructured.Unstructured {
name: "bad gamma", instance := newNoxuValidationInstance(ns, "foo")
instanceFn: func() *unstructured.Unstructured { instance.Object["gamma"] = "qux"
instance := newNoxuValidationInstance(ns, "foo") return instance
instance.Object["gamma"] = "qux" },
return instance expectedError: "gamma in body should be one of [foo bar baz]",
}, },
expectedError: "gamma in body should be one of [foo bar baz]", {
}, name: "bad delta",
{ instanceFn: func() *unstructured.Unstructured {
name: "bad delta", instance := newNoxuValidationInstance(ns, "foo")
instanceFn: func() *unstructured.Unstructured { instance.Object["delta"] = "foobarbaz"
instance := newNoxuValidationInstance(ns, "foo") return instance
instance.Object["delta"] = "foobarbaz" },
return instance expectedError: "must validate at least one schema (anyOf)\ndelta in body should be at most 5 chars long",
}, },
expectedError: "must validate at least one schema (anyOf)\ndelta in body should be at most 5 chars long", {
}, name: "absent alpha and beta",
{ instanceFn: func() *unstructured.Unstructured {
name: "absent alpha and beta", instance := newNoxuValidationInstance(ns, "foo")
instanceFn: func() *unstructured.Unstructured { instance.Object = map[string]interface{}{
instance := newNoxuValidationInstance(ns, "foo") "apiVersion": "mygroup.example.com/v1beta1",
instance.Object = map[string]interface{}{ "kind": "WishIHadChosenNoxu",
"apiVersion": "mygroup.example.com/v1beta1", "metadata": map[string]interface{}{
"kind": "WishIHadChosenNoxu", "namespace": "not-the-default",
"metadata": map[string]interface{}{ "name": "foo",
"namespace": "not-the-default", },
"name": "foo", "gamma": "bar",
}, "delta": "hello",
"gamma": "bar", }
"delta": "hello", return instance
} },
return instance expectedError: ".alpha in body is required\n.beta in body is required",
}, },
expectedError: ".alpha in body is required\n.beta in body is required", }
},
}
for _, tc := range tests { for _, tc := range tests {
_, err := noxuResourceClient.Create(tc.instanceFn(), metav1.CreateOptions{}) for _, v := range noxuDefinition.Spec.Versions {
if err == nil { noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
t.Errorf("%v: expected %v", tc.name, tc.expectedError) instanceToCreate := tc.instanceFn()
continue instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
_, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{})
if err == nil {
t.Errorf("%v: expected %v", tc.name, tc.expectedError)
continue
}
// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
if !strings.Contains(err.Error(), tc.expectedError) {
t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
continue
}
}
} }
// this only works when status errors contain the expect kind and version, so this effectively tests serializations too if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
if !strings.Contains(err.Error(), tc.expectedError) { t.Fatal(err)
t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
continue
} }
} }
} }
func TestCRValidationOnCRDUpdate(t *testing.T) { func TestCRValidationOnCRDUpdate(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDown() defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
for i, noxuDefinition := range noxuDefinitions {
for _, v := range noxuDefinition.Spec.Versions {
// Re-define the CRD to make sure we start with a clean CRD
noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[i]
validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name)
if err != nil {
t.Fatal(err)
}
// set stricter schema // set stricter schema
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"} validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"}
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
ns := "not-the-default" ns := "not-the-default"
noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
instanceToCreate := newNoxuValidationInstance(ns, "foo")
// CR is rejected instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err == nil { // CR is rejected
t.Fatalf("unexpected non-error: CR should be rejected") _, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
} if err == nil {
t.Fatalf("unexpected non-error: CR should be rejected")
}
// update the CRD to a less stricter schema // update the CRD to a less stricter schema
_, err = updateCustomResourceDefinitionWithRetry(apiExtensionClient, "noxus.mygroup.example.com", func(crd *apiextensionsv1beta1.CustomResourceDefinition) { _, err = updateCustomResourceDefinitionWithRetry(apiExtensionClient, "noxus.mygroup.example.com", func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
crd.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta"} validationSchema, err := getSchemaForVersion(crd, v.Name)
}) if err != nil {
if err != nil { t.Fatal(err)
t.Fatal(err) }
} validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta"}
})
if err != nil {
t.Fatal(err)
}
// CR is now accepted // CR is now accepted
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
_, err := noxuResourceClient.Create(newNoxuValidationInstance(ns, "foo"), metav1.CreateOptions{}) _, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{})
if statusError, isStatus := err.(*apierrors.StatusError); isStatus { if _, isStatus := err.(*apierrors.StatusError); isStatus {
if strings.Contains(statusError.Error(), "is invalid") { if apierrors.IsInvalid(err) {
return false, nil return false, nil
}
}
if err != nil {
return false, err
}
return true, nil
})
if err != nil {
t.Fatal(err)
}
noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
} }
} }
if err != nil {
return false, err
}
return true, nil
})
if err != nil {
t.Fatal(err)
} }
} }
func TestForbiddenFieldsInSchema(t *testing.T) { func TestForbiddenFieldsInSchema(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer tearDown() defer tearDown()
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = false for i, noxuDefinition := range noxuDefinitions {
for _, v := range noxuDefinition.Spec.Versions {
// Re-define the CRD to make sure we start with a clean CRD
noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[i]
validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name)
if err != nil {
t.Fatal(err)
}
validationSchema.OpenAPIV3Schema.AdditionalProperties.Allows = false
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil { if err == nil {
t.Fatalf("unexpected non-error: additionalProperties cannot be set to false") t.Fatalf("unexpected non-error: additionalProperties cannot be set to false")
} }
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{
Type: "array", Type: "array",
UniqueItems: true, UniqueItems: true,
} }
noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = true validationSchema.OpenAPIV3Schema.AdditionalProperties.Allows = true
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil { if err == nil {
t.Fatalf("unexpected non-error: uniqueItems cannot be set to true") t.Fatalf("unexpected non-error: uniqueItems cannot be set to true")
} }
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Ref = strPtr("#/definition/zeta") validationSchema.OpenAPIV3Schema.Ref = strPtr("#/definition/zeta")
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{
Type: "array", Type: "array",
UniqueItems: false, UniqueItems: false,
} }
_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil { if err == nil {
t.Fatal("unexpected non-error: $ref cannot be non-empty string") t.Fatal("unexpected non-error: $ref cannot be non-empty string")
} }
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Ref = nil validationSchema.OpenAPIV3Schema.Ref = nil
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
}
if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
t.Fatal(err)
}
}
} }
} }
......
...@@ -27,10 +27,13 @@ import ( ...@@ -27,10 +27,13 @@ import (
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types" "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" "k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/fixtures" "k8s.io/apiextensions-apiserver/test/integration/fixtures"
) )
...@@ -354,6 +357,7 @@ values: ...@@ -354,6 +357,7 @@ values:
} }
func TestYAMLSubresource(t *testing.T) { func TestYAMLSubresource(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)()
tearDown, config, _, err := fixtures.StartDefaultServer(t) tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -369,7 +373,7 @@ func TestYAMLSubresource(t *testing.T) { ...@@ -369,7 +373,7 @@ func TestYAMLSubresource(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.ClusterScoped) noxuDefinition := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.ClusterScoped)[0]
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment