mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-28 22:17:14 +00:00
Merge pull request #69449 from apelisse/verify-dry-run-support
kubectl: Verify dry run support
This commit is contained in:
commit
b1ea6dcfbc
@ -36,6 +36,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
|
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
|
||||||
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/printers:go_default_library",
|
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/printers:go_default_library",
|
||||||
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource:go_default_library",
|
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/discovery:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/dynamic:go_default_library",
|
"//staging/src/k8s.io/client-go/dynamic:go_default_library",
|
||||||
"//vendor/github.com/ghodss/yaml:go_default_library",
|
"//vendor/github.com/ghodss/yaml:go_default_library",
|
||||||
"//vendor/github.com/golang/glog:go_default_library",
|
"//vendor/github.com/golang/glog:go_default_library",
|
||||||
@ -49,6 +50,7 @@ go_test(
|
|||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = ["apply_test.go"],
|
srcs = ["apply_test.go"],
|
||||||
data = [
|
data = [
|
||||||
|
"//api/openapi-spec:swagger-spec",
|
||||||
"//test/fixtures",
|
"//test/fixtures",
|
||||||
],
|
],
|
||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
@ -71,6 +73,7 @@ go_test(
|
|||||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/rest/fake:go_default_library",
|
"//staging/src/k8s.io/client-go/rest/fake:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||||
|
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
||||||
"//vendor/github.com/spf13/cobra:go_default_library",
|
"//vendor/github.com/spf13/cobra:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -42,6 +42,7 @@ import (
|
|||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions/resource"
|
"k8s.io/cli-runtime/pkg/genericclioptions/resource"
|
||||||
|
"k8s.io/client-go/discovery"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
oapi "k8s.io/kube-openapi/pkg/util/proto"
|
oapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||||
"k8s.io/kubernetes/pkg/kubectl"
|
"k8s.io/kubernetes/pkg/kubectl"
|
||||||
@ -76,11 +77,12 @@ type ApplyOptions struct {
|
|||||||
PruneWhitelist []string
|
PruneWhitelist []string
|
||||||
ShouldIncludeUninitialized bool
|
ShouldIncludeUninitialized bool
|
||||||
|
|
||||||
Validator validation.Schema
|
Validator validation.Schema
|
||||||
Builder *resource.Builder
|
Builder *resource.Builder
|
||||||
Mapper meta.RESTMapper
|
Mapper meta.RESTMapper
|
||||||
DynamicClient dynamic.Interface
|
DynamicClient dynamic.Interface
|
||||||
OpenAPISchema openapi.Resources
|
DiscoveryClient discovery.DiscoveryInterface
|
||||||
|
OpenAPISchema openapi.Resources
|
||||||
|
|
||||||
Namespace string
|
Namespace string
|
||||||
EnforceNamespace bool
|
EnforceNamespace bool
|
||||||
@ -211,6 +213,11 @@ func (o *ApplyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
o.DiscoveryClient, err = f.ToDiscoveryClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
dynamicClient, err := f.DynamicClient()
|
dynamicClient, err := f.DynamicClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -403,19 +410,25 @@ func (o *ApplyOptions) Run() error {
|
|||||||
fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, o.cmdBaseName)
|
fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, o.cmdBaseName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dryRunVerifier := &DryRunVerifier{
|
||||||
|
Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(o.DynamicClient)),
|
||||||
|
OpenAPIGetter: o.DiscoveryClient,
|
||||||
|
}
|
||||||
|
|
||||||
helper := resource.NewHelper(info.Client, info.Mapping)
|
helper := resource.NewHelper(info.Client, info.Mapping)
|
||||||
patcher := &Patcher{
|
patcher := &Patcher{
|
||||||
Mapping: info.Mapping,
|
Mapping: info.Mapping,
|
||||||
Helper: helper,
|
Helper: helper,
|
||||||
DynamicClient: o.DynamicClient,
|
DynamicClient: o.DynamicClient,
|
||||||
Overwrite: o.Overwrite,
|
DryRunVerifier: dryRunVerifier,
|
||||||
BackOff: clockwork.NewRealClock(),
|
Overwrite: o.Overwrite,
|
||||||
Force: o.DeleteOptions.ForceDeletion,
|
BackOff: clockwork.NewRealClock(),
|
||||||
Cascade: o.DeleteOptions.Cascade,
|
Force: o.DeleteOptions.ForceDeletion,
|
||||||
Timeout: o.DeleteOptions.Timeout,
|
Cascade: o.DeleteOptions.Cascade,
|
||||||
GracePeriod: o.DeleteOptions.GracePeriod,
|
Timeout: o.DeleteOptions.Timeout,
|
||||||
ServerDryRun: o.ServerDryRun,
|
GracePeriod: o.DeleteOptions.GracePeriod,
|
||||||
OpenapiSchema: openapiSchema,
|
ServerDryRun: o.ServerDryRun,
|
||||||
|
OpenapiSchema: openapiSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, o.ErrOut)
|
patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, o.ErrOut)
|
||||||
@ -668,9 +681,10 @@ func (p *Patcher) delete(namespace, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Patcher struct {
|
type Patcher struct {
|
||||||
Mapping *meta.RESTMapping
|
Mapping *meta.RESTMapping
|
||||||
Helper *resource.Helper
|
Helper *resource.Helper
|
||||||
DynamicClient dynamic.Interface
|
DynamicClient dynamic.Interface
|
||||||
|
DryRunVerifier *DryRunVerifier
|
||||||
|
|
||||||
Overwrite bool
|
Overwrite bool
|
||||||
BackOff clockwork.Clock
|
BackOff clockwork.Clock
|
||||||
@ -684,7 +698,52 @@ type Patcher struct {
|
|||||||
OpenapiSchema openapi.Resources
|
OpenapiSchema openapi.Resources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DryRunVerifier verifies if a given group-version-kind supports DryRun
|
||||||
|
// against the current server. Sending dryRun requests to apiserver that
|
||||||
|
// don't support it will result in objects being unwillingly persisted.
|
||||||
|
//
|
||||||
|
// It reads the OpenAPI to see if the given GVK supports dryRun. If the
|
||||||
|
// GVK can not be found, we assume that CRDs will have the same level of
|
||||||
|
// support as "namespaces", and non-CRDs will not be supported. We
|
||||||
|
// delay the check for CRDs as much as possible though, since it
|
||||||
|
// requires an extra round-trip to the server.
|
||||||
|
type DryRunVerifier struct {
|
||||||
|
Finder cmdutil.CRDFinder
|
||||||
|
OpenAPIGetter discovery.OpenAPISchemaInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasSupport verifies if the given gvk supports DryRun. An error is
|
||||||
|
// returned if it doesn't.
|
||||||
|
func (v *DryRunVerifier) HasSupport(gvk schema.GroupVersionKind) error {
|
||||||
|
oapi, err := v.OpenAPIGetter.OpenAPISchema()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download openapi: %v", err)
|
||||||
|
}
|
||||||
|
supports, err := openapi.SupportsDryRun(oapi, gvk)
|
||||||
|
if err != nil {
|
||||||
|
// We assume that we couldn't find the type, then check for namespace:
|
||||||
|
supports, _ = openapi.SupportsDryRun(oapi, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})
|
||||||
|
// If namespace supports dryRun, then we will support dryRun for CRDs only.
|
||||||
|
if supports {
|
||||||
|
supports, err = v.Finder.HasCRD(gvk.GroupKind())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check CRD: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !supports {
|
||||||
|
return fmt.Errorf("%v doesn't support dry-run", gvk)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
|
func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
|
||||||
|
if p.ServerDryRun {
|
||||||
|
if err := p.DryRunVerifier.HasSupport(p.Mapping.GroupVersionKind); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize the current configuration of the object from the server.
|
// Serialize the current configuration of the object from the server.
|
||||||
current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/googleapis/gnostic/OpenAPIv2"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@ -52,7 +53,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "api", "openapi-spec", "swagger.json")}
|
fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "..", "api", "openapi-spec", "swagger.json")}
|
||||||
testingOpenAPISchemaFns = []func() (openapi.Resources, error){nil, AlwaysErrorOpenAPISchemaFn, openAPISchemaFn}
|
testingOpenAPISchemaFns = []func() (openapi.Resources, error){nil, AlwaysErrorOpenAPISchemaFn, openAPISchemaFn}
|
||||||
AlwaysErrorOpenAPISchemaFn = func() (openapi.Resources, error) {
|
AlwaysErrorOpenAPISchemaFn = func() (openapi.Resources, error) {
|
||||||
return nil, errors.New("cannot get openapi spec")
|
return nil, errors.New("cannot get openapi spec")
|
||||||
@ -1317,3 +1318,75 @@ func TestForceApply(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDryRunVerifier(t *testing.T) {
|
||||||
|
dryRunVerifier := DryRunVerifier{
|
||||||
|
Finder: cmdutil.NewCRDFinder(func() ([]schema.GroupKind, error) {
|
||||||
|
return []schema.GroupKind{
|
||||||
|
{
|
||||||
|
Group: "crd.com",
|
||||||
|
Kind: "MyCRD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Group: "crd.com",
|
||||||
|
Kind: "MyNewCRD",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
OpenAPIGetter: &fakeSchema,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "NodeProxyOptions"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("NodeProxyOptions doesn't support dry-run, yet no error found")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Pod should support dry-run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "MyCRD"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MyCRD should support dry-run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "Random"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Random doesn't support dry-run, yet no error found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmptyOpenAPI struct{}
|
||||||
|
|
||||||
|
func (EmptyOpenAPI) OpenAPISchema() (*openapi_v2.Document, error) {
|
||||||
|
return &openapi_v2.Document{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDryRunVerifierNoOpenAPI(t *testing.T) {
|
||||||
|
dryRunVerifier := DryRunVerifier{
|
||||||
|
Finder: cmdutil.NewCRDFinder(func() ([]schema.GroupKind, error) {
|
||||||
|
return []schema.GroupKind{
|
||||||
|
{
|
||||||
|
Group: "crd.com",
|
||||||
|
Kind: "MyCRD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Group: "crd.com",
|
||||||
|
Kind: "MyNewCRD",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
OpenAPIGetter: EmptyOpenAPI{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Pod doesn't support dry-run, yet no error found")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "MyCRD"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("MyCRD doesn't support dry-run, yet no error found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -226,10 +226,11 @@ type Object interface {
|
|||||||
// InfoObject is an implementation of the Object interface. It gets all
|
// InfoObject is an implementation of the Object interface. It gets all
|
||||||
// the information from the Info object.
|
// the information from the Info object.
|
||||||
type InfoObject struct {
|
type InfoObject struct {
|
||||||
LocalObj runtime.Object
|
LocalObj runtime.Object
|
||||||
Info *resource.Info
|
Info *resource.Info
|
||||||
Encoder runtime.Encoder
|
Encoder runtime.Encoder
|
||||||
OpenAPI openapi.Resources
|
OpenAPI openapi.Resources
|
||||||
|
DryRunVerifier *apply.DryRunVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Object = &InfoObject{}
|
var _ Object = &InfoObject{}
|
||||||
@ -261,12 +262,13 @@ func (obj InfoObject) Merged() (runtime.Object, error) {
|
|||||||
// This is using the patcher from apply, to keep the same behavior.
|
// This is using the patcher from apply, to keep the same behavior.
|
||||||
// We plan on replacing this with server-side apply when it becomes available.
|
// We plan on replacing this with server-side apply when it becomes available.
|
||||||
patcher := &apply.Patcher{
|
patcher := &apply.Patcher{
|
||||||
Mapping: obj.Info.Mapping,
|
DryRunVerifier: obj.DryRunVerifier,
|
||||||
Helper: resource.NewHelper(obj.Info.Client, obj.Info.Mapping),
|
Mapping: obj.Info.Mapping,
|
||||||
Overwrite: true,
|
Helper: resource.NewHelper(obj.Info.Client, obj.Info.Mapping),
|
||||||
BackOff: clockwork.NewRealClock(),
|
Overwrite: true,
|
||||||
ServerDryRun: true,
|
BackOff: clockwork.NewRealClock(),
|
||||||
OpenapiSchema: obj.OpenAPI,
|
ServerDryRun: true,
|
||||||
|
OpenapiSchema: obj.OpenAPI,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, nil)
|
_, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, nil)
|
||||||
@ -330,6 +332,21 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
discovery, err := f.ToDiscoveryClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic, err := f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dryRunVerifier := &apply.DryRunVerifier{
|
||||||
|
Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(dynamic)),
|
||||||
|
OpenAPIGetter: discovery,
|
||||||
|
}
|
||||||
|
|
||||||
differ, err := NewDiffer("LIVE", "MERGED")
|
differ, err := NewDiffer("LIVE", "MERGED")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -367,10 +384,11 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
obj := InfoObject{
|
obj := InfoObject{
|
||||||
LocalObj: local,
|
LocalObj: local,
|
||||||
Info: info,
|
Info: info,
|
||||||
Encoder: scheme.DefaultJSONEncoder(),
|
Encoder: scheme.DefaultJSONEncoder(),
|
||||||
OpenAPI: schema,
|
OpenAPI: schema,
|
||||||
|
DryRunVerifier: dryRunVerifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
return differ.Diff(obj, printer)
|
return differ.Diff(obj, printer)
|
||||||
|
@ -4,6 +4,7 @@ go_library(
|
|||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = [
|
srcs = [
|
||||||
"conversion.go",
|
"conversion.go",
|
||||||
|
"crdfinder.go",
|
||||||
"factory.go",
|
"factory.go",
|
||||||
"factory_client_access.go",
|
"factory_client_access.go",
|
||||||
"generator.go",
|
"generator.go",
|
||||||
@ -34,6 +35,7 @@ go_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",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||||
@ -57,7 +59,10 @@ go_library(
|
|||||||
|
|
||||||
go_test(
|
go_test(
|
||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = ["helpers_test.go"],
|
srcs = [
|
||||||
|
"crdfinder_test.go",
|
||||||
|
"helpers_test.go",
|
||||||
|
],
|
||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
deps = [
|
deps = [
|
||||||
"//pkg/kubectl/scheme:go_default_library",
|
"//pkg/kubectl/scheme:go_default_library",
|
||||||
|
108
pkg/kubectl/cmd/util/crdfinder.go
Normal file
108
pkg/kubectl/cmd/util/crdfinder.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CRDGetter is a function that can download the list of GVK for all
|
||||||
|
// CRDs.
|
||||||
|
type CRDGetter func() ([]schema.GroupKind, error)
|
||||||
|
|
||||||
|
func CRDFromDynamic(client dynamic.Interface) CRDGetter {
|
||||||
|
return func() ([]schema.GroupKind, error) {
|
||||||
|
list, err := client.Resource(schema.GroupVersionResource{
|
||||||
|
Group: "apiextensions.k8s.io",
|
||||||
|
Version: "v1beta1",
|
||||||
|
Resource: "curstomresourcedefinitions",
|
||||||
|
}).List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gks := []schema.GroupKind{}
|
||||||
|
|
||||||
|
// We need to parse the list to get the gvk, I guess that's fine.
|
||||||
|
for _, crd := range (*list).Items {
|
||||||
|
// Look for group, version, and kind
|
||||||
|
group, _, _ := unstructured.NestedString(crd.Object, "spec", "group")
|
||||||
|
kind, _, _ := unstructured.NestedString(crd.Object, "spec", "names", "kind")
|
||||||
|
|
||||||
|
gks = append(gks, schema.GroupKind{
|
||||||
|
Group: group,
|
||||||
|
Kind: kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return gks, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRDFinder keeps a cache of known CRDs and finds a given GVK in the
|
||||||
|
// list.
|
||||||
|
type CRDFinder interface {
|
||||||
|
HasCRD(gvk schema.GroupKind) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCRDFinder(getter CRDGetter) CRDFinder {
|
||||||
|
return &crdFinder{
|
||||||
|
getter: getter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type crdFinder struct {
|
||||||
|
getter CRDGetter
|
||||||
|
cache *[]schema.GroupKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *crdFinder) cacheCRDs() error {
|
||||||
|
if f.cache != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := f.getter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.cache = &list
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *crdFinder) findCRD(gvk schema.GroupKind) bool {
|
||||||
|
for _, crd := range *f.cache {
|
||||||
|
if reflect.DeepEqual(gvk, crd) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *crdFinder) HasCRD(gvk schema.GroupKind) (bool, error) {
|
||||||
|
if err := f.cacheCRDs(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return f.findCRD(gvk), nil
|
||||||
|
}
|
89
pkg/kubectl/cmd/util/crdfinder_test.go
Normal file
89
pkg/kubectl/cmd/util/crdfinder_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
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 util_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCacheCRDFinder(t *testing.T) {
|
||||||
|
called := 0
|
||||||
|
getter := func() ([]schema.GroupKind, error) {
|
||||||
|
called += 1
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
finder := util.NewCRDFinder(getter)
|
||||||
|
if called != 0 {
|
||||||
|
t.Fatalf("Creating the finder shouldn't call the getter, has called = %v", called)
|
||||||
|
}
|
||||||
|
_, err := finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to call HasCRD: %v", err)
|
||||||
|
}
|
||||||
|
if called != 1 {
|
||||||
|
t.Fatalf("First call should call the getter, has called = %v", called)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to call HasCRD: %v", err)
|
||||||
|
}
|
||||||
|
if called != 1 {
|
||||||
|
t.Fatalf("Second call should NOT call the getter, has called = %v", called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDFinderErrors(t *testing.T) {
|
||||||
|
getter := func() ([]schema.GroupKind, error) {
|
||||||
|
return nil, errors.New("not working")
|
||||||
|
}
|
||||||
|
finder := util.NewCRDFinder(getter)
|
||||||
|
found, err := finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"})
|
||||||
|
if found == true {
|
||||||
|
t.Fatalf("Found the CRD with non-working getter function")
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Error in getter should be reported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRDFinder(t *testing.T) {
|
||||||
|
getter := func() ([]schema.GroupKind, error) {
|
||||||
|
return []schema.GroupKind{
|
||||||
|
{
|
||||||
|
Group: "crd.com",
|
||||||
|
Kind: "MyCRD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Group: "crd.com",
|
||||||
|
Kind: "MyNewCRD",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
finder := util.NewCRDFinder(getter)
|
||||||
|
|
||||||
|
if found, _ := finder.HasCRD(schema.GroupKind{Group: "crd.com", Kind: "MyCRD"}); !found {
|
||||||
|
t.Fatalf("Failed to find CRD MyCRD")
|
||||||
|
}
|
||||||
|
if found, _ := finder.HasCRD(schema.GroupKind{Group: "crd.com", Kind: "Random"}); found {
|
||||||
|
t.Fatalf("Found crd Random that doesn't exist")
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ go_library(
|
|||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = [
|
srcs = [
|
||||||
"doc.go",
|
"doc.go",
|
||||||
|
"dryrun.go",
|
||||||
"extensions.go",
|
"extensions.go",
|
||||||
"openapi.go",
|
"openapi.go",
|
||||||
"openapi_getter.go",
|
"openapi_getter.go",
|
||||||
@ -20,6 +21,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/client-go/discovery:go_default_library",
|
"//staging/src/k8s.io/client-go/discovery:go_default_library",
|
||||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||||
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
||||||
|
"//vendor/gopkg.in/yaml.v2:go_default_library",
|
||||||
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
|
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -28,6 +30,7 @@ go_test(
|
|||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
size = "small",
|
size = "small",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"dryrun_test.go",
|
||||||
"openapi_getter_test.go",
|
"openapi_getter_test.go",
|
||||||
"openapi_suite_test.go",
|
"openapi_suite_test.go",
|
||||||
"openapi_test.go",
|
"openapi_test.go",
|
||||||
|
65
pkg/kubectl/cmd/util/openapi/dryrun.go
Normal file
65
pkg/kubectl/cmd/util/openapi/dryrun.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
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 openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hasGVKExtension(extensions []*openapi_v2.NamedAny, gvk schema.GroupVersionKind) bool {
|
||||||
|
for _, extension := range extensions {
|
||||||
|
if extension.GetValue().GetYaml() == "" ||
|
||||||
|
extension.GetName() != "x-kubernetes-group-version-kind" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var value map[string]string
|
||||||
|
err := yaml.Unmarshal([]byte(extension.GetValue().GetYaml()), &value)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if value["group"] == gvk.Group && value["kind"] == gvk.Kind && value["version"] == gvk.Version {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsDryRun is a method that let's us look in the OpenAPI if the
|
||||||
|
// specific group-version-kind supports the dryRun query parameter for
|
||||||
|
// the PATCH end-point.
|
||||||
|
func SupportsDryRun(doc *openapi_v2.Document, gvk schema.GroupVersionKind) (bool, error) {
|
||||||
|
for _, path := range doc.GetPaths().GetPath() {
|
||||||
|
// Is this describing the gvk we're looking for?
|
||||||
|
if !hasGVKExtension(path.GetValue().GetPatch().GetVendorExtension(), gvk) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, param := range path.GetValue().GetPatch().GetParameters() {
|
||||||
|
if param.GetParameter().GetNonBodyParameter().GetQueryParameterSubSchema().GetName() == "dryRun" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, errors.New("couldn't find GVK in openapi")
|
||||||
|
}
|
80
pkg/kubectl/cmd/util/openapi/dryrun_test.go
Normal file
80
pkg/kubectl/cmd/util/openapi/dryrun_test.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
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 openapi_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSupportsDryRun(t *testing.T) {
|
||||||
|
doc, err := fakeSchema.OpenAPISchema()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get OpenAPI Schema: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
gvk schema.GroupVersionKind
|
||||||
|
success bool
|
||||||
|
supports bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
gvk: schema.GroupVersionKind{
|
||||||
|
Group: "",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "Pod",
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
supports: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gvk: schema.GroupVersionKind{
|
||||||
|
Group: "",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "UnknownKind",
|
||||||
|
},
|
||||||
|
success: false,
|
||||||
|
supports: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gvk: schema.GroupVersionKind{
|
||||||
|
Group: "",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "NodeProxyOptions",
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
supports: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
supports, err := openapi.SupportsDryRun(doc, test.gvk)
|
||||||
|
if supports != test.supports || ((err == nil) != test.success) {
|
||||||
|
errStr := "nil"
|
||||||
|
if test.success == false {
|
||||||
|
errStr = "err"
|
||||||
|
}
|
||||||
|
t.Errorf("SupportsDryRun(doc, %v) = (%v, %v), expected (%v, %v)",
|
||||||
|
test.gvk,
|
||||||
|
supports, err,
|
||||||
|
test.supports, errStr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user