mirror of
https://github.com/kubernetes/kubectl.git
synced 2026-06-09 00:32:30 -04:00
Move pkg/kubectl/cmd/{command} to staging
Kubernetes-commit: 0e0ea523392f1121f61f99ac30a9bc2043eaed90
This commit is contained in:
parent
ef04362827
commit
c6063dd846
381 changed files with 53901 additions and 0 deletions
49
pkg/cmd/alpha.go
Normal file
49
pkg/cmd/alpha.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// NewCmdAlpha creates a command that acts as an alternate root command for features in alpha
|
||||
func NewCmdAlpha(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "alpha",
|
||||
Short: i18n.T("Commands for features in alpha"),
|
||||
Long: templates.LongDesc(i18n.T("These commands correspond to alpha features that are not enabled in Kubernetes clusters by default.")),
|
||||
}
|
||||
|
||||
// Alpha commands should be added here. As features graduate from alpha they should move
|
||||
// from here to the CommandGroups defined by NewKubeletCommand() in cmd.go.
|
||||
//cmd.AddCommand(NewCmdDebug(f, in, out, err))
|
||||
|
||||
// NewKubeletCommand() will hide the alpha command if it has no subcommands. Overriding
|
||||
// the help function ensures a reasonable message if someone types the hidden command anyway.
|
||||
if !cmd.HasSubCommands() {
|
||||
cmd.SetHelpFunc(func(*cobra.Command, []string) {
|
||||
cmd.Println(i18n.T("No alpha commands are available in this version of kubectl"))
|
||||
})
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
382
pkg/cmd/annotate/annotate.go
Normal file
382
pkg/cmd/annotate/annotate.go
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
/*
|
||||
Copyright 2014 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 annotate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// AnnotateOptions have the data required to perform the annotate operation
|
||||
type AnnotateOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
|
||||
// Filename options
|
||||
resource.FilenameOptions
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
|
||||
// Common user flags
|
||||
overwrite bool
|
||||
local bool
|
||||
dryrun bool
|
||||
all bool
|
||||
resourceVersion string
|
||||
selector string
|
||||
fieldSelector string
|
||||
outputFormat string
|
||||
|
||||
// results of arg parsing
|
||||
resources []string
|
||||
newAnnotations map[string]string
|
||||
removeAnnotations []string
|
||||
Recorder genericclioptions.Recorder
|
||||
namespace string
|
||||
enforceNamespace bool
|
||||
builder *resource.Builder
|
||||
unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
annotateLong = templates.LongDesc(`
|
||||
Update the annotations on one or more resources
|
||||
|
||||
All Kubernetes objects support the ability to store additional data with the object as
|
||||
annotations. Annotations are key/value pairs that can be larger than labels and include
|
||||
arbitrary string values such as structured JSON. Tools and system extensions may use
|
||||
annotations to store their own data.
|
||||
|
||||
Attempting to set an annotation that already exists will fail unless --overwrite is set.
|
||||
If --resource-version is specified and does not match the current resource version on
|
||||
the server the command will fail.`)
|
||||
|
||||
annotateExample = templates.Examples(i18n.T(`
|
||||
# Update pod 'foo' with the annotation 'description' and the value 'my frontend'.
|
||||
# If the same annotation is set multiple times, only the last value will be applied
|
||||
kubectl annotate pods foo description='my frontend'
|
||||
|
||||
# Update a pod identified by type and name in "pod.json"
|
||||
kubectl annotate -f pod.json description='my frontend'
|
||||
|
||||
# Update pod 'foo' with the annotation 'description' and the value 'my frontend running nginx', overwriting any existing value.
|
||||
kubectl annotate --overwrite pods foo description='my frontend running nginx'
|
||||
|
||||
# Update all pods in the namespace
|
||||
kubectl annotate pods --all description='my frontend running nginx'
|
||||
|
||||
# Update pod 'foo' only if the resource is unchanged from version 1.
|
||||
kubectl annotate pods foo description='my frontend running nginx' --resource-version=1
|
||||
|
||||
# Update pod 'foo' by removing an annotation named 'description' if it exists.
|
||||
# Does not require the --overwrite flag.
|
||||
kubectl annotate pods foo description-`))
|
||||
)
|
||||
|
||||
// NewAnnotateOptions creates the options for annotate
|
||||
func NewAnnotateOptions(ioStreams genericclioptions.IOStreams) *AnnotateOptions {
|
||||
return &AnnotateOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("annotated").WithTypeSetter(scheme.Scheme),
|
||||
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdAnnotate creates the `annotate` command
|
||||
func NewCmdAnnotate(parent string, f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewAnnotateOptions(ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "annotate [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Update the annotations on a resource"),
|
||||
Long: annotateLong + "\n\n" + cmdutil.SuggestAPIResources(parent),
|
||||
Example: annotateExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunAnnotate())
|
||||
},
|
||||
}
|
||||
|
||||
// bind flag structs
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddIncludeUninitializedFlag(cmd)
|
||||
cmd.Flags().BoolVar(&o.overwrite, "overwrite", o.overwrite, "If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations.")
|
||||
cmd.Flags().BoolVar(&o.local, "local", o.local, "If true, annotation will NOT contact api-server but run locally.")
|
||||
cmd.Flags().StringVarP(&o.selector, "selector", "l", o.selector, "Selector (label query) to filter on, not including uninitialized ones, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2).")
|
||||
cmd.Flags().StringVar(&o.fieldSelector, "field-selector", o.fieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
|
||||
cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all resources, including uninitialized ones, in the namespace of the specified resource types.")
|
||||
cmd.Flags().StringVar(&o.resourceVersion, "resource-version", o.resourceVersion, i18n.T("If non-empty, the annotation update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource."))
|
||||
usage := "identifying the resource to update the annotation"
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete adapts from the command line args and factory to the data required.
|
||||
func (o *AnnotateOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
o.RecordFlags.Complete(cmd)
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.outputFormat = cmdutil.GetFlagString(cmd, "output")
|
||||
o.dryrun = cmdutil.GetDryRunFlag(cmd)
|
||||
|
||||
if o.dryrun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = func(obj runtime.Object, out io.Writer) error {
|
||||
return printer.PrintObj(obj, out)
|
||||
}
|
||||
|
||||
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.builder = f.NewBuilder()
|
||||
o.unstructuredClientForMapping = f.UnstructuredClientForMapping
|
||||
|
||||
// retrieves resource and annotation args from args
|
||||
// also checks args to verify that all resources are specified before annotations
|
||||
resources, annotationArgs, err := cmdutil.GetResourcesAndPairs(args, "annotation")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.resources = resources
|
||||
o.newAnnotations, o.removeAnnotations, err = parseAnnotations(annotationArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks to the AnnotateOptions to see if there is sufficient information run the command.
|
||||
func (o AnnotateOptions) Validate() error {
|
||||
if o.all && len(o.selector) > 0 {
|
||||
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||
}
|
||||
if o.all && len(o.fieldSelector) > 0 {
|
||||
return fmt.Errorf("cannot set --all and --field-selector at the same time")
|
||||
}
|
||||
if len(o.resources) < 1 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||
return fmt.Errorf("one or more resources must be specified as <resource> <name> or <resource>/<name>")
|
||||
}
|
||||
if len(o.newAnnotations) < 1 && len(o.removeAnnotations) < 1 {
|
||||
return fmt.Errorf("at least one annotation update is required")
|
||||
}
|
||||
return validateAnnotations(o.removeAnnotations, o.newAnnotations)
|
||||
}
|
||||
|
||||
// RunAnnotate does the work
|
||||
func (o AnnotateOptions) RunAnnotate() error {
|
||||
b := o.builder.
|
||||
Unstructured().
|
||||
LocalParam(o.local).
|
||||
ContinueOnError().
|
||||
NamespaceParam(o.namespace).DefaultNamespace().
|
||||
FilenameParam(o.enforceNamespace, &o.FilenameOptions).
|
||||
Flatten()
|
||||
|
||||
if !o.local {
|
||||
b = b.LabelSelectorParam(o.selector).
|
||||
FieldSelectorParam(o.fieldSelector).
|
||||
ResourceTypeOrNameArgs(o.all, o.resources...).
|
||||
Latest()
|
||||
}
|
||||
|
||||
r := b.Do()
|
||||
if err := r.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var singleItemImpliedResource bool
|
||||
r.IntoSingleItemImplied(&singleItemImpliedResource)
|
||||
|
||||
// only apply resource version locking on a single resource.
|
||||
// we must perform this check after o.builder.Do() as
|
||||
// []o.resources can not accurately return the proper number
|
||||
// of resources when they are not passed in "resource/name" format.
|
||||
if !singleItemImpliedResource && len(o.resourceVersion) > 0 {
|
||||
return fmt.Errorf("--resource-version may only be used with a single resource")
|
||||
}
|
||||
|
||||
return r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var outputObj runtime.Object
|
||||
obj := info.Object
|
||||
|
||||
if o.dryrun || o.local {
|
||||
if err := o.updateAnnotations(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
outputObj = obj
|
||||
} else {
|
||||
name, namespace := info.Name, info.Namespace
|
||||
oldData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.Recorder.Record(info.Object); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
if err := o.updateAnnotations(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
newData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData)
|
||||
createdPatch := err == nil
|
||||
if err != nil {
|
||||
klog.V(2).Infof("couldn't compute patch: %v", err)
|
||||
}
|
||||
|
||||
mapping := info.ResourceMapping()
|
||||
client, err := o.unstructuredClientForMapping(mapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
helper := resource.NewHelper(client, mapping)
|
||||
|
||||
if createdPatch {
|
||||
outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes, nil)
|
||||
} else {
|
||||
outputObj, err = helper.Replace(namespace, name, false, obj)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return o.PrintObj(outputObj, o.Out)
|
||||
})
|
||||
}
|
||||
|
||||
// parseAnnotations retrieves new and remove annotations from annotation args
|
||||
func parseAnnotations(annotationArgs []string) (map[string]string, []string, error) {
|
||||
return cmdutil.ParsePairs(annotationArgs, "annotation", true)
|
||||
}
|
||||
|
||||
// validateAnnotations checks the format of annotation args and checks removed annotations aren't in the new annotations map
|
||||
func validateAnnotations(removeAnnotations []string, newAnnotations map[string]string) error {
|
||||
var modifyRemoveBuf bytes.Buffer
|
||||
for _, removeAnnotation := range removeAnnotations {
|
||||
if _, found := newAnnotations[removeAnnotation]; found {
|
||||
if modifyRemoveBuf.Len() > 0 {
|
||||
modifyRemoveBuf.WriteString(", ")
|
||||
}
|
||||
modifyRemoveBuf.WriteString(fmt.Sprintf(removeAnnotation))
|
||||
}
|
||||
}
|
||||
if modifyRemoveBuf.Len() > 0 {
|
||||
return fmt.Errorf("can not both modify and remove the following annotation(s) in the same command: %s", modifyRemoveBuf.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateNoAnnotationOverwrites validates that when overwrite is false, to-be-updated annotations don't exist in the object annotation map (yet)
|
||||
func validateNoAnnotationOverwrites(accessor metav1.Object, annotations map[string]string) error {
|
||||
var buf bytes.Buffer
|
||||
for key := range annotations {
|
||||
// change-cause annotation can always be overwritten
|
||||
if key == polymorphichelpers.ChangeCauseAnnotation {
|
||||
continue
|
||||
}
|
||||
if value, found := accessor.GetAnnotations()[key]; found {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteString("; ")
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("'%s' already has a value (%s)", key, value))
|
||||
}
|
||||
}
|
||||
if buf.Len() > 0 {
|
||||
return fmt.Errorf("--overwrite is false but found the following declared annotation(s): %s", buf.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateAnnotations updates annotations of obj
|
||||
func (o AnnotateOptions) updateAnnotations(obj runtime.Object) error {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !o.overwrite {
|
||||
if err := validateNoAnnotationOverwrites(accessor, o.newAnnotations); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
annotations := accessor.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
for key, value := range o.newAnnotations {
|
||||
annotations[key] = value
|
||||
}
|
||||
for _, annotation := range o.removeAnnotations {
|
||||
delete(annotations, annotation)
|
||||
}
|
||||
accessor.SetAnnotations(annotations)
|
||||
|
||||
if len(o.resourceVersion) != 0 {
|
||||
accessor.SetResourceVersion(o.resourceVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
643
pkg/cmd/annotate/annotate_test.go
Normal file
643
pkg/cmd/annotate/annotate_test.go
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
/*
|
||||
Copyright 2014 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 annotate
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestValidateAnnotationOverwrites(t *testing.T) {
|
||||
tests := []struct {
|
||||
meta *metav1.ObjectMeta
|
||||
annotations map[string]string
|
||||
expectErr bool
|
||||
scenario string
|
||||
}{
|
||||
{
|
||||
meta: &metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"a": "a",
|
||||
"c": "C",
|
||||
},
|
||||
scenario: "share first annotation",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
meta: &metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
"a": "A",
|
||||
"c": "C",
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"b": "B",
|
||||
"c": "c",
|
||||
},
|
||||
scenario: "share second annotation",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
meta: &metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
"a": "A",
|
||||
"c": "C",
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"b": "B",
|
||||
"d": "D",
|
||||
},
|
||||
scenario: "no overlap",
|
||||
},
|
||||
{
|
||||
meta: &metav1.ObjectMeta{},
|
||||
annotations: map[string]string{
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
},
|
||||
scenario: "no annotations",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
err := validateNoAnnotationOverwrites(test.meta, test.annotations)
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("%s: unexpected non-error", test.scenario)
|
||||
} else if !test.expectErr && err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", test.scenario, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAnnotations(t *testing.T) {
|
||||
testURL := "https://test.com/index.htm?id=123#u=user-name"
|
||||
testJSON := `'{"kind":"SerializedReference","apiVersion":"v1","reference":{"kind":"ReplicationController","namespace":"default","name":"my-nginx","uid":"c544ee78-2665-11e5-8051-42010af0c213","apiVersion":"v1","resourceVersion":"61368"}}'`
|
||||
tests := []struct {
|
||||
annotations []string
|
||||
expected map[string]string
|
||||
expectedRemove []string
|
||||
scenario string
|
||||
expectedErr string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
annotations: []string{"a=b", "c=d"},
|
||||
expected: map[string]string{"a": "b", "c": "d"},
|
||||
expectedRemove: []string{},
|
||||
scenario: "add two annotations",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
annotations: []string{"url=" + testURL, "fake.kubernetes.io/annotation=" + testJSON},
|
||||
expected: map[string]string{"url": testURL, "fake.kubernetes.io/annotation": testJSON},
|
||||
expectedRemove: []string{},
|
||||
scenario: "add annotations with special characters",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
annotations: []string{},
|
||||
expected: map[string]string{},
|
||||
expectedRemove: []string{},
|
||||
scenario: "add no annotations",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
annotations: []string{"a=b", "c=d", "e-"},
|
||||
expected: map[string]string{"a": "b", "c": "d"},
|
||||
expectedRemove: []string{"e"},
|
||||
scenario: "add two annotations, remove one",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
annotations: []string{"ab", "c=d"},
|
||||
expectedErr: "invalid annotation format: ab",
|
||||
scenario: "incorrect annotation input (missing =value)",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
annotations: []string{"a="},
|
||||
expected: map[string]string{"a": ""},
|
||||
expectedRemove: []string{},
|
||||
scenario: "add valid annotation with empty value",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
annotations: []string{"ab", "a="},
|
||||
expectedErr: "invalid annotation format: ab",
|
||||
scenario: "incorrect annotation input (missing =value)",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
annotations: []string{"-"},
|
||||
expectedErr: "invalid annotation format: -",
|
||||
scenario: "incorrect annotation input (missing key)",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
annotations: []string{"=bar"},
|
||||
expectedErr: "invalid annotation format: =bar",
|
||||
scenario: "incorrect annotation input (missing key)",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
annotations, remove, err := parseAnnotations(test.annotations)
|
||||
switch {
|
||||
case test.expectErr && err == nil:
|
||||
t.Errorf("%s: unexpected non-error, should return %v", test.scenario, test.expectedErr)
|
||||
case test.expectErr && err.Error() != test.expectedErr:
|
||||
t.Errorf("%s: unexpected error %v, expected %v", test.scenario, err, test.expectedErr)
|
||||
case !test.expectErr && err != nil:
|
||||
t.Errorf("%s: unexpected error %v", test.scenario, err)
|
||||
case !test.expectErr && !reflect.DeepEqual(annotations, test.expected):
|
||||
t.Errorf("%s: expected %v, got %v", test.scenario, test.expected, annotations)
|
||||
case !test.expectErr && !reflect.DeepEqual(remove, test.expectedRemove):
|
||||
t.Errorf("%s: expected %v, got %v", test.scenario, test.expectedRemove, remove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAnnotations(t *testing.T) {
|
||||
tests := []struct {
|
||||
removeAnnotations []string
|
||||
newAnnotations map[string]string
|
||||
expectedErr string
|
||||
scenario string
|
||||
}{
|
||||
{
|
||||
expectedErr: "can not both modify and remove the following annotation(s) in the same command: a",
|
||||
removeAnnotations: []string{"a"},
|
||||
newAnnotations: map[string]string{"a": "b", "c": "d"},
|
||||
scenario: "remove an added annotation",
|
||||
},
|
||||
{
|
||||
expectedErr: "can not both modify and remove the following annotation(s) in the same command: a, c",
|
||||
removeAnnotations: []string{"a", "c"},
|
||||
newAnnotations: map[string]string{"a": "b", "c": "d"},
|
||||
scenario: "remove added annotations",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if err := validateAnnotations(test.removeAnnotations, test.newAnnotations); err == nil {
|
||||
t.Errorf("%s: unexpected non-error", test.scenario)
|
||||
} else if err.Error() != test.expectedErr {
|
||||
t.Errorf("%s: expected error %s, got %s", test.scenario, test.expectedErr, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAnnotations(t *testing.T) {
|
||||
tests := []struct {
|
||||
obj runtime.Object
|
||||
overwrite bool
|
||||
version string
|
||||
annotations map[string]string
|
||||
remove []string
|
||||
expected runtime.Object
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b"},
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{"a": "b"},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b"},
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{"a": "c"},
|
||||
overwrite: true,
|
||||
expected: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "c"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b"},
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{"c": "d"},
|
||||
expected: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b", "c": "d"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b"},
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{"c": "d"},
|
||||
version: "2",
|
||||
expected: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b", "c": "d"},
|
||||
ResourceVersion: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b"},
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{},
|
||||
remove: []string{"a"},
|
||||
expected: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b", "c": "d"},
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{"e": "f"},
|
||||
remove: []string{"a"},
|
||||
expected: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
"c": "d",
|
||||
"e": "f",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b", "c": "d"},
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{"e": "f"},
|
||||
remove: []string{"g"},
|
||||
expected: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
"a": "b",
|
||||
"c": "d",
|
||||
"e": "f",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b", "c": "d"},
|
||||
},
|
||||
},
|
||||
remove: []string{"e"},
|
||||
expected: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
"a": "b",
|
||||
"c": "d",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{},
|
||||
},
|
||||
annotations: map[string]string{"a": "b"},
|
||||
expected: &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"a": "b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
options := &AnnotateOptions{
|
||||
overwrite: test.overwrite,
|
||||
newAnnotations: test.annotations,
|
||||
removeAnnotations: test.remove,
|
||||
resourceVersion: test.version,
|
||||
}
|
||||
err := options.updateAnnotations(test.obj)
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Errorf("unexpected non-error: %v", test)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("unexpected error: %v %v", err, test)
|
||||
}
|
||||
if !reflect.DeepEqual(test.obj, test.expected) {
|
||||
t.Errorf("expected: %v, got %v", test.expected, test.obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotateErrors(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
errFn func(error) bool
|
||||
}{
|
||||
"no args": {
|
||||
args: []string{},
|
||||
errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
|
||||
},
|
||||
"not enough annotations": {
|
||||
args: []string{"pods"},
|
||||
errFn: func(err error) bool {
|
||||
return strings.Contains(err.Error(), "at least one annotation update is required")
|
||||
},
|
||||
},
|
||||
"wrong annotations": {
|
||||
args: []string{"pods", "-"},
|
||||
errFn: func(err error) bool {
|
||||
return strings.Contains(err.Error(), "at least one annotation update is required")
|
||||
},
|
||||
},
|
||||
"wrong annotations 2": {
|
||||
args: []string{"pods", "=bar"},
|
||||
errFn: func(err error) bool {
|
||||
return strings.Contains(err.Error(), "at least one annotation update is required")
|
||||
},
|
||||
},
|
||||
"no resources remove annotations": {
|
||||
args: []string{"pods-"},
|
||||
errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
|
||||
},
|
||||
"no resources add annotations": {
|
||||
args: []string{"pods=bar"},
|
||||
errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") },
|
||||
},
|
||||
}
|
||||
|
||||
for k, testCase := range testCases {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
iostreams, _, bufOut, bufErr := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdAnnotate("kubectl", tf, iostreams)
|
||||
cmd.SetOutput(bufOut)
|
||||
|
||||
for k, v := range testCase.flags {
|
||||
cmd.Flags().Set(k, v)
|
||||
}
|
||||
options := NewAnnotateOptions(iostreams)
|
||||
err := options.Complete(tf, cmd, testCase.args)
|
||||
if err == nil {
|
||||
err = options.Validate()
|
||||
}
|
||||
if !testCase.errFn(err) {
|
||||
t.Errorf("%s: unexpected error: %v", k, err)
|
||||
return
|
||||
}
|
||||
if bufOut.Len() > 0 {
|
||||
t.Errorf("buffer should be empty: %s", string(bufOut.Bytes()))
|
||||
}
|
||||
if bufErr.Len() > 0 {
|
||||
t.Errorf("buffer should be empty: %s", string(bufErr.Bytes()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotateObject(t *testing.T) {
|
||||
pods, _, _ := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"},
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
switch req.URL.Path {
|
||||
case "/namespaces/test/pods/foo":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
case "PATCH":
|
||||
switch req.URL.Path {
|
||||
case "/namespaces/test/pods/foo":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
iostreams, _, bufOut, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdAnnotate("kubectl", tf, iostreams)
|
||||
cmd.SetOutput(bufOut)
|
||||
options := NewAnnotateOptions(iostreams)
|
||||
args := []string{"pods/foo", "a=b", "c-"}
|
||||
if err := options.Complete(tf, cmd, args); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := options.Validate(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := options.RunAnnotate(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotateObjectFromFile(t *testing.T) {
|
||||
pods, _, _ := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"},
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
switch req.URL.Path {
|
||||
case "/namespaces/test/replicationcontrollers/cassandra":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
case "PATCH":
|
||||
switch req.URL.Path {
|
||||
case "/namespaces/test/replicationcontrollers/cassandra":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
iostreams, _, bufOut, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdAnnotate("kubectl", tf, iostreams)
|
||||
cmd.SetOutput(bufOut)
|
||||
options := NewAnnotateOptions(iostreams)
|
||||
options.Filenames = []string{"../../../../test/e2e/testing-manifests/statefulset/cassandra/controller.yaml"}
|
||||
args := []string{"a=b", "c-"}
|
||||
if err := options.Complete(tf, cmd, args); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := options.Validate(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := options.RunAnnotate(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotateLocal(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"},
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
iostreams, _, _, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdAnnotate("kubectl", tf, iostreams)
|
||||
options := NewAnnotateOptions(iostreams)
|
||||
options.local = true
|
||||
options.Filenames = []string{"../../../../test/e2e/testing-manifests/statefulset/cassandra/controller.yaml"}
|
||||
args := []string{"a=b"}
|
||||
if err := options.Complete(tf, cmd, args); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := options.Validate(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := options.RunAnnotate(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotateMultipleObjects(t *testing.T) {
|
||||
pods, _, _ := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"},
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
switch req.URL.Path {
|
||||
case "/namespaces/test/pods":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
case "PATCH":
|
||||
switch req.URL.Path {
|
||||
case "/namespaces/test/pods/foo":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
|
||||
case "/namespaces/test/pods/bar":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[1])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
iostreams, _, _, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdAnnotate("kubectl", tf, iostreams)
|
||||
cmd.SetOutput(iostreams.Out)
|
||||
options := NewAnnotateOptions(iostreams)
|
||||
options.all = true
|
||||
args := []string{"pods", "a=b", "c-"}
|
||||
if err := options.Complete(tf, cmd, args); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := options.Validate(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := options.RunAnnotate(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
248
pkg/cmd/apiresources/apiresources.go
Normal file
248
pkg/cmd/apiresources/apiresources.go
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
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 apiresources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/printers"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
apiresourcesExample = templates.Examples(`
|
||||
# Print the supported API Resources
|
||||
kubectl api-resources
|
||||
|
||||
# Print the supported API Resources with more information
|
||||
kubectl api-resources -o wide
|
||||
|
||||
# Print the supported namespaced resources
|
||||
kubectl api-resources --namespaced=true
|
||||
|
||||
# Print the supported non-namespaced resources
|
||||
kubectl api-resources --namespaced=false
|
||||
|
||||
# Print the supported API Resources with specific APIGroup
|
||||
kubectl api-resources --api-group=extensions`)
|
||||
)
|
||||
|
||||
// APIResourceOptions is the start of the data required to perform the operation.
|
||||
// As new fields are added, add them here instead of referencing the cmd.Flags()
|
||||
type APIResourceOptions struct {
|
||||
Output string
|
||||
APIGroup string
|
||||
Namespaced bool
|
||||
Verbs []string
|
||||
NoHeaders bool
|
||||
Cached bool
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// groupResource contains the APIGroup and APIResource
|
||||
type groupResource struct {
|
||||
APIGroup string
|
||||
APIResource metav1.APIResource
|
||||
}
|
||||
|
||||
// NewAPIResourceOptions creates the options for APIResource
|
||||
func NewAPIResourceOptions(ioStreams genericclioptions.IOStreams) *APIResourceOptions {
|
||||
return &APIResourceOptions{
|
||||
IOStreams: ioStreams,
|
||||
Namespaced: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdAPIResources creates the `api-resources` command
|
||||
func NewCmdAPIResources(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewAPIResourceOptions(ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "api-resources",
|
||||
Short: "Print the supported API resources on the server",
|
||||
Long: "Print the supported API resources on the server",
|
||||
Example: apiresourcesExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunAPIResources(cmd, f))
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).")
|
||||
cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, "Output format. One of: wide|name.")
|
||||
|
||||
cmd.Flags().StringVar(&o.APIGroup, "api-group", o.APIGroup, "Limit to resources in the specified API group.")
|
||||
cmd.Flags().BoolVar(&o.Namespaced, "namespaced", o.Namespaced, "If false, non-namespaced resources will be returned, otherwise returning namespaced resources by default.")
|
||||
cmd.Flags().StringSliceVar(&o.Verbs, "verbs", o.Verbs, "Limit to resources that support the specified verbs.")
|
||||
cmd.Flags().BoolVar(&o.Cached, "cached", o.Cached, "Use the cached list of resources if available.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Validate checks to the APIResourceOptions to see if there is sufficient information run the command
|
||||
func (o *APIResourceOptions) Validate() error {
|
||||
supportedOutputTypes := sets.NewString("", "wide", "name")
|
||||
if !supportedOutputTypes.Has(o.Output) {
|
||||
return fmt.Errorf("--output %v is not available", o.Output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Complete adapts from the command line args and validates them
|
||||
func (o *APIResourceOptions) Complete(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunAPIResources does the work
|
||||
func (o *APIResourceOptions) RunAPIResources(cmd *cobra.Command, f cmdutil.Factory) error {
|
||||
w := printers.GetNewTabWriter(o.Out)
|
||||
defer w.Flush()
|
||||
|
||||
discoveryclient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !o.Cached {
|
||||
// Always request fresh data from the server
|
||||
discoveryclient.Invalidate()
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
lists, err := discoveryclient.ServerPreferredResources()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
resources := []groupResource{}
|
||||
|
||||
groupChanged := cmd.Flags().Changed("api-group")
|
||||
nsChanged := cmd.Flags().Changed("namespaced")
|
||||
|
||||
for _, list := range lists {
|
||||
if len(list.APIResources) == 0 {
|
||||
continue
|
||||
}
|
||||
gv, err := schema.ParseGroupVersion(list.GroupVersion)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, resource := range list.APIResources {
|
||||
if len(resource.Verbs) == 0 {
|
||||
continue
|
||||
}
|
||||
// filter apiGroup
|
||||
if groupChanged && o.APIGroup != gv.Group {
|
||||
continue
|
||||
}
|
||||
// filter namespaced
|
||||
if nsChanged && o.Namespaced != resource.Namespaced {
|
||||
continue
|
||||
}
|
||||
// filter to resources that support the specified verbs
|
||||
if len(o.Verbs) > 0 && !sets.NewString(resource.Verbs...).HasAll(o.Verbs...) {
|
||||
continue
|
||||
}
|
||||
resources = append(resources, groupResource{
|
||||
APIGroup: gv.Group,
|
||||
APIResource: resource,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if o.NoHeaders == false && o.Output != "name" {
|
||||
if err = printContextHeaders(w, o.Output); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sort.Stable(sortableGroupResource(resources))
|
||||
for _, r := range resources {
|
||||
switch o.Output {
|
||||
case "name":
|
||||
name := r.APIResource.Name
|
||||
if len(r.APIGroup) > 0 {
|
||||
name += "." + r.APIGroup
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s\n", name); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
case "wide":
|
||||
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\t%v\n",
|
||||
r.APIResource.Name,
|
||||
strings.Join(r.APIResource.ShortNames, ","),
|
||||
r.APIGroup,
|
||||
r.APIResource.Namespaced,
|
||||
r.APIResource.Kind,
|
||||
r.APIResource.Verbs); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
case "":
|
||||
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n",
|
||||
r.APIResource.Name,
|
||||
strings.Join(r.APIResource.ShortNames, ","),
|
||||
r.APIGroup,
|
||||
r.APIResource.Namespaced,
|
||||
r.APIResource.Kind); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.NewAggregate(errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printContextHeaders(out io.Writer, output string) error {
|
||||
columnNames := []string{"NAME", "SHORTNAMES", "APIGROUP", "NAMESPACED", "KIND"}
|
||||
if output == "wide" {
|
||||
columnNames = append(columnNames, "VERBS")
|
||||
}
|
||||
_, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t"))
|
||||
return err
|
||||
}
|
||||
|
||||
type sortableGroupResource []groupResource
|
||||
|
||||
func (s sortableGroupResource) Len() int { return len(s) }
|
||||
func (s sortableGroupResource) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s sortableGroupResource) Less(i, j int) bool {
|
||||
ret := strings.Compare(s[i].APIGroup, s[j].APIGroup)
|
||||
if ret > 0 {
|
||||
return false
|
||||
} else if ret == 0 {
|
||||
return strings.Compare(s[i].APIResource.Name, s[j].APIResource.Name) < 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
97
pkg/cmd/apiresources/apiversions.go
Normal file
97
pkg/cmd/apiresources/apiversions.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2014 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 apiresources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/discovery"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
apiversionsExample = templates.Examples(i18n.T(`
|
||||
# Print the supported API versions
|
||||
kubectl api-versions`))
|
||||
)
|
||||
|
||||
// APIVersionsOptions have the data required for API versions
|
||||
type APIVersionsOptions struct {
|
||||
discoveryClient discovery.CachedDiscoveryInterface
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewAPIVersionsOptions creates the options for APIVersions
|
||||
func NewAPIVersionsOptions(ioStreams genericclioptions.IOStreams) *APIVersionsOptions {
|
||||
return &APIVersionsOptions{
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdAPIVersions creates the `api-versions` command
|
||||
func NewCmdAPIVersions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewAPIVersionsOptions(ioStreams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "api-versions",
|
||||
Short: "Print the supported API versions on the server, in the form of \"group/version\"",
|
||||
Long: "Print the supported API versions on the server, in the form of \"group/version\"",
|
||||
Example: apiversionsExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.RunAPIVersions())
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete adapts from the command line args and factory to the data required
|
||||
func (o *APIVersionsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args)
|
||||
}
|
||||
var err error
|
||||
o.discoveryClient, err = f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunAPIVersions does the work
|
||||
func (o *APIVersionsOptions) RunAPIVersions() error {
|
||||
// Always request fresh data from the server
|
||||
o.discoveryClient.Invalidate()
|
||||
|
||||
groupList, err := o.discoveryClient.ServerGroups()
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't get available api versions from server: %v", err)
|
||||
}
|
||||
apiVersions := metav1.ExtractGroupVersions(groupList)
|
||||
sort.Strings(apiVersions)
|
||||
for _, v := range apiVersions {
|
||||
fmt.Fprintln(o.Out, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
997
pkg/cmd/apply/apply.go
Normal file
997
pkg/cmd/apply/apply.go
Normal file
|
|
@ -0,0 +1,997 @@
|
|||
/*
|
||||
Copyright 2014 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 apply
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/jsonmergepatch"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/klog"
|
||||
oapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/delete"
|
||||
)
|
||||
|
||||
// ApplyOptions defines flags and other configuration parameters for the `apply` command
|
||||
type ApplyOptions struct {
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
Recorder genericclioptions.Recorder
|
||||
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||
|
||||
DeleteFlags *delete.DeleteFlags
|
||||
DeleteOptions *delete.DeleteOptions
|
||||
|
||||
ServerSideApply bool
|
||||
ForceConflicts bool
|
||||
FieldManager string
|
||||
Selector string
|
||||
DryRun bool
|
||||
ServerDryRun bool
|
||||
Prune bool
|
||||
PruneResources []pruneResource
|
||||
cmdBaseName string
|
||||
All bool
|
||||
Overwrite bool
|
||||
OpenAPIPatch bool
|
||||
PruneWhitelist []string
|
||||
|
||||
Validator validation.Schema
|
||||
Builder *resource.Builder
|
||||
Mapper meta.RESTMapper
|
||||
DynamicClient dynamic.Interface
|
||||
DiscoveryClient discovery.DiscoveryInterface
|
||||
OpenAPISchema openapi.Resources
|
||||
|
||||
Namespace string
|
||||
EnforceNamespace bool
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
const (
|
||||
// maxPatchRetry is the maximum number of conflicts retry for during a patch operation before returning failure
|
||||
maxPatchRetry = 5
|
||||
// backOffPeriod is the period to back off when apply patch results in error.
|
||||
backOffPeriod = 1 * time.Second
|
||||
// how many times we can retry before back off
|
||||
triesBeforeBackOff = 1
|
||||
)
|
||||
|
||||
var (
|
||||
applyLong = templates.LongDesc(i18n.T(`
|
||||
Apply a configuration to a resource by filename or stdin.
|
||||
The resource name must be specified. This resource will be created if it doesn't exist yet.
|
||||
To use 'apply', always create the resource initially with either 'apply' or 'create --save-config'.
|
||||
|
||||
JSON and YAML formats are accepted.
|
||||
|
||||
Alpha Disclaimer: the --prune functionality is not yet complete. Do not use unless you are aware of what the current state is. See https://issues.k8s.io/34274.`))
|
||||
|
||||
applyExample = templates.Examples(i18n.T(`
|
||||
# Apply the configuration in pod.json to a pod.
|
||||
kubectl apply -f ./pod.json
|
||||
|
||||
# Apply resources from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml.
|
||||
kubectl apply -k dir/
|
||||
|
||||
# Apply the JSON passed into stdin to a pod.
|
||||
cat pod.json | kubectl apply -f -
|
||||
|
||||
# Note: --prune is still in Alpha
|
||||
# Apply the configuration in manifest.yaml that matches label app=nginx and delete all the other resources that are not in the file and match label app=nginx.
|
||||
kubectl apply --prune -f manifest.yaml -l app=nginx
|
||||
|
||||
# Apply the configuration in manifest.yaml and delete all the other configmaps that are not in the file.
|
||||
kubectl apply --prune -f manifest.yaml --all --prune-whitelist=core/v1/ConfigMap`))
|
||||
|
||||
warningNoLastAppliedConfigAnnotation = "Warning: %[1]s apply should be used on resource created by either %[1]s create --save-config or %[1]s apply\n"
|
||||
)
|
||||
|
||||
// NewApplyOptions creates new ApplyOptions for the `apply` command
|
||||
func NewApplyOptions(ioStreams genericclioptions.IOStreams) *ApplyOptions {
|
||||
return &ApplyOptions{
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
DeleteFlags: delete.NewDeleteFlags("that contains the configuration to apply"),
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
|
||||
Overwrite: true,
|
||||
OpenAPIPatch: true,
|
||||
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdApply creates the `apply` command
|
||||
func NewCmdApply(baseName string, f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewApplyOptions(ioStreams)
|
||||
|
||||
// Store baseName for use in printing warnings / messages involving the base command name.
|
||||
// This is useful for downstream command that wrap this one.
|
||||
o.cmdBaseName = baseName
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "apply (-f FILENAME | -k DIRECTORY)",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Apply a configuration to a resource by filename or stdin"),
|
||||
Long: applyLong,
|
||||
Example: applyExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd))
|
||||
cmdutil.CheckErr(validateArgs(cmd, args))
|
||||
cmdutil.CheckErr(validatePruneAll(o.Prune, o.All, o.Selector))
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
// bind flag structs
|
||||
o.DeleteFlags.AddFlags(cmd)
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmd.Flags().BoolVar(&o.Overwrite, "overwrite", o.Overwrite, "Automatically resolve conflicts between the modified and live configuration by using values from the modified configuration")
|
||||
cmd.Flags().BoolVar(&o.Prune, "prune", o.Prune, "Automatically delete resource objects, including the uninitialized ones, that do not appear in the configs and are created by either apply or create --save-config. Should be used with either -l or --all.")
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||
cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources in the namespace of the specified resource types.")
|
||||
cmd.Flags().StringArrayVar(&o.PruneWhitelist, "prune-whitelist", o.PruneWhitelist, "Overwrite the default whitelist with <group/version/kind> for --prune")
|
||||
cmd.Flags().BoolVar(&o.OpenAPIPatch, "openapi-patch", o.OpenAPIPatch, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.")
|
||||
cmd.Flags().BoolVar(&o.ServerDryRun, "server-dry-run", o.ServerDryRun, "If true, request will be sent to server with dry-run flag, which means the modifications won't be persisted. This is an alpha feature and flag.")
|
||||
cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it. Warning: --dry-run cannot accurately output the result of merging the local manifest and the server-side data. Use --server-dry-run to get the merged result instead.")
|
||||
cmdutil.AddIncludeUninitializedFlag(cmd)
|
||||
cmdutil.AddServerSideApplyFlags(cmd)
|
||||
|
||||
// apply subcommands
|
||||
cmd.AddCommand(NewCmdApplyViewLastApplied(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdApplySetLastApplied(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdApplyEditLastApplied(f, ioStreams))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete verifies if ApplyOptions are valid and without conflicts.
|
||||
func (o *ApplyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||
o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd)
|
||||
o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd)
|
||||
o.FieldManager = cmdutil.GetFieldManagerFlag(cmd)
|
||||
o.DryRun = cmdutil.GetDryRunFlag(cmd)
|
||||
|
||||
if o.ForceConflicts && !o.ServerSideApply {
|
||||
return fmt.Errorf("--experimental-force-conflicts only works with --experimental-server-side")
|
||||
}
|
||||
|
||||
if o.DryRun && o.ServerSideApply {
|
||||
return fmt.Errorf("--dry-run doesn't work with --experimental-server-side (did you mean --server-dry-run instead?)")
|
||||
}
|
||||
|
||||
if o.DryRun && o.ServerDryRun {
|
||||
return fmt.Errorf("--dry-run and --server-dry-run can't be used together")
|
||||
}
|
||||
|
||||
// allow for a success message operation to be specified at print time
|
||||
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||
if o.DryRun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
if o.ServerDryRun {
|
||||
o.PrintFlags.Complete("%s (server dry run)")
|
||||
}
|
||||
return o.PrintFlags.ToPrinter()
|
||||
}
|
||||
|
||||
var err error
|
||||
o.RecordFlags.Complete(cmd)
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.DiscoveryClient, err = f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.DeleteOptions = o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams)
|
||||
err = o.DeleteOptions.FilenameOptions.RequireFilenameOrKustomize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.OpenAPISchema, _ = f.OpenAPISchema()
|
||||
o.Validator, err = f.Validator(cmdutil.GetFlagBool(cmd, "validate"))
|
||||
o.Builder = f.NewBuilder()
|
||||
o.Mapper, err = f.ToRESTMapper()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.DynamicClient, err = f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateArgs(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePruneAll(prune, all bool, selector string) error {
|
||||
if all && len(selector) > 0 {
|
||||
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||
}
|
||||
if prune && !all && selector == "" {
|
||||
return fmt.Errorf("all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePruneResources(mapper meta.RESTMapper, gvks []string) ([]pruneResource, error) {
|
||||
pruneResources := []pruneResource{}
|
||||
for _, groupVersionKind := range gvks {
|
||||
gvk := strings.Split(groupVersionKind, "/")
|
||||
if len(gvk) != 3 {
|
||||
return nil, fmt.Errorf("invalid GroupVersionKind format: %v, please follow <group/version/kind>", groupVersionKind)
|
||||
}
|
||||
|
||||
if gvk[0] == "core" {
|
||||
gvk[0] = ""
|
||||
}
|
||||
mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gvk[0], Kind: gvk[2]}, gvk[1])
|
||||
if err != nil {
|
||||
return pruneResources, err
|
||||
}
|
||||
var namespaced bool
|
||||
namespaceScope := mapping.Scope.Name()
|
||||
switch namespaceScope {
|
||||
case meta.RESTScopeNameNamespace:
|
||||
namespaced = true
|
||||
case meta.RESTScopeNameRoot:
|
||||
namespaced = false
|
||||
default:
|
||||
return pruneResources, fmt.Errorf("Unknown namespace scope: %q", namespaceScope)
|
||||
}
|
||||
|
||||
pruneResources = append(pruneResources, pruneResource{gvk[0], gvk[1], gvk[2], namespaced})
|
||||
}
|
||||
return pruneResources, nil
|
||||
}
|
||||
|
||||
func isIncompatibleServerError(err error) bool {
|
||||
// 415: Unsupported media type means we're talking to a server which doesn't
|
||||
// support server-side apply.
|
||||
if _, ok := err.(*errors.StatusError); !ok {
|
||||
// Non-StatusError means the error isn't because the server is incompatible.
|
||||
return false
|
||||
}
|
||||
return err.(*errors.StatusError).Status().Code == http.StatusUnsupportedMediaType
|
||||
}
|
||||
|
||||
// Run executes the `apply` command.
|
||||
func (o *ApplyOptions) Run() error {
|
||||
var openapiSchema openapi.Resources
|
||||
if o.OpenAPIPatch {
|
||||
openapiSchema = o.OpenAPISchema
|
||||
}
|
||||
|
||||
dryRunVerifier := &DryRunVerifier{
|
||||
Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(o.DynamicClient)),
|
||||
OpenAPIGetter: o.DiscoveryClient,
|
||||
}
|
||||
|
||||
// include the uninitialized objects by default if --prune is true
|
||||
// unless explicitly set --include-uninitialized=false
|
||||
r := o.Builder.
|
||||
Unstructured().
|
||||
Schema(o.Validator).
|
||||
ContinueOnError().
|
||||
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
|
||||
LabelSelectorParam(o.Selector).
|
||||
Flatten().
|
||||
Do()
|
||||
if err := r.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
if o.Prune {
|
||||
o.PruneResources, err = parsePruneResources(o.Mapper, o.PruneWhitelist)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
output := *o.PrintFlags.OutputFormat
|
||||
shortOutput := output == "name"
|
||||
|
||||
visitedUids := sets.NewString()
|
||||
visitedNamespaces := sets.NewString()
|
||||
|
||||
var objs []runtime.Object
|
||||
|
||||
count := 0
|
||||
err = r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If server-dry-run is requested but the type doesn't support it, fail right away.
|
||||
if o.ServerDryRun {
|
||||
if err := dryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if info.Namespaced() {
|
||||
visitedNamespaces.Insert(info.Namespace)
|
||||
}
|
||||
|
||||
if err := o.Recorder.Record(info.Object); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
|
||||
if o.ServerSideApply {
|
||||
// Send the full object to be applied on the server side.
|
||||
data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object)
|
||||
if err != nil {
|
||||
return cmdutil.AddSourceToErr("serverside-apply", info.Source, err)
|
||||
}
|
||||
|
||||
options := metav1.PatchOptions{
|
||||
Force: &o.ForceConflicts,
|
||||
FieldManager: o.FieldManager,
|
||||
}
|
||||
if o.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
|
||||
obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(
|
||||
info.Namespace,
|
||||
info.Name,
|
||||
types.ApplyPatchType,
|
||||
data,
|
||||
&options,
|
||||
)
|
||||
if err != nil {
|
||||
if isIncompatibleServerError(err) {
|
||||
err = fmt.Errorf("Server-side apply not available on the server: (%v)", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
info.Refresh(obj, true)
|
||||
metadata, err := meta.Accessor(info.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
visitedUids.Insert(string(metadata.GetUID()))
|
||||
count++
|
||||
if len(output) > 0 && !shortOutput {
|
||||
objs = append(objs, info.Object)
|
||||
return nil
|
||||
}
|
||||
|
||||
printer, err := o.ToPrinter("serverside-applied")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printer.PrintObj(info.Object, o.Out)
|
||||
}
|
||||
|
||||
// Get the modified configuration of the object. Embed the result
|
||||
// as an annotation in the modified configuration, so that it will appear
|
||||
// in the patch sent to the server.
|
||||
modified, err := util.GetModifiedConfiguration(info.Object, true, unstructured.UnstructuredJSONScheme)
|
||||
if err != nil {
|
||||
return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving modified configuration from:\n%s\nfor:", info.String()), info.Source, err)
|
||||
}
|
||||
|
||||
// Print object only if output format other than "name" is specified
|
||||
printObject := len(output) > 0 && !shortOutput
|
||||
|
||||
if err := info.Get(); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err)
|
||||
}
|
||||
|
||||
// Create the resource if it doesn't exist
|
||||
// First, update the annotation used by kubectl apply
|
||||
if err := util.CreateApplyAnnotation(info.Object, unstructured.UnstructuredJSONScheme); err != nil {
|
||||
return cmdutil.AddSourceToErr("creating", info.Source, err)
|
||||
}
|
||||
|
||||
if !o.DryRun {
|
||||
// Then create the resource and skip the three-way merge
|
||||
options := metav1.CreateOptions{}
|
||||
if o.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, &options)
|
||||
if err != nil {
|
||||
return cmdutil.AddSourceToErr("creating", info.Source, err)
|
||||
}
|
||||
info.Refresh(obj, true)
|
||||
}
|
||||
|
||||
metadata, err := meta.Accessor(info.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
visitedUids.Insert(string(metadata.GetUID()))
|
||||
|
||||
count++
|
||||
|
||||
if printObject {
|
||||
objs = append(objs, info.Object)
|
||||
return nil
|
||||
}
|
||||
|
||||
printer, err := o.ToPrinter("created")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(info.Object, o.Out)
|
||||
}
|
||||
|
||||
metadata, err := meta.Accessor(info.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
visitedUids.Insert(string(metadata.GetUID()))
|
||||
|
||||
if !o.DryRun {
|
||||
annotationMap := metadata.GetAnnotations()
|
||||
if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok {
|
||||
fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, o.cmdBaseName)
|
||||
}
|
||||
|
||||
helper := resource.NewHelper(info.Client, info.Mapping)
|
||||
patcher := &Patcher{
|
||||
Mapping: info.Mapping,
|
||||
Helper: helper,
|
||||
DynamicClient: o.DynamicClient,
|
||||
Overwrite: o.Overwrite,
|
||||
BackOff: clockwork.NewRealClock(),
|
||||
Force: o.DeleteOptions.ForceDeletion,
|
||||
Cascade: o.DeleteOptions.Cascade,
|
||||
Timeout: o.DeleteOptions.Timeout,
|
||||
GracePeriod: o.DeleteOptions.GracePeriod,
|
||||
ServerDryRun: o.ServerDryRun,
|
||||
OpenapiSchema: openapiSchema,
|
||||
Retries: maxPatchRetry,
|
||||
}
|
||||
|
||||
patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, o.ErrOut)
|
||||
if err != nil {
|
||||
return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err)
|
||||
}
|
||||
|
||||
info.Refresh(patchedObject, true)
|
||||
|
||||
if string(patchBytes) == "{}" && !printObject {
|
||||
count++
|
||||
|
||||
printer, err := o.ToPrinter("unchanged")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(info.Object, o.Out)
|
||||
}
|
||||
}
|
||||
count++
|
||||
|
||||
if printObject {
|
||||
objs = append(objs, info.Object)
|
||||
return nil
|
||||
}
|
||||
|
||||
printer, err := o.ToPrinter("configured")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(info.Object, o.Out)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return fmt.Errorf("no objects passed to apply")
|
||||
}
|
||||
|
||||
// print objects
|
||||
if len(objs) > 0 {
|
||||
printer, err := o.ToPrinter("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objToPrint := objs[0]
|
||||
if len(objs) > 1 {
|
||||
list := &corev1.List{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "List",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ListMeta: metav1.ListMeta{},
|
||||
}
|
||||
if err := meta.SetList(list, objs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objToPrint = list
|
||||
}
|
||||
if err := printer.PrintObj(objToPrint, o.Out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !o.Prune {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := pruner{
|
||||
mapper: o.Mapper,
|
||||
dynamicClient: o.DynamicClient,
|
||||
|
||||
labelSelector: o.Selector,
|
||||
visitedUids: visitedUids,
|
||||
|
||||
cascade: o.DeleteOptions.Cascade,
|
||||
dryRun: o.DryRun,
|
||||
serverDryRun: o.ServerDryRun,
|
||||
gracePeriod: o.DeleteOptions.GracePeriod,
|
||||
|
||||
toPrinter: o.ToPrinter,
|
||||
|
||||
out: o.Out,
|
||||
}
|
||||
|
||||
namespacedRESTMappings, nonNamespacedRESTMappings, err := getRESTMappings(o.Mapper, &(o.PruneResources))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error retrieving RESTMappings to prune: %v", err)
|
||||
}
|
||||
|
||||
for n := range visitedNamespaces {
|
||||
for _, m := range namespacedRESTMappings {
|
||||
if err := p.prune(n, m); err != nil {
|
||||
return fmt.Errorf("error pruning namespaced object %v: %v", m.GroupVersionKind, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range nonNamespacedRESTMappings {
|
||||
if err := p.prune(metav1.NamespaceNone, m); err != nil {
|
||||
return fmt.Errorf("error pruning nonNamespaced object %v: %v", m.GroupVersionKind, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type pruneResource struct {
|
||||
group string
|
||||
version string
|
||||
kind string
|
||||
namespaced bool
|
||||
}
|
||||
|
||||
func (pr pruneResource) String() string {
|
||||
return fmt.Sprintf("%v/%v, Kind=%v, Namespaced=%v", pr.group, pr.version, pr.kind, pr.namespaced)
|
||||
}
|
||||
|
||||
func getRESTMappings(mapper meta.RESTMapper, pruneResources *[]pruneResource) (namespaced, nonNamespaced []*meta.RESTMapping, err error) {
|
||||
if len(*pruneResources) == 0 {
|
||||
// default whitelist
|
||||
// TODO: need to handle the older api versions - e.g. v1beta1 jobs. Github issue: #35991
|
||||
*pruneResources = []pruneResource{
|
||||
{"", "v1", "ConfigMap", true},
|
||||
{"", "v1", "Endpoints", true},
|
||||
{"", "v1", "Namespace", false},
|
||||
{"", "v1", "PersistentVolumeClaim", true},
|
||||
{"", "v1", "PersistentVolume", false},
|
||||
{"", "v1", "Pod", true},
|
||||
{"", "v1", "ReplicationController", true},
|
||||
{"", "v1", "Secret", true},
|
||||
{"", "v1", "Service", true},
|
||||
{"batch", "v1", "Job", true},
|
||||
{"batch", "v1beta1", "CronJob", true},
|
||||
{"extensions", "v1beta1", "Ingress", true},
|
||||
{"apps", "v1", "DaemonSet", true},
|
||||
{"apps", "v1", "Deployment", true},
|
||||
{"apps", "v1", "ReplicaSet", true},
|
||||
{"apps", "v1", "StatefulSet", true},
|
||||
}
|
||||
}
|
||||
|
||||
for _, resource := range *pruneResources {
|
||||
addedMapping, err := mapper.RESTMapping(schema.GroupKind{Group: resource.group, Kind: resource.kind}, resource.version)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid resource %v: %v", resource, err)
|
||||
}
|
||||
if resource.namespaced {
|
||||
namespaced = append(namespaced, addedMapping)
|
||||
} else {
|
||||
nonNamespaced = append(nonNamespaced, addedMapping)
|
||||
}
|
||||
}
|
||||
|
||||
return namespaced, nonNamespaced, nil
|
||||
}
|
||||
|
||||
type pruner struct {
|
||||
mapper meta.RESTMapper
|
||||
dynamicClient dynamic.Interface
|
||||
|
||||
visitedUids sets.String
|
||||
labelSelector string
|
||||
fieldSelector string
|
||||
|
||||
cascade bool
|
||||
serverDryRun bool
|
||||
dryRun bool
|
||||
gracePeriod int
|
||||
|
||||
toPrinter func(string) (printers.ResourcePrinter, error)
|
||||
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func (p *pruner) prune(namespace string, mapping *meta.RESTMapping) error {
|
||||
objList, err := p.dynamicClient.Resource(mapping.Resource).
|
||||
Namespace(namespace).
|
||||
List(metav1.ListOptions{
|
||||
LabelSelector: p.labelSelector,
|
||||
FieldSelector: p.fieldSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objs, err := meta.ExtractList(objList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, obj := range objs {
|
||||
metadata, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
annots := metadata.GetAnnotations()
|
||||
if _, ok := annots[corev1.LastAppliedConfigAnnotation]; !ok {
|
||||
// don't prune resources not created with apply
|
||||
continue
|
||||
}
|
||||
uid := metadata.GetUID()
|
||||
if p.visitedUids.Has(string(uid)) {
|
||||
continue
|
||||
}
|
||||
name := metadata.GetName()
|
||||
if !p.dryRun {
|
||||
if err := p.delete(namespace, name, mapping); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
printer, err := p.toPrinter("pruned")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printer.PrintObj(obj, p.out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *pruner) delete(namespace, name string, mapping *meta.RESTMapping) error {
|
||||
return runDelete(namespace, name, mapping, p.dynamicClient, p.cascade, p.gracePeriod, p.serverDryRun)
|
||||
}
|
||||
|
||||
func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascade bool, gracePeriod int, serverDryRun bool) error {
|
||||
options := &metav1.DeleteOptions{}
|
||||
if gracePeriod >= 0 {
|
||||
options = metav1.NewDeleteOptions(int64(gracePeriod))
|
||||
}
|
||||
if serverDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
policy := metav1.DeletePropagationForeground
|
||||
if !cascade {
|
||||
policy = metav1.DeletePropagationOrphan
|
||||
}
|
||||
options.PropagationPolicy = &policy
|
||||
return c.Resource(mapping.Resource).Namespace(namespace).Delete(name, options)
|
||||
}
|
||||
|
||||
func (p *Patcher) delete(namespace, name string) error {
|
||||
return runDelete(namespace, name, p.Mapping, p.DynamicClient, p.Cascade, p.GracePeriod, p.ServerDryRun)
|
||||
}
|
||||
|
||||
// Patcher defines options to patch OpenAPI objects.
|
||||
type Patcher struct {
|
||||
Mapping *meta.RESTMapping
|
||||
Helper *resource.Helper
|
||||
DynamicClient dynamic.Interface
|
||||
|
||||
Overwrite bool
|
||||
BackOff clockwork.Clock
|
||||
|
||||
Force bool
|
||||
Cascade bool
|
||||
Timeout time.Duration
|
||||
GracePeriod int
|
||||
ServerDryRun bool
|
||||
|
||||
// If set, forces the patch against a specific resourceVersion
|
||||
ResourceVersion *string
|
||||
|
||||
// Number of retries to make if the patch fails with conflict
|
||||
Retries int
|
||||
|
||||
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 addResourceVersion(patch []byte, rv string) ([]byte, error) {
|
||||
var patchMap map[string]interface{}
|
||||
err := json.Unmarshal(patch, &patchMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := unstructured.Unstructured{Object: patchMap}
|
||||
a, err := meta.Accessor(&u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.SetResourceVersion(rv)
|
||||
|
||||
return json.Marshal(patchMap)
|
||||
}
|
||||
|
||||
func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
|
||||
// Serialize the current configuration of the object from the server.
|
||||
current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
||||
if err != nil {
|
||||
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("serializing current configuration from:\n%v\nfor:", obj), source, err)
|
||||
}
|
||||
|
||||
// Retrieve the original configuration of the object from the annotation.
|
||||
original, err := util.GetOriginalConfiguration(obj)
|
||||
if err != nil {
|
||||
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("retrieving original configuration from:\n%v\nfor:", obj), source, err)
|
||||
}
|
||||
|
||||
var patchType types.PatchType
|
||||
var patch []byte
|
||||
var lookupPatchMeta strategicpatch.LookupPatchMeta
|
||||
var schema oapi.Schema
|
||||
createPatchErrFormat := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"
|
||||
|
||||
// Create the versioned struct from the type defined in the restmapping
|
||||
// (which is the API version we'll be submitting the patch to)
|
||||
versionedObject, err := scheme.Scheme.New(p.Mapping.GroupVersionKind)
|
||||
switch {
|
||||
case runtime.IsNotRegisteredError(err):
|
||||
// fall back to generic JSON merge patch
|
||||
patchType = types.MergePatchType
|
||||
preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
|
||||
mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
|
||||
patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
|
||||
if err != nil {
|
||||
if mergepatch.IsPreconditionFailed(err) {
|
||||
return nil, nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
|
||||
}
|
||||
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
|
||||
}
|
||||
case err != nil:
|
||||
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("getting instance of versioned object for %v:", p.Mapping.GroupVersionKind), source, err)
|
||||
case err == nil:
|
||||
// Compute a three way strategic merge patch to send to server.
|
||||
patchType = types.StrategicMergePatchType
|
||||
|
||||
// Try to use openapi first if the openapi spec is available and can successfully calculate the patch.
|
||||
// Otherwise, fall back to baked-in types.
|
||||
if p.OpenapiSchema != nil {
|
||||
if schema = p.OpenapiSchema.LookupResource(p.Mapping.GroupVersionKind); schema != nil {
|
||||
lookupPatchMeta = strategicpatch.PatchMetaFromOpenAPI{Schema: schema}
|
||||
if openapiPatch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil {
|
||||
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
|
||||
} else {
|
||||
patchType = types.StrategicMergePatchType
|
||||
patch = openapiPatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if patch == nil {
|
||||
lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject)
|
||||
if err != nil {
|
||||
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
|
||||
}
|
||||
patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite)
|
||||
if err != nil {
|
||||
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if string(patch) == "{}" {
|
||||
return patch, obj, nil
|
||||
}
|
||||
|
||||
if p.ResourceVersion != nil {
|
||||
patch, err = addResourceVersion(patch, *p.ResourceVersion)
|
||||
if err != nil {
|
||||
return nil, nil, cmdutil.AddSourceToErr("Failed to insert resourceVersion in patch", source, err)
|
||||
}
|
||||
}
|
||||
|
||||
options := metav1.PatchOptions{}
|
||||
if p.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
|
||||
patchedObj, err := p.Helper.Patch(namespace, name, patchType, patch, &options)
|
||||
return patch, patchedObj, err
|
||||
}
|
||||
|
||||
// Patch tries to patch an OpenAPI resource. On success, returns the merge patch as well
|
||||
// the final patched object. On failure, returns an error.
|
||||
func (p *Patcher) Patch(current runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
|
||||
var getErr error
|
||||
patchBytes, patchObject, err := p.patchSimple(current, modified, source, namespace, name, errOut)
|
||||
if p.Retries == 0 {
|
||||
p.Retries = maxPatchRetry
|
||||
}
|
||||
for i := 1; i <= p.Retries && errors.IsConflict(err); i++ {
|
||||
if i > triesBeforeBackOff {
|
||||
p.BackOff.Sleep(backOffPeriod)
|
||||
}
|
||||
current, getErr = p.Helper.Get(namespace, name, false)
|
||||
if getErr != nil {
|
||||
return nil, nil, getErr
|
||||
}
|
||||
patchBytes, patchObject, err = p.patchSimple(current, modified, source, namespace, name, errOut)
|
||||
}
|
||||
if err != nil && (errors.IsConflict(err) || errors.IsInvalid(err)) && p.Force {
|
||||
patchBytes, patchObject, err = p.deleteAndCreate(current, modified, namespace, name)
|
||||
}
|
||||
return patchBytes, patchObject, err
|
||||
}
|
||||
|
||||
func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, namespace, name string) ([]byte, runtime.Object, error) {
|
||||
if err := p.delete(namespace, name); err != nil {
|
||||
return modified, nil, err
|
||||
}
|
||||
// TODO: use wait
|
||||
if err := wait.PollImmediate(1*time.Second, p.Timeout, func() (bool, error) {
|
||||
if _, err := p.Helper.Get(namespace, name, false); !errors.IsNotFound(err) {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return modified, nil, err
|
||||
}
|
||||
versionedObject, _, err := unstructured.UnstructuredJSONScheme.Decode(modified, nil, nil)
|
||||
if err != nil {
|
||||
return modified, nil, err
|
||||
}
|
||||
options := metav1.CreateOptions{}
|
||||
if p.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
createdObject, err := p.Helper.Create(namespace, true, versionedObject, &options)
|
||||
if err != nil {
|
||||
// restore the original object if we fail to create the new one
|
||||
// but still propagate and advertise error to user
|
||||
recreated, recreateErr := p.Helper.Create(namespace, true, original, &options)
|
||||
if recreateErr != nil {
|
||||
err = fmt.Errorf("An error occurred force-replacing the existing object with the newly provided one:\n\n%v.\n\nAdditionally, an error occurred attempting to restore the original object:\n\n%v", err, recreateErr)
|
||||
} else {
|
||||
createdObject = recreated
|
||||
}
|
||||
}
|
||||
return modified, createdObject, err
|
||||
}
|
||||
89
pkg/cmd/apply/apply_edit_last_applied.go
Normal file
89
pkg/cmd/apply/apply_edit_last_applied.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
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 apply
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/cmd/util/editor"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
applyEditLastAppliedLong = templates.LongDesc(`
|
||||
Edit the latest last-applied-configuration annotations of resources from the default editor.
|
||||
|
||||
The edit-last-applied command allows you to directly edit any API resource you can retrieve via the
|
||||
command line tools. It will open the editor defined by your KUBE_EDITOR, or EDITOR
|
||||
environment variables, or fall back to 'vi' for Linux or 'notepad' for Windows.
|
||||
You can edit multiple objects, although changes are applied one at a time. The command
|
||||
accepts filenames as well as command line arguments, although the files you point to must
|
||||
be previously saved versions of resources.
|
||||
|
||||
The default format is YAML. To edit in JSON, specify "-o json".
|
||||
|
||||
The flag --windows-line-endings can be used to force Windows line endings,
|
||||
otherwise the default for your operating system will be used.
|
||||
|
||||
In the event an error occurs while updating, a temporary file will be created on disk
|
||||
that contains your unapplied changes. The most common error when updating a resource
|
||||
is another editor changing the resource on the server. When this occurs, you will have
|
||||
to apply your changes to the newer version of the resource, or update your temporary
|
||||
saved copy to include the latest resource version.`)
|
||||
|
||||
applyEditLastAppliedExample = templates.Examples(`
|
||||
# Edit the last-applied-configuration annotations by type/name in YAML.
|
||||
kubectl apply edit-last-applied deployment/nginx
|
||||
|
||||
# Edit the last-applied-configuration annotations by file in JSON.
|
||||
kubectl apply edit-last-applied -f deploy.yaml -o json`)
|
||||
)
|
||||
|
||||
// NewCmdApplyEditLastApplied created the cobra CLI command for the `apply edit-last-applied` command.
|
||||
func NewCmdApplyEditLastApplied(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := editor.NewEditOptions(editor.ApplyEditMode, ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "edit-last-applied (RESOURCE/NAME | -f FILENAME)",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Edit latest last-applied-configuration annotations of a resource/object",
|
||||
Long: applyEditLastAppliedLong,
|
||||
Example: applyEditLastAppliedExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := o.Complete(f, args, cmd); err != nil {
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
if err := o.Run(); err != nil {
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// bind flag structs
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
usage := "to use to edit the resource"
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||
cmd.Flags().BoolVar(&o.WindowsLineEndings, "windows-line-endings", o.WindowsLineEndings,
|
||||
"Defaults to the line ending native to your platform.")
|
||||
cmdutil.AddIncludeUninitializedFlag(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
219
pkg/cmd/apply/apply_set_last_applied.go
Normal file
219
pkg/cmd/apply/apply_set_last_applied.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
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 apply
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/cmd/util/editor"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// SetLastAppliedOptions defines options for the `apply set-last-applied` command.`
|
||||
type SetLastAppliedOptions struct {
|
||||
CreateAnnotation bool
|
||||
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
|
||||
FilenameOptions resource.FilenameOptions
|
||||
|
||||
infoList []*resource.Info
|
||||
namespace string
|
||||
enforceNamespace bool
|
||||
dryRun bool
|
||||
shortOutput bool
|
||||
output string
|
||||
patchBufferList []PatchBuffer
|
||||
builder *resource.Builder
|
||||
unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// PatchBuffer caches changes that are to be applied.
|
||||
type PatchBuffer struct {
|
||||
Patch []byte
|
||||
PatchType types.PatchType
|
||||
}
|
||||
|
||||
var (
|
||||
applySetLastAppliedLong = templates.LongDesc(i18n.T(`
|
||||
Set the latest last-applied-configuration annotations by setting it to match the contents of a file.
|
||||
This results in the last-applied-configuration being updated as though 'kubectl apply -f <file>' was run,
|
||||
without updating any other parts of the object.`))
|
||||
|
||||
applySetLastAppliedExample = templates.Examples(i18n.T(`
|
||||
# Set the last-applied-configuration of a resource to match the contents of a file.
|
||||
kubectl apply set-last-applied -f deploy.yaml
|
||||
|
||||
# Execute set-last-applied against each configuration file in a directory.
|
||||
kubectl apply set-last-applied -f path/
|
||||
|
||||
# Set the last-applied-configuration of a resource to match the contents of a file, will create the annotation if it does not already exist.
|
||||
kubectl apply set-last-applied -f deploy.yaml --create-annotation=true
|
||||
`))
|
||||
)
|
||||
|
||||
// NewSetLastAppliedOptions takes option arguments from a CLI stream and returns it at SetLastAppliedOptions type.
|
||||
func NewSetLastAppliedOptions(ioStreams genericclioptions.IOStreams) *SetLastAppliedOptions {
|
||||
return &SetLastAppliedOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("configured").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdApplySetLastApplied creates the cobra CLI `apply` subcommand `set-last-applied`.`
|
||||
func NewCmdApplySetLastApplied(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewSetLastAppliedOptions(ioStreams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "set-last-applied -f FILENAME",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Set the last-applied-configuration annotation on a live object to match the contents of a file."),
|
||||
Long: applySetLastAppliedLong,
|
||||
Example: applySetLastAppliedExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunSetLastApplied())
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().BoolVar(&o.CreateAnnotation, "create-annotation", o.CreateAnnotation, "Will create 'last-applied-configuration' annotations if current objects doesn't have one")
|
||||
cmdutil.AddJsonFilenameFlag(cmd.Flags(), &o.FilenameOptions.Filenames, "Filename, directory, or URL to files that contains the last-applied-configuration annotations")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete populates dry-run and output flag options.
|
||||
func (o *SetLastAppliedOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||
o.dryRun = cmdutil.GetDryRunFlag(cmd)
|
||||
o.output = cmdutil.GetFlagString(cmd, "output")
|
||||
o.shortOutput = o.output == "name"
|
||||
|
||||
var err error
|
||||
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.builder = f.NewBuilder()
|
||||
o.unstructuredClientForMapping = f.UnstructuredClientForMapping
|
||||
|
||||
if o.dryRun {
|
||||
// TODO(juanvallejo): This can be cleaned up even further by creating
|
||||
// a PrintFlags struct that binds the --dry-run flag, and whose
|
||||
// ToPrinter method returns a printer that understands how to print
|
||||
// this success message.
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = printer.PrintObj
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks SetLastAppliedOptions for validity.
|
||||
func (o *SetLastAppliedOptions) Validate() error {
|
||||
r := o.builder.
|
||||
Unstructured().
|
||||
NamespaceParam(o.namespace).DefaultNamespace().
|
||||
FilenameParam(o.enforceNamespace, &o.FilenameOptions).
|
||||
Flatten().
|
||||
Do()
|
||||
|
||||
err := r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patchBuf, diffBuf, patchType, err := editor.GetApplyPatch(info.Object.(runtime.Unstructured))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify the object exists in the cluster before trying to patch it.
|
||||
if err := info.Get(); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err)
|
||||
}
|
||||
originalBuf, err := util.GetOriginalConfiguration(info.Object)
|
||||
if err != nil {
|
||||
return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err)
|
||||
}
|
||||
if originalBuf == nil && !o.CreateAnnotation {
|
||||
return fmt.Errorf("no last-applied-configuration annotation found on resource: %s, to create the annotation, run the command with --create-annotation", info.Name)
|
||||
}
|
||||
|
||||
//only add to PatchBufferList when changed
|
||||
if !bytes.Equal(cmdutil.StripComments(originalBuf), cmdutil.StripComments(diffBuf)) {
|
||||
p := PatchBuffer{Patch: patchBuf, PatchType: patchType}
|
||||
o.patchBufferList = append(o.patchBufferList, p)
|
||||
o.infoList = append(o.infoList, info)
|
||||
} else {
|
||||
fmt.Fprintf(o.Out, "set-last-applied %s: no changes required.\n", info.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RunSetLastApplied executes the `set-last-applied` command according to SetLastAppliedOptions.
|
||||
func (o *SetLastAppliedOptions) RunSetLastApplied() error {
|
||||
for i, patch := range o.patchBufferList {
|
||||
info := o.infoList[i]
|
||||
finalObj := info.Object
|
||||
|
||||
if !o.dryRun {
|
||||
mapping := info.ResourceMapping()
|
||||
client, err := o.unstructuredClientForMapping(mapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
helper := resource.NewHelper(client, mapping)
|
||||
finalObj, err = helper.Patch(o.namespace, info.Name, patch.PatchType, patch.Patch, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := o.PrintObj(finalObj, o.Out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
1414
pkg/cmd/apply/apply_test.go
Normal file
1414
pkg/cmd/apply/apply_test.go
Normal file
File diff suppressed because it is too large
Load diff
172
pkg/cmd/apply/apply_view_last_applied.go
Normal file
172
pkg/cmd/apply/apply_view_last_applied.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
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 apply
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// ViewLastAppliedOptions defines options for the `apply view-last-applied` command.`
|
||||
type ViewLastAppliedOptions struct {
|
||||
FilenameOptions resource.FilenameOptions
|
||||
Selector string
|
||||
LastAppliedConfigurationList []string
|
||||
OutputFormat string
|
||||
All bool
|
||||
Factory cmdutil.Factory
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
applyViewLastAppliedLong = templates.LongDesc(i18n.T(`
|
||||
View the latest last-applied-configuration annotations by type/name or file.
|
||||
|
||||
The default output will be printed to stdout in YAML format. One can use -o option
|
||||
to change output format.`))
|
||||
|
||||
applyViewLastAppliedExample = templates.Examples(i18n.T(`
|
||||
# View the last-applied-configuration annotations by type/name in YAML.
|
||||
kubectl apply view-last-applied deployment/nginx
|
||||
|
||||
# View the last-applied-configuration annotations by file in JSON
|
||||
kubectl apply view-last-applied -f deploy.yaml -o json`))
|
||||
)
|
||||
|
||||
// NewViewLastAppliedOptions takes option arguments from a CLI stream and returns it at ViewLastAppliedOptions type.
|
||||
func NewViewLastAppliedOptions(ioStreams genericclioptions.IOStreams) *ViewLastAppliedOptions {
|
||||
return &ViewLastAppliedOptions{
|
||||
OutputFormat: "yaml",
|
||||
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdApplyViewLastApplied creates the cobra CLI `apply` subcommand `view-last-applied`.`
|
||||
func NewCmdApplyViewLastApplied(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := NewViewLastAppliedOptions(ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "view-last-applied (TYPE [NAME | -l label] | TYPE/NAME | -f FILENAME)",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("View latest last-applied-configuration annotations of a resource/object"),
|
||||
Long: applyViewLastAppliedLong,
|
||||
Example: applyViewLastAppliedExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(cmd, f, args))
|
||||
cmdutil.CheckErr(options.Validate(cmd))
|
||||
cmdutil.CheckErr(options.RunApplyViewLastApplied(cmd))
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&options.OutputFormat, "output", "o", options.OutputFormat, "Output format. Must be one of yaml|json")
|
||||
cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||
cmd.Flags().BoolVar(&options.All, "all", options.All, "Select all resources in the namespace of the specified resource types")
|
||||
usage := "that contains the last-applied-configuration annotations"
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete checks an object for last-applied-configuration annotations.
|
||||
func (o *ViewLastAppliedOptions) Complete(cmd *cobra.Command, f cmdutil.Factory, args []string) error {
|
||||
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := f.NewBuilder().
|
||||
Unstructured().
|
||||
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||
ResourceTypeOrNameArgs(enforceNamespace, args...).
|
||||
SelectAllParam(o.All).
|
||||
LabelSelectorParam(o.Selector).
|
||||
Latest().
|
||||
Flatten().
|
||||
Do()
|
||||
err = r.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configString, err := util.GetOriginalConfiguration(info.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if configString == nil {
|
||||
return cmdutil.AddSourceToErr(fmt.Sprintf("no last-applied-configuration annotation found on resource: %s\n", info.Name), info.Source, err)
|
||||
}
|
||||
o.LastAppliedConfigurationList = append(o.LastAppliedConfigurationList, string(configString))
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks ViewLastAppliedOptions for validity.
|
||||
func (o *ViewLastAppliedOptions) Validate(cmd *cobra.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunApplyViewLastApplied executes the `view-last-applied` command according to ViewLastAppliedOptions.
|
||||
func (o *ViewLastAppliedOptions) RunApplyViewLastApplied(cmd *cobra.Command) error {
|
||||
for _, str := range o.LastAppliedConfigurationList {
|
||||
switch o.OutputFormat {
|
||||
case "json":
|
||||
jsonBuffer := &bytes.Buffer{}
|
||||
err := json.Indent(jsonBuffer, []byte(str), "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(o.Out, string(jsonBuffer.Bytes()))
|
||||
case "yaml":
|
||||
yamlOutput, err := yaml.JSONToYAML([]byte(str))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(o.Out, string(yamlOutput))
|
||||
default:
|
||||
return cmdutil.UsageErrorf(
|
||||
cmd,
|
||||
"Unexpected -o output mode: %s, the flag 'output' must be one of yaml|json",
|
||||
o.OutputFormat)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
344
pkg/cmd/attach/attach.go
Normal file
344
pkg/cmd/attach/attach.go
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
/*
|
||||
Copyright 2014 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 attach
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/exec"
|
||||
)
|
||||
|
||||
var (
|
||||
attachExample = templates.Examples(i18n.T(`
|
||||
# Get output from running pod 123456-7890, using the first container by default
|
||||
kubectl attach 123456-7890
|
||||
|
||||
# Get output from ruby-container from pod 123456-7890
|
||||
kubectl attach 123456-7890 -c ruby-container
|
||||
|
||||
# Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-7890
|
||||
# and sends stdout/stderr from 'bash' back to the client
|
||||
kubectl attach 123456-7890 -c ruby-container -i -t
|
||||
|
||||
# Get output from the first pod of a ReplicaSet named nginx
|
||||
kubectl attach rs/nginx
|
||||
`))
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPodAttachTimeout = 60 * time.Second
|
||||
defaultPodLogsTimeout = 20 * time.Second
|
||||
)
|
||||
|
||||
// AttachOptions declare the arguments accepted by the Attach command
|
||||
type AttachOptions struct {
|
||||
exec.StreamOptions
|
||||
|
||||
// whether to disable use of standard error when streaming output from tty
|
||||
DisableStderr bool
|
||||
|
||||
CommandName string
|
||||
ParentCommandName string
|
||||
EnableSuggestedCmdUsage bool
|
||||
|
||||
Pod *corev1.Pod
|
||||
|
||||
AttachFunc func(*AttachOptions, *corev1.Container, bool, remotecommand.TerminalSizeQueue) func() error
|
||||
Resources []string
|
||||
Builder func() *resource.Builder
|
||||
AttachablePodFn polymorphichelpers.AttachablePodForObjectFunc
|
||||
restClientGetter genericclioptions.RESTClientGetter
|
||||
|
||||
Attach RemoteAttach
|
||||
GetPodTimeout time.Duration
|
||||
Config *restclient.Config
|
||||
}
|
||||
|
||||
// NewAttachOptions creates the options for attach
|
||||
func NewAttachOptions(streams genericclioptions.IOStreams) *AttachOptions {
|
||||
return &AttachOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
IOStreams: streams,
|
||||
},
|
||||
Attach: &DefaultRemoteAttach{},
|
||||
AttachFunc: DefaultAttachFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdAttach returns the attach Cobra command
|
||||
func NewCmdAttach(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewAttachOptions(streams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "attach (POD | TYPE/NAME) -c CONTAINER",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Attach to a running container"),
|
||||
Long: "Attach to a process that is already running inside an existing container.",
|
||||
Example: attachExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout)
|
||||
cmd.Flags().StringVarP(&o.ContainerName, "container", "c", o.ContainerName, "Container name. If omitted, the first container in the pod will be chosen")
|
||||
cmd.Flags().BoolVarP(&o.Stdin, "stdin", "i", o.Stdin, "Pass stdin to the container")
|
||||
cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, "Stdin is a TTY")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing
|
||||
type RemoteAttach interface {
|
||||
Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
|
||||
}
|
||||
|
||||
// DefaultAttachFunc is the default AttachFunc used
|
||||
func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
|
||||
return func() error {
|
||||
restClient, err := restclient.RESTClientFor(o.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := restClient.Post().
|
||||
Resource("pods").
|
||||
Name(o.Pod.Name).
|
||||
Namespace(o.Pod.Namespace).
|
||||
SubResource("attach")
|
||||
req.VersionedParams(&corev1.PodAttachOptions{
|
||||
Container: containerToAttach.Name,
|
||||
Stdin: o.Stdin,
|
||||
Stdout: o.Out != nil,
|
||||
Stderr: !o.DisableStderr,
|
||||
TTY: raw,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
return o.Attach.Attach("POST", req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRemoteAttach is the standard implementation of attaching
|
||||
type DefaultRemoteAttach struct{}
|
||||
|
||||
// Attach executes attach to a running container
|
||||
func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, method, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return exec.Stream(remotecommand.StreamOptions{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
Tty: tty,
|
||||
TerminalSizeQueue: terminalSizeQueue,
|
||||
})
|
||||
}
|
||||
|
||||
// Complete verifies command line arguments and loads data from the command environment
|
||||
func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.AttachablePodFn = polymorphichelpers.AttachablePodForObjectFn
|
||||
|
||||
o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
|
||||
if err != nil {
|
||||
return cmdutil.UsageErrorf(cmd, err.Error())
|
||||
}
|
||||
|
||||
o.Builder = f.NewBuilder
|
||||
o.Resources = args
|
||||
o.restClientGetter = f
|
||||
|
||||
cmdParent := cmd.Parent()
|
||||
if cmdParent != nil {
|
||||
o.ParentCommandName = cmdParent.CommandPath()
|
||||
}
|
||||
if len(o.ParentCommandName) > 0 && cmdutil.IsSiblingCommandExists(cmd, "describe") {
|
||||
o.EnableSuggestedCmdUsage = true
|
||||
}
|
||||
|
||||
config, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Config = config
|
||||
|
||||
if o.CommandName == "" {
|
||||
o.CommandName = cmd.CommandPath()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks that the provided attach options are specified.
|
||||
func (o *AttachOptions) Validate() error {
|
||||
if len(o.Resources) == 0 {
|
||||
return fmt.Errorf("at least 1 argument is required for attach")
|
||||
}
|
||||
if len(o.Resources) > 2 {
|
||||
return fmt.Errorf("expected POD, TYPE/NAME, or TYPE NAME, (at most 2 arguments) saw %d: %v", len(o.Resources), o.Resources)
|
||||
}
|
||||
if o.GetPodTimeout <= 0 {
|
||||
return fmt.Errorf("--pod-running-timeout must be higher than zero")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run executes a validated remote execution against a pod.
|
||||
func (o *AttachOptions) Run() error {
|
||||
if o.Pod == nil {
|
||||
b := o.Builder().
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
NamespaceParam(o.Namespace).DefaultNamespace()
|
||||
|
||||
switch len(o.Resources) {
|
||||
case 1:
|
||||
b.ResourceNames("pods", o.Resources[0])
|
||||
case 2:
|
||||
b.ResourceNames(o.Resources[0], o.Resources[1])
|
||||
}
|
||||
|
||||
obj, err := b.Do().Object()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Pod, err = o.findAttachablePod(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Pod.Status.Phase == corev1.PodSucceeded || o.Pod.Status.Phase == corev1.PodFailed {
|
||||
return fmt.Errorf("cannot attach a container in a completed pod; current phase is %s", o.Pod.Status.Phase)
|
||||
}
|
||||
// TODO: convert this to a clean "wait" behavior
|
||||
}
|
||||
|
||||
// check for TTY
|
||||
containerToAttach, err := o.containerToAttachTo(o.Pod)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot attach to the container: %v", err)
|
||||
}
|
||||
if o.TTY && !containerToAttach.TTY {
|
||||
o.TTY = false
|
||||
if o.ErrOut != nil {
|
||||
fmt.Fprintf(o.ErrOut, "Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
|
||||
}
|
||||
} else if !o.TTY && containerToAttach.TTY {
|
||||
// the container was launched with a TTY, so we have to force a TTY here, otherwise you'll get
|
||||
// an error "Unrecognized input header"
|
||||
o.TTY = true
|
||||
}
|
||||
|
||||
// ensure we can recover the terminal while attached
|
||||
t := o.SetupTTY()
|
||||
|
||||
var sizeQueue remotecommand.TerminalSizeQueue
|
||||
if t.Raw {
|
||||
if size := t.GetSize(); size != nil {
|
||||
// fake resizing +1 and then back to normal so that attach-detach-reattach will result in the
|
||||
// screen being redrawn
|
||||
sizePlusOne := *size
|
||||
sizePlusOne.Width++
|
||||
sizePlusOne.Height++
|
||||
|
||||
// this call spawns a goroutine to monitor/update the terminal size
|
||||
sizeQueue = t.MonitorSize(&sizePlusOne, size)
|
||||
}
|
||||
|
||||
o.DisableStderr = true
|
||||
}
|
||||
|
||||
if !o.Quiet {
|
||||
fmt.Fprintln(o.ErrOut, "If you don't see a command prompt, try pressing enter.")
|
||||
}
|
||||
if err := t.Safe(o.AttachFunc(o, containerToAttach, t.Raw, sizeQueue)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Stdin && t.Raw && o.Pod.Spec.RestartPolicy == corev1.RestartPolicyAlways {
|
||||
fmt.Fprintf(o.Out, "Session ended, resume using '%s %s -c %s -i -t' command when the pod is running\n", o.CommandName, o.Pod.Name, containerToAttach.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *AttachOptions) findAttachablePod(obj runtime.Object) (*corev1.Pod, error) {
|
||||
attachablePod, err := o.AttachablePodFn(o.restClientGetter, obj, o.GetPodTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.StreamOptions.PodName = attachablePod.Name
|
||||
return attachablePod, nil
|
||||
}
|
||||
|
||||
// containerToAttach returns a reference to the container to attach to, given
|
||||
// by name or the first container if name is empty.
|
||||
func (o *AttachOptions) containerToAttachTo(pod *corev1.Pod) (*corev1.Container, error) {
|
||||
if len(o.ContainerName) > 0 {
|
||||
for i := range pod.Spec.Containers {
|
||||
if pod.Spec.Containers[i].Name == o.ContainerName {
|
||||
return &pod.Spec.Containers[i], nil
|
||||
}
|
||||
}
|
||||
for i := range pod.Spec.InitContainers {
|
||||
if pod.Spec.InitContainers[i].Name == o.ContainerName {
|
||||
return &pod.Spec.InitContainers[i], nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("container not found (%s)", o.ContainerName)
|
||||
}
|
||||
|
||||
if o.EnableSuggestedCmdUsage {
|
||||
fmt.Fprintf(o.ErrOut, "Defaulting container name to %s.\n", pod.Spec.Containers[0].Name)
|
||||
fmt.Fprintf(o.ErrOut, "Use '%s describe pod/%s -n %s' to see all of the containers in this pod.\n", o.ParentCommandName, o.PodName, o.Namespace)
|
||||
}
|
||||
|
||||
klog.V(4).Infof("defaulting container name to %s", pod.Spec.Containers[0].Name)
|
||||
return &pod.Spec.Containers[0], nil
|
||||
}
|
||||
|
||||
// GetContainerName returns the name of the container to attach to, with a fallback.
|
||||
func (o *AttachOptions) GetContainerName(pod *corev1.Pod) (string, error) {
|
||||
c, err := o.containerToAttachTo(pod)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return c.Name, nil
|
||||
}
|
||||
422
pkg/cmd/attach/attach_test.go
Normal file
422
pkg/cmd/attach/attach_test.go
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
/*
|
||||
Copyright 2014 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 attach
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/exec"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
type fakeRemoteAttach struct {
|
||||
method string
|
||||
url *url.URL
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
f.method = method
|
||||
f.url = url
|
||||
return f.err
|
||||
}
|
||||
|
||||
func fakeAttachablePodFn(pod *corev1.Pod) polymorphichelpers.AttachablePodForObjectFunc {
|
||||
return func(getter genericclioptions.RESTClientGetter, obj runtime.Object, timeout time.Duration) (*corev1.Pod, error) {
|
||||
return pod, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodAndContainerAttach(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
options *AttachOptions
|
||||
expectError string
|
||||
expectedPodName string
|
||||
expectedContainerName string
|
||||
obj *corev1.Pod
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
options: &AttachOptions{GetPodTimeout: 1},
|
||||
expectError: "at least 1 argument is required",
|
||||
},
|
||||
{
|
||||
name: "too many args",
|
||||
options: &AttachOptions{GetPodTimeout: 2},
|
||||
args: []string{"one", "two", "three"},
|
||||
expectError: "at most 2 arguments",
|
||||
},
|
||||
{
|
||||
name: "no container, no flags",
|
||||
options: &AttachOptions{GetPodTimeout: defaultPodLogsTimeout},
|
||||
args: []string{"foo"},
|
||||
expectedPodName: "foo",
|
||||
expectedContainerName: "bar",
|
||||
obj: attachPod(),
|
||||
},
|
||||
{
|
||||
name: "container in flag",
|
||||
options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "bar"}, GetPodTimeout: 10000000},
|
||||
args: []string{"foo"},
|
||||
expectedPodName: "foo",
|
||||
expectedContainerName: "bar",
|
||||
obj: attachPod(),
|
||||
},
|
||||
{
|
||||
name: "init container in flag",
|
||||
options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "initfoo"}, GetPodTimeout: 30},
|
||||
args: []string{"foo"},
|
||||
expectedPodName: "foo",
|
||||
expectedContainerName: "initfoo",
|
||||
obj: attachPod(),
|
||||
},
|
||||
{
|
||||
name: "non-existing container",
|
||||
options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "wrong"}, GetPodTimeout: 10},
|
||||
args: []string{"foo"},
|
||||
expectedPodName: "foo",
|
||||
expectError: "container not found",
|
||||
obj: attachPod(),
|
||||
},
|
||||
{
|
||||
name: "no container, no flags, pods and name",
|
||||
options: &AttachOptions{GetPodTimeout: 10000},
|
||||
args: []string{"pods", "foo"},
|
||||
expectedPodName: "foo",
|
||||
expectedContainerName: "bar",
|
||||
obj: attachPod(),
|
||||
},
|
||||
{
|
||||
name: "invalid get pod timeout value",
|
||||
options: &AttachOptions{GetPodTimeout: 0},
|
||||
args: []string{"pod/foo"},
|
||||
expectedPodName: "foo",
|
||||
expectedContainerName: "bar",
|
||||
obj: attachPod(),
|
||||
expectError: "must be higher than zero",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// setup opts to fetch our test pod
|
||||
test.options.AttachablePodFn = fakeAttachablePodFn(test.obj)
|
||||
test.options.Resources = test.args
|
||||
|
||||
if err := test.options.Validate(); err != nil {
|
||||
if !strings.Contains(err.Error(), test.expectError) {
|
||||
t.Errorf("unexpected error: expected %q, got %q", test.expectError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pod, err := test.options.findAttachablePod(&corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "test"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "foobar",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), test.expectError) {
|
||||
t.Errorf("unexpected error: expected %q, got %q", err, test.expectError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if pod.Name != test.expectedPodName {
|
||||
t.Errorf("unexpected pod name: expected %q, got %q", test.expectedContainerName, pod.Name)
|
||||
}
|
||||
|
||||
container, err := test.options.containerToAttachTo(attachPod())
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), test.expectError) {
|
||||
t.Errorf("unexpected error: expected %q, got %q", err, test.expectError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if container.Name != test.expectedContainerName {
|
||||
t.Errorf("unexpected container name: expected %q, got %q", test.expectedContainerName, container.Name)
|
||||
}
|
||||
|
||||
if test.options.PodName != test.expectedPodName {
|
||||
t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedPodName, test.options.PodName)
|
||||
}
|
||||
|
||||
if len(test.expectError) > 0 {
|
||||
t.Fatalf("expected error %q, but saw none", test.expectError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttach(t *testing.T) {
|
||||
version := "v1"
|
||||
tests := []struct {
|
||||
name, version, podPath, fetchPodPath, attachPath, container string
|
||||
pod *corev1.Pod
|
||||
remoteAttachErr bool
|
||||
exepctedErr string
|
||||
}{
|
||||
{
|
||||
name: "pod attach",
|
||||
version: version,
|
||||
podPath: "/api/" + version + "/namespaces/test/pods/foo",
|
||||
fetchPodPath: "/namespaces/test/pods/foo",
|
||||
attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
|
||||
pod: attachPod(),
|
||||
container: "bar",
|
||||
},
|
||||
{
|
||||
name: "pod attach error",
|
||||
version: version,
|
||||
podPath: "/api/" + version + "/namespaces/test/pods/foo",
|
||||
fetchPodPath: "/namespaces/test/pods/foo",
|
||||
attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
|
||||
pod: attachPod(),
|
||||
remoteAttachErr: true,
|
||||
container: "bar",
|
||||
exepctedErr: "attach error",
|
||||
},
|
||||
{
|
||||
name: "container not found error",
|
||||
version: version,
|
||||
podPath: "/api/" + version + "/namespaces/test/pods/foo",
|
||||
fetchPodPath: "/namespaces/test/pods/foo",
|
||||
attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
|
||||
pod: attachPod(),
|
||||
container: "foo",
|
||||
exepctedErr: "cannot attach to the container: container not found (foo)",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == test.podPath && m == "GET":
|
||||
body := cmdtesting.ObjBody(codec, test.pod)
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
|
||||
case p == test.fetchPodPath && m == "GET":
|
||||
body := cmdtesting.ObjBody(codec, test.pod)
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
|
||||
default:
|
||||
t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req)
|
||||
return nil, fmt.Errorf("unexpected request")
|
||||
}
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
|
||||
|
||||
remoteAttach := &fakeRemoteAttach{}
|
||||
if test.remoteAttachErr {
|
||||
remoteAttach.err = fmt.Errorf("attach error")
|
||||
}
|
||||
options := &AttachOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
ContainerName: test.container,
|
||||
IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
|
||||
},
|
||||
Attach: remoteAttach,
|
||||
GetPodTimeout: 1000,
|
||||
}
|
||||
|
||||
options.restClientGetter = tf
|
||||
options.Namespace = "test"
|
||||
options.Resources = []string{"foo"}
|
||||
options.Builder = tf.NewBuilder
|
||||
options.AttachablePodFn = fakeAttachablePodFn(test.pod)
|
||||
options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
|
||||
return func() error {
|
||||
u, err := url.Parse(fmt.Sprintf("%s?container=%s", test.attachPath, containerToAttach.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return options.Attach.Attach("POST", u, nil, nil, nil, nil, raw, sizeQueue)
|
||||
}
|
||||
}
|
||||
|
||||
err := options.Run()
|
||||
if test.exepctedErr != "" && err.Error() != test.exepctedErr {
|
||||
t.Errorf("%s: Unexpected exec error: %v", test.name, err)
|
||||
return
|
||||
}
|
||||
if test.exepctedErr == "" && err != nil {
|
||||
t.Errorf("%s: Unexpected error: %v", test.name, err)
|
||||
return
|
||||
}
|
||||
if test.exepctedErr != "" {
|
||||
return
|
||||
}
|
||||
if remoteAttach.url.Path != test.attachPath {
|
||||
t.Errorf("%s: Did not get expected path for exec request: %q %q", test.name, test.attachPath, remoteAttach.url.Path)
|
||||
return
|
||||
}
|
||||
if remoteAttach.method != "POST" {
|
||||
t.Errorf("%s: Did not get method for attach request: %s", test.name, remoteAttach.method)
|
||||
}
|
||||
if remoteAttach.url.Query().Get("container") != "bar" {
|
||||
t.Errorf("%s: Did not have query parameters: %s", test.name, remoteAttach.url.Query())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachWarnings(t *testing.T) {
|
||||
version := "v1"
|
||||
tests := []struct {
|
||||
name, container, version, podPath, fetchPodPath, expectedErr string
|
||||
pod *corev1.Pod
|
||||
stdin, tty bool
|
||||
}{
|
||||
{
|
||||
name: "fallback tty if not supported",
|
||||
version: version,
|
||||
podPath: "/api/" + version + "/namespaces/test/pods/foo",
|
||||
fetchPodPath: "/namespaces/test/pods/foo",
|
||||
pod: attachPod(),
|
||||
stdin: true,
|
||||
tty: true,
|
||||
expectedErr: "Unable to use a TTY - container bar did not allocate one",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
streams, _, _, bufErr := genericclioptions.NewTestIOStreams()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == test.podPath && m == "GET":
|
||||
body := cmdtesting.ObjBody(codec, test.pod)
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
|
||||
case p == test.fetchPodPath && m == "GET":
|
||||
body := cmdtesting.ObjBody(codec, test.pod)
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil
|
||||
default:
|
||||
t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req)
|
||||
return nil, fmt.Errorf("unexpected request")
|
||||
}
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
|
||||
|
||||
options := &AttachOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
Stdin: test.stdin,
|
||||
TTY: test.tty,
|
||||
ContainerName: test.container,
|
||||
IOStreams: streams,
|
||||
},
|
||||
|
||||
Attach: &fakeRemoteAttach{},
|
||||
GetPodTimeout: 1000,
|
||||
}
|
||||
|
||||
options.restClientGetter = tf
|
||||
options.Namespace = "test"
|
||||
options.Resources = []string{"foo"}
|
||||
options.Builder = tf.NewBuilder
|
||||
options.AttachablePodFn = fakeAttachablePodFn(test.pod)
|
||||
options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
|
||||
return func() error {
|
||||
u, err := url.Parse("http://foo.bar")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return options.Attach.Attach("POST", u, nil, nil, nil, nil, raw, sizeQueue)
|
||||
}
|
||||
}
|
||||
|
||||
if err := options.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if test.stdin && test.tty {
|
||||
if !test.pod.Spec.Containers[0].TTY {
|
||||
if !strings.Contains(bufErr.String(), test.expectedErr) {
|
||||
t.Errorf("%s: Expected TTY fallback warning for attach request: %s", test.name, bufErr.String())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func attachPod() *corev1.Pod {
|
||||
return &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyAlways,
|
||||
DNSPolicy: corev1.DNSClusterFirst,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "initfoo",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
}
|
||||
}
|
||||
284
pkg/cmd/autoscale/autoscale.go
Normal file
284
pkg/cmd/autoscale/autoscale.go
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
Copyright 2015 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 autoscale
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog"
|
||||
|
||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
autoscalingv1client "k8s.io/client-go/kubernetes/typed/autoscaling/v1"
|
||||
"k8s.io/client-go/scale"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
autoscaleLong = templates.LongDesc(i18n.T(`
|
||||
Creates an autoscaler that automatically chooses and sets the number of pods that run in a kubernetes cluster.
|
||||
|
||||
Looks up a Deployment, ReplicaSet, StatefulSet, or ReplicationController by name and creates an autoscaler that uses the given resource as a reference.
|
||||
An autoscaler can automatically increase or decrease number of pods deployed within the system as needed.`))
|
||||
|
||||
autoscaleExample = templates.Examples(i18n.T(`
|
||||
# Auto scale a deployment "foo", with the number of pods between 2 and 10, no target CPU utilization specified so a default autoscaling policy will be used:
|
||||
kubectl autoscale deployment foo --min=2 --max=10
|
||||
|
||||
# Auto scale a replication controller "foo", with the number of pods between 1 and 5, target CPU utilization at 80%:
|
||||
kubectl autoscale rc foo --max=5 --cpu-percent=80`))
|
||||
)
|
||||
|
||||
// AutoscaleOptions declare the arguments accepted by the Autoscale command
|
||||
type AutoscaleOptions struct {
|
||||
FilenameOptions *resource.FilenameOptions
|
||||
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
Recorder genericclioptions.Recorder
|
||||
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||
|
||||
Name string
|
||||
Generator string
|
||||
Min int32
|
||||
Max int32
|
||||
CPUPercent int32
|
||||
|
||||
createAnnotation bool
|
||||
args []string
|
||||
enforceNamespace bool
|
||||
namespace string
|
||||
dryRun bool
|
||||
builder *resource.Builder
|
||||
generatorFunc func(string, *meta.RESTMapping) (generate.StructuredGenerator, error)
|
||||
|
||||
HPAClient autoscalingv1client.HorizontalPodAutoscalersGetter
|
||||
scaleKindResolver scale.ScaleKindResolver
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewAutoscaleOptions creates the options for autoscale
|
||||
func NewAutoscaleOptions(ioStreams genericclioptions.IOStreams) *AutoscaleOptions {
|
||||
return &AutoscaleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("autoscaled").WithTypeSetter(scheme.Scheme),
|
||||
FilenameOptions: &resource.FilenameOptions{},
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdAutoscale returns the autoscale Cobra command
|
||||
func NewCmdAutoscale(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewAutoscaleOptions(ioStreams)
|
||||
|
||||
validArgs := []string{"deployment", "replicaset", "replicationcontroller"}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS] --max=MAXPODS [--cpu-percent=CPU]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Auto-scale a Deployment, ReplicaSet, or ReplicationController"),
|
||||
Long: autoscaleLong,
|
||||
Example: autoscaleExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
ValidArgs: validArgs,
|
||||
}
|
||||
|
||||
// bind flag structs
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmd.Flags().StringVar(&o.Generator, "generator", generateversioned.HorizontalPodAutoscalerV1GeneratorName, i18n.T("The name of the API generator to use. Currently there is only 1 generator."))
|
||||
cmd.Flags().Int32Var(&o.Min, "min", -1, "The lower limit for the number of pods that can be set by the autoscaler. If it's not specified or negative, the server will apply a default value.")
|
||||
cmd.Flags().Int32Var(&o.Max, "max", -1, "The upper limit for the number of pods that can be set by the autoscaler. Required.")
|
||||
cmd.MarkFlagRequired("max")
|
||||
cmd.Flags().Int32Var(&o.CPUPercent, "cpu-percent", -1, fmt.Sprintf("The target average CPU utilization (represented as a percent of requested CPU) over all the pods. If it's not specified or negative, a default autoscaling policy will be used."))
|
||||
cmd.Flags().StringVar(&o.Name, "name", "", i18n.T("The name for the newly created object. If not specified, the name of the input resource will be used."))
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, "identifying the resource to autoscale.")
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete verifies command line arguments and loads data from the command environment
|
||||
func (o *AutoscaleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
o.dryRun = cmdutil.GetFlagBool(cmd, "dry-run")
|
||||
o.createAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
|
||||
o.builder = f.NewBuilder()
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.scaleKindResolver = scale.NewDiscoveryScaleKindResolver(discoveryClient)
|
||||
o.args = args
|
||||
o.RecordFlags.Complete(cmd)
|
||||
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kubeClient, err := f.KubernetesClientSet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.HPAClient = kubeClient.AutoscalingV1()
|
||||
|
||||
// get the generator
|
||||
o.generatorFunc = func(name string, mapping *meta.RESTMapping) (generate.StructuredGenerator, error) {
|
||||
switch o.Generator {
|
||||
case generateversioned.HorizontalPodAutoscalerV1GeneratorName:
|
||||
return &generateversioned.HorizontalPodAutoscalerGeneratorV1{
|
||||
Name: name,
|
||||
MinReplicas: o.Min,
|
||||
MaxReplicas: o.Max,
|
||||
CPUPercent: o.CPUPercent,
|
||||
ScaleRefName: name,
|
||||
ScaleRefKind: mapping.GroupVersionKind.Kind,
|
||||
ScaleRefAPIVersion: mapping.GroupVersionKind.GroupVersion().String(),
|
||||
}, nil
|
||||
default:
|
||||
return nil, cmdutil.UsageErrorf(cmd, "Generator %s not supported. ", o.Generator)
|
||||
}
|
||||
}
|
||||
|
||||
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||
if o.dryRun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
|
||||
return o.PrintFlags.ToPrinter()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks that the provided attach options are specified.
|
||||
func (o *AutoscaleOptions) Validate() error {
|
||||
if o.Max < 1 {
|
||||
return fmt.Errorf("--max=MAXPODS is required and must be at least 1, max: %d", o.Max)
|
||||
}
|
||||
if o.Max < o.Min {
|
||||
return fmt.Errorf("--max=MAXPODS must be larger or equal to --min=MINPODS, max: %d, min: %d", o.Max, o.Min)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run performs the execution
|
||||
func (o *AutoscaleOptions) Run() error {
|
||||
r := o.builder.
|
||||
Unstructured().
|
||||
ContinueOnError().
|
||||
NamespaceParam(o.namespace).DefaultNamespace().
|
||||
FilenameParam(o.enforceNamespace, o.FilenameOptions).
|
||||
ResourceTypeOrNameArgs(false, o.args...).
|
||||
Flatten().
|
||||
Do()
|
||||
if err := r.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count := 0
|
||||
err := r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mapping := info.ResourceMapping()
|
||||
gvr := mapping.GroupVersionKind.GroupVersion().WithResource(mapping.Resource.Resource)
|
||||
if _, err := o.scaleKindResolver.ScaleForResource(gvr); err != nil {
|
||||
return fmt.Errorf("cannot autoscale a %v: %v", mapping.GroupVersionKind.Kind, err)
|
||||
}
|
||||
|
||||
generator, err := o.generatorFunc(info.Name, mapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate new object
|
||||
object, err := generator.StructuredGenerate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hpa, ok := object.(*autoscalingv1.HorizontalPodAutoscaler)
|
||||
if !ok {
|
||||
return fmt.Errorf("generator made %T, not autoscalingv1.HorizontalPodAutoscaler", object)
|
||||
}
|
||||
|
||||
if err := o.Recorder.Record(hpa); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
|
||||
if o.dryRun {
|
||||
count++
|
||||
|
||||
printer, err := o.ToPrinter("created")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(hpa, o.Out)
|
||||
}
|
||||
|
||||
if err := util.CreateOrUpdateAnnotation(o.createAnnotation, hpa, scheme.DefaultJSONEncoder()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actualHPA, err := o.HPAClient.HorizontalPodAutoscalers(o.namespace).Create(hpa)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count++
|
||||
printer, err := o.ToPrinter("autoscaled")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(actualHPA, o.Out)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("no objects passed to autoscale")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
258
pkg/cmd/certificates/certificates.go
Normal file
258
pkg/cmd/certificates/certificates.go
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
Copyright 2016 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 certificates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
certificatesv1beta1client "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
func NewCmdCertificate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "certificate SUBCOMMAND",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Modify certificate resources."),
|
||||
Long: "Modify certificate resources.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(NewCmdCertificateApprove(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCertificateDeny(f, ioStreams))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type CertificateOptions struct {
|
||||
resource.FilenameOptions
|
||||
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
|
||||
csrNames []string
|
||||
outputStyle string
|
||||
|
||||
clientSet certificatesv1beta1client.CertificatesV1beta1Interface
|
||||
builder *resource.Builder
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewCertificateOptions creates the options for certificate
|
||||
func NewCertificateOptions(ioStreams genericclioptions.IOStreams) *CertificateOptions {
|
||||
return &CertificateOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("approved").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *CertificateOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
o.csrNames = args
|
||||
o.outputStyle = cmdutil.GetFlagString(cmd, "output")
|
||||
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.PrintObj = func(obj runtime.Object, out io.Writer) error {
|
||||
return printer.PrintObj(obj, out)
|
||||
}
|
||||
|
||||
o.builder = f.NewBuilder()
|
||||
|
||||
clientConfig, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.clientSet, err = certificatesv1beta1client.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CertificateOptions) Validate() error {
|
||||
if len(o.csrNames) < 1 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||
return fmt.Errorf("one or more CSRs must be specified as <name> or -f <filename>")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCmdCertificateApprove(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewCertificateOptions(ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "approve (-f FILENAME | NAME)",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Approve a certificate signing request"),
|
||||
Long: templates.LongDesc(`
|
||||
Approve a certificate signing request.
|
||||
|
||||
kubectl certificate approve allows a cluster admin to approve a certificate
|
||||
signing request (CSR). This action tells a certificate signing controller to
|
||||
issue a certificate to the requestor with the attributes requested in the CSR.
|
||||
|
||||
SECURITY NOTICE: Depending on the requested attributes, the issued certificate
|
||||
can potentially grant a requester access to cluster resources or to authenticate
|
||||
as a requested identity. Before approving a CSR, ensure you understand what the
|
||||
signed certificate can do.
|
||||
`),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunCertificateApprove(cmdutil.GetFlagBool(cmd, "force")))
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmd.Flags().Bool("force", false, "Update the CSR even if it is already approved.")
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *CertificateOptions) RunCertificateApprove(force bool) error {
|
||||
return o.modifyCertificateCondition(o.builder, o.clientSet, force, func(csr *certificatesv1beta1.CertificateSigningRequest) (*certificatesv1beta1.CertificateSigningRequest, bool) {
|
||||
var alreadyApproved bool
|
||||
for _, c := range csr.Status.Conditions {
|
||||
if c.Type == certificatesv1beta1.CertificateApproved {
|
||||
alreadyApproved = true
|
||||
}
|
||||
}
|
||||
if alreadyApproved {
|
||||
return csr, true
|
||||
}
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1beta1.CertificateApproved,
|
||||
Reason: "KubectlApprove",
|
||||
Message: "This CSR was approved by kubectl certificate approve.",
|
||||
LastUpdateTime: metav1.Now(),
|
||||
})
|
||||
return csr, false
|
||||
})
|
||||
}
|
||||
|
||||
func NewCmdCertificateDeny(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewCertificateOptions(ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "deny (-f FILENAME | NAME)",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Deny a certificate signing request"),
|
||||
Long: templates.LongDesc(`
|
||||
Deny a certificate signing request.
|
||||
|
||||
kubectl certificate deny allows a cluster admin to deny a certificate
|
||||
signing request (CSR). This action tells a certificate signing controller to
|
||||
not to issue a certificate to the requestor.
|
||||
`),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunCertificateDeny(cmdutil.GetFlagBool(cmd, "force")))
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmd.Flags().Bool("force", false, "Update the CSR even if it is already denied.")
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *CertificateOptions) RunCertificateDeny(force bool) error {
|
||||
return o.modifyCertificateCondition(o.builder, o.clientSet, force, func(csr *certificatesv1beta1.CertificateSigningRequest) (*certificatesv1beta1.CertificateSigningRequest, bool) {
|
||||
var alreadyDenied bool
|
||||
for _, c := range csr.Status.Conditions {
|
||||
if c.Type == certificatesv1beta1.CertificateDenied {
|
||||
alreadyDenied = true
|
||||
}
|
||||
}
|
||||
if alreadyDenied {
|
||||
return csr, true
|
||||
}
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1beta1.CertificateDenied,
|
||||
Reason: "KubectlDeny",
|
||||
Message: "This CSR was denied by kubectl certificate deny.",
|
||||
LastUpdateTime: metav1.Now(),
|
||||
})
|
||||
return csr, false
|
||||
})
|
||||
}
|
||||
|
||||
func (o *CertificateOptions) modifyCertificateCondition(builder *resource.Builder, clientSet certificatesv1beta1client.CertificatesV1beta1Interface, force bool, modify func(csr *certificatesv1beta1.CertificateSigningRequest) (*certificatesv1beta1.CertificateSigningRequest, bool)) error {
|
||||
var found int
|
||||
r := builder.
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
ContinueOnError().
|
||||
FilenameParam(false, &o.FilenameOptions).
|
||||
ResourceNames("certificatesigningrequest", o.csrNames...).
|
||||
RequireObject(true).
|
||||
Flatten().
|
||||
Latest().
|
||||
Do()
|
||||
err := r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; ; i++ {
|
||||
csr := info.Object.(*certificatesv1beta1.CertificateSigningRequest)
|
||||
csr, hasCondition := modify(csr)
|
||||
if !hasCondition || force {
|
||||
csr, err = clientSet.CertificateSigningRequests().UpdateApproval(csr)
|
||||
if errors.IsConflict(err) && i < 10 {
|
||||
if err := info.Get(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
found++
|
||||
|
||||
return o.PrintObj(info.Object, o.Out)
|
||||
})
|
||||
if found == 0 {
|
||||
fmt.Fprintf(o.Out, "No resources found\n")
|
||||
}
|
||||
return err
|
||||
}
|
||||
166
pkg/cmd/clusterinfo/clusterinfo.go
Normal file
166
pkg/cmd/clusterinfo/clusterinfo.go
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
Copyright 2015 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 clusterinfo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
|
||||
ct "github.com/daviddengcn/go-colortext"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
longDescr = templates.LongDesc(i18n.T(`
|
||||
Display addresses of the master and services with label kubernetes.io/cluster-service=true
|
||||
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.`))
|
||||
|
||||
clusterinfoExample = templates.Examples(i18n.T(`
|
||||
# Print the address of the master and cluster services
|
||||
kubectl cluster-info`))
|
||||
)
|
||||
|
||||
type ClusterInfoOptions struct {
|
||||
genericclioptions.IOStreams
|
||||
|
||||
Namespace string
|
||||
|
||||
Builder *resource.Builder
|
||||
Client *restclient.Config
|
||||
}
|
||||
|
||||
func NewCmdClusterInfo(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := &ClusterInfoOptions{
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "cluster-info",
|
||||
Short: i18n.T("Display cluster info"),
|
||||
Long: longDescr,
|
||||
Example: clusterinfoExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd))
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(NewCmdClusterInfoDump(f, ioStreams))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *ClusterInfoOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||
var err error
|
||||
o.Client, err = f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmdNamespace := cmdutil.GetFlagString(cmd, "namespace")
|
||||
if cmdNamespace == "" {
|
||||
cmdNamespace = metav1.NamespaceSystem
|
||||
}
|
||||
o.Namespace = cmdNamespace
|
||||
|
||||
o.Builder = f.NewBuilder()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *ClusterInfoOptions) Run() error {
|
||||
// TODO use generalized labels once they are implemented (#341)
|
||||
b := o.Builder.
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||
LabelSelectorParam("kubernetes.io/cluster-service=true").
|
||||
ResourceTypeOrNameArgs(false, []string{"services"}...).
|
||||
Latest()
|
||||
err := b.Do().Visit(func(r *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printService(o.Out, "Kubernetes master", o.Client.Host)
|
||||
|
||||
services := r.Object.(*corev1.ServiceList).Items
|
||||
for _, service := range services {
|
||||
var link string
|
||||
if len(service.Status.LoadBalancer.Ingress) > 0 {
|
||||
ingress := service.Status.LoadBalancer.Ingress[0]
|
||||
ip := ingress.IP
|
||||
if ip == "" {
|
||||
ip = ingress.Hostname
|
||||
}
|
||||
for _, port := range service.Spec.Ports {
|
||||
link += "http://" + ip + ":" + strconv.Itoa(int(port.Port)) + " "
|
||||
}
|
||||
} else {
|
||||
name := service.ObjectMeta.Name
|
||||
|
||||
if len(service.Spec.Ports) > 0 {
|
||||
port := service.Spec.Ports[0]
|
||||
|
||||
// guess if the scheme is https
|
||||
scheme := ""
|
||||
if port.Name == "https" || port.Port == 443 {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// format is <scheme>:<service-name>:<service-port-name>
|
||||
name = utilnet.JoinSchemeNamePort(scheme, service.ObjectMeta.Name, port.Name)
|
||||
}
|
||||
|
||||
if len(o.Client.GroupVersion.Group) == 0 {
|
||||
link = o.Client.Host + "/api/" + o.Client.GroupVersion.Version + "/namespaces/" + service.ObjectMeta.Namespace + "/services/" + name + "/proxy"
|
||||
} else {
|
||||
link = o.Client.Host + "/api/" + o.Client.GroupVersion.Group + "/" + o.Client.GroupVersion.Version + "/namespaces/" + service.ObjectMeta.Namespace + "/services/" + name + "/proxy"
|
||||
|
||||
}
|
||||
}
|
||||
name := service.ObjectMeta.Labels["kubernetes.io/name"]
|
||||
if len(name) == 0 {
|
||||
name = service.ObjectMeta.Name
|
||||
}
|
||||
printService(o.Out, name, link)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
o.Out.Write([]byte("\nTo further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.\n"))
|
||||
return err
|
||||
|
||||
// TODO consider printing more information about cluster
|
||||
}
|
||||
|
||||
func printService(out io.Writer, name, link string) {
|
||||
ct.ChangeColor(ct.Green, false, ct.None, false)
|
||||
fmt.Fprint(out, name)
|
||||
ct.ResetColor()
|
||||
fmt.Fprint(out, " is running at ")
|
||||
ct.ChangeColor(ct.Yellow, false, ct.None, false)
|
||||
fmt.Fprint(out, link)
|
||||
ct.ResetColor()
|
||||
fmt.Fprintln(out, "")
|
||||
}
|
||||
295
pkg/cmd/clusterinfo/clusterinfo_dump.go
Normal file
295
pkg/cmd/clusterinfo/clusterinfo_dump.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
Copyright 2016 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 clusterinfo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
appsv1client "k8s.io/client-go/kubernetes/typed/apps/v1"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPodLogsTimeout = 20 * time.Second
|
||||
timeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
type ClusterInfoDumpOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
|
||||
OutputDir string
|
||||
AllNamespaces bool
|
||||
Namespaces []string
|
||||
|
||||
Timeout time.Duration
|
||||
AppsClient appsv1client.AppsV1Interface
|
||||
CoreClient corev1client.CoreV1Interface
|
||||
Namespace string
|
||||
RESTClientGetter genericclioptions.RESTClientGetter
|
||||
LogsForObject polymorphichelpers.LogsForObjectFunc
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
func NewCmdClusterInfoDump(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := &ClusterInfoDumpOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme).WithDefaultOutput("json"),
|
||||
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "dump",
|
||||
Short: i18n.T("Dump lots of relevant info for debugging and diagnosis"),
|
||||
Long: dumpLong,
|
||||
Example: dumpExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd))
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmd.Flags().StringVar(&o.OutputDir, "output-directory", o.OutputDir, i18n.T("Where to output the files. If empty or '-' uses stdout, otherwise creates a directory hierarchy in that directory"))
|
||||
cmd.Flags().StringSliceVar(&o.Namespaces, "namespaces", o.Namespaces, "A comma separated list of namespaces to dump.")
|
||||
cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If true, dump all namespaces. If true, --namespaces is ignored.")
|
||||
cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodLogsTimeout)
|
||||
return cmd
|
||||
}
|
||||
|
||||
var (
|
||||
dumpLong = templates.LongDesc(i18n.T(`
|
||||
Dumps cluster info out suitable for debugging and diagnosing cluster problems. By default, dumps everything to
|
||||
stdout. You can optionally specify a directory with --output-directory. If you specify a directory, kubernetes will
|
||||
build a set of files in that directory. By default only dumps things in the 'kube-system' namespace, but you can
|
||||
switch to a different namespace with the --namespaces flag, or specify --all-namespaces to dump all namespaces.
|
||||
|
||||
The command also dumps the logs of all of the pods in the cluster, these logs are dumped into different directories
|
||||
based on namespace and pod name.`))
|
||||
|
||||
dumpExample = templates.Examples(i18n.T(`
|
||||
# Dump current cluster state to stdout
|
||||
kubectl cluster-info dump
|
||||
|
||||
# Dump current cluster state to /path/to/cluster-state
|
||||
kubectl cluster-info dump --output-directory=/path/to/cluster-state
|
||||
|
||||
# Dump all namespaces to stdout
|
||||
kubectl cluster-info dump --all-namespaces
|
||||
|
||||
# Dump a set of namespaces to /path/to/cluster-state
|
||||
kubectl cluster-info dump --namespaces default,kube-system --output-directory=/path/to/cluster-state`))
|
||||
)
|
||||
|
||||
func setupOutputWriter(dir string, defaultWriter io.Writer, filename string) io.Writer {
|
||||
if len(dir) == 0 || dir == "-" {
|
||||
return defaultWriter
|
||||
}
|
||||
fullFile := path.Join(dir, filename)
|
||||
parent := path.Dir(fullFile)
|
||||
cmdutil.CheckErr(os.MkdirAll(parent, 0755))
|
||||
|
||||
file, err := os.Create(path.Join(dir, filename))
|
||||
cmdutil.CheckErr(err)
|
||||
return file
|
||||
}
|
||||
|
||||
func (o *ClusterInfoDumpOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.PrintObj = printer.PrintObj
|
||||
|
||||
config, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.CoreClient, err = corev1client.NewForConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.AppsClient, err = appsv1client.NewForConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Timeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO this should eventually just be the completed kubeconfigflag struct
|
||||
o.RESTClientGetter = f
|
||||
o.LogsForObject = polymorphichelpers.LogsForObjectFn
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *ClusterInfoDumpOptions) Run() error {
|
||||
nodes, err := o.CoreClient.Nodes().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := o.PrintObj(nodes, setupOutputWriter(o.OutputDir, o.Out, "nodes.json")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var namespaces []string
|
||||
if o.AllNamespaces {
|
||||
namespaceList, err := o.CoreClient.Namespaces().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for ix := range namespaceList.Items {
|
||||
namespaces = append(namespaces, namespaceList.Items[ix].Name)
|
||||
}
|
||||
} else {
|
||||
if len(o.Namespaces) == 0 {
|
||||
namespaces = []string{
|
||||
metav1.NamespaceSystem,
|
||||
o.Namespace,
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, namespace := range namespaces {
|
||||
// TODO: this is repetitive in the extreme. Use reflection or
|
||||
// something to make this a for loop.
|
||||
events, err := o.CoreClient.Events(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.PrintObj(events, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "events.json"))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rcs, err := o.CoreClient.ReplicationControllers(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.PrintObj(rcs, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "replication-controllers.json"))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svcs, err := o.CoreClient.Services(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.PrintObj(svcs, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "services.json"))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sets, err := o.AppsClient.DaemonSets(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.PrintObj(sets, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "daemonsets.json"))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deps, err := o.AppsClient.Deployments(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.PrintObj(deps, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "deployments.json"))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rps, err := o.AppsClient.ReplicaSets(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.PrintObj(rps, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "replicasets.json"))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pods, err := o.CoreClient.Pods(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := o.PrintObj(pods, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "pods.json"))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printContainer := func(writer io.Writer, container corev1.Container, pod *corev1.Pod) {
|
||||
writer.Write([]byte(fmt.Sprintf("==== START logs for container %s of pod %s/%s ====\n", container.Name, pod.Namespace, pod.Name)))
|
||||
defer writer.Write([]byte(fmt.Sprintf("==== END logs for container %s of pod %s/%s ====\n", container.Name, pod.Namespace, pod.Name)))
|
||||
|
||||
requests, err := o.LogsForObject(o.RESTClientGetter, pod, &corev1.PodLogOptions{Container: container.Name}, timeout, false)
|
||||
if err != nil {
|
||||
// Print error and return.
|
||||
writer.Write([]byte(fmt.Sprintf("Create log request error: %s\n", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
for _, request := range requests {
|
||||
data, err := request.DoRaw()
|
||||
if err != nil {
|
||||
// Print error and return.
|
||||
writer.Write([]byte(fmt.Sprintf("Request log error: %s\n", err.Error())))
|
||||
return
|
||||
}
|
||||
writer.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
for ix := range pods.Items {
|
||||
pod := &pods.Items[ix]
|
||||
containers := pod.Spec.Containers
|
||||
writer := setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, pod.Name, "logs.txt"))
|
||||
|
||||
for i := range containers {
|
||||
printContainer(writer, containers[i], pod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dest := o.OutputDir
|
||||
if len(dest) == 0 {
|
||||
dest = "standard output"
|
||||
}
|
||||
if dest != "-" {
|
||||
fmt.Fprintf(o.Out, "Cluster info dumped to %s\n", dest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
70
pkg/cmd/clusterinfo/clusterinfo_dump_test.go
Normal file
70
pkg/cmd/clusterinfo/clusterinfo_dump_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2016 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 clusterinfo
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestSetupOutputWriterNoOp(t *testing.T) {
|
||||
tests := []string{"", "-"}
|
||||
for _, test := range tests {
|
||||
_, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
writer := setupOutputWriter(test, buf, "/some/file/that/should/be/ignored")
|
||||
if writer != buf {
|
||||
t.Errorf("expected: %v, saw: %v", buf, writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupOutputWriterFile(t *testing.T) {
|
||||
file := "output.json"
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "out")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
fullPath := path.Join(dir, file)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
_, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
writer := setupOutputWriter(dir, buf, file)
|
||||
if writer == buf {
|
||||
t.Errorf("expected: %v, saw: %v", buf, writer)
|
||||
}
|
||||
output := "some data here"
|
||||
writer.Write([]byte(output))
|
||||
|
||||
data, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != output {
|
||||
t.Errorf("expected: %v, saw: %v", output, data)
|
||||
}
|
||||
}
|
||||
314
pkg/cmd/completion/completion.go
Normal file
314
pkg/cmd/completion/completion.go
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
/*
|
||||
Copyright 2016 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 completion
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
const defaultBoilerPlate = `
|
||||
# Copyright 2016 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.
|
||||
`
|
||||
|
||||
var (
|
||||
completionLong = templates.LongDesc(i18n.T(`
|
||||
Output shell completion code for the specified shell (bash or zsh).
|
||||
The shell code must be evaluated to provide interactive
|
||||
completion of kubectl commands. This can be done by sourcing it from
|
||||
the .bash_profile.
|
||||
|
||||
Detailed instructions on how to do this are available here:
|
||||
https://kubernetes.io/docs/tasks/tools/install-kubectl/#enabling-shell-autocompletion
|
||||
|
||||
Note for zsh users: [1] zsh completions are only supported in versions of zsh >= 5.2`))
|
||||
|
||||
completionExample = templates.Examples(i18n.T(`
|
||||
# Installing bash completion on macOS using homebrew
|
||||
## If running Bash 3.2 included with macOS
|
||||
brew install bash-completion
|
||||
## or, if running Bash 4.1+
|
||||
brew install bash-completion@2
|
||||
## If kubectl is installed via homebrew, this should start working immediately.
|
||||
## If you've installed via other means, you may need add the completion to your completion directory
|
||||
kubectl completion bash > $(brew --prefix)/etc/bash_completion.d/kubectl
|
||||
|
||||
|
||||
# Installing bash completion on Linux
|
||||
## If bash-completion is not installed on Linux, please install the 'bash-completion' package
|
||||
## via your distribution's package manager.
|
||||
## Load the kubectl completion code for bash into the current shell
|
||||
source <(kubectl completion bash)
|
||||
## Write bash completion code to a file and source if from .bash_profile
|
||||
kubectl completion bash > ~/.kube/completion.bash.inc
|
||||
printf "
|
||||
# Kubectl shell completion
|
||||
source '$HOME/.kube/completion.bash.inc'
|
||||
" >> $HOME/.bash_profile
|
||||
source $HOME/.bash_profile
|
||||
|
||||
# Load the kubectl completion code for zsh[1] into the current shell
|
||||
source <(kubectl completion zsh)
|
||||
# Set the kubectl completion code for zsh[1] to autoload on startup
|
||||
kubectl completion zsh > "${fpath[1]}/_kubectl"`))
|
||||
)
|
||||
|
||||
var (
|
||||
completionShells = map[string]func(out io.Writer, boilerPlate string, cmd *cobra.Command) error{
|
||||
"bash": runCompletionBash,
|
||||
"zsh": runCompletionZsh,
|
||||
}
|
||||
)
|
||||
|
||||
// NewCmdCompletion creates the `completion` command
|
||||
func NewCmdCompletion(out io.Writer, boilerPlate string) *cobra.Command {
|
||||
shells := []string{}
|
||||
for s := range completionShells {
|
||||
shells = append(shells, s)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "completion SHELL",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Output shell completion code for the specified shell (bash or zsh)"),
|
||||
Long: completionLong,
|
||||
Example: completionExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := RunCompletion(out, boilerPlate, cmd, args)
|
||||
cmdutil.CheckErr(err)
|
||||
},
|
||||
ValidArgs: shells,
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunCompletion checks given arguments and executes command
|
||||
func RunCompletion(out io.Writer, boilerPlate string, cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "Shell not specified.")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return cmdutil.UsageErrorf(cmd, "Too many arguments. Expected only the shell type.")
|
||||
}
|
||||
run, found := completionShells[args[0]]
|
||||
if !found {
|
||||
return cmdutil.UsageErrorf(cmd, "Unsupported shell type %q.", args[0])
|
||||
}
|
||||
|
||||
return run(out, boilerPlate, cmd.Parent())
|
||||
}
|
||||
|
||||
func runCompletionBash(out io.Writer, boilerPlate string, kubectl *cobra.Command) error {
|
||||
if len(boilerPlate) == 0 {
|
||||
boilerPlate = defaultBoilerPlate
|
||||
}
|
||||
if _, err := out.Write([]byte(boilerPlate)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return kubectl.GenBashCompletion(out)
|
||||
}
|
||||
|
||||
func runCompletionZsh(out io.Writer, boilerPlate string, kubectl *cobra.Command) error {
|
||||
zshHead := "#compdef kubectl\n"
|
||||
|
||||
out.Write([]byte(zshHead))
|
||||
|
||||
if len(boilerPlate) == 0 {
|
||||
boilerPlate = defaultBoilerPlate
|
||||
}
|
||||
if _, err := out.Write([]byte(boilerPlate)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zshInitialization := `
|
||||
__kubectl_bash_source() {
|
||||
alias shopt=':'
|
||||
alias _expand=_bash_expand
|
||||
alias _complete=_bash_comp
|
||||
emulate -L sh
|
||||
setopt kshglob noshglob braceexpand
|
||||
|
||||
source "$@"
|
||||
}
|
||||
|
||||
__kubectl_type() {
|
||||
# -t is not supported by zsh
|
||||
if [ "$1" == "-t" ]; then
|
||||
shift
|
||||
|
||||
# fake Bash 4 to disable "complete -o nospace". Instead
|
||||
# "compopt +-o nospace" is used in the code to toggle trailing
|
||||
# spaces. We don't support that, but leave trailing spaces on
|
||||
# all the time
|
||||
if [ "$1" = "__kubectl_compopt" ]; then
|
||||
echo builtin
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
type "$@"
|
||||
}
|
||||
|
||||
__kubectl_compgen() {
|
||||
local completions w
|
||||
completions=( $(compgen "$@") ) || return $?
|
||||
|
||||
# filter by given word as prefix
|
||||
while [[ "$1" = -* && "$1" != -- ]]; do
|
||||
shift
|
||||
shift
|
||||
done
|
||||
if [[ "$1" == -- ]]; then
|
||||
shift
|
||||
fi
|
||||
for w in "${completions[@]}"; do
|
||||
if [[ "${w}" = "$1"* ]]; then
|
||||
echo "${w}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
__kubectl_compopt() {
|
||||
true # don't do anything. Not supported by bashcompinit in zsh
|
||||
}
|
||||
|
||||
__kubectl_ltrim_colon_completions()
|
||||
{
|
||||
if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
|
||||
# Remove colon-word prefix from COMPREPLY items
|
||||
local colon_word=${1%${1##*:}}
|
||||
local i=${#COMPREPLY[*]}
|
||||
while [[ $((--i)) -ge 0 ]]; do
|
||||
COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
__kubectl_get_comp_words_by_ref() {
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[${COMP_CWORD}-1]}"
|
||||
words=("${COMP_WORDS[@]}")
|
||||
cword=("${COMP_CWORD[@]}")
|
||||
}
|
||||
|
||||
__kubectl_filedir() {
|
||||
local RET OLD_IFS w qw
|
||||
|
||||
__kubectl_debug "_filedir $@ cur=$cur"
|
||||
if [[ "$1" = \~* ]]; then
|
||||
# somehow does not work. Maybe, zsh does not call this at all
|
||||
eval echo "$1"
|
||||
return 0
|
||||
fi
|
||||
|
||||
OLD_IFS="$IFS"
|
||||
IFS=$'\n'
|
||||
if [ "$1" = "-d" ]; then
|
||||
shift
|
||||
RET=( $(compgen -d) )
|
||||
else
|
||||
RET=( $(compgen -f) )
|
||||
fi
|
||||
IFS="$OLD_IFS"
|
||||
|
||||
IFS="," __kubectl_debug "RET=${RET[@]} len=${#RET[@]}"
|
||||
|
||||
for w in ${RET[@]}; do
|
||||
if [[ ! "${w}" = "${cur}"* ]]; then
|
||||
continue
|
||||
fi
|
||||
if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
|
||||
qw="$(__kubectl_quote "${w}")"
|
||||
if [ -d "${w}" ]; then
|
||||
COMPREPLY+=("${qw}/")
|
||||
else
|
||||
COMPREPLY+=("${qw}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
__kubectl_quote() {
|
||||
if [[ $1 == \'* || $1 == \"* ]]; then
|
||||
# Leave out first character
|
||||
printf %q "${1:1}"
|
||||
else
|
||||
printf %q "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
autoload -U +X bashcompinit && bashcompinit
|
||||
|
||||
# use word boundary patterns for BSD or GNU sed
|
||||
LWORD='[[:<:]]'
|
||||
RWORD='[[:>:]]'
|
||||
if sed --help 2>&1 | grep -q GNU; then
|
||||
LWORD='\<'
|
||||
RWORD='\>'
|
||||
fi
|
||||
|
||||
__kubectl_convert_bash_to_zsh() {
|
||||
sed \
|
||||
-e 's/declare -F/whence -w/' \
|
||||
-e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \
|
||||
-e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
|
||||
-e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
|
||||
-e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
|
||||
-e "s/${LWORD}_filedir${RWORD}/__kubectl_filedir/g" \
|
||||
-e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__kubectl_get_comp_words_by_ref/g" \
|
||||
-e "s/${LWORD}__ltrim_colon_completions${RWORD}/__kubectl_ltrim_colon_completions/g" \
|
||||
-e "s/${LWORD}compgen${RWORD}/__kubectl_compgen/g" \
|
||||
-e "s/${LWORD}compopt${RWORD}/__kubectl_compopt/g" \
|
||||
-e "s/${LWORD}declare${RWORD}/builtin declare/g" \
|
||||
-e "s/\\\$(type${RWORD}/\$(__kubectl_type/g" \
|
||||
<<'BASH_COMPLETION_EOF'
|
||||
`
|
||||
out.Write([]byte(zshInitialization))
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
kubectl.GenBashCompletion(buf)
|
||||
out.Write(buf.Bytes())
|
||||
|
||||
zshTail := `
|
||||
BASH_COMPLETION_EOF
|
||||
}
|
||||
|
||||
__kubectl_bash_source <(__kubectl_convert_bash_to_zsh)
|
||||
_complete kubectl 2>/dev/null
|
||||
`
|
||||
out.Write([]byte(zshTail))
|
||||
return nil
|
||||
}
|
||||
92
pkg/cmd/config/config.go
Normal file
92
pkg/cmd/config/config.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// NewCmdConfig creates a command object for the "config" action, and adds all child commands to it.
|
||||
func NewCmdConfig(f cmdutil.Factory, pathOptions *clientcmd.PathOptions, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
if len(pathOptions.ExplicitFileFlag) == 0 {
|
||||
pathOptions.ExplicitFileFlag = clientcmd.RecommendedConfigPathFlag
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "config SUBCOMMAND",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Modify kubeconfig files"),
|
||||
Long: templates.LongDesc(`
|
||||
Modify kubeconfig files using subcommands like "kubectl config set current-context my-context"
|
||||
|
||||
The loading order follows these rules:
|
||||
|
||||
1. If the --` + pathOptions.ExplicitFileFlag + ` flag is set, then only that file is loaded. The flag may only be set once and no merging takes place.
|
||||
2. If $` + pathOptions.EnvVar + ` environment variable is set, then it is used as a list of paths (normal path delimiting rules for your system). These paths are merged. When a value is modified, it is modified in the file that defines the stanza. When a value is created, it is created in the first file that exists. If no files in the chain exist, then it creates the last file in the list.
|
||||
3. Otherwise, ` + path.Join("${HOME}", pathOptions.GlobalFileSubpath) + ` is used and no merging takes place.`),
|
||||
Run: cmdutil.DefaultSubCommandRun(streams.ErrOut),
|
||||
}
|
||||
|
||||
// file paths are common to all sub commands
|
||||
cmd.PersistentFlags().StringVar(&pathOptions.LoadingRules.ExplicitPath, pathOptions.ExplicitFileFlag, pathOptions.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
|
||||
|
||||
// TODO(juanvallejo): update all subcommands to work with genericclioptions.IOStreams
|
||||
cmd.AddCommand(NewCmdConfigView(f, streams, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSetCluster(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSetAuthInfo(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSetContext(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSet(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigUnset(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigCurrentContext(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigUseContext(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigGetContexts(streams, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigGetClusters(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigDeleteCluster(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigDeleteContext(streams.Out, streams.ErrOut, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigRenameContext(streams.Out, pathOptions))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func toBool(propertyValue string) (bool, error) {
|
||||
boolValue := false
|
||||
if len(propertyValue) != 0 {
|
||||
var err error
|
||||
boolValue, err = strconv.ParseBool(propertyValue)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return boolValue, nil
|
||||
}
|
||||
|
||||
func helpErrorf(cmd *cobra.Command, format string, args ...interface{}) error {
|
||||
cmd.Help()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
949
pkg/cmd/config/config_test.go
Normal file
949
pkg/cmd/config/config_test.go
Normal file
|
|
@ -0,0 +1,949 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func newRedFederalCowHammerConfig() clientcmdapi.Config {
|
||||
return clientcmdapi.Config{
|
||||
AuthInfos: map[string]*clientcmdapi.AuthInfo{
|
||||
"red-user": {Token: "red-token"}},
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"cow-cluster": {Server: "http://cow.org:8080"}},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster"}},
|
||||
CurrentContext: "federal-context",
|
||||
}
|
||||
}
|
||||
|
||||
func Example_view() {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
test := configCommandTest{
|
||||
args: []string{"view"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
output := test.run(nil)
|
||||
fmt.Printf("%v", output)
|
||||
// Output:
|
||||
// apiVersion: v1
|
||||
// clusters:
|
||||
// - cluster:
|
||||
// server: http://cow.org:8080
|
||||
// name: cow-cluster
|
||||
// contexts:
|
||||
// - context:
|
||||
// cluster: cow-cluster
|
||||
// user: red-user
|
||||
// name: federal-context
|
||||
// current-context: federal-context
|
||||
// kind: Config
|
||||
// preferences: {}
|
||||
// users:
|
||||
// - name: red-user
|
||||
// user:
|
||||
// token: red-token
|
||||
}
|
||||
|
||||
func TestCurrentContext(t *testing.T) {
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
test := configCommandTest{
|
||||
args: []string{"current-context"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: startingConfig,
|
||||
expectedOutputs: []string{startingConfig.CurrentContext},
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestSetCurrentContext(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
|
||||
newContextName := "the-new-context"
|
||||
|
||||
startingConfig.Contexts[newContextName] = clientcmdapi.NewContext()
|
||||
expectedConfig.Contexts[newContextName] = clientcmdapi.NewContext()
|
||||
|
||||
expectedConfig.CurrentContext = newContextName
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"use-context", "the-new-context"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestSetNonExistentContext(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"use-context", "non-existent-config"},
|
||||
startingConfig: expectedConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
// Restore cmdutil behavior.
|
||||
cmdutil.DefaultBehaviorOnFatal()
|
||||
}()
|
||||
|
||||
// Check exit code.
|
||||
cmdutil.BehaviorOnFatal(func(e string, code int) {
|
||||
if code != 1 {
|
||||
t.Errorf("The exit code is %d, expected 1", code)
|
||||
}
|
||||
expectedOutputs := []string{`no context exists with the name: "non-existent-config"`}
|
||||
test.checkOutput(e, expectedOutputs, t)
|
||||
})
|
||||
|
||||
test.run(t)
|
||||
}()
|
||||
}
|
||||
|
||||
func TestSetIntoExistingStruct(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.AuthInfos["red-user"].Password = "new-path-value"
|
||||
test := configCommandTest{
|
||||
args: []string{"set", "users.red-user.password", "new-path-value"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestSetWithPathPrefixIntoExistingStruct(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.Clusters["cow-cluster"].Server = "http://cow.org:8080/foo/baz"
|
||||
test := configCommandTest{
|
||||
args: []string{"set", "clusters.cow-cluster.server", "http://cow.org:8080/foo/baz"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
|
||||
dc := clientcmd.NewDefaultClientConfig(expectedConfig, &clientcmd.ConfigOverrides{})
|
||||
dcc, err := dc.ClientConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
expectedHost := "http://cow.org:8080/foo/baz"
|
||||
if expectedHost != dcc.Host {
|
||||
t.Fatalf("expected client.Config.Host = %q instead of %q", expectedHost, dcc.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsetStruct(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
delete(expectedConfig.AuthInfos, "red-user")
|
||||
test := configCommandTest{
|
||||
args: []string{"unset", "users.red-user"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestUnsetField(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.AuthInfos["red-user"] = clientcmdapi.NewAuthInfo()
|
||||
test := configCommandTest{
|
||||
args: []string{"unset", "users.red-user.token"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestSetIntoNewStruct(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
cluster := clientcmdapi.NewCluster()
|
||||
cluster.Server = "new-server-value"
|
||||
expectedConfig.Clusters["big-cluster"] = cluster
|
||||
test := configCommandTest{
|
||||
args: []string{"set", "clusters.big-cluster.server", "new-server-value"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestSetBoolean(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
cluster := clientcmdapi.NewCluster()
|
||||
cluster.InsecureSkipTLSVerify = true
|
||||
expectedConfig.Clusters["big-cluster"] = cluster
|
||||
test := configCommandTest{
|
||||
args: []string{"set", "clusters.big-cluster.insecure-skip-tls-verify", "true"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestSetIntoNewConfig(t *testing.T) {
|
||||
expectedConfig := *clientcmdapi.NewConfig()
|
||||
context := clientcmdapi.NewContext()
|
||||
context.AuthInfo = "fake-user"
|
||||
expectedConfig.Contexts["new-context"] = context
|
||||
test := configCommandTest{
|
||||
args: []string{"set", "contexts.new-context.user", "fake-user"},
|
||||
startingConfig: *clientcmdapi.NewConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestNewEmptyAuth(t *testing.T) {
|
||||
expectedConfig := *clientcmdapi.NewConfig()
|
||||
expectedConfig.AuthInfos["the-user-name"] = clientcmdapi.NewAuthInfo()
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "the-user-name"},
|
||||
startingConfig: *clientcmdapi.NewConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestAdditionalAuth(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := clientcmdapi.NewAuthInfo()
|
||||
authInfo.Token = "token"
|
||||
expectedConfig.AuthInfos["another-user"] = authInfo
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagBearerToken + "=token"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestEmbedClientCert(t *testing.T) {
|
||||
fakeCertFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(fakeCertFile.Name())
|
||||
fakeData := []byte("fake-data")
|
||||
ioutil.WriteFile(fakeCertFile.Name(), fakeData, 0600)
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := clientcmdapi.NewAuthInfo()
|
||||
authInfo.ClientCertificateData = fakeData
|
||||
expectedConfig.AuthInfos["another-user"] = authInfo
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=" + fakeCertFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestEmbedClientKey(t *testing.T) {
|
||||
fakeKeyFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(fakeKeyFile.Name())
|
||||
fakeData := []byte("fake-data")
|
||||
ioutil.WriteFile(fakeKeyFile.Name(), fakeData, 0600)
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := clientcmdapi.NewAuthInfo()
|
||||
authInfo.ClientKeyData = fakeData
|
||||
expectedConfig.AuthInfos["another-user"] = authInfo
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagKeyFile + "=" + fakeKeyFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestEmbedNoKeyOrCertDisallowed(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagEmbedCerts + "=true"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
// Restore cmdutil behavior.
|
||||
cmdutil.DefaultBehaviorOnFatal()
|
||||
}()
|
||||
|
||||
// Check exit code.
|
||||
cmdutil.BehaviorOnFatal(func(e string, code int) {
|
||||
if code != 1 {
|
||||
t.Errorf("The exit code is %d, expected 1", code)
|
||||
}
|
||||
expectedOutputs := []string{"--client-certificate", "--client-key", "embed"}
|
||||
test.checkOutput(e, expectedOutputs, t)
|
||||
})
|
||||
|
||||
test.run(t)
|
||||
}()
|
||||
}
|
||||
|
||||
func TestEmptyTokenAndCertAllowed(t *testing.T) {
|
||||
fakeCertFile, _ := ioutil.TempFile("", "cert-file")
|
||||
defer os.Remove(fakeCertFile.Name())
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := clientcmdapi.NewAuthInfo()
|
||||
authInfo.ClientCertificate = path.Base(fakeCertFile.Name())
|
||||
expectedConfig.AuthInfos["another-user"] = authInfo
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=" + fakeCertFile.Name(), "--" + clientcmd.FlagBearerToken + "="},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestTokenAndCertAllowed(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := clientcmdapi.NewAuthInfo()
|
||||
authInfo.Token = "token"
|
||||
authInfo.ClientCertificate = "/cert-file"
|
||||
expectedConfig.AuthInfos["another-user"] = authInfo
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=/cert-file", "--" + clientcmd.FlagBearerToken + "=token"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestTokenAndBasicDisallowed(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagUsername + "=myuser", "--" + clientcmd.FlagBearerToken + "=token"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
// Restore cmdutil behavior.
|
||||
cmdutil.DefaultBehaviorOnFatal()
|
||||
}()
|
||||
|
||||
// Check exit code.
|
||||
cmdutil.BehaviorOnFatal(func(e string, code int) {
|
||||
if code != 1 {
|
||||
t.Errorf("The exit code is %d, expected 1", code)
|
||||
}
|
||||
|
||||
expectedOutputs := []string{"--token", "--username"}
|
||||
test.checkOutput(e, expectedOutputs, t)
|
||||
})
|
||||
|
||||
test.run(t)
|
||||
}()
|
||||
}
|
||||
|
||||
func TestBasicClearsToken(t *testing.T) {
|
||||
authInfoWithToken := clientcmdapi.NewAuthInfo()
|
||||
authInfoWithToken.Token = "token"
|
||||
|
||||
authInfoWithBasic := clientcmdapi.NewAuthInfo()
|
||||
authInfoWithBasic.Username = "myuser"
|
||||
authInfoWithBasic.Password = "mypass"
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.AuthInfos["another-user"] = authInfoWithToken
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.AuthInfos["another-user"] = authInfoWithBasic
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagUsername + "=myuser", "--" + clientcmd.FlagPassword + "=mypass"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestTokenClearsBasic(t *testing.T) {
|
||||
authInfoWithBasic := clientcmdapi.NewAuthInfo()
|
||||
authInfoWithBasic.Username = "myuser"
|
||||
authInfoWithBasic.Password = "mypass"
|
||||
|
||||
authInfoWithToken := clientcmdapi.NewAuthInfo()
|
||||
authInfoWithToken.Token = "token"
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.AuthInfos["another-user"] = authInfoWithBasic
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.AuthInfos["another-user"] = authInfoWithToken
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagBearerToken + "=token"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestTokenLeavesCert(t *testing.T) {
|
||||
authInfoWithCerts := clientcmdapi.NewAuthInfo()
|
||||
authInfoWithCerts.ClientCertificate = "cert"
|
||||
authInfoWithCerts.ClientCertificateData = []byte("certdata")
|
||||
authInfoWithCerts.ClientKey = "key"
|
||||
authInfoWithCerts.ClientKeyData = []byte("keydata")
|
||||
|
||||
authInfoWithTokenAndCerts := clientcmdapi.NewAuthInfo()
|
||||
authInfoWithTokenAndCerts.Token = "token"
|
||||
authInfoWithTokenAndCerts.ClientCertificate = "cert"
|
||||
authInfoWithTokenAndCerts.ClientCertificateData = []byte("certdata")
|
||||
authInfoWithTokenAndCerts.ClientKey = "key"
|
||||
authInfoWithTokenAndCerts.ClientKeyData = []byte("keydata")
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.AuthInfos["another-user"] = authInfoWithCerts
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.AuthInfos["another-user"] = authInfoWithTokenAndCerts
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagBearerToken + "=token"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestCertLeavesToken(t *testing.T) {
|
||||
authInfoWithToken := clientcmdapi.NewAuthInfo()
|
||||
authInfoWithToken.Token = "token"
|
||||
|
||||
authInfoWithTokenAndCerts := clientcmdapi.NewAuthInfo()
|
||||
authInfoWithTokenAndCerts.Token = "token"
|
||||
authInfoWithTokenAndCerts.ClientCertificate = "/cert"
|
||||
authInfoWithTokenAndCerts.ClientKey = "/key"
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.AuthInfos["another-user"] = authInfoWithToken
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.AuthInfos["another-user"] = authInfoWithTokenAndCerts
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=/cert", "--" + clientcmd.FlagKeyFile + "=/key"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestSetBytesBad(t *testing.T) {
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clientcmdapi.NewCluster()
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set", "clusters.another-cluster.certificate-authority-data", "cadata"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: startingConfig,
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
// Restore cmdutil behavior.
|
||||
cmdutil.DefaultBehaviorOnFatal()
|
||||
}()
|
||||
|
||||
// Check exit code.
|
||||
cmdutil.BehaviorOnFatal(func(e string, code int) {
|
||||
if code != 1 {
|
||||
t.Errorf("The exit code is %d, expected 1", code)
|
||||
}
|
||||
})
|
||||
|
||||
test.run(t)
|
||||
}()
|
||||
}
|
||||
|
||||
func TestSetBytes(t *testing.T) {
|
||||
clusterInfoWithCAData := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata")
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clientcmdapi.NewCluster()
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.Clusters["another-cluster"] = clusterInfoWithCAData
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set", "clusters.another-cluster.certificate-authority-data", "cadata", "--set-raw-bytes"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestSetBase64Bytes(t *testing.T) {
|
||||
clusterInfoWithCAData := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata")
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clientcmdapi.NewCluster()
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.Clusters["another-cluster"] = clusterInfoWithCAData
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set", "clusters.another-cluster.certificate-authority-data", "Y2FkYXRh"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestUnsetBytes(t *testing.T) {
|
||||
clusterInfoWithCAData := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata")
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clusterInfoWithCAData
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.Clusters["another-cluster"] = clientcmdapi.NewCluster()
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"unset", "clusters.another-cluster.certificate-authority-data"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestCAClearsInsecure(t *testing.T) {
|
||||
fakeCAFile, _ := ioutil.TempFile("", "ca-file")
|
||||
defer os.Remove(fakeCAFile.Name())
|
||||
clusterInfoWithInsecure := clientcmdapi.NewCluster()
|
||||
clusterInfoWithInsecure.InsecureSkipTLSVerify = true
|
||||
|
||||
clusterInfoWithCA := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCA.CertificateAuthority = path.Base(fakeCAFile.Name())
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clusterInfoWithInsecure
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.Clusters["another-cluster"] = clusterInfoWithCA
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=" + fakeCAFile.Name()},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestCAClearsCAData(t *testing.T) {
|
||||
clusterInfoWithCAData := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata")
|
||||
|
||||
clusterInfoWithCA := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCA.CertificateAuthority = "/cafile"
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clusterInfoWithCAData
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.Clusters["another-cluster"] = clusterInfoWithCA
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=/cafile", "--" + clientcmd.FlagInsecure + "=false"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestInsecureClearsCA(t *testing.T) {
|
||||
clusterInfoWithInsecure := clientcmdapi.NewCluster()
|
||||
clusterInfoWithInsecure.InsecureSkipTLSVerify = true
|
||||
|
||||
clusterInfoWithCA := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCA.CertificateAuthority = "cafile"
|
||||
clusterInfoWithCA.CertificateAuthorityData = []byte("cadata")
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clusterInfoWithCA
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.Clusters["another-cluster"] = clusterInfoWithInsecure
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagInsecure + "=true"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestCADataClearsCA(t *testing.T) {
|
||||
fakeCAFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(fakeCAFile.Name())
|
||||
fakeData := []byte("cadata")
|
||||
ioutil.WriteFile(fakeCAFile.Name(), fakeData, 0600)
|
||||
|
||||
clusterInfoWithCAData := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCAData.CertificateAuthorityData = fakeData
|
||||
|
||||
clusterInfoWithCA := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCA.CertificateAuthority = "cafile"
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clusterInfoWithCA
|
||||
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
expectedConfig.Clusters["another-cluster"] = clusterInfoWithCAData
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=" + fakeCAFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"},
|
||||
startingConfig: startingConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestEmbedNoCADisallowed(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagEmbedCerts + "=true"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
// Restore cmdutil behavior.
|
||||
cmdutil.DefaultBehaviorOnFatal()
|
||||
}()
|
||||
|
||||
// Check exit code.
|
||||
cmdutil.BehaviorOnFatal(func(e string, code int) {
|
||||
if code != 1 {
|
||||
t.Errorf("The exit code is %d, expected 1", code)
|
||||
}
|
||||
|
||||
expectedOutputs := []string{"--certificate-authority", "embed"}
|
||||
test.checkOutput(e, expectedOutputs, t)
|
||||
})
|
||||
|
||||
test.run(t)
|
||||
}()
|
||||
}
|
||||
|
||||
func TestCAAndInsecureDisallowed(t *testing.T) {
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=cafile", "--" + clientcmd.FlagInsecure + "=true"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: newRedFederalCowHammerConfig(),
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
// Restore cmdutil behavior.
|
||||
cmdutil.DefaultBehaviorOnFatal()
|
||||
}()
|
||||
|
||||
// Check exit code.
|
||||
cmdutil.BehaviorOnFatal(func(e string, code int) {
|
||||
if code != 1 {
|
||||
t.Errorf("The exit code is %d, expected 1", code)
|
||||
}
|
||||
|
||||
expectedOutputs := []string{"certificate", "insecure"}
|
||||
test.checkOutput(e, expectedOutputs, t)
|
||||
})
|
||||
|
||||
test.run(t)
|
||||
}()
|
||||
}
|
||||
|
||||
func TestMergeExistingAuth(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := expectedConfig.AuthInfos["red-user"]
|
||||
authInfo.ClientKey = "/key"
|
||||
expectedConfig.AuthInfos["red-user"] = authInfo
|
||||
test := configCommandTest{
|
||||
args: []string{"set-credentials", "red-user", "--" + clientcmd.FlagKeyFile + "=/key"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestNewEmptyCluster(t *testing.T) {
|
||||
expectedConfig := *clientcmdapi.NewConfig()
|
||||
expectedConfig.Clusters["new-cluster"] = clientcmdapi.NewCluster()
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "new-cluster"},
|
||||
startingConfig: *clientcmdapi.NewConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestAdditionalCluster(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
cluster := clientcmdapi.NewCluster()
|
||||
cluster.CertificateAuthority = "/ca-location"
|
||||
cluster.InsecureSkipTLSVerify = false
|
||||
cluster.Server = "serverlocation"
|
||||
expectedConfig.Clusters["different-cluster"] = cluster
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "different-cluster", "--" + clientcmd.FlagAPIServer + "=serverlocation", "--" + clientcmd.FlagInsecure + "=false", "--" + clientcmd.FlagCAFile + "=/ca-location"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestOverwriteExistingCluster(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
cluster := clientcmdapi.NewCluster()
|
||||
cluster.Server = "serverlocation"
|
||||
expectedConfig.Clusters["cow-cluster"] = cluster
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-cluster", "cow-cluster", "--" + clientcmd.FlagAPIServer + "=serverlocation"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestNewEmptyContext(t *testing.T) {
|
||||
expectedConfig := *clientcmdapi.NewConfig()
|
||||
expectedConfig.Contexts["new-context"] = clientcmdapi.NewContext()
|
||||
test := configCommandTest{
|
||||
args: []string{"set-context", "new-context"},
|
||||
startingConfig: *clientcmdapi.NewConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestAdditionalContext(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
context := clientcmdapi.NewContext()
|
||||
context.Cluster = "some-cluster"
|
||||
context.AuthInfo = "some-user"
|
||||
context.Namespace = "different-namespace"
|
||||
expectedConfig.Contexts["different-context"] = context
|
||||
test := configCommandTest{
|
||||
args: []string{"set-context", "different-context", "--" + clientcmd.FlagClusterName + "=some-cluster", "--" + clientcmd.FlagAuthInfoName + "=some-user", "--" + clientcmd.FlagNamespace + "=different-namespace"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestMergeExistingContext(t *testing.T) {
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
context := expectedConfig.Contexts["federal-context"]
|
||||
context.Namespace = "hammer"
|
||||
expectedConfig.Contexts["federal-context"] = context
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{"set-context", "federal-context", "--" + clientcmd.FlagNamespace + "=hammer"},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestToBool(t *testing.T) {
|
||||
type test struct {
|
||||
in string
|
||||
out bool
|
||||
err string
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
{"", false, ""},
|
||||
{"true", true, ""},
|
||||
{"on", false, `strconv.ParseBool: parsing "on": invalid syntax`},
|
||||
}
|
||||
|
||||
for _, curr := range tests {
|
||||
b, err := toBool(curr.in)
|
||||
if (len(curr.err) != 0) && err == nil {
|
||||
t.Errorf("Expected error: %v, but got nil", curr.err)
|
||||
}
|
||||
if (len(curr.err) == 0) && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if (err != nil) && (err.Error() != curr.err) {
|
||||
t.Errorf("Expected %v, got %v", curr.err, err)
|
||||
|
||||
}
|
||||
if b != curr.out {
|
||||
t.Errorf("Expected %v, got %v", curr.out, b)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testConfigCommand(args []string, startingConfig clientcmdapi.Config, t *testing.T) (string, clientcmdapi.Config) {
|
||||
fakeKubeFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err := clientcmd.WriteToFile(startingConfig, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
argsToUse := make([]string, 0, 2+len(args))
|
||||
argsToUse = append(argsToUse, "--kubeconfig="+fakeKubeFile.Name())
|
||||
argsToUse = append(argsToUse, args...)
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdConfig(cmdutil.NewFactory(genericclioptions.NewTestConfigFlags()), clientcmd.NewDefaultPathOptions(), streams)
|
||||
// "context" is a global flag, inherited from base kubectl command in the real world
|
||||
cmd.PersistentFlags().String("context", "", "The name of the kubeconfig context to use")
|
||||
cmd.SetArgs(argsToUse)
|
||||
cmd.Execute()
|
||||
|
||||
config := clientcmd.GetConfigFromFileOrDie(fakeKubeFile.Name())
|
||||
return buf.String(), *config
|
||||
}
|
||||
|
||||
type configCommandTest struct {
|
||||
args []string
|
||||
startingConfig clientcmdapi.Config
|
||||
expectedConfig clientcmdapi.Config
|
||||
expectedOutputs []string
|
||||
}
|
||||
|
||||
func (test configCommandTest) checkOutput(out string, expectedOutputs []string, t *testing.T) {
|
||||
for _, expectedOutput := range expectedOutputs {
|
||||
if !strings.Contains(out, expectedOutput) {
|
||||
t.Errorf("expected '%s' in output, got '%s'", expectedOutput, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (test configCommandTest) run(t *testing.T) string {
|
||||
out, actualConfig := testConfigCommand(test.args, test.startingConfig, t)
|
||||
|
||||
testSetNilMapsToEmpties(reflect.ValueOf(&test.expectedConfig))
|
||||
testSetNilMapsToEmpties(reflect.ValueOf(&actualConfig))
|
||||
testClearLocationOfOrigin(&actualConfig)
|
||||
|
||||
if !apiequality.Semantic.DeepEqual(test.expectedConfig, actualConfig) {
|
||||
t.Errorf("diff: %v", diff.ObjectDiff(test.expectedConfig, actualConfig))
|
||||
t.Errorf("expected: %#v\n actual: %#v", test.expectedConfig, actualConfig)
|
||||
}
|
||||
|
||||
test.checkOutput(out, test.expectedOutputs, t)
|
||||
|
||||
return out
|
||||
}
|
||||
func testClearLocationOfOrigin(config *clientcmdapi.Config) {
|
||||
for key, obj := range config.AuthInfos {
|
||||
obj.LocationOfOrigin = ""
|
||||
config.AuthInfos[key] = obj
|
||||
}
|
||||
for key, obj := range config.Clusters {
|
||||
obj.LocationOfOrigin = ""
|
||||
config.Clusters[key] = obj
|
||||
}
|
||||
for key, obj := range config.Contexts {
|
||||
obj.LocationOfOrigin = ""
|
||||
config.Contexts[key] = obj
|
||||
}
|
||||
}
|
||||
func testSetNilMapsToEmpties(curr reflect.Value) {
|
||||
actualCurrValue := curr
|
||||
if curr.Kind() == reflect.Ptr {
|
||||
actualCurrValue = curr.Elem()
|
||||
}
|
||||
|
||||
switch actualCurrValue.Kind() {
|
||||
case reflect.Map:
|
||||
for _, mapKey := range actualCurrValue.MapKeys() {
|
||||
currMapValue := actualCurrValue.MapIndex(mapKey)
|
||||
testSetNilMapsToEmpties(currMapValue)
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ {
|
||||
currFieldValue := actualCurrValue.Field(fieldIndex)
|
||||
|
||||
if currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil() {
|
||||
newValue := reflect.MakeMap(currFieldValue.Type())
|
||||
currFieldValue.Set(newValue)
|
||||
} else {
|
||||
testSetNilMapsToEmpties(currFieldValue.Addr())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
437
pkg/cmd/config/create_authinfo.go
Normal file
437
pkg/cmd/config/create_authinfo.go
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
type createAuthInfoOptions struct {
|
||||
configAccess clientcmd.ConfigAccess
|
||||
name string
|
||||
authPath cliflag.StringFlag
|
||||
clientCertificate cliflag.StringFlag
|
||||
clientKey cliflag.StringFlag
|
||||
token cliflag.StringFlag
|
||||
username cliflag.StringFlag
|
||||
password cliflag.StringFlag
|
||||
embedCertData cliflag.Tristate
|
||||
authProvider cliflag.StringFlag
|
||||
|
||||
authProviderArgs map[string]string
|
||||
authProviderArgsToRemove []string
|
||||
|
||||
execCommand cliflag.StringFlag
|
||||
execAPIVersion cliflag.StringFlag
|
||||
execArgs []string
|
||||
execEnv map[string]string
|
||||
execEnvToRemove []string
|
||||
}
|
||||
|
||||
const (
|
||||
flagAuthProvider = "auth-provider"
|
||||
flagAuthProviderArg = "auth-provider-arg"
|
||||
|
||||
flagExecCommand = "exec-command"
|
||||
flagExecAPIVersion = "exec-api-version"
|
||||
flagExecArg = "exec-arg"
|
||||
flagExecEnv = "exec-env"
|
||||
)
|
||||
|
||||
var (
|
||||
createAuthInfoLong = fmt.Sprintf(templates.LongDesc(`
|
||||
Sets a user entry in kubeconfig
|
||||
|
||||
Specifying a name that already exists will merge new fields on top of existing values.
|
||||
|
||||
Client-certificate flags:
|
||||
--%v=certfile --%v=keyfile
|
||||
|
||||
Bearer token flags:
|
||||
--%v=bearer_token
|
||||
|
||||
Basic auth flags:
|
||||
--%v=basic_user --%v=basic_password
|
||||
|
||||
Bearer token and basic auth are mutually exclusive.`), clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword)
|
||||
|
||||
createAuthInfoExample = templates.Examples(`
|
||||
# Set only the "client-key" field on the "cluster-admin"
|
||||
# entry, without touching other values:
|
||||
kubectl config set-credentials cluster-admin --client-key=~/.kube/admin.key
|
||||
|
||||
# Set basic auth for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --username=admin --password=uXFGweU9l35qcif
|
||||
|
||||
# Embed client certificate data in the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --client-certificate=~/.kube/admin.crt --embed-certs=true
|
||||
|
||||
# Enable the Google Compute Platform auth provider for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --auth-provider=gcp
|
||||
|
||||
# Enable the OpenID Connect auth provider for the "cluster-admin" entry with additional args
|
||||
kubectl config set-credentials cluster-admin --auth-provider=oidc --auth-provider-arg=client-id=foo --auth-provider-arg=client-secret=bar
|
||||
|
||||
# Remove the "client-secret" config value for the OpenID Connect auth provider for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --auth-provider=oidc --auth-provider-arg=client-secret-
|
||||
|
||||
# Enable new exec auth plugin for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta
|
||||
|
||||
# Define new exec auth plugin args for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --exec-arg=arg1 --exec-arg=arg2
|
||||
|
||||
# Create or update exec auth plugin environment variables for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --exec-env=key1=val1 --exec-env=key2=val2
|
||||
|
||||
# Remove exec auth plugin environment variables for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --exec-env=var-to-remove-`)
|
||||
)
|
||||
|
||||
// NewCmdConfigSetAuthInfo returns an Command option instance for 'config set-credentials' sub command
|
||||
func NewCmdConfigSetAuthInfo(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &createAuthInfoOptions{configAccess: configAccess}
|
||||
return newCmdConfigSetAuthInfo(out, options)
|
||||
}
|
||||
|
||||
func newCmdConfigSetAuthInfo(out io.Writer, options *createAuthInfoOptions) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf(
|
||||
"set-credentials NAME [--%v=path/to/certfile] "+
|
||||
"[--%v=path/to/keyfile] "+
|
||||
"[--%v=bearer_token] "+
|
||||
"[--%v=basic_user] "+
|
||||
"[--%v=basic_password] "+
|
||||
"[--%v=provider_name] "+
|
||||
"[--%v=key=value] "+
|
||||
"[--%v=exec_command] "+
|
||||
"[--%v=exec_api_version] "+
|
||||
"[--%v=arg] "+
|
||||
"[--%v=key=value]",
|
||||
clientcmd.FlagCertFile,
|
||||
clientcmd.FlagKeyFile,
|
||||
clientcmd.FlagBearerToken,
|
||||
clientcmd.FlagUsername,
|
||||
clientcmd.FlagPassword,
|
||||
flagAuthProvider,
|
||||
flagAuthProviderArg,
|
||||
flagExecCommand,
|
||||
flagExecAPIVersion,
|
||||
flagExecArg,
|
||||
flagExecEnv,
|
||||
),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Sets a user entry in kubeconfig"),
|
||||
Long: createAuthInfoLong,
|
||||
Example: createAuthInfoExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := options.complete(cmd, out)
|
||||
if err != nil {
|
||||
cmd.Help()
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
cmdutil.CheckErr(options.run())
|
||||
fmt.Fprintf(out, "User %q set.\n", options.name)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Var(&options.clientCertificate, clientcmd.FlagCertFile, "Path to "+clientcmd.FlagCertFile+" file for the user entry in kubeconfig")
|
||||
cmd.MarkFlagFilename(clientcmd.FlagCertFile)
|
||||
cmd.Flags().Var(&options.clientKey, clientcmd.FlagKeyFile, "Path to "+clientcmd.FlagKeyFile+" file for the user entry in kubeconfig")
|
||||
cmd.MarkFlagFilename(clientcmd.FlagKeyFile)
|
||||
cmd.Flags().Var(&options.token, clientcmd.FlagBearerToken, clientcmd.FlagBearerToken+" for the user entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.username, clientcmd.FlagUsername, clientcmd.FlagUsername+" for the user entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.password, clientcmd.FlagPassword, clientcmd.FlagPassword+" for the user entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.authProvider, flagAuthProvider, "Auth provider for the user entry in kubeconfig")
|
||||
cmd.Flags().StringSlice(flagAuthProviderArg, nil, "'key=value' arguments for the auth provider")
|
||||
cmd.Flags().Var(&options.execCommand, flagExecCommand, "Command for the exec credential plugin for the user entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.execAPIVersion, flagExecAPIVersion, "API version of the exec credential plugin for the user entry in kubeconfig")
|
||||
cmd.Flags().StringSlice(flagExecArg, nil, "New arguments for the exec credential plugin command for the user entry in kubeconfig")
|
||||
cmd.Flags().StringArray(flagExecEnv, nil, "'key=value' environment values for the exec credential plugin")
|
||||
f := cmd.Flags().VarPF(&options.embedCertData, clientcmd.FlagEmbedCerts, "", "Embed client cert/key for the user entry in kubeconfig")
|
||||
f.NoOptDefVal = "true"
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o createAuthInfoOptions) run() error {
|
||||
err := o.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := o.configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startingStanza, exists := config.AuthInfos[o.name]
|
||||
if !exists {
|
||||
startingStanza = clientcmdapi.NewAuthInfo()
|
||||
}
|
||||
authInfo := o.modifyAuthInfo(*startingStanza)
|
||||
config.AuthInfos[o.name] = &authInfo
|
||||
|
||||
if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// authInfo builds an AuthInfo object from the options
|
||||
func (o *createAuthInfoOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.AuthInfo) clientcmdapi.AuthInfo {
|
||||
modifiedAuthInfo := existingAuthInfo
|
||||
|
||||
var setToken, setBasic bool
|
||||
|
||||
if o.clientCertificate.Provided() {
|
||||
certPath := o.clientCertificate.Value()
|
||||
if o.embedCertData.Value() {
|
||||
modifiedAuthInfo.ClientCertificateData, _ = ioutil.ReadFile(certPath)
|
||||
modifiedAuthInfo.ClientCertificate = ""
|
||||
} else {
|
||||
certPath, _ = filepath.Abs(certPath)
|
||||
modifiedAuthInfo.ClientCertificate = certPath
|
||||
if len(modifiedAuthInfo.ClientCertificate) > 0 {
|
||||
modifiedAuthInfo.ClientCertificateData = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if o.clientKey.Provided() {
|
||||
keyPath := o.clientKey.Value()
|
||||
if o.embedCertData.Value() {
|
||||
modifiedAuthInfo.ClientKeyData, _ = ioutil.ReadFile(keyPath)
|
||||
modifiedAuthInfo.ClientKey = ""
|
||||
} else {
|
||||
keyPath, _ = filepath.Abs(keyPath)
|
||||
modifiedAuthInfo.ClientKey = keyPath
|
||||
if len(modifiedAuthInfo.ClientKey) > 0 {
|
||||
modifiedAuthInfo.ClientKeyData = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if o.token.Provided() {
|
||||
modifiedAuthInfo.Token = o.token.Value()
|
||||
setToken = len(modifiedAuthInfo.Token) > 0
|
||||
}
|
||||
|
||||
if o.username.Provided() {
|
||||
modifiedAuthInfo.Username = o.username.Value()
|
||||
setBasic = setBasic || len(modifiedAuthInfo.Username) > 0
|
||||
}
|
||||
if o.password.Provided() {
|
||||
modifiedAuthInfo.Password = o.password.Value()
|
||||
setBasic = setBasic || len(modifiedAuthInfo.Password) > 0
|
||||
}
|
||||
if o.authProvider.Provided() {
|
||||
newName := o.authProvider.Value()
|
||||
|
||||
// Only overwrite if the existing auth-provider is nil, or different than the newly specified one.
|
||||
if modifiedAuthInfo.AuthProvider == nil || modifiedAuthInfo.AuthProvider.Name != newName {
|
||||
modifiedAuthInfo.AuthProvider = &clientcmdapi.AuthProviderConfig{
|
||||
Name: newName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if modifiedAuthInfo.AuthProvider != nil {
|
||||
if modifiedAuthInfo.AuthProvider.Config == nil {
|
||||
modifiedAuthInfo.AuthProvider.Config = make(map[string]string)
|
||||
}
|
||||
for _, toRemove := range o.authProviderArgsToRemove {
|
||||
delete(modifiedAuthInfo.AuthProvider.Config, toRemove)
|
||||
}
|
||||
for key, value := range o.authProviderArgs {
|
||||
modifiedAuthInfo.AuthProvider.Config[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if o.execCommand.Provided() {
|
||||
newExecCommand := o.execCommand.Value()
|
||||
|
||||
// create new Exec if doesn't exist, otherwise just modify the command
|
||||
if modifiedAuthInfo.Exec == nil {
|
||||
modifiedAuthInfo.Exec = &clientcmdapi.ExecConfig{
|
||||
Command: newExecCommand,
|
||||
}
|
||||
} else {
|
||||
modifiedAuthInfo.Exec.Command = newExecCommand
|
||||
// explicitly reset exec arguments
|
||||
modifiedAuthInfo.Exec.Args = nil
|
||||
}
|
||||
}
|
||||
|
||||
// modify next values only if Exec exists, ignore these changes otherwise
|
||||
if modifiedAuthInfo.Exec != nil {
|
||||
if o.execAPIVersion.Provided() {
|
||||
modifiedAuthInfo.Exec.APIVersion = o.execAPIVersion.Value()
|
||||
}
|
||||
|
||||
// rewrite exec arguments list with new values
|
||||
if o.execArgs != nil {
|
||||
modifiedAuthInfo.Exec.Args = o.execArgs
|
||||
}
|
||||
|
||||
// iterate over the existing exec env values and remove the specified
|
||||
if o.execEnvToRemove != nil {
|
||||
newExecEnv := []clientcmdapi.ExecEnvVar{}
|
||||
for _, value := range modifiedAuthInfo.Exec.Env {
|
||||
needToRemove := false
|
||||
for _, elemToRemove := range o.execEnvToRemove {
|
||||
if value.Name == elemToRemove {
|
||||
needToRemove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !needToRemove {
|
||||
newExecEnv = append(newExecEnv, value)
|
||||
}
|
||||
}
|
||||
modifiedAuthInfo.Exec.Env = newExecEnv
|
||||
}
|
||||
|
||||
// update or create specified environment variables for the exec plugin
|
||||
if o.execEnv != nil {
|
||||
newEnv := []clientcmdapi.ExecEnvVar{}
|
||||
for newEnvName, newEnvValue := range o.execEnv {
|
||||
needToCreate := true
|
||||
for i := 0; i < len(modifiedAuthInfo.Exec.Env); i++ {
|
||||
if modifiedAuthInfo.Exec.Env[i].Name == newEnvName {
|
||||
// update the existing value
|
||||
needToCreate = false
|
||||
modifiedAuthInfo.Exec.Env[i].Value = newEnvValue
|
||||
break
|
||||
}
|
||||
}
|
||||
if needToCreate {
|
||||
// create a new env value
|
||||
newEnv = append(newEnv, clientcmdapi.ExecEnvVar{Name: newEnvName, Value: newEnvValue})
|
||||
}
|
||||
}
|
||||
modifiedAuthInfo.Exec.Env = append(modifiedAuthInfo.Exec.Env, newEnv...)
|
||||
}
|
||||
}
|
||||
|
||||
// If any auth info was set, make sure any other existing auth types are cleared
|
||||
if setToken || setBasic {
|
||||
if !setToken {
|
||||
modifiedAuthInfo.Token = ""
|
||||
}
|
||||
if !setBasic {
|
||||
modifiedAuthInfo.Username = ""
|
||||
modifiedAuthInfo.Password = ""
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedAuthInfo
|
||||
}
|
||||
|
||||
func (o *createAuthInfoOptions) complete(cmd *cobra.Command, out io.Writer) error {
|
||||
args := cmd.Flags().Args()
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("Unexpected args: %v", args)
|
||||
}
|
||||
|
||||
authProviderArgs, err := cmd.Flags().GetStringSlice(flagAuthProviderArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %s", err)
|
||||
}
|
||||
|
||||
if len(authProviderArgs) > 0 {
|
||||
newPairs, removePairs, err := cmdutil.ParsePairs(authProviderArgs, flagAuthProviderArg, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %s", err)
|
||||
}
|
||||
o.authProviderArgs = newPairs
|
||||
o.authProviderArgsToRemove = removePairs
|
||||
}
|
||||
|
||||
execArgs, err := cmd.Flags().GetStringSlice(flagExecArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %s", err)
|
||||
}
|
||||
if len(execArgs) > 0 {
|
||||
o.execArgs = execArgs
|
||||
}
|
||||
|
||||
execEnv, err := cmd.Flags().GetStringArray(flagExecEnv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %s", err)
|
||||
}
|
||||
if len(execEnv) > 0 {
|
||||
newPairs, removePairs, err := cmdutil.ParsePairs(execEnv, flagExecEnv, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %s", err)
|
||||
}
|
||||
o.execEnv = newPairs
|
||||
o.execEnvToRemove = removePairs
|
||||
}
|
||||
|
||||
o.name = args[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o createAuthInfoOptions) validate() error {
|
||||
if len(o.name) == 0 {
|
||||
return errors.New("you must specify a non-empty user name")
|
||||
}
|
||||
methods := []string{}
|
||||
if len(o.token.Value()) > 0 {
|
||||
methods = append(methods, fmt.Sprintf("--%v", clientcmd.FlagBearerToken))
|
||||
}
|
||||
if len(o.username.Value()) > 0 || len(o.password.Value()) > 0 {
|
||||
methods = append(methods, fmt.Sprintf("--%v/--%v", clientcmd.FlagUsername, clientcmd.FlagPassword))
|
||||
}
|
||||
if len(methods) > 1 {
|
||||
return fmt.Errorf("you cannot specify more than one authentication method at the same time: %v", strings.Join(methods, ", "))
|
||||
}
|
||||
if o.embedCertData.Value() {
|
||||
certPath := o.clientCertificate.Value()
|
||||
keyPath := o.clientKey.Value()
|
||||
if certPath == "" && keyPath == "" {
|
||||
return fmt.Errorf("you must specify a --%s or --%s to embed", clientcmd.FlagCertFile, clientcmd.FlagKeyFile)
|
||||
}
|
||||
if certPath != "" {
|
||||
if _, err := ioutil.ReadFile(certPath); err != nil {
|
||||
return fmt.Errorf("error reading %s data from %s: %v", clientcmd.FlagCertFile, certPath, err)
|
||||
}
|
||||
}
|
||||
if keyPath != "" {
|
||||
if _, err := ioutil.ReadFile(keyPath); err != nil {
|
||||
return fmt.Errorf("error reading %s data from %s: %v", clientcmd.FlagKeyFile, keyPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
493
pkg/cmd/config/create_authinfo_test.go
Normal file
493
pkg/cmd/config/create_authinfo_test.go
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
/*
|
||||
Copyright 2016 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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
)
|
||||
|
||||
func stringFlagFor(s string) cliflag.StringFlag {
|
||||
var f cliflag.StringFlag
|
||||
f.Set(s)
|
||||
return f
|
||||
}
|
||||
|
||||
func TestCreateAuthInfoOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
wantParseErr bool
|
||||
wantCompleteErr bool
|
||||
wantValidateErr bool
|
||||
|
||||
wantOptions *createAuthInfoOptions
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
flags: []string{
|
||||
"me",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
flags: []string{
|
||||
"me",
|
||||
"--token=foo",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
token: stringFlagFor("foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test3",
|
||||
flags: []string{
|
||||
"me",
|
||||
"--username=jane",
|
||||
"--password=bar",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
username: stringFlagFor("jane"),
|
||||
password: stringFlagFor("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test4",
|
||||
// Cannot provide both token and basic auth.
|
||||
flags: []string{
|
||||
"me",
|
||||
"--token=foo",
|
||||
"--username=jane",
|
||||
"--password=bar",
|
||||
},
|
||||
wantValidateErr: true,
|
||||
},
|
||||
{
|
||||
name: "test5",
|
||||
flags: []string{
|
||||
"--auth-provider=oidc",
|
||||
"--auth-provider-arg=client-id=foo",
|
||||
"--auth-provider-arg=client-secret=bar",
|
||||
"me",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
authProvider: stringFlagFor("oidc"),
|
||||
authProviderArgs: map[string]string{
|
||||
"client-id": "foo",
|
||||
"client-secret": "bar",
|
||||
},
|
||||
authProviderArgsToRemove: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test6",
|
||||
flags: []string{
|
||||
"--auth-provider=oidc",
|
||||
"--auth-provider-arg=client-id-",
|
||||
"--auth-provider-arg=client-secret-",
|
||||
"me",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
authProvider: stringFlagFor("oidc"),
|
||||
authProviderArgs: map[string]string{},
|
||||
authProviderArgsToRemove: []string{
|
||||
"client-id",
|
||||
"client-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test7",
|
||||
flags: []string{
|
||||
"--auth-provider-arg=client-id-", // auth provider name not required
|
||||
"--auth-provider-arg=client-secret-",
|
||||
"me",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
authProviderArgs: map[string]string{},
|
||||
authProviderArgsToRemove: []string{
|
||||
"client-id",
|
||||
"client-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test8",
|
||||
flags: []string{
|
||||
"--auth-provider=oidc",
|
||||
"--auth-provider-arg=client-id", // values must be of form 'key=value' or 'key-'
|
||||
"me",
|
||||
},
|
||||
wantCompleteErr: true,
|
||||
},
|
||||
{
|
||||
name: "test9",
|
||||
flags: []string{
|
||||
// No name for authinfo provided.
|
||||
},
|
||||
wantCompleteErr: true,
|
||||
},
|
||||
{
|
||||
name: "test10",
|
||||
flags: []string{
|
||||
"--exec-command=example-client-go-exec-plugin",
|
||||
"me",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
execCommand: stringFlagFor("example-client-go-exec-plugin"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test11",
|
||||
flags: []string{
|
||||
"--exec-command=example-client-go-exec-plugin",
|
||||
"--exec-arg=arg1",
|
||||
"--exec-arg=arg2",
|
||||
"me",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
execCommand: stringFlagFor("example-client-go-exec-plugin"),
|
||||
execArgs: []string{"arg1", "arg2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test12",
|
||||
flags: []string{
|
||||
"--exec-command=example-client-go-exec-plugin",
|
||||
"--exec-env=key1=val1",
|
||||
"--exec-env=key2=val2",
|
||||
"--exec-env=env-remove1-",
|
||||
"--exec-env=env-remove2-",
|
||||
"me",
|
||||
},
|
||||
wantOptions: &createAuthInfoOptions{
|
||||
name: "me",
|
||||
execCommand: stringFlagFor("example-client-go-exec-plugin"),
|
||||
execEnv: map[string]string{"key1": "val1", "key2": "val2"},
|
||||
execEnvToRemove: []string{"env-remove1", "env-remove2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buff := new(bytes.Buffer)
|
||||
|
||||
opts := new(createAuthInfoOptions)
|
||||
cmd := newCmdConfigSetAuthInfo(buff, opts)
|
||||
if err := cmd.ParseFlags(tt.flags); err != nil {
|
||||
if !tt.wantParseErr {
|
||||
t.Errorf("case %s: parsing error for flags %q: %v: %s", tt.name, tt.flags, err, buff)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantParseErr {
|
||||
t.Errorf("case %s: expected parsing error for flags %q: %s", tt.name, tt.flags, buff)
|
||||
return
|
||||
}
|
||||
|
||||
if err := opts.complete(cmd, buff); err != nil {
|
||||
if !tt.wantCompleteErr {
|
||||
t.Errorf("case %s: complete() error for flags %q: %s", tt.name, tt.flags, buff)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantCompleteErr {
|
||||
t.Errorf("case %s: complete() expected errors for flags %q: %s", tt.name, tt.flags, buff)
|
||||
return
|
||||
}
|
||||
|
||||
if err := opts.validate(); err != nil {
|
||||
if !tt.wantValidateErr {
|
||||
t.Errorf("case %s: flags %q: validate failed: %v", tt.name, tt.flags, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantValidateErr {
|
||||
t.Errorf("case %s: flags %q: expected validate to fail", tt.name, tt.flags)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(opts, tt.wantOptions) {
|
||||
t.Errorf("case %s: flags %q: mis-matched options,\nwanted=%#v\ngot= %#v", tt.name, tt.flags, tt.wantOptions, opts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModifyExistingAuthInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
wantParseErr bool
|
||||
wantCompleteErr bool
|
||||
wantValidateErr bool
|
||||
|
||||
existingAuthInfo clientcmdapi.AuthInfo
|
||||
wantAuthInfo clientcmdapi.AuthInfo
|
||||
}{
|
||||
{
|
||||
name: "1. create new exec config",
|
||||
flags: []string{
|
||||
"--exec-command=example-client-go-exec-plugin",
|
||||
"--exec-api-version=client.authentication.k8s.io/v1",
|
||||
"me",
|
||||
},
|
||||
existingAuthInfo: clientcmdapi.AuthInfo{},
|
||||
wantAuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "example-client-go-exec-plugin",
|
||||
APIVersion: "client.authentication.k8s.io/v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2. redefine exec args",
|
||||
flags: []string{
|
||||
"--exec-arg=new-arg1",
|
||||
"--exec-arg=new-arg2",
|
||||
"me",
|
||||
},
|
||||
existingAuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "example-client-go-exec-plugin",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Args: []string{"existing-arg1", "existing-arg2"},
|
||||
},
|
||||
},
|
||||
wantAuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "example-client-go-exec-plugin",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Args: []string{"new-arg1", "new-arg2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "3. reset exec args",
|
||||
flags: []string{
|
||||
"--exec-command=example-client-go-exec-plugin",
|
||||
"me",
|
||||
},
|
||||
existingAuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "example-client-go-exec-plugin",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Args: []string{"existing-arg1", "existing-arg2"},
|
||||
},
|
||||
},
|
||||
wantAuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "example-client-go-exec-plugin",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "4. modify exec env variables",
|
||||
flags: []string{
|
||||
"--exec-command=example-client-go-exec-plugin",
|
||||
"--exec-env=name1=value1000",
|
||||
"--exec-env=name3=value3",
|
||||
"--exec-env=name2-",
|
||||
"--exec-env=non-existing-",
|
||||
"me",
|
||||
},
|
||||
existingAuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "existing-command",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Env: []clientcmdapi.ExecEnvVar{
|
||||
{Name: "name1", Value: "value1"},
|
||||
{Name: "name2", Value: "value2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
Command: "example-client-go-exec-plugin",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Env: []clientcmdapi.ExecEnvVar{
|
||||
{Name: "name1", Value: "value1000"},
|
||||
{Name: "name3", Value: "value3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "5. modify auth provider arguments",
|
||||
flags: []string{
|
||||
"--auth-provider=new-auth-provider",
|
||||
"--auth-provider-arg=key1=val1000",
|
||||
"--auth-provider-arg=key3=val3",
|
||||
"--auth-provider-arg=key2-",
|
||||
"--auth-provider-arg=non-existing-",
|
||||
"me",
|
||||
},
|
||||
existingAuthInfo: clientcmdapi.AuthInfo{
|
||||
AuthProvider: &clientcmdapi.AuthProviderConfig{
|
||||
Name: "auth-provider",
|
||||
Config: map[string]string{
|
||||
"key1": "val1",
|
||||
"key2": "val2",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAuthInfo: clientcmdapi.AuthInfo{
|
||||
AuthProvider: &clientcmdapi.AuthProviderConfig{
|
||||
Name: "new-auth-provider",
|
||||
Config: map[string]string{
|
||||
"key1": "val1000",
|
||||
"key3": "val3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buff := new(bytes.Buffer)
|
||||
|
||||
opts := new(createAuthInfoOptions)
|
||||
cmd := newCmdConfigSetAuthInfo(buff, opts)
|
||||
if err := cmd.ParseFlags(tt.flags); err != nil {
|
||||
if !tt.wantParseErr {
|
||||
t.Errorf("case %s: parsing error for flags %q: %v: %s", tt.name, tt.flags, err, buff)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantParseErr {
|
||||
t.Errorf("case %s: expected parsing error for flags %q: %s", tt.name, tt.flags, buff)
|
||||
return
|
||||
}
|
||||
|
||||
if err := opts.complete(cmd, buff); err != nil {
|
||||
if !tt.wantCompleteErr {
|
||||
t.Errorf("case %s: complete() error for flags %q: %s", tt.name, tt.flags, buff)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantCompleteErr {
|
||||
t.Errorf("case %s: complete() expected errors for flags %q: %s", tt.name, tt.flags, buff)
|
||||
return
|
||||
}
|
||||
|
||||
if err := opts.validate(); err != nil {
|
||||
if !tt.wantValidateErr {
|
||||
t.Errorf("case %s: flags %q: validate failed: %v", tt.name, tt.flags, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantValidateErr {
|
||||
t.Errorf("case %s: flags %q: expected validate to fail", tt.name, tt.flags)
|
||||
return
|
||||
}
|
||||
|
||||
modifiedAuthInfo := opts.modifyAuthInfo(tt.existingAuthInfo)
|
||||
|
||||
if !reflect.DeepEqual(modifiedAuthInfo, tt.wantAuthInfo) {
|
||||
t.Errorf("case %s: flags %q: mis-matched auth info,\nwanted=%#v\ngot= %#v", tt.name, tt.flags, tt.wantAuthInfo, modifiedAuthInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type createAuthInfoTest struct {
|
||||
description string
|
||||
config clientcmdapi.Config
|
||||
args []string
|
||||
flags []string
|
||||
expected string
|
||||
expectedConfig clientcmdapi.Config
|
||||
}
|
||||
|
||||
func TestCreateAuthInfo(t *testing.T) {
|
||||
conf := clientcmdapi.Config{}
|
||||
test := createAuthInfoTest{
|
||||
description: "Testing for create aythinfo",
|
||||
config: conf,
|
||||
args: []string{"cluster-admin"},
|
||||
flags: []string{
|
||||
"--username=admin",
|
||||
"--password=uXFGweU9l35qcif",
|
||||
},
|
||||
expected: `User "cluster-admin" set.` + "\n",
|
||||
expectedConfig: clientcmdapi.Config{
|
||||
AuthInfos: map[string]*clientcmdapi.AuthInfo{
|
||||
"cluster-admin": {Username: "admin", Password: "uXFGweU9l35qcif"}},
|
||||
},
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
func (test createAuthInfoTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile(os.TempDir(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigSetAuthInfo(buf, pathOptions)
|
||||
cmd.SetArgs(test.args)
|
||||
cmd.Flags().Parse(test.flags)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v,kubectl config set-credentials args: %v,flags: %v", err, test.args, test.flags)
|
||||
}
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
if len(test.expected) != 0 {
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("Fail in %q:\n expected %v\n but got %v\n", test.description, test.expected, buf.String())
|
||||
}
|
||||
}
|
||||
if test.expectedConfig.AuthInfos != nil {
|
||||
expectAuthInfo := test.expectedConfig.AuthInfos[test.args[0]]
|
||||
actualAuthInfo := config.AuthInfos[test.args[0]]
|
||||
if expectAuthInfo.Username != actualAuthInfo.Username || expectAuthInfo.Password != actualAuthInfo.Password {
|
||||
t.Errorf("Fail in %q:\n expected AuthInfo%v\n but found %v in kubeconfig\n", test.description, expectAuthInfo, actualAuthInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
180
pkg/cmd/config/create_cluster.go
Normal file
180
pkg/cmd/config/create_cluster.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
type createClusterOptions struct {
|
||||
configAccess clientcmd.ConfigAccess
|
||||
name string
|
||||
server cliflag.StringFlag
|
||||
insecureSkipTLSVerify cliflag.Tristate
|
||||
certificateAuthority cliflag.StringFlag
|
||||
embedCAData cliflag.Tristate
|
||||
}
|
||||
|
||||
var (
|
||||
createClusterLong = templates.LongDesc(`
|
||||
Sets a cluster entry in kubeconfig.
|
||||
|
||||
Specifying a name that already exists will merge new fields on top of existing values for those fields.`)
|
||||
|
||||
createClusterExample = templates.Examples(`
|
||||
# Set only the server field on the e2e cluster entry without touching other values.
|
||||
kubectl config set-cluster e2e --server=https://1.2.3.4
|
||||
|
||||
# Embed certificate authority data for the e2e cluster entry
|
||||
kubectl config set-cluster e2e --certificate-authority=~/.kube/e2e/kubernetes.ca.crt
|
||||
|
||||
# Disable cert checking for the dev cluster entry
|
||||
kubectl config set-cluster e2e --insecure-skip-tls-verify=true`)
|
||||
)
|
||||
|
||||
// NewCmdConfigSetCluster returns a Command instance for 'config set-cluster' sub command
|
||||
func NewCmdConfigSetCluster(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &createClusterOptions{configAccess: configAccess}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("set-cluster NAME [--%v=server] [--%v=path/to/certificate/authority] [--%v=true]", clientcmd.FlagAPIServer, clientcmd.FlagCAFile, clientcmd.FlagInsecure),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Sets a cluster entry in kubeconfig"),
|
||||
Long: createClusterLong,
|
||||
Example: createClusterExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.complete(cmd))
|
||||
cmdutil.CheckErr(options.run())
|
||||
fmt.Fprintf(out, "Cluster %q set.\n", options.name)
|
||||
},
|
||||
}
|
||||
|
||||
options.insecureSkipTLSVerify.Default(false)
|
||||
|
||||
cmd.Flags().Var(&options.server, clientcmd.FlagAPIServer, clientcmd.FlagAPIServer+" for the cluster entry in kubeconfig")
|
||||
f := cmd.Flags().VarPF(&options.insecureSkipTLSVerify, clientcmd.FlagInsecure, "", clientcmd.FlagInsecure+" for the cluster entry in kubeconfig")
|
||||
f.NoOptDefVal = "true"
|
||||
cmd.Flags().Var(&options.certificateAuthority, clientcmd.FlagCAFile, "Path to "+clientcmd.FlagCAFile+" file for the cluster entry in kubeconfig")
|
||||
cmd.MarkFlagFilename(clientcmd.FlagCAFile)
|
||||
f = cmd.Flags().VarPF(&options.embedCAData, clientcmd.FlagEmbedCerts, "", clientcmd.FlagEmbedCerts+" for the cluster entry in kubeconfig")
|
||||
f.NoOptDefVal = "true"
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o createClusterOptions) run() error {
|
||||
err := o.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := o.configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startingStanza, exists := config.Clusters[o.name]
|
||||
if !exists {
|
||||
startingStanza = clientcmdapi.NewCluster()
|
||||
}
|
||||
cluster := o.modifyCluster(*startingStanza)
|
||||
config.Clusters[o.name] = &cluster
|
||||
|
||||
if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cluster builds a Cluster object from the options
|
||||
func (o *createClusterOptions) modifyCluster(existingCluster clientcmdapi.Cluster) clientcmdapi.Cluster {
|
||||
modifiedCluster := existingCluster
|
||||
|
||||
if o.server.Provided() {
|
||||
modifiedCluster.Server = o.server.Value()
|
||||
}
|
||||
if o.insecureSkipTLSVerify.Provided() {
|
||||
modifiedCluster.InsecureSkipTLSVerify = o.insecureSkipTLSVerify.Value()
|
||||
// Specifying insecure mode clears any certificate authority
|
||||
if modifiedCluster.InsecureSkipTLSVerify {
|
||||
modifiedCluster.CertificateAuthority = ""
|
||||
modifiedCluster.CertificateAuthorityData = nil
|
||||
}
|
||||
}
|
||||
if o.certificateAuthority.Provided() {
|
||||
caPath := o.certificateAuthority.Value()
|
||||
if o.embedCAData.Value() {
|
||||
modifiedCluster.CertificateAuthorityData, _ = ioutil.ReadFile(caPath)
|
||||
modifiedCluster.InsecureSkipTLSVerify = false
|
||||
modifiedCluster.CertificateAuthority = ""
|
||||
} else {
|
||||
caPath, _ = filepath.Abs(caPath)
|
||||
modifiedCluster.CertificateAuthority = caPath
|
||||
// Specifying a certificate authority file clears certificate authority data and insecure mode
|
||||
if caPath != "" {
|
||||
modifiedCluster.InsecureSkipTLSVerify = false
|
||||
modifiedCluster.CertificateAuthorityData = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedCluster
|
||||
}
|
||||
|
||||
func (o *createClusterOptions) complete(cmd *cobra.Command) error {
|
||||
args := cmd.Flags().Args()
|
||||
if len(args) != 1 {
|
||||
return helpErrorf(cmd, "Unexpected args: %v", args)
|
||||
}
|
||||
|
||||
o.name = args[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o createClusterOptions) validate() error {
|
||||
if len(o.name) == 0 {
|
||||
return errors.New("you must specify a non-empty cluster name")
|
||||
}
|
||||
if o.insecureSkipTLSVerify.Value() && o.certificateAuthority.Value() != "" {
|
||||
return errors.New("you cannot specify a certificate authority and insecure mode at the same time")
|
||||
}
|
||||
if o.embedCAData.Value() {
|
||||
caPath := o.certificateAuthority.Value()
|
||||
if caPath == "" {
|
||||
return fmt.Errorf("you must specify a --%s to embed", clientcmd.FlagCAFile)
|
||||
}
|
||||
if _, err := ioutil.ReadFile(caPath); err != nil {
|
||||
return fmt.Errorf("could not read %s data from %s: %v", clientcmd.FlagCAFile, caPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
119
pkg/cmd/config/create_cluster_test.go
Normal file
119
pkg/cmd/config/create_cluster_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type createClusterTest struct {
|
||||
description string
|
||||
config clientcmdapi.Config
|
||||
args []string
|
||||
flags []string
|
||||
expected string
|
||||
expectedConfig clientcmdapi.Config
|
||||
}
|
||||
|
||||
func TestCreateCluster(t *testing.T) {
|
||||
conf := clientcmdapi.Config{}
|
||||
test := createClusterTest{
|
||||
description: "Testing 'kubectl config set-cluster' with a new cluster",
|
||||
config: conf,
|
||||
args: []string{"my-cluster"},
|
||||
flags: []string{
|
||||
"--server=http://192.168.0.1",
|
||||
},
|
||||
expected: `Cluster "my-cluster" set.` + "\n",
|
||||
expectedConfig: clientcmdapi.Config{
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"my-cluster": {Server: "http://192.168.0.1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestModifyCluster(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"my-cluster": {Server: "https://192.168.0.1"},
|
||||
},
|
||||
}
|
||||
test := createClusterTest{
|
||||
description: "Testing 'kubectl config set-cluster' with an existing cluster",
|
||||
config: conf,
|
||||
args: []string{"my-cluster"},
|
||||
flags: []string{
|
||||
"--server=https://192.168.0.99",
|
||||
},
|
||||
expected: `Cluster "my-cluster" set.` + "\n",
|
||||
expectedConfig: clientcmdapi.Config{
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"my-cluster": {Server: "https://192.168.0.99"},
|
||||
},
|
||||
},
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test createClusterTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile(os.TempDir(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigSetCluster(buf, pathOptions)
|
||||
cmd.SetArgs(test.args)
|
||||
cmd.Flags().Parse(test.flags)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v, args: %v, flags: %v", err, test.args, test.flags)
|
||||
}
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
if len(test.expected) != 0 {
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("Failed in %q\n expected %v\n but got %v", test.description, test.expected, buf.String())
|
||||
}
|
||||
}
|
||||
if len(test.args) > 0 {
|
||||
cluster, ok := config.Clusters[test.args[0]]
|
||||
if !ok {
|
||||
t.Errorf("expected cluster %v, but got nil", test.args[0])
|
||||
return
|
||||
}
|
||||
if cluster.Server != test.expectedConfig.Clusters[test.args[0]].Server {
|
||||
t.Errorf("Fail in %q\n expected cluster server %v\n but got %v\n ", test.description, test.expectedConfig.Clusters[test.args[0]].Server, cluster.Server)
|
||||
}
|
||||
}
|
||||
}
|
||||
153
pkg/cmd/config/create_context.go
Normal file
153
pkg/cmd/config/create_context.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
type createContextOptions struct {
|
||||
configAccess clientcmd.ConfigAccess
|
||||
name string
|
||||
currContext bool
|
||||
cluster cliflag.StringFlag
|
||||
authInfo cliflag.StringFlag
|
||||
namespace cliflag.StringFlag
|
||||
}
|
||||
|
||||
var (
|
||||
createContextLong = templates.LongDesc(`
|
||||
Sets a context entry in kubeconfig
|
||||
|
||||
Specifying a name that already exists will merge new fields on top of existing values for those fields.`)
|
||||
|
||||
createContextExample = templates.Examples(`
|
||||
# Set the user field on the gce context entry without touching other values
|
||||
kubectl config set-context gce --user=cluster-admin`)
|
||||
)
|
||||
|
||||
// NewCmdConfigSetContext returns a Command instance for 'config set-context' sub command
|
||||
func NewCmdConfigSetContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &createContextOptions{configAccess: configAccess}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("set-context [NAME | --current] [--%v=cluster_nickname] [--%v=user_nickname] [--%v=namespace]", clientcmd.FlagClusterName, clientcmd.FlagAuthInfoName, clientcmd.FlagNamespace),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Sets a context entry in kubeconfig"),
|
||||
Long: createContextLong,
|
||||
Example: createContextExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.complete(cmd))
|
||||
name, exists, err := options.run()
|
||||
cmdutil.CheckErr(err)
|
||||
if exists {
|
||||
fmt.Fprintf(out, "Context %q modified.\n", name)
|
||||
} else {
|
||||
fmt.Fprintf(out, "Context %q created.\n", name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&options.currContext, "current", options.currContext, "Modify the current context")
|
||||
cmd.Flags().Var(&options.cluster, clientcmd.FlagClusterName, clientcmd.FlagClusterName+" for the context entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.authInfo, clientcmd.FlagAuthInfoName, clientcmd.FlagAuthInfoName+" for the context entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.namespace, clientcmd.FlagNamespace, clientcmd.FlagNamespace+" for the context entry in kubeconfig")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o createContextOptions) run() (string, bool, error) {
|
||||
err := o.validate()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
config, err := o.configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
name := o.name
|
||||
if o.currContext {
|
||||
if len(config.CurrentContext) == 0 {
|
||||
return "", false, errors.New("no current context is set")
|
||||
}
|
||||
name = config.CurrentContext
|
||||
}
|
||||
|
||||
startingStanza, exists := config.Contexts[name]
|
||||
if !exists {
|
||||
startingStanza = clientcmdapi.NewContext()
|
||||
}
|
||||
context := o.modifyContext(*startingStanza)
|
||||
config.Contexts[name] = &context
|
||||
|
||||
if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil {
|
||||
return name, exists, err
|
||||
}
|
||||
|
||||
return name, exists, nil
|
||||
}
|
||||
|
||||
func (o *createContextOptions) modifyContext(existingContext clientcmdapi.Context) clientcmdapi.Context {
|
||||
modifiedContext := existingContext
|
||||
|
||||
if o.cluster.Provided() {
|
||||
modifiedContext.Cluster = o.cluster.Value()
|
||||
}
|
||||
if o.authInfo.Provided() {
|
||||
modifiedContext.AuthInfo = o.authInfo.Value()
|
||||
}
|
||||
if o.namespace.Provided() {
|
||||
modifiedContext.Namespace = o.namespace.Value()
|
||||
}
|
||||
|
||||
return modifiedContext
|
||||
}
|
||||
|
||||
func (o *createContextOptions) complete(cmd *cobra.Command) error {
|
||||
args := cmd.Flags().Args()
|
||||
if len(args) > 1 {
|
||||
return helpErrorf(cmd, "Unexpected args: %v", args)
|
||||
}
|
||||
if len(args) == 1 {
|
||||
o.name = args[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o createContextOptions) validate() error {
|
||||
if len(o.name) == 0 && !o.currContext {
|
||||
return errors.New("you must specify a non-empty context name or --current")
|
||||
}
|
||||
if len(o.name) > 0 && o.currContext {
|
||||
return errors.New("you cannot specify both a context name and --current")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
147
pkg/cmd/config/create_context_test.go
Normal file
147
pkg/cmd/config/create_context_test.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type createContextTest struct {
|
||||
description string
|
||||
testContext string // name of the context being modified
|
||||
config clientcmdapi.Config //initiate kubectl config
|
||||
args []string //kubectl set-context args
|
||||
flags []string //kubectl set-context flags
|
||||
expected string //expectd out
|
||||
expectedConfig clientcmdapi.Config //expect kubectl config
|
||||
}
|
||||
|
||||
func TestCreateContext(t *testing.T) {
|
||||
conf := clientcmdapi.Config{}
|
||||
test := createContextTest{
|
||||
testContext: "shaker-context",
|
||||
description: "Testing for create a new context",
|
||||
config: conf,
|
||||
args: []string{"shaker-context"},
|
||||
flags: []string{
|
||||
"--cluster=cluster_nickname",
|
||||
"--user=user_nickname",
|
||||
"--namespace=namespace",
|
||||
},
|
||||
expected: `Context "shaker-context" created.` + "\n",
|
||||
expectedConfig: clientcmdapi.Config{
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "user_nickname", Cluster: "cluster_nickname", Namespace: "namespace"}},
|
||||
},
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
func TestModifyContext(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"},
|
||||
"not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}
|
||||
test := createContextTest{
|
||||
testContext: "shaker-context",
|
||||
description: "Testing for modify a already exist context",
|
||||
config: conf,
|
||||
args: []string{"shaker-context"},
|
||||
flags: []string{
|
||||
"--cluster=cluster_nickname",
|
||||
"--user=user_nickname",
|
||||
"--namespace=namespace",
|
||||
},
|
||||
expected: `Context "shaker-context" modified.` + "\n",
|
||||
expectedConfig: clientcmdapi.Config{
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "user_nickname", Cluster: "cluster_nickname", Namespace: "namespace"},
|
||||
"not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}},
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestModifyCurrentContext(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
CurrentContext: "shaker-context",
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"},
|
||||
"not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}
|
||||
test := createContextTest{
|
||||
testContext: "shaker-context",
|
||||
description: "Testing for modify a current context",
|
||||
config: conf,
|
||||
args: []string{},
|
||||
flags: []string{
|
||||
"--current",
|
||||
"--cluster=cluster_nickname",
|
||||
"--user=user_nickname",
|
||||
"--namespace=namespace",
|
||||
},
|
||||
expected: `Context "shaker-context" modified.` + "\n",
|
||||
expectedConfig: clientcmdapi.Config{
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "user_nickname", Cluster: "cluster_nickname", Namespace: "namespace"},
|
||||
"not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}},
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test createContextTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile(os.TempDir(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigSetContext(buf, pathOptions)
|
||||
cmd.SetArgs(test.args)
|
||||
cmd.Flags().Parse(test.flags)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v,kubectl set-context args: %v,flags: %v", err, test.args, test.flags)
|
||||
}
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
if len(test.expected) != 0 {
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("Fail in %q:\n expected %v\n but got %v\n", test.description, test.expected, buf.String())
|
||||
}
|
||||
}
|
||||
if test.expectedConfig.Contexts != nil {
|
||||
expectContext := test.expectedConfig.Contexts[test.testContext]
|
||||
actualContext := config.Contexts[test.testContext]
|
||||
if expectContext.AuthInfo != actualContext.AuthInfo || expectContext.Cluster != actualContext.Cluster ||
|
||||
expectContext.Namespace != actualContext.Namespace {
|
||||
t.Errorf("Fail in %q:\n expected Context %v\n but found %v in kubeconfig\n", test.description, expectContext, actualContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
76
pkg/cmd/config/current_context.go
Normal file
76
pkg/cmd/config/current_context.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// CurrentContextOptions holds the command-line options for 'config current-context' sub command
|
||||
type CurrentContextOptions struct {
|
||||
ConfigAccess clientcmd.ConfigAccess
|
||||
}
|
||||
|
||||
var (
|
||||
currentContextLong = templates.LongDesc(`
|
||||
Displays the current-context`)
|
||||
|
||||
currentContextExample = templates.Examples(`
|
||||
# Display the current-context
|
||||
kubectl config current-context`)
|
||||
)
|
||||
|
||||
// NewCmdConfigCurrentContext returns a Command instance for 'config current-context' sub command
|
||||
func NewCmdConfigCurrentContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &CurrentContextOptions{ConfigAccess: configAccess}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "current-context",
|
||||
Short: i18n.T("Displays the current-context"),
|
||||
Long: currentContextLong,
|
||||
Example: currentContextExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(RunCurrentContext(out, options))
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunCurrentContext performs the execution of 'config current-context' sub command
|
||||
func RunCurrentContext(out io.Writer, options *CurrentContextOptions) error {
|
||||
config, err := options.ConfigAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.CurrentContext == "" {
|
||||
err = fmt.Errorf("current-context is not set")
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "%s\n", config.CurrentContext)
|
||||
return nil
|
||||
}
|
||||
93
pkg/cmd/config/current_context_test.go
Normal file
93
pkg/cmd/config/current_context_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type currentContextTest struct {
|
||||
startingConfig clientcmdapi.Config
|
||||
expectedError string
|
||||
}
|
||||
|
||||
func newFederalContextConfig() clientcmdapi.Config {
|
||||
return clientcmdapi.Config{
|
||||
CurrentContext: "federal-context",
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentContextWithSetContext(t *testing.T) {
|
||||
test := currentContextTest{
|
||||
startingConfig: newFederalContextConfig(),
|
||||
expectedError: "",
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestCurrentContextWithUnsetContext(t *testing.T) {
|
||||
test := currentContextTest{
|
||||
startingConfig: *clientcmdapi.NewConfig(),
|
||||
expectedError: "current-context is not set",
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test currentContextTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.startingConfig, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
options := CurrentContextOptions{
|
||||
ConfigAccess: pathOptions,
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err = RunCurrentContext(buf, &options)
|
||||
if len(test.expectedError) != 0 {
|
||||
if err == nil {
|
||||
t.Errorf("Did not get %v", test.expectedError)
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), test.expectedError) {
|
||||
t.Errorf("Expected %v, but got %v", test.expectedError, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
84
pkg/cmd/config/delete_cluster.go
Normal file
84
pkg/cmd/config/delete_cluster.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2016 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
deleteClusterExample = templates.Examples(`
|
||||
# Delete the minikube cluster
|
||||
kubectl config delete-cluster minikube`)
|
||||
)
|
||||
|
||||
// NewCmdConfigDeleteCluster returns a Command instance for 'config delete-cluster' sub command
|
||||
func NewCmdConfigDeleteCluster(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete-cluster NAME",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Delete the specified cluster from the kubeconfig"),
|
||||
Long: "Delete the specified cluster from the kubeconfig",
|
||||
Example: deleteClusterExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(runDeleteCluster(out, configAccess, cmd))
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runDeleteCluster(out io.Writer, configAccess clientcmd.ConfigAccess, cmd *cobra.Command) error {
|
||||
config, err := configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := cmd.Flags().Args()
|
||||
if len(args) != 1 {
|
||||
cmd.Help()
|
||||
return nil
|
||||
}
|
||||
|
||||
configFile := configAccess.GetDefaultFilename()
|
||||
if configAccess.IsExplicitFile() {
|
||||
configFile = configAccess.GetExplicitFile()
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
_, ok := config.Clusters[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot delete cluster %s, not in %s", name, configFile)
|
||||
}
|
||||
|
||||
delete(config.Clusters, name)
|
||||
|
||||
if err := clientcmd.ModifyConfig(configAccess, *config, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "deleted cluster %s from %s\n", name, configFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
97
pkg/cmd/config/delete_cluster_test.go
Normal file
97
pkg/cmd/config/delete_cluster_test.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2016 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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type deleteClusterTest struct {
|
||||
config clientcmdapi.Config
|
||||
clusterToDelete string
|
||||
expectedClusters []string
|
||||
expectedOut string
|
||||
}
|
||||
|
||||
func TestDeleteCluster(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.0.99"},
|
||||
"otherkube": {Server: "https://192.168.0.100"},
|
||||
},
|
||||
}
|
||||
test := deleteClusterTest{
|
||||
config: conf,
|
||||
clusterToDelete: "minikube",
|
||||
expectedClusters: []string{"otherkube"},
|
||||
expectedOut: "deleted cluster minikube from %s\n",
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test deleteClusterTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigDeleteCluster(buf, pathOptions)
|
||||
cmd.SetArgs([]string{test.clusterToDelete})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v", err)
|
||||
}
|
||||
|
||||
expectedOutWithFile := fmt.Sprintf(test.expectedOut, fakeKubeFile.Name())
|
||||
if expectedOutWithFile != buf.String() {
|
||||
t.Errorf("expected output %s, but got %s", expectedOutWithFile, buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Verify cluster was removed from kubeconfig file
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
|
||||
clusters := make([]string, 0, len(config.Clusters))
|
||||
for k := range config.Clusters {
|
||||
clusters = append(clusters, k)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.expectedClusters, clusters) {
|
||||
t.Errorf("expected clusters %v, but found %v in kubeconfig", test.expectedClusters, clusters)
|
||||
}
|
||||
}
|
||||
88
pkg/cmd/config/delete_context.go
Normal file
88
pkg/cmd/config/delete_context.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2016 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
deleteContextExample = templates.Examples(`
|
||||
# Delete the context for the minikube cluster
|
||||
kubectl config delete-context minikube`)
|
||||
)
|
||||
|
||||
// NewCmdConfigDeleteContext returns a Command instance for 'config delete-context' sub command
|
||||
func NewCmdConfigDeleteContext(out, errOut io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete-context NAME",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Delete the specified context from the kubeconfig"),
|
||||
Long: "Delete the specified context from the kubeconfig",
|
||||
Example: deleteContextExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(runDeleteContext(out, errOut, configAccess, cmd))
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runDeleteContext(out, errOut io.Writer, configAccess clientcmd.ConfigAccess, cmd *cobra.Command) error {
|
||||
config, err := configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := cmd.Flags().Args()
|
||||
if len(args) != 1 {
|
||||
cmd.Help()
|
||||
return nil
|
||||
}
|
||||
|
||||
configFile := configAccess.GetDefaultFilename()
|
||||
if configAccess.IsExplicitFile() {
|
||||
configFile = configAccess.GetExplicitFile()
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
_, ok := config.Contexts[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot delete context %s, not in %s", name, configFile)
|
||||
}
|
||||
|
||||
if config.CurrentContext == name {
|
||||
fmt.Fprint(errOut, "warning: this removed your active context, use \"kubectl config use-context\" to select a different one\n")
|
||||
}
|
||||
|
||||
delete(config.Contexts, name)
|
||||
|
||||
if err := clientcmd.ModifyConfig(configAccess, *config, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "deleted context %s from %s\n", name, configFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
98
pkg/cmd/config/delete_context_test.go
Normal file
98
pkg/cmd/config/delete_context_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
Copyright 2016 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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type deleteContextTest struct {
|
||||
config clientcmdapi.Config
|
||||
contextToDelete string
|
||||
expectedContexts []string
|
||||
expectedOut string
|
||||
}
|
||||
|
||||
func TestDeleteContext(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"minikube": {Cluster: "minikube"},
|
||||
"otherkube": {Cluster: "otherkube"},
|
||||
},
|
||||
}
|
||||
test := deleteContextTest{
|
||||
config: conf,
|
||||
contextToDelete: "minikube",
|
||||
expectedContexts: []string{"otherkube"},
|
||||
expectedOut: "deleted context minikube from %s\n",
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test deleteContextTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
errBuf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigDeleteContext(buf, errBuf, pathOptions)
|
||||
cmd.SetArgs([]string{test.contextToDelete})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v", err)
|
||||
}
|
||||
|
||||
expectedOutWithFile := fmt.Sprintf(test.expectedOut, fakeKubeFile.Name())
|
||||
if expectedOutWithFile != buf.String() {
|
||||
t.Errorf("expected output %s, but got %s", expectedOutWithFile, buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Verify context was removed from kubeconfig file
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
|
||||
contexts := make([]string, 0, len(config.Contexts))
|
||||
for k := range config.Contexts {
|
||||
contexts = append(contexts, k)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.expectedContexts, contexts) {
|
||||
t.Errorf("expected contexts %v, but found %v in kubeconfig", test.expectedContexts, contexts)
|
||||
}
|
||||
}
|
||||
64
pkg/cmd/config/get_clusters.go
Normal file
64
pkg/cmd/config/get_clusters.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
Copyright 2016 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
getClustersExample = templates.Examples(`
|
||||
# List the clusters kubectl knows about
|
||||
kubectl config get-clusters`)
|
||||
)
|
||||
|
||||
// NewCmdConfigGetClusters creates a command object for the "get-clusters" action, which
|
||||
// lists all clusters defined in the kubeconfig.
|
||||
func NewCmdConfigGetClusters(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "get-clusters",
|
||||
Short: i18n.T("Display clusters defined in the kubeconfig"),
|
||||
Long: "Display clusters defined in the kubeconfig.",
|
||||
Example: getClustersExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(runGetClusters(out, configAccess))
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGetClusters(out io.Writer, configAccess clientcmd.ConfigAccess) error {
|
||||
config, err := configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "NAME\n")
|
||||
for name := range config.Clusters {
|
||||
fmt.Fprintf(out, "%s\n", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
84
pkg/cmd/config/get_clusters_test.go
Normal file
84
pkg/cmd/config/get_clusters_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type getClustersTest struct {
|
||||
config clientcmdapi.Config
|
||||
expected string
|
||||
}
|
||||
|
||||
func TestGetClusters(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.0.99"},
|
||||
},
|
||||
}
|
||||
test := getClustersTest{
|
||||
config: conf,
|
||||
expected: `NAME
|
||||
minikube
|
||||
`,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestGetClustersEmpty(t *testing.T) {
|
||||
test := getClustersTest{
|
||||
config: clientcmdapi.Config{},
|
||||
expected: "NAME\n",
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test getClustersTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigGetClusters(buf, pathOptions)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v", err)
|
||||
}
|
||||
if len(test.expected) != 0 {
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("expected %v, but got %v", test.expected, buf.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
180
pkg/cmd/config/get_contexts.go
Normal file
180
pkg/cmd/config/get_contexts.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
Copyright 2016 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/liggitt/tabwriter"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/printers"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// GetContextsOptions contains the assignable options from the args.
|
||||
type GetContextsOptions struct {
|
||||
configAccess clientcmd.ConfigAccess
|
||||
nameOnly bool
|
||||
showHeaders bool
|
||||
contextNames []string
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
getContextsLong = templates.LongDesc(`Displays one or many contexts from the kubeconfig file.`)
|
||||
|
||||
getContextsExample = templates.Examples(`
|
||||
# List all the contexts in your kubeconfig file
|
||||
kubectl config get-contexts
|
||||
|
||||
# Describe one context in your kubeconfig file.
|
||||
kubectl config get-contexts my-context`)
|
||||
)
|
||||
|
||||
// NewCmdConfigGetContexts creates a command object for the "get-contexts" action, which
|
||||
// retrieves one or more contexts from a kubeconfig.
|
||||
func NewCmdConfigGetContexts(streams genericclioptions.IOStreams, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &GetContextsOptions{
|
||||
configAccess: configAccess,
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "get-contexts [(-o|--output=)name)]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Describe one or many contexts"),
|
||||
Long: getContextsLong,
|
||||
Example: getContextsExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
validOutputTypes := sets.NewString("", "json", "yaml", "wide", "name", "custom-columns", "custom-columns-file", "go-template", "go-template-file", "jsonpath", "jsonpath-file")
|
||||
supportedOutputTypes := sets.NewString("", "name")
|
||||
outputFormat := cmdutil.GetFlagString(cmd, "output")
|
||||
if !validOutputTypes.Has(outputFormat) {
|
||||
cmdutil.CheckErr(fmt.Errorf("output must be one of '' or 'name': %v", outputFormat))
|
||||
}
|
||||
if !supportedOutputTypes.Has(outputFormat) {
|
||||
fmt.Fprintf(options.Out, "--output %v is not available in kubectl config get-contexts; resetting to default output format\n", outputFormat)
|
||||
cmd.Flags().Set("output", "")
|
||||
}
|
||||
cmdutil.CheckErr(options.Complete(cmd, args))
|
||||
cmdutil.CheckErr(options.RunGetContexts())
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("no-headers", false, "When using the default or custom-column output format, don't print headers (default print headers).")
|
||||
cmd.Flags().StringP("output", "o", "", "Output format. One of: name")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete assigns GetContextsOptions from the args.
|
||||
func (o *GetContextsOptions) Complete(cmd *cobra.Command, args []string) error {
|
||||
o.contextNames = args
|
||||
o.nameOnly = false
|
||||
if cmdutil.GetFlagString(cmd, "output") == "name" {
|
||||
o.nameOnly = true
|
||||
}
|
||||
o.showHeaders = true
|
||||
if cmdutil.GetFlagBool(cmd, "no-headers") || o.nameOnly {
|
||||
o.showHeaders = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunGetContexts implements all the necessary functionality for context retrieval.
|
||||
func (o GetContextsOptions) RunGetContexts() error {
|
||||
config, err := o.configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, found := o.Out.(*tabwriter.Writer)
|
||||
if !found {
|
||||
out = printers.GetNewTabWriter(o.Out)
|
||||
defer out.Flush()
|
||||
}
|
||||
|
||||
// Build a list of context names to print, and warn if any requested contexts are not found.
|
||||
// Do this before printing the headers so it doesn't look ugly.
|
||||
allErrs := []error{}
|
||||
toPrint := []string{}
|
||||
if len(o.contextNames) == 0 {
|
||||
for name := range config.Contexts {
|
||||
toPrint = append(toPrint, name)
|
||||
}
|
||||
} else {
|
||||
for _, name := range o.contextNames {
|
||||
_, ok := config.Contexts[name]
|
||||
if ok {
|
||||
toPrint = append(toPrint, name)
|
||||
} else {
|
||||
allErrs = append(allErrs, fmt.Errorf("context %v not found", name))
|
||||
}
|
||||
}
|
||||
}
|
||||
if o.showHeaders {
|
||||
err = printContextHeaders(out, o.nameOnly)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(toPrint)
|
||||
for _, name := range toPrint {
|
||||
err = printContext(name, config.Contexts[name], out, o.nameOnly, config.CurrentContext == name)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return utilerrors.NewAggregate(allErrs)
|
||||
}
|
||||
|
||||
func printContextHeaders(out io.Writer, nameOnly bool) error {
|
||||
columnNames := []string{"CURRENT", "NAME", "CLUSTER", "AUTHINFO", "NAMESPACE"}
|
||||
if nameOnly {
|
||||
columnNames = columnNames[:1]
|
||||
}
|
||||
_, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t"))
|
||||
return err
|
||||
}
|
||||
|
||||
func printContext(name string, context *clientcmdapi.Context, w io.Writer, nameOnly, current bool) error {
|
||||
if nameOnly {
|
||||
_, err := fmt.Fprintf(w, "%s\n", name)
|
||||
return err
|
||||
}
|
||||
prefix := " "
|
||||
if current {
|
||||
prefix = "*"
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", prefix, name, context.Cluster, context.AuthInfo, context.Namespace)
|
||||
return err
|
||||
}
|
||||
178
pkg/cmd/config/get_contexts_test.go
Normal file
178
pkg/cmd/config/get_contexts_test.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type getContextsTest struct {
|
||||
startingConfig clientcmdapi.Config
|
||||
names []string
|
||||
noHeader bool
|
||||
nameOnly bool
|
||||
expectedOut string
|
||||
}
|
||||
|
||||
func TestGetContextsAll(t *testing.T) {
|
||||
tconf := clientcmdapi.Config{
|
||||
CurrentContext: "shaker-context",
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}
|
||||
test := getContextsTest{
|
||||
startingConfig: tconf,
|
||||
names: []string{},
|
||||
noHeader: false,
|
||||
nameOnly: false,
|
||||
expectedOut: `CURRENT NAME CLUSTER AUTHINFO NAMESPACE
|
||||
* shaker-context big-cluster blue-user saw-ns
|
||||
`,
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestGetContextsAllNoHeader(t *testing.T) {
|
||||
tconf := clientcmdapi.Config{
|
||||
CurrentContext: "shaker-context",
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}
|
||||
test := getContextsTest{
|
||||
startingConfig: tconf,
|
||||
names: []string{},
|
||||
noHeader: true,
|
||||
nameOnly: false,
|
||||
expectedOut: "* shaker-context big-cluster blue-user saw-ns\n",
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestGetContextsAllSorted(t *testing.T) {
|
||||
tconf := clientcmdapi.Config{
|
||||
CurrentContext: "shaker-context",
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"},
|
||||
"abc": {AuthInfo: "blue-user", Cluster: "abc-cluster", Namespace: "kube-system"},
|
||||
"xyz": {AuthInfo: "blue-user", Cluster: "xyz-cluster", Namespace: "default"}}}
|
||||
test := getContextsTest{
|
||||
startingConfig: tconf,
|
||||
names: []string{},
|
||||
noHeader: false,
|
||||
nameOnly: false,
|
||||
expectedOut: `CURRENT NAME CLUSTER AUTHINFO NAMESPACE
|
||||
abc abc-cluster blue-user kube-system
|
||||
* shaker-context big-cluster blue-user saw-ns
|
||||
xyz xyz-cluster blue-user default
|
||||
`,
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestGetContextsAllName(t *testing.T) {
|
||||
tconf := clientcmdapi.Config{
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}
|
||||
test := getContextsTest{
|
||||
startingConfig: tconf,
|
||||
names: []string{},
|
||||
noHeader: false,
|
||||
nameOnly: true,
|
||||
expectedOut: "shaker-context\n",
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestGetContextsAllNameNoHeader(t *testing.T) {
|
||||
tconf := clientcmdapi.Config{
|
||||
CurrentContext: "shaker-context",
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}
|
||||
test := getContextsTest{
|
||||
startingConfig: tconf,
|
||||
names: []string{},
|
||||
noHeader: true,
|
||||
nameOnly: true,
|
||||
expectedOut: "shaker-context\n",
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestGetContextsAllNone(t *testing.T) {
|
||||
test := getContextsTest{
|
||||
startingConfig: *clientcmdapi.NewConfig(),
|
||||
names: []string{},
|
||||
noHeader: true,
|
||||
nameOnly: false,
|
||||
expectedOut: "",
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestGetContextsSelectOneOfTwo(t *testing.T) {
|
||||
tconf := clientcmdapi.Config{
|
||||
CurrentContext: "shaker-context",
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"},
|
||||
"not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}
|
||||
test := getContextsTest{
|
||||
startingConfig: tconf,
|
||||
names: []string{"shaker-context"},
|
||||
noHeader: true,
|
||||
nameOnly: true,
|
||||
expectedOut: "shaker-context\n",
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test getContextsTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.startingConfig, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
options := GetContextsOptions{
|
||||
configAccess: pathOptions,
|
||||
}
|
||||
cmd := NewCmdConfigGetContexts(streams, options.configAccess)
|
||||
if test.nameOnly {
|
||||
cmd.Flags().Set("output", "name")
|
||||
}
|
||||
if test.noHeader {
|
||||
cmd.Flags().Set("no-headers", "true")
|
||||
}
|
||||
cmd.Run(cmd, test.names)
|
||||
if len(test.expectedOut) != 0 {
|
||||
if buf.String() != test.expectedOut {
|
||||
t.Errorf("Expected\n%s\ngot\n%s", test.expectedOut, buf.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
154
pkg/cmd/config/navigation_step_parser.go
Normal file
154
pkg/cmd/config/navigation_step_parser.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type navigationSteps struct {
|
||||
steps []navigationStep
|
||||
currentStepIndex int
|
||||
}
|
||||
|
||||
type navigationStep struct {
|
||||
stepValue string
|
||||
stepType reflect.Type
|
||||
}
|
||||
|
||||
func newNavigationSteps(path string) (*navigationSteps, error) {
|
||||
steps := []navigationStep{}
|
||||
individualParts := strings.Split(path, ".")
|
||||
|
||||
currType := reflect.TypeOf(clientcmdapi.Config{})
|
||||
currPartIndex := 0
|
||||
for currPartIndex < len(individualParts) {
|
||||
switch currType.Kind() {
|
||||
case reflect.Map:
|
||||
// if we're in a map, we need to locate a name. That name may contain dots, so we need to know what tokens are legal for the map's value type
|
||||
// for example, we could have a set request like: `set clusters.10.10.12.56.insecure-skip-tls-verify true`. We enter this case with
|
||||
// steps representing 10, 10, 12, 56, insecure-skip-tls-verify. The name is "10.10.12.56", so we want to collect all those parts together and
|
||||
// store them as a single step. In order to do that, we need to determine what set of tokens is a legal step AFTER the name of the map key
|
||||
// This set of reflective code pulls the type of the map values, uses that type to look up the set of legal tags. Those legal tags are used to
|
||||
// walk the list of remaining parts until we find a match to a legal tag or the end of the string. That name is used to burn all the used parts.
|
||||
mapValueType := currType.Elem().Elem()
|
||||
mapValueOptions, err := getPotentialTypeValues(mapValueType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nextPart := findNameStep(individualParts[currPartIndex:], sets.StringKeySet(mapValueOptions))
|
||||
|
||||
steps = append(steps, navigationStep{nextPart, mapValueType})
|
||||
currPartIndex += len(strings.Split(nextPart, "."))
|
||||
currType = mapValueType
|
||||
|
||||
case reflect.Struct:
|
||||
nextPart := individualParts[currPartIndex]
|
||||
|
||||
options, err := getPotentialTypeValues(currType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fieldType, exists := options[nextPart]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unable to parse %v after %v at %v", path, steps, currType)
|
||||
}
|
||||
|
||||
steps = append(steps, navigationStep{nextPart, fieldType})
|
||||
currPartIndex += len(strings.Split(nextPart, "."))
|
||||
currType = fieldType
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to parse one or more field values of %v", path)
|
||||
}
|
||||
}
|
||||
|
||||
return &navigationSteps{steps, 0}, nil
|
||||
}
|
||||
|
||||
func (s *navigationSteps) pop() navigationStep {
|
||||
if s.moreStepsRemaining() {
|
||||
s.currentStepIndex++
|
||||
return s.steps[s.currentStepIndex-1]
|
||||
}
|
||||
return navigationStep{}
|
||||
}
|
||||
|
||||
func (s *navigationSteps) peek() navigationStep {
|
||||
if s.moreStepsRemaining() {
|
||||
return s.steps[s.currentStepIndex]
|
||||
}
|
||||
return navigationStep{}
|
||||
}
|
||||
|
||||
func (s *navigationSteps) moreStepsRemaining() bool {
|
||||
return len(s.steps) > s.currentStepIndex
|
||||
}
|
||||
|
||||
// findNameStep takes the list of parts and a set of valid tags that can be used after the name. It then walks the list of parts
|
||||
// until it find a valid "next" tag or until it reaches the end of the parts and then builds the name back up out of the individual parts
|
||||
func findNameStep(parts []string, typeOptions sets.String) string {
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
numberOfPartsInStep := findKnownValue(parts[1:], typeOptions) + 1
|
||||
// if we didn't find a known value, then the entire thing must be a name
|
||||
if numberOfPartsInStep == 0 {
|
||||
numberOfPartsInStep = len(parts)
|
||||
}
|
||||
nextParts := parts[0:numberOfPartsInStep]
|
||||
|
||||
return strings.Join(nextParts, ".")
|
||||
}
|
||||
|
||||
// getPotentialTypeValues takes a type and looks up the tags used to represent its fields when serialized.
|
||||
func getPotentialTypeValues(typeValue reflect.Type) (map[string]reflect.Type, error) {
|
||||
if typeValue.Kind() == reflect.Ptr {
|
||||
typeValue = typeValue.Elem()
|
||||
}
|
||||
|
||||
if typeValue.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("%v is not of type struct", typeValue)
|
||||
}
|
||||
|
||||
ret := make(map[string]reflect.Type)
|
||||
|
||||
for fieldIndex := 0; fieldIndex < typeValue.NumField(); fieldIndex++ {
|
||||
fieldType := typeValue.Field(fieldIndex)
|
||||
yamlTag := fieldType.Tag.Get("json")
|
||||
yamlTagName := strings.Split(yamlTag, ",")[0]
|
||||
|
||||
ret[yamlTagName] = fieldType.Type
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func findKnownValue(parts []string, valueOptions sets.String) int {
|
||||
for i := range parts {
|
||||
if valueOptions.Has(parts[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
108
pkg/cmd/config/navigation_step_parser_test.go
Normal file
108
pkg/cmd/config/navigation_step_parser_test.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type stepParserTest struct {
|
||||
path string
|
||||
expectedNavigationSteps navigationSteps
|
||||
expectedError string
|
||||
}
|
||||
|
||||
func TestParseWithDots(t *testing.T) {
|
||||
test := stepParserTest{
|
||||
path: "clusters.my.dot.delimited.name.server",
|
||||
expectedNavigationSteps: navigationSteps{
|
||||
steps: []navigationStep{
|
||||
{"clusters", reflect.TypeOf(make(map[string]*clientcmdapi.Cluster))},
|
||||
{"my.dot.delimited.name", reflect.TypeOf(clientcmdapi.Cluster{})},
|
||||
{"server", reflect.TypeOf("")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestParseWithDotsEndingWithName(t *testing.T) {
|
||||
test := stepParserTest{
|
||||
path: "contexts.10.12.12.12",
|
||||
expectedNavigationSteps: navigationSteps{
|
||||
steps: []navigationStep{
|
||||
{"contexts", reflect.TypeOf(make(map[string]*clientcmdapi.Context))},
|
||||
{"10.12.12.12", reflect.TypeOf(clientcmdapi.Context{})},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestParseWithBadValue(t *testing.T) {
|
||||
test := stepParserTest{
|
||||
path: "user.bad",
|
||||
expectedNavigationSteps: navigationSteps{
|
||||
steps: []navigationStep{},
|
||||
},
|
||||
expectedError: "unable to parse user.bad after [] at api.Config",
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestParseWithNoMatchingValue(t *testing.T) {
|
||||
test := stepParserTest{
|
||||
path: "users.jheiss.exec.command",
|
||||
expectedNavigationSteps: navigationSteps{
|
||||
steps: []navigationStep{},
|
||||
},
|
||||
expectedError: "unable to parse one or more field values of users.jheiss.exec",
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test stepParserTest) run(t *testing.T) {
|
||||
actualSteps, err := newNavigationSteps(test.path)
|
||||
if len(test.expectedError) != 0 {
|
||||
if err == nil {
|
||||
t.Errorf("Did not get %v", test.expectedError)
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), test.expectedError) {
|
||||
t.Errorf("Expected %v, but got %v", test.expectedError, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.expectedNavigationSteps, *actualSteps) {
|
||||
t.Errorf("diff: %v", diff.ObjectDiff(test.expectedNavigationSteps, *actualSteps))
|
||||
t.Errorf("expected: %#v\n actual: %#v", test.expectedNavigationSteps, *actualSteps)
|
||||
}
|
||||
}
|
||||
132
pkg/cmd/config/rename_context.go
Normal file
132
pkg/cmd/config/rename_context.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// RenameContextOptions contains the options for running the rename-context cli command.
|
||||
type RenameContextOptions struct {
|
||||
configAccess clientcmd.ConfigAccess
|
||||
contextName string
|
||||
newName string
|
||||
}
|
||||
|
||||
const (
|
||||
renameContextUse = "rename-context CONTEXT_NAME NEW_NAME"
|
||||
|
||||
renameContextShort = "Renames a context from the kubeconfig file."
|
||||
)
|
||||
|
||||
var (
|
||||
renameContextLong = templates.LongDesc(`
|
||||
Renames a context from the kubeconfig file.
|
||||
|
||||
CONTEXT_NAME is the context name that you wish to change.
|
||||
|
||||
NEW_NAME is the new name you wish to set.
|
||||
|
||||
Note: In case the context being renamed is the 'current-context', this field will also be updated.`)
|
||||
|
||||
renameContextExample = templates.Examples(`
|
||||
# Rename the context 'old-name' to 'new-name' in your kubeconfig file
|
||||
kubectl config rename-context old-name new-name`)
|
||||
)
|
||||
|
||||
// NewCmdConfigRenameContext creates a command object for the "rename-context" action
|
||||
func NewCmdConfigRenameContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &RenameContextOptions{configAccess: configAccess}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: renameContextUse,
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: renameContextShort,
|
||||
Long: renameContextLong,
|
||||
Example: renameContextExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(cmd, args, out))
|
||||
cmdutil.CheckErr(options.Validate())
|
||||
cmdutil.CheckErr(options.RunRenameContext(out))
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete assigns RenameContextOptions from the args.
|
||||
func (o *RenameContextOptions) Complete(cmd *cobra.Command, args []string, out io.Writer) error {
|
||||
if len(args) != 2 {
|
||||
return helpErrorf(cmd, "Unexpected args: %v", args)
|
||||
}
|
||||
|
||||
o.contextName = args[0]
|
||||
o.newName = args[1]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure that provided values for command-line options are valid
|
||||
func (o RenameContextOptions) Validate() error {
|
||||
if len(o.newName) == 0 {
|
||||
return errors.New("You must specify a new non-empty context name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunRenameContext performs the execution for 'config rename-context' sub command
|
||||
func (o RenameContextOptions) RunRenameContext(out io.Writer) error {
|
||||
config, err := o.configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFile := o.configAccess.GetDefaultFilename()
|
||||
if o.configAccess.IsExplicitFile() {
|
||||
configFile = o.configAccess.GetExplicitFile()
|
||||
}
|
||||
|
||||
context, exists := config.Contexts[o.contextName]
|
||||
if !exists {
|
||||
return fmt.Errorf("cannot rename the context %q, it's not in %s", o.contextName, configFile)
|
||||
}
|
||||
|
||||
_, newExists := config.Contexts[o.newName]
|
||||
if newExists {
|
||||
return fmt.Errorf("cannot rename the context %q, the context %q already exists in %s", o.contextName, o.newName, configFile)
|
||||
}
|
||||
|
||||
config.Contexts[o.newName] = context
|
||||
delete(config.Contexts, o.contextName)
|
||||
|
||||
if config.CurrentContext == o.contextName {
|
||||
config.CurrentContext = o.newName
|
||||
}
|
||||
|
||||
if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Context %q renamed to %q.\n", o.contextName, o.newName)
|
||||
return nil
|
||||
}
|
||||
156
pkg/cmd/config/rename_context_test.go
Normal file
156
pkg/cmd/config/rename_context_test.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
const (
|
||||
currentContext = "current-context"
|
||||
newContext = "new-context"
|
||||
nonexistentCurrentContext = "nonexistent-current-context"
|
||||
existentNewContext = "existent-new-context"
|
||||
)
|
||||
|
||||
var (
|
||||
contextData = clientcmdapi.NewContext()
|
||||
)
|
||||
|
||||
type renameContextTest struct {
|
||||
description string
|
||||
initialConfig clientcmdapi.Config // initial config
|
||||
expectedConfig clientcmdapi.Config // expected config
|
||||
args []string // kubectl rename-context args
|
||||
expectedOut string // expected out message
|
||||
expectedErr string // expected error message
|
||||
}
|
||||
|
||||
func TestRenameContext(t *testing.T) {
|
||||
initialConfig := clientcmdapi.Config{
|
||||
CurrentContext: currentContext,
|
||||
Contexts: map[string]*clientcmdapi.Context{currentContext: contextData}}
|
||||
|
||||
expectedConfig := clientcmdapi.Config{
|
||||
CurrentContext: newContext,
|
||||
Contexts: map[string]*clientcmdapi.Context{newContext: contextData}}
|
||||
|
||||
test := renameContextTest{
|
||||
description: "Testing for kubectl config rename-context whose context to be renamed is the CurrentContext",
|
||||
initialConfig: initialConfig,
|
||||
expectedConfig: expectedConfig,
|
||||
args: []string{currentContext, newContext},
|
||||
expectedOut: fmt.Sprintf("Context %q renamed to %q.\n", currentContext, newContext),
|
||||
expectedErr: "",
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestRenameNonexistentContext(t *testing.T) {
|
||||
initialConfig := clientcmdapi.Config{
|
||||
CurrentContext: currentContext,
|
||||
Contexts: map[string]*clientcmdapi.Context{currentContext: contextData}}
|
||||
|
||||
test := renameContextTest{
|
||||
description: "Testing for kubectl config rename-context whose context to be renamed no exists",
|
||||
initialConfig: initialConfig,
|
||||
expectedConfig: initialConfig,
|
||||
args: []string{nonexistentCurrentContext, newContext},
|
||||
expectedOut: "",
|
||||
expectedErr: fmt.Sprintf("cannot rename the context %q, it's not in", nonexistentCurrentContext),
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestRenameToAlreadyExistingContext(t *testing.T) {
|
||||
initialConfig := clientcmdapi.Config{
|
||||
CurrentContext: currentContext,
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
currentContext: contextData,
|
||||
existentNewContext: contextData}}
|
||||
|
||||
test := renameContextTest{
|
||||
description: "Testing for kubectl config rename-context whose the new name is already in another context.",
|
||||
initialConfig: initialConfig,
|
||||
expectedConfig: initialConfig,
|
||||
args: []string{currentContext, existentNewContext},
|
||||
expectedOut: "",
|
||||
expectedErr: fmt.Sprintf("cannot rename the context %q, the context %q already exists", currentContext, existentNewContext),
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test renameContextTest) run(t *testing.T) {
|
||||
fakeKubeFile, _ := ioutil.TempFile("", "")
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err := clientcmd.WriteToFile(test.initialConfig, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
options := RenameContextOptions{
|
||||
configAccess: pathOptions,
|
||||
contextName: test.args[0],
|
||||
newName: test.args[1],
|
||||
}
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigRenameContext(buf, options.configAccess)
|
||||
|
||||
options.Complete(cmd, test.args, buf)
|
||||
options.Validate()
|
||||
err = options.RunRenameContext(buf)
|
||||
|
||||
if len(test.expectedErr) != 0 {
|
||||
if err == nil {
|
||||
t.Errorf("Did not get %v", test.expectedErr)
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), test.expectedErr) {
|
||||
t.Errorf("Expected error %v, but got %v", test.expectedErr, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
|
||||
_, oldExists := config.Contexts[currentContext]
|
||||
_, newExists := config.Contexts[newContext]
|
||||
|
||||
if (!newExists) || (oldExists) || (config.CurrentContext != newContext) {
|
||||
t.Errorf("Failed in: %q\n expected %v\n but got %v", test.description, test.expectedConfig, *config)
|
||||
}
|
||||
|
||||
if len(test.expectedOut) != 0 {
|
||||
if buf.String() != test.expectedOut {
|
||||
t.Errorf("Failed in:%q\n expected out %v\n but got %v", test.description, test.expectedOut, buf.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
262
pkg/cmd/config/set.go
Normal file
262
pkg/cmd/config/set.go
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
type setOptions struct {
|
||||
configAccess clientcmd.ConfigAccess
|
||||
propertyName string
|
||||
propertyValue string
|
||||
setRawBytes cliflag.Tristate
|
||||
}
|
||||
|
||||
var (
|
||||
setLong = templates.LongDesc(`
|
||||
Sets an individual value in a kubeconfig file
|
||||
|
||||
PROPERTY_NAME is a dot delimited name where each token represents either an attribute name or a map key. Map keys may not contain dots.
|
||||
|
||||
PROPERTY_VALUE is the new value you wish to set. Binary fields such as 'certificate-authority-data' expect a base64 encoded string unless the --set-raw-bytes flag is used.
|
||||
|
||||
Specifying a attribute name that already exists will merge new fields on top of existing values.`)
|
||||
|
||||
setExample = templates.Examples(`
|
||||
# Set server field on the my-cluster cluster to https://1.2.3.4
|
||||
kubectl config set clusters.my-cluster.server https://1.2.3.4
|
||||
|
||||
# Set certificate-authority-data field on the my-cluster cluster.
|
||||
kubectl config set clusters.my-cluster.certificate-authority-data $(echo "cert_data_here" | base64 -i -)
|
||||
|
||||
# Set cluster field in the my-context context to my-cluster.
|
||||
kubectl config set contexts.my-context.cluster my-cluster
|
||||
|
||||
# Set client-key-data field in the cluster-admin user using --set-raw-bytes option.
|
||||
kubectl config set users.cluster-admin.client-key-data cert_data_here --set-raw-bytes=true`)
|
||||
)
|
||||
|
||||
// NewCmdConfigSet returns a Command instance for 'config set' sub command
|
||||
func NewCmdConfigSet(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &setOptions{configAccess: configAccess}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "set PROPERTY_NAME PROPERTY_VALUE",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Sets an individual value in a kubeconfig file"),
|
||||
Long: setLong,
|
||||
Example: setExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.complete(cmd))
|
||||
cmdutil.CheckErr(options.run())
|
||||
fmt.Fprintf(out, "Property %q set.\n", options.propertyName)
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags().VarPF(&options.setRawBytes, "set-raw-bytes", "", "When writing a []byte PROPERTY_VALUE, write the given string directly without base64 decoding.")
|
||||
f.NoOptDefVal = "true"
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o setOptions) run() error {
|
||||
err := o.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := o.configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
steps, err := newNavigationSteps(o.propertyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
setRawBytes := false
|
||||
if o.setRawBytes.Provided() {
|
||||
setRawBytes = o.setRawBytes.Value()
|
||||
}
|
||||
|
||||
err = modifyConfig(reflect.ValueOf(config), steps, o.propertyValue, false, setRawBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := clientcmd.ModifyConfig(o.configAccess, *config, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *setOptions) complete(cmd *cobra.Command) error {
|
||||
endingArgs := cmd.Flags().Args()
|
||||
if len(endingArgs) != 2 {
|
||||
return helpErrorf(cmd, "Unexpected args: %v", endingArgs)
|
||||
}
|
||||
|
||||
o.propertyValue = endingArgs[1]
|
||||
o.propertyName = endingArgs[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o setOptions) validate() error {
|
||||
if len(o.propertyValue) == 0 {
|
||||
return errors.New("you cannot use set to unset a property")
|
||||
}
|
||||
|
||||
if len(o.propertyName) == 0 {
|
||||
return errors.New("you must specify a property")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func modifyConfig(curr reflect.Value, steps *navigationSteps, propertyValue string, unset bool, setRawBytes bool) error {
|
||||
currStep := steps.pop()
|
||||
|
||||
actualCurrValue := curr
|
||||
if curr.Kind() == reflect.Ptr {
|
||||
actualCurrValue = curr.Elem()
|
||||
}
|
||||
|
||||
switch actualCurrValue.Kind() {
|
||||
case reflect.Map:
|
||||
if !steps.moreStepsRemaining() && !unset {
|
||||
return fmt.Errorf("can't set a map to a value: %v", actualCurrValue)
|
||||
}
|
||||
|
||||
mapKey := reflect.ValueOf(currStep.stepValue)
|
||||
mapValueType := curr.Type().Elem().Elem()
|
||||
|
||||
if !steps.moreStepsRemaining() && unset {
|
||||
actualCurrValue.SetMapIndex(mapKey, reflect.Value{})
|
||||
return nil
|
||||
}
|
||||
|
||||
currMapValue := actualCurrValue.MapIndex(mapKey)
|
||||
|
||||
needToSetNewMapValue := currMapValue.Kind() == reflect.Invalid
|
||||
if needToSetNewMapValue {
|
||||
if unset {
|
||||
return fmt.Errorf("current map key `%v` is invalid", mapKey.Interface())
|
||||
}
|
||||
currMapValue = reflect.New(mapValueType.Elem()).Elem().Addr()
|
||||
actualCurrValue.SetMapIndex(mapKey, currMapValue)
|
||||
}
|
||||
|
||||
err := modifyConfig(currMapValue, steps, propertyValue, unset, setRawBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case reflect.String:
|
||||
if steps.moreStepsRemaining() {
|
||||
return fmt.Errorf("can't have more steps after a string. %v", steps)
|
||||
}
|
||||
actualCurrValue.SetString(propertyValue)
|
||||
return nil
|
||||
|
||||
case reflect.Slice:
|
||||
if steps.moreStepsRemaining() {
|
||||
return fmt.Errorf("can't have more steps after bytes. %v", steps)
|
||||
}
|
||||
innerKind := actualCurrValue.Type().Elem().Kind()
|
||||
if innerKind != reflect.Uint8 {
|
||||
return fmt.Errorf("unrecognized slice type. %v", innerKind)
|
||||
}
|
||||
|
||||
if unset {
|
||||
actualCurrValue.Set(reflect.Zero(actualCurrValue.Type()))
|
||||
return nil
|
||||
}
|
||||
|
||||
if setRawBytes {
|
||||
actualCurrValue.SetBytes([]byte(propertyValue))
|
||||
} else {
|
||||
val, err := base64.StdEncoding.DecodeString(propertyValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding input value: %v", err)
|
||||
}
|
||||
actualCurrValue.SetBytes(val)
|
||||
}
|
||||
return nil
|
||||
|
||||
case reflect.Bool:
|
||||
if steps.moreStepsRemaining() {
|
||||
return fmt.Errorf("can't have more steps after a bool. %v", steps)
|
||||
}
|
||||
boolValue, err := toBool(propertyValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualCurrValue.SetBool(boolValue)
|
||||
return nil
|
||||
|
||||
case reflect.Struct:
|
||||
for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ {
|
||||
currFieldValue := actualCurrValue.Field(fieldIndex)
|
||||
currFieldType := actualCurrValue.Type().Field(fieldIndex)
|
||||
currYamlTag := currFieldType.Tag.Get("json")
|
||||
currFieldTypeYamlName := strings.Split(currYamlTag, ",")[0]
|
||||
|
||||
if currFieldTypeYamlName == currStep.stepValue {
|
||||
thisMapHasNoValue := (currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil())
|
||||
|
||||
if thisMapHasNoValue {
|
||||
newValue := reflect.MakeMap(currFieldValue.Type())
|
||||
currFieldValue.Set(newValue)
|
||||
|
||||
if !steps.moreStepsRemaining() && unset {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !steps.moreStepsRemaining() && unset {
|
||||
// if we're supposed to unset the value or if the value is a map that doesn't exist, create a new value and overwrite
|
||||
newValue := reflect.New(currFieldValue.Type()).Elem()
|
||||
currFieldValue.Set(newValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
return modifyConfig(currFieldValue.Addr(), steps, propertyValue, unset, setRawBytes)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to locate path %#v under %v", currStep, actualCurrValue)
|
||||
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("unrecognized type: %v", actualCurrValue))
|
||||
}
|
||||
88
pkg/cmd/config/set_test.go
Normal file
88
pkg/cmd/config/set_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"reflect"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type setConfigTest struct {
|
||||
description string
|
||||
config clientcmdapi.Config
|
||||
args []string
|
||||
expected string
|
||||
expectedConfig clientcmdapi.Config
|
||||
}
|
||||
|
||||
func TestSetConfigCurrentContext(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
CurrentContext: "minikube",
|
||||
}
|
||||
expectedConfig := *clientcmdapi.NewConfig()
|
||||
expectedConfig.CurrentContext = "my-cluster"
|
||||
test := setConfigTest{
|
||||
description: "Testing for kubectl config set current-context",
|
||||
config: conf,
|
||||
args: []string{"current-context", "my-cluster"},
|
||||
expected: `Property "current-context" set.` + "\n",
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test setConfigTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigSet(buf, pathOptions)
|
||||
cmd.SetArgs(test.args)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v", err)
|
||||
}
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
if len(test.expected) != 0 {
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("Failed in:%q\n expected %v\n but got %v", test.description, test.expected, buf.String())
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(*config, test.expectedConfig) {
|
||||
t.Errorf("Failed in: %q\n expected %v\n but got %v", test.description, *config, test.expectedConfig)
|
||||
}
|
||||
}
|
||||
116
pkg/cmd/config/unset.go
Normal file
116
pkg/cmd/config/unset.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
)
|
||||
|
||||
type unsetOptions struct {
|
||||
configAccess clientcmd.ConfigAccess
|
||||
propertyName string
|
||||
}
|
||||
|
||||
var (
|
||||
unsetLong = templates.LongDesc(`
|
||||
Unsets an individual value in a kubeconfig file
|
||||
|
||||
PROPERTY_NAME is a dot delimited name where each token represents either an attribute name or a map key. Map keys may not contain dots.`)
|
||||
|
||||
unsetExample = templates.Examples(`
|
||||
# Unset the current-context.
|
||||
kubectl config unset current-context
|
||||
|
||||
# Unset namespace in foo context.
|
||||
kubectl config unset contexts.foo.namespace`)
|
||||
)
|
||||
|
||||
// NewCmdConfigUnset returns a Command instance for 'config unset' sub command
|
||||
func NewCmdConfigUnset(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &unsetOptions{configAccess: configAccess}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "unset PROPERTY_NAME",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Unsets an individual value in a kubeconfig file"),
|
||||
Long: unsetLong,
|
||||
Example: unsetExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.complete(cmd, args))
|
||||
cmdutil.CheckErr(options.run(out))
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o unsetOptions) run(out io.Writer) error {
|
||||
err := o.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := o.configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
steps, err := newNavigationSteps(o.propertyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = modifyConfig(reflect.ValueOf(config), steps, "", true, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := clientcmd.ModifyConfig(o.configAccess, *config, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintf(out, "Property %q unset.\n", o.propertyName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *unsetOptions) complete(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return helpErrorf(cmd, "Unexpected args: %v", args)
|
||||
}
|
||||
|
||||
o.propertyName = args[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o unsetOptions) validate() error {
|
||||
if len(o.propertyName) == 0 {
|
||||
return errors.New("you must specify a property")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
150
pkg/cmd/config/unset_test.go
Normal file
150
pkg/cmd/config/unset_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type unsetConfigTest struct {
|
||||
description string
|
||||
config clientcmdapi.Config
|
||||
args []string
|
||||
expected string
|
||||
expectedErr string
|
||||
}
|
||||
|
||||
func TestUnsetConfigString(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.99.100:8443"},
|
||||
"my-cluster": {Server: "https://192.168.0.1:3434"},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"minikube": {AuthInfo: "minikube", Cluster: "minikube"},
|
||||
"my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"},
|
||||
},
|
||||
CurrentContext: "minikube",
|
||||
}
|
||||
test := unsetConfigTest{
|
||||
description: "Testing for kubectl config unset a value",
|
||||
config: conf,
|
||||
args: []string{"current-context"},
|
||||
expected: `Property "current-context" unset.` + "\n",
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestUnsetConfigMap(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.99.100:8443"},
|
||||
"my-cluster": {Server: "https://192.168.0.1:3434"},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"minikube": {AuthInfo: "minikube", Cluster: "minikube"},
|
||||
"my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"},
|
||||
},
|
||||
CurrentContext: "minikube",
|
||||
}
|
||||
test := unsetConfigTest{
|
||||
description: "Testing for kubectl config unset a map",
|
||||
config: conf,
|
||||
args: []string{"clusters"},
|
||||
expected: `Property "clusters" unset.` + "\n",
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestUnsetUnexistConfig(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.99.100:8443"},
|
||||
"my-cluster": {Server: "https://192.168.0.1:3434"},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"minikube": {AuthInfo: "minikube", Cluster: "minikube"},
|
||||
"my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"},
|
||||
},
|
||||
CurrentContext: "minikube",
|
||||
}
|
||||
|
||||
test := unsetConfigTest{
|
||||
description: "Testing for kubectl config unset a unexist map key",
|
||||
config: conf,
|
||||
args: []string{"contexts.foo.namespace"},
|
||||
expectedErr: "current map key `foo` is invalid",
|
||||
}
|
||||
test.run(t)
|
||||
|
||||
}
|
||||
|
||||
func (test unsetConfigTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile(os.TempDir(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigUnset(buf, pathOptions)
|
||||
opts := &unsetOptions{configAccess: pathOptions}
|
||||
err = opts.complete(cmd, test.args)
|
||||
if err == nil {
|
||||
err = opts.run(buf)
|
||||
}
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
|
||||
if err != nil && err.Error() != test.expectedErr {
|
||||
t.Fatalf("expected error:\n %v\nbut got error:\n%v", test.expectedErr, err)
|
||||
}
|
||||
if len(test.expected) != 0 {
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("Failed in :%q\n expected %v\n but got %v", test.description, test.expected, buf.String())
|
||||
}
|
||||
}
|
||||
if test.args[0] == "current-context" {
|
||||
if config.CurrentContext != "" {
|
||||
t.Errorf("Failed in :%q\n expected current-context nil,but got %v", test.description, config.CurrentContext)
|
||||
}
|
||||
} else if test.args[0] == "clusters" {
|
||||
if len(config.Clusters) != 0 {
|
||||
t.Errorf("Failed in :%q\n expected clusters nil map, but got %v", test.description, config.Clusters)
|
||||
}
|
||||
}
|
||||
}
|
||||
103
pkg/cmd/config/use_context.go
Normal file
103
pkg/cmd/config/use_context.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
useContextExample = templates.Examples(`
|
||||
# Use the context for the minikube cluster
|
||||
kubectl config use-context minikube`)
|
||||
)
|
||||
|
||||
type useContextOptions struct {
|
||||
configAccess clientcmd.ConfigAccess
|
||||
contextName string
|
||||
}
|
||||
|
||||
// NewCmdConfigUseContext returns a Command instance for 'config use-context' sub command
|
||||
func NewCmdConfigUseContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &useContextOptions{configAccess: configAccess}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "use-context CONTEXT_NAME",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Sets the current-context in a kubeconfig file"),
|
||||
Aliases: []string{"use"},
|
||||
Long: `Sets the current-context in a kubeconfig file`,
|
||||
Example: useContextExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.complete(cmd))
|
||||
cmdutil.CheckErr(options.run())
|
||||
fmt.Fprintf(out, "Switched to context %q.\n", options.contextName)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o useContextOptions) run() error {
|
||||
config, err := o.configAccess.GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = o.validate(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.CurrentContext = o.contextName
|
||||
|
||||
return clientcmd.ModifyConfig(o.configAccess, *config, true)
|
||||
}
|
||||
|
||||
func (o *useContextOptions) complete(cmd *cobra.Command) error {
|
||||
endingArgs := cmd.Flags().Args()
|
||||
if len(endingArgs) != 1 {
|
||||
return helpErrorf(cmd, "Unexpected args: %v", endingArgs)
|
||||
}
|
||||
|
||||
o.contextName = endingArgs[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o useContextOptions) validate(config *clientcmdapi.Config) error {
|
||||
if len(o.contextName) == 0 {
|
||||
return errors.New("empty context names are not allowed")
|
||||
}
|
||||
|
||||
for name := range config.Contexts {
|
||||
if name == o.contextName {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("no context exists with the name: %q", o.contextName)
|
||||
}
|
||||
104
pkg/cmd/config/use_context_test.go
Normal file
104
pkg/cmd/config/use_context_test.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type useContextTest struct {
|
||||
description string
|
||||
config clientcmdapi.Config //initiate kubectl config
|
||||
args []string //kubectl config use-context args
|
||||
expected string //expect out
|
||||
expectedConfig clientcmdapi.Config //expect kubectl config
|
||||
}
|
||||
|
||||
func TestUseContext(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.99.100:8443"},
|
||||
"my-cluster": {Server: "https://192.168.0.1:3434"},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"minikube": {AuthInfo: "minikube", Cluster: "minikube"},
|
||||
"my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"},
|
||||
},
|
||||
CurrentContext: "minikube",
|
||||
}
|
||||
test := useContextTest{
|
||||
description: "Testing for kubectl config use-context",
|
||||
config: conf,
|
||||
args: []string{"my-cluster"},
|
||||
expected: `Switched to context "my-cluster".` + "\n",
|
||||
expectedConfig: clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.99.100:8443"},
|
||||
"my-cluster": {Server: "https://192.168.0.1:3434"},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"minikube": {AuthInfo: "minikube", Cluster: "minikube"},
|
||||
"my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"},
|
||||
},
|
||||
CurrentContext: "my-cluster",
|
||||
},
|
||||
}
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func (test useContextTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile(os.TempDir(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigUseContext(buf, pathOptions)
|
||||
cmd.SetArgs(test.args)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v,kubectl config use-context args: %v", err, test.args)
|
||||
}
|
||||
config, err := clientcmd.LoadFromFile(fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error loading kubeconfig file: %v", err)
|
||||
}
|
||||
if len(test.expected) != 0 {
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("Failed in :%q\n expected %v\n, but got %v\n", test.description, test.expected, buf.String())
|
||||
}
|
||||
}
|
||||
if test.expectedConfig.CurrentContext != config.CurrentContext {
|
||||
t.Errorf("Failed in :%q\n expected config %v, but found %v\n in kubeconfig\n", test.description, test.expectedConfig, config)
|
||||
}
|
||||
}
|
||||
185
pkg/cmd/config/view.go
Normal file
185
pkg/cmd/config/view.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
Copyright 2014 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 config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
"k8s.io/client-go/tools/clientcmd/api/latest"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// ViewOptions holds the command-line options for 'config view' sub command
|
||||
type ViewOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
PrintObject printers.ResourcePrinterFunc
|
||||
|
||||
ConfigAccess clientcmd.ConfigAccess
|
||||
Merge cliflag.Tristate
|
||||
Flatten bool
|
||||
Minify bool
|
||||
RawByteData bool
|
||||
|
||||
Context string
|
||||
OutputFormat string
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
viewLong = templates.LongDesc(`
|
||||
Display merged kubeconfig settings or a specified kubeconfig file.
|
||||
|
||||
You can use --output jsonpath={...} to extract specific values using a jsonpath expression.`)
|
||||
|
||||
viewExample = templates.Examples(`
|
||||
# Show merged kubeconfig settings.
|
||||
kubectl config view
|
||||
|
||||
# Show merged kubeconfig settings and raw certificate data.
|
||||
kubectl config view --raw
|
||||
|
||||
# Get the password for the e2e user
|
||||
kubectl config view -o jsonpath='{.users[?(@.name == "e2e")].user.password}'`)
|
||||
|
||||
defaultOutputFormat = "yaml"
|
||||
)
|
||||
|
||||
// NewCmdConfigView returns a Command instance for 'config view' sub command
|
||||
func NewCmdConfigView(f cmdutil.Factory, streams genericclioptions.IOStreams, ConfigAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
o := &ViewOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme).WithDefaultOutput("yaml"),
|
||||
ConfigAccess: ConfigAccess,
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "view",
|
||||
Short: i18n.T("Display merged kubeconfig settings or a specified kubeconfig file"),
|
||||
Long: viewLong,
|
||||
Example: viewExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
o.Merge.Default(true)
|
||||
mergeFlag := cmd.Flags().VarPF(&o.Merge, "merge", "", "Merge the full hierarchy of kubeconfig files")
|
||||
mergeFlag.NoOptDefVal = "true"
|
||||
cmd.Flags().BoolVar(&o.RawByteData, "raw", o.RawByteData, "Display raw byte data")
|
||||
cmd.Flags().BoolVar(&o.Flatten, "flatten", o.Flatten, "Flatten the resulting kubeconfig file into self-contained output (useful for creating portable kubeconfig files)")
|
||||
cmd.Flags().BoolVar(&o.Minify, "minify", o.Minify, "Remove all information not used by current-context from the output")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes the required command-line options
|
||||
func (o *ViewOptions) Complete(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args)
|
||||
}
|
||||
if o.ConfigAccess.IsExplicitFile() {
|
||||
if !o.Merge.Provided() {
|
||||
o.Merge.Set("false")
|
||||
}
|
||||
}
|
||||
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObject = printer.PrintObj
|
||||
o.Context = cmdutil.GetFlagString(cmd, "context")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure that provided values for command-line options are valid
|
||||
func (o ViewOptions) Validate() error {
|
||||
if !o.Merge.Value() && !o.ConfigAccess.IsExplicitFile() {
|
||||
return errors.New("if merge==false a precise file must to specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run performs the execution of 'config view' sub command
|
||||
func (o ViewOptions) Run() error {
|
||||
config, err := o.loadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Minify {
|
||||
if len(o.Context) > 0 {
|
||||
config.CurrentContext = o.Context
|
||||
}
|
||||
if err := clientcmdapi.MinifyConfig(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if o.Flatten {
|
||||
if err := clientcmdapi.FlattenConfig(config); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !o.RawByteData {
|
||||
clientcmdapi.ShortenConfig(config)
|
||||
}
|
||||
|
||||
convertedObj, err := latest.Scheme.ConvertToVersion(config, latest.ExternalVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return o.PrintObject(convertedObj, o.Out)
|
||||
}
|
||||
|
||||
func (o ViewOptions) loadConfig() (*clientcmdapi.Config, error) {
|
||||
err := o.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := o.getStartingConfig()
|
||||
return config, err
|
||||
}
|
||||
|
||||
// getStartingConfig returns the Config object built from the sources specified by the options, the filename read (only if it was a single file), and an error if something goes wrong
|
||||
func (o *ViewOptions) getStartingConfig() (*clientcmdapi.Config, error) {
|
||||
switch {
|
||||
case !o.Merge.Value():
|
||||
return clientcmd.LoadFromFile(o.ConfigAccess.GetExplicitFile())
|
||||
|
||||
default:
|
||||
return o.ConfigAccess.GetStartingConfig()
|
||||
}
|
||||
}
|
||||
201
pkg/cmd/config/view_test.go
Normal file
201
pkg/cmd/config/view_test.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type viewClusterTest struct {
|
||||
description string
|
||||
config clientcmdapi.Config //initiate kubectl config
|
||||
flags []string //kubectl config viw flags
|
||||
expected string //expect out
|
||||
}
|
||||
|
||||
func TestViewCluster(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.99.100:8443"},
|
||||
"my-cluster": {Server: "https://192.168.0.1:3434"},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"minikube": {AuthInfo: "minikube", Cluster: "minikube"},
|
||||
"my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"},
|
||||
},
|
||||
CurrentContext: "minikube",
|
||||
AuthInfos: map[string]*clientcmdapi.AuthInfo{
|
||||
"minikube": {Token: "minikube-token"},
|
||||
"mu-cluster": {Token: "minikube-token"},
|
||||
},
|
||||
}
|
||||
|
||||
test := viewClusterTest{
|
||||
description: "Testing for kubectl config view",
|
||||
config: conf,
|
||||
expected: `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://192.168.99.100:8443
|
||||
name: minikube
|
||||
- cluster:
|
||||
server: https://192.168.0.1:3434
|
||||
name: my-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: minikube
|
||||
user: minikube
|
||||
name: minikube
|
||||
- context:
|
||||
cluster: my-cluster
|
||||
user: mu-cluster
|
||||
name: my-cluster
|
||||
current-context: minikube
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: minikube
|
||||
user:
|
||||
token: minikube-token
|
||||
- name: mu-cluster
|
||||
user:
|
||||
token: minikube-token` + "\n",
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
|
||||
}
|
||||
|
||||
func TestViewClusterMinify(t *testing.T) {
|
||||
conf := clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: "v1",
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"minikube": {Server: "https://192.168.99.100:8443"},
|
||||
"my-cluster": {Server: "https://192.168.0.1:3434"},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"minikube": {AuthInfo: "minikube", Cluster: "minikube"},
|
||||
"my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"},
|
||||
},
|
||||
CurrentContext: "minikube",
|
||||
AuthInfos: map[string]*clientcmdapi.AuthInfo{
|
||||
"minikube": {Token: "minikube-token"},
|
||||
"mu-cluster": {Token: "minikube-token"},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
description string
|
||||
config clientcmdapi.Config
|
||||
flags []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
description: "Testing for kubectl config view --minify=true",
|
||||
config: conf,
|
||||
flags: []string{"--minify=true"},
|
||||
expected: `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://192.168.99.100:8443
|
||||
name: minikube
|
||||
contexts:
|
||||
- context:
|
||||
cluster: minikube
|
||||
user: minikube
|
||||
name: minikube
|
||||
current-context: minikube
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: minikube
|
||||
user:
|
||||
token: minikube-token` + "\n",
|
||||
},
|
||||
{
|
||||
description: "Testing for kubectl config view --minify=true --context=my-cluster",
|
||||
config: conf,
|
||||
flags: []string{"--minify=true", "--context=my-cluster"},
|
||||
expected: `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://192.168.0.1:3434
|
||||
name: my-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: my-cluster
|
||||
user: mu-cluster
|
||||
name: my-cluster
|
||||
current-context: my-cluster
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: mu-cluster
|
||||
user:
|
||||
token: minikube-token` + "\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
cmdTest := viewClusterTest{
|
||||
description: test.description,
|
||||
config: test.config,
|
||||
flags: test.flags,
|
||||
expected: test.expected,
|
||||
}
|
||||
cmdTest.run(t)
|
||||
}
|
||||
}
|
||||
|
||||
func (test viewClusterTest) run(t *testing.T) {
|
||||
fakeKubeFile, err := ioutil.TempFile(os.TempDir(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Remove(fakeKubeFile.Name())
|
||||
err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
pathOptions := clientcmd.NewDefaultPathOptions()
|
||||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdConfigView(cmdutil.NewFactory(genericclioptions.NewTestConfigFlags()), streams, pathOptions)
|
||||
// "context" is a global flag, inherited from base kubectl command in the real world
|
||||
cmd.Flags().String("context", "", "The name of the kubeconfig context to use")
|
||||
cmd.Flags().Parse(test.flags)
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error executing command: %v,kubectl config view flags: %v", err, test.flags)
|
||||
}
|
||||
if len(test.expected) != 0 {
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("Failed in %q\n expected %v\n but got %v\n", test.description, test.expected, buf.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
540
pkg/cmd/cp/cp.go
Normal file
540
pkg/cmd/cp/cp.go
Normal file
|
|
@ -0,0 +1,540 @@
|
|||
/*
|
||||
Copyright 2016 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 cp
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/exec"
|
||||
|
||||
"bytes"
|
||||
|
||||
"github.com/lithammer/dedent"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
cpExample = templates.Examples(i18n.T(`
|
||||
# !!!Important Note!!!
|
||||
# Requires that the 'tar' binary is present in your container
|
||||
# image. If 'tar' is not present, 'kubectl cp' will fail.
|
||||
|
||||
# Copy /tmp/foo_dir local directory to /tmp/bar_dir in a remote pod in the default namespace
|
||||
kubectl cp /tmp/foo_dir <some-pod>:/tmp/bar_dir
|
||||
|
||||
# Copy /tmp/foo local file to /tmp/bar in a remote pod in a specific container
|
||||
kubectl cp /tmp/foo <some-pod>:/tmp/bar -c <specific-container>
|
||||
|
||||
# Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace <some-namespace>
|
||||
kubectl cp /tmp/foo <some-namespace>/<some-pod>:/tmp/bar
|
||||
|
||||
# Copy /tmp/foo from a remote pod to /tmp/bar locally
|
||||
kubectl cp <some-namespace>/<some-pod>:/tmp/foo /tmp/bar`))
|
||||
|
||||
cpUsageStr = dedent.Dedent(`
|
||||
expected 'cp <file-spec-src> <file-spec-dest> [-c container]'.
|
||||
<file-spec> is:
|
||||
[namespace/]pod-name:/file/path for a remote file
|
||||
/file/path for a local file`)
|
||||
)
|
||||
|
||||
// CopyOptions have the data required to perform the copy operation
|
||||
type CopyOptions struct {
|
||||
Container string
|
||||
Namespace string
|
||||
NoPreserve bool
|
||||
|
||||
ClientConfig *restclient.Config
|
||||
Clientset kubernetes.Interface
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewCopyOptions creates the options for copy
|
||||
func NewCopyOptions(ioStreams genericclioptions.IOStreams) *CopyOptions {
|
||||
return &CopyOptions{
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdCp creates a new Copy command.
|
||||
func NewCmdCp(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewCopyOptions(ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "cp <file-spec-src> <file-spec-dest>",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Copy files and directories to and from containers."),
|
||||
Long: "Copy files and directories to and from containers.",
|
||||
Example: cpExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd))
|
||||
cmdutil.CheckErr(o.Run(args))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&o.Container, "container", "c", o.Container, "Container name. If omitted, the first container in the pod will be chosen")
|
||||
cmd.Flags().BoolVarP(&o.NoPreserve, "no-preserve", "", false, "The copied file/directory's ownership and permissions will not be preserved in the container")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type fileSpec struct {
|
||||
PodNamespace string
|
||||
PodName string
|
||||
File string
|
||||
}
|
||||
|
||||
var (
|
||||
errFileSpecDoesntMatchFormat = errors.New("Filespec must match the canonical format: [[namespace/]pod:]file/path")
|
||||
errFileCannotBeEmpty = errors.New("Filepath can not be empty")
|
||||
)
|
||||
|
||||
func extractFileSpec(arg string) (fileSpec, error) {
|
||||
if i := strings.Index(arg, ":"); i == -1 {
|
||||
return fileSpec{File: arg}, nil
|
||||
} else if i > 0 {
|
||||
file := arg[i+1:]
|
||||
pod := arg[:i]
|
||||
pieces := strings.Split(pod, "/")
|
||||
if len(pieces) == 1 {
|
||||
return fileSpec{
|
||||
PodName: pieces[0],
|
||||
File: file,
|
||||
}, nil
|
||||
}
|
||||
if len(pieces) == 2 {
|
||||
return fileSpec{
|
||||
PodNamespace: pieces[0],
|
||||
PodName: pieces[1],
|
||||
File: file,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return fileSpec{}, errFileSpecDoesntMatchFormat
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *CopyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||
var err error
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Clientset, err = f.KubernetesClientSet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.ClientConfig, err = f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure provided values for CopyOptions are valid
|
||||
func (o *CopyOptions) Validate(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return cmdutil.UsageErrorf(cmd, cpUsageStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run performs the execution
|
||||
func (o *CopyOptions) Run(args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("source and destination are required")
|
||||
}
|
||||
srcSpec, err := extractFileSpec(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destSpec, err := extractFileSpec(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(srcSpec.PodName) != 0 && len(destSpec.PodName) != 0 {
|
||||
if _, err := os.Stat(args[0]); err == nil {
|
||||
return o.copyToPod(fileSpec{File: args[0]}, destSpec, &exec.ExecOptions{})
|
||||
}
|
||||
return fmt.Errorf("src doesn't exist in local filesystem")
|
||||
}
|
||||
|
||||
if len(srcSpec.PodName) != 0 {
|
||||
return o.copyFromPod(srcSpec, destSpec)
|
||||
}
|
||||
if len(destSpec.PodName) != 0 {
|
||||
return o.copyToPod(srcSpec, destSpec, &exec.ExecOptions{})
|
||||
}
|
||||
return fmt.Errorf("one of src or dest must be a remote file specification")
|
||||
}
|
||||
|
||||
// checkDestinationIsDir receives a destination fileSpec and
|
||||
// determines if the provided destination path exists on the
|
||||
// pod. If the destination path does not exist or is _not_ a
|
||||
// directory, an error is returned with the exit code received.
|
||||
func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error {
|
||||
options := &exec.ExecOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
IOStreams: genericclioptions.IOStreams{
|
||||
Out: bytes.NewBuffer([]byte{}),
|
||||
ErrOut: bytes.NewBuffer([]byte{}),
|
||||
},
|
||||
|
||||
Namespace: dest.PodNamespace,
|
||||
PodName: dest.PodName,
|
||||
},
|
||||
|
||||
Command: []string{"test", "-d", dest.File},
|
||||
Executor: &exec.DefaultRemoteExecutor{},
|
||||
}
|
||||
|
||||
return o.execute(options)
|
||||
}
|
||||
|
||||
func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) error {
|
||||
if len(src.File) == 0 || len(dest.File) == 0 {
|
||||
return errFileCannotBeEmpty
|
||||
}
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
// strip trailing slash (if any)
|
||||
if dest.File != "/" && strings.HasSuffix(string(dest.File[len(dest.File)-1]), "/") {
|
||||
dest.File = dest.File[:len(dest.File)-1]
|
||||
}
|
||||
|
||||
if err := o.checkDestinationIsDir(dest); err == nil {
|
||||
// If no error, dest.File was found to be a directory.
|
||||
// Copy specified src into it
|
||||
dest.File = dest.File + "/" + path.Base(src.File)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
err := makeTar(src.File, dest.File, writer)
|
||||
cmdutil.CheckErr(err)
|
||||
}()
|
||||
var cmdArr []string
|
||||
|
||||
// TODO: Improve error messages by first testing if 'tar' is present in the container?
|
||||
if o.NoPreserve {
|
||||
cmdArr = []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-"}
|
||||
} else {
|
||||
cmdArr = []string{"tar", "-xmf", "-"}
|
||||
}
|
||||
destDir := path.Dir(dest.File)
|
||||
if len(destDir) > 0 {
|
||||
cmdArr = append(cmdArr, "-C", destDir)
|
||||
}
|
||||
|
||||
options.StreamOptions = exec.StreamOptions{
|
||||
IOStreams: genericclioptions.IOStreams{
|
||||
In: reader,
|
||||
Out: o.Out,
|
||||
ErrOut: o.ErrOut,
|
||||
},
|
||||
Stdin: true,
|
||||
|
||||
Namespace: dest.PodNamespace,
|
||||
PodName: dest.PodName,
|
||||
}
|
||||
|
||||
options.Command = cmdArr
|
||||
options.Executor = &exec.DefaultRemoteExecutor{}
|
||||
return o.execute(options)
|
||||
}
|
||||
|
||||
func (o *CopyOptions) copyFromPod(src, dest fileSpec) error {
|
||||
if len(src.File) == 0 || len(dest.File) == 0 {
|
||||
return errFileCannotBeEmpty
|
||||
}
|
||||
|
||||
reader, outStream := io.Pipe()
|
||||
options := &exec.ExecOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
IOStreams: genericclioptions.IOStreams{
|
||||
In: nil,
|
||||
Out: outStream,
|
||||
ErrOut: o.Out,
|
||||
},
|
||||
|
||||
Namespace: src.PodNamespace,
|
||||
PodName: src.PodName,
|
||||
},
|
||||
|
||||
// TODO: Improve error messages by first testing if 'tar' is present in the container?
|
||||
Command: []string{"tar", "cf", "-", src.File},
|
||||
Executor: &exec.DefaultRemoteExecutor{},
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer outStream.Close()
|
||||
err := o.execute(options)
|
||||
cmdutil.CheckErr(err)
|
||||
}()
|
||||
prefix := getPrefix(src.File)
|
||||
prefix = path.Clean(prefix)
|
||||
// remove extraneous path shortcuts - these could occur if a path contained extra "../"
|
||||
// and attempted to navigate beyond "/" in a remote filesystem
|
||||
prefix = stripPathShortcuts(prefix)
|
||||
return o.untarAll(reader, dest.File, prefix)
|
||||
}
|
||||
|
||||
// stripPathShortcuts removes any leading or trailing "../" from a given path
|
||||
func stripPathShortcuts(p string) string {
|
||||
newPath := path.Clean(p)
|
||||
trimmed := strings.TrimPrefix(newPath, "../")
|
||||
|
||||
for trimmed != newPath {
|
||||
newPath = trimmed
|
||||
trimmed = strings.TrimPrefix(newPath, "../")
|
||||
}
|
||||
|
||||
// trim leftover {".", ".."}
|
||||
if newPath == "." || newPath == ".." {
|
||||
newPath = ""
|
||||
}
|
||||
|
||||
if len(newPath) > 0 && string(newPath[0]) == "/" {
|
||||
return newPath[1:]
|
||||
}
|
||||
|
||||
return newPath
|
||||
}
|
||||
|
||||
func makeTar(srcPath, destPath string, writer io.Writer) error {
|
||||
// TODO: use compression here?
|
||||
tarWriter := tar.NewWriter(writer)
|
||||
defer tarWriter.Close()
|
||||
|
||||
srcPath = path.Clean(srcPath)
|
||||
destPath = path.Clean(destPath)
|
||||
return recursiveTar(path.Dir(srcPath), path.Base(srcPath), path.Dir(destPath), path.Base(destPath), tarWriter)
|
||||
}
|
||||
|
||||
func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *tar.Writer) error {
|
||||
srcPath := path.Join(srcBase, srcFile)
|
||||
matchedPaths, err := filepath.Glob(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fpath := range matchedPaths {
|
||||
stat, err := os.Lstat(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stat.IsDir() {
|
||||
files, err := ioutil.ReadDir(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
//case empty directory
|
||||
hdr, _ := tar.FileInfoHeader(stat, fpath)
|
||||
hdr.Name = destFile
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := recursiveTar(srcBase, path.Join(srcFile, f.Name()), destBase, path.Join(destFile, f.Name()), tw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else if stat.Mode()&os.ModeSymlink != 0 {
|
||||
//case soft link
|
||||
hdr, _ := tar.FileInfoHeader(stat, fpath)
|
||||
target, err := os.Readlink(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hdr.Linkname = target
|
||||
hdr.Name = destFile
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
//case regular file or other file type like pipe
|
||||
hdr, err := tar.FileInfoHeader(stat, fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hdr.Name = destFile
|
||||
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(tw, f); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CopyOptions) untarAll(reader io.Reader, destDir, prefix string) error {
|
||||
// TODO: use compression here?
|
||||
tarReader := tar.NewReader(reader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// All the files will start with the prefix, which is the directory where
|
||||
// they were located on the pod, we need to strip down that prefix, but
|
||||
// if the prefix is missing it means the tar was tempered with.
|
||||
// For the case where prefix is empty we need to ensure that the path
|
||||
// is not absolute, which also indicates the tar file was tempered with.
|
||||
if !strings.HasPrefix(header.Name, prefix) {
|
||||
return fmt.Errorf("tar contents corrupted")
|
||||
}
|
||||
|
||||
// basic file information
|
||||
mode := header.FileInfo().Mode()
|
||||
destFileName := filepath.Join(destDir, header.Name[len(prefix):])
|
||||
|
||||
if !isDestRelative(destDir, destFileName) {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is outside target destination, skipping\n", destFileName)
|
||||
continue
|
||||
}
|
||||
|
||||
baseName := filepath.Dir(destFileName)
|
||||
if err := os.MkdirAll(baseName, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if header.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(destFileName, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// We need to ensure that the destination file is always within boundries
|
||||
// of the destination directory. This prevents any kind of path traversal
|
||||
// from within tar archive.
|
||||
evaledPath, err := filepath.EvalSymlinks(baseName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// For scrutiny we verify both the actual destination as well as we follow
|
||||
// all the links that might lead outside of the destination directory.
|
||||
if !isDestRelative(destDir, filepath.Join(evaledPath, filepath.Base(destFileName))) {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is outside target destination, skipping\n", destFileName)
|
||||
continue
|
||||
}
|
||||
|
||||
if mode&os.ModeSymlink != 0 {
|
||||
linkname := header.Linkname
|
||||
// We need to ensure that the link destination is always within boundries
|
||||
// of the destination directory. This prevents any kind of path traversal
|
||||
// from within tar archive.
|
||||
linkTarget := linkname
|
||||
if !filepath.IsAbs(linkname) {
|
||||
linkTarget = filepath.Join(evaledPath, linkname)
|
||||
}
|
||||
if !isDestRelative(destDir, linkTarget) {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "warning: link %q is pointing to %q which is outside target destination, skipping\n", destFileName, header.Linkname)
|
||||
continue
|
||||
}
|
||||
if err := os.Symlink(linkname, destFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
outFile, err := os.Create(destFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := outFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDestRelative returns true if dest is pointing outside the base directory,
|
||||
// false otherwise.
|
||||
func isDestRelative(base, dest string) bool {
|
||||
relative, err := filepath.Rel(base, dest)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return relative == "." || relative == stripPathShortcuts(relative)
|
||||
}
|
||||
|
||||
func getPrefix(file string) string {
|
||||
// tar strips the leading '/' if it's there, so we will too
|
||||
return strings.TrimLeft(file, "/")
|
||||
}
|
||||
|
||||
func (o *CopyOptions) execute(options *exec.ExecOptions) error {
|
||||
if len(options.Namespace) == 0 {
|
||||
options.Namespace = o.Namespace
|
||||
}
|
||||
|
||||
if len(o.Container) > 0 {
|
||||
options.ContainerName = o.Container
|
||||
}
|
||||
|
||||
options.Config = o.ClientConfig
|
||||
options.PodClient = o.Clientset.CoreV1()
|
||||
|
||||
if err := options.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := options.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
975
pkg/cmd/cp/cp_test.go
Normal file
975
pkg/cmd/cp/cp_test.go
Normal file
|
|
@ -0,0 +1,975 @@
|
|||
/*
|
||||
Copyright 2014 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 cp
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
kexec "k8s.io/kubernetes/pkg/kubectl/cmd/exec"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
type FileType int
|
||||
|
||||
const (
|
||||
RegularFile FileType = 0
|
||||
SymLink FileType = 1
|
||||
RegexFile FileType = 2
|
||||
)
|
||||
|
||||
func TestExtractFileSpec(t *testing.T) {
|
||||
tests := []struct {
|
||||
spec string
|
||||
expectedPod string
|
||||
expectedNamespace string
|
||||
expectedFile string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
spec: "namespace/pod:/some/file",
|
||||
expectedPod: "pod",
|
||||
expectedNamespace: "namespace",
|
||||
expectedFile: "/some/file",
|
||||
},
|
||||
{
|
||||
spec: "pod:/some/file",
|
||||
expectedPod: "pod",
|
||||
expectedFile: "/some/file",
|
||||
},
|
||||
{
|
||||
spec: "/some/file",
|
||||
expectedFile: "/some/file",
|
||||
},
|
||||
{
|
||||
spec: ":file:not:exist:in:local:filesystem",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
spec: "namespace/pod/invalid:/some/file",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
spec: "pod:/some/filenamewith:in",
|
||||
expectedPod: "pod",
|
||||
expectedFile: "/some/filenamewith:in",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
spec, err := extractFileSpec(test.spec)
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
continue
|
||||
}
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
continue
|
||||
}
|
||||
if spec.PodName != test.expectedPod {
|
||||
t.Errorf("expected: %s, saw: %s", test.expectedPod, spec.PodName)
|
||||
}
|
||||
if spec.PodNamespace != test.expectedNamespace {
|
||||
t.Errorf("expected: %s, saw: %s", test.expectedNamespace, spec.PodNamespace)
|
||||
}
|
||||
if spec.File != test.expectedFile {
|
||||
t.Errorf("expected: %s, saw: %s", test.expectedFile, spec.File)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "/foo/bar",
|
||||
expected: "foo/bar",
|
||||
},
|
||||
{
|
||||
input: "foo/bar",
|
||||
expected: "foo/bar",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
out := getPrefix(test.input)
|
||||
if out != test.expected {
|
||||
t.Errorf("expected: %s, saw: %s", test.expected, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripPathShortcuts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "test single path shortcut prefix",
|
||||
input: "../foo/bar",
|
||||
expected: "foo/bar",
|
||||
},
|
||||
{
|
||||
name: "test multiple path shortcuts",
|
||||
input: "../../foo/bar",
|
||||
expected: "foo/bar",
|
||||
},
|
||||
{
|
||||
name: "test multiple path shortcuts with absolute path",
|
||||
input: "/tmp/one/two/../../foo/bar",
|
||||
expected: "tmp/foo/bar",
|
||||
},
|
||||
{
|
||||
name: "test multiple path shortcuts with no named directory",
|
||||
input: "../../",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "test multiple path shortcuts with no named directory and no trailing slash",
|
||||
input: "../..",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "test multiple path shortcuts with absolute path and filename containing leading dots",
|
||||
input: "/tmp/one/two/../../foo/..bar",
|
||||
expected: "tmp/foo/..bar",
|
||||
},
|
||||
{
|
||||
name: "test multiple path shortcuts with no named directory and filename containing leading dots",
|
||||
input: "../...foo",
|
||||
expected: "...foo",
|
||||
},
|
||||
{
|
||||
name: "test filename containing leading dots",
|
||||
input: "...foo",
|
||||
expected: "...foo",
|
||||
},
|
||||
{
|
||||
name: "test root directory",
|
||||
input: "/",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
out := stripPathShortcuts(test.input)
|
||||
if out != test.expected {
|
||||
t.Errorf("expected: %s, saw: %s", test.expected, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestIsDestRelative(t *testing.T) {
|
||||
tests := []struct {
|
||||
base string
|
||||
dest string
|
||||
relative bool
|
||||
}{
|
||||
{
|
||||
base: "/dir",
|
||||
dest: "/dir/../link",
|
||||
relative: false,
|
||||
},
|
||||
{
|
||||
base: "/dir",
|
||||
dest: "/dir/../../link",
|
||||
relative: false,
|
||||
},
|
||||
{
|
||||
base: "/dir",
|
||||
dest: "/link",
|
||||
relative: false,
|
||||
},
|
||||
{
|
||||
base: "/dir",
|
||||
dest: "/dir/link",
|
||||
relative: true,
|
||||
},
|
||||
{
|
||||
base: "/dir",
|
||||
dest: "/dir/int/../link",
|
||||
relative: true,
|
||||
},
|
||||
{
|
||||
base: "dir",
|
||||
dest: "dir/link",
|
||||
relative: true,
|
||||
},
|
||||
{
|
||||
base: "dir",
|
||||
dest: "dir/int/../link",
|
||||
relative: true,
|
||||
},
|
||||
{
|
||||
base: "dir",
|
||||
dest: "dir/../../link",
|
||||
relative: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
if test.relative != isDestRelative(test.base, test.dest) {
|
||||
t.Errorf("unexpected result for: base %q, dest %q", test.base, test.dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkErr(t *testing.T, err error) {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTarUntar(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "input")
|
||||
checkErr(t, err)
|
||||
dir2, err := ioutil.TempDir("", "output")
|
||||
checkErr(t, err)
|
||||
dir3, err := ioutil.TempDir("", "dir")
|
||||
checkErr(t, err)
|
||||
|
||||
dir = dir + "/"
|
||||
defer func() {
|
||||
os.RemoveAll(dir)
|
||||
os.RemoveAll(dir2)
|
||||
os.RemoveAll(dir3)
|
||||
}()
|
||||
|
||||
files := []struct {
|
||||
name string
|
||||
nameList []string
|
||||
data string
|
||||
omitted bool
|
||||
fileType FileType
|
||||
}{
|
||||
{
|
||||
name: "foo",
|
||||
data: "foobarbaz",
|
||||
fileType: RegularFile,
|
||||
},
|
||||
{
|
||||
name: "dir/blah",
|
||||
data: "bazblahfoo",
|
||||
fileType: RegularFile,
|
||||
},
|
||||
{
|
||||
name: "some/other/directory/",
|
||||
data: "with more data here",
|
||||
fileType: RegularFile,
|
||||
},
|
||||
{
|
||||
name: "blah",
|
||||
data: "same file name different data",
|
||||
fileType: RegularFile,
|
||||
},
|
||||
{
|
||||
name: "gakki",
|
||||
data: "tmp/gakki",
|
||||
fileType: SymLink,
|
||||
},
|
||||
{
|
||||
name: "relative_to_dest",
|
||||
data: path.Join(dir2, "foo"),
|
||||
fileType: SymLink,
|
||||
},
|
||||
{
|
||||
name: "tricky_relative",
|
||||
data: path.Join(dir3, "xyz"),
|
||||
omitted: true,
|
||||
fileType: SymLink,
|
||||
},
|
||||
{
|
||||
name: "absolute_path",
|
||||
data: "/tmp/gakki",
|
||||
omitted: true,
|
||||
fileType: SymLink,
|
||||
},
|
||||
{
|
||||
name: "blah*",
|
||||
nameList: []string{"blah1", "blah2"},
|
||||
data: "regexp file name",
|
||||
fileType: RegexFile,
|
||||
},
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
filepath := path.Join(dir, file.name)
|
||||
if err := os.MkdirAll(path.Dir(filepath), 0755); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if file.fileType == RegularFile {
|
||||
createTmpFile(t, filepath, file.data)
|
||||
} else if file.fileType == SymLink {
|
||||
err := os.Symlink(file.data, filepath)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
} else if file.fileType == RegexFile {
|
||||
for _, fileName := range file.nameList {
|
||||
createTmpFile(t, path.Join(dir, fileName), file.data)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("unexpected file type: %v", file)
|
||||
}
|
||||
}
|
||||
|
||||
opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard())
|
||||
|
||||
writer := &bytes.Buffer{}
|
||||
if err := makeTar(dir, dir, writer); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
reader := bytes.NewBuffer(writer.Bytes())
|
||||
if err := opts.untarAll(reader, dir2, ""); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
absPath := filepath.Join(dir2, strings.TrimPrefix(dir, os.TempDir()))
|
||||
filePath := filepath.Join(absPath, file.name)
|
||||
|
||||
if file.fileType == RegularFile {
|
||||
cmpFileData(t, filePath, file.data)
|
||||
} else if file.fileType == SymLink {
|
||||
dest, err := os.Readlink(filePath)
|
||||
if file.omitted {
|
||||
if err != nil && strings.Contains(err.Error(), "no such file or directory") {
|
||||
continue
|
||||
}
|
||||
t.Fatalf("expected to omit symlink for %s", filePath)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if file.data != dest {
|
||||
t.Fatalf("expected: %s, saw: %s", file.data, dest)
|
||||
}
|
||||
} else if file.fileType == RegexFile {
|
||||
for _, fileName := range file.nameList {
|
||||
cmpFileData(t, path.Join(dir, fileName), file.data)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("unexpected file type: %v", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTarUntarWrongPrefix(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "input")
|
||||
checkErr(t, err)
|
||||
dir2, err := ioutil.TempDir("", "output")
|
||||
checkErr(t, err)
|
||||
|
||||
dir = dir + "/"
|
||||
defer func() {
|
||||
os.RemoveAll(dir)
|
||||
os.RemoveAll(dir2)
|
||||
}()
|
||||
|
||||
filepath := path.Join(dir, "foo")
|
||||
if err := os.MkdirAll(path.Dir(filepath), 0755); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
createTmpFile(t, filepath, "sample data")
|
||||
|
||||
opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard())
|
||||
|
||||
writer := &bytes.Buffer{}
|
||||
if err := makeTar(dir, dir, writer); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
reader := bytes.NewBuffer(writer.Bytes())
|
||||
err = opts.untarAll(reader, dir2, "verylongprefix-showing-the-tar-was-tempered-with")
|
||||
if err == nil || !strings.Contains(err.Error(), "tar contents corrupted") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTarDestinationName(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "input")
|
||||
dir2, err2 := ioutil.TempDir(os.TempDir(), "output")
|
||||
if err != nil || err2 != nil {
|
||||
t.Errorf("unexpected error: %v | %v", err, err2)
|
||||
t.FailNow()
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Errorf("Unexpected error cleaning up: %v", err)
|
||||
}
|
||||
if err := os.RemoveAll(dir2); err != nil {
|
||||
t.Errorf("Unexpected error cleaning up: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
files := []struct {
|
||||
name string
|
||||
data string
|
||||
}{
|
||||
{
|
||||
name: "foo",
|
||||
data: "foobarbaz",
|
||||
},
|
||||
{
|
||||
name: "dir/blah",
|
||||
data: "bazblahfoo",
|
||||
},
|
||||
{
|
||||
name: "some/other/directory",
|
||||
data: "with more data here",
|
||||
},
|
||||
{
|
||||
name: "blah",
|
||||
data: "same file name different data",
|
||||
},
|
||||
}
|
||||
|
||||
// ensure files exist on disk
|
||||
for _, file := range files {
|
||||
filepath := path.Join(dir, file.name)
|
||||
if err := os.MkdirAll(path.Dir(filepath), 0755); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
createTmpFile(t, filepath, file.data)
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
go func() {
|
||||
if err := makeTar(dir, dir2, writer); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
tarReader := tar.NewReader(reader)
|
||||
for {
|
||||
hdr, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(hdr.Name, path.Base(dir2)) {
|
||||
t.Errorf("expected %q as destination filename prefix, saw: %q", path.Base(dir2), hdr.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadTar(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "dest")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v ", err)
|
||||
t.FailNow()
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// More or less cribbed from https://golang.org/pkg/archive/tar/#example__minimal
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
var files = []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{"/prefix/foo/bar/../../home/bburns/names.txt", "Down and back"},
|
||||
}
|
||||
for _, file := range files {
|
||||
hdr := &tar.Header{
|
||||
Name: file.name,
|
||||
Mode: 0600,
|
||||
Size: int64(len(file.body)),
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
t.Errorf("unexpected error: %v ", err)
|
||||
t.FailNow()
|
||||
}
|
||||
if _, err := tw.Write([]byte(file.body)); err != nil {
|
||||
t.Errorf("unexpected error: %v ", err)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
t.Errorf("unexpected error: %v ", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
opts := NewCopyOptions(genericclioptions.NewTestIOStreamsDiscard())
|
||||
if err := opts.untarAll(&buf, dir, "/prefix"); err != nil {
|
||||
t.Errorf("unexpected error: %v ", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
_, err := os.Stat(path.Join(dir, path.Clean(file.name[len("/prefix"):])))
|
||||
if err != nil {
|
||||
t.Errorf("Error finding file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clean prevents path traversals by stripping them out.
|
||||
// This is adapted from https://golang.org/src/net/http/fs.go#L74
|
||||
func clean(fileName string) string {
|
||||
return path.Clean(string(os.PathSeparator) + fileName)
|
||||
}
|
||||
|
||||
func TestClean(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
cleaned string
|
||||
}{
|
||||
{
|
||||
"../../../tmp/foo",
|
||||
"/tmp/foo",
|
||||
},
|
||||
{
|
||||
"/../../../tmp/foo",
|
||||
"/tmp/foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
out := clean(test.input)
|
||||
if out != test.cleaned {
|
||||
t.Errorf("Expected: %s, saw %s", test.cleaned, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyToPod(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
ns := scheme.Codecs
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
responsePod := &v1.Pod{}
|
||||
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, responsePod))))}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdCp(tf, ioStreams)
|
||||
|
||||
srcFile, err := ioutil.TempDir("", "test")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
defer os.RemoveAll(srcFile)
|
||||
|
||||
tests := map[string]struct {
|
||||
dest string
|
||||
expectedErr bool
|
||||
}{
|
||||
"copy to directory": {
|
||||
dest: "/tmp/",
|
||||
expectedErr: false,
|
||||
},
|
||||
"copy to root": {
|
||||
dest: "/",
|
||||
expectedErr: false,
|
||||
},
|
||||
"copy to empty file name": {
|
||||
dest: "",
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
opts := NewCopyOptions(ioStreams)
|
||||
src := fileSpec{
|
||||
File: srcFile,
|
||||
}
|
||||
dest := fileSpec{
|
||||
PodNamespace: "pod-ns",
|
||||
PodName: "pod-name",
|
||||
File: test.dest,
|
||||
}
|
||||
opts.Complete(tf, cmd)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err = opts.copyToPod(src, dest, &kexec.ExecOptions{})
|
||||
//If error is NotFound error , it indicates that the
|
||||
//request has been sent correctly.
|
||||
//Treat this as no error.
|
||||
if test.expectedErr && errors.IsNotFound(err) {
|
||||
t.Errorf("expected error but didn't get one")
|
||||
}
|
||||
if !test.expectedErr && !errors.IsNotFound(err) {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyToPodNoPreserve(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
ns := scheme.Codecs
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
responsePod := &v1.Pod{}
|
||||
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, responsePod))))}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdCp(tf, ioStreams)
|
||||
|
||||
srcFile, err := ioutil.TempDir("", "test")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
defer os.RemoveAll(srcFile)
|
||||
|
||||
tests := map[string]struct {
|
||||
expectedCmd []string
|
||||
nopreserve bool
|
||||
}{
|
||||
"copy to pod no preserve user and permissions": {
|
||||
expectedCmd: []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-", "-C", "."},
|
||||
nopreserve: true,
|
||||
},
|
||||
"copy to pod preserve user and permissions": {
|
||||
expectedCmd: []string{"tar", "-xmf", "-", "-C", "."},
|
||||
nopreserve: false,
|
||||
},
|
||||
}
|
||||
opts := NewCopyOptions(ioStreams)
|
||||
src := fileSpec{
|
||||
File: srcFile,
|
||||
}
|
||||
dest := fileSpec{
|
||||
PodNamespace: "pod-ns",
|
||||
PodName: "pod-name",
|
||||
File: "foo",
|
||||
}
|
||||
opts.Complete(tf, cmd)
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
options := &kexec.ExecOptions{}
|
||||
opts.NoPreserve = test.nopreserve
|
||||
err = opts.copyToPod(src, dest, options)
|
||||
if !(reflect.DeepEqual(test.expectedCmd, options.Command)) {
|
||||
t.Errorf("expected cmd: %v, got: %v", test.expectedCmd, options.Command)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "Validate Succeed",
|
||||
args: []string{"1", "2"},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "Validate Fail",
|
||||
args: []string{"1", "2", "3"},
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
tf := cmdtesting.NewTestFactory()
|
||||
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams()
|
||||
opts := NewCopyOptions(ioStreams)
|
||||
cmd := NewCmdCp(tf, ioStreams)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := opts.Validate(cmd, test.args)
|
||||
if (err != nil) != test.expectedErr {
|
||||
t.Errorf("expected error: %v, saw: %v, error: %v", test.expectedErr, err != nil, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUntar(t *testing.T) {
|
||||
testdir, err := ioutil.TempDir("", "test-untar")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testdir)
|
||||
t.Logf("Test base: %s", testdir)
|
||||
|
||||
basedir := filepath.Join(testdir, "base")
|
||||
|
||||
type file struct {
|
||||
path string
|
||||
linkTarget string // For link types
|
||||
expected string // Expect to find the file here (or not, if empty)
|
||||
}
|
||||
files := []file{{
|
||||
// Absolute file within dest
|
||||
path: filepath.Join(basedir, "abs"),
|
||||
expected: filepath.Join(basedir, basedir, "abs"),
|
||||
}, { // Absolute file outside dest
|
||||
path: filepath.Join(testdir, "abs-out"),
|
||||
expected: filepath.Join(basedir, testdir, "abs-out"),
|
||||
}, { // Absolute nested file within dest
|
||||
path: filepath.Join(basedir, "nested/nest-abs"),
|
||||
expected: filepath.Join(basedir, basedir, "nested/nest-abs"),
|
||||
}, { // Absolute nested file outside dest
|
||||
path: filepath.Join(basedir, "nested/../../nest-abs-out"),
|
||||
expected: filepath.Join(basedir, testdir, "nest-abs-out"),
|
||||
}, { // Relative file inside dest
|
||||
path: "relative",
|
||||
expected: filepath.Join(basedir, "relative"),
|
||||
}, { // Relative file outside dest
|
||||
path: "../unrelative",
|
||||
expected: "",
|
||||
}, { // Nested relative file inside dest
|
||||
path: "nested/nest-rel",
|
||||
expected: filepath.Join(basedir, "nested/nest-rel"),
|
||||
}, { // Nested relative file outside dest
|
||||
path: "nested/../../nest-unrelative",
|
||||
expected: "",
|
||||
}}
|
||||
|
||||
mkExpectation := func(expected, suffix string) string {
|
||||
if expected == "" {
|
||||
return ""
|
||||
}
|
||||
return expected + suffix
|
||||
}
|
||||
mkBacklinkExpectation := func(expected, suffix string) string {
|
||||
// "resolve" the back link relative to the expectation
|
||||
targetDir := filepath.Dir(filepath.Dir(expected))
|
||||
// If the "resolved" target is not nested in basedir, it is escaping.
|
||||
if !filepath.HasPrefix(targetDir, basedir) {
|
||||
return ""
|
||||
}
|
||||
return expected + suffix
|
||||
}
|
||||
links := []file{}
|
||||
for _, f := range files {
|
||||
links = append(links, file{
|
||||
path: f.path + "-innerlink",
|
||||
linkTarget: "link-target",
|
||||
expected: mkExpectation(f.expected, "-innerlink"),
|
||||
}, file{
|
||||
path: f.path + "-innerlink-abs",
|
||||
linkTarget: filepath.Join(basedir, "link-target"),
|
||||
expected: mkExpectation(f.expected, "-innerlink-abs"),
|
||||
}, file{
|
||||
path: f.path + "-backlink",
|
||||
linkTarget: filepath.Join("..", "link-target"),
|
||||
expected: mkBacklinkExpectation(f.expected, "-backlink"),
|
||||
}, file{
|
||||
path: f.path + "-outerlink-abs",
|
||||
linkTarget: filepath.Join(testdir, "link-target"),
|
||||
expected: "",
|
||||
})
|
||||
|
||||
if f.expected != "" {
|
||||
// outerlink is the number of backticks to escape to testdir
|
||||
outerlink, _ := filepath.Rel(f.expected, testdir)
|
||||
links = append(links, file{
|
||||
path: f.path + "outerlink",
|
||||
linkTarget: filepath.Join(outerlink, "link-target"),
|
||||
expected: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
files = append(files, links...)
|
||||
|
||||
// Test back-tick escaping through a symlink.
|
||||
files = append(files,
|
||||
file{
|
||||
path: "nested/again/back-link",
|
||||
linkTarget: "../../nested",
|
||||
expected: filepath.Join(basedir, "nested/again/back-link"),
|
||||
},
|
||||
file{
|
||||
path: "nested/again/back-link/../../../back-link-file",
|
||||
expected: filepath.Join(basedir, "back-link-file"),
|
||||
})
|
||||
|
||||
// Test chaining back-tick symlinks.
|
||||
files = append(files,
|
||||
file{
|
||||
path: "nested/back-link-first",
|
||||
linkTarget: "../",
|
||||
expected: filepath.Join(basedir, "nested/back-link-first"),
|
||||
},
|
||||
file{
|
||||
path: "nested/back-link-first/back-link-second",
|
||||
linkTarget: "../",
|
||||
expected: "",
|
||||
})
|
||||
|
||||
files = append(files,
|
||||
file{ // Relative directory path with terminating /
|
||||
path: "direct/dir/",
|
||||
expected: "",
|
||||
})
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tw := tar.NewWriter(buf)
|
||||
expectations := map[string]bool{}
|
||||
for _, f := range files {
|
||||
if f.expected != "" {
|
||||
expectations[f.expected] = false
|
||||
}
|
||||
if f.linkTarget == "" {
|
||||
hdr := &tar.Header{
|
||||
Name: f.path,
|
||||
Mode: 0666,
|
||||
Size: int64(len(f.path)),
|
||||
}
|
||||
require.NoError(t, tw.WriteHeader(hdr), f.path)
|
||||
if !strings.HasSuffix(f.path, "/") {
|
||||
_, err := tw.Write([]byte(f.path))
|
||||
require.NoError(t, err, f.path)
|
||||
}
|
||||
} else {
|
||||
hdr := &tar.Header{
|
||||
Name: f.path,
|
||||
Mode: int64(0777 | os.ModeSymlink),
|
||||
Typeflag: tar.TypeSymlink,
|
||||
Linkname: f.linkTarget,
|
||||
}
|
||||
require.NoError(t, tw.WriteHeader(hdr), f.path)
|
||||
}
|
||||
}
|
||||
tw.Close()
|
||||
|
||||
// Capture warnings to stderr for debugging.
|
||||
output := (*testWriter)(t)
|
||||
opts := NewCopyOptions(genericclioptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output})
|
||||
|
||||
require.NoError(t, opts.untarAll(buf, filepath.Join(basedir), ""))
|
||||
|
||||
filepath.Walk(testdir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil // Ignore directories.
|
||||
}
|
||||
if _, ok := expectations[path]; !ok {
|
||||
t.Errorf("Unexpected file at %s", path)
|
||||
} else {
|
||||
expectations[path] = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for path, found := range expectations {
|
||||
if !found {
|
||||
t.Errorf("Missing expected file %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUntar_SingleFile(t *testing.T) {
|
||||
testdir, err := ioutil.TempDir("", "test-untar")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testdir)
|
||||
|
||||
dest := filepath.Join(testdir, "target")
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tw := tar.NewWriter(buf)
|
||||
|
||||
const (
|
||||
srcName = "source"
|
||||
content = "file contents"
|
||||
)
|
||||
hdr := &tar.Header{
|
||||
Name: srcName,
|
||||
Mode: 0666,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
require.NoError(t, tw.WriteHeader(hdr))
|
||||
_, err = tw.Write([]byte(content))
|
||||
require.NoError(t, err)
|
||||
tw.Close()
|
||||
|
||||
// Capture warnings to stderr for debugging.
|
||||
output := (*testWriter)(t)
|
||||
opts := NewCopyOptions(genericclioptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output})
|
||||
|
||||
require.NoError(t, opts.untarAll(buf, filepath.Join(dest), srcName))
|
||||
cmpFileData(t, dest, content)
|
||||
}
|
||||
|
||||
func createTmpFile(t *testing.T, filepath, data string) {
|
||||
f, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(f, bytes.NewBuffer([]byte(data))); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpFileData(t *testing.T, filePath, data string) {
|
||||
actual, err := ioutil.ReadFile(filePath)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, data, actual)
|
||||
}
|
||||
|
||||
type testWriter testing.T
|
||||
|
||||
func (t *testWriter) Write(p []byte) (n int, err error) {
|
||||
t.Logf(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
440
pkg/cmd/create/create.go
Normal file
440
pkg/cmd/create/create.go
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
/*
|
||||
Copyright 2014 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 create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
kruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/dynamic"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/cmd/util/editor"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
"k8s.io/kubectl/pkg/rawhttp"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// CreateOptions is the commandline options for 'create' sub command
|
||||
type CreateOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
|
||||
DryRun bool
|
||||
|
||||
FilenameOptions resource.FilenameOptions
|
||||
Selector string
|
||||
EditBeforeCreate bool
|
||||
Raw string
|
||||
|
||||
Recorder genericclioptions.Recorder
|
||||
PrintObj func(obj kruntime.Object) error
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
createLong = templates.LongDesc(i18n.T(`
|
||||
Create a resource from a file or from stdin.
|
||||
|
||||
JSON and YAML formats are accepted.`))
|
||||
|
||||
createExample = templates.Examples(i18n.T(`
|
||||
# Create a pod using the data in pod.json.
|
||||
kubectl create -f ./pod.json
|
||||
|
||||
# Create a pod based on the JSON passed into stdin.
|
||||
cat pod.json | kubectl create -f -
|
||||
|
||||
# Edit the data in docker-registry.yaml in JSON then create the resource using the edited data.
|
||||
kubectl create -f docker-registry.yaml --edit -o json`))
|
||||
)
|
||||
|
||||
// NewCreateOptions returns an initialized CreateOptions instance
|
||||
func NewCreateOptions(ioStreams genericclioptions.IOStreams) *CreateOptions {
|
||||
return &CreateOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
|
||||
Recorder: genericclioptions.NoopRecorder{},
|
||||
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdCreate returns new initialized instance of create sub command
|
||||
func NewCmdCreate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewCreateOptions(ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create -f FILENAME",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a resource from a file or from stdin."),
|
||||
Long: createLong,
|
||||
Example: createExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) {
|
||||
ioStreams.ErrOut.Write([]byte("Error: must specify one of -f and -k\n\n"))
|
||||
defaultRunFunc := cmdutil.DefaultSubCommandRun(ioStreams.ErrOut)
|
||||
defaultRunFunc(cmd, args)
|
||||
return
|
||||
}
|
||||
cmdutil.CheckErr(o.Complete(f, cmd))
|
||||
cmdutil.CheckErr(o.ValidateArgs(cmd, args))
|
||||
cmdutil.CheckErr(o.RunCreate(f, cmd))
|
||||
},
|
||||
}
|
||||
|
||||
// bind flag structs
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
|
||||
usage := "to use to create the resource"
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmd.Flags().BoolVar(&o.EditBeforeCreate, "edit", o.EditBeforeCreate, "Edit the API resource before creating")
|
||||
cmd.Flags().Bool("windows-line-endings", runtime.GOOS == "windows",
|
||||
"Only relevant if --edit=true. Defaults to the line ending native to your platform.")
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||
cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to POST to the server. Uses the transport specified by the kubeconfig file.")
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
// create subcommands
|
||||
cmd.AddCommand(NewCmdCreateNamespace(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateQuota(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateSecret(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateConfigMap(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateServiceAccount(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateService(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateDeployment(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateClusterRole(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateClusterRoleBinding(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateRole(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateRoleBinding(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreatePodDisruptionBudget(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreatePriorityClass(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateJob(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ValidateArgs makes sure there is no discrepency in command options
|
||||
func (o *CreateOptions) ValidateArgs(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
|
||||
}
|
||||
if len(o.Raw) > 0 {
|
||||
if o.EditBeforeCreate {
|
||||
return cmdutil.UsageErrorf(cmd, "--raw and --edit are mutually exclusive")
|
||||
}
|
||||
if len(o.FilenameOptions.Filenames) != 1 {
|
||||
return cmdutil.UsageErrorf(cmd, "--raw can only use a single local file or stdin")
|
||||
}
|
||||
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "--raw cannot read from a url")
|
||||
}
|
||||
if o.FilenameOptions.Recursive {
|
||||
return cmdutil.UsageErrorf(cmd, "--raw and --recursive are mutually exclusive")
|
||||
}
|
||||
if len(o.Selector) > 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "--raw and --selector (-l) are mutually exclusive")
|
||||
}
|
||||
if len(cmdutil.GetFlagString(cmd, "output")) > 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "--raw and --output are mutually exclusive")
|
||||
}
|
||||
if _, err := url.ParseRequestURI(o.Raw); err != nil {
|
||||
return cmdutil.UsageErrorf(cmd, "--raw must be a valid URL path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *CreateOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||
var err error
|
||||
o.RecordFlags.Complete(cmd)
|
||||
o.Recorder, err = o.RecordFlags.ToRecorder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.DryRun = cmdutil.GetDryRunFlag(cmd)
|
||||
|
||||
if o.DryRun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.PrintObj = func(obj kruntime.Object) error {
|
||||
return printer.PrintObj(obj, o.Out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunCreate performs the creation
|
||||
func (o *CreateOptions) RunCreate(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||
// raw only makes sense for a single file resource multiple objects aren't likely to do what you want.
|
||||
// the validator enforces this, so
|
||||
if len(o.Raw) > 0 {
|
||||
restClient, err := f.RESTClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rawhttp.RawPost(restClient, o.IOStreams, o.Raw, o.FilenameOptions.Filenames[0])
|
||||
}
|
||||
|
||||
if o.EditBeforeCreate {
|
||||
return RunEditOnCreate(f, o.PrintFlags, o.RecordFlags, o.IOStreams, cmd, &o.FilenameOptions)
|
||||
}
|
||||
schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := f.NewBuilder().
|
||||
Unstructured().
|
||||
Schema(schema).
|
||||
ContinueOnError().
|
||||
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||
LabelSelectorParam(o.Selector).
|
||||
Flatten().
|
||||
Do()
|
||||
err = r.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count := 0
|
||||
err = r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), info.Object, scheme.DefaultJSONEncoder()); err != nil {
|
||||
return cmdutil.AddSourceToErr("creating", info.Source, err)
|
||||
}
|
||||
|
||||
if err := o.Recorder.Record(info.Object); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
}
|
||||
|
||||
if !o.DryRun {
|
||||
if err := createAndRefresh(info); err != nil {
|
||||
return cmdutil.AddSourceToErr("creating", info.Source, err)
|
||||
}
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
return o.PrintObj(info.Object)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("no objects passed to create")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunEditOnCreate performs edit on creation
|
||||
func RunEditOnCreate(f cmdutil.Factory, printFlags *genericclioptions.PrintFlags, recordFlags *genericclioptions.RecordFlags, ioStreams genericclioptions.IOStreams, cmd *cobra.Command, options *resource.FilenameOptions) error {
|
||||
editOptions := editor.NewEditOptions(editor.EditBeforeCreateMode, ioStreams)
|
||||
editOptions.FilenameOptions = *options
|
||||
editOptions.ValidateOptions = cmdutil.ValidateOptions{
|
||||
EnableValidation: cmdutil.GetFlagBool(cmd, "validate"),
|
||||
}
|
||||
editOptions.PrintFlags = printFlags
|
||||
editOptions.ApplyAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
|
||||
editOptions.RecordFlags = recordFlags
|
||||
|
||||
err := editOptions.Complete(f, []string{}, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return editOptions.Run()
|
||||
}
|
||||
|
||||
// createAndRefresh creates an object from input info and refreshes info with that object
|
||||
func createAndRefresh(info *resource.Info) error {
|
||||
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.Refresh(obj, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NameFromCommandArgs is a utility function for commands that assume the first argument is a resource name
|
||||
func NameFromCommandArgs(cmd *cobra.Command, args []string) (string, error) {
|
||||
argsLen := cmd.ArgsLenAtDash()
|
||||
// ArgsLenAtDash returns -1 when -- was not specified
|
||||
if argsLen == -1 {
|
||||
argsLen = len(args)
|
||||
}
|
||||
if argsLen != 1 {
|
||||
return "", cmdutil.UsageErrorf(cmd, "exactly one NAME is required, got %d", argsLen)
|
||||
}
|
||||
return args[0], nil
|
||||
}
|
||||
|
||||
// CreateSubcommandOptions is an options struct to support create subcommands
|
||||
type CreateSubcommandOptions struct {
|
||||
// PrintFlags holds options necessary for obtaining a printer
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
// Name of resource being created
|
||||
Name string
|
||||
// StructuredGenerator is the resource generator for the object being created
|
||||
StructuredGenerator generate.StructuredGenerator
|
||||
// DryRun is true if the command should be simulated but not run against the server
|
||||
DryRun bool
|
||||
CreateAnnotation bool
|
||||
|
||||
Namespace string
|
||||
EnforceNamespace bool
|
||||
|
||||
Mapper meta.RESTMapper
|
||||
DynamicClient dynamic.Interface
|
||||
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewCreateSubcommandOptions returns initialized CreateSubcommandOptions
|
||||
func NewCreateSubcommandOptions(ioStreams genericclioptions.IOStreams) *CreateSubcommandOptions {
|
||||
return &CreateSubcommandOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *CreateSubcommandOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string, generator generate.StructuredGenerator) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Name = name
|
||||
o.StructuredGenerator = generator
|
||||
o.DryRun = cmdutil.GetDryRunFlag(cmd)
|
||||
o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
|
||||
|
||||
if o.DryRun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.PrintObj = func(obj kruntime.Object, out io.Writer) error {
|
||||
return printer.PrintObj(obj, out)
|
||||
}
|
||||
|
||||
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.DynamicClient, err = f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Mapper, err = f.ToRESTMapper()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run executes a create subcommand using the specified options
|
||||
func (o *CreateSubcommandOptions) Run() error {
|
||||
obj, err := o.StructuredGenerator.StructuredGenerate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !o.DryRun {
|
||||
// create subcommands have compiled knowledge of things they create, so type them directly
|
||||
gvks, _, err := scheme.Scheme.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gvk := gvks[0]
|
||||
mapping, err := o.Mapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, obj, scheme.DefaultJSONEncoder()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asUnstructured := &unstructured.Unstructured{}
|
||||
|
||||
if err := scheme.Scheme.Convert(obj, asUnstructured, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if mapping.Scope.Name() == meta.RESTScopeNameRoot {
|
||||
o.Namespace = ""
|
||||
}
|
||||
actualObject, err := o.DynamicClient.Resource(mapping.Resource).Namespace(o.Namespace).Create(asUnstructured, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure we pass a versioned object to the printer
|
||||
obj = actualObject
|
||||
} else {
|
||||
if meta, err := meta.Accessor(obj); err == nil && o.EnforceNamespace {
|
||||
meta.SetNamespace(o.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
return o.PrintObj(obj, o.Out)
|
||||
}
|
||||
210
pkg/cmd/create/create_clusterrole.go
Normal file
210
pkg/cmd/create/create_clusterrole.go
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
clusterRoleLong = templates.LongDesc(i18n.T(`
|
||||
Create a ClusterRole.`))
|
||||
|
||||
clusterRoleExample = templates.Examples(i18n.T(`
|
||||
# Create a ClusterRole named "pod-reader" that allows user to perform "get", "watch" and "list" on pods
|
||||
kubectl create clusterrole pod-reader --verb=get,list,watch --resource=pods
|
||||
|
||||
# Create a ClusterRole named "pod-reader" with ResourceName specified
|
||||
kubectl create clusterrole pod-reader --verb=get --resource=pods --resource-name=readablepod --resource-name=anotherpod
|
||||
|
||||
# Create a ClusterRole named "foo" with API Group specified
|
||||
kubectl create clusterrole foo --verb=get,list,watch --resource=rs.extensions
|
||||
|
||||
# Create a ClusterRole named "foo" with SubResource specified
|
||||
kubectl create clusterrole foo --verb=get,list,watch --resource=pods,pods/status
|
||||
|
||||
# Create a ClusterRole name "foo" with NonResourceURL specified
|
||||
kubectl create clusterrole "foo" --verb=get --non-resource-url=/logs/*
|
||||
|
||||
# Create a ClusterRole name "monitoring" with AggregationRule specified
|
||||
kubectl create clusterrole monitoring --aggregation-rule="rbac.example.com/aggregate-to-monitoring=true"`))
|
||||
|
||||
// Valid nonResource verb list for validation.
|
||||
validNonResourceVerbs = []string{"*", "get", "post", "put", "delete", "patch", "head", "options"}
|
||||
)
|
||||
|
||||
// CreateClusterRoleOptions is returned by NewCmdCreateClusterRole
|
||||
type CreateClusterRoleOptions struct {
|
||||
*CreateRoleOptions
|
||||
NonResourceURLs []string
|
||||
AggregationRule map[string]string
|
||||
}
|
||||
|
||||
// NewCmdCreateClusterRole initializes and returns new ClusterRoles command
|
||||
func NewCmdCreateClusterRole(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
c := &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: NewCreateRoleOptions(ioStreams),
|
||||
AggregationRule: map[string]string{},
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "clusterrole NAME --verb=verb --resource=resource.group [--resource-name=resourcename] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: clusterRoleLong,
|
||||
Long: clusterRoleLong,
|
||||
Example: clusterRoleExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(c.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(c.Validate())
|
||||
cmdutil.CheckErr(c.RunCreateRole())
|
||||
},
|
||||
}
|
||||
|
||||
c.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringSliceVar(&c.Verbs, "verb", c.Verbs, "Verb that applies to the resources contained in the rule")
|
||||
cmd.Flags().StringSliceVar(&c.NonResourceURLs, "non-resource-url", c.NonResourceURLs, "A partial url that user should have access to.")
|
||||
cmd.Flags().StringSlice("resource", []string{}, "Resource that the rule applies to")
|
||||
cmd.Flags().StringArrayVar(&c.ResourceNames, "resource-name", c.ResourceNames, "Resource in the white list that the rule applies to, repeat this flag for multiple items")
|
||||
cmd.Flags().Var(cliflag.NewMapStringString(&c.AggregationRule), "aggregation-rule", "An aggregation label selector for combining ClusterRoles.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (c *CreateClusterRoleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
// Remove duplicate nonResourceURLs
|
||||
nonResourceURLs := []string{}
|
||||
for _, n := range c.NonResourceURLs {
|
||||
if !arrayContains(nonResourceURLs, n) {
|
||||
nonResourceURLs = append(nonResourceURLs, n)
|
||||
}
|
||||
}
|
||||
c.NonResourceURLs = nonResourceURLs
|
||||
|
||||
return c.CreateRoleOptions.Complete(f, cmd, args)
|
||||
}
|
||||
|
||||
// Validate makes sure there is no discrepency in CreateClusterRoleOptions
|
||||
func (c *CreateClusterRoleOptions) Validate() error {
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("name must be specified")
|
||||
}
|
||||
|
||||
if len(c.AggregationRule) > 0 {
|
||||
if len(c.NonResourceURLs) > 0 || len(c.Verbs) > 0 || len(c.Resources) > 0 || len(c.ResourceNames) > 0 {
|
||||
return fmt.Errorf("aggregation rule must be specified without nonResourceURLs, verbs, resources or resourceNames")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate verbs.
|
||||
if len(c.Verbs) == 0 {
|
||||
return fmt.Errorf("at least one verb must be specified")
|
||||
}
|
||||
|
||||
if len(c.Resources) == 0 && len(c.NonResourceURLs) == 0 {
|
||||
return fmt.Errorf("one of resource or nonResourceURL must be specified")
|
||||
}
|
||||
|
||||
// validate resources
|
||||
if len(c.Resources) > 0 {
|
||||
for _, v := range c.Verbs {
|
||||
if !arrayContains(validResourceVerbs, v) {
|
||||
return fmt.Errorf("invalid verb: '%s'", v)
|
||||
}
|
||||
}
|
||||
if err := c.validateResource(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//validate non-resource-url
|
||||
if len(c.NonResourceURLs) > 0 {
|
||||
for _, v := range c.Verbs {
|
||||
if !arrayContains(validNonResourceVerbs, v) {
|
||||
return fmt.Errorf("invalid verb: '%s' for nonResourceURL", v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, nonResourceURL := range c.NonResourceURLs {
|
||||
if nonResourceURL == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
if nonResourceURL == "" || !strings.HasPrefix(nonResourceURL, "/") {
|
||||
return fmt.Errorf("nonResourceURL should start with /")
|
||||
}
|
||||
|
||||
if strings.ContainsRune(nonResourceURL[:len(nonResourceURL)-1], '*') {
|
||||
return fmt.Errorf("nonResourceURL only supports wildcard matches when '*' is at the end")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// RunCreateRole creates a new clusterRole
|
||||
func (c *CreateClusterRoleOptions) RunCreateRole() error {
|
||||
clusterRole := &rbacv1.ClusterRole{
|
||||
// this is ok because we know exactly how we want to be serialized
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "ClusterRole"},
|
||||
}
|
||||
clusterRole.Name = c.Name
|
||||
|
||||
var err error
|
||||
if len(c.AggregationRule) == 0 {
|
||||
rules, err := generateResourcePolicyRules(c.Mapper, c.Verbs, c.Resources, c.ResourceNames, c.NonResourceURLs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusterRole.Rules = rules
|
||||
} else {
|
||||
clusterRole.AggregationRule = &rbacv1.AggregationRule{
|
||||
ClusterRoleSelectors: []metav1.LabelSelector{
|
||||
{
|
||||
MatchLabels: c.AggregationRule,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create ClusterRole.
|
||||
if !c.DryRun {
|
||||
clusterRole, err = c.Client.ClusterRoles().Create(clusterRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.PrintObj(clusterRole)
|
||||
}
|
||||
513
pkg/cmd/create/create_clusterrole_test.go
Normal file
513
pkg/cmd/create/create_clusterrole_test.go
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateClusterRole(t *testing.T) {
|
||||
clusterRoleName := "my-cluster-role"
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{}
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
tests := map[string]struct {
|
||||
verbs string
|
||||
resources string
|
||||
nonResourceURL string
|
||||
resourceNames string
|
||||
aggregationRule string
|
||||
expectedClusterRole *rbac.ClusterRole
|
||||
}{
|
||||
"test-duplicate-resources": {
|
||||
verbs: "get,watch,list",
|
||||
resources: "pods,pods",
|
||||
expectedClusterRole: &rbac.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
Resources: []string{"pods"},
|
||||
APIGroups: []string{""},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-valid-case-with-multiple-apigroups": {
|
||||
verbs: "get,watch,list",
|
||||
resources: "pods,deployments.extensions",
|
||||
expectedClusterRole: &rbac.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
Resources: []string{"pods"},
|
||||
APIGroups: []string{""},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
Resources: []string{"deployments"},
|
||||
APIGroups: []string{"extensions"},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-non-resource-url": {
|
||||
verbs: "get",
|
||||
nonResourceURL: "/logs/,/healthz",
|
||||
expectedClusterRole: &rbac.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"get"},
|
||||
NonResourceURLs: []string{"/logs/", "/healthz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-resource-and-non-resource-url": {
|
||||
verbs: "get",
|
||||
nonResourceURL: "/logs/,/healthz",
|
||||
resources: "pods",
|
||||
expectedClusterRole: &rbac.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"get"},
|
||||
Resources: []string{"pods"},
|
||||
APIGroups: []string{""},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"get"},
|
||||
NonResourceURLs: []string{"/logs/", "/healthz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-aggregation-rules": {
|
||||
aggregationRule: "foo1=foo2,foo3=foo4",
|
||||
expectedClusterRole: &rbac.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
AggregationRule: &rbac.AggregationRule{
|
||||
ClusterRoleSelectors: []metav1.LabelSelector{
|
||||
{
|
||||
MatchLabels: map[string]string{
|
||||
"foo1": "foo2",
|
||||
"foo3": "foo4",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateClusterRole(tf, ioStreams)
|
||||
cmd.Flags().Set("dry-run", "true")
|
||||
cmd.Flags().Set("output", "yaml")
|
||||
cmd.Flags().Set("verb", test.verbs)
|
||||
cmd.Flags().Set("resource", test.resources)
|
||||
cmd.Flags().Set("non-resource-url", test.nonResourceURL)
|
||||
cmd.Flags().Set("aggregation-rule", test.aggregationRule)
|
||||
if test.resourceNames != "" {
|
||||
cmd.Flags().Set("resource-name", test.resourceNames)
|
||||
}
|
||||
cmd.Run(cmd, []string{clusterRoleName})
|
||||
actual := &rbac.ClusterRole{}
|
||||
if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), buf.Bytes(), actual); err != nil {
|
||||
t.Log(string(buf.Bytes()))
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(test.expectedClusterRole, actual) {
|
||||
t.Errorf("%s:\nexpected:\n%#v\nsaw:\n%#v", name, test.expectedClusterRole, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterRoleValidate(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tests := map[string]struct {
|
||||
clusterRoleOptions *CreateClusterRoleOptions
|
||||
expectErr bool
|
||||
}{
|
||||
"test-missing-name": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-verb": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-resource": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"get"},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-resource-existing-apigroup": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-resource-existing-subresource": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
SubResource: "scale",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-verb": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"invalid-verb"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-nonresource-verb": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"post"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-special-verb": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"use"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-mix-verbs": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"impersonate", "use"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "userextras",
|
||||
SubResource: "scopes",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-special-verb-with-wrong-apigroup": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"impersonate"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "userextras",
|
||||
SubResource: "scopes",
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-resource": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "invalid-resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-resource-name-with-multiple-resources": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
{
|
||||
Resource: "deployments",
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-valid-case": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "role-binder",
|
||||
Verbs: []string{"get", "list", "bind"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "roles",
|
||||
Group: "rbac.authorization.k8s.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-valid-case-with-subresource": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"get", "list"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "replicasets",
|
||||
SubResource: "scale",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-valid-case-with-additional-resource": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"impersonate"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "userextras",
|
||||
SubResource: "scopes",
|
||||
Group: "authentication.k8s.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-invalid-empty-non-resource-url": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"create"},
|
||||
},
|
||||
NonResourceURLs: []string{""},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-non-resource-url": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"create"},
|
||||
},
|
||||
NonResourceURLs: []string{"logs"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-non-resource-url-with-*": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"create"},
|
||||
},
|
||||
NonResourceURLs: []string{"/logs/*/"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-non-resource-url-with-multiple-*": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"create"},
|
||||
},
|
||||
NonResourceURLs: []string{"/logs*/*"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-verb-for-non-resource-url": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"create"},
|
||||
},
|
||||
NonResourceURLs: []string{"/logs/"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-resource-and-non-resource-url-specified-together": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "replicasets",
|
||||
SubResource: "scale",
|
||||
},
|
||||
},
|
||||
},
|
||||
NonResourceURLs: []string{"/logs/", "/logs/*"},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-aggregation-rule-with-verb": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Verbs: []string{"get"},
|
||||
},
|
||||
AggregationRule: map[string]string{"foo-key": "foo-vlue"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-aggregation-rule-with-resource": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "replicasets",
|
||||
SubResource: "scale",
|
||||
},
|
||||
},
|
||||
},
|
||||
AggregationRule: map[string]string{"foo-key": "foo-vlue"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-aggregation-rule-with-no-resource-url": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
},
|
||||
NonResourceURLs: []string{"/logs/"},
|
||||
AggregationRule: map[string]string{"foo-key": "foo-vlue"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-aggregation-rule": {
|
||||
clusterRoleOptions: &CreateClusterRoleOptions{
|
||||
CreateRoleOptions: &CreateRoleOptions{
|
||||
Name: "my-clusterrole",
|
||||
},
|
||||
AggregationRule: map[string]string{"foo-key": "foo-vlue"},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var err error
|
||||
test.clusterRoleOptions.Mapper, err = tf.ToRESTMapper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = test.clusterRoleOptions.Validate()
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("%s: expect error happens, but validate passes.", name)
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
102
pkg/cmd/create/create_clusterrolebinding.go
Normal file
102
pkg/cmd/create/create_clusterrolebinding.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
clusterRoleBindingLong = templates.LongDesc(i18n.T(`
|
||||
Create a ClusterRoleBinding for a particular ClusterRole.`))
|
||||
|
||||
clusterRoleBindingExample = templates.Examples(i18n.T(`
|
||||
# Create a ClusterRoleBinding for user1, user2, and group1 using the cluster-admin ClusterRole
|
||||
kubectl create clusterrolebinding cluster-admin --clusterrole=cluster-admin --user=user1 --user=user2 --group=group1`))
|
||||
)
|
||||
|
||||
// ClusterRoleBindingOpts is returned by NewCmdCreateClusterRoleBinding
|
||||
type ClusterRoleBindingOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateClusterRoleBinding returns an initialized command instance of ClusterRoleBinding
|
||||
func NewCmdCreateClusterRoleBinding(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := &ClusterRoleBindingOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "clusterrolebinding NAME --clusterrole=NAME [--user=username] [--group=groupname] [--serviceaccount=namespace:serviceaccountname] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a ClusterRoleBinding for a particular ClusterRole"),
|
||||
Long: clusterRoleBindingLong,
|
||||
Example: clusterRoleBindingExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
o.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.ClusterRoleBindingV1GeneratorName)
|
||||
cmd.Flags().String("clusterrole", "", i18n.T("ClusterRole this ClusterRoleBinding should reference"))
|
||||
cmd.MarkFlagCustom("clusterrole", "__kubectl_get_resource_clusterrole")
|
||||
cmd.Flags().StringArray("user", []string{}, "Usernames to bind to the clusterrole")
|
||||
cmd.Flags().StringArray("group", []string{}, "Groups to bind to the clusterrole")
|
||||
cmd.Flags().StringArray("serviceaccount", []string{}, "Service accounts to bind to the clusterrole, in the format <namespace>:<name>")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *ClusterRoleBindingOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.ClusterRoleBindingV1GeneratorName:
|
||||
generator = &generateversioned.ClusterRoleBindingGeneratorV1{
|
||||
Name: name,
|
||||
ClusterRole: cmdutil.GetFlagString(cmd, "clusterrole"),
|
||||
Users: cmdutil.GetFlagStringArray(cmd, "user"),
|
||||
Groups: cmdutil.GetFlagStringArray(cmd, "group"),
|
||||
ServiceAccounts: cmdutil.GetFlagStringArray(cmd, "serviceaccount"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in ClusterRoleBindingOpts instance
|
||||
func (o *ClusterRoleBindingOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
147
pkg/cmd/create/create_clusterrolebinding_test.go
Normal file
147
pkg/cmd/create/create_clusterrolebinding_test.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
rbac "k8s.io/api/rbac/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateClusterRoleBinding(t *testing.T) {
|
||||
expectBinding := &rbac.ClusterRoleBinding{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "fake-binding",
|
||||
},
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ClusterRoleBinding",
|
||||
APIVersion: "rbac.authorization.k8s.io/v1beta1",
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: rbac.GroupName,
|
||||
Kind: "ClusterRole",
|
||||
Name: "fake-clusterrole",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: rbac.UserKind,
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Name: "fake-user",
|
||||
},
|
||||
{
|
||||
Kind: rbac.GroupKind,
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Name: "fake-group",
|
||||
},
|
||||
{
|
||||
Kind: rbac.ServiceAccountKind,
|
||||
Namespace: "fake-namespace",
|
||||
Name: "fake-account",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
ns := scheme.Codecs
|
||||
|
||||
info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
|
||||
encoder := ns.EncoderForVersion(info.Serializer, groupVersion)
|
||||
decoder := ns.DecoderToVersion(info.Serializer, groupVersion)
|
||||
|
||||
tf.Client = &ClusterRoleBindingRESTClient{
|
||||
RESTClient: &fake.RESTClient{
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/clusterrolebindings" && m == "POST":
|
||||
bodyBits, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("TestCreateClusterRoleBinding error: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if obj, _, err := decoder.Decode(bodyBits, nil, &rbac.ClusterRoleBinding{}); err == nil {
|
||||
if !reflect.DeepEqual(obj.(*rbac.ClusterRoleBinding), expectBinding) {
|
||||
t.Fatalf("TestCreateClusterRoleBinding: expected:\n%#v\nsaw:\n%#v", expectBinding, obj.(*rbac.ClusterRoleBinding))
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("TestCreateClusterRoleBinding error, could not decode the request body into rbac.ClusterRoleBinding object: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseBinding := &rbac.ClusterRoleBinding{}
|
||||
responseBinding.Name = "fake-binding"
|
||||
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseBinding))))}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
expectedOutput := "clusterrolebinding.rbac.authorization.k8s.io/" + expectBinding.Name + "\n"
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateClusterRoleBinding(tf, ioStreams)
|
||||
cmd.Flags().Set("clusterrole", "fake-clusterrole")
|
||||
cmd.Flags().Set("user", "fake-user")
|
||||
cmd.Flags().Set("group", "fake-group")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("serviceaccount", "fake-namespace:fake-account")
|
||||
cmd.Run(cmd, []string{"fake-binding"})
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("TestCreateClusterRoleBinding: expected %v\n but got %v\n", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
type ClusterRoleBindingRESTClient struct {
|
||||
*fake.RESTClient
|
||||
}
|
||||
|
||||
func (c *ClusterRoleBindingRESTClient) Post() *restclient.Request {
|
||||
config := restclient.ContentConfig{
|
||||
ContentType: runtime.ContentTypeJSON,
|
||||
NegotiatedSerializer: c.NegotiatedSerializer,
|
||||
}
|
||||
|
||||
info, _ := runtime.SerializerInfoForMediaType(c.NegotiatedSerializer.SupportedMediaTypes(), runtime.ContentTypeJSON)
|
||||
serializers := restclient.Serializers{
|
||||
Encoder: c.NegotiatedSerializer.EncoderForVersion(info.Serializer, schema.GroupVersion{Group: "rbac.authorization.k8s.io", Version: "v1beta1"}),
|
||||
Decoder: c.NegotiatedSerializer.DecoderToVersion(info.Serializer, schema.GroupVersion{Group: "rbac.authorization.k8s.io", Version: "v1beta1"}),
|
||||
}
|
||||
if info.StreamSerializer != nil {
|
||||
serializers.StreamingSerializer = info.StreamSerializer.Serializer
|
||||
serializers.Framer = info.StreamSerializer.Framer
|
||||
}
|
||||
return restclient.NewRequest(c, "POST", &url.URL{Host: "localhost"}, c.VersionedAPIPath, config, serializers, nil, nil, 0)
|
||||
}
|
||||
123
pkg/cmd/create/create_configmap.go
Normal file
123
pkg/cmd/create/create_configmap.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
configMapLong = templates.LongDesc(i18n.T(`
|
||||
Create a configmap based on a file, directory, or specified literal value.
|
||||
|
||||
A single configmap may package one or more key/value pairs.
|
||||
|
||||
When creating a configmap based on a file, the key will default to the basename of the file, and the value will
|
||||
default to the file content. If the basename is an invalid key, you may specify an alternate key.
|
||||
|
||||
When creating a configmap based on a directory, each file whose basename is a valid key in the directory will be
|
||||
packaged into the configmap. Any directory entries except regular files are ignored (e.g. subdirectories,
|
||||
symlinks, devices, pipes, etc).`))
|
||||
|
||||
configMapExample = templates.Examples(i18n.T(`
|
||||
# Create a new configmap named my-config based on folder bar
|
||||
kubectl create configmap my-config --from-file=path/to/bar
|
||||
|
||||
# Create a new configmap named my-config with specified keys instead of file basenames on disk
|
||||
kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt
|
||||
|
||||
# Create a new configmap named my-config with key1=config1 and key2=config2
|
||||
kubectl create configmap my-config --from-literal=key1=config1 --from-literal=key2=config2
|
||||
|
||||
# Create a new configmap named my-config from the key=value pairs in the file
|
||||
kubectl create configmap my-config --from-file=path/to/bar
|
||||
|
||||
# Create a new configmap named my-config from an env file
|
||||
kubectl create configmap my-config --from-env-file=path/to/bar.env`))
|
||||
)
|
||||
|
||||
// ConfigMapOpts holds properties for create configmap sub-command
|
||||
type ConfigMapOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateConfigMap initializes and returns ConfigMapOpts
|
||||
func NewCmdCreateConfigMap(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &ConfigMapOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "configmap NAME [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Aliases: []string{"cm"},
|
||||
Short: i18n.T("Create a configmap from a local file, directory or literal value"),
|
||||
Long: configMapLong,
|
||||
Example: configMapExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.ConfigMapV1GeneratorName)
|
||||
cmd.Flags().StringSlice("from-file", []string{}, "Key file can be specified using its file path, in which case file basename will be used as configmap key, or optionally with a key and file path, in which case the given key will be used. Specifying a directory will iterate each named file in the directory whose basename is a valid configmap key.")
|
||||
cmd.Flags().StringArray("from-literal", []string{}, "Specify a key and literal value to insert in configmap (i.e. mykey=somevalue)")
|
||||
cmd.Flags().String("from-env-file", "", "Specify the path to a file to read lines of key=val pairs to create a configmap (i.e. a Docker .env file).")
|
||||
cmd.Flags().Bool("append-hash", false, "Append a hash of the configmap to its name.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *ConfigMapOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.ConfigMapV1GeneratorName:
|
||||
generator = &generateversioned.ConfigMapGeneratorV1{
|
||||
Name: name,
|
||||
FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"),
|
||||
LiteralSources: cmdutil.GetFlagStringArray(cmd, "from-literal"),
|
||||
EnvFileSource: cmdutil.GetFlagString(cmd, "from-env-file"),
|
||||
AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run performs the execution of 'create' sub command options
|
||||
func (o *ConfigMapOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
61
pkg/cmd/create/create_configmap_test.go
Normal file
61
pkg/cmd/create/create_configmap_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateConfigMap(t *testing.T) {
|
||||
configMap := &v1.ConfigMap{}
|
||||
configMap.Name = "my-configmap"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/configmaps" && m == "POST":
|
||||
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, configMap)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateConfigMap(tf, ioStreams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{configMap.Name})
|
||||
expectedOutput := "configmap/" + configMap.Name + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
205
pkg/cmd/create/create_cronjob.go
Normal file
205
pkg/cmd/create/create_cronjob.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
batchv1beta1client "k8s.io/client-go/kubernetes/typed/batch/v1beta1"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
cronjobLong = templates.LongDesc(`
|
||||
Create a cronjob with the specified name.`)
|
||||
|
||||
cronjobExample = templates.Examples(`
|
||||
# Create a cronjob
|
||||
kubectl create cronjob my-job --image=busybox
|
||||
|
||||
# Create a cronjob with command
|
||||
kubectl create cronjob my-job --image=busybox -- date
|
||||
|
||||
# Create a cronjob with schedule
|
||||
kubectl create cronjob test-job --image=busybox --schedule="*/1 * * * *"`)
|
||||
)
|
||||
|
||||
type CreateCronJobOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
|
||||
PrintObj func(obj runtime.Object) error
|
||||
|
||||
Name string
|
||||
Image string
|
||||
Schedule string
|
||||
Command []string
|
||||
Restart string
|
||||
|
||||
Namespace string
|
||||
Client batchv1beta1client.BatchV1beta1Interface
|
||||
DryRun bool
|
||||
Builder *resource.Builder
|
||||
Cmd *cobra.Command
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
func NewCreateCronJobOptions(ioStreams genericclioptions.IOStreams) *CreateCronJobOptions {
|
||||
return &CreateCronJobOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdCreateCronJob is a command to to create CronJobs.
|
||||
func NewCmdCreateCronJob(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewCreateCronJobOptions(ioStreams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "cronjob NAME --image=image --schedule='0/5 * * * ?' -- [COMMAND] [args...]",
|
||||
Aliases: []string{"cj"},
|
||||
Short: cronjobLong,
|
||||
Long: cronjobLong,
|
||||
Example: cronjobExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringVar(&o.Image, "image", o.Image, "Image name to run.")
|
||||
cmd.Flags().StringVar(&o.Schedule, "schedule", o.Schedule, "A schedule in the Cron format the job should be run with.")
|
||||
cmd.Flags().StringVar(&o.Restart, "restart", o.Restart, "job's restart policy. supported values: OnFailure, Never")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *CreateCronJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Name = name
|
||||
if len(args) > 1 {
|
||||
o.Command = args[1:]
|
||||
}
|
||||
if len(o.Restart) == 0 {
|
||||
o.Restart = "OnFailure"
|
||||
}
|
||||
|
||||
clientConfig, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Client, err = batchv1beta1client.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Builder = f.NewBuilder()
|
||||
o.Cmd = cmd
|
||||
|
||||
o.DryRun = cmdutil.GetDryRunFlag(cmd)
|
||||
if o.DryRun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = func(obj runtime.Object) error {
|
||||
return printer.PrintObj(obj, o.Out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CreateCronJobOptions) Validate() error {
|
||||
if len(o.Image) == 0 {
|
||||
return fmt.Errorf("--image must be specified")
|
||||
}
|
||||
if len(o.Schedule) == 0 {
|
||||
return fmt.Errorf("--schedule must be specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CreateCronJobOptions) Run() error {
|
||||
var cronjob *batchv1beta1.CronJob
|
||||
cronjob = o.createCronJob()
|
||||
|
||||
if !o.DryRun {
|
||||
var err error
|
||||
cronjob, err = o.Client.CronJobs(o.Namespace).Create(cronjob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create cronjob: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return o.PrintObj(cronjob)
|
||||
}
|
||||
|
||||
func (o *CreateCronJobOptions) createCronJob() *batchv1beta1.CronJob {
|
||||
return &batchv1beta1.CronJob{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1beta1.SchemeGroupVersion.String(), Kind: "CronJob"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: o.Name,
|
||||
},
|
||||
Spec: batchv1beta1.CronJobSpec{
|
||||
Schedule: o.Schedule,
|
||||
JobTemplate: batchv1beta1.JobTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: o.Name,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: o.Name,
|
||||
Image: o.Image,
|
||||
Command: o.Command,
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicy(o.Restart),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
121
pkg/cmd/create/create_cronjob_test.go
Normal file
121
pkg/cmd/create/create_cronjob_test.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestCreateCronJob(t *testing.T) {
|
||||
cronjobName := "test-job"
|
||||
tests := map[string]struct {
|
||||
image string
|
||||
command []string
|
||||
schedule string
|
||||
restart string
|
||||
expected *batchv1beta1.CronJob
|
||||
}{
|
||||
"just image and OnFailure restart policy": {
|
||||
image: "busybox",
|
||||
schedule: "0/5 * * * ?",
|
||||
restart: "OnFailure",
|
||||
expected: &batchv1beta1.CronJob{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1beta1.SchemeGroupVersion.String(), Kind: "CronJob"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cronjobName,
|
||||
},
|
||||
Spec: batchv1beta1.CronJobSpec{
|
||||
Schedule: "0/5 * * * ?",
|
||||
JobTemplate: batchv1beta1.JobTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cronjobName,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: cronjobName,
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyOnFailure,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"image, command , schedule and Never restart policy": {
|
||||
image: "busybox",
|
||||
command: []string{"date"},
|
||||
schedule: "0/5 * * * ?",
|
||||
restart: "Never",
|
||||
expected: &batchv1beta1.CronJob{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1beta1.SchemeGroupVersion.String(), Kind: "CronJob"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cronjobName,
|
||||
},
|
||||
Spec: batchv1beta1.CronJobSpec{
|
||||
Schedule: "0/5 * * * ?",
|
||||
JobTemplate: batchv1beta1.JobTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cronjobName,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: cronjobName,
|
||||
Image: "busybox",
|
||||
Command: []string{"date"},
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
o := &CreateCronJobOptions{
|
||||
Name: cronjobName,
|
||||
Image: tc.image,
|
||||
Command: tc.command,
|
||||
Schedule: tc.schedule,
|
||||
Restart: tc.restart,
|
||||
}
|
||||
cronjob := o.createCronJob()
|
||||
if !apiequality.Semantic.DeepEqual(cronjob, tc.expected) {
|
||||
t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, cronjob)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
155
pkg/cmd/create/create_deployment.go
Normal file
155
pkg/cmd/create/create_deployment.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
deploymentLong = templates.LongDesc(i18n.T(`
|
||||
Create a deployment with the specified name.`))
|
||||
|
||||
deploymentExample = templates.Examples(i18n.T(`
|
||||
# Create a new deployment named my-dep that runs the busybox image.
|
||||
kubectl create deployment my-dep --image=busybox`))
|
||||
)
|
||||
|
||||
// DeploymentOpts is returned by NewCmdCreateDeployment
|
||||
type DeploymentOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateDeployment is a macro command to create a new deployment.
|
||||
// This command is better known to users as `kubectl create deployment`.
|
||||
// Note that this command overlaps significantly with the `kubectl run` command.
|
||||
func NewCmdCreateDeployment(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &DeploymentOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "deployment NAME --image=image [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Aliases: []string{"deploy"},
|
||||
Short: i18n.T("Create a deployment with the specified name."),
|
||||
Long: deploymentLong,
|
||||
Example: deploymentExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, "")
|
||||
cmd.Flags().StringSlice("image", []string{}, "Image name to run.")
|
||||
cmd.MarkFlagRequired("image")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// generatorFromName returns the appropriate StructuredGenerator based on the
|
||||
// generatorName. If the generatorName is unrecognized, then return (nil,
|
||||
// false).
|
||||
func generatorFromName(
|
||||
generatorName string,
|
||||
imageNames []string,
|
||||
deploymentName string,
|
||||
) (generate.StructuredGenerator, bool) {
|
||||
|
||||
switch generatorName {
|
||||
case generateversioned.DeploymentBasicAppsV1GeneratorName:
|
||||
generator := &generateversioned.DeploymentBasicAppsGeneratorV1{
|
||||
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
|
||||
Name: deploymentName,
|
||||
Images: imageNames,
|
||||
},
|
||||
}
|
||||
return generator, true
|
||||
|
||||
case generateversioned.DeploymentBasicAppsV1Beta1GeneratorName:
|
||||
generator := &generateversioned.DeploymentBasicAppsGeneratorV1Beta1{
|
||||
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
|
||||
Name: deploymentName,
|
||||
Images: imageNames,
|
||||
},
|
||||
}
|
||||
return generator, true
|
||||
|
||||
case generateversioned.DeploymentBasicV1Beta1GeneratorName:
|
||||
generator := &generateversioned.DeploymentBasicGeneratorV1{
|
||||
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
|
||||
Name: deploymentName,
|
||||
Images: imageNames,
|
||||
},
|
||||
}
|
||||
return generator, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Complete completes all the options
|
||||
func (o *DeploymentOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientset, err := f.KubernetesClientSet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
generatorName := cmdutil.GetFlagString(cmd, "generator")
|
||||
|
||||
if len(generatorName) == 0 {
|
||||
generatorName = generateversioned.DeploymentBasicAppsV1GeneratorName
|
||||
generatorNameTemp, err := generateversioned.FallbackGeneratorNameIfNecessary(generatorName, clientset.Discovery(), o.CreateSubcommandOptions.ErrOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if generatorNameTemp != generatorName {
|
||||
cmdutil.Warning(o.CreateSubcommandOptions.ErrOut, generatorName, generatorNameTemp)
|
||||
} else {
|
||||
generatorName = generatorNameTemp
|
||||
}
|
||||
}
|
||||
|
||||
imageNames := cmdutil.GetFlagStringSlice(cmd, "image")
|
||||
generator, ok := generatorFromName(generatorName, imageNames, name)
|
||||
if !ok {
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run performs the execution of 'create deployment' sub command
|
||||
func (o *DeploymentOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
155
pkg/cmd/create/create_deployment_test.go
Normal file
155
pkg/cmd/create/create_deployment_test.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func Test_generatorFromName(t *testing.T) {
|
||||
const (
|
||||
nonsenseName = "not-a-real-generator-name"
|
||||
basicName = generateversioned.DeploymentBasicV1Beta1GeneratorName
|
||||
basicAppsV1Beta1Name = generateversioned.DeploymentBasicAppsV1Beta1GeneratorName
|
||||
basicAppsV1Name = generateversioned.DeploymentBasicAppsV1GeneratorName
|
||||
deploymentName = "deployment-name"
|
||||
)
|
||||
imageNames := []string{"image-1", "image-2"}
|
||||
|
||||
generator, ok := generatorFromName(nonsenseName, imageNames, deploymentName)
|
||||
assert.Nil(t, generator)
|
||||
assert.False(t, ok)
|
||||
|
||||
generator, ok = generatorFromName(basicName, imageNames, deploymentName)
|
||||
assert.True(t, ok)
|
||||
|
||||
{
|
||||
expectedGenerator := &generateversioned.DeploymentBasicGeneratorV1{
|
||||
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
|
||||
Name: deploymentName,
|
||||
Images: imageNames,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedGenerator, generator)
|
||||
}
|
||||
|
||||
generator, ok = generatorFromName(basicAppsV1Beta1Name, imageNames, deploymentName)
|
||||
assert.True(t, ok)
|
||||
|
||||
{
|
||||
expectedGenerator := &generateversioned.DeploymentBasicAppsGeneratorV1Beta1{
|
||||
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
|
||||
Name: deploymentName,
|
||||
Images: imageNames,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedGenerator, generator)
|
||||
}
|
||||
|
||||
generator, ok = generatorFromName(basicAppsV1Name, imageNames, deploymentName)
|
||||
assert.True(t, ok)
|
||||
|
||||
{
|
||||
expectedGenerator := &generateversioned.DeploymentBasicAppsGeneratorV1{
|
||||
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
|
||||
Name: deploymentName,
|
||||
Images: imageNames,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedGenerator, generator)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDeployment(t *testing.T) {
|
||||
depName := "jonny-dep"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
ns := scheme.Codecs
|
||||
fakeDiscovery := "{\"kind\":\"APIResourceList\",\"apiVersion\":\"v1\",\"groupVersion\":\"apps/v1\",\"resources\":[{\"name\":\"deployments\",\"singularName\":\"\",\"namespaced\":true,\"kind\":\"Deployment\",\"verbs\":[\"create\",\"delete\",\"deletecollection\",\"get\",\"list\",\"patch\",\"update\",\"watch\"],\"shortNames\":[\"deploy\"],\"categories\":[\"all\"]}]}"
|
||||
tf.Client = &fake.RESTClient{
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer([]byte(fakeDiscovery))),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{}
|
||||
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateDeployment(tf, ioStreams)
|
||||
cmd.Flags().Set("dry-run", "true")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("image", "hollywood/jonny.depp:v2")
|
||||
cmd.Run(cmd, []string{depName})
|
||||
expectedOutput := "deployment.apps/" + depName + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDeploymentNoImage(t *testing.T) {
|
||||
depName := "jonny-dep"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
ns := scheme.Codecs
|
||||
fakeDiscovery := "{\"kind\":\"APIResourceList\",\"apiVersion\":\"v1\",\"groupVersion\":\"apps/v1\",\"resources\":[{\"name\":\"deployments\",\"singularName\":\"\",\"namespaced\":true,\"kind\":\"Deployment\",\"verbs\":[\"create\",\"delete\",\"deletecollection\",\"get\",\"list\",\"patch\",\"update\",\"watch\"],\"shortNames\":[\"deploy\"],\"categories\":[\"all\"]}]}"
|
||||
tf.Client = &fake.RESTClient{
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer([]byte(fakeDiscovery))),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{}
|
||||
|
||||
ioStreams := genericclioptions.NewTestIOStreamsDiscard()
|
||||
cmd := NewCmdCreateDeployment(tf, ioStreams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
options := &DeploymentOpts{
|
||||
CreateSubcommandOptions: &CreateSubcommandOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
DryRun: true,
|
||||
IOStreams: ioStreams,
|
||||
},
|
||||
}
|
||||
|
||||
err := options.Complete(tf, cmd, []string{depName})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
err = options.Run()
|
||||
assert.Error(t, err, "at least one image must be specified")
|
||||
}
|
||||
248
pkg/cmd/create/create_job.go
Normal file
248
pkg/cmd/create/create_job.go
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
batchv1client "k8s.io/client-go/kubernetes/typed/batch/v1"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
jobLong = templates.LongDesc(i18n.T(`
|
||||
Create a job with the specified name.`))
|
||||
|
||||
jobExample = templates.Examples(i18n.T(`
|
||||
# Create a job
|
||||
kubectl create job my-job --image=busybox
|
||||
|
||||
# Create a job with command
|
||||
kubectl create job my-job --image=busybox -- date
|
||||
|
||||
# Create a job from a CronJob named "a-cronjob"
|
||||
kubectl create job test-job --from=cronjob/a-cronjob`))
|
||||
)
|
||||
|
||||
// CreateJobOptions is the command line options for 'create job'
|
||||
type CreateJobOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
|
||||
PrintObj func(obj runtime.Object) error
|
||||
|
||||
Name string
|
||||
Image string
|
||||
From string
|
||||
Command []string
|
||||
|
||||
Namespace string
|
||||
Client batchv1client.BatchV1Interface
|
||||
DryRun bool
|
||||
Builder *resource.Builder
|
||||
Cmd *cobra.Command
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewCreateJobOptions initializes and returns new CreateJobOptions instance
|
||||
func NewCreateJobOptions(ioStreams genericclioptions.IOStreams) *CreateJobOptions {
|
||||
return &CreateJobOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdCreateJob is a command to ease creating Jobs from CronJobs.
|
||||
func NewCmdCreateJob(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewCreateJobOptions(ioStreams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "job NAME --image=image [--from=cronjob/name] -- [COMMAND] [args...]",
|
||||
Short: jobLong,
|
||||
Long: jobLong,
|
||||
Example: jobExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringVar(&o.Image, "image", o.Image, "Image name to run.")
|
||||
cmd.Flags().StringVar(&o.From, "from", o.From, "The name of the resource to create a Job from (only cronjob is supported).")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *CreateJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Name = name
|
||||
if len(args) > 1 {
|
||||
o.Command = args[1:]
|
||||
}
|
||||
|
||||
clientConfig, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Client, err = batchv1client.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Builder = f.NewBuilder()
|
||||
o.Cmd = cmd
|
||||
|
||||
o.DryRun = cmdutil.GetDryRunFlag(cmd)
|
||||
if o.DryRun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = func(obj runtime.Object) error {
|
||||
return printer.PrintObj(obj, o.Out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure provided values and valid Job options
|
||||
func (o *CreateJobOptions) Validate() error {
|
||||
if (len(o.Image) == 0 && len(o.From) == 0) || (len(o.Image) != 0 && len(o.From) != 0) {
|
||||
return fmt.Errorf("either --image or --from must be specified")
|
||||
}
|
||||
if o.Command != nil && len(o.Command) != 0 && len(o.From) != 0 {
|
||||
return fmt.Errorf("cannot specify --from and command")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run performs the execution of 'create job' sub command
|
||||
func (o *CreateJobOptions) Run() error {
|
||||
var job *batchv1.Job
|
||||
if len(o.Image) > 0 {
|
||||
job = o.createJob()
|
||||
} else {
|
||||
infos, err := o.Builder.
|
||||
Unstructured().
|
||||
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||
ResourceTypeOrNameArgs(false, o.From).
|
||||
Flatten().
|
||||
Latest().
|
||||
Do().
|
||||
Infos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(infos) != 1 {
|
||||
return fmt.Errorf("from must be an existing cronjob")
|
||||
}
|
||||
|
||||
uncastVersionedObj, err := scheme.Scheme.ConvertToVersion(infos[0].Object, batchv1beta1.SchemeGroupVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("from must be an existing cronjob: %v", err)
|
||||
}
|
||||
cronJob, ok := uncastVersionedObj.(*batchv1beta1.CronJob)
|
||||
if !ok {
|
||||
return fmt.Errorf("from must be an existing cronjob")
|
||||
}
|
||||
|
||||
job = o.createJobFromCronJob(cronJob)
|
||||
}
|
||||
if !o.DryRun {
|
||||
var err error
|
||||
job, err = o.Client.Jobs(o.Namespace).Create(job)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create job: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return o.PrintObj(job)
|
||||
}
|
||||
|
||||
func (o *CreateJobOptions) createJob() *batchv1.Job {
|
||||
return &batchv1.Job{
|
||||
// this is ok because we know exactly how we want to be serialized
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: o.Name,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: o.Name,
|
||||
Image: o.Image,
|
||||
Command: o.Command,
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (o *CreateJobOptions) createJobFromCronJob(cronJob *batchv1beta1.CronJob) *batchv1.Job {
|
||||
annotations := make(map[string]string)
|
||||
annotations["cronjob.kubernetes.io/instantiate"] = "manual"
|
||||
for k, v := range cronJob.Spec.JobTemplate.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
|
||||
return &batchv1.Job{
|
||||
// this is ok because we know exactly how we want to be serialized
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: o.Name,
|
||||
Annotations: annotations,
|
||||
Labels: cronJob.Spec.JobTemplate.Labels,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
*metav1.NewControllerRef(cronJob, appsv1.SchemeGroupVersion.WithKind("CronJob")),
|
||||
},
|
||||
},
|
||||
Spec: cronJob.Spec.JobTemplate.Spec,
|
||||
}
|
||||
}
|
||||
196
pkg/cmd/create/create_job_test.go
Normal file
196
pkg/cmd/create/create_job_test.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
apps "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestCreateJobValidation(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
image string
|
||||
command []string
|
||||
from string
|
||||
expected string
|
||||
}{
|
||||
"empty flags": {
|
||||
expected: "--image or --from must be specified",
|
||||
},
|
||||
"both image and from specified": {
|
||||
image: "my-image",
|
||||
from: "cronjob/xyz",
|
||||
expected: "--image or --from must be specified",
|
||||
},
|
||||
"from and command specified": {
|
||||
from: "cronjob/xyz",
|
||||
command: []string{"test", "command"},
|
||||
expected: "cannot specify --from and command",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
o := &CreateJobOptions{
|
||||
Image: tc.image,
|
||||
From: tc.from,
|
||||
Command: tc.command,
|
||||
}
|
||||
|
||||
err := o.Validate()
|
||||
if err != nil && !strings.Contains(err.Error(), tc.expected) {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateJob(t *testing.T) {
|
||||
jobName := "test-job"
|
||||
tests := map[string]struct {
|
||||
image string
|
||||
command []string
|
||||
expected *batchv1.Job
|
||||
}{
|
||||
"just image": {
|
||||
image: "busybox",
|
||||
expected: &batchv1.Job{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jobName,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: jobName,
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"image and command": {
|
||||
image: "busybox",
|
||||
command: []string{"date"},
|
||||
expected: &batchv1.Job{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jobName,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: jobName,
|
||||
Image: "busybox",
|
||||
Command: []string{"date"},
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
o := &CreateJobOptions{
|
||||
Name: jobName,
|
||||
Image: tc.image,
|
||||
Command: tc.command,
|
||||
}
|
||||
job := o.createJob()
|
||||
if !apiequality.Semantic.DeepEqual(job, tc.expected) {
|
||||
t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, job)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateJobFromCronJob(t *testing.T) {
|
||||
jobName := "test-job"
|
||||
cronJob := &batchv1beta1.CronJob{
|
||||
Spec: batchv1beta1.CronJobSpec{
|
||||
JobTemplate: batchv1beta1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Image: "test-image"},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tests := map[string]struct {
|
||||
from *batchv1beta1.CronJob
|
||||
expected *batchv1.Job
|
||||
}{
|
||||
"from CronJob": {
|
||||
from: cronJob,
|
||||
expected: &batchv1.Job{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jobName,
|
||||
Annotations: map[string]string{"cronjob.kubernetes.io/instantiate": "manual"},
|
||||
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cronJob, apps.SchemeGroupVersion.WithKind("CronJob"))},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Image: "test-image"},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
o := &CreateJobOptions{
|
||||
Name: jobName,
|
||||
}
|
||||
job := o.createJobFromCronJob(tc.from)
|
||||
|
||||
if !apiequality.Semantic.DeepEqual(job, tc.expected) {
|
||||
t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, job)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
93
pkg/cmd/create/create_namespace.go
Normal file
93
pkg/cmd/create/create_namespace.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2015 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
namespaceLong = templates.LongDesc(i18n.T(`
|
||||
Create a namespace with the specified name.`))
|
||||
|
||||
namespaceExample = templates.Examples(i18n.T(`
|
||||
# Create a new namespace named my-namespace
|
||||
kubectl create namespace my-namespace`))
|
||||
)
|
||||
|
||||
// NamespaceOpts is the options for 'create namespare' sub command
|
||||
type NamespaceOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateNamespace is a macro command to create a new namespace
|
||||
func NewCmdCreateNamespace(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &NamespaceOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "namespace NAME [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Aliases: []string{"ns"},
|
||||
Short: i18n.T("Create a namespace with the specified name"),
|
||||
Long: namespaceLong,
|
||||
Example: namespaceExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.NamespaceV1GeneratorName)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *NamespaceOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.NamespaceV1GeneratorName:
|
||||
generator = &generateversioned.NamespaceGeneratorV1{Name: name}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in NamespaceOpts instance
|
||||
func (o *NamespaceOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
61
pkg/cmd/create/create_namespace_test.go
Normal file
61
pkg/cmd/create/create_namespace_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2014 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 create
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateNamespace(t *testing.T) {
|
||||
namespaceObject := &v1.Namespace{}
|
||||
namespaceObject.Name = "my-namespace"
|
||||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces" && m == "POST":
|
||||
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, namespaceObject)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateNamespace(tf, ioStreams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{namespaceObject.Name})
|
||||
expectedOutput := "namespace/" + namespaceObject.Name + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
112
pkg/cmd/create/create_pdb.go
Normal file
112
pkg/cmd/create/create_pdb.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
pdbLong = templates.LongDesc(i18n.T(`
|
||||
Create a pod disruption budget with the specified name, selector, and desired minimum available pods`))
|
||||
|
||||
pdbExample = templates.Examples(i18n.T(`
|
||||
# Create a pod disruption budget named my-pdb that will select all pods with the app=rails label
|
||||
# and require at least one of them being available at any point in time.
|
||||
kubectl create poddisruptionbudget my-pdb --selector=app=rails --min-available=1
|
||||
|
||||
# Create a pod disruption budget named my-pdb that will select all pods with the app=nginx label
|
||||
# and require at least half of the pods selected to be available at any point in time.
|
||||
kubectl create pdb my-pdb --selector=app=nginx --min-available=50%`))
|
||||
)
|
||||
|
||||
// PodDisruptionBudgetOpts holds the command-line options for poddisruptionbudget sub command
|
||||
type PodDisruptionBudgetOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreatePodDisruptionBudget is a macro command to create a new pod disruption budget.
|
||||
func NewCmdCreatePodDisruptionBudget(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &PodDisruptionBudgetOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "poddisruptionbudget NAME --selector=SELECTOR --min-available=N [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Aliases: []string{"pdb"},
|
||||
Short: i18n.T("Create a pod disruption budget with the specified name."),
|
||||
Long: pdbLong,
|
||||
Example: pdbExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.PodDisruptionBudgetV2GeneratorName)
|
||||
|
||||
cmd.Flags().String("min-available", "", i18n.T("The minimum number or percentage of available pods this budget requires."))
|
||||
cmd.Flags().String("max-unavailable", "", i18n.T("The maximum number or percentage of unavailable pods this budget requires."))
|
||||
cmd.Flags().String("selector", "", i18n.T("A label selector to use for this budget. Only equality-based selector requirements are supported."))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *PodDisruptionBudgetOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.PodDisruptionBudgetV1GeneratorName:
|
||||
generator = &generateversioned.PodDisruptionBudgetV1Generator{
|
||||
Name: name,
|
||||
MinAvailable: cmdutil.GetFlagString(cmd, "min-available"),
|
||||
Selector: cmdutil.GetFlagString(cmd, "selector"),
|
||||
}
|
||||
case generateversioned.PodDisruptionBudgetV2GeneratorName:
|
||||
generator = &generateversioned.PodDisruptionBudgetV2Generator{
|
||||
Name: name,
|
||||
MinAvailable: cmdutil.GetFlagString(cmd, "min-available"),
|
||||
MaxUnavailable: cmdutil.GetFlagString(cmd, "max-unavailable"),
|
||||
Selector: cmdutil.GetFlagString(cmd, "selector"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in PodDisruptionBudgetOpts instance
|
||||
func (o *PodDisruptionBudgetOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
85
pkg/cmd/create/create_pdb_test.go
Normal file
85
pkg/cmd/create/create_pdb_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreatePdb(t *testing.T) {
|
||||
pdbName := "my-pdb"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "policy", Version: "v1beta1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(&bytes.Buffer{}),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{}
|
||||
|
||||
outputFormat := "name"
|
||||
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreatePodDisruptionBudget(tf, ioStreams)
|
||||
cmd.Flags().Set("min-available", "1")
|
||||
cmd.Flags().Set("selector", "app=rails")
|
||||
cmd.Flags().Set("dry-run", "true")
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
|
||||
printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme)
|
||||
printFlags.OutputFormat = &outputFormat
|
||||
|
||||
options := &PodDisruptionBudgetOpts{
|
||||
CreateSubcommandOptions: &CreateSubcommandOptions{
|
||||
PrintFlags: printFlags,
|
||||
Name: pdbName,
|
||||
IOStreams: ioStreams,
|
||||
},
|
||||
}
|
||||
err := options.Complete(tf, cmd, []string{pdbName})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
err = options.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedOutput := "poddisruptionbudget.policy/" + pdbName + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
110
pkg/cmd/create/create_priorityclass.go
Normal file
110
pkg/cmd/create/create_priorityclass.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
pcLong = templates.LongDesc(i18n.T(`
|
||||
Create a priorityclass with the specified name, value, globalDefault and description`))
|
||||
|
||||
pcExample = templates.Examples(i18n.T(`
|
||||
# Create a priorityclass named high-priority
|
||||
kubectl create priorityclass high-priority --value=1000 --description="high priority"
|
||||
|
||||
# Create a priorityclass named default-priority that considered as the global default priority
|
||||
kubectl create priorityclass default-priority --value=1000 --global-default=true --description="default priority"
|
||||
|
||||
# Create a priorityclass named high-priority that can not preempt pods with lower priority
|
||||
kubectl create priorityclass high-priority --value=1000 --description="high priority" --preemption-policy="Never"`))
|
||||
)
|
||||
|
||||
// PriorityClassOpts holds the options for 'create priorityclass' sub command
|
||||
type PriorityClassOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreatePriorityClass is a macro command to create a new priorityClass.
|
||||
func NewCmdCreatePriorityClass(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &PriorityClassOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "priorityclass NAME --value=VALUE --global-default=BOOL [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Aliases: []string{"pc"},
|
||||
Short: i18n.T("Create a priorityclass with the specified name."),
|
||||
Long: pcLong,
|
||||
Example: pcExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.PriorityClassV1GeneratorName)
|
||||
|
||||
cmd.Flags().Int32("value", 0, i18n.T("the value of this priority class."))
|
||||
cmd.Flags().Bool("global-default", false, i18n.T("global-default specifies whether this PriorityClass should be considered as the default priority."))
|
||||
cmd.Flags().String("description", "", i18n.T("description is an arbitrary string that usually provides guidelines on when this priority class should be used."))
|
||||
cmd.Flags().String("preemption-policy", "", i18n.T("preemption-policy is the policy for preempting pods with lower priority."))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *PriorityClassOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.PriorityClassV1GeneratorName:
|
||||
generator = &generateversioned.PriorityClassV1Generator{
|
||||
Name: name,
|
||||
Value: cmdutil.GetFlagInt32(cmd, "value"),
|
||||
GlobalDefault: cmdutil.GetFlagBool(cmd, "global-default"),
|
||||
Description: cmdutil.GetFlagString(cmd, "description"),
|
||||
PreemptionPolicy: apiv1.PreemptionPolicy(cmdutil.GetFlagString(cmd, "preemption-policy")),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in the PriorityClassOpts instance
|
||||
func (o *PriorityClassOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
87
pkg/cmd/create/create_priorityclass_test.go
Normal file
87
pkg/cmd/create/create_priorityclass_test.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreatePriorityClass(t *testing.T) {
|
||||
pcName := "my-pc"
|
||||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Group: "scheduling.k8s.io", Version: "v1beta1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(&bytes.Buffer{}),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{}
|
||||
|
||||
outputFormat := "name"
|
||||
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreatePriorityClass(tf, ioStreams)
|
||||
cmd.Flags().Set("value", "1000")
|
||||
cmd.Flags().Set("global-default", "true")
|
||||
cmd.Flags().Set("description", "my priority")
|
||||
cmd.Flags().Set("dry-run", "true")
|
||||
cmd.Flags().Set("output", outputFormat)
|
||||
cmd.Flags().Set("preemption-policy", "Never")
|
||||
|
||||
printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme)
|
||||
printFlags.OutputFormat = &outputFormat
|
||||
|
||||
options := &PriorityClassOpts{
|
||||
CreateSubcommandOptions: &CreateSubcommandOptions{
|
||||
PrintFlags: printFlags,
|
||||
Name: pcName,
|
||||
IOStreams: ioStreams,
|
||||
},
|
||||
}
|
||||
err := options.Complete(tf, cmd, []string{pcName})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
err = options.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedOutput := "priorityclass.scheduling.k8s.io/" + pcName + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
101
pkg/cmd/create/create_quota.go
Normal file
101
pkg/cmd/create/create_quota.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
quotaLong = templates.LongDesc(i18n.T(`
|
||||
Create a resourcequota with the specified name, hard limits and optional scopes`))
|
||||
|
||||
quotaExample = templates.Examples(i18n.T(`
|
||||
# Create a new resourcequota named my-quota
|
||||
kubectl create quota my-quota --hard=cpu=1,memory=1G,pods=2,services=3,replicationcontrollers=2,resourcequotas=1,secrets=5,persistentvolumeclaims=10
|
||||
|
||||
# Create a new resourcequota named best-effort
|
||||
kubectl create quota best-effort --hard=pods=100 --scopes=BestEffort`))
|
||||
)
|
||||
|
||||
// QuotaOpts holds the command-line options for 'create quota' sub command
|
||||
type QuotaOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateQuota is a macro command to create a new quota
|
||||
func NewCmdCreateQuota(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &QuotaOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "quota NAME [--hard=key1=value1,key2=value2] [--scopes=Scope1,Scope2] [--dry-run=bool]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Aliases: []string{"resourcequota"},
|
||||
Short: i18n.T("Create a quota with the specified name."),
|
||||
Long: quotaLong,
|
||||
Example: quotaExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.ResourceQuotaV1GeneratorName)
|
||||
cmd.Flags().String("hard", "", i18n.T("A comma-delimited set of resource=quantity pairs that define a hard limit."))
|
||||
cmd.Flags().String("scopes", "", i18n.T("A comma-delimited set of quota scopes that must all match each object tracked by the quota."))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *QuotaOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.ResourceQuotaV1GeneratorName:
|
||||
generator = &generateversioned.ResourceQuotaGeneratorV1{
|
||||
Name: name,
|
||||
Hard: cmdutil.GetFlagString(cmd, "hard"),
|
||||
Scopes: cmdutil.GetFlagString(cmd, "scopes"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in QuotaOpts instance
|
||||
func (o *QuotaOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
68
pkg/cmd/create/create_quota_test.go
Normal file
68
pkg/cmd/create/create_quota_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateQuota(t *testing.T) {
|
||||
resourceQuotaObject := &v1.ResourceQuota{}
|
||||
resourceQuotaObject.Name = "my-quota"
|
||||
|
||||
tests := map[string]struct {
|
||||
flags []string
|
||||
expectedOutput string
|
||||
}{
|
||||
"single resource": {
|
||||
flags: []string{"--hard=cpu=1"},
|
||||
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
|
||||
},
|
||||
"single resource with a scope": {
|
||||
flags: []string{"--hard=cpu=1", "--scopes=BestEffort"},
|
||||
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
|
||||
},
|
||||
"multiple resources": {
|
||||
flags: []string{"--hard=cpu=1,pods=42", "--scopes=BestEffort"},
|
||||
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
|
||||
},
|
||||
"single resource with multiple scopes": {
|
||||
flags: []string{"--hard=cpu=1", "--scopes=BestEffort,NotTerminating"},
|
||||
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateQuota(tf, ioStreams)
|
||||
cmd.Flags().Parse(test.flags)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{resourceQuotaObject.Name})
|
||||
|
||||
if buf.String() != test.expectedOutput {
|
||||
t.Errorf("%s: expected output: %s, but got: %s", name, test.expectedOutput, buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
402
pkg/cmd/create/create_role.go
Normal file
402
pkg/cmd/create/create_role.go
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
clientgorbacv1 "k8s.io/client-go/kubernetes/typed/rbac/v1"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
roleLong = templates.LongDesc(i18n.T(`
|
||||
Create a role with single rule.`))
|
||||
|
||||
roleExample = templates.Examples(i18n.T(`
|
||||
# Create a Role named "pod-reader" that allows user to perform "get", "watch" and "list" on pods
|
||||
kubectl create role pod-reader --verb=get --verb=list --verb=watch --resource=pods
|
||||
|
||||
# Create a Role named "pod-reader" with ResourceName specified
|
||||
kubectl create role pod-reader --verb=get --resource=pods --resource-name=readablepod --resource-name=anotherpod
|
||||
|
||||
# Create a Role named "foo" with API Group specified
|
||||
kubectl create role foo --verb=get,list,watch --resource=rs.extensions
|
||||
|
||||
# Create a Role named "foo" with SubResource specified
|
||||
kubectl create role foo --verb=get,list,watch --resource=pods,pods/status`))
|
||||
|
||||
// Valid resource verb list for validation.
|
||||
validResourceVerbs = []string{"*", "get", "delete", "list", "create", "update", "patch", "watch", "proxy", "deletecollection", "use", "bind", "escalate", "impersonate"}
|
||||
|
||||
// Specialized verbs and GroupResources
|
||||
specialVerbs = map[string][]schema.GroupResource{
|
||||
"use": {
|
||||
{
|
||||
Group: "extensions",
|
||||
Resource: "podsecuritypolicies",
|
||||
},
|
||||
},
|
||||
"bind": {
|
||||
{
|
||||
Group: "rbac.authorization.k8s.io",
|
||||
Resource: "roles",
|
||||
},
|
||||
{
|
||||
Group: "rbac.authorization.k8s.io",
|
||||
Resource: "clusterroles",
|
||||
},
|
||||
},
|
||||
"escalate": {
|
||||
{
|
||||
Group: "rbac.authorization.k8s.io",
|
||||
Resource: "roles",
|
||||
},
|
||||
{
|
||||
Group: "rbac.authorization.k8s.io",
|
||||
Resource: "clusterroles",
|
||||
},
|
||||
},
|
||||
"impersonate": {
|
||||
{
|
||||
Group: "",
|
||||
Resource: "users",
|
||||
},
|
||||
{
|
||||
Group: "",
|
||||
Resource: "serviceaccounts",
|
||||
},
|
||||
{
|
||||
Group: "",
|
||||
Resource: "groups",
|
||||
},
|
||||
{
|
||||
Group: "authentication.k8s.io",
|
||||
Resource: "userextras",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// ResourceOptions holds the related options for '--resource' option
|
||||
type ResourceOptions struct {
|
||||
Group string
|
||||
Resource string
|
||||
SubResource string
|
||||
}
|
||||
|
||||
// CreateRoleOptions holds the options for 'create role' sub command
|
||||
type CreateRoleOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
|
||||
Name string
|
||||
Verbs []string
|
||||
Resources []ResourceOptions
|
||||
ResourceNames []string
|
||||
|
||||
DryRun bool
|
||||
OutputFormat string
|
||||
Namespace string
|
||||
Client clientgorbacv1.RbacV1Interface
|
||||
Mapper meta.RESTMapper
|
||||
PrintObj func(obj runtime.Object) error
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewCreateRoleOptions returns an initialized CreateRoleOptions instance
|
||||
func NewCreateRoleOptions(ioStreams genericclioptions.IOStreams) *CreateRoleOptions {
|
||||
return &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
|
||||
IOStreams: ioStreams,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCmdCreateRole returnns an initialized Command instance for 'create role' sub command
|
||||
func NewCmdCreateRole(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewCreateRoleOptions(ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "role NAME --verb=verb --resource=resource.group/subresource [--resource-name=resourcename] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: roleLong,
|
||||
Long: roleLong,
|
||||
Example: roleExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunCreateRole())
|
||||
},
|
||||
}
|
||||
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringSliceVar(&o.Verbs, "verb", o.Verbs, "Verb that applies to the resources contained in the rule")
|
||||
cmd.Flags().StringSlice("resource", []string{}, "Resource that the rule applies to")
|
||||
cmd.Flags().StringArrayVar(&o.ResourceNames, "resource-name", o.ResourceNames, "Resource in the white list that the rule applies to, repeat this flag for multiple items")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *CreateRoleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Name = name
|
||||
|
||||
// Remove duplicate verbs.
|
||||
verbs := []string{}
|
||||
for _, v := range o.Verbs {
|
||||
// VerbAll respresents all kinds of verbs.
|
||||
if v == "*" {
|
||||
verbs = []string{"*"}
|
||||
break
|
||||
}
|
||||
if !arrayContains(verbs, v) {
|
||||
verbs = append(verbs, v)
|
||||
}
|
||||
}
|
||||
o.Verbs = verbs
|
||||
|
||||
// Support resource.group pattern. If no API Group specified, use "" as core API Group.
|
||||
// e.g. --resource=pods,deployments.extensions
|
||||
resources := cmdutil.GetFlagStringSlice(cmd, "resource")
|
||||
for _, r := range resources {
|
||||
sections := strings.SplitN(r, "/", 2)
|
||||
|
||||
resource := &ResourceOptions{}
|
||||
if len(sections) == 2 {
|
||||
resource.SubResource = sections[1]
|
||||
}
|
||||
|
||||
parts := strings.SplitN(sections[0], ".", 2)
|
||||
if len(parts) == 2 {
|
||||
resource.Group = parts[1]
|
||||
}
|
||||
resource.Resource = parts[0]
|
||||
|
||||
if resource.Resource == "*" && len(parts) == 1 && len(sections) == 1 {
|
||||
o.Resources = []ResourceOptions{*resource}
|
||||
break
|
||||
}
|
||||
|
||||
o.Resources = append(o.Resources, *resource)
|
||||
}
|
||||
|
||||
// Remove duplicate resource names.
|
||||
resourceNames := []string{}
|
||||
for _, n := range o.ResourceNames {
|
||||
if !arrayContains(resourceNames, n) {
|
||||
resourceNames = append(resourceNames, n)
|
||||
}
|
||||
}
|
||||
o.ResourceNames = resourceNames
|
||||
|
||||
// Complete other options for Run.
|
||||
o.Mapper, err = f.ToRESTMapper()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.DryRun = cmdutil.GetDryRunFlag(cmd)
|
||||
o.OutputFormat = cmdutil.GetFlagString(cmd, "output")
|
||||
|
||||
if o.DryRun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.PrintObj = func(obj runtime.Object) error {
|
||||
return printer.PrintObj(obj, o.Out)
|
||||
}
|
||||
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientset, err := f.KubernetesClientSet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Client = clientset.RbacV1()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure there is no discrepency in provided option values
|
||||
func (o *CreateRoleOptions) Validate() error {
|
||||
if o.Name == "" {
|
||||
return fmt.Errorf("name must be specified")
|
||||
}
|
||||
|
||||
// validate verbs.
|
||||
if len(o.Verbs) == 0 {
|
||||
return fmt.Errorf("at least one verb must be specified")
|
||||
}
|
||||
|
||||
for _, v := range o.Verbs {
|
||||
if !arrayContains(validResourceVerbs, v) {
|
||||
return fmt.Errorf("invalid verb: '%s'", v)
|
||||
}
|
||||
}
|
||||
|
||||
// validate resources.
|
||||
if len(o.Resources) == 0 {
|
||||
return fmt.Errorf("at least one resource must be specified")
|
||||
}
|
||||
|
||||
return o.validateResource()
|
||||
}
|
||||
|
||||
func (o *CreateRoleOptions) validateResource() error {
|
||||
for _, r := range o.Resources {
|
||||
if len(r.Resource) == 0 {
|
||||
return fmt.Errorf("resource must be specified if apiGroup/subresource specified")
|
||||
}
|
||||
if r.Resource == "*" {
|
||||
return nil
|
||||
}
|
||||
|
||||
resource := schema.GroupVersionResource{Resource: r.Resource, Group: r.Group}
|
||||
groupVersionResource, err := o.Mapper.ResourceFor(schema.GroupVersionResource{Resource: r.Resource, Group: r.Group})
|
||||
if err == nil {
|
||||
resource = groupVersionResource
|
||||
}
|
||||
|
||||
for _, v := range o.Verbs {
|
||||
if groupResources, ok := specialVerbs[v]; ok {
|
||||
match := false
|
||||
for _, extra := range groupResources {
|
||||
if resource.Resource == extra.Resource && resource.Group == extra.Group {
|
||||
match = true
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return fmt.Errorf("can not perform '%s' on '%s' in group '%s'", v, resource.Resource, resource.Group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunCreateRole performs the execution of 'create role' sub command
|
||||
func (o *CreateRoleOptions) RunCreateRole() error {
|
||||
role := &rbacv1.Role{
|
||||
// this is ok because we know exactly how we want to be serialized
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "Role"},
|
||||
}
|
||||
role.Name = o.Name
|
||||
rules, err := generateResourcePolicyRules(o.Mapper, o.Verbs, o.Resources, o.ResourceNames, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
role.Rules = rules
|
||||
|
||||
// Create role.
|
||||
if !o.DryRun {
|
||||
role, err = o.Client.Roles(o.Namespace).Create(role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return o.PrintObj(role)
|
||||
}
|
||||
|
||||
func arrayContains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func generateResourcePolicyRules(mapper meta.RESTMapper, verbs []string, resources []ResourceOptions, resourceNames []string, nonResourceURLs []string) ([]rbacv1.PolicyRule, error) {
|
||||
// groupResourceMapping is a apigroup-resource map. The key of this map is api group, while the value
|
||||
// is a string array of resources under this api group.
|
||||
// E.g. groupResourceMapping = {"extensions": ["replicasets", "deployments"], "batch":["jobs"]}
|
||||
groupResourceMapping := map[string][]string{}
|
||||
|
||||
// This loop does the following work:
|
||||
// 1. Constructs groupResourceMapping based on input resources.
|
||||
// 2. Prevents pointing to non-existent resources.
|
||||
// 3. Transfers resource short name to long name. E.g. rs.extensions is transferred to replicasets.extensions
|
||||
for _, r := range resources {
|
||||
resource := schema.GroupVersionResource{Resource: r.Resource, Group: r.Group}
|
||||
groupVersionResource, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: r.Resource, Group: r.Group})
|
||||
if err == nil {
|
||||
resource = groupVersionResource
|
||||
}
|
||||
|
||||
if len(r.SubResource) > 0 {
|
||||
resource.Resource = resource.Resource + "/" + r.SubResource
|
||||
}
|
||||
if !arrayContains(groupResourceMapping[resource.Group], resource.Resource) {
|
||||
groupResourceMapping[resource.Group] = append(groupResourceMapping[resource.Group], resource.Resource)
|
||||
}
|
||||
}
|
||||
|
||||
// Create separate rule for each of the api group.
|
||||
rules := []rbacv1.PolicyRule{}
|
||||
for _, g := range sets.StringKeySet(groupResourceMapping).List() {
|
||||
rule := rbacv1.PolicyRule{}
|
||||
rule.Verbs = verbs
|
||||
rule.Resources = groupResourceMapping[g]
|
||||
rule.APIGroups = []string{g}
|
||||
rule.ResourceNames = resourceNames
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
if len(nonResourceURLs) > 0 {
|
||||
rule := rbacv1.PolicyRule{}
|
||||
rule.Verbs = verbs
|
||||
rule.NonResourceURLs = nonResourceURLs
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
673
pkg/cmd/create/create_role_test.go
Normal file
673
pkg/cmd/create/create_role_test.go
Normal file
|
|
@ -0,0 +1,673 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateRole(t *testing.T) {
|
||||
roleName := "my-role"
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{}
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
tests := map[string]struct {
|
||||
verbs string
|
||||
resources string
|
||||
resourceNames string
|
||||
expectedRole *rbac.Role
|
||||
}{
|
||||
"test-duplicate-resources": {
|
||||
verbs: "get,watch,list",
|
||||
resources: "pods,pods",
|
||||
expectedRole: &rbac.Role{
|
||||
TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: roleName,
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
Resources: []string{"pods"},
|
||||
APIGroups: []string{""},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-subresources": {
|
||||
verbs: "get,watch,list",
|
||||
resources: "replicasets/scale",
|
||||
expectedRole: &rbac.Role{
|
||||
TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: roleName,
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
Resources: []string{"replicasets/scale"},
|
||||
APIGroups: []string{"extensions"},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-subresources-with-apigroup": {
|
||||
verbs: "get,watch,list",
|
||||
resources: "replicasets.extensions/scale",
|
||||
expectedRole: &rbac.Role{
|
||||
TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: roleName,
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
Resources: []string{"replicasets/scale"},
|
||||
APIGroups: []string{"extensions"},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-valid-case-with-multiple-apigroups": {
|
||||
verbs: "get,watch,list",
|
||||
resources: "pods,deployments.extensions",
|
||||
expectedRole: &rbac.Role{
|
||||
TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: roleName,
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
Resources: []string{"pods"},
|
||||
APIGroups: []string{""},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"get", "watch", "list"},
|
||||
Resources: []string{"deployments"},
|
||||
APIGroups: []string{"extensions"},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateRole(tf, ioStreams)
|
||||
cmd.Flags().Set("dry-run", "true")
|
||||
cmd.Flags().Set("output", "yaml")
|
||||
cmd.Flags().Set("verb", test.verbs)
|
||||
cmd.Flags().Set("resource", test.resources)
|
||||
if test.resourceNames != "" {
|
||||
cmd.Flags().Set("resource-name", test.resourceNames)
|
||||
}
|
||||
cmd.Run(cmd, []string{roleName})
|
||||
actual := &rbac.Role{}
|
||||
if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), buf.Bytes(), actual); err != nil {
|
||||
t.Log(string(buf.Bytes()))
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(test.expectedRole, actual) {
|
||||
t.Errorf("%s", diff.ObjectReflectDiff(test.expectedRole, actual))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tests := map[string]struct {
|
||||
roleOptions *CreateRoleOptions
|
||||
expectErr bool
|
||||
}{
|
||||
"test-missing-name": {
|
||||
roleOptions: &CreateRoleOptions{},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-verb": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-resource": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"get"},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-resource-existing-apigroup": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-missing-resource-existing-subresource": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
SubResource: "scale",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-verb": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"invalid-verb"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-nonresource-verb": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"post"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-special-verb": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"use"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-mix-verbs": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"impersonate", "use"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "userextras",
|
||||
SubResource: "scopes",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-special-verb-with-wrong-apigroup": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"impersonate"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "userextras",
|
||||
SubResource: "scopes",
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-invalid-resource": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "invalid-resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-resource-name-with-multiple-resources": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"get"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
{
|
||||
Resource: "deployments",
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{"foo"},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-valid-case": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "role-binder",
|
||||
Verbs: []string{"get", "list", "bind"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "roles",
|
||||
Group: "rbac.authorization.k8s.io",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{"foo"},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-valid-case-with-subresource": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"get", "list"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "replicasets",
|
||||
SubResource: "scale",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{"bar"},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-valid-case-with-additional-resource": {
|
||||
roleOptions: &CreateRoleOptions{
|
||||
Name: "my-role",
|
||||
Verbs: []string{"impersonate"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "userextras",
|
||||
SubResource: "scopes",
|
||||
Group: "authentication.k8s.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
var err error
|
||||
test.roleOptions.Mapper, err = tf.ToRESTMapper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = test.roleOptions.Validate()
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("%s: expect error happens but validate passes.", name)
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete(t *testing.T) {
|
||||
roleName := "my-role"
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.Client = &fake.RESTClient{}
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
defaultTestResources := "pods,deployments.extensions"
|
||||
|
||||
tests := map[string]struct {
|
||||
params []string
|
||||
resources string
|
||||
roleOptions *CreateRoleOptions
|
||||
expected *CreateRoleOptions
|
||||
expectErr bool
|
||||
}{
|
||||
"test-missing-name": {
|
||||
params: []string{},
|
||||
resources: defaultTestResources,
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
"test-duplicate-verbs": {
|
||||
params: []string{roleName},
|
||||
resources: defaultTestResources,
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
Name: roleName,
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"watch",
|
||||
"list",
|
||||
"get",
|
||||
},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"watch",
|
||||
"list",
|
||||
},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
Group: "",
|
||||
},
|
||||
{
|
||||
Resource: "deployments",
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-verball": {
|
||||
params: []string{roleName},
|
||||
resources: defaultTestResources,
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
Name: roleName,
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"watch",
|
||||
"list",
|
||||
"*",
|
||||
},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
Group: "",
|
||||
},
|
||||
{
|
||||
Resource: "deployments",
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-allresource": {
|
||||
params: []string{roleName},
|
||||
resources: "*,pods",
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created"),
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "*",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-allresource-subresource": {
|
||||
params: []string{roleName},
|
||||
resources: "*/scale,pods",
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created"),
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "*",
|
||||
SubResource: "scale",
|
||||
},
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-allresrouce-allgroup": {
|
||||
params: []string{roleName},
|
||||
resources: "*.*,pods",
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created"),
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "*",
|
||||
Group: "*",
|
||||
},
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-allresource-allgroup-subresource": {
|
||||
params: []string{roleName},
|
||||
resources: "*.*/scale,pods",
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created"),
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "*",
|
||||
Group: "*",
|
||||
SubResource: "scale",
|
||||
},
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-allresource-specificgroup": {
|
||||
params: []string{roleName},
|
||||
resources: "*.extensions,pods",
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created"),
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "*",
|
||||
Group: "extensions",
|
||||
},
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-allresource-specificgroup-subresource": {
|
||||
params: []string{roleName},
|
||||
resources: "*.extensions/scale,pods",
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created"),
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "*",
|
||||
Group: "extensions",
|
||||
SubResource: "scale",
|
||||
},
|
||||
{
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-duplicate-resourcenames": {
|
||||
params: []string{roleName},
|
||||
resources: defaultTestResources,
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
ResourceNames: []string{"foo", "foo"},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
Group: "",
|
||||
},
|
||||
{
|
||||
Resource: "deployments",
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{"foo"},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
"test-valid-complete-case": {
|
||||
params: []string{roleName},
|
||||
resources: defaultTestResources,
|
||||
roleOptions: &CreateRoleOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
ResourceNames: []string{"foo"},
|
||||
},
|
||||
expected: &CreateRoleOptions{
|
||||
Name: roleName,
|
||||
Verbs: []string{"*"},
|
||||
Resources: []ResourceOptions{
|
||||
{
|
||||
Resource: "pods",
|
||||
Group: "",
|
||||
},
|
||||
{
|
||||
Resource: "deployments",
|
||||
Group: "extensions",
|
||||
},
|
||||
},
|
||||
ResourceNames: []string{"foo"},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
cmd := NewCmdCreateRole(tf, genericclioptions.NewTestIOStreamsDiscard())
|
||||
cmd.Flags().Set("resource", test.resources)
|
||||
|
||||
err := test.roleOptions.Complete(tf, cmd, test.params)
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", name, err)
|
||||
}
|
||||
|
||||
if test.expectErr {
|
||||
if err != nil {
|
||||
continue
|
||||
} else {
|
||||
t.Errorf("%s: expect error happens but test passes.", name)
|
||||
}
|
||||
}
|
||||
|
||||
if test.roleOptions.Name != test.expected.Name {
|
||||
t.Errorf("%s:\nexpected name:\n%#v\nsaw name:\n%#v", name, test.expected.Name, test.roleOptions.Name)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.roleOptions.Verbs, test.expected.Verbs) {
|
||||
t.Errorf("%s:\nexpected verbs:\n%#v\nsaw verbs:\n%#v", name, test.expected.Verbs, test.roleOptions.Verbs)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.roleOptions.Resources, test.expected.Resources) {
|
||||
t.Errorf("%s:\nexpected resources:\n%#v\nsaw resources:\n%#v", name, test.expected.Resources, test.roleOptions.Resources)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.roleOptions.ResourceNames, test.expected.ResourceNames) {
|
||||
t.Errorf("%s:\nexpected resource names:\n%#v\nsaw resource names:\n%#v", name, test.expected.ResourceNames, test.roleOptions.ResourceNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
103
pkg/cmd/create/create_rolebinding.go
Normal file
103
pkg/cmd/create/create_rolebinding.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
roleBindingLong = templates.LongDesc(i18n.T(`
|
||||
Create a RoleBinding for a particular Role or ClusterRole.`))
|
||||
|
||||
roleBindingExample = templates.Examples(i18n.T(`
|
||||
# Create a RoleBinding for user1, user2, and group1 using the admin ClusterRole
|
||||
kubectl create rolebinding admin --clusterrole=admin --user=user1 --user=user2 --group=group1`))
|
||||
)
|
||||
|
||||
// RoleBindingOpts holds the options for 'create rolebinding' sub command
|
||||
type RoleBindingOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateRoleBinding returns an initialized Command instance for 'create rolebinding' sub command
|
||||
func NewCmdCreateRoleBinding(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := &RoleBindingOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rolebinding NAME --clusterrole=NAME|--role=NAME [--user=username] [--group=groupname] [--serviceaccount=namespace:serviceaccountname] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a RoleBinding for a particular Role or ClusterRole"),
|
||||
Long: roleBindingLong,
|
||||
Example: roleBindingExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
o.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.RoleBindingV1GeneratorName)
|
||||
cmd.Flags().String("clusterrole", "", i18n.T("ClusterRole this RoleBinding should reference"))
|
||||
cmd.Flags().String("role", "", i18n.T("Role this RoleBinding should reference"))
|
||||
cmd.Flags().StringArray("user", []string{}, "Usernames to bind to the role")
|
||||
cmd.Flags().StringArray("group", []string{}, "Groups to bind to the role")
|
||||
cmd.Flags().StringArray("serviceaccount", []string{}, "Service accounts to bind to the role, in the format <namespace>:<name>")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *RoleBindingOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.RoleBindingV1GeneratorName:
|
||||
generator = &generateversioned.RoleBindingGeneratorV1{
|
||||
Name: name,
|
||||
ClusterRole: cmdutil.GetFlagString(cmd, "clusterrole"),
|
||||
Role: cmdutil.GetFlagString(cmd, "role"),
|
||||
Users: cmdutil.GetFlagStringArray(cmd, "user"),
|
||||
Groups: cmdutil.GetFlagStringArray(cmd, "group"),
|
||||
ServiceAccounts: cmdutil.GetFlagStringArray(cmd, "serviceaccount"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in RoleBindingOpts instance
|
||||
func (o *RoleBindingOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
144
pkg/cmd/create/create_rolebinding_test.go
Normal file
144
pkg/cmd/create/create_rolebinding_test.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
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 create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
var groupVersion = schema.GroupVersion{Group: "rbac.authorization.k8s.io", Version: "v1"}
|
||||
|
||||
func TestCreateRoleBinding(t *testing.T) {
|
||||
expectBinding := &rbac.RoleBinding{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "RoleBinding",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "fake-binding",
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: rbac.GroupName,
|
||||
Kind: "Role",
|
||||
Name: "fake-role",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: rbac.UserKind,
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Name: "fake-user",
|
||||
},
|
||||
{
|
||||
Kind: rbac.GroupKind,
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Name: "fake-group",
|
||||
},
|
||||
{
|
||||
Kind: rbac.ServiceAccountKind,
|
||||
Namespace: "fake-namespace",
|
||||
Name: "fake-account",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
ns := scheme.Codecs
|
||||
|
||||
info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
|
||||
encoder := ns.EncoderForVersion(info.Serializer, groupVersion)
|
||||
decoder := ns.DecoderToVersion(info.Serializer, groupVersion)
|
||||
|
||||
tf.Client = &RoleBindingRESTClient{
|
||||
RESTClient: &fake.RESTClient{
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/rolebindings" && m == "POST":
|
||||
bodyBits, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("TestCreateRoleBinding error: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if obj, _, err := decoder.Decode(bodyBits, nil, &rbac.RoleBinding{}); err == nil {
|
||||
if !reflect.DeepEqual(obj.(*rbac.RoleBinding), expectBinding) {
|
||||
t.Fatalf("TestCreateRoleBinding: expected:\n%#v\nsaw:\n%#v", expectBinding, obj.(*rbac.RoleBinding))
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("TestCreateRoleBinding error, could not decode the request body into rbac.RoleBinding object: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseBinding := &rbac.RoleBinding{}
|
||||
responseBinding.Name = "fake-binding"
|
||||
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseBinding))))}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdCreateRoleBinding(tf, genericclioptions.NewTestIOStreamsDiscard())
|
||||
cmd.Flags().Set("role", "fake-role")
|
||||
cmd.Flags().Set("user", "fake-user")
|
||||
cmd.Flags().Set("group", "fake-group")
|
||||
cmd.Flags().Set("serviceaccount", "fake-namespace:fake-account")
|
||||
cmd.Run(cmd, []string{"fake-binding"})
|
||||
}
|
||||
|
||||
type RoleBindingRESTClient struct {
|
||||
*fake.RESTClient
|
||||
}
|
||||
|
||||
func (c *RoleBindingRESTClient) Post() *restclient.Request {
|
||||
config := restclient.ContentConfig{
|
||||
ContentType: runtime.ContentTypeJSON,
|
||||
GroupVersion: &groupVersion,
|
||||
NegotiatedSerializer: c.NegotiatedSerializer,
|
||||
}
|
||||
|
||||
info, _ := runtime.SerializerInfoForMediaType(c.NegotiatedSerializer.SupportedMediaTypes(), runtime.ContentTypeJSON)
|
||||
serializers := restclient.Serializers{
|
||||
Encoder: c.NegotiatedSerializer.EncoderForVersion(info.Serializer, groupVersion),
|
||||
Decoder: c.NegotiatedSerializer.DecoderToVersion(info.Serializer, groupVersion),
|
||||
}
|
||||
if info.StreamSerializer != nil {
|
||||
serializers.StreamingSerializer = info.StreamSerializer.Serializer
|
||||
serializers.Framer = info.StreamSerializer.Framer
|
||||
}
|
||||
return restclient.NewRequest(c, "POST", &url.URL{Host: "localhost"}, c.VersionedAPIPath, config, serializers, nil, nil, 0)
|
||||
}
|
||||
322
pkg/cmd/create/create_secret.go
Normal file
322
pkg/cmd/create/create_secret.go
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
Copyright 2015 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// NewCmdCreateSecret groups subcommands to create various types of secrets
|
||||
func NewCmdCreateSecret(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "secret",
|
||||
Short: i18n.T("Create a secret using specified subcommand"),
|
||||
Long: "Create a secret using specified subcommand.",
|
||||
Run: cmdutil.DefaultSubCommandRun(ioStreams.ErrOut),
|
||||
}
|
||||
cmd.AddCommand(NewCmdCreateSecretDockerRegistry(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateSecretTLS(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateSecretGeneric(f, ioStreams))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var (
|
||||
secretLong = templates.LongDesc(i18n.T(`
|
||||
Create a secret based on a file, directory, or specified literal value.
|
||||
|
||||
A single secret may package one or more key/value pairs.
|
||||
|
||||
When creating a secret based on a file, the key will default to the basename of the file, and the value will
|
||||
default to the file content. If the basename is an invalid key or you wish to chose your own, you may specify
|
||||
an alternate key.
|
||||
|
||||
When creating a secret based on a directory, each file whose basename is a valid key in the directory will be
|
||||
packaged into the secret. Any directory entries except regular files are ignored (e.g. subdirectories,
|
||||
symlinks, devices, pipes, etc).`))
|
||||
|
||||
secretExample = templates.Examples(i18n.T(`
|
||||
# Create a new secret named my-secret with keys for each file in folder bar
|
||||
kubectl create secret generic my-secret --from-file=path/to/bar
|
||||
|
||||
# Create a new secret named my-secret with specified keys instead of names on disk
|
||||
kubectl create secret generic my-secret --from-file=ssh-privatekey=path/to/id_rsa --from-file=ssh-publickey=path/to/id_rsa.pub
|
||||
|
||||
# Create a new secret named my-secret with key1=supersecret and key2=topsecret
|
||||
kubectl create secret generic my-secret --from-literal=key1=supersecret --from-literal=key2=topsecret
|
||||
|
||||
# Create a new secret named my-secret using a combination of a file and a literal
|
||||
kubectl create secret generic my-secret --from-file=ssh-privatekey=path/to/id_rsa --from-literal=passphrase=topsecret
|
||||
|
||||
# Create a new secret named my-secret from an env file
|
||||
kubectl create secret generic my-secret --from-env-file=path/to/bar.env`))
|
||||
)
|
||||
|
||||
// SecretGenericOpts holds the options for 'create secret' sub command
|
||||
type SecretGenericOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateSecretGeneric is a command to create generic secrets from files, directories, or literal values
|
||||
func NewCmdCreateSecretGeneric(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &SecretGenericOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "generic NAME [--type=string] [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a secret from a local file, directory or literal value"),
|
||||
Long: secretLong,
|
||||
Example: secretExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.SecretV1GeneratorName)
|
||||
cmd.Flags().StringSlice("from-file", []string{}, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.")
|
||||
cmd.Flags().StringArray("from-literal", []string{}, "Specify a key and literal value to insert in secret (i.e. mykey=somevalue)")
|
||||
cmd.Flags().String("from-env-file", "", "Specify the path to a file to read lines of key=val pairs to create a secret (i.e. a Docker .env file).")
|
||||
cmd.Flags().String("type", "", i18n.T("The type of secret to create"))
|
||||
cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *SecretGenericOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.SecretV1GeneratorName:
|
||||
generator = &generateversioned.SecretGeneratorV1{
|
||||
Name: name,
|
||||
Type: cmdutil.GetFlagString(cmd, "type"),
|
||||
FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"),
|
||||
LiteralSources: cmdutil.GetFlagStringArray(cmd, "from-literal"),
|
||||
EnvFileSource: cmdutil.GetFlagString(cmd, "from-env-file"),
|
||||
AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in SecretGenericOpts instance
|
||||
func (o *SecretGenericOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
|
||||
var (
|
||||
secretForDockerRegistryLong = templates.LongDesc(i18n.T(`
|
||||
Create a new secret for use with Docker registries.
|
||||
|
||||
Dockercfg secrets are used to authenticate against Docker registries.
|
||||
|
||||
When using the Docker command line to push images, you can authenticate to a given registry by running:
|
||||
'$ docker login DOCKER_REGISTRY_SERVER --username=DOCKER_USER --password=DOCKER_PASSWORD --email=DOCKER_EMAIL'.
|
||||
|
||||
That produces a ~/.dockercfg file that is used by subsequent 'docker push' and 'docker pull' commands to
|
||||
authenticate to the registry. The email address is optional.
|
||||
|
||||
When creating applications, you may have a Docker registry that requires authentication. In order for the
|
||||
nodes to pull images on your behalf, they have to have the credentials. You can provide this information
|
||||
by creating a dockercfg secret and attaching it to your service account.`))
|
||||
|
||||
secretForDockerRegistryExample = templates.Examples(i18n.T(`
|
||||
# If you don't already have a .dockercfg file, you can create a dockercfg secret directly by using:
|
||||
kubectl create secret docker-registry my-secret --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL`))
|
||||
)
|
||||
|
||||
// SecretDockerRegistryOpts holds the options for 'create secret docker-registry' sub command
|
||||
type SecretDockerRegistryOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateSecretDockerRegistry is a macro command for creating secrets to work with Docker registries
|
||||
func NewCmdCreateSecretDockerRegistry(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &SecretDockerRegistryOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "docker-registry NAME --docker-username=user --docker-password=password --docker-email=email [--docker-server=string] [--from-literal=key1=value1] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a secret for use with a Docker registry"),
|
||||
Long: secretForDockerRegistryLong,
|
||||
Example: secretForDockerRegistryExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.SecretForDockerRegistryV1GeneratorName)
|
||||
cmd.Flags().String("docker-username", "", i18n.T("Username for Docker registry authentication"))
|
||||
cmd.MarkFlagRequired("docker-username")
|
||||
cmd.Flags().String("docker-password", "", i18n.T("Password for Docker registry authentication"))
|
||||
cmd.MarkFlagRequired("docker-password")
|
||||
cmd.Flags().String("docker-email", "", i18n.T("Email for Docker registry"))
|
||||
cmd.Flags().String("docker-server", "https://index.docker.io/v1/", i18n.T("Server location for Docker registry"))
|
||||
cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.")
|
||||
cmd.Flags().StringSlice("from-file", []string{}, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *SecretDockerRegistryOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fromFileFlag := cmdutil.GetFlagStringSlice(cmd, "from-file")
|
||||
if len(fromFileFlag) == 0 {
|
||||
requiredFlags := []string{"docker-username", "docker-password", "docker-server"}
|
||||
for _, requiredFlag := range requiredFlags {
|
||||
if value := cmdutil.GetFlagString(cmd, requiredFlag); len(value) == 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "flag %s is required", requiredFlag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.SecretForDockerRegistryV1GeneratorName:
|
||||
generator = &generateversioned.SecretForDockerRegistryGeneratorV1{
|
||||
Name: name,
|
||||
Username: cmdutil.GetFlagString(cmd, "docker-username"),
|
||||
Email: cmdutil.GetFlagString(cmd, "docker-email"),
|
||||
Password: cmdutil.GetFlagString(cmd, "docker-password"),
|
||||
Server: cmdutil.GetFlagString(cmd, "docker-server"),
|
||||
AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"),
|
||||
FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls CreateSubcommandOptions.Run in SecretDockerRegistryOpts instance
|
||||
func (o *SecretDockerRegistryOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
|
||||
var (
|
||||
secretForTLSLong = templates.LongDesc(i18n.T(`
|
||||
Create a TLS secret from the given public/private key pair.
|
||||
|
||||
The public/private key pair must exist before hand. The public key certificate must be .PEM encoded and match
|
||||
the given private key.`))
|
||||
|
||||
secretForTLSExample = templates.Examples(i18n.T(`
|
||||
# Create a new TLS secret named tls-secret with the given key pair:
|
||||
kubectl create secret tls tls-secret --cert=path/to/tls.cert --key=path/to/tls.key`))
|
||||
)
|
||||
|
||||
// SecretTLSOpts holds the options for 'create secret tls' sub command
|
||||
type SecretTLSOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateSecretTLS is a macro command for creating secrets to work with Docker registries
|
||||
func NewCmdCreateSecretTLS(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &SecretTLSOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "tls NAME --cert=path/to/cert/file --key=path/to/key/file [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a TLS secret"),
|
||||
Long: secretForTLSLong,
|
||||
Example: secretForTLSExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.SecretForTLSV1GeneratorName)
|
||||
cmd.Flags().String("cert", "", i18n.T("Path to PEM encoded public key certificate."))
|
||||
cmd.Flags().String("key", "", i18n.T("Path to private key associated with given certificate."))
|
||||
cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *SecretTLSOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
requiredFlags := []string{"cert", "key"}
|
||||
for _, requiredFlag := range requiredFlags {
|
||||
if value := cmdutil.GetFlagString(cmd, requiredFlag); len(value) == 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "flag %s is required", requiredFlag)
|
||||
}
|
||||
}
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.SecretForTLSV1GeneratorName:
|
||||
generator = &generateversioned.SecretForTLSGeneratorV1{
|
||||
Name: name,
|
||||
Key: cmdutil.GetFlagString(cmd, "key"),
|
||||
Cert: cmdutil.GetFlagString(cmd, "cert"),
|
||||
AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls CreateSubcommandOptions.Run in the SecretTLSOpts instance
|
||||
func (o *SecretTLSOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
101
pkg/cmd/create/create_secret_test.go
Normal file
101
pkg/cmd/create/create_secret_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
Copyright 2014 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 create
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateSecretGeneric(t *testing.T) {
|
||||
secretObject := &v1.Secret{
|
||||
Data: map[string][]byte{
|
||||
"password": []byte("includes,comma"),
|
||||
"username": []byte("test_user"),
|
||||
},
|
||||
}
|
||||
secretObject.Name = "my-secret"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/secrets" && m == "POST":
|
||||
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, secretObject)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateSecretGeneric(tf, ioStreams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("from-literal", "password=includes,comma")
|
||||
cmd.Flags().Set("from-literal", "username=test_user")
|
||||
cmd.Run(cmd, []string{secretObject.Name})
|
||||
expectedOutput := "secret/" + secretObject.Name + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSecretDockerRegistry(t *testing.T) {
|
||||
secretObject := &v1.Secret{}
|
||||
secretObject.Name = "my-secret"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/secrets" && m == "POST":
|
||||
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, secretObject)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateSecretDockerRegistry(tf, ioStreams)
|
||||
cmd.Flags().Set("docker-username", "test-user")
|
||||
cmd.Flags().Set("docker-password", "test-pass")
|
||||
cmd.Flags().Set("docker-email", "test-email")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{secretObject.Name})
|
||||
expectedOutput := "secret/" + secretObject.Name + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", buf.String(), expectedOutput)
|
||||
}
|
||||
}
|
||||
342
pkg/cmd/create/create_service.go
Normal file
342
pkg/cmd/create/create_service.go
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// NewCmdCreateService is a macro command to create a new service
|
||||
func NewCmdCreateService(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "service",
|
||||
Aliases: []string{"svc"},
|
||||
Short: i18n.T("Create a service using specified subcommand."),
|
||||
Long: "Create a service using specified subcommand.",
|
||||
Run: cmdutil.DefaultSubCommandRun(ioStreams.ErrOut),
|
||||
}
|
||||
cmd.AddCommand(NewCmdCreateServiceClusterIP(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateServiceNodePort(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateServiceLoadBalancer(f, ioStreams))
|
||||
cmd.AddCommand(NewCmdCreateServiceExternalName(f, ioStreams))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var (
|
||||
serviceClusterIPLong = templates.LongDesc(i18n.T(`
|
||||
Create a ClusterIP service with the specified name.`))
|
||||
|
||||
serviceClusterIPExample = templates.Examples(i18n.T(`
|
||||
# Create a new ClusterIP service named my-cs
|
||||
kubectl create service clusterip my-cs --tcp=5678:8080
|
||||
|
||||
# Create a new ClusterIP service named my-cs (in headless mode)
|
||||
kubectl create service clusterip my-cs --clusterip="None"`))
|
||||
)
|
||||
|
||||
func addPortFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringSlice("tcp", []string{}, "Port pairs can be specified as '<port>:<targetPort>'.")
|
||||
}
|
||||
|
||||
// ServiceClusterIPOpts holds the options for 'create service clusterip' sub command
|
||||
type ServiceClusterIPOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateServiceClusterIP is a command to create a ClusterIP service
|
||||
func NewCmdCreateServiceClusterIP(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &ServiceClusterIPOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "clusterip NAME [--tcp=<port>:<targetPort>] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a ClusterIP service."),
|
||||
Long: serviceClusterIPLong,
|
||||
Example: serviceClusterIPExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.ServiceClusterIPGeneratorV1Name)
|
||||
addPortFlags(cmd)
|
||||
cmd.Flags().String("clusterip", "", i18n.T("Assign your own ClusterIP or set to 'None' for a 'headless' service (no loadbalancing)."))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func errUnsupportedGenerator(cmd *cobra.Command, generatorName string) error {
|
||||
return cmdutil.UsageErrorf(cmd, "Generator %s not supported. ", generatorName)
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *ServiceClusterIPOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.ServiceClusterIPGeneratorV1Name:
|
||||
generator = &generateversioned.ServiceCommonGeneratorV1{
|
||||
Name: name,
|
||||
TCP: cmdutil.GetFlagStringSlice(cmd, "tcp"),
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
ClusterIP: cmdutil.GetFlagString(cmd, "clusterip"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in ServiceClusterIPOpts instance
|
||||
func (o *ServiceClusterIPOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
|
||||
var (
|
||||
serviceNodePortLong = templates.LongDesc(i18n.T(`
|
||||
Create a NodePort service with the specified name.`))
|
||||
|
||||
serviceNodePortExample = templates.Examples(i18n.T(`
|
||||
# Create a new NodePort service named my-ns
|
||||
kubectl create service nodeport my-ns --tcp=5678:8080`))
|
||||
)
|
||||
|
||||
// ServiceNodePortOpts holds the options for 'create service nodeport' sub command
|
||||
type ServiceNodePortOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateServiceNodePort is a macro command for creating a NodePort service
|
||||
func NewCmdCreateServiceNodePort(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &ServiceNodePortOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "nodeport NAME [--tcp=port:targetPort] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a NodePort service."),
|
||||
Long: serviceNodePortLong,
|
||||
Example: serviceNodePortExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.ServiceNodePortGeneratorV1Name)
|
||||
cmd.Flags().Int("node-port", 0, "Port used to expose the service on each node in a cluster.")
|
||||
addPortFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *ServiceNodePortOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.ServiceNodePortGeneratorV1Name:
|
||||
generator = &generateversioned.ServiceCommonGeneratorV1{
|
||||
Name: name,
|
||||
TCP: cmdutil.GetFlagStringSlice(cmd, "tcp"),
|
||||
Type: v1.ServiceTypeNodePort,
|
||||
ClusterIP: "",
|
||||
NodePort: cmdutil.GetFlagInt(cmd, "node-port"),
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in ServiceNodePortOpts instance
|
||||
func (o *ServiceNodePortOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
|
||||
var (
|
||||
serviceLoadBalancerLong = templates.LongDesc(i18n.T(`
|
||||
Create a LoadBalancer service with the specified name.`))
|
||||
|
||||
serviceLoadBalancerExample = templates.Examples(i18n.T(`
|
||||
# Create a new LoadBalancer service named my-lbs
|
||||
kubectl create service loadbalancer my-lbs --tcp=5678:8080`))
|
||||
)
|
||||
|
||||
// ServiceLoadBalancerOpts holds the options for 'create service loadbalancer' sub command
|
||||
type ServiceLoadBalancerOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateServiceLoadBalancer is a macro command for creating a LoadBalancer service
|
||||
func NewCmdCreateServiceLoadBalancer(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &ServiceLoadBalancerOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "loadbalancer NAME [--tcp=port:targetPort] [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create a LoadBalancer service."),
|
||||
Long: serviceLoadBalancerLong,
|
||||
Example: serviceLoadBalancerExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.ServiceLoadBalancerGeneratorV1Name)
|
||||
addPortFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *ServiceLoadBalancerOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.ServiceLoadBalancerGeneratorV1Name:
|
||||
generator = &generateversioned.ServiceCommonGeneratorV1{
|
||||
Name: name,
|
||||
TCP: cmdutil.GetFlagStringSlice(cmd, "tcp"),
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
ClusterIP: "",
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in ServiceLoadBalancerOpts instance
|
||||
func (o *ServiceLoadBalancerOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
|
||||
var (
|
||||
serviceExternalNameLong = templates.LongDesc(i18n.T(`
|
||||
Create an ExternalName service with the specified name.
|
||||
|
||||
ExternalName service references to an external DNS address instead of
|
||||
only pods, which will allow application authors to reference services
|
||||
that exist off platform, on other clusters, or locally.`))
|
||||
|
||||
serviceExternalNameExample = templates.Examples(i18n.T(`
|
||||
# Create a new ExternalName service named my-ns
|
||||
kubectl create service externalname my-ns --external-name bar.com`))
|
||||
)
|
||||
|
||||
// ServiceExternalNameOpts holds the options for 'create service externalname' sub command
|
||||
type ServiceExternalNameOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateServiceExternalName is a macro command for creating an ExternalName service
|
||||
func NewCmdCreateServiceExternalName(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &ServiceExternalNameOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "externalname NAME --external-name external.name [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Create an ExternalName service."),
|
||||
Long: serviceExternalNameLong,
|
||||
Example: serviceExternalNameExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.ServiceExternalNameGeneratorV1Name)
|
||||
addPortFlags(cmd)
|
||||
cmd.Flags().String("external-name", "", i18n.T("External name of service"))
|
||||
cmd.MarkFlagRequired("external-name")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *ServiceExternalNameOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.ServiceExternalNameGeneratorV1Name:
|
||||
generator = &generateversioned.ServiceCommonGeneratorV1{
|
||||
Name: name,
|
||||
Type: v1.ServiceTypeExternalName,
|
||||
ExternalName: cmdutil.GetFlagString(cmd, "external-name"),
|
||||
ClusterIP: "",
|
||||
}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in ServiceExternalNameOpts instance
|
||||
func (o *ServiceExternalNameOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
128
pkg/cmd/create/create_service_test.go
Normal file
128
pkg/cmd/create/create_service_test.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateService(t *testing.T) {
|
||||
service := &v1.Service{}
|
||||
service.Name = "my-service"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
negSer := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: negSer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/services" && m == "POST":
|
||||
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, service)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateServiceClusterIP(tf, ioStreams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("tcp", "8080:8000")
|
||||
cmd.Run(cmd, []string{service.Name})
|
||||
expectedOutput := "service/" + service.Name + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateServiceNodePort(t *testing.T) {
|
||||
service := &v1.Service{}
|
||||
service.Name = "my-node-port-service"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
negSer := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: negSer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/services" && m == http.MethodPost:
|
||||
return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, service)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateServiceNodePort(tf, ioStreams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("tcp", "30000:8000")
|
||||
cmd.Run(cmd, []string{service.Name})
|
||||
expectedOutput := "service/" + service.Name + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateServiceExternalName(t *testing.T) {
|
||||
service := &v1.Service{}
|
||||
service.Name = "my-external-name-service"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
negSer := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: negSer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/services" && m == http.MethodPost:
|
||||
return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, service)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateServiceExternalName(tf, ioStreams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("external-name", "name")
|
||||
cmd.Run(cmd, []string{service.Name})
|
||||
expectedOutput := "service/" + service.Name + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
92
pkg/cmd/create/create_serviceaccount.go
Normal file
92
pkg/cmd/create/create_serviceaccount.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/generate"
|
||||
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
serviceAccountLong = templates.LongDesc(i18n.T(`
|
||||
Create a service account with the specified name.`))
|
||||
|
||||
serviceAccountExample = templates.Examples(i18n.T(`
|
||||
# Create a new service account named my-service-account
|
||||
kubectl create serviceaccount my-service-account`))
|
||||
)
|
||||
|
||||
// ServiceAccountOpts holds the options for 'create serviceaccount' sub command
|
||||
type ServiceAccountOpts struct {
|
||||
CreateSubcommandOptions *CreateSubcommandOptions
|
||||
}
|
||||
|
||||
// NewCmdCreateServiceAccount is a macro command to create a new service account
|
||||
func NewCmdCreateServiceAccount(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := &ServiceAccountOpts{
|
||||
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "serviceaccount NAME [--dry-run]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Aliases: []string{"sa"},
|
||||
Short: i18n.T("Create a service account with the specified name"),
|
||||
Long: serviceAccountLong,
|
||||
Example: serviceAccountExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddGeneratorFlags(cmd, generateversioned.ServiceAccountV1GeneratorName)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes all the required options
|
||||
func (o *ServiceAccountOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
name, err := NameFromCommandArgs(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var generator generate.StructuredGenerator
|
||||
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
|
||||
case generateversioned.ServiceAccountV1GeneratorName:
|
||||
generator = &generateversioned.ServiceAccountGeneratorV1{Name: name}
|
||||
default:
|
||||
return errUnsupportedGenerator(cmd, generatorName)
|
||||
}
|
||||
|
||||
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
|
||||
}
|
||||
|
||||
// Run calls the CreateSubcommandOptions.Run in ServiceAccountOpts instance
|
||||
func (o *ServiceAccountOpts) Run() error {
|
||||
return o.CreateSubcommandOptions.Run()
|
||||
}
|
||||
61
pkg/cmd/create/create_serviceaccount_test.go
Normal file
61
pkg/cmd/create/create_serviceaccount_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2016 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 create
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateServiceAccount(t *testing.T) {
|
||||
serviceAccountObject := &v1.ServiceAccount{}
|
||||
serviceAccountObject.Name = "my-service-account"
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
ns := scheme.Codecs
|
||||
|
||||
tf.Client = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/serviceaccounts" && m == "POST":
|
||||
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, serviceAccountObject)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateServiceAccount(tf, ioStreams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{serviceAccountObject.Name})
|
||||
expectedOutput := "serviceaccount/" + serviceAccountObject.Name + "\n"
|
||||
if buf.String() != expectedOutput {
|
||||
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
|
||||
}
|
||||
}
|
||||
151
pkg/cmd/create/create_test.go
Normal file
151
pkg/cmd/create/create_test.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
Copyright 2014 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 create
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func TestExtraArgsFail(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
c := NewCmdCreate(f, genericclioptions.NewTestIOStreamsDiscard())
|
||||
options := CreateOptions{}
|
||||
if options.ValidateArgs(c, []string{"rc"}) == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateObject(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
rc.Items[0].Name = "redis-master-controller"
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost:
|
||||
return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreate(tf, ioStreams)
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/legacy/redis-master-controller.yaml")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
// uses the name from the file, not the response
|
||||
if buf.String() != "replicationcontroller/redis-master-controller\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateMultipleObject(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, svc, rc := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/services" && m == http.MethodPost:
|
||||
return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
|
||||
case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost:
|
||||
return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreate(tf, ioStreams)
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/legacy/redis-master-controller.yaml")
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/frontend-service.yaml")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
// Names should come from the REST response, NOT the files
|
||||
if buf.String() != "replicationcontroller/rc1\nservice/baz\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDirectory(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
rc.Items[0].Name = "name"
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
GroupVersion: schema.GroupVersion{Version: "v1"},
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost:
|
||||
return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreate(tf, ioStreams)
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/legacy")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "replicationcontroller/name\nreplicationcontroller/name\nreplicationcontroller/name\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
392
pkg/cmd/delete/delete.go
Normal file
392
pkg/cmd/delete/delete.go
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
Copyright 2014 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 delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/dynamic"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/rawhttp"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
cmdwait "k8s.io/kubernetes/pkg/kubectl/cmd/wait"
|
||||
)
|
||||
|
||||
var (
|
||||
deleteLong = templates.LongDesc(i18n.T(`
|
||||
Delete resources by filenames, stdin, resources and names, or by resources and label selector.
|
||||
|
||||
JSON and YAML formats are accepted. Only one type of the arguments may be specified: filenames,
|
||||
resources and names, or resources and label selector.
|
||||
|
||||
Some resources, such as pods, support graceful deletion. These resources define a default period
|
||||
before they are forcibly terminated (the grace period) but you may override that value with
|
||||
the --grace-period flag, or pass --now to set a grace-period of 1. Because these resources often
|
||||
represent entities in the cluster, deletion may not be acknowledged immediately. If the node
|
||||
hosting a pod is down or cannot reach the API server, termination may take significantly longer
|
||||
than the grace period. To force delete a resource, you must pass a grace period of 0 and specify
|
||||
the --force flag.
|
||||
|
||||
IMPORTANT: Force deleting pods does not wait for confirmation that the pod's processes have been
|
||||
terminated, which can leave those processes running until the node detects the deletion and
|
||||
completes graceful deletion. If your processes use shared storage or talk to a remote API and
|
||||
depend on the name of the pod to identify themselves, force deleting those pods may result in
|
||||
multiple processes running on different machines using the same identification which may lead
|
||||
to data corruption or inconsistency. Only force delete pods when you are sure the pod is
|
||||
terminated, or if your application can tolerate multiple copies of the same pod running at once.
|
||||
Also, if you force delete pods the scheduler may place new pods on those nodes before the node
|
||||
has released those resources and causing those pods to be evicted immediately.
|
||||
|
||||
Note that the delete command does NOT do resource version checks, so if someone submits an
|
||||
update to a resource right when you submit a delete, their update will be lost along with the
|
||||
rest of the resource.`))
|
||||
|
||||
deleteExample = templates.Examples(i18n.T(`
|
||||
# Delete a pod using the type and name specified in pod.json.
|
||||
kubectl delete -f ./pod.json
|
||||
|
||||
# Delete resources from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml.
|
||||
kubectl delete -k dir
|
||||
|
||||
# Delete a pod based on the type and name in the JSON passed into stdin.
|
||||
cat pod.json | kubectl delete -f -
|
||||
|
||||
# Delete pods and services with same names "baz" and "foo"
|
||||
kubectl delete pod,service baz foo
|
||||
|
||||
# Delete pods and services with label name=myLabel.
|
||||
kubectl delete pods,services -l name=myLabel
|
||||
|
||||
# Delete a pod with minimal delay
|
||||
kubectl delete pod foo --now
|
||||
|
||||
# Force delete a pod on a dead node
|
||||
kubectl delete pod foo --grace-period=0 --force
|
||||
|
||||
# Delete all pods
|
||||
kubectl delete pods --all`))
|
||||
)
|
||||
|
||||
type DeleteOptions struct {
|
||||
resource.FilenameOptions
|
||||
|
||||
LabelSelector string
|
||||
FieldSelector string
|
||||
DeleteAll bool
|
||||
DeleteAllNamespaces bool
|
||||
IgnoreNotFound bool
|
||||
Cascade bool
|
||||
DeleteNow bool
|
||||
ForceDeletion bool
|
||||
WaitForDeletion bool
|
||||
Quiet bool
|
||||
WarnClusterScope bool
|
||||
Raw string
|
||||
|
||||
GracePeriod int
|
||||
Timeout time.Duration
|
||||
|
||||
Output string
|
||||
|
||||
DynamicClient dynamic.Interface
|
||||
Mapper meta.RESTMapper
|
||||
Result *resource.Result
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
func NewCmdDelete(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
deleteFlags := NewDeleteCommandFlags("containing the resource to delete.")
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete ([-f FILENAME] | [-k DIRECTORY] | TYPE [(NAME | -l label | --all)])",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Delete resources by filenames, stdin, resources and names, or by resources and label selector"),
|
||||
Long: deleteLong,
|
||||
Example: deleteExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o := deleteFlags.ToOptions(nil, streams)
|
||||
cmdutil.CheckErr(o.Complete(f, args, cmd))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunDelete(f))
|
||||
},
|
||||
SuggestFor: []string{"rm"},
|
||||
}
|
||||
|
||||
deleteFlags.AddFlags(cmd)
|
||||
|
||||
cmdutil.AddIncludeUninitializedFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Command) error {
|
||||
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.WarnClusterScope = enforceNamespace && !o.DeleteAllNamespaces
|
||||
|
||||
if o.DeleteAll || len(o.LabelSelector) > 0 || len(o.FieldSelector) > 0 {
|
||||
if f := cmd.Flags().Lookup("ignore-not-found"); f != nil && !f.Changed {
|
||||
// If the user didn't explicitly set the option, default to ignoring NotFound errors when used with --all, -l, or --field-selector
|
||||
o.IgnoreNotFound = true
|
||||
}
|
||||
}
|
||||
if o.DeleteNow {
|
||||
if o.GracePeriod != -1 {
|
||||
return fmt.Errorf("--now and --grace-period cannot be specified together")
|
||||
}
|
||||
o.GracePeriod = 1
|
||||
}
|
||||
if o.GracePeriod == 0 && !o.ForceDeletion {
|
||||
// To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0
|
||||
// into --grace-period=1. Users may provide --force to bypass this conversion.
|
||||
o.GracePeriod = 1
|
||||
}
|
||||
|
||||
if len(o.Raw) == 0 {
|
||||
r := f.NewBuilder().
|
||||
Unstructured().
|
||||
ContinueOnError().
|
||||
NamespaceParam(cmdNamespace).DefaultNamespace().
|
||||
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||
LabelSelectorParam(o.LabelSelector).
|
||||
FieldSelectorParam(o.FieldSelector).
|
||||
SelectAllParam(o.DeleteAll).
|
||||
AllNamespaces(o.DeleteAllNamespaces).
|
||||
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
|
||||
Flatten().
|
||||
Do()
|
||||
err = r.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Result = r
|
||||
|
||||
o.Mapper, err = f.ToRESTMapper()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.DynamicClient, err = f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DeleteOptions) Validate() error {
|
||||
if o.Output != "" && o.Output != "name" {
|
||||
return fmt.Errorf("unexpected -o output mode: %v. We only support '-o name'", o.Output)
|
||||
}
|
||||
|
||||
if o.DeleteAll && len(o.LabelSelector) > 0 {
|
||||
return fmt.Errorf("cannot set --all and --selector at the same time")
|
||||
}
|
||||
if o.DeleteAll && len(o.FieldSelector) > 0 {
|
||||
return fmt.Errorf("cannot set --all and --field-selector at the same time")
|
||||
}
|
||||
|
||||
switch {
|
||||
case o.GracePeriod == 0 && o.ForceDeletion:
|
||||
fmt.Fprintf(o.ErrOut, "warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n")
|
||||
case o.ForceDeletion:
|
||||
fmt.Fprintf(o.ErrOut, "warning: --force is ignored because --grace-period is not 0.\n")
|
||||
}
|
||||
|
||||
if len(o.Raw) > 0 {
|
||||
if len(o.FilenameOptions.Filenames) > 1 {
|
||||
return fmt.Errorf("--raw can only use a single local file or stdin")
|
||||
} else if len(o.FilenameOptions.Filenames) == 1 {
|
||||
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
|
||||
return fmt.Errorf("--raw cannot read from a url")
|
||||
}
|
||||
}
|
||||
|
||||
if o.FilenameOptions.Recursive {
|
||||
return fmt.Errorf("--raw and --recursive are mutually exclusive")
|
||||
}
|
||||
if len(o.Output) > 0 {
|
||||
return fmt.Errorf("--raw and --output are mutually exclusive")
|
||||
}
|
||||
if _, err := url.ParseRequestURI(o.Raw); err != nil {
|
||||
return fmt.Errorf("--raw must be a valid URL path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DeleteOptions) RunDelete(f cmdutil.Factory) error {
|
||||
if len(o.Raw) > 0 {
|
||||
restClient, err := f.RESTClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(o.Filenames) == 0 {
|
||||
return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, "")
|
||||
}
|
||||
return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, o.Filenames[0])
|
||||
}
|
||||
return o.DeleteResult(o.Result)
|
||||
}
|
||||
|
||||
func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
|
||||
found := 0
|
||||
if o.IgnoreNotFound {
|
||||
r = r.IgnoreErrors(errors.IsNotFound)
|
||||
}
|
||||
warnClusterScope := o.WarnClusterScope
|
||||
deletedInfos := []*resource.Info{}
|
||||
uidMap := cmdwait.UIDMap{}
|
||||
err := r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deletedInfos = append(deletedInfos, info)
|
||||
found++
|
||||
|
||||
options := &metav1.DeleteOptions{}
|
||||
if o.GracePeriod >= 0 {
|
||||
options = metav1.NewDeleteOptions(int64(o.GracePeriod))
|
||||
}
|
||||
policy := metav1.DeletePropagationBackground
|
||||
if !o.Cascade {
|
||||
policy = metav1.DeletePropagationOrphan
|
||||
}
|
||||
options.PropagationPolicy = &policy
|
||||
|
||||
if warnClusterScope && info.Mapping.Scope.Name() == meta.RESTScopeNameRoot {
|
||||
fmt.Fprintf(o.ErrOut, "warning: deleting cluster-scoped resources, not scoped to the provided namespace\n")
|
||||
warnClusterScope = false
|
||||
}
|
||||
response, err := o.deleteResource(info, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resourceLocation := cmdwait.ResourceLocation{
|
||||
GroupResource: info.Mapping.Resource.GroupResource(),
|
||||
Namespace: info.Namespace,
|
||||
Name: info.Name,
|
||||
}
|
||||
if status, ok := response.(*metav1.Status); ok && status.Details != nil {
|
||||
uidMap[resourceLocation] = status.Details.UID
|
||||
return nil
|
||||
}
|
||||
responseMetadata, err := meta.Accessor(response)
|
||||
if err != nil {
|
||||
// we don't have UID, but we didn't fail the delete, next best thing is just skipping the UID
|
||||
klog.V(1).Info(err)
|
||||
return nil
|
||||
}
|
||||
uidMap[resourceLocation] = responseMetadata.GetUID()
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if found == 0 {
|
||||
fmt.Fprintf(o.Out, "No resources found\n")
|
||||
return nil
|
||||
}
|
||||
if !o.WaitForDeletion {
|
||||
return nil
|
||||
}
|
||||
// if we don't have a dynamic client, we don't want to wait. Eventually when delete is cleaned up, this will likely
|
||||
// drop out.
|
||||
if o.DynamicClient == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
effectiveTimeout := o.Timeout
|
||||
if effectiveTimeout == 0 {
|
||||
// if we requested to wait forever, set it to a week.
|
||||
effectiveTimeout = 168 * time.Hour
|
||||
}
|
||||
waitOptions := cmdwait.WaitOptions{
|
||||
ResourceFinder: genericclioptions.ResourceFinderForResult(resource.InfoListVisitor(deletedInfos)),
|
||||
UIDMap: uidMap,
|
||||
DynamicClient: o.DynamicClient,
|
||||
Timeout: effectiveTimeout,
|
||||
|
||||
Printer: printers.NewDiscardingPrinter(),
|
||||
ConditionFn: cmdwait.IsDeleted,
|
||||
IOStreams: o.IOStreams,
|
||||
}
|
||||
err = waitOptions.RunWait()
|
||||
if errors.IsForbidden(err) || errors.IsMethodNotSupported(err) {
|
||||
// if we're forbidden from waiting, we shouldn't fail.
|
||||
// if the resource doesn't support a verb we need, we shouldn't fail.
|
||||
klog.V(1).Info(err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *DeleteOptions) deleteResource(info *resource.Info, deleteOptions *metav1.DeleteOptions) (runtime.Object, error) {
|
||||
deleteResponse, err := resource.NewHelper(info.Client, info.Mapping).DeleteWithOptions(info.Namespace, info.Name, deleteOptions)
|
||||
if err != nil {
|
||||
return nil, cmdutil.AddSourceToErr("deleting", info.Source, err)
|
||||
}
|
||||
|
||||
if !o.Quiet {
|
||||
o.PrintObj(info)
|
||||
}
|
||||
return deleteResponse, nil
|
||||
}
|
||||
|
||||
// PrintObj for deleted objects is special because we do not have an object to print.
|
||||
// This mirrors name printer behavior
|
||||
func (o *DeleteOptions) PrintObj(info *resource.Info) {
|
||||
operation := "deleted"
|
||||
groupKind := info.Mapping.GroupVersionKind
|
||||
kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group)
|
||||
if len(groupKind.Group) == 0 {
|
||||
kindString = strings.ToLower(groupKind.Kind)
|
||||
}
|
||||
|
||||
if o.GracePeriod == 0 {
|
||||
operation = "force deleted"
|
||||
}
|
||||
|
||||
if o.Output == "name" {
|
||||
// -o name: prints resource/name
|
||||
fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name)
|
||||
return
|
||||
}
|
||||
|
||||
// understandable output by default
|
||||
fmt.Fprintf(o.Out, "%s \"%s\" %s\n", kindString, info.Name, operation)
|
||||
}
|
||||
214
pkg/cmd/delete/delete_flags.go
Normal file
214
pkg/cmd/delete/delete_flags.go
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
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 delete
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
// DeleteFlags composes common printer flag structs
|
||||
// used for commands requiring deletion logic.
|
||||
type DeleteFlags struct {
|
||||
FileNameFlags *genericclioptions.FileNameFlags
|
||||
LabelSelector *string
|
||||
FieldSelector *string
|
||||
|
||||
All *bool
|
||||
AllNamespaces *bool
|
||||
Cascade *bool
|
||||
Force *bool
|
||||
GracePeriod *int
|
||||
IgnoreNotFound *bool
|
||||
Now *bool
|
||||
Timeout *time.Duration
|
||||
Wait *bool
|
||||
Output *string
|
||||
Raw *string
|
||||
}
|
||||
|
||||
func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericclioptions.IOStreams) *DeleteOptions {
|
||||
options := &DeleteOptions{
|
||||
DynamicClient: dynamicClient,
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
// add filename options
|
||||
if f.FileNameFlags != nil {
|
||||
options.FilenameOptions = f.FileNameFlags.ToOptions()
|
||||
}
|
||||
if f.LabelSelector != nil {
|
||||
options.LabelSelector = *f.LabelSelector
|
||||
}
|
||||
if f.FieldSelector != nil {
|
||||
options.FieldSelector = *f.FieldSelector
|
||||
}
|
||||
|
||||
// add output format
|
||||
if f.Output != nil {
|
||||
options.Output = *f.Output
|
||||
}
|
||||
|
||||
if f.All != nil {
|
||||
options.DeleteAll = *f.All
|
||||
}
|
||||
if f.AllNamespaces != nil {
|
||||
options.DeleteAllNamespaces = *f.AllNamespaces
|
||||
}
|
||||
if f.Cascade != nil {
|
||||
options.Cascade = *f.Cascade
|
||||
}
|
||||
if f.Force != nil {
|
||||
options.ForceDeletion = *f.Force
|
||||
}
|
||||
if f.GracePeriod != nil {
|
||||
options.GracePeriod = *f.GracePeriod
|
||||
}
|
||||
if f.IgnoreNotFound != nil {
|
||||
options.IgnoreNotFound = *f.IgnoreNotFound
|
||||
}
|
||||
if f.Now != nil {
|
||||
options.DeleteNow = *f.Now
|
||||
}
|
||||
if f.Timeout != nil {
|
||||
options.Timeout = *f.Timeout
|
||||
}
|
||||
if f.Wait != nil {
|
||||
options.WaitForDeletion = *f.Wait
|
||||
}
|
||||
if f.Raw != nil {
|
||||
options.Raw = *f.Raw
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
|
||||
f.FileNameFlags.AddFlags(cmd.Flags())
|
||||
if f.LabelSelector != nil {
|
||||
cmd.Flags().StringVarP(f.LabelSelector, "selector", "l", *f.LabelSelector, "Selector (label query) to filter on, not including uninitialized ones.")
|
||||
}
|
||||
if f.FieldSelector != nil {
|
||||
cmd.Flags().StringVarP(f.FieldSelector, "field-selector", "", *f.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
|
||||
}
|
||||
if f.All != nil {
|
||||
cmd.Flags().BoolVar(f.All, "all", *f.All, "Delete all resources, including uninitialized ones, in the namespace of the specified resource types.")
|
||||
}
|
||||
if f.AllNamespaces != nil {
|
||||
cmd.Flags().BoolVarP(f.AllNamespaces, "all-namespaces", "A", *f.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||
}
|
||||
if f.Force != nil {
|
||||
cmd.Flags().BoolVar(f.Force, "force", *f.Force, "Only used when grace-period=0. If true, immediately remove resources from API and bypass graceful deletion. Note that immediate deletion of some resources may result in inconsistency or data loss and requires confirmation.")
|
||||
}
|
||||
if f.Cascade != nil {
|
||||
cmd.Flags().BoolVar(f.Cascade, "cascade", *f.Cascade, "If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true.")
|
||||
}
|
||||
if f.Now != nil {
|
||||
cmd.Flags().BoolVar(f.Now, "now", *f.Now, "If true, resources are signaled for immediate shutdown (same as --grace-period=1).")
|
||||
}
|
||||
if f.GracePeriod != nil {
|
||||
cmd.Flags().IntVar(f.GracePeriod, "grace-period", *f.GracePeriod, "Period of time in seconds given to the resource to terminate gracefully. Ignored if negative. Set to 1 for immediate shutdown. Can only be set to 0 when --force is true (force deletion).")
|
||||
}
|
||||
if f.Timeout != nil {
|
||||
cmd.Flags().DurationVar(f.Timeout, "timeout", *f.Timeout, "The length of time to wait before giving up on a delete, zero means determine a timeout from the size of the object")
|
||||
}
|
||||
if f.IgnoreNotFound != nil {
|
||||
cmd.Flags().BoolVar(f.IgnoreNotFound, "ignore-not-found", *f.IgnoreNotFound, "Treat \"resource not found\" as a successful delete. Defaults to \"true\" when --all is specified.")
|
||||
}
|
||||
if f.Wait != nil {
|
||||
cmd.Flags().BoolVar(f.Wait, "wait", *f.Wait, "If true, wait for resources to be gone before returning. This waits for finalizers.")
|
||||
}
|
||||
if f.Output != nil {
|
||||
cmd.Flags().StringVarP(f.Output, "output", "o", *f.Output, "Output mode. Use \"-o name\" for shorter output (resource/name).")
|
||||
}
|
||||
if f.Raw != nil {
|
||||
cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.")
|
||||
}
|
||||
}
|
||||
|
||||
// NewDeleteCommandFlags provides default flags and values for use with the "delete" command
|
||||
func NewDeleteCommandFlags(usage string) *DeleteFlags {
|
||||
cascade := true
|
||||
gracePeriod := -1
|
||||
|
||||
// setup command defaults
|
||||
all := false
|
||||
allNamespaces := false
|
||||
force := false
|
||||
ignoreNotFound := false
|
||||
now := false
|
||||
output := ""
|
||||
labelSelector := ""
|
||||
fieldSelector := ""
|
||||
timeout := time.Duration(0)
|
||||
wait := true
|
||||
raw := ""
|
||||
|
||||
filenames := []string{}
|
||||
recursive := false
|
||||
kustomize := ""
|
||||
|
||||
return &DeleteFlags{
|
||||
// Not using helpers.go since it provides function to add '-k' for FileNameOptions, but not FileNameFlags
|
||||
FileNameFlags: &genericclioptions.FileNameFlags{Usage: usage, Filenames: &filenames, Kustomize: &kustomize, Recursive: &recursive},
|
||||
LabelSelector: &labelSelector,
|
||||
FieldSelector: &fieldSelector,
|
||||
|
||||
Cascade: &cascade,
|
||||
GracePeriod: &gracePeriod,
|
||||
|
||||
All: &all,
|
||||
AllNamespaces: &allNamespaces,
|
||||
Force: &force,
|
||||
IgnoreNotFound: &ignoreNotFound,
|
||||
Now: &now,
|
||||
Timeout: &timeout,
|
||||
Wait: &wait,
|
||||
Output: &output,
|
||||
Raw: &raw,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDeleteFlags provides default flags and values for use in commands outside of "delete"
|
||||
func NewDeleteFlags(usage string) *DeleteFlags {
|
||||
cascade := true
|
||||
gracePeriod := -1
|
||||
|
||||
force := false
|
||||
timeout := time.Duration(0)
|
||||
wait := false
|
||||
|
||||
filenames := []string{}
|
||||
kustomize := ""
|
||||
recursive := false
|
||||
|
||||
return &DeleteFlags{
|
||||
FileNameFlags: &genericclioptions.FileNameFlags{Usage: usage, Filenames: &filenames, Kustomize: &kustomize, Recursive: &recursive},
|
||||
|
||||
Cascade: &cascade,
|
||||
GracePeriod: &gracePeriod,
|
||||
|
||||
// add non-defaults
|
||||
Force: &force,
|
||||
Timeout: &timeout,
|
||||
Wait: &wait,
|
||||
}
|
||||
}
|
||||
713
pkg/cmd/delete/delete_test.go
Normal file
713
pkg/cmd/delete/delete_test.go
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
/*
|
||||
Copyright 2014 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 delete
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
func fakecmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete ([-f FILENAME] | TYPE [(NAME | -l label | --all)])",
|
||||
DisableFlagsInUseLine: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestDeleteObjectByTuple(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
|
||||
// replication controller with cascade off
|
||||
case p == "/namespaces/test/replicationcontrollers/redis-master-controller" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
|
||||
// secret with cascade on, but no client-side reaper
|
||||
case p == "/namespaces/test/secrets/mysecret" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
|
||||
default:
|
||||
// Ensures no GET is performed when deleting by name
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("namespace", "test")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"replicationcontrollers/redis-master-controller"})
|
||||
if buf.String() != "replicationcontroller/redis-master-controller\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
|
||||
// Test cascading delete of object without client-side reaper doesn't make GET requests
|
||||
streams, _, buf, _ = genericclioptions.NewTestIOStreams()
|
||||
cmd = NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("namespace", "test")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"secrets/mysecret"})
|
||||
if buf.String() != "secret/mysecret\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func hasExpectedPropagationPolicy(body io.ReadCloser, policy *metav1.DeletionPropagation) bool {
|
||||
if body == nil || policy == nil {
|
||||
return body == nil && policy == nil
|
||||
}
|
||||
var parsedBody metav1.DeleteOptions
|
||||
rawBody, _ := ioutil.ReadAll(body)
|
||||
json.Unmarshal(rawBody, &parsedBody)
|
||||
if parsedBody.PropagationPolicy == nil {
|
||||
return false
|
||||
}
|
||||
return *policy == *parsedBody.PropagationPolicy
|
||||
}
|
||||
|
||||
// Tests that DeleteOptions.OrphanDependents is appropriately set while deleting objects.
|
||||
func TestOrphanDependentsInDeleteObject(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
var policy *metav1.DeletionPropagation
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m, b := req.URL.Path, req.Method, req.Body; {
|
||||
|
||||
case p == "/namespaces/test/secrets/mysecret" && m == "DELETE" && hasExpectedPropagationPolicy(b, policy):
|
||||
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// DeleteOptions.PropagationPolicy should be Background, when cascade is true (default).
|
||||
backgroundPolicy := metav1.DeletePropagationBackground
|
||||
policy = &backgroundPolicy
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("namespace", "test")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"secrets/mysecret"})
|
||||
if buf.String() != "secret/mysecret\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
|
||||
// Test that delete options should be set to orphan when cascade is false.
|
||||
orphanPolicy := metav1.DeletePropagationOrphan
|
||||
policy = &orphanPolicy
|
||||
streams, _, buf, _ = genericclioptions.NewTestIOStreams()
|
||||
cmd = NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("namespace", "test")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"secrets/mysecret"})
|
||||
if buf.String() != "secret/mysecret\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteNamedObject(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
|
||||
// replication controller with cascade off
|
||||
case p == "/namespaces/test/replicationcontrollers/redis-master-controller" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
|
||||
// secret with cascade on, but no client-side reaper
|
||||
case p == "/namespaces/test/secrets/mysecret" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
|
||||
default:
|
||||
// Ensures no GET is performed when deleting by name
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("namespace", "test")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"replicationcontrollers", "redis-master-controller"})
|
||||
if buf.String() != "replicationcontroller/redis-master-controller\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
|
||||
// Test cascading delete of object without client-side reaper doesn't make GET requests
|
||||
streams, _, buf, _ = genericclioptions.NewTestIOStreams()
|
||||
cmd = NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("namespace", "test")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"secrets", "mysecret"})
|
||||
if buf.String() != "secret/mysecret\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteObject(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/legacy/redis-master-controller.yaml")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
// uses the name from the file, not the response
|
||||
if buf.String() != "replicationcontroller/redis-master\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteObjectGraceZero(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
pods, _, _ := cmdtesting.TestData()
|
||||
|
||||
count := 0
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
t.Logf("got request %s %s", req.Method, req.URL.Path)
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/pods/nginx" && m == "GET":
|
||||
count++
|
||||
switch count {
|
||||
case 1, 2, 3:
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
|
||||
default:
|
||||
return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &metav1.Status{})}, nil
|
||||
}
|
||||
case p == "/api/v1/namespaces/test" && m == "GET":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil
|
||||
case p == "/namespaces/test/pods/nginx" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
streams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("grace-period", "0")
|
||||
cmd.Run(cmd, []string{"pods/nginx"})
|
||||
|
||||
// uses the name from the file, not the response
|
||||
if buf.String() != "pod/nginx\n" {
|
||||
t.Errorf("unexpected output: %s\n---\n%s", buf.String(), errBuf.String())
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("unexpected calls to GET: %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteObjectNotFound(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
options := &DeleteOptions{
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"../../../../test/e2e/testing-manifests/guestbook/legacy/redis-master-controller.yaml"},
|
||||
},
|
||||
GracePeriod: -1,
|
||||
Cascade: false,
|
||||
Output: "name",
|
||||
IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
|
||||
}
|
||||
err := options.Complete(tf, []string{}, fakecmd())
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
err = options.RunDelete(nil)
|
||||
if err == nil || !errors.IsNotFound(err) {
|
||||
t.Errorf("unexpected error: expected NotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteObjectIgnoreNotFound(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/legacy/redis-master-controller.yaml")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("ignore-not-found", "true")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAllNotFound(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, svc, _ := cmdtesting.TestData()
|
||||
// Add an item to the list which will result in a 404 on delete
|
||||
svc.Items = append(svc.Items, corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo"}})
|
||||
notFoundError := &errors.NewNotFound(corev1.Resource("services"), "foo").ErrStatus
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/services" && m == "GET":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil
|
||||
case p == "/namespaces/test/services/foo" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, notFoundError)}, nil
|
||||
case p == "/namespaces/test/services/baz" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// Make sure we can explicitly choose to fail on NotFound errors, even with --all
|
||||
options := &DeleteOptions{
|
||||
FilenameOptions: resource.FilenameOptions{},
|
||||
GracePeriod: -1,
|
||||
Cascade: false,
|
||||
DeleteAll: true,
|
||||
IgnoreNotFound: false,
|
||||
Output: "name",
|
||||
IOStreams: genericclioptions.NewTestIOStreamsDiscard(),
|
||||
}
|
||||
err := options.Complete(tf, []string{"services"}, fakecmd())
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
err = options.RunDelete(nil)
|
||||
if err == nil || !errors.IsNotFound(err) {
|
||||
t.Errorf("unexpected error: expected NotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAllIgnoreNotFound(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, svc, _ := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
// Add an item to the list which will result in a 404 on delete
|
||||
svc.Items = append(svc.Items, corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo"}})
|
||||
notFoundError := &errors.NewNotFound(corev1.Resource("services"), "foo").ErrStatus
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/services" && m == "GET":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil
|
||||
case p == "/namespaces/test/services/foo" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, notFoundError)}, nil
|
||||
case p == "/namespaces/test/services/baz" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("all", "true")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"services"})
|
||||
|
||||
if buf.String() != "service/baz\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMultipleObject(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, svc, rc := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
case p == "/namespaces/test/services/frontend" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/legacy/redis-master-controller.yaml")
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/frontend-service.yaml")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "replicationcontroller/redis-master\nservice/frontend\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMultipleObjectContinueOnMissing(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, svc, _ := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil
|
||||
case p == "/namespaces/test/services/frontend" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
options := &DeleteOptions{
|
||||
FilenameOptions: resource.FilenameOptions{
|
||||
Filenames: []string{"../../../../test/e2e/testing-manifests/guestbook/legacy/redis-master-controller.yaml", "../../../../test/e2e/testing-manifests/guestbook/frontend-service.yaml"},
|
||||
},
|
||||
GracePeriod: -1,
|
||||
Cascade: false,
|
||||
Output: "name",
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := options.Complete(tf, []string{}, fakecmd())
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
err = options.RunDelete(nil)
|
||||
if err == nil || !errors.IsNotFound(err) {
|
||||
t.Errorf("unexpected error: expected NotFound, got %v", err)
|
||||
}
|
||||
|
||||
if buf.String() != "service/frontend\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMultipleResourcesWithTheSameName(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, svc, rc := cmdtesting.TestData()
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers/baz" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
case p == "/namespaces/test/replicationcontrollers/foo" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
case p == "/namespaces/test/services/baz" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
|
||||
case p == "/namespaces/test/services/foo" && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
|
||||
default:
|
||||
// Ensures no GET is performed when deleting by name
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("namespace", "test")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"replicationcontrollers,services", "baz", "foo"})
|
||||
if buf.String() != "replicationcontroller/baz\nreplicationcontroller/foo\nservice/baz\nservice/foo\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteDirectory(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/legacy")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "replicationcontroller/frontend\nreplicationcontroller/redis-master\nreplicationcontroller/redis-slave\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMultipleSelector(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
pods, svc, _ := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/pods" && m == "GET":
|
||||
if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" {
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
}
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil
|
||||
case p == "/namespaces/test/services" && m == "GET":
|
||||
if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" {
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
}
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil
|
||||
case strings.HasPrefix(p, "/namespaces/test/pods/") && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil
|
||||
case strings.HasPrefix(p, "/namespaces/test/services/") && m == "DELETE":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("selector", "a=b")
|
||||
cmd.Flags().Set("cascade", "false")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Run(cmd, []string{"pods,services"})
|
||||
|
||||
if buf.String() != "pod/foo\npod/bar\nservice/baz\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceErrors(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
errFn func(error) bool
|
||||
}{
|
||||
"no args": {
|
||||
args: []string{},
|
||||
errFn: func(err error) bool { return strings.Contains(err.Error(), "You must provide one or more resources") },
|
||||
},
|
||||
"resources but no selectors": {
|
||||
args: []string{"pods"},
|
||||
errFn: func(err error) bool {
|
||||
return strings.Contains(err.Error(), "resource(s) were provided, but no name, label selector, or --all flag specified")
|
||||
},
|
||||
},
|
||||
"multiple resources but no selectors": {
|
||||
args: []string{"pods,deployments"},
|
||||
errFn: func(err error) bool {
|
||||
return strings.Contains(err.Error(), "resource(s) were provided, but no name, label selector, or --all flag specified")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k, testCase := range testCases {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
options := &DeleteOptions{
|
||||
FilenameOptions: resource.FilenameOptions{},
|
||||
GracePeriod: -1,
|
||||
Cascade: false,
|
||||
Output: "name",
|
||||
IOStreams: streams,
|
||||
}
|
||||
err := options.Complete(tf, testCase.args, fakecmd())
|
||||
if !testCase.errFn(err) {
|
||||
t.Errorf("%s: unexpected error: %v", k, err)
|
||||
return
|
||||
}
|
||||
|
||||
if buf.Len() > 0 {
|
||||
t.Errorf("buffer should be empty: %s", string(buf.Bytes()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
246
pkg/cmd/describe/describe.go
Normal file
246
pkg/cmd/describe/describe.go
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
Copyright 2014 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 describe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/describe"
|
||||
describeversioned "k8s.io/kubectl/pkg/describe/versioned"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
describeLong = templates.LongDesc(`
|
||||
Show details of a specific resource or group of resources
|
||||
|
||||
Print a detailed description of the selected resources, including related resources such
|
||||
as events or controllers. You may select a single object by name, all objects of that
|
||||
type, provide a name prefix, or label selector. For example:
|
||||
|
||||
$ kubectl describe TYPE NAME_PREFIX
|
||||
|
||||
will first check for an exact match on TYPE and NAME_PREFIX. If no such resource
|
||||
exists, it will output details for every resource that has a name prefixed with NAME_PREFIX.`)
|
||||
|
||||
describeExample = templates.Examples(i18n.T(`
|
||||
# Describe a node
|
||||
kubectl describe nodes kubernetes-node-emt8.c.myproject.internal
|
||||
|
||||
# Describe a pod
|
||||
kubectl describe pods/nginx
|
||||
|
||||
# Describe a pod identified by type and name in "pod.json"
|
||||
kubectl describe -f pod.json
|
||||
|
||||
# Describe all pods
|
||||
kubectl describe pods
|
||||
|
||||
# Describe pods by label name=myLabel
|
||||
kubectl describe po -l name=myLabel
|
||||
|
||||
# Describe all pods managed by the 'frontend' replication controller (rc-created pods
|
||||
# get the name of the rc as a prefix in the pod the name).
|
||||
kubectl describe pods frontend`))
|
||||
)
|
||||
|
||||
type DescribeOptions struct {
|
||||
CmdParent string
|
||||
Selector string
|
||||
Namespace string
|
||||
|
||||
Describer func(*meta.RESTMapping) (describe.Describer, error)
|
||||
NewBuilder func() *resource.Builder
|
||||
|
||||
BuilderArgs []string
|
||||
|
||||
EnforceNamespace bool
|
||||
AllNamespaces bool
|
||||
|
||||
DescriberSettings *describe.DescriberSettings
|
||||
FilenameOptions *resource.FilenameOptions
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
func NewCmdDescribe(parent string, f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := &DescribeOptions{
|
||||
FilenameOptions: &resource.FilenameOptions{},
|
||||
DescriberSettings: &describe.DescriberSettings{
|
||||
ShowEvents: true,
|
||||
},
|
||||
|
||||
CmdParent: parent,
|
||||
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "describe (-f FILENAME | TYPE [NAME_PREFIX | -l label] | TYPE/NAME)",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Show details of a specific resource or group of resources"),
|
||||
Long: describeLong + "\n\n" + cmdutil.SuggestAPIResources(parent),
|
||||
Example: describeExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
usage := "containing the resource to describe"
|
||||
cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, usage)
|
||||
cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||
cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||
cmd.Flags().BoolVar(&o.DescriberSettings.ShowEvents, "show-events", o.DescriberSettings.ShowEvents, "If true, display events related to the described object.")
|
||||
cmdutil.AddIncludeUninitializedFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *DescribeOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.AllNamespaces {
|
||||
o.EnforceNamespace = false
|
||||
}
|
||||
|
||||
if len(args) == 0 && cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) {
|
||||
return fmt.Errorf("You must specify the type of resource to describe. %s\n", cmdutil.SuggestAPIResources(o.CmdParent))
|
||||
}
|
||||
|
||||
o.BuilderArgs = args
|
||||
|
||||
o.Describer = func(mapping *meta.RESTMapping) (describe.Describer, error) {
|
||||
return describeversioned.DescriberFn(f, mapping)
|
||||
}
|
||||
|
||||
o.NewBuilder = f.NewBuilder
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DescribeOptions) Validate(args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DescribeOptions) Run() error {
|
||||
r := o.NewBuilder().
|
||||
Unstructured().
|
||||
ContinueOnError().
|
||||
NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces).
|
||||
FilenameParam(o.EnforceNamespace, o.FilenameOptions).
|
||||
LabelSelectorParam(o.Selector).
|
||||
ResourceTypeOrNameArgs(true, o.BuilderArgs...).
|
||||
Flatten().
|
||||
Do()
|
||||
err := r.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allErrs := []error{}
|
||||
infos, err := r.Infos()
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) && len(o.BuilderArgs) == 2 {
|
||||
return o.DescribeMatchingResources(err, o.BuilderArgs[0], o.BuilderArgs[1])
|
||||
}
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
|
||||
errs := sets.NewString()
|
||||
first := true
|
||||
for _, info := range infos {
|
||||
mapping := info.ResourceMapping()
|
||||
describer, err := o.Describer(mapping)
|
||||
if err != nil {
|
||||
if errs.Has(err.Error()) {
|
||||
continue
|
||||
}
|
||||
allErrs = append(allErrs, err)
|
||||
errs.Insert(err.Error())
|
||||
continue
|
||||
}
|
||||
s, err := describer.Describe(info.Namespace, info.Name, *o.DescriberSettings)
|
||||
if err != nil {
|
||||
if errs.Has(err.Error()) {
|
||||
continue
|
||||
}
|
||||
allErrs = append(allErrs, err)
|
||||
errs.Insert(err.Error())
|
||||
continue
|
||||
}
|
||||
if first {
|
||||
first = false
|
||||
fmt.Fprint(o.Out, s)
|
||||
} else {
|
||||
fmt.Fprintf(o.Out, "\n\n%s", s)
|
||||
}
|
||||
}
|
||||
|
||||
return utilerrors.NewAggregate(allErrs)
|
||||
}
|
||||
|
||||
func (o *DescribeOptions) DescribeMatchingResources(originalError error, resource, prefix string) error {
|
||||
r := o.NewBuilder().
|
||||
Unstructured().
|
||||
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||
ResourceTypeOrNameArgs(true, resource).
|
||||
SingleResourceType().
|
||||
Flatten().
|
||||
Do()
|
||||
mapping, err := r.ResourceMapping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
describer, err := o.Describer(mapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
infos, err := r.Infos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isFound := false
|
||||
for ix := range infos {
|
||||
info := infos[ix]
|
||||
if strings.HasPrefix(info.Name, prefix) {
|
||||
isFound = true
|
||||
s, err := describer.Describe(info.Namespace, info.Name, *o.DescriberSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(o.Out, "%s\n", s)
|
||||
}
|
||||
}
|
||||
if !isFound {
|
||||
return originalError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
262
pkg/cmd/describe/describe_test.go
Normal file
262
pkg/cmd/describe/describe_test.go
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
Copyright 2014 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 describe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/kubectl/pkg/describe"
|
||||
versioneddescribe "k8s.io/kubectl/pkg/describe/versioned"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
// Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get.
|
||||
func TestDescribeUnknownSchemaObject(t *testing.T) {
|
||||
d := &testDescriber{Output: "test output"}
|
||||
oldFn := versioneddescribe.DescriberFn
|
||||
defer func() {
|
||||
versioneddescribe.DescriberFn = oldFn
|
||||
}()
|
||||
versioneddescribe.DescriberFn = d.describerFor
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("non-default")
|
||||
defer tf.Cleanup()
|
||||
_, _, codec := cmdtesting.NewExternalScheme()
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, cmdtesting.NewInternalType("", "", "foo"))},
|
||||
}
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDescribe("kubectl", tf, streams)
|
||||
cmd.Run(cmd, []string{"type", "foo"})
|
||||
|
||||
if d.Name != "foo" || d.Namespace != "" {
|
||||
t.Errorf("unexpected describer: %#v", d)
|
||||
}
|
||||
|
||||
if buf.String() != fmt.Sprintf("%s", d.Output) {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get.
|
||||
func TestDescribeUnknownNamespacedSchemaObject(t *testing.T) {
|
||||
d := &testDescriber{Output: "test output"}
|
||||
oldFn := versioneddescribe.DescriberFn
|
||||
defer func() {
|
||||
versioneddescribe.DescriberFn = oldFn
|
||||
}()
|
||||
versioneddescribe.DescriberFn = d.describerFor
|
||||
|
||||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
_, _, codec := cmdtesting.NewExternalScheme()
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, cmdtesting.NewInternalNamespacedType("", "", "foo", "non-default"))},
|
||||
}
|
||||
tf.WithNamespace("non-default")
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDescribe("kubectl", tf, streams)
|
||||
cmd.Run(cmd, []string{"namespacedtype", "foo"})
|
||||
|
||||
if d.Name != "foo" || d.Namespace != "non-default" {
|
||||
t.Errorf("unexpected describer: %#v", d)
|
||||
}
|
||||
|
||||
if buf.String() != fmt.Sprintf("%s", d.Output) {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeObject(t *testing.T) {
|
||||
d := &testDescriber{Output: "test output"}
|
||||
oldFn := versioneddescribe.DescriberFn
|
||||
defer func() {
|
||||
versioneddescribe.DescriberFn = oldFn
|
||||
}()
|
||||
versioneddescribe.DescriberFn = d.describerFor
|
||||
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "GET":
|
||||
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDescribe("kubectl", tf, streams)
|
||||
cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/guestbook/legacy/redis-master-controller.yaml")
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if d.Name != "redis-master" || d.Namespace != "test" {
|
||||
t.Errorf("unexpected describer: %#v", d)
|
||||
}
|
||||
|
||||
if buf.String() != fmt.Sprintf("%s", d.Output) {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeListObjects(t *testing.T) {
|
||||
d := &testDescriber{Output: "test output"}
|
||||
oldFn := versioneddescribe.DescriberFn
|
||||
defer func() {
|
||||
versioneddescribe.DescriberFn = oldFn
|
||||
}()
|
||||
versioneddescribe.DescriberFn = d.describerFor
|
||||
|
||||
pods, _, _ := cmdtesting.TestData()
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)},
|
||||
}
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDescribe("kubectl", tf, streams)
|
||||
cmd.Run(cmd, []string{"pods"})
|
||||
if buf.String() != fmt.Sprintf("%s\n\n%s", d.Output, d.Output) {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeObjectShowEvents(t *testing.T) {
|
||||
d := &testDescriber{Output: "test output"}
|
||||
oldFn := versioneddescribe.DescriberFn
|
||||
defer func() {
|
||||
versioneddescribe.DescriberFn = oldFn
|
||||
}()
|
||||
versioneddescribe.DescriberFn = d.describerFor
|
||||
|
||||
pods, _, _ := cmdtesting.TestData()
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)},
|
||||
}
|
||||
|
||||
cmd := NewCmdDescribe("kubectl", tf, genericclioptions.NewTestIOStreamsDiscard())
|
||||
cmd.Flags().Set("show-events", "true")
|
||||
cmd.Run(cmd, []string{"pods"})
|
||||
if d.Settings.ShowEvents != true {
|
||||
t.Errorf("ShowEvents = true expected, got ShowEvents = %v", d.Settings.ShowEvents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeObjectSkipEvents(t *testing.T) {
|
||||
d := &testDescriber{Output: "test output"}
|
||||
oldFn := versioneddescribe.DescriberFn
|
||||
defer func() {
|
||||
versioneddescribe.DescriberFn = oldFn
|
||||
}()
|
||||
versioneddescribe.DescriberFn = d.describerFor
|
||||
|
||||
pods, _, _ := cmdtesting.TestData()
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)},
|
||||
}
|
||||
|
||||
cmd := NewCmdDescribe("kubectl", tf, genericclioptions.NewTestIOStreamsDiscard())
|
||||
cmd.Flags().Set("show-events", "false")
|
||||
cmd.Run(cmd, []string{"pods"})
|
||||
if d.Settings.ShowEvents != false {
|
||||
t.Errorf("ShowEvents = false expected, got ShowEvents = %v", d.Settings.ShowEvents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeHelpMessage(t *testing.T) {
|
||||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmd := NewCmdDescribe("kubectl", tf, streams)
|
||||
cmd.SetArgs([]string{"-h"})
|
||||
cmd.SetOutput(buf)
|
||||
_, err := cmd.ExecuteC()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
|
||||
expected := `describe (-f FILENAME | TYPE [NAME_PREFIX | -l label] | TYPE/NAME)`
|
||||
if !strings.Contains(got, expected) {
|
||||
t.Errorf("Expected to contain: \n %v\nGot:\n %v\n", expected, got)
|
||||
}
|
||||
|
||||
unexpected := `describe (-f FILENAME | TYPE [NAME_PREFIX | -l label] | TYPE/NAME) [flags]`
|
||||
if strings.Contains(got, unexpected) {
|
||||
t.Errorf("Expected not to contain: \n %v\nGot:\n %v\n", unexpected, got)
|
||||
}
|
||||
}
|
||||
|
||||
type testDescriber struct {
|
||||
Name, Namespace string
|
||||
Settings describe.DescriberSettings
|
||||
Output string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (t *testDescriber) Describe(namespace, name string, describerSettings describe.DescriberSettings) (output string, err error) {
|
||||
t.Namespace, t.Name = namespace, name
|
||||
t.Settings = describerSettings
|
||||
return t.Output, t.Err
|
||||
}
|
||||
func (t *testDescriber) describerFor(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (describe.Describer, error) {
|
||||
return t, nil
|
||||
}
|
||||
509
pkg/cmd/diff/diff.go
Normal file
509
pkg/cmd/diff/diff.go
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
/*
|
||||
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 diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/klog"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/apply"
|
||||
"k8s.io/utils/exec"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
diffLong = templates.LongDesc(i18n.T(`
|
||||
Diff configurations specified by filename or stdin between the current online
|
||||
configuration, and the configuration as it would be if applied.
|
||||
|
||||
Output is always YAML.
|
||||
|
||||
KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own
|
||||
diff command. By default, the "diff" command available in your path will be
|
||||
run with "-u" (unicode) and "-N" (treat new files as empty) options.`))
|
||||
diffExample = templates.Examples(i18n.T(`
|
||||
# Diff resources included in pod.json.
|
||||
kubectl diff -f pod.json
|
||||
|
||||
# Diff file read from stdin
|
||||
cat service.yaml | kubectl diff -f -`))
|
||||
)
|
||||
|
||||
// Number of times we try to diff before giving-up
|
||||
const maxRetries = 4
|
||||
|
||||
type DiffOptions struct {
|
||||
FilenameOptions resource.FilenameOptions
|
||||
|
||||
ServerSideApply bool
|
||||
ForceConflicts bool
|
||||
|
||||
OpenAPISchema openapi.Resources
|
||||
DiscoveryClient discovery.DiscoveryInterface
|
||||
DynamicClient dynamic.Interface
|
||||
DryRunVerifier *apply.DryRunVerifier
|
||||
CmdNamespace string
|
||||
EnforceNamespace bool
|
||||
Builder *resource.Builder
|
||||
Diff *DiffProgram
|
||||
}
|
||||
|
||||
func validateArgs(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDiffOptions(ioStreams genericclioptions.IOStreams) *DiffOptions {
|
||||
return &DiffOptions{
|
||||
Diff: &DiffProgram{
|
||||
Exec: exec.New(),
|
||||
IOStreams: ioStreams,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
options := NewDiffOptions(streams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "diff -f FILENAME",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Diff live version against would-be applied version"),
|
||||
Long: diffLong,
|
||||
Example: diffExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(options.Complete(f, cmd))
|
||||
cmdutil.CheckErr(validateArgs(cmd, args))
|
||||
cmdutil.CheckErr(options.Run())
|
||||
},
|
||||
}
|
||||
|
||||
usage := "contains the configuration to diff"
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
|
||||
cmdutil.AddServerSideApplyFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// DiffProgram finds and run the diff program. The value of
|
||||
// KUBECTL_EXTERNAL_DIFF environment variable will be used a diff
|
||||
// program. By default, `diff(1)` will be used.
|
||||
type DiffProgram struct {
|
||||
Exec exec.Interface
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
func (d *DiffProgram) getCommand(args ...string) exec.Cmd {
|
||||
diff := ""
|
||||
if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" {
|
||||
diff = envDiff
|
||||
} else {
|
||||
diff = "diff"
|
||||
args = append([]string{"-u", "-N"}, args...)
|
||||
}
|
||||
|
||||
cmd := d.Exec.Command(diff, args...)
|
||||
cmd.SetStdout(d.Out)
|
||||
cmd.SetStderr(d.ErrOut)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Run runs the detected diff program. `from` and `to` are the directory to diff.
|
||||
func (d *DiffProgram) Run(from, to string) error {
|
||||
return d.getCommand(from, to).Run()
|
||||
}
|
||||
|
||||
// Printer is used to print an object.
|
||||
type Printer struct{}
|
||||
|
||||
// Print the object inside the writer w.
|
||||
func (p *Printer) Print(obj runtime.Object, w io.Writer) error {
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
data, err := yaml.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// DiffVersion gets the proper version of objects, and aggregate them into a directory.
|
||||
type DiffVersion struct {
|
||||
Dir *Directory
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewDiffVersion creates a new DiffVersion with the named version.
|
||||
func NewDiffVersion(name string) (*DiffVersion, error) {
|
||||
dir, err := CreateDirectory(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DiffVersion{
|
||||
Dir: dir,
|
||||
Name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *DiffVersion) getObject(obj Object) (runtime.Object, error) {
|
||||
switch v.Name {
|
||||
case "LIVE":
|
||||
return obj.Live(), nil
|
||||
case "MERGED":
|
||||
return obj.Merged()
|
||||
}
|
||||
return nil, fmt.Errorf("Unknown version: %v", v.Name)
|
||||
}
|
||||
|
||||
// Print prints the object using the printer into a new file in the directory.
|
||||
func (v *DiffVersion) Print(obj Object, printer Printer) error {
|
||||
vobj, err := v.getObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := v.Dir.NewFile(obj.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return printer.Print(vobj, f)
|
||||
}
|
||||
|
||||
// Directory creates a new temp directory, and allows to easily create new files.
|
||||
type Directory struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// CreateDirectory does create the actual disk directory, and return a
|
||||
// new representation of it.
|
||||
func CreateDirectory(prefix string) (*Directory, error) {
|
||||
name, err := ioutil.TempDir("", prefix+"-")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Directory{
|
||||
Name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewFile creates a new file in the directory.
|
||||
func (d *Directory) NewFile(name string) (*os.File, error) {
|
||||
return os.OpenFile(filepath.Join(d.Name, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)
|
||||
}
|
||||
|
||||
// Delete removes the directory recursively.
|
||||
func (d *Directory) Delete() error {
|
||||
return os.RemoveAll(d.Name)
|
||||
}
|
||||
|
||||
// Object is an interface that let's you retrieve multiple version of
|
||||
// it.
|
||||
type Object interface {
|
||||
Live() runtime.Object
|
||||
Merged() (runtime.Object, error)
|
||||
|
||||
Name() string
|
||||
}
|
||||
|
||||
// InfoObject is an implementation of the Object interface. It gets all
|
||||
// the information from the Info object.
|
||||
type InfoObject struct {
|
||||
LocalObj runtime.Object
|
||||
Info *resource.Info
|
||||
Encoder runtime.Encoder
|
||||
OpenAPI openapi.Resources
|
||||
Force bool
|
||||
ServerSideApply bool
|
||||
ForceConflicts bool
|
||||
}
|
||||
|
||||
var _ Object = &InfoObject{}
|
||||
|
||||
// Returns the live version of the object
|
||||
func (obj InfoObject) Live() runtime.Object {
|
||||
return obj.Info.Object
|
||||
}
|
||||
|
||||
// Returns the "merged" object, as it would look like if applied or
|
||||
// created.
|
||||
func (obj InfoObject) Merged() (runtime.Object, error) {
|
||||
if obj.ServerSideApply {
|
||||
data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj.LocalObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options := metav1.PatchOptions{
|
||||
Force: &obj.ForceConflicts,
|
||||
DryRun: []string{metav1.DryRunAll},
|
||||
}
|
||||
return resource.NewHelper(obj.Info.Client, obj.Info.Mapping).Patch(
|
||||
obj.Info.Namespace,
|
||||
obj.Info.Name,
|
||||
types.ApplyPatchType,
|
||||
data,
|
||||
&options,
|
||||
)
|
||||
}
|
||||
|
||||
// Build the patcher, and then apply the patch with dry-run, unless the object doesn't exist, in which case we need to create it.
|
||||
if obj.Live() == nil {
|
||||
// Dry-run create if the object doesn't exist.
|
||||
return resource.NewHelper(obj.Info.Client, obj.Info.Mapping).Create(
|
||||
obj.Info.Namespace,
|
||||
true,
|
||||
obj.LocalObj,
|
||||
&metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
|
||||
)
|
||||
}
|
||||
|
||||
var resourceVersion *string
|
||||
if !obj.Force {
|
||||
accessor, err := meta.Accessor(obj.Info.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
str := accessor.GetResourceVersion()
|
||||
resourceVersion = &str
|
||||
}
|
||||
|
||||
modified, err := util.GetModifiedConfiguration(obj.LocalObj, false, unstructured.UnstructuredJSONScheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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.
|
||||
patcher := &apply.Patcher{
|
||||
Mapping: obj.Info.Mapping,
|
||||
Helper: resource.NewHelper(obj.Info.Client, obj.Info.Mapping),
|
||||
Overwrite: true,
|
||||
BackOff: clockwork.NewRealClock(),
|
||||
ServerDryRun: true,
|
||||
OpenapiSchema: obj.OpenAPI,
|
||||
ResourceVersion: resourceVersion,
|
||||
}
|
||||
|
||||
_, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, nil)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (obj InfoObject) Name() string {
|
||||
group := ""
|
||||
if obj.Info.Mapping.GroupVersionKind.Group != "" {
|
||||
group = fmt.Sprintf("%v.", obj.Info.Mapping.GroupVersionKind.Group)
|
||||
}
|
||||
return group + fmt.Sprintf(
|
||||
"%v.%v.%v.%v",
|
||||
obj.Info.Mapping.GroupVersionKind.Version,
|
||||
obj.Info.Mapping.GroupVersionKind.Kind,
|
||||
obj.Info.Namespace,
|
||||
obj.Info.Name,
|
||||
)
|
||||
}
|
||||
|
||||
// Differ creates two DiffVersion and diffs them.
|
||||
type Differ struct {
|
||||
From *DiffVersion
|
||||
To *DiffVersion
|
||||
}
|
||||
|
||||
func NewDiffer(from, to string) (*Differ, error) {
|
||||
differ := Differ{}
|
||||
var err error
|
||||
differ.From, err = NewDiffVersion(from)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
differ.To, err = NewDiffVersion(to)
|
||||
if err != nil {
|
||||
differ.From.Dir.Delete()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &differ, nil
|
||||
}
|
||||
|
||||
// Diff diffs to versions of a specific object, and print both versions to directories.
|
||||
func (d *Differ) Diff(obj Object, printer Printer) error {
|
||||
if err := d.From.Print(obj, printer); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.To.Print(obj, printer); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run runs the diff program against both directories.
|
||||
func (d *Differ) Run(diff *DiffProgram) error {
|
||||
return diff.Run(d.From.Dir.Name, d.To.Dir.Name)
|
||||
}
|
||||
|
||||
// TearDown removes both temporary directories recursively.
|
||||
func (d *Differ) TearDown() {
|
||||
d.From.Dir.Delete() // Ignore error
|
||||
d.To.Dir.Delete() // Ignore error
|
||||
}
|
||||
|
||||
func isConflict(err error) bool {
|
||||
return err != nil && errors.IsConflict(err)
|
||||
}
|
||||
|
||||
func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||
var err error
|
||||
|
||||
err = o.FilenameOptions.RequireFilenameOrKustomize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd)
|
||||
o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd)
|
||||
if o.ForceConflicts && !o.ServerSideApply {
|
||||
return fmt.Errorf("--experimental-force-conflicts only works with --experimental-server-side")
|
||||
}
|
||||
|
||||
if !o.ServerSideApply {
|
||||
o.OpenAPISchema, err = f.OpenAPISchema()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
o.DiscoveryClient, err = f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.DynamicClient, err = f.DynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.DryRunVerifier = &apply.DryRunVerifier{
|
||||
Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(o.DynamicClient)),
|
||||
OpenAPIGetter: o.DiscoveryClient,
|
||||
}
|
||||
|
||||
o.CmdNamespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Builder = f.NewBuilder()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunDiff uses the factory to parse file arguments, find the version to
|
||||
// diff, and find each Info object for each files, and runs against the
|
||||
// differ.
|
||||
func (o *DiffOptions) Run() error {
|
||||
differ, err := NewDiffer("LIVE", "MERGED")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer differ.TearDown()
|
||||
|
||||
printer := Printer{}
|
||||
|
||||
r := o.Builder.
|
||||
Unstructured().
|
||||
NamespaceParam(o.CmdNamespace).DefaultNamespace().
|
||||
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
|
||||
Flatten().
|
||||
Do()
|
||||
if err := r.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
local := info.Object.DeepCopyObject()
|
||||
for i := 1; i <= maxRetries; i++ {
|
||||
if err = info.Get(); err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
info.Object = nil
|
||||
}
|
||||
|
||||
force := i == maxRetries
|
||||
if force {
|
||||
klog.Warningf(
|
||||
"Object (%v: %v) keeps changing, diffing without lock",
|
||||
info.Object.GetObjectKind().GroupVersionKind(),
|
||||
info.Name,
|
||||
)
|
||||
}
|
||||
obj := InfoObject{
|
||||
LocalObj: local,
|
||||
Info: info,
|
||||
Encoder: scheme.DefaultJSONEncoder(),
|
||||
OpenAPI: o.OpenAPISchema,
|
||||
Force: force,
|
||||
ServerSideApply: o.ServerSideApply,
|
||||
ForceConflicts: o.ForceConflicts,
|
||||
}
|
||||
|
||||
err = differ.Diff(obj, printer)
|
||||
if !isConflict(err) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return differ.Run(o.Diff)
|
||||
}
|
||||
196
pkg/cmd/diff/diff_test.go
Normal file
196
pkg/cmd/diff/diff_test.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
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 diff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
type FakeObject struct {
|
||||
name string
|
||||
merged map[string]interface{}
|
||||
live map[string]interface{}
|
||||
}
|
||||
|
||||
var _ Object = &FakeObject{}
|
||||
|
||||
func (f *FakeObject) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *FakeObject) Merged() (runtime.Object, error) {
|
||||
return &unstructured.Unstructured{Object: f.merged}, nil
|
||||
}
|
||||
|
||||
func (f *FakeObject) Live() runtime.Object {
|
||||
return &unstructured.Unstructured{Object: f.live}
|
||||
}
|
||||
|
||||
func TestDiffProgram(t *testing.T) {
|
||||
os.Setenv("KUBECTL_EXTERNAL_DIFF", "echo")
|
||||
streams, _, stdout, _ := genericclioptions.NewTestIOStreams()
|
||||
diff := DiffProgram{
|
||||
IOStreams: streams,
|
||||
Exec: exec.New(),
|
||||
}
|
||||
err := diff.Run("one", "two")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if output := stdout.String(); output != "one two\n" {
|
||||
t.Fatalf(`stdout = %q, expected "one two\n"`, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrinter(t *testing.T) {
|
||||
printer := Printer{}
|
||||
|
||||
obj := &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"string": "string",
|
||||
"list": []int{1, 2, 3},
|
||||
"int": 12,
|
||||
}}
|
||||
buf := bytes.Buffer{}
|
||||
printer.Print(obj, &buf)
|
||||
want := `int: 12
|
||||
list:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
string: string
|
||||
`
|
||||
if buf.String() != want {
|
||||
t.Errorf("Print() = %q, want %q", buf.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffVersion(t *testing.T) {
|
||||
diff, err := NewDiffVersion("MERGED")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer diff.Dir.Delete()
|
||||
|
||||
obj := FakeObject{
|
||||
name: "bla",
|
||||
live: map[string]interface{}{"live": true},
|
||||
merged: map[string]interface{}{"merged": true},
|
||||
}
|
||||
err = diff.Print(&obj, Printer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fcontent, err := ioutil.ReadFile(path.Join(diff.Dir.Name, obj.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
econtent := "merged: true\n"
|
||||
if string(fcontent) != econtent {
|
||||
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectory(t *testing.T) {
|
||||
dir, err := CreateDirectory("prefix")
|
||||
defer dir.Delete()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = os.Stat(dir.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(filepath.Base(dir.Name), "prefix") {
|
||||
t.Fatalf(`Directory doesn't start with "prefix": %q`, dir.Name)
|
||||
}
|
||||
entries, err := ioutil.ReadDir(dir.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
t.Fatalf("Directory should be empty, has %d elements", len(entries))
|
||||
}
|
||||
_, err = dir.NewFile("ONE")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = dir.NewFile("TWO")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries, err = ioutil.ReadDir(dir.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 2 {
|
||||
t.Fatalf("ReadDir should have two elements, has %d elements", len(entries))
|
||||
}
|
||||
err = dir.Delete()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = os.Stat(dir.Name)
|
||||
if err == nil {
|
||||
t.Fatal("Directory should be gone, still present.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffer(t *testing.T) {
|
||||
diff, err := NewDiffer("LIVE", "MERGED")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer diff.TearDown()
|
||||
|
||||
obj := FakeObject{
|
||||
name: "bla",
|
||||
live: map[string]interface{}{"live": true},
|
||||
merged: map[string]interface{}{"merged": true},
|
||||
}
|
||||
err = diff.Diff(&obj, Printer{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fcontent, err := ioutil.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
econtent := "live: true\n"
|
||||
if string(fcontent) != econtent {
|
||||
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
||||
}
|
||||
|
||||
fcontent, err = ioutil.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
econtent = "merged: true\n"
|
||||
if string(fcontent) != econtent {
|
||||
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
||||
}
|
||||
}
|
||||
535
pkg/cmd/drain/drain.go
Normal file
535
pkg/cmd/drain/drain.go
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
/*
|
||||
Copyright 2015 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 drain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/drain"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
type DrainCmdOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
ToPrinter func(string) (printers.ResourcePrinterFunc, error)
|
||||
|
||||
Namespace string
|
||||
|
||||
drainer *drain.Helper
|
||||
nodeInfos []*resource.Info
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
cordonLong = templates.LongDesc(i18n.T(`
|
||||
Mark node as unschedulable.`))
|
||||
|
||||
cordonExample = templates.Examples(i18n.T(`
|
||||
# Mark node "foo" as unschedulable.
|
||||
kubectl cordon foo`))
|
||||
)
|
||||
|
||||
func NewCmdCordon(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewDrainCmdOptions(f, ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "cordon NODE",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Mark node as unschedulable"),
|
||||
Long: cordonLong,
|
||||
Example: cordonExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.RunCordonOrUncordon(true))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&o.drainer.Selector, "selector", "l", o.drainer.Selector, "Selector (label query) to filter on")
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
var (
|
||||
uncordonLong = templates.LongDesc(i18n.T(`
|
||||
Mark node as schedulable.`))
|
||||
|
||||
uncordonExample = templates.Examples(i18n.T(`
|
||||
# Mark node "foo" as schedulable.
|
||||
$ kubectl uncordon foo`))
|
||||
)
|
||||
|
||||
func NewCmdUncordon(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewDrainCmdOptions(f, ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "uncordon NODE",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Mark node as schedulable"),
|
||||
Long: uncordonLong,
|
||||
Example: uncordonExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.RunCordonOrUncordon(false))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&o.drainer.Selector, "selector", "l", o.drainer.Selector, "Selector (label query) to filter on")
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
var (
|
||||
drainLong = templates.LongDesc(i18n.T(`
|
||||
Drain node in preparation for maintenance.
|
||||
|
||||
The given node will be marked unschedulable to prevent new pods from arriving.
|
||||
'drain' evicts the pods if the APIServer supports
|
||||
[eviction](http://kubernetes.io/docs/admin/disruptions/). Otherwise, it will use normal
|
||||
DELETE to delete the pods.
|
||||
The 'drain' evicts or deletes all pods except mirror pods (which cannot be deleted through
|
||||
the API server). If there are DaemonSet-managed pods, drain will not proceed
|
||||
without --ignore-daemonsets, and regardless it will not delete any
|
||||
DaemonSet-managed pods, because those pods would be immediately replaced by the
|
||||
DaemonSet controller, which ignores unschedulable markings. If there are any
|
||||
pods that are neither mirror pods nor managed by ReplicationController,
|
||||
ReplicaSet, DaemonSet, StatefulSet or Job, then drain will not delete any pods unless you
|
||||
use --force. --force will also allow deletion to proceed if the managing resource of one
|
||||
or more pods is missing.
|
||||
|
||||
'drain' waits for graceful termination. You should not operate on the machine until
|
||||
the command completes.
|
||||
|
||||
When you are ready to put the node back into service, use kubectl uncordon, which
|
||||
will make the node schedulable again.
|
||||
|
||||
`))
|
||||
|
||||
drainExample = templates.Examples(i18n.T(`
|
||||
# Drain node "foo", even if there are pods not managed by a ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet on it.
|
||||
$ kubectl drain foo --force
|
||||
|
||||
# As above, but abort if there are pods not managed by a ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet, and use a grace period of 15 minutes.
|
||||
$ kubectl drain foo --grace-period=900`))
|
||||
)
|
||||
|
||||
func NewDrainCmdOptions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *DrainCmdOptions {
|
||||
return &DrainCmdOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("drained").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: ioStreams,
|
||||
drainer: &drain.Helper{
|
||||
GracePeriodSeconds: -1,
|
||||
ErrOut: ioStreams.ErrOut,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewCmdDrain(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := NewDrainCmdOptions(f, ioStreams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "drain NODE",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Drain node in preparation for maintenance"),
|
||||
Long: drainLong,
|
||||
Example: drainExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.RunDrain())
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&o.drainer.Force, "force", o.drainer.Force, "Continue even if there are pods not managed by a ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet.")
|
||||
cmd.Flags().BoolVar(&o.drainer.IgnoreAllDaemonSets, "ignore-daemonsets", o.drainer.IgnoreAllDaemonSets, "Ignore DaemonSet-managed pods.")
|
||||
cmd.Flags().BoolVar(&o.drainer.DeleteLocalData, "delete-local-data", o.drainer.DeleteLocalData, "Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained).")
|
||||
cmd.Flags().IntVar(&o.drainer.GracePeriodSeconds, "grace-period", o.drainer.GracePeriodSeconds, "Period of time in seconds given to each pod to terminate gracefully. If negative, the default value specified in the pod will be used.")
|
||||
cmd.Flags().DurationVar(&o.drainer.Timeout, "timeout", o.drainer.Timeout, "The length of time to wait before giving up, zero means infinite")
|
||||
cmd.Flags().StringVarP(&o.drainer.Selector, "selector", "l", o.drainer.Selector, "Selector (label query) to filter on")
|
||||
cmd.Flags().StringVarP(&o.drainer.PodSelector, "pod-selector", "", o.drainer.PodSelector, "Label selector to filter pods on the node")
|
||||
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Complete populates some fields from the factory, grabs command line
|
||||
// arguments and looks up the node using Builder
|
||||
func (o *DrainCmdOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) == 0 && !cmd.Flags().Changed("selector") {
|
||||
return cmdutil.UsageErrorf(cmd, fmt.Sprintf("USAGE: %s [flags]", cmd.Use))
|
||||
}
|
||||
if len(args) > 0 && len(o.drainer.Selector) > 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "error: cannot specify both a node name and a --selector option")
|
||||
}
|
||||
|
||||
o.drainer.DryRun = cmdutil.GetDryRunFlag(cmd)
|
||||
|
||||
if o.drainer.Client, err = f.KubernetesClientSet(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(o.drainer.PodSelector) > 0 {
|
||||
if _, err := labels.Parse(o.drainer.PodSelector); err != nil {
|
||||
return errors.New("--pod-selector=<pod_selector> must be a valid label selector")
|
||||
}
|
||||
}
|
||||
|
||||
o.nodeInfos = []*resource.Info{}
|
||||
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.ToPrinter = func(operation string) (printers.ResourcePrinterFunc, error) {
|
||||
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||
if o.drainer.DryRun {
|
||||
o.PrintFlags.Complete("%s (dry run)")
|
||||
}
|
||||
|
||||
printer, err := o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return printer.PrintObj, nil
|
||||
}
|
||||
|
||||
builder := f.NewBuilder().
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||
ResourceNames("nodes", args...).
|
||||
SingleResourceType().
|
||||
Flatten()
|
||||
|
||||
if len(o.drainer.Selector) > 0 {
|
||||
builder = builder.LabelSelectorParam(o.drainer.Selector).
|
||||
ResourceTypes("nodes")
|
||||
}
|
||||
|
||||
r := builder.Do()
|
||||
|
||||
if err = r.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mapping.Resource.GroupResource() != (schema.GroupResource{Group: "", Resource: "nodes"}) {
|
||||
return fmt.Errorf("error: expected resource of type node, got %q", info.Mapping.Resource)
|
||||
}
|
||||
|
||||
o.nodeInfos = append(o.nodeInfos, info)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RunDrain runs the 'drain' command
|
||||
func (o *DrainCmdOptions) RunDrain() error {
|
||||
if err := o.RunCordonOrUncordon(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printObj, err := o.ToPrinter("drained")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
drainedNodes := sets.NewString()
|
||||
var fatal error
|
||||
|
||||
for _, info := range o.nodeInfos {
|
||||
var err error
|
||||
if !o.drainer.DryRun {
|
||||
err = o.deleteOrEvictPodsSimple(info)
|
||||
}
|
||||
if err == nil || o.drainer.DryRun {
|
||||
drainedNodes.Insert(info.Name)
|
||||
printObj(info.Object, o.Out)
|
||||
} else {
|
||||
fmt.Fprintf(o.ErrOut, "error: unable to drain node %q, aborting command...\n\n", info.Name)
|
||||
remainingNodes := []string{}
|
||||
fatal = err
|
||||
for _, remainingInfo := range o.nodeInfos {
|
||||
if drainedNodes.Has(remainingInfo.Name) {
|
||||
continue
|
||||
}
|
||||
remainingNodes = append(remainingNodes, remainingInfo.Name)
|
||||
}
|
||||
|
||||
if len(remainingNodes) > 0 {
|
||||
fmt.Fprintf(o.ErrOut, "There are pending nodes to be drained:\n")
|
||||
for _, nodeName := range remainingNodes {
|
||||
fmt.Fprintf(o.ErrOut, " %s\n", nodeName)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return fatal
|
||||
}
|
||||
|
||||
func (o *DrainCmdOptions) deleteOrEvictPodsSimple(nodeInfo *resource.Info) error {
|
||||
list, errs := o.drainer.GetPodsForDeletion(nodeInfo.Name)
|
||||
if errs != nil {
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
if warnings := list.Warnings(); warnings != "" {
|
||||
fmt.Fprintf(o.ErrOut, "WARNING: %s\n", warnings)
|
||||
}
|
||||
|
||||
if err := o.deleteOrEvictPods(list.Pods()); err != nil {
|
||||
pendingList, newErrs := o.drainer.GetPodsForDeletion(nodeInfo.Name)
|
||||
|
||||
fmt.Fprintf(o.ErrOut, "There are pending pods in node %q when an error occurred: %v\n", nodeInfo.Name, err)
|
||||
for _, pendingPod := range pendingList.Pods() {
|
||||
fmt.Fprintf(o.ErrOut, "%s/%s\n", "pod", pendingPod.Name)
|
||||
}
|
||||
if newErrs != nil {
|
||||
fmt.Fprintf(o.ErrOut, "following errors also occurred:\n%s", utilerrors.NewAggregate(newErrs))
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteOrEvictPods deletes or evicts the pods on the api server
|
||||
func (o *DrainCmdOptions) deleteOrEvictPods(pods []corev1.Pod) error {
|
||||
if len(pods) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
policyGroupVersion, err := drain.CheckEvictionSupport(o.drainer.Client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
getPodFn := func(namespace, name string) (*corev1.Pod, error) {
|
||||
return o.drainer.Client.CoreV1().Pods(namespace).Get(name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
if len(policyGroupVersion) > 0 {
|
||||
return o.evictPods(pods, policyGroupVersion, getPodFn)
|
||||
} else {
|
||||
return o.deletePods(pods, getPodFn)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *DrainCmdOptions) evictPods(pods []corev1.Pod, policyGroupVersion string, getPodFn func(namespace, name string) (*corev1.Pod, error)) error {
|
||||
returnCh := make(chan error, 1)
|
||||
|
||||
for _, pod := range pods {
|
||||
go func(pod corev1.Pod, returnCh chan error) {
|
||||
for {
|
||||
fmt.Fprintf(o.Out, "evicting pod %q\n", pod.Name)
|
||||
err := o.drainer.EvictPod(pod, policyGroupVersion)
|
||||
if err == nil {
|
||||
break
|
||||
} else if apierrors.IsNotFound(err) {
|
||||
returnCh <- nil
|
||||
return
|
||||
} else if apierrors.IsTooManyRequests(err) {
|
||||
fmt.Fprintf(o.ErrOut, "error when evicting pod %q (will retry after 5s): %v\n", pod.Name, err)
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
returnCh <- fmt.Errorf("error when evicting pod %q: %v", pod.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err := o.waitForDelete([]corev1.Pod{pod}, 1*time.Second, time.Duration(math.MaxInt64), true, getPodFn)
|
||||
if err == nil {
|
||||
returnCh <- nil
|
||||
} else {
|
||||
returnCh <- fmt.Errorf("error when waiting for pod %q terminating: %v", pod.Name, err)
|
||||
}
|
||||
}(pod, returnCh)
|
||||
}
|
||||
|
||||
doneCount := 0
|
||||
var errors []error
|
||||
|
||||
// 0 timeout means infinite, we use MaxInt64 to represent it.
|
||||
var globalTimeout time.Duration
|
||||
if o.drainer.Timeout == 0 {
|
||||
globalTimeout = time.Duration(math.MaxInt64)
|
||||
} else {
|
||||
globalTimeout = o.drainer.Timeout
|
||||
}
|
||||
globalTimeoutCh := time.After(globalTimeout)
|
||||
numPods := len(pods)
|
||||
for doneCount < numPods {
|
||||
select {
|
||||
case err := <-returnCh:
|
||||
doneCount++
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
case <-globalTimeoutCh:
|
||||
return fmt.Errorf("drain did not complete within %v", globalTimeout)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(errors)
|
||||
}
|
||||
|
||||
func (o *DrainCmdOptions) deletePods(pods []corev1.Pod, getPodFn func(namespace, name string) (*corev1.Pod, error)) error {
|
||||
// 0 timeout means infinite, we use MaxInt64 to represent it.
|
||||
var globalTimeout time.Duration
|
||||
if o.drainer.Timeout == 0 {
|
||||
globalTimeout = time.Duration(math.MaxInt64)
|
||||
} else {
|
||||
globalTimeout = o.drainer.Timeout
|
||||
}
|
||||
for _, pod := range pods {
|
||||
err := o.drainer.DeletePod(pod)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := o.waitForDelete(pods, 1*time.Second, globalTimeout, false, getPodFn)
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *DrainCmdOptions) waitForDelete(pods []corev1.Pod, interval, timeout time.Duration, usingEviction bool, getPodFn func(string, string) (*corev1.Pod, error)) ([]corev1.Pod, error) {
|
||||
var verbStr string
|
||||
if usingEviction {
|
||||
verbStr = "evicted"
|
||||
} else {
|
||||
verbStr = "deleted"
|
||||
}
|
||||
printObj, err := o.ToPrinter(verbStr)
|
||||
if err != nil {
|
||||
return pods, err
|
||||
}
|
||||
|
||||
err = wait.PollImmediate(interval, timeout, func() (bool, error) {
|
||||
pendingPods := []corev1.Pod{}
|
||||
for i, pod := range pods {
|
||||
p, err := getPodFn(pod.Namespace, pod.Name)
|
||||
if apierrors.IsNotFound(err) || (p != nil && p.ObjectMeta.UID != pod.ObjectMeta.UID) {
|
||||
printObj(&pod, o.Out)
|
||||
continue
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
} else {
|
||||
pendingPods = append(pendingPods, pods[i])
|
||||
}
|
||||
}
|
||||
pods = pendingPods
|
||||
if len(pendingPods) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
return pods, err
|
||||
}
|
||||
|
||||
// RunCordonOrUncordon runs either Cordon or Uncordon. The desired value for
|
||||
// "Unschedulable" is passed as the first arg.
|
||||
func (o *DrainCmdOptions) RunCordonOrUncordon(desired bool) error {
|
||||
cordonOrUncordon := "cordon"
|
||||
if !desired {
|
||||
cordonOrUncordon = "un" + cordonOrUncordon
|
||||
}
|
||||
|
||||
for _, nodeInfo := range o.nodeInfos {
|
||||
|
||||
printError := func(err error) {
|
||||
fmt.Fprintf(o.ErrOut, "error: unable to %s node %q: %v\n", cordonOrUncordon, nodeInfo.Name, err)
|
||||
}
|
||||
|
||||
gvk := nodeInfo.ResourceMapping().GroupVersionKind
|
||||
if gvk.Kind == "Node" {
|
||||
c, err := drain.NewCordonHelperFromRuntimeObject(nodeInfo.Object, scheme.Scheme, gvk)
|
||||
if err != nil {
|
||||
printError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if updateRequired := c.UpdateIfRequired(desired); !updateRequired {
|
||||
printObj, err := o.ToPrinter(already(desired))
|
||||
if err != nil {
|
||||
fmt.Fprintf(o.ErrOut, "error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
printObj(nodeInfo.Object, o.Out)
|
||||
} else {
|
||||
if !o.drainer.DryRun {
|
||||
err, patchErr := c.PatchOrReplace(o.drainer.Client)
|
||||
if patchErr != nil {
|
||||
printError(patchErr)
|
||||
}
|
||||
if err != nil {
|
||||
printError(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
printObj, err := o.ToPrinter(changed(desired))
|
||||
if err != nil {
|
||||
fmt.Fprintf(o.ErrOut, "%v\n", err)
|
||||
continue
|
||||
}
|
||||
printObj(nodeInfo.Object, o.Out)
|
||||
}
|
||||
} else {
|
||||
printObj, err := o.ToPrinter("skipped")
|
||||
if err != nil {
|
||||
fmt.Fprintf(o.ErrOut, "%v\n", err)
|
||||
continue
|
||||
}
|
||||
printObj(nodeInfo.Object, o.Out)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// already() and changed() return suitable strings for {un,}cordoning
|
||||
|
||||
func already(desired bool) string {
|
||||
if desired {
|
||||
return "already cordoned"
|
||||
}
|
||||
return "already uncordoned"
|
||||
}
|
||||
|
||||
func changed(desired bool) string {
|
||||
if desired {
|
||||
return "cordoned"
|
||||
}
|
||||
return "uncordoned"
|
||||
}
|
||||
1050
pkg/cmd/drain/drain_test.go
Normal file
1050
pkg/cmd/drain/drain_test.go
Normal file
File diff suppressed because it is too large
Load diff
105
pkg/cmd/edit/edit.go
Normal file
105
pkg/cmd/edit/edit.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
Copyright 2015 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 edit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/cmd/util/editor"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
editLong = templates.LongDesc(i18n.T(`
|
||||
Edit a resource from the default editor.
|
||||
|
||||
The edit command allows you to directly edit any API resource you can retrieve via the
|
||||
command line tools. It will open the editor defined by your KUBE_EDITOR, or EDITOR
|
||||
environment variables, or fall back to 'vi' for Linux or 'notepad' for Windows.
|
||||
You can edit multiple objects, although changes are applied one at a time. The command
|
||||
accepts filenames as well as command line arguments, although the files you point to must
|
||||
be previously saved versions of resources.
|
||||
|
||||
Editing is done with the API version used to fetch the resource.
|
||||
To edit using a specific API version, fully-qualify the resource, version, and group.
|
||||
|
||||
The default format is YAML. To edit in JSON, specify "-o json".
|
||||
|
||||
The flag --windows-line-endings can be used to force Windows line endings,
|
||||
otherwise the default for your operating system will be used.
|
||||
|
||||
In the event an error occurs while updating, a temporary file will be created on disk
|
||||
that contains your unapplied changes. The most common error when updating a resource
|
||||
is another editor changing the resource on the server. When this occurs, you will have
|
||||
to apply your changes to the newer version of the resource, or update your temporary
|
||||
saved copy to include the latest resource version.`))
|
||||
|
||||
editExample = templates.Examples(i18n.T(`
|
||||
# Edit the service named 'docker-registry':
|
||||
kubectl edit svc/docker-registry
|
||||
|
||||
# Use an alternative editor
|
||||
KUBE_EDITOR="nano" kubectl edit svc/docker-registry
|
||||
|
||||
# Edit the job 'myjob' in JSON using the v1 API format:
|
||||
kubectl edit job.v1.batch/myjob -o json
|
||||
|
||||
# Edit the deployment 'mydeployment' in YAML and save the modified config in its annotation:
|
||||
kubectl edit deployment/mydeployment -o yaml --save-config`))
|
||||
)
|
||||
|
||||
// NewCmdEdit creates the `edit` command
|
||||
func NewCmdEdit(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := editor.NewEditOptions(editor.NormalEditMode, ioStreams)
|
||||
o.ValidateOptions = cmdutil.ValidateOptions{EnableValidation: true}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "edit (RESOURCE/NAME | -f FILENAME)",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Edit a resource on the server"),
|
||||
Long: editLong,
|
||||
Example: fmt.Sprintf(editExample),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := o.Complete(f, args, cmd); err != nil {
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
if err := o.Run(); err != nil {
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// bind flag structs
|
||||
o.RecordFlags.AddFlags(cmd)
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
usage := "to use to edit the resource"
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||
cmdutil.AddValidateOptionFlags(cmd, &o.ValidateOptions)
|
||||
cmd.Flags().BoolVarP(&o.OutputPatch, "output-patch", "", o.OutputPatch, "Output the patch if the resource is edited.")
|
||||
cmd.Flags().BoolVar(&o.WindowsLineEndings, "windows-line-endings", o.WindowsLineEndings,
|
||||
"Defaults to the line ending native to your platform.")
|
||||
|
||||
cmdutil.AddApplyAnnotationVarFlags(cmd, &o.ApplyAnnotation)
|
||||
cmdutil.AddIncludeUninitializedFlag(cmd)
|
||||
return cmd
|
||||
}
|
||||
292
pkg/cmd/edit/edit_test.go
Normal file
292
pkg/cmd/edit/edit_test.go
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
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 edit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/apply"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/create"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
type EditTestCase struct {
|
||||
Description string `yaml:"description"`
|
||||
// create or edit
|
||||
Mode string `yaml:"mode"`
|
||||
Args []string `yaml:"args"`
|
||||
Filename string `yaml:"filename"`
|
||||
Output string `yaml:"outputFormat"`
|
||||
OutputPatch string `yaml:"outputPatch"`
|
||||
SaveConfig string `yaml:"saveConfig"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
ExpectedStdout []string `yaml:"expectedStdout"`
|
||||
ExpectedStderr []string `yaml:"expectedStderr"`
|
||||
ExpectedExitCode int `yaml:"expectedExitCode"`
|
||||
|
||||
Steps []EditStep `yaml:"steps"`
|
||||
}
|
||||
|
||||
type EditStep struct {
|
||||
// edit or request
|
||||
StepType string `yaml:"type"`
|
||||
|
||||
// only applies to request
|
||||
RequestMethod string `yaml:"expectedMethod,omitempty"`
|
||||
RequestPath string `yaml:"expectedPath,omitempty"`
|
||||
RequestContentType string `yaml:"expectedContentType,omitempty"`
|
||||
Input string `yaml:"expectedInput"`
|
||||
|
||||
// only applies to request
|
||||
ResponseStatusCode int `yaml:"resultingStatusCode,omitempty"`
|
||||
|
||||
Output string `yaml:"resultingOutput"`
|
||||
}
|
||||
|
||||
func TestEdit(t *testing.T) {
|
||||
var (
|
||||
name string
|
||||
testcase EditTestCase
|
||||
i int
|
||||
err error
|
||||
)
|
||||
|
||||
const updateEnvVar = "UPDATE_EDIT_FIXTURE_DATA"
|
||||
updateInputFixtures := os.Getenv(updateEnvVar) == "true"
|
||||
|
||||
reqResp := func(req *http.Request) (*http.Response, error) {
|
||||
defer func() { i++ }()
|
||||
if i > len(testcase.Steps)-1 {
|
||||
t.Fatalf("%s, step %d: more requests than steps, got %s %s", name, i, req.Method, req.URL.Path)
|
||||
}
|
||||
step := testcase.Steps[i]
|
||||
|
||||
body := []byte{}
|
||||
if req.Body != nil {
|
||||
body, err = ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("%s, step %d: %v", name, i, err)
|
||||
}
|
||||
}
|
||||
|
||||
inputFile := filepath.Join("testdata", "testcase-"+name, step.Input)
|
||||
expectedInput, err := ioutil.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("%s, step %d: %v", name, i, err)
|
||||
}
|
||||
|
||||
outputFile := filepath.Join("testdata", "testcase-"+name, step.Output)
|
||||
resultingOutput, err := ioutil.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("%s, step %d: %v", name, i, err)
|
||||
}
|
||||
|
||||
if req.Method == "POST" && req.URL.Path == "/callback" {
|
||||
if step.StepType != "edit" {
|
||||
t.Fatalf("%s, step %d: expected edit step, got %s %s", name, i, req.Method, req.URL.Path)
|
||||
}
|
||||
if !bytes.Equal(body, expectedInput) {
|
||||
if updateInputFixtures {
|
||||
// Convenience to allow recapturing the input and persisting it here
|
||||
ioutil.WriteFile(inputFile, body, os.FileMode(0644))
|
||||
} else {
|
||||
t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, diff.StringDiff(string(body), string(expectedInput)))
|
||||
t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar)
|
||||
}
|
||||
}
|
||||
return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(resultingOutput))}, nil
|
||||
}
|
||||
if step.StepType != "request" {
|
||||
t.Fatalf("%s, step %d: expected request step, got %s %s", name, i, req.Method, req.URL.Path)
|
||||
}
|
||||
body = tryIndent(body)
|
||||
expectedInput = tryIndent(expectedInput)
|
||||
if req.Method != step.RequestMethod || req.URL.Path != step.RequestPath || req.Header.Get("Content-Type") != step.RequestContentType {
|
||||
t.Fatalf(
|
||||
"%s, step %d: expected \n%s %s (content-type=%s)\ngot\n%s %s (content-type=%s)", name, i,
|
||||
step.RequestMethod, step.RequestPath, step.RequestContentType,
|
||||
req.Method, req.URL.Path, req.Header.Get("Content-Type"),
|
||||
)
|
||||
}
|
||||
if !bytes.Equal(body, expectedInput) {
|
||||
if updateInputFixtures {
|
||||
// Convenience to allow recapturing the input and persisting it here
|
||||
ioutil.WriteFile(inputFile, body, os.FileMode(0644))
|
||||
} else {
|
||||
t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, diff.StringDiff(string(body), string(expectedInput)))
|
||||
t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar)
|
||||
}
|
||||
}
|
||||
return &http.Response{StatusCode: step.ResponseStatusCode, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader(resultingOutput))}, nil
|
||||
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
resp, _ := reqResp(req)
|
||||
for k, vs := range resp.Header {
|
||||
w.Header().Del(k)
|
||||
for _, v := range vs {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
})
|
||||
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
os.Setenv("KUBE_EDITOR", "testdata/test_editor.sh")
|
||||
os.Setenv("KUBE_EDITOR_CALLBACK", server.URL+"/callback")
|
||||
|
||||
testcases := sets.NewString()
|
||||
filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if path == "testdata" {
|
||||
return nil
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
if info.IsDir() {
|
||||
if strings.HasPrefix(name, "testcase-") {
|
||||
testcases.Insert(strings.TrimPrefix(name, "testcase-"))
|
||||
}
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// sanity check that we found the right folder
|
||||
if !testcases.Has("create-list") {
|
||||
t.Fatalf("Error locating edit testcases")
|
||||
}
|
||||
|
||||
for _, testcaseName := range testcases.List() {
|
||||
t.Run(testcaseName, func(t *testing.T) {
|
||||
i = 0
|
||||
name = testcaseName
|
||||
testcase = EditTestCase{}
|
||||
testcaseDir := filepath.Join("testdata", "testcase-"+name)
|
||||
testcaseData, err := ioutil.ReadFile(filepath.Join(testcaseDir, "test.yaml"))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
if err := yaml.Unmarshal(testcaseData, &testcase); err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
|
||||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
|
||||
tf.UnstructuredClientForMappingFunc = func(gv schema.GroupVersion) (resource.RESTClient, error) {
|
||||
versionedAPIPath := ""
|
||||
if gv.Group == "" {
|
||||
versionedAPIPath = "/api/" + gv.Version
|
||||
} else {
|
||||
versionedAPIPath = "/apis/" + gv.Group + "/" + gv.Version
|
||||
}
|
||||
return &fake.RESTClient{
|
||||
VersionedAPIPath: versionedAPIPath,
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(reqResp),
|
||||
}, nil
|
||||
}
|
||||
tf.WithNamespace(testcase.Namespace)
|
||||
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
|
||||
ioStreams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
|
||||
|
||||
var cmd *cobra.Command
|
||||
switch testcase.Mode {
|
||||
case "edit":
|
||||
cmd = NewCmdEdit(tf, ioStreams)
|
||||
case "create":
|
||||
cmd = create.NewCmdCreate(tf, ioStreams)
|
||||
cmd.Flags().Set("edit", "true")
|
||||
case "edit-last-applied":
|
||||
cmd = apply.NewCmdApplyEditLastApplied(tf, ioStreams)
|
||||
default:
|
||||
t.Fatalf("%s: unexpected mode %s", name, testcase.Mode)
|
||||
}
|
||||
if len(testcase.Filename) > 0 {
|
||||
cmd.Flags().Set("filename", filepath.Join(testcaseDir, testcase.Filename))
|
||||
}
|
||||
if len(testcase.Output) > 0 {
|
||||
cmd.Flags().Set("output", testcase.Output)
|
||||
}
|
||||
if len(testcase.OutputPatch) > 0 {
|
||||
cmd.Flags().Set("output-patch", testcase.OutputPatch)
|
||||
}
|
||||
if len(testcase.SaveConfig) > 0 {
|
||||
cmd.Flags().Set("save-config", testcase.SaveConfig)
|
||||
}
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
errBuf.WriteString(str)
|
||||
if testcase.ExpectedExitCode != code {
|
||||
t.Errorf("%s: expected exit code %d, got %d: %s", name, testcase.ExpectedExitCode, code, str)
|
||||
}
|
||||
})
|
||||
|
||||
cmd.Run(cmd, testcase.Args)
|
||||
|
||||
stdout := buf.String()
|
||||
stderr := errBuf.String()
|
||||
|
||||
for _, s := range testcase.ExpectedStdout {
|
||||
if !strings.Contains(stdout, s) {
|
||||
t.Errorf("%s: expected to see '%s' in stdout\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr)
|
||||
}
|
||||
}
|
||||
for _, s := range testcase.ExpectedStderr {
|
||||
if !strings.Contains(stderr, s) {
|
||||
t.Errorf("%s: expected to see '%s' in stderr\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr)
|
||||
}
|
||||
}
|
||||
if i < len(testcase.Steps) {
|
||||
t.Errorf("%s: saw %d steps, testcase included %d additional steps that were not exercised", name, i, len(testcase.Steps)-i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func tryIndent(data []byte) []byte {
|
||||
indented := &bytes.Buffer{}
|
||||
if err := json.Indent(indented, data, "", "\t"); err == nil {
|
||||
return indented.Bytes()
|
||||
}
|
||||
return data
|
||||
}
|
||||
22
pkg/cmd/edit/testdata/README
vendored
Normal file
22
pkg/cmd/edit/testdata/README
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
This folder contains test cases for interactive edit, and helpers for recording new test cases
|
||||
|
||||
To record a new test:
|
||||
|
||||
1. Start a local cluster running unsecured on http://localhost:8080 (e.g. hack/local-up-cluster.sh)
|
||||
2. Set up any pre-existing resources you want to be available on that server (namespaces, resources to edit, etc)
|
||||
3. Run ./pkg/kubectl/cmd/testdata/edit/record_testcase.sh my-testcase
|
||||
4. Run the desired `kubectl edit ...` command, and interact with the editor as desired until it completes.
|
||||
* You can do things that cause errors to appear in the editor (change immutable fields, fail validation, etc)
|
||||
* You can perform edit flows that invoke the editor multiple times
|
||||
* You can make out-of-band changes to the server resources that cause conflict errors to be returned
|
||||
* The API requests/responses and editor inputs/outputs are captured in your testcase folder
|
||||
5. Type exit.
|
||||
6. Inspect the captured requests/responses and inputs/outputs for sanity
|
||||
7. Modify the generated test.yaml file:
|
||||
* Set a description of what the test is doing
|
||||
* Enter the args (if any) you invoked edit with
|
||||
* Enter the filename (if any) you invoked edit with
|
||||
* Enter the output format (if any) you invoked edit with
|
||||
* Optionally specify substrings to look for in the stdout or stderr of the edit command
|
||||
8. Add your new testcase name to the list of testcases in edit_test.go
|
||||
9. Run `go test ./pkg/kubectl/cmd -run TestEdit -v` to run edit tests
|
||||
169
pkg/cmd/edit/testdata/record.go
vendored
Normal file
169
pkg/cmd/edit/testdata/record.go
vendored
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type EditTestCase struct {
|
||||
Description string `yaml:"description"`
|
||||
// create or edit
|
||||
Mode string `yaml:"mode"`
|
||||
Args []string `yaml:"args"`
|
||||
Filename string `yaml:"filename"`
|
||||
Output string `yaml:"outputFormat"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
ExpectedStdout []string `yaml:"expectedStdout"`
|
||||
ExpectedStderr []string `yaml:"expectedStderr"`
|
||||
ExpectedExitCode int `yaml:"expectedExitCode"`
|
||||
|
||||
Steps []EditStep `yaml:"steps"`
|
||||
}
|
||||
|
||||
type EditStep struct {
|
||||
// edit or request
|
||||
StepType string `yaml:"type"`
|
||||
|
||||
// only applies to request
|
||||
RequestMethod string `yaml:"expectedMethod,omitempty"`
|
||||
RequestPath string `yaml:"expectedPath,omitempty"`
|
||||
RequestContentType string `yaml:"expectedContentType,omitempty"`
|
||||
Input string `yaml:"expectedInput"`
|
||||
|
||||
// only applies to request
|
||||
ResponseStatusCode int `yaml:"resultingStatusCode,omitempty"`
|
||||
|
||||
Output string `yaml:"resultingOutput"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
tc := &EditTestCase{
|
||||
Description: "add a testcase description",
|
||||
Mode: "edit",
|
||||
Args: []string{"set", "args"},
|
||||
ExpectedStdout: []string{"expected stdout substring"},
|
||||
ExpectedStderr: []string{"expected stderr substring"},
|
||||
}
|
||||
|
||||
var currentStep *EditStep
|
||||
|
||||
fmt.Println(http.ListenAndServe(":8081", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// Record non-discovery things
|
||||
record := false
|
||||
switch segments := strings.Split(strings.Trim(req.URL.Path, "/"), "/"); segments[0] {
|
||||
case "api":
|
||||
// api, version
|
||||
record = len(segments) > 2
|
||||
case "apis":
|
||||
// apis, group, version
|
||||
record = len(segments) > 3
|
||||
case "callback":
|
||||
record = true
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
checkErr(err)
|
||||
|
||||
switch m, p := req.Method, req.URL.Path; {
|
||||
case m == "POST" && p == "/callback/in":
|
||||
if currentStep != nil {
|
||||
panic("cannot post input with step already in progress")
|
||||
}
|
||||
filename := fmt.Sprintf("%d.original", len(tc.Steps))
|
||||
checkErr(ioutil.WriteFile(filename, body, os.FileMode(0755)))
|
||||
currentStep = &EditStep{StepType: "edit", Input: filename}
|
||||
case m == "POST" && p == "/callback/out":
|
||||
if currentStep == nil || currentStep.StepType != "edit" {
|
||||
panic("cannot post output without posting input first")
|
||||
}
|
||||
filename := fmt.Sprintf("%d.edited", len(tc.Steps))
|
||||
checkErr(ioutil.WriteFile(filename, body, os.FileMode(0755)))
|
||||
currentStep.Output = filename
|
||||
tc.Steps = append(tc.Steps, *currentStep)
|
||||
currentStep = nil
|
||||
default:
|
||||
if currentStep != nil {
|
||||
panic("cannot make request with step already in progress")
|
||||
}
|
||||
|
||||
urlCopy := *req.URL
|
||||
urlCopy.Host = "localhost:8080"
|
||||
urlCopy.Scheme = "http"
|
||||
proxiedReq, err := http.NewRequest(req.Method, urlCopy.String(), bytes.NewReader(body))
|
||||
checkErr(err)
|
||||
proxiedReq.Header = req.Header
|
||||
resp, err := http.DefaultClient.Do(proxiedReq)
|
||||
checkErr(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyOut, err := ioutil.ReadAll(resp.Body)
|
||||
checkErr(err)
|
||||
|
||||
for k, vs := range resp.Header {
|
||||
for _, v := range vs {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
w.Write(bodyOut)
|
||||
|
||||
if record {
|
||||
infile := fmt.Sprintf("%d.request", len(tc.Steps))
|
||||
outfile := fmt.Sprintf("%d.response", len(tc.Steps))
|
||||
checkErr(ioutil.WriteFile(infile, tryIndent(body), os.FileMode(0755)))
|
||||
checkErr(ioutil.WriteFile(outfile, tryIndent(bodyOut), os.FileMode(0755)))
|
||||
tc.Steps = append(tc.Steps, EditStep{
|
||||
StepType: "request",
|
||||
Input: infile,
|
||||
Output: outfile,
|
||||
RequestContentType: req.Header.Get("Content-Type"),
|
||||
RequestMethod: req.Method,
|
||||
RequestPath: req.URL.Path,
|
||||
ResponseStatusCode: resp.StatusCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tcData, err := yaml.Marshal(tc)
|
||||
checkErr(err)
|
||||
checkErr(ioutil.WriteFile("test.yaml", tcData, os.FileMode(0755)))
|
||||
})))
|
||||
}
|
||||
|
||||
func checkErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func tryIndent(data []byte) []byte {
|
||||
indented := &bytes.Buffer{}
|
||||
if err := json.Indent(indented, data, "", "\t"); err == nil {
|
||||
return indented.Bytes()
|
||||
}
|
||||
return data
|
||||
}
|
||||
26
pkg/cmd/edit/testdata/record_editor.sh
vendored
Executable file
26
pkg/cmd/edit/testdata/record_editor.sh
vendored
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# 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.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
# send the original content to the server
|
||||
curl -s -k -XPOST "http://localhost:8081/callback/in" --data-binary "@${1}"
|
||||
# allow the user to edit the file
|
||||
vi "${1}"
|
||||
# send the resulting content to the server
|
||||
curl -s -k -XPOST "http://localhost:8081/callback/out" --data-binary "@${1}"
|
||||
70
pkg/cmd/edit/testdata/record_testcase.sh
vendored
Executable file
70
pkg/cmd/edit/testdata/record_testcase.sh
vendored
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# 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.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
if [[ -z "${1-}" ]]; then
|
||||
echo "Usage: record_testcase.sh testcase-name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up the test server
|
||||
function cleanup {
|
||||
if [[ -n "${pid-}" ]]; then
|
||||
echo "Stopping recording server (${pid})"
|
||||
# kill the process `go run` launched
|
||||
pkill -P "${pid}"
|
||||
# kill the `go run` process itself
|
||||
kill -9 "${pid}"
|
||||
fi
|
||||
}
|
||||
|
||||
testcase="${1}"
|
||||
|
||||
test_root="$(dirname "${BASH_SOURCE[0]}")"
|
||||
testcase_dir="${test_root}/testcase-${testcase}"
|
||||
mkdir -p "${testcase_dir}"
|
||||
|
||||
pushd "${testcase_dir}"
|
||||
export EDITOR="../record_editor.sh"
|
||||
go run "../record.go" &
|
||||
pid=$!
|
||||
trap cleanup EXIT
|
||||
echo "Started recording server (${pid})"
|
||||
|
||||
# Make a kubeconfig that makes kubectl talk to our test server
|
||||
edit_kubeconfig="${TMP:-/tmp}/edit_test.kubeconfig"
|
||||
echo "apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: http://localhost:8081
|
||||
name: test
|
||||
contexts:
|
||||
- context:
|
||||
cluster: test
|
||||
user: test
|
||||
name: test
|
||||
current-context: test
|
||||
kind: Config
|
||||
users: []
|
||||
" > "${edit_kubeconfig}"
|
||||
export KUBECONFIG="${edit_kubeconfig}"
|
||||
|
||||
echo "Starting subshell. Type exit when finished."
|
||||
bash
|
||||
popd
|
||||
32
pkg/cmd/edit/testdata/test_editor.sh
vendored
Executable file
32
pkg/cmd/edit/testdata/test_editor.sh
vendored
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# 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.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
# Send the file content to the server
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -s -k -XPOST --data-binary "@${1}" -o "${1}.result" "${KUBE_EDITOR_CALLBACK}"
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget --post-file="${1}" -O "${1}.result" "${KUBE_EDITOR_CALLBACK}"
|
||||
else
|
||||
echo "curl and wget are unavailable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use the response as the edited version
|
||||
mv "${1}.result" "${1}"
|
||||
0
pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail/0.request
vendored
Normal file
0
pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail/0.request
vendored
Normal file
21
pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail/0.response
vendored
Normal file
21
pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail/0.response
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"kind": "ConfigMap",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {
|
||||
"name": "cm1",
|
||||
"namespace": "myproject",
|
||||
"selfLink": "/api/v1/namespaces/myproject/configmaps/cm1",
|
||||
"uid": "cc08a131-3d6f-11e7-8ef0-c85b76034b7b",
|
||||
"resourceVersion": "3518",
|
||||
"creationTimestamp": "2017-05-20T15:20:03Z",
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"baz": "qux",
|
||||
"foo": "changed-value",
|
||||
"new-data": "new-value",
|
||||
"new-data2": "new-value"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue