diff --git a/pkg/cmd/annotate/annotate.go b/pkg/cmd/annotate/annotate.go index 78f83638..35da6cfa 100644 --- a/pkg/cmd/annotate/annotate.go +++ b/pkg/cmd/annotate/annotate.go @@ -264,6 +264,16 @@ func (o AnnotateOptions) RunAnnotate() error { outputObj = obj } else { name, namespace := info.Name, info.Namespace + + if len(o.resourceVersion) != 0 { + // ensure resourceVersion is always sent in the patch by clearing it from the starting JSON + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + accessor.SetResourceVersion("") + } + oldData, err := json.Marshal(obj) if err != nil { return err diff --git a/pkg/cmd/annotate/annotate_test.go b/pkg/cmd/annotate/annotate_test.go index f4cf6463..c1306f1f 100644 --- a/pkg/cmd/annotate/annotate_test.go +++ b/pkg/cmd/annotate/annotate_test.go @@ -17,12 +17,14 @@ limitations under the License. package annotate import ( + "bytes" + "io/ioutil" "net/http" "reflect" "strings" "testing" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -502,6 +504,73 @@ func TestAnnotateObject(t *testing.T) { } } +func TestAnnotateResourceVersion(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) { + switch req.Method { + case "GET": + switch req.URL.Path { + case "/namespaces/test/pods/foo": + return &http.Response{ + StatusCode: http.StatusOK, + Header: cmdtesting.DefaultHeader(), + Body: ioutil.NopCloser(bytes.NewBufferString( + `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","resourceVersion":"10"}}`, + ))}, 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": + body, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(body, []byte(`{"metadata":{"annotations":{"a":"b"},"resourceVersion":"10"}}`)) { + t.Fatalf("expected patch with resourceVersion set, got %s", string(body)) + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: cmdtesting.DefaultHeader(), + Body: ioutil.NopCloser(bytes.NewBufferString( + `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","resourceVersion":"11"}}`, + ))}, 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.resourceVersion = "10" + args := []string{"pods/foo", "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 TestAnnotateObjectFromFile(t *testing.T) { pods, _, _ := cmdtesting.TestData() diff --git a/pkg/cmd/label/label.go b/pkg/cmd/label/label.go index 6e640246..6a5ce303 100644 --- a/pkg/cmd/label/label.go +++ b/pkg/cmd/label/label.go @@ -253,6 +253,16 @@ func (o *LabelOptions) RunLabel() error { var outputObj runtime.Object var dataChangeMsg string obj := info.Object + + if len(o.resourceVersion) != 0 { + // ensure resourceVersion is always sent in the patch by clearing it from the starting JSON + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + accessor.SetResourceVersion("") + } + oldData, err := json.Marshal(obj) if err != nil { return err diff --git a/pkg/cmd/label/label_test.go b/pkg/cmd/label/label_test.go index ec2b8c86..e3f71697 100644 --- a/pkg/cmd/label/label_test.go +++ b/pkg/cmd/label/label_test.go @@ -18,6 +18,7 @@ package label import ( "bytes" + "io/ioutil" "net/http" "reflect" "strings" @@ -26,6 +27,7 @@ import ( "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" @@ -496,3 +498,70 @@ func TestLabelMultipleObjects(t *testing.T) { t.Errorf("not all labels are set: %s", buf.String()) } } + +func TestLabelResourceVersion(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) { + switch req.Method { + case "GET": + switch req.URL.Path { + case "/namespaces/test/pods/foo": + return &http.Response{ + StatusCode: http.StatusOK, + Header: cmdtesting.DefaultHeader(), + Body: ioutil.NopCloser(bytes.NewBufferString( + `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","resourceVersion":"10"}}`, + ))}, 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": + body, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(body, []byte(`{"metadata":{"labels":{"a":"b"},"resourceVersion":"10"}}`)) { + t.Fatalf("expected patch with resourceVersion set, got %s", string(body)) + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: cmdtesting.DefaultHeader(), + Body: ioutil.NopCloser(bytes.NewBufferString( + `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","resourceVersion":"11"}}`, + ))}, 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 := NewCmdLabel(tf, iostreams) + cmd.SetOutput(bufOut) + options := NewLabelOptions(iostreams) + options.resourceVersion = "10" + args := []string{"pods/foo", "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.RunLabel(); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/pkg/cmd/set/set_selector.go b/pkg/cmd/set/set_selector.go index a7cb0259..d7fecdff 100644 --- a/pkg/cmd/set/set_selector.go +++ b/pkg/cmd/set/set_selector.go @@ -22,7 +22,8 @@ import ( "github.com/spf13/cobra" "k8s.io/klog" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -46,8 +47,9 @@ type SetSelectorOptions struct { dryrun bool // set by args - resources []string - selector *metav1.LabelSelector + resources []string + selector *metav1.LabelSelector + resourceVersion string // computed WriteToServer bool @@ -111,7 +113,7 @@ func NewCmdSelector(f cmdutil.Factory, streams genericclioptions.IOStreams) *cob o.PrintFlags.AddFlags(cmd) o.RecordFlags.AddFlags(cmd) - 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.") + cmd.Flags().StringVarP(&o.resourceVersion, "resource-version", "", o.resourceVersion, "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.") cmdutil.AddDryRunFlag(cmd) return cmd @@ -163,7 +165,26 @@ func (o *SetSelectorOptions) RunSelector() error { return r.Visit(func(info *resource.Info, err error) error { patch := &Patch{Info: info} + + if len(o.resourceVersion) != 0 { + // ensure resourceVersion is always sent in the patch by clearing it from the starting JSON + accessor, err := meta.Accessor(info.Object) + if err != nil { + return err + } + accessor.SetResourceVersion("") + } + CalculatePatch(patch, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) { + + if len(o.resourceVersion) != 0 { + accessor, err := meta.Accessor(info.Object) + if err != nil { + return nil, err + } + accessor.SetResourceVersion(o.resourceVersion) + } + selectErr := updateSelectorForObject(info.Object, *o.selector) if selectErr != nil { return nil, selectErr