Add new command "kubectl set selector"

This commit is contained in:
Angus Salkeld 2016-11-14 21:53:27 +10:00 committed by Michail Kargakis
parent f9707a7d9b
commit 17a711d8fd
16 changed files with 622 additions and 57 deletions

View File

@ -80,6 +80,7 @@ docs/man/man1/kubectl-run.1
docs/man/man1/kubectl-scale.1
docs/man/man1/kubectl-set-image.1
docs/man/man1/kubectl-set-resources.1
docs/man/man1/kubectl-set-selector.1
docs/man/man1/kubectl-set.1
docs/man/man1/kubectl-stop.1
docs/man/man1/kubectl-taint.1
@ -162,6 +163,7 @@ docs/user-guide/kubectl/kubectl_scale.md
docs/user-guide/kubectl/kubectl_set.md
docs/user-guide/kubectl/kubectl_set_image.md
docs/user-guide/kubectl/kubectl_set_resources.md
docs/user-guide/kubectl/kubectl_set_selector.md
docs/user-guide/kubectl/kubectl_taint.md
docs/user-guide/kubectl/kubectl_top.md
docs/user-guide/kubectl/kubectl_top_node.md

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.

View File

@ -0,0 +1,7 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_set_selector.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->

View File

@ -1933,6 +1933,19 @@ __EOF__
# Describe command should print events information when show-events=true
kube::test::describe_resource_events_assert services true
### set selector
# prove role=master
kube::test::get_object_assert 'services redis-master' "{{range$service_selector_field}}{{.}}:{{end}}" "redis:master:backend:"
# Set command to change the selector.
kubectl set selector -f examples/guestbook/redis-master-service.yaml role=padawan
# prove role=padawan
kube::test::get_object_assert 'services redis-master' "{{range$service_selector_field}}{{.}}:{{end}}" "padawan:"
# Set command to reset the selector back to the original one.
kubectl set selector -f examples/guestbook/redis-master-service.yaml app=redis,role=master,tier=backend
# prove role=master
kube::test::get_object_assert 'services redis-master' "{{range$service_selector_field}}{{.}}:{{end}}" "redis:master:backend:"
### Dump current redis-master service
output_service=$(kubectl get service redis-master -o json --output-version=v1 "${kube_flags[@]}")

View File

@ -38,7 +38,7 @@ import (
type PauseConfig struct {
resource.FilenameOptions
Pauser func(info *resource.Info) (bool, error)
Pauser func(info *resource.Info) ([]byte, error)
Mapper meta.RESTMapper
Typer runtime.ObjectTyper
Encoder runtime.Encoder

View File

@ -38,7 +38,7 @@ import (
type ResumeConfig struct {
resource.FilenameOptions
Resumer func(object *resource.Info) (bool, error)
Resumer func(object *resource.Info) ([]byte, error)
Mapper meta.RESTMapper
Typer runtime.ObjectTyper
Encoder runtime.Encoder

View File

@ -15,12 +15,14 @@ go_library(
"set.go",
"set_image.go",
"set_resources.go",
"set_selector.go",
],
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/meta:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/kubectl:go_default_library",
"//pkg/kubectl/cmd/templates:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
@ -36,6 +38,7 @@ go_test(
name = "go_default_test",
srcs = [
"set_image_test.go",
"set_selector_test.go",
"set_test.go",
],
data = [
@ -46,11 +49,16 @@ go_test(
deps = [
"//pkg/api:go_default_library",
"//pkg/apimachinery/registered:go_default_library",
"//pkg/apis/batch:go_default_library",
"//pkg/apis/extensions:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/client/restclient/fake:go_default_library",
"//pkg/kubectl/cmd/testing:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/resource:go_default_library",
"//pkg/runtime:go_default_library",
"//vendor:github.com/spf13/cobra",
"//vendor:github.com/stretchr/testify/assert",
],
)

View File

@ -117,46 +117,44 @@ type Patch struct {
Patch []byte
}
// CalculatePatches calls the mutation function on each provided info object, and generates a strategic merge patch for
// the changes in the object. Encoder must be able to encode the info into the appropriate destination type. If mutateFn
// returns false, the object is not included in the final list of patches.
func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn func(*resource.Info) (bool, error)) []*Patch {
// patchFn is a function type that accepts an info object and returns a byte slice.
// Implementations of patchFn should update the object and return it encoded.
type patchFn func(*resource.Info) ([]byte, error)
// CalculatePatch calls the mutation function on the provided info object, and generates a strategic merge patch for
// the changes in the object. Encoder must be able to encode the info into the appropriate destination type.
// This function returns whether the mutation function made any change in the original object.
func CalculatePatch(patch *Patch, encoder runtime.Encoder, mutateFn patchFn) bool {
patch.Before, patch.Err = runtime.Encode(encoder, patch.Info.Object)
patch.After, patch.Err = mutateFn(patch.Info)
if patch.Err != nil {
return true
}
if patch.After == nil {
return false
}
// TODO: should be via New
versioned, err := patch.Info.Mapping.ConvertToVersion(patch.Info.Object, patch.Info.Mapping.GroupVersionKind.GroupVersion())
if err != nil {
patch.Err = err
return true
}
patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, versioned)
return true
}
// CalculatePatches calculates patches on each provided info object. If the provided mutateFn
// makes no change in an object, the object is not included in the final list of patches.
func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn patchFn) []*Patch {
var patches []*Patch
for _, info := range infos {
patch := &Patch{Info: info}
patch.Before, patch.Err = runtime.Encode(encoder, info.Object)
if patch.Err != nil {
if CalculatePatch(patch, encoder, mutateFn) {
patches = append(patches, patch)
continue
}
ok, err := mutateFn(info)
if err != nil {
patch.Err = err
patches = append(patches, patch)
continue
}
if !ok {
continue
}
patches = append(patches, patch)
if patch.Err != nil {
continue
}
patch.After, patch.Err = runtime.Encode(encoder, info.Object)
if patch.Err != nil {
continue
}
// TODO: should be via New
versioned, err := info.Mapping.ConvertToVersion(info.Object, info.Mapping.GroupVersionKind.GroupVersion())
if err != nil {
patch.Err = err
continue
}
patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, versioned)
}
return patches
}

View File

@ -42,6 +42,7 @@ func NewCmdSet(f cmdutil.Factory, out, err io.Writer) *cobra.Command {
// add subcommands
cmd.AddCommand(NewCmdImage(f, out, err))
cmd.AddCommand(NewCmdResources(f, out, err))
cmd.AddCommand(NewCmdSelector(f, out))
return cmd
}

View File

@ -169,7 +169,7 @@ func (o *ImageOptions) Validate() error {
func (o *ImageOptions) Run() error {
allErrs := []error{}
patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) (bool, error) {
patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) ([]byte, error) {
transformed := false
_, err := o.UpdatePodSpecForObject(info.Object, func(spec *api.PodSpec) error {
for name, image := range o.ContainerImages {
@ -205,7 +205,10 @@ func (o *ImageOptions) Run() error {
}
return nil
})
return transformed, err
if transformed && err == nil {
return runtime.Encode(o.Encoder, info.Object)
}
return nil, err
})
for _, patch := range patches {

View File

@ -174,7 +174,7 @@ func (o *ResourcesOptions) Validate() error {
func (o *ResourcesOptions) Run() error {
allErrs := []error{}
patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) (bool, error) {
patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) ([]byte, error) {
transformed := false
_, err := o.UpdatePodSpecForObject(info.Object, func(spec *api.PodSpec) error {
containers, _ := selectContainers(spec.Containers, o.ContainerSelector)
@ -200,7 +200,10 @@ func (o *ResourcesOptions) Run() error {
}
return nil
})
return transformed, err
if transformed && err == nil {
return runtime.Encode(o.Encoder, info.Object)
}
return nil, err
})
for _, patch := range patches {

View File

@ -0,0 +1,221 @@
/*
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 set
import (
"fmt"
"io"
"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/meta"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/runtime"
)
// SelectorOptions 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 SelectorOptions struct {
fileOptions resource.FilenameOptions
local bool
dryrun bool
all bool
record bool
changeCause string
resources []string
selector *metav1.LabelSelector
out io.Writer
PrintObject func(obj runtime.Object) error
ClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
builder *resource.Builder
mapper meta.RESTMapper
encoder runtime.Encoder
}
var (
selectorLong = templates.LongDesc(`
Set the selector on a resource. Note that the new selector will overwrite the old selector if the resource had one prior to the invocation
of 'set selector'.
A selector must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[1]d characters.
If --resource-version is specified, then updates will use this resource version, otherwise the existing resource-version will be used.
Note: currently selectors can only be set on Service objects.`)
selectorExample = templates.Examples(`
# set the labels and selector before creating a deployment/service pair.
kubectl create service clusterip my-svc -o yaml --dry-run | kubectl set selector --local -f - 'environment=qa' -o yaml | kubectl create -f -
kubectl create deployment my-dep -o yaml --dry-run | kubectl label --local -f - environment=qa -o yaml | kubectl create -f -`)
)
// NewCmdSelector is the "set selector" command.
func NewCmdSelector(f cmdutil.Factory, out io.Writer) *cobra.Command {
options := &SelectorOptions{
out: out,
}
cmd := &cobra.Command{
Use: "selector (-f FILENAME | TYPE NAME) EXPRESSIONS [--resource-version=version]",
Short: "Set the selector on a resource",
Long: fmt.Sprintf(selectorLong),
Example: selectorExample,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(options.Complete(f, cmd, args, out))
cmdutil.CheckErr(options.Validate())
cmdutil.CheckErr(options.RunSelector())
},
}
cmdutil.AddPrinterFlags(cmd)
cmd.Flags().Bool("all", false, "Select all resources in the namespace of the specified resource types")
cmd.Flags().Bool("local", false, "If true, set selector will NOT contact api-server but run locally.")
cmd.Flags().String("resource-version", "", "If non-empty, the selectors update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.")
usage := "the resource to update the selectors"
cmdutil.AddFilenameOptionFlags(cmd, &options.fileOptions, usage)
cmdutil.AddDryRunFlag(cmd)
cmdutil.AddRecordFlag(cmd)
return cmd
}
// Complete assigns the SelectorOptions from args.
func (o *SelectorOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
o.local = cmdutil.GetFlagBool(cmd, "local")
o.all = cmdutil.GetFlagBool(cmd, "all")
o.record = cmdutil.GetRecordFlag(cmd)
o.dryrun = cmdutil.GetDryRunFlag(cmd)
cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
if err != nil {
return err
}
o.changeCause = f.Command()
mapper, _ := f.Object()
o.mapper = mapper
o.encoder = f.JSONEncoder()
o.builder = f.NewBuilder().
ContinueOnError().
NamespaceParam(cmdNamespace).DefaultNamespace().
FilenameParam(enforceNamespace, &o.fileOptions).
Flatten()
o.PrintObject = func(obj runtime.Object) error {
return f.PrintObject(cmd, mapper, obj, o.out)
}
o.ClientForMapping = func(mapping *meta.RESTMapping) (resource.RESTClient, error) {
return f.ClientForMapping(mapping)
}
o.resources, o.selector, err = getResourcesAndSelector(args)
return err
}
// Validate basic inputs
func (o *SelectorOptions) Validate() error {
if len(o.resources) < 1 && cmdutil.IsFilenameEmpty(o.fileOptions.Filenames) {
return fmt.Errorf("one or more resources must be specified as <resource> <name> or <resource>/<name>")
}
return nil
}
// RunSelector executes the command.
func (o *SelectorOptions) RunSelector() error {
if !o.local {
o.builder = o.builder.ResourceTypeOrNameArgs(o.all, o.resources...).
Latest()
}
r := o.builder.Do()
err := r.Err()
if err != nil {
return err
}
return r.Visit(func(info *resource.Info, err error) error {
patch := &Patch{Info: info}
CalculatePatch(patch, o.encoder, func(info *resource.Info) ([]byte, error) {
selectErr := updateSelectorForObject(info.Object, *o.selector)
if selectErr == nil {
return runtime.Encode(o.encoder, info.Object)
}
return nil, selectErr
})
if patch.Err != nil {
return patch.Err
}
if o.local || o.dryrun {
fmt.Fprintln(o.out, "running in local/dry-run mode...")
o.PrintObject(info.Object)
return nil
}
patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch.Patch)
if err != nil {
return err
}
if o.record || cmdutil.ContainsChangeCause(info) {
if err := cmdutil.RecordChangeCause(patched, o.changeCause); err == nil {
if patched, err = resource.NewHelper(info.Client, info.Mapping).Replace(info.Namespace, info.Name, false, patched); err != nil {
return fmt.Errorf("changes to %s/%s can't be recorded: %v\n", info.Mapping.Resource, info.Name, err)
}
}
}
info.Refresh(patched, true)
cmdutil.PrintSuccess(o.mapper, false, o.out, info.Mapping.Resource, info.Name, o.dryrun, "selector updated")
return nil
})
}
func updateSelectorForObject(obj runtime.Object, selector metav1.LabelSelector) error {
copyOldSelector := func() (map[string]string, error) {
if len(selector.MatchExpressions) > 0 {
return nil, fmt.Errorf("match expression %v not supported on this object", selector.MatchExpressions)
}
dst := make(map[string]string)
for label, value := range selector.MatchLabels {
dst[label] = value
}
return dst, nil
}
var err error
switch t := obj.(type) {
case *api.Service:
t.Spec.Selector, err = copyOldSelector()
default:
err = fmt.Errorf("setting a selector is only supported for Services")
}
return err
}
// getResourcesAndSelector retrieves resources and the selector expression from the given args (assuming selectors the last arg)
func getResourcesAndSelector(args []string) (resources []string, selector *metav1.LabelSelector, err error) {
if len(args) > 1 {
resources = args[:len(args)-1]
}
selector, err = metav1.ParseToLabelSelector(args[len(args)-1])
return resources, selector, err
}

View File

@ -0,0 +1,302 @@
/*
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 set
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/apis/extensions"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/runtime"
)
func TestUpdateSelectorForObjectTypes(t *testing.T) {
before := metav1.LabelSelector{MatchLabels: map[string]string{"fee": "true"},
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "foo",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"on", "yes"},
},
}}
rc := api.ReplicationController{}
ser := api.Service{}
dep := extensions.Deployment{Spec: extensions.DeploymentSpec{Selector: &before}}
ds := extensions.DaemonSet{Spec: extensions.DaemonSetSpec{Selector: &before}}
rs := extensions.ReplicaSet{Spec: extensions.ReplicaSetSpec{Selector: &before}}
job := batch.Job{Spec: batch.JobSpec{Selector: &before}}
pvc := api.PersistentVolumeClaim{Spec: api.PersistentVolumeClaimSpec{Selector: &before}}
sa := api.ServiceAccount{}
type args struct {
obj runtime.Object
selector metav1.LabelSelector
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "rc",
args: args{
obj: &rc,
selector: metav1.LabelSelector{},
},
wantErr: true,
},
{name: "ser",
args: args{
obj: &ser,
selector: metav1.LabelSelector{},
},
wantErr: false,
},
{name: "dep",
args: args{
obj: &dep,
selector: metav1.LabelSelector{},
},
wantErr: true,
},
{name: "ds",
args: args{
obj: &ds,
selector: metav1.LabelSelector{},
},
wantErr: true,
},
{name: "rs",
args: args{
obj: &rs,
selector: metav1.LabelSelector{},
},
wantErr: true,
},
{name: "job",
args: args{
obj: &job,
selector: metav1.LabelSelector{},
},
wantErr: true,
},
{name: "pvc - no updates",
args: args{
obj: &pvc,
selector: metav1.LabelSelector{},
},
wantErr: true,
},
{name: "sa - no selector",
args: args{
obj: &sa,
selector: metav1.LabelSelector{},
},
wantErr: true,
},
}
for _, tt := range tests {
if err := updateSelectorForObject(tt.args.obj, tt.args.selector); (err != nil) != tt.wantErr {
t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
}
}
func TestUpdateNewSelectorValuesForObject(t *testing.T) {
ser := api.Service{}
type args struct {
obj runtime.Object
selector metav1.LabelSelector
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "empty",
args: args{
obj: &ser,
selector: metav1.LabelSelector{
MatchLabels: map[string]string{},
MatchExpressions: []metav1.LabelSelectorRequirement{},
},
},
wantErr: false,
},
{name: "label-only",
args: args{
obj: &ser,
selector: metav1.LabelSelector{
MatchLabels: map[string]string{"b": "u"},
MatchExpressions: []metav1.LabelSelectorRequirement{},
},
},
wantErr: false,
},
}
for _, tt := range tests {
if err := updateSelectorForObject(tt.args.obj, tt.args.selector); (err != nil) != tt.wantErr {
t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
assert.EqualValues(t, tt.args.selector.MatchLabels, ser.Spec.Selector, tt.name)
}
}
func TestUpdateOldSelectorValuesForObject(t *testing.T) {
ser := api.Service{Spec: api.ServiceSpec{Selector: map[string]string{"fee": "true"}}}
type args struct {
obj runtime.Object
selector metav1.LabelSelector
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "empty",
args: args{
obj: &ser,
selector: metav1.LabelSelector{
MatchLabels: map[string]string{},
MatchExpressions: []metav1.LabelSelectorRequirement{},
},
},
wantErr: false,
},
{name: "label-only",
args: args{
obj: &ser,
selector: metav1.LabelSelector{
MatchLabels: map[string]string{"fee": "false", "x": "y"},
MatchExpressions: []metav1.LabelSelectorRequirement{},
},
},
wantErr: false,
},
{name: "expr-only - err",
args: args{
obj: &ser,
selector: metav1.LabelSelector{
MatchLabels: map[string]string{},
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "a",
Operator: "In",
Values: []string{"x", "y"},
},
},
},
},
wantErr: true,
},
{name: "both - err",
args: args{
obj: &ser,
selector: metav1.LabelSelector{
MatchLabels: map[string]string{"b": "u"},
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "a",
Operator: "In",
Values: []string{"x", "y"},
},
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
err := updateSelectorForObject(tt.args.obj, tt.args.selector)
if (err != nil) != tt.wantErr {
t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr)
} else if !tt.wantErr {
assert.EqualValues(t, tt.args.selector.MatchLabels, ser.Spec.Selector, tt.name)
}
}
}
func TestGetResourcesAndSelector(t *testing.T) {
type args struct {
args []string
}
tests := []struct {
name string
args args
wantResources []string
wantSelector *metav1.LabelSelector
wantErr bool
}{
{
name: "basic match",
args: args{args: []string{"rc/foo", "healthy=true"}},
wantResources: []string{"rc/foo"},
wantErr: false,
wantSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"healthy": "true"},
MatchExpressions: []metav1.LabelSelectorRequirement{},
},
},
{
name: "basic expression",
args: args{args: []string{"rc/foo", "buildType notin (debug, test)"}},
wantResources: []string{"rc/foo"},
wantErr: false,
wantSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{},
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "buildType",
Operator: "NotIn",
Values: []string{"debug", "test"},
},
},
},
},
{
name: "selector error",
args: args{args: []string{"rc/foo", "buildType notthis (debug, test)"}},
wantResources: []string{"rc/foo"},
wantErr: true,
wantSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{},
MatchExpressions: []metav1.LabelSelectorRequirement{},
},
},
}
for _, tt := range tests {
gotResources, gotSelector, err := getResourcesAndSelector(tt.args.args)
if err != nil {
if !tt.wantErr {
t.Errorf("%q. getResourcesAndSelector() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
continue
}
if !reflect.DeepEqual(gotResources, tt.wantResources) {
t.Errorf("%q. getResourcesAndSelector() gotResources = %v, want %v", tt.name, gotResources, tt.wantResources)
}
if !reflect.DeepEqual(gotSelector, tt.wantSelector) {
t.Errorf("%q. getResourcesAndSelector() gotSelector = %v, want %v", tt.name, gotSelector, tt.wantSelector)
}
}
}

View File

@ -303,12 +303,12 @@ func (f *FakeFactory) LogsForObject(object, options runtime.Object) (*restclient
return nil, nil
}
func (f *FakeFactory) Pauser(info *resource.Info) (bool, error) {
return false, nil
func (f *FakeFactory) Pauser(info *resource.Info) ([]byte, error) {
return nil, nil
}
func (f *FakeFactory) Resumer(info *resource.Info) (bool, error) {
return false, nil
func (f *FakeFactory) Resumer(info *resource.Info) ([]byte, error) {
return nil, nil
}
func (f *FakeFactory) ResolveImage(name string) (string, error) {

View File

@ -146,10 +146,14 @@ type ClientAccessFactory interface {
// Returns a Printer for formatting objects of the given type or an error.
Printer(mapping *meta.RESTMapping, options kubectl.PrintOptions) (kubectl.ResourcePrinter, error)
// Pauser marks the object in the info as paused ie. it will not be reconciled by its controller.
Pauser(info *resource.Info) (bool, error)
// Resumer resumes a paused object inside the info ie. it will be reconciled by its controller.
Resumer(info *resource.Info) (bool, error)
// Pauser marks the object in the info as paused. Currently supported only for Deployments.
// Returns the patched object in bytes and any error that occured during the encoding or
// in case the object is already paused.
Pauser(info *resource.Info) ([]byte, error)
// Resumer resumes a paused object inside the info. Currently supported only for Deployments.
// Returns the patched object in bytes and any error that occured during the encoding or
// in case the object is already resumed.
Resumer(info *resource.Info) ([]byte, error)
// ResolveImage resolves the image names. For kubernetes this function is just
// passthrough but it allows to perform more sophisticated image name resolving for

View File

@ -398,16 +398,16 @@ func (f *ring0Factory) Printer(mapping *meta.RESTMapping, options kubectl.PrintO
return kubectl.NewHumanReadablePrinter(options), nil
}
func (f *ring0Factory) Pauser(info *resource.Info) (bool, error) {
func (f *ring0Factory) Pauser(info *resource.Info) ([]byte, error) {
switch obj := info.Object.(type) {
case *extensions.Deployment:
if obj.Spec.Paused {
return true, errors.New("is already paused")
return nil, errors.New("is already paused")
}
obj.Spec.Paused = true
return true, nil
return runtime.Encode(f.JSONEncoder(), info.Object)
default:
return false, fmt.Errorf("pausing is not supported")
return nil, fmt.Errorf("pausing is not supported")
}
}
@ -415,16 +415,16 @@ func (f *ring0Factory) ResolveImage(name string) (string, error) {
return name, nil
}
func (f *ring0Factory) Resumer(info *resource.Info) (bool, error) {
func (f *ring0Factory) Resumer(info *resource.Info) ([]byte, error) {
switch obj := info.Object.(type) {
case *extensions.Deployment:
if !obj.Spec.Paused {
return true, errors.New("is not paused")
return nil, errors.New("is not paused")
}
obj.Spec.Paused = false
return true, nil
return runtime.Encode(f.JSONEncoder(), info.Object)
default:
return false, fmt.Errorf("resuming is not supported")
return nil, fmt.Errorf("resuming is not supported")
}
}