Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
ecdc1638
Commit
ecdc1638
authored
Mar 09, 2018
by
Dr. Stefan Schimanski
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
apiextensions-apiserver: add columns to CRD spec
parent
b9e46f54
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
556 additions
and
54 deletions
+556
-54
fuzzer.go
...ensions-apiserver/pkg/apis/apiextensions/fuzzer/fuzzer.go
+8
-0
types.go
...o/apiextensions-apiserver/pkg/apis/apiextensions/types.go
+24
-0
defaults.go
...ions-apiserver/pkg/apis/apiextensions/v1beta1/defaults.go
+8
-0
types.go
...ensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go
+24
-0
validation.go
...apiserver/pkg/apis/apiextensions/validation/validation.go
+39
-0
customresource_handler.go
...ensions-apiserver/pkg/apiserver/customresource_handler.go
+3
-2
etcd_test.go
...nsions-apiserver/pkg/registry/customresource/etcd_test.go
+97
-5
tableconvertor.go
.../registry/customresource/tableconvertor/tableconvertor.go
+94
-42
tableconvertor_test.go
...stry/customresource/tableconvertor/tableconvertor_test.go
+68
-0
table_test.go
...io/apiextensions-apiserver/test/integration/table_test.go
+185
-0
table.go
staging/src/k8s.io/apimachinery/pkg/api/meta/table/table.go
+3
-3
resttest.go
...c/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go
+3
-2
No files found.
staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer/fuzzer.go
View file @
ecdc1638
...
...
@@ -23,9 +23,12 @@ import (
"github.com/google/gofuzz"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
runtimeserializer
"k8s.io/apimachinery/pkg/runtime/serializer"
)
var
swaggerMetadataDescriptions
=
metav1
.
ObjectMeta
{}
.
SwaggerDoc
()
// Funcs returns the fuzzer functions for the apiextensions apis.
func
Funcs
(
codecs
runtimeserializer
.
CodecFactory
)
[]
interface
{}
{
return
[]
interface
{}{
...
...
@@ -53,6 +56,11 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
}
else
if
len
(
obj
.
Versions
)
!=
0
{
obj
.
Version
=
obj
.
Versions
[
0
]
.
Name
}
if
len
(
obj
.
AdditionalPrinterColumns
)
==
0
{
obj
.
AdditionalPrinterColumns
=
[]
apiextensions
.
CustomResourceColumnDefinition
{
{
Name
:
"Age"
,
Type
:
"date"
,
Description
:
swaggerMetadataDescriptions
[
"creationTimestamp"
],
JSONPath
:
".metadata.creationTimestamp"
},
}
}
},
func
(
obj
*
apiextensions
.
CustomResourceDefinition
,
c
fuzz
.
Continue
)
{
c
.
FuzzNoCustom
(
obj
)
...
...
staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go
View file @
ecdc1638
...
...
@@ -49,6 +49,8 @@ type CustomResourceDefinitionSpec struct {
// major version, then minor version. An example sorted list of versions:
// v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.
Versions
[]
CustomResourceDefinitionVersion
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
AdditionalPrinterColumns
[]
CustomResourceColumnDefinition
}
type
CustomResourceDefinitionVersion
struct
{
...
...
@@ -61,6 +63,28 @@ type CustomResourceDefinitionVersion struct {
Storage
bool
}
// CustomResourceColumnDefinition specifies a column for server side printing.
type
CustomResourceColumnDefinition
struct
{
// name is a human readable name for the column.
Name
string
// type is an OpenAPI type definition for this column.
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.
Type
string
// format is an optional OpenAPI type definition for this column. The 'name' format is applied
// to the primary identifier column to assist in clients identifying column is the resource name.
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.
Format
string
// description is a human readable description of this column.
Description
string
// priority is an integer defining the relative importance of this column compared to others. Lower
// numbers are considered higher priority. Columns that may be omitted in limited space scenarios
// should be given a higher priority.
Priority
int32
// JSONPath is a simple JSON path, i.e. without array notation.
JSONPath
string
}
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
type
CustomResourceDefinitionNames
struct
{
// Plural is the plural name of the resource to serve. It must match the name of the CustomResourceDefinition-registration
...
...
staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/defaults.go
View file @
ecdc1638
...
...
@@ -19,9 +19,12 @@ package v1beta1
import
(
"strings"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
var
swaggerMetadataDescriptions
=
metav1
.
ObjectMeta
{}
.
SwaggerDoc
()
func
addDefaultingFuncs
(
scheme
*
runtime
.
Scheme
)
error
{
scheme
.
AddTypeDefaultingFunc
(
&
CustomResourceDefinition
{},
func
(
obj
interface
{})
{
SetDefaults_CustomResourceDefinition
(
obj
.
(
*
CustomResourceDefinition
))
})
// TODO figure out why I can't seem to get my defaulter generated
...
...
@@ -63,4 +66,9 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec)
if
len
(
obj
.
Version
)
==
0
&&
len
(
obj
.
Versions
)
!=
0
{
obj
.
Version
=
obj
.
Versions
[
0
]
.
Name
}
if
len
(
obj
.
AdditionalPrinterColumns
)
==
0
{
obj
.
AdditionalPrinterColumns
=
[]
CustomResourceColumnDefinition
{
{
Name
:
"Age"
,
Type
:
"date"
,
Description
:
swaggerMetadataDescriptions
[
"creationTimestamp"
],
JSONPath
:
".metadata.creationTimestamp"
},
}
}
}
staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go
View file @
ecdc1638
...
...
@@ -52,6 +52,8 @@ type CustomResourceDefinitionSpec struct {
// major version, then minor version. An example sorted list of versions:
// v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.
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
[]
CustomResourceColumnDefinition
`json:"additionalPrinterColumns,omitempty" protobuf:"bytes,8,rep,name=additionalPrinterColumns"`
}
type
CustomResourceDefinitionVersion
struct
{
...
...
@@ -64,6 +66,28 @@ type CustomResourceDefinitionVersion struct {
Storage
bool
`json:"storage" protobuf:"varint,3,opt,name=storage"`
}
// CustomResourceColumnDefinition specifies a column for server side printing.
type
CustomResourceColumnDefinition
struct
{
// name is a human readable name for the column.
Name
string
`json:"name" protobuf:"bytes,1,opt,name=name"`
// type is an OpenAPI type definition for this column.
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.
Type
string
`json:"type" protobuf:"bytes,2,opt,name=type"`
// format is an optional OpenAPI type definition for this column. The 'name' format is applied
// to the primary identifier column to assist in clients identifying column is the resource name.
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.
Format
string
`json:"format,omitempty" protobuf:"bytes,3,opt,name=format"`
// description is a human readable description of this column.
Description
string
`json:"description,omitempty" protobuf:"bytes,4,opt,name=description"`
// priority is an integer defining the relative importance of this column compared to others. Lower
// numbers are considered higher priority. Columns that may be omitted in limited space scenarios
// should be given a higher priority.
Priority
int32
`json:"priority,omitempty" protobuf:"bytes,5,opt,name=priority"`
// JSONPath is a simple JSON path, i.e. with array notation.
JSONPath
string
`json:"JSONPath" protobuf:"bytes,6,opt,name=JSONPath"`
}
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
type
CustomResourceDefinitionNames
struct
{
// Plural is the plural name of the resource to serve. It must match the name of the CustomResourceDefinition-registration
...
...
staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go
View file @
ecdc1638
...
...
@@ -22,6 +22,7 @@ import (
"strings"
genericvalidation
"k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/sets"
validationutil
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
utilfeature
"k8s.io/apiserver/pkg/util/feature"
...
...
@@ -31,6 +32,11 @@ import (
apiextensionsfeatures
"k8s.io/apiextensions-apiserver/pkg/features"
)
var
(
printerColumnDatatypes
=
sets
.
NewString
(
"integer"
,
"number"
,
"string"
,
"boolean"
,
"date"
)
customResourceColumnDefinitionFormats
=
sets
.
NewString
(
"int32"
,
"int64"
,
"float"
,
"double"
,
"byte"
,
"date"
,
"date-time"
,
"password"
)
)
// ValidateCustomResourceDefinition statically validates
func
ValidateCustomResourceDefinition
(
obj
*
apiextensions
.
CustomResourceDefinition
)
field
.
ErrorList
{
nameValidationFn
:=
func
(
name
string
,
prefix
bool
)
[]
string
{
...
...
@@ -175,6 +181,12 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
allErrs
=
append
(
allErrs
,
field
.
Forbidden
(
fldPath
.
Child
(
"subresources"
),
"disabled by feature-gate CustomResourceSubresources"
))
}
for
i
:=
range
spec
.
AdditionalPrinterColumns
{
if
errs
:=
ValidateCustomResourceColumnDefinition
(
&
spec
.
AdditionalPrinterColumns
[
i
],
fldPath
.
Child
(
"columns"
)
.
Index
(
i
));
len
(
errs
)
>
0
{
allErrs
=
append
(
allErrs
,
errs
...
)
}
}
return
allErrs
}
...
...
@@ -238,6 +250,33 @@ func ValidateCustomResourceDefinitionNames(names *apiextensions.CustomResourceDe
return
allErrs
}
// ValidateCustomResourceColumnDefinition statically validates a printer column.
func
ValidateCustomResourceColumnDefinition
(
col
*
apiextensions
.
CustomResourceColumnDefinition
,
fldPath
*
field
.
Path
)
field
.
ErrorList
{
allErrs
:=
field
.
ErrorList
{}
if
len
(
col
.
Name
)
==
0
{
allErrs
=
append
(
allErrs
,
field
.
Required
(
fldPath
.
Child
(
"header"
),
""
))
}
if
len
(
col
.
Type
)
==
0
{
allErrs
=
append
(
allErrs
,
field
.
Required
(
fldPath
.
Child
(
"type"
),
fmt
.
Sprintf
(
"must be one of %s"
,
strings
.
Join
(
printerColumnDatatypes
.
List
(),
","
))))
}
else
if
!
printerColumnDatatypes
.
Has
(
col
.
Type
)
{
allErrs
=
append
(
allErrs
,
field
.
Invalid
(
fldPath
.
Child
(
"type"
),
col
.
Type
,
fmt
.
Sprintf
(
"must be one of %s"
,
strings
.
Join
(
printerColumnDatatypes
.
List
(),
","
))))
}
if
len
(
col
.
Format
)
>
0
&&
!
customResourceColumnDefinitionFormats
.
Has
(
col
.
Format
)
{
allErrs
=
append
(
allErrs
,
field
.
Invalid
(
fldPath
.
Child
(
"format"
),
col
.
Format
,
fmt
.
Sprintf
(
"must be one of %s"
,
strings
.
Join
(
customResourceColumnDefinitionFormats
.
List
(),
","
))))
}
if
len
(
col
.
JSONPath
)
==
0
{
allErrs
=
append
(
allErrs
,
field
.
Required
(
fldPath
.
Child
(
"path"
),
""
))
}
else
if
errs
:=
validateSimpleJSONPath
(
col
.
JSONPath
,
fldPath
.
Child
(
"path"
));
len
(
errs
)
>
0
{
allErrs
=
append
(
allErrs
,
errs
...
)
}
return
allErrs
}
// specStandardValidator applies validations for different OpenAPI specification versions.
type
specStandardValidator
interface
{
validate
(
spec
*
apiextensions
.
JSONSchemaProps
,
fldPath
*
field
.
Path
)
field
.
ErrorList
...
...
staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go
View file @
ecdc1638
...
...
@@ -370,6 +370,8 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions
return
info
.
storages
[
info
.
storageVersion
]
.
CustomResource
,
nil
}
var
swaggerMetadataDescriptions
=
metav1
.
ObjectMeta
{}
.
SwaggerDoc
()
func
(
r
*
crdHandler
)
getOrCreateServingInfoFor
(
crd
*
apiextensions
.
CustomResourceDefinition
)
(
*
crdInfo
,
error
)
{
storageMap
:=
r
.
customStorage
.
Load
()
.
(
crdStorageMap
)
if
ret
,
ok
:=
storageMap
[
crd
.
UID
];
ok
{
...
...
@@ -439,8 +441,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
scaleSpec
=
crd
.
Spec
.
Subresources
.
Scale
}
// TODO: identify how to pass printer specification from the CRD
table
,
err
:=
tableconvertor
.
New
(
nil
)
table
,
err
:=
tableconvertor
.
New
(
crd
.
Spec
.
AdditionalPrinterColumns
)
if
err
!=
nil
{
glog
.
V
(
2
)
.
Infof
(
"The CRD for %v has an invalid printer specification, falling back to default printing: %v"
,
kind
,
err
)
}
...
...
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/etcd_test.go
View file @
ecdc1638
...
...
@@ -21,12 +21,15 @@ import (
"reflect"
"strings"
"testing"
"time"
autoscalingv1
"k8s.io/api/autoscaling/v1"
apiequality
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metainternal
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1
"k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff"
...
...
@@ -72,8 +75,19 @@ func newStorage(t *testing.T) (customresource.CustomResourceStorage, *etcdtestin
status
:=
&
apiextensions
.
CustomResourceSubresourceStatus
{}
// TODO: identify how to pass printer specification from the CRD
table
,
_
:=
tableconvertor
.
New
(
nil
)
headers
:=
[]
apiextensions
.
CustomResourceColumnDefinition
{
{
Name
:
"Age"
,
Type
:
"date"
,
JSONPath
:
".metadata.creationTimestamp"
},
{
Name
:
"Replicas"
,
Type
:
"integer"
,
JSONPath
:
".spec.replicas"
},
{
Name
:
"Missing"
,
Type
:
"string"
,
JSONPath
:
".spec.missing"
},
{
Name
:
"Invalid"
,
Type
:
"integer"
,
JSONPath
:
".spec.string"
},
{
Name
:
"String"
,
Type
:
"string"
,
JSONPath
:
".spec.string"
},
{
Name
:
"StringFloat64"
,
Type
:
"string"
,
JSONPath
:
".spec.float64"
},
{
Name
:
"StringInt64"
,
Type
:
"string"
,
JSONPath
:
".spec.replicas"
},
{
Name
:
"StringBool"
,
Type
:
"string"
,
JSONPath
:
".spec.bool"
},
{
Name
:
"Float64"
,
Type
:
"number"
,
JSONPath
:
".spec.float64"
},
{
Name
:
"Bool"
,
Type
:
"boolean"
,
JSONPath
:
".spec.bool"
},
}
table
,
_
:=
tableconvertor
.
New
(
headers
)
storage
:=
customresource
.
NewStorage
(
schema
.
GroupResource
{
Group
:
"mygroup.example.com"
,
Resource
:
"noxus"
},
...
...
@@ -112,11 +126,18 @@ func validNewCustomResource() *unstructured.Unstructured {
"apiVersion"
:
"mygroup.example.com/v1beta1"
,
"kind"
:
"Noxu"
,
"metadata"
:
map
[
string
]
interface
{}{
"namespace"
:
"default"
,
"name"
:
"foo"
,
"namespace"
:
"default"
,
"name"
:
"foo"
,
"creationTimestamp"
:
time
.
Now
()
.
Add
(
-
time
.
Hour
*
12
-
30
*
time
.
Minute
)
.
UTC
()
.
Format
(
time
.
RFC3339
),
},
"spec"
:
map
[
string
]
interface
{}{
"replicas"
:
int64
(
7
),
"replicas"
:
int64
(
7
),
"string"
:
"string"
,
"float64"
:
float64
(
3.1415926
),
"bool"
:
true
,
"stringList"
:
[]
interface
{}{
"foo"
,
"bar"
},
"mixedList"
:
[]
interface
{}{
"foo"
,
int64
(
42
)},
"nonPrimitiveList"
:
[]
interface
{}{
"foo"
,
[]
interface
{}{
int64
(
1
),
int64
(
2
)}},
},
},
}
...
...
@@ -225,6 +246,77 @@ func TestCategories(t *testing.T) {
}
}
func
TestColumns
(
t
*
testing
.
T
)
{
storage
,
server
:=
newStorage
(
t
)
defer
server
.
Terminate
(
t
)
defer
storage
.
CustomResource
.
Store
.
DestroyFunc
()
ctx
:=
genericapirequest
.
WithNamespace
(
genericapirequest
.
NewContext
(),
metav1
.
NamespaceDefault
)
key
:=
"/noxus/"
+
metav1
.
NamespaceDefault
+
"/foo"
validCustomResource
:=
validNewCustomResource
()
if
err
:=
storage
.
CustomResource
.
Storage
.
Create
(
ctx
,
key
,
validCustomResource
,
nil
,
0
);
err
!=
nil
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
gottenList
,
err
:=
storage
.
CustomResource
.
List
(
ctx
,
&
metainternal
.
ListOptions
{})
if
err
!=
nil
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
tbl
,
err
:=
storage
.
CustomResource
.
ConvertToTable
(
ctx
,
gottenList
,
&
metav1beta1
.
TableOptions
{})
if
err
!=
nil
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
expectedColumns
:=
[]
struct
{
Name
,
Type
string
}{
{
"Name"
,
"string"
},
{
"Age"
,
"date"
},
{
"Replicas"
,
"integer"
},
{
"Missing"
,
"string"
},
{
"Invalid"
,
"integer"
},
{
"String"
,
"string"
},
{
"StringFloat64"
,
"string"
},
{
"StringInt64"
,
"string"
},
{
"StringBool"
,
"string"
},
{
"Float64"
,
"number"
},
{
"Bool"
,
"boolean"
},
}
if
len
(
tbl
.
ColumnDefinitions
)
!=
len
(
expectedColumns
)
{
t
.
Fatalf
(
"got %d columns, expected %d. Got: %+v"
,
len
(
tbl
.
ColumnDefinitions
),
len
(
expectedColumns
),
tbl
.
ColumnDefinitions
)
}
for
i
,
d
:=
range
tbl
.
ColumnDefinitions
{
if
d
.
Name
!=
expectedColumns
[
i
]
.
Name
{
t
.
Errorf
(
"got column %d name %q, expected %q"
,
i
,
d
.
Name
,
expectedColumns
[
i
]
.
Name
)
}
if
d
.
Type
!=
expectedColumns
[
i
]
.
Type
{
t
.
Errorf
(
"got column %d type %q, expected %q"
,
i
,
d
.
Type
,
expectedColumns
[
i
]
.
Type
)
}
}
expectedRows
:=
[][]
interface
{}{
{
"foo"
,
"12h"
,
int64
(
7
),
nil
,
nil
,
"string"
,
"3.1415926"
,
"7"
,
"true"
,
float64
(
3.1415926
),
true
,
},
}
for
i
,
r
:=
range
tbl
.
Rows
{
if
!
reflect
.
DeepEqual
(
r
.
Cells
,
expectedRows
[
i
])
{
t
.
Errorf
(
"got row %d with cells %#v, expected %#v"
,
i
,
r
.
Cells
,
expectedRows
[
i
])
}
}
}
func
TestStatusUpdate
(
t
*
testing
.
T
)
{
storage
,
server
:=
newStorage
(
t
)
defer
server
.
Terminate
(
t
)
...
...
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go
View file @
ecdc1638
...
...
@@ -19,11 +19,11 @@ package tableconvertor
import
(
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"github.com/go-openapi/spec"
"reflect"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apimachinery/pkg/api/meta"
metatable
"k8s.io/apimachinery/pkg/api/meta/table"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
...
...
@@ -33,56 +33,46 @@ import (
"k8s.io/client-go/util/jsonpath"
)
const
printColumnsKey
=
"x-kubernetes-print-columns"
var
swaggerMetadataDescriptions
=
metav1
.
ObjectMeta
{}
.
SwaggerDoc
()
// New creates a new table convertor for the provided
OpenAPI schema
. If the printer definition cannot be parsed,
// New creates a new table convertor for the provided
CRD column definition
. If the printer definition cannot be parsed,
// error will be returned along with a default table convertor.
func
New
(
extensions
spec
.
Extensions
)
(
rest
.
TableConvertor
,
error
)
{
func
New
(
crdColumns
[]
apiextensions
.
CustomResourceColumnDefinition
)
(
rest
.
TableConvertor
,
error
)
{
headers
:=
[]
metav1beta1
.
TableColumnDefinition
{
{
Name
:
"Name"
,
Type
:
"string"
,
Format
:
"name"
,
Description
:
swaggerMetadataDescriptions
[
"name"
]},
{
Name
:
"Age"
,
Type
:
"date"
,
Description
:
swaggerMetadataDescriptions
[
"creationTimestamp"
]},
}
c
:=
&
convertor
{
headers
:
headers
,
}
format
,
ok
:=
extensions
.
GetString
(
printColumnsKey
)
if
!
ok
{
return
c
,
nil
}
// "x-kubernetes-print-columns": "custom-columns=NAME:.metadata.name,RSRC:.metadata.resourceVersion"
parts
:=
strings
.
SplitN
(
format
,
"="
,
2
)
if
len
(
parts
)
!=
2
||
parts
[
0
]
!=
"custom-columns"
{
return
c
,
fmt
.
Errorf
(
"unrecognized column definition in 'x-kubernetes-print-columns', only support 'custom-columns=NAME=JSONPATH[,NAME=JSONPATH]'"
)
}
columnSpecs
:=
strings
.
Split
(
parts
[
1
],
","
)
var
columns
[]
*
jsonpath
.
JSONPath
for
_
,
spec
:=
range
columnSpecs
{
parts
:=
strings
.
SplitN
(
spec
,
":"
,
2
)
if
len
(
parts
)
!=
2
||
len
(
parts
[
0
])
==
0
||
len
(
parts
[
1
])
==
0
{
return
c
,
fmt
.
Errorf
(
"unrecognized column definition in 'x-kubernetes-print-columns', must specify NAME=JSONPATH: %s"
,
spec
)
}
path
:=
jsonpath
.
New
(
parts
[
0
])
if
err
:=
path
.
Parse
(
parts
[
1
]);
err
!=
nil
{
return
c
,
fmt
.
Errorf
(
"unrecognized column definition in 'x-kubernetes-print-columns': %v"
,
spec
)
for
_
,
col
:=
range
crdColumns
{
path
:=
jsonpath
.
New
(
col
.
Name
)
if
err
:=
path
.
Parse
(
fmt
.
Sprintf
(
"{%s}"
,
col
.
JSONPath
));
err
!=
nil
{
return
c
,
fmt
.
Errorf
(
"unrecognized column definition %q"
,
col
.
JSONPath
)
}
path
.
AllowMissingKeys
(
true
)
columns
=
append
(
columns
,
path
)
headers
=
append
(
headers
,
metav1beta1
.
TableColumnDefinition
{
Name
:
parts
[
0
],
Type
:
"string"
,
Description
:
fmt
.
Sprintf
(
"Custom resource definition column from OpenAPI (in JSONPath format): %s"
,
parts
[
1
]),
desc
:=
fmt
.
Sprintf
(
"Custom resource definition column (in JSONPath format): %s"
,
col
.
JSONPath
)
if
len
(
col
.
Description
)
>
0
{
desc
=
col
.
Description
}
c
.
additionalColumns
=
append
(
c
.
additionalColumns
,
path
)
c
.
headers
=
append
(
c
.
headers
,
metav1beta1
.
TableColumnDefinition
{
Name
:
col
.
Name
,
Type
:
col
.
Type
,
Format
:
col
.
Format
,
Description
:
desc
,
Priority
:
col
.
Priority
,
})
}
c
.
columns
=
columns
c
.
headers
=
headers
return
c
,
nil
}
type
convertor
struct
{
headers
[]
metav1beta1
.
TableColumnDefinition
c
olumns
[]
*
jsonpath
.
JSONPath
headers
[]
metav1beta1
.
TableColumnDefinition
additionalC
olumns
[]
*
jsonpath
.
JSONPath
}
func
(
c
*
convertor
)
ConvertToTable
(
ctx
context
.
Context
,
obj
runtime
.
Object
,
tableOptions
runtime
.
Object
)
(
*
metav1beta1
.
Table
,
error
)
{
...
...
@@ -103,18 +93,80 @@ func (c *convertor) ConvertToTable(ctx context.Context, obj runtime.Object, tabl
var
err
error
buf
:=
&
bytes
.
Buffer
{}
table
.
Rows
,
err
=
metatable
.
MetaToTableRow
(
obj
,
func
(
obj
runtime
.
Object
,
m
metav1
.
Object
,
name
,
age
string
)
([]
interface
{},
error
)
{
cells
:=
make
([]
interface
{},
2
,
2
+
len
(
c
.
c
olumns
))
cells
:=
make
([]
interface
{},
1
,
1
+
len
(
c
.
additionalC
olumns
))
cells
[
0
]
=
name
cells
[
1
]
=
age
for
_
,
column
:=
range
c
.
columns
{
if
err
:=
column
.
Execute
(
buf
,
obj
);
err
!=
nil
{
customHeaders
:=
c
.
headers
[
1
:
]
for
i
,
column
:=
range
c
.
additionalColumns
{
results
,
err
:=
column
.
FindResults
(
obj
.
(
runtime
.
Unstructured
)
.
UnstructuredContent
())
if
err
!=
nil
||
len
(
results
)
==
0
||
len
(
results
[
0
])
==
0
{
cells
=
append
(
cells
,
nil
)
continue
}
cells
=
append
(
cells
,
buf
.
String
())
buf
.
Reset
()
// as we only support simple JSON path, we can assume to have only one result (or none, filtered out above)
value
:=
results
[
0
][
0
]
.
Interface
()
if
customHeaders
[
i
]
.
Type
==
"string"
{
if
err
:=
column
.
PrintResults
(
buf
,
[]
reflect
.
Value
{
reflect
.
ValueOf
(
value
)});
err
==
nil
{
cells
=
append
(
cells
,
buf
.
String
())
buf
.
Reset
()
}
else
{
cells
=
append
(
cells
,
nil
)
}
}
else
{
cells
=
append
(
cells
,
cellForJSONValue
(
customHeaders
[
i
]
.
Type
,
value
))
}
}
return
cells
,
nil
})
return
table
,
err
}
func
cellForJSONValue
(
headerType
string
,
value
interface
{})
interface
{}
{
if
value
==
nil
{
return
nil
}
switch
headerType
{
case
"integer"
:
switch
typed
:=
value
.
(
type
)
{
case
int64
:
return
typed
case
float64
:
return
int64
(
typed
)
case
json
.
Number
:
if
i64
,
err
:=
typed
.
Int64
();
err
==
nil
{
return
i64
}
}
case
"number"
:
switch
typed
:=
value
.
(
type
)
{
case
int64
:
return
float64
(
typed
)
case
float64
:
return
typed
case
json
.
Number
:
if
f
,
err
:=
typed
.
Float64
();
err
==
nil
{
return
f
}
}
case
"boolean"
:
if
b
,
ok
:=
value
.
(
bool
);
ok
{
return
b
}
case
"string"
:
if
s
,
ok
:=
value
.
(
string
);
ok
{
return
s
}
case
"date"
:
if
typed
,
ok
:=
value
.
(
string
);
ok
{
var
timestamp
metav1
.
Time
err
:=
timestamp
.
UnmarshalQueryParameter
(
typed
)
if
err
!=
nil
{
return
"<invalid>"
}
return
metatable
.
ConvertToHumanReadableDateType
(
timestamp
)
}
}
return
nil
}
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor_test.go
0 → 100644
View file @
ecdc1638
/*
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
tableconvertor
import
(
"fmt"
"reflect"
"testing"
"time"
)
func
Test_cellForJSONValue
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
headerType
string
value
interface
{}
want
interface
{}
}{
{
"integer"
,
int64
(
42
),
int64
(
42
)},
{
"integer"
,
float64
(
3.14
),
int64
(
3
)},
{
"integer"
,
true
,
nil
},
{
"integer"
,
"foo"
,
nil
},
{
"number"
,
int64
(
42
),
float64
(
42
)},
{
"number"
,
float64
(
3.14
),
float64
(
3.14
)},
{
"number"
,
true
,
nil
},
{
"number"
,
"foo"
,
nil
},
{
"boolean"
,
int64
(
42
),
nil
},
{
"boolean"
,
float64
(
3.14
),
nil
},
{
"boolean"
,
true
,
true
},
{
"boolean"
,
"foo"
,
nil
},
{
"string"
,
int64
(
42
),
nil
},
{
"string"
,
float64
(
3.14
),
nil
},
{
"string"
,
true
,
nil
},
{
"string"
,
"foo"
,
"foo"
},
{
"date"
,
int64
(
42
),
nil
},
{
"date"
,
float64
(
3.14
),
nil
},
{
"date"
,
true
,
nil
},
{
"date"
,
time
.
Now
()
.
Add
(
-
time
.
Hour
*
12
-
30
*
time
.
Minute
)
.
UTC
()
.
Format
(
time
.
RFC3339
),
"12h"
},
{
"date"
,
time
.
Now
()
.
Add
(
+
time
.
Hour
*
12
+
30
*
time
.
Minute
)
.
UTC
()
.
Format
(
time
.
RFC3339
),
"<invalid>"
},
{
"date"
,
""
,
"<unknown>"
},
{
"unknown"
,
"foo"
,
nil
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
fmt
.
Sprintf
(
"%#v of type %s"
,
tt
.
value
,
tt
.
headerType
),
func
(
t
*
testing
.
T
)
{
if
got
:=
cellForJSONValue
(
tt
.
headerType
,
tt
.
value
);
!
reflect
.
DeepEqual
(
got
,
tt
.
want
)
{
t
.
Errorf
(
"cellForJSONValue() = %#v, want %#v"
,
got
,
tt
.
want
)
}
})
}
}
staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go
0 → 100644
View file @
ecdc1638
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package
integration
import
(
"fmt"
"testing"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1
"k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
apiextensionsv1beta1
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
)
func
newTableCRD
()
*
apiextensionsv1beta1
.
CustomResourceDefinition
{
return
&
apiextensionsv1beta1
.
CustomResourceDefinition
{
ObjectMeta
:
metav1
.
ObjectMeta
{
Name
:
"tables.mygroup.example.com"
},
Spec
:
apiextensionsv1beta1
.
CustomResourceDefinitionSpec
{
Group
:
"mygroup.example.com"
,
Version
:
"v1beta1"
,
Names
:
apiextensionsv1beta1
.
CustomResourceDefinitionNames
{
Plural
:
"tables"
,
Singular
:
"table"
,
Kind
:
"Table"
,
ListKind
:
"TablemList"
,
},
Scope
:
apiextensionsv1beta1
.
ClusterScoped
,
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"
},
},
},
}
}
func
newTableInstance
(
name
string
)
*
unstructured
.
Unstructured
{
return
&
unstructured
.
Unstructured
{
Object
:
map
[
string
]
interface
{}{
"apiVersion"
:
"mygroup.example.com/v1beta1"
,
"kind"
:
"Table"
,
"metadata"
:
map
[
string
]
interface
{}{
"name"
:
name
,
},
"spec"
:
map
[
string
]
interface
{}{
"alpha"
:
"foo_123"
,
"beta"
:
10
,
"gamma"
:
"bar"
,
"delta"
:
"hello"
,
},
},
}
}
func
TestTableGet
(
t
*
testing
.
T
)
{
stopCh
,
config
,
err
:=
testserver
.
StartDefaultServer
()
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
defer
close
(
stopCh
)
apiExtensionClient
,
err
:=
clientset
.
NewForConfig
(
config
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
dynamicClient
,
err
:=
dynamic
.
NewForConfig
(
config
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
crd
:=
newTableCRD
()
crd
,
err
=
testserver
.
CreateNewCustomResourceDefinition
(
crd
,
apiExtensionClient
,
dynamicClient
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
crd
,
err
=
apiExtensionClient
.
ApiextensionsV1beta1
()
.
CustomResourceDefinitions
()
.
Get
(
crd
.
Name
,
metav1
.
GetOptions
{})
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
t
.
Logf
(
"table crd created: %#v"
,
crd
)
crClient
:=
newNamespacedCustomResourceClient
(
""
,
dynamicClient
,
crd
)
foo
,
err
:=
crClient
.
Create
(
newTableInstance
(
"foo"
))
if
err
!=
nil
{
t
.
Fatalf
(
"unable to create noxu instance: %v"
,
err
)
}
t
.
Logf
(
"foo created: %#v"
,
foo
.
UnstructuredContent
())
gv
:=
schema
.
GroupVersion
{
Group
:
crd
.
Spec
.
Group
,
Version
:
crd
.
Spec
.
Version
}
gvk
:=
gv
.
WithKind
(
crd
.
Spec
.
Names
.
Kind
)
scheme
:=
runtime
.
NewScheme
()
codecs
:=
serializer
.
NewCodecFactory
(
scheme
)
parameterCodec
:=
runtime
.
NewParameterCodec
(
scheme
)
metav1
.
AddToGroupVersion
(
scheme
,
gv
)
scheme
.
AddKnownTypes
(
gv
,
&
metav1beta1
.
Table
{},
&
metav1beta1
.
TableOptions
{})
scheme
.
AddKnownTypes
(
metav1beta1
.
SchemeGroupVersion
,
&
metav1beta1
.
Table
{},
&
metav1beta1
.
TableOptions
{})
crConfig
:=
*
config
crConfig
.
GroupVersion
=
&
gv
crConfig
.
APIPath
=
"/apis"
crConfig
.
NegotiatedSerializer
=
serializer
.
DirectCodecFactory
{
CodecFactory
:
codecs
}
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
)
if
got
,
expected
:=
len
(
tbl
.
ColumnDefinitions
),
5
;
got
!=
expected
{
t
.
Errorf
(
"expected %d headers, got %d"
,
expected
,
got
)
}
else
{
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
)
}
}
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
),
5
;
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
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[3] to equal %#v although the type does not match the column, got %#v"
,
expected
,
got
)
}
}
}
staging/src/k8s.io/apimachinery/pkg/api/meta/table/table.go
View file @
ecdc1638
...
...
@@ -53,7 +53,7 @@ func MetaToTableRow(obj runtime.Object, rowFn func(obj runtime.Object, m metav1.
row
:=
metav1beta1
.
TableRow
{
Object
:
runtime
.
RawExtension
{
Object
:
obj
},
}
row
.
Cells
,
err
=
rowFn
(
obj
,
m
,
m
.
GetName
(),
translateTimestamp
(
m
.
GetCreationTimestamp
()))
row
.
Cells
,
err
=
rowFn
(
obj
,
m
,
m
.
GetName
(),
ConvertToHumanReadableDateType
(
m
.
GetCreationTimestamp
()))
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -61,9 +61,9 @@ func MetaToTableRow(obj runtime.Object, rowFn func(obj runtime.Object, m metav1.
return
rows
,
nil
}
//
translateTimestamp
returns the elapsed time since timestamp in
//
ConvertToHumanReadableDateType
returns the elapsed time since timestamp in
// human-readable approximation.
func
translateTimestamp
(
timestamp
metav1
.
Time
)
string
{
func
ConvertToHumanReadableDateType
(
timestamp
metav1
.
Time
)
string
{
if
timestamp
.
IsZero
()
{
return
"<unknown>"
}
...
...
staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go
View file @
ecdc1638
...
...
@@ -1320,7 +1320,7 @@ func (t *Tester) testListTableConversion(obj runtime.Object, assignFn AssignFunc
t
.
Errorf
(
"column %d has no name"
,
j
)
}
switch
column
.
Type
{
case
"string"
,
"date"
,
"integer"
:
case
"string"
,
"date"
,
"integer"
,
"number"
,
"boolean"
:
default
:
t
.
Errorf
(
"column %d has unexpected type: %q"
,
j
,
column
.
Type
)
}
...
...
@@ -1342,13 +1342,14 @@ func (t *Tester) testListTableConversion(obj runtime.Object, assignFn AssignFunc
}
for
i
,
row
:=
range
table
.
Rows
{
if
len
(
row
.
Cells
)
!=
len
(
table
.
ColumnDefinitions
)
{
t
.
Errorf
(
"row %d did not have the correct number of cells: %d in %v
"
,
i
,
len
(
table
.
ColumnDefinitions
),
row
.
Cells
)
t
.
Errorf
(
"row %d did not have the correct number of cells: %d in %v
, expected %d"
,
i
,
len
(
row
.
Cells
),
row
.
Cells
,
len
(
table
.
ColumnDefinitions
)
)
}
for
j
,
cell
:=
range
row
.
Cells
{
// do not add to this test without discussion - may break clients
switch
cell
.
(
type
)
{
case
float64
,
int64
,
int32
,
int
,
string
,
bool
:
case
[]
interface
{}
:
case
nil
:
default
:
t
.
Errorf
(
"row %d, cell %d has an unrecognized type, only JSON serialization safe types are allowed: %T "
,
i
,
j
,
cell
)
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment