Move pkg/kubectl/cmd/{command} to staging

Kubernetes-commit: 0e0ea523392f1121f61f99ac30a9bc2043eaed90
This commit is contained in:
Sean Sullivan 2019-08-01 11:01:40 -07:00 committed by Kubernetes Publisher
parent ef04362827
commit c6063dd846
381 changed files with 53901 additions and 0 deletions

49
pkg/cmd/alpha.go Normal file
View 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
}

View 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
}

View 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)
}
}

View 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
}

View 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
View 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
}

View 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
}

View 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

File diff suppressed because it is too large Load diff

View 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
View 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
}

View 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,
},
}
}

View 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
}

View 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
}

View 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, "")
}

View 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
}

View 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)
}
}

View 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
View 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)
}

View 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())
}
}
}
}

View 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
}

View 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)
}
}
}

View 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
}

View 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)
}
}
}

View 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
}

View 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)
}
}
}

View 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
}

View 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)
}
}

View 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
}

View 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)
}
}

View 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
}

View 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)
}
}

View 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
}

View 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
}
}

View 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
}

View 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
}
}

View 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
}

View 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)
}
}

View 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
}

View 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
View 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))
}

View 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
View 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
}

View 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)
}
}
}

View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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)
}

View 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)
}
})
}
}

View 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()
}

View 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)
}

View 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()
}

View 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())
}
}

View 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),
},
},
},
},
},
}
}

View 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)
}
})
}
}

View 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()
}

View 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")
}

View 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,
}
}

View 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)
}
})
}
}

View 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()
}

View 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())
}
}

View 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()
}

View 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())
}
}

View 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()
}

View 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())
}
}

View 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()
}

View 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())
}
})
}
}

View 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
}

View 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)
}
}
}

View 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()
}

View 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)
}

View 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()
}

View 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)
}
}

View 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()
}

View 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())
}
}

View 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()
}

View 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())
}
}

View 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
View 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)
}

View 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,
}
}

View 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()))
}
})
}
}

View 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
}

View 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
View 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
View 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
View 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.
![Workflow](http://kubernetes.io/images/docs/kubectl_drain.svg)`))
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

File diff suppressed because it is too large Load diff

105
pkg/cmd/edit/edit.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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}"

View 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