Merge pull request #99758 from aramperes/feat/selector-in-rollout-commands

Add label selector in 'kubectl rollout' commands
This commit is contained in:
Kubernetes Prow Robot 2022-01-06 14:55:59 -08:00 committed by GitHub
commit 6845df1729
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 227 additions and 7 deletions

View File

@ -28,14 +28,20 @@ import (
var (
rolloutLong = templates.LongDesc(i18n.T(`
Manage the rollout of a resource.`) + rolloutValidResources)
Manage the rollout of one or many resources.`) + rolloutValidResources)
rolloutExample = templates.Examples(`
# Rollback to the previous deployment
kubectl rollout undo deployment/abc
# Check the rollout status of a daemonset
kubectl rollout status daemonset/foo`)
kubectl rollout status daemonset/foo
# Restart a deployment
kubectl rollout restart deployment/abc
# Restart deployments with the app=nginx label
kubectl rollout restart deployment --selector=app=nginx`)
rolloutValidResources = dedent.Dedent(`
Valid resource types include:

View File

@ -55,6 +55,7 @@ type RolloutHistoryOptions struct {
Resources []string
Namespace string
EnforceNamespace bool
LabelSelector string
HistoryViewer polymorphichelpers.HistoryViewerFunc
RESTClientGetter genericclioptions.RESTClientGetter
@ -92,6 +93,7 @@ func NewCmdRolloutHistory(f cmdutil.Factory, streams genericclioptions.IOStreams
}
cmd.Flags().Int64Var(&o.Revision, "revision", o.Revision, "See the details, including podTemplate of the revision specified")
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
usage := "identifying the resource to get from a server."
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
@ -141,6 +143,7 @@ func (o *RolloutHistoryOptions) Run() error {
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
NamespaceParam(o.Namespace).DefaultNamespace().
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
LabelSelectorParam(o.LabelSelector).
ResourceTypeOrNameArgs(true, o.Resources...).
ContinueOnError().
Latest().

View File

@ -46,6 +46,7 @@ type PauseOptions struct {
Namespace string
EnforceNamespace bool
Resources []string
LabelSelector string
resource.FilenameOptions
genericclioptions.IOStreams
@ -96,6 +97,7 @@ func NewCmdRolloutPause(f cmdutil.Factory, streams genericclioptions.IOStreams)
usage := "identifying the resource to get from a server."
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-rollout")
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
return cmd
}
@ -132,6 +134,7 @@ func (o *PauseOptions) RunPause() error {
r := o.Builder().
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
NamespaceParam(o.Namespace).DefaultNamespace().
LabelSelectorParam(o.LabelSelector).
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
ResourceTypeOrNameArgs(true, o.Resources...).
ContinueOnError().
@ -153,7 +156,14 @@ func (o *PauseOptions) RunPause() error {
allErrs = append(allErrs, err)
}
for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Pauser)) {
patches := set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Pauser))
if len(patches) == 0 && len(allErrs) == 0 {
fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace)
return nil
}
for _, patch := range patches {
info := patch.Info
if patch.Err != nil {

View File

@ -34,7 +34,6 @@ import (
)
var rolloutPauseGroupVersionEncoder = schema.GroupVersion{Group: "apps", Version: "v1"}
var rolloutPauseGroupVersionDecoder = schema.GroupVersion{Group: "apps", Version: "v1"}
func TestRolloutPause(t *testing.T) {
deploymentName := "deployment/nginx-deployment"

View File

@ -46,6 +46,7 @@ type RestartOptions struct {
Restarter polymorphichelpers.ObjectRestarterFunc
Namespace string
EnforceNamespace bool
LabelSelector string
resource.FilenameOptions
genericclioptions.IOStreams
@ -64,7 +65,10 @@ var (
kubectl rollout restart deployment/nginx
# Restart a daemon set
kubectl rollout restart daemonset/abc`)
kubectl rollout restart daemonset/abc
# Restart deployments with the app=nginx label
kubectl rollout restart deployment --selector=app=nginx`)
)
// NewRolloutRestartOptions returns an initialized RestartOptions instance
@ -98,6 +102,7 @@ func NewCmdRolloutRestart(f cmdutil.Factory, streams genericclioptions.IOStreams
usage := "identifying the resource to get from a server."
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-rollout")
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
o.PrintFlags.AddFlags(cmd)
return cmd
}
@ -137,6 +142,7 @@ func (o RestartOptions) RunRestart() error {
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
NamespaceParam(o.Namespace).DefaultNamespace().
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
LabelSelectorParam(o.LabelSelector).
ResourceTypeOrNameArgs(true, o.Resources...).
ContinueOnError().
Latest().
@ -157,7 +163,14 @@ func (o RestartOptions) RunRestart() error {
allErrs = append(allErrs, err)
}
for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Restarter)) {
patches := set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Restarter))
if len(patches) == 0 && len(allErrs) == 0 {
fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace)
return nil
}
for _, patch := range patches {
info := patch.Info
if patch.Err != nil {

View File

@ -0,0 +1,169 @@
/*
Copyright 2021 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 rollout
import (
"bytes"
"io/ioutil"
"net/http"
"strings"
"testing"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest/fake"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
"k8s.io/kubectl/pkg/scheme"
)
var rolloutRestartGroupVersionEncoder = schema.GroupVersion{Group: "apps", Version: "v1"}
func TestRolloutRestartOne(t *testing.T) {
deploymentName := "deployment/nginx-deployment"
ns := scheme.Codecs.WithoutConversion()
tf := cmdtesting.NewTestFactory().WithNamespace("test")
info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
encoder := ns.EncoderForVersion(info.Serializer, rolloutRestartGroupVersionEncoder)
tf.Client = &RolloutRestartRESTClient{
RESTClient: &fake.RESTClient{
GroupVersion: rolloutRestartGroupVersionEncoder,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == "/namespaces/test/deployments/nginx-deployment" && (m == "GET" || m == "PATCH"):
responseDeployment := &appsv1.Deployment{}
responseDeployment.Name = deploymentName
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
},
}
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
cmd := NewCmdRolloutRestart(tf, streams)
cmd.Run(cmd, []string{deploymentName})
expectedOutput := "deployment.apps/" + deploymentName + " restarted\n"
if buf.String() != expectedOutput {
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
}
}
// Tests that giving selectors with no matching objects shows an error
func TestRolloutRestartSelectorNone(t *testing.T) {
labelSelector := "app=test"
ns := scheme.Codecs.WithoutConversion()
tf := cmdtesting.NewTestFactory().WithNamespace("test")
info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
encoder := ns.EncoderForVersion(info.Serializer, rolloutRestartGroupVersionEncoder)
tf.Client = &RolloutRestartRESTClient{
RESTClient: &fake.RESTClient{
GroupVersion: rolloutRestartGroupVersionEncoder,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m, q := req.URL.Path, req.Method, req.URL.Query(); {
case p == "/namespaces/test/deployments" && m == "GET" && q.Get("labelSelector") == labelSelector:
// Return an empty list
responseDeployments := &appsv1.DeploymentList{}
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployments))))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
},
}
streams, _, outBuf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdRolloutRestart(tf, streams)
cmd.Flags().Set("selector", "app=test")
cmd.Run(cmd, []string{"deployment"})
if len(outBuf.String()) != 0 {
t.Errorf("expected empty output, but got: %s", outBuf.String())
}
expectedError := "No resources found in test namespace.\n"
if errBuf.String() != expectedError {
t.Errorf("expected output: %s, but got: %s", expectedError, errBuf.String())
}
}
// Tests that giving selectors with no matching objects shows an error
func TestRolloutRestartSelectorMany(t *testing.T) {
firstDeployment := appsv1.Deployment{}
firstDeployment.Name = "nginx-deployment-1"
secondDeployment := appsv1.Deployment{}
secondDeployment.Name = "nginx-deployment-2"
labelSelector := "app=test"
ns := scheme.Codecs.WithoutConversion()
tf := cmdtesting.NewTestFactory().WithNamespace("test")
info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
encoder := ns.EncoderForVersion(info.Serializer, rolloutRestartGroupVersionEncoder)
tf.Client = &RolloutRestartRESTClient{
RESTClient: &fake.RESTClient{
GroupVersion: rolloutRestartGroupVersionEncoder,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m, q := req.URL.Path, req.Method, req.URL.Query(); {
case p == "/namespaces/test/deployments" && m == "GET" && q.Get("labelSelector") == labelSelector:
// Return the list of 2 deployments
responseDeployments := &appsv1.DeploymentList{}
responseDeployments.Items = []appsv1.Deployment{firstDeployment, secondDeployment}
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployments))))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
case (p == "/namespaces/test/deployments/nginx-deployment-1" || p == "/namespaces/test/deployments/nginx-deployment-2") && m == "PATCH":
// Pick deployment based on path
responseDeployment := firstDeployment
if strings.HasSuffix(p, "nginx-deployment-2") {
responseDeployment = secondDeployment
}
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, &responseDeployment))))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
},
}
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
cmd := NewCmdRolloutRestart(tf, streams)
cmd.Flags().Set("selector", labelSelector)
cmd.Run(cmd, []string{"deployment"})
expectedOutput := "deployment.apps/" + firstDeployment.Name + " restarted\ndeployment.apps/" + secondDeployment.Name + " restarted\n"
if buf.String() != expectedOutput {
t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String())
}
}
type RolloutRestartRESTClient struct {
*fake.RESTClient
}

View File

@ -47,6 +47,7 @@ type ResumeOptions struct {
Resumer polymorphichelpers.ObjectResumerFunc
Namespace string
EnforceNamespace bool
LabelSelector string
resource.FilenameOptions
genericclioptions.IOStreams
@ -98,6 +99,7 @@ func NewCmdRolloutResume(f cmdutil.Factory, streams genericclioptions.IOStreams)
usage := "identifying the resource to get from a server."
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-rollout")
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
o.PrintFlags.AddFlags(cmd)
return cmd
}
@ -136,6 +138,7 @@ func (o ResumeOptions) RunResume() error {
r := o.Builder().
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
NamespaceParam(o.Namespace).DefaultNamespace().
LabelSelectorParam(o.LabelSelector).
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
ResourceTypeOrNameArgs(true, o.Resources...).
ContinueOnError().
@ -157,7 +160,14 @@ func (o ResumeOptions) RunResume() error {
allErrs = append(allErrs, err)
}
for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Resumer)) {
patches := set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Resumer))
if len(patches) == 0 && len(allErrs) == 0 {
fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace)
return nil
}
for _, patch := range patches {
info := patch.Info
if patch.Err != nil {

View File

@ -66,6 +66,7 @@ type RolloutStatusOptions struct {
Namespace string
EnforceNamespace bool
BuilderArgs []string
LabelSelector string
Watch bool
Revision int64
@ -115,6 +116,7 @@ func NewCmdRolloutStatus(f cmdutil.Factory, streams genericclioptions.IOStreams)
cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "Watch the status of the rollout until it's done.")
cmd.Flags().Int64Var(&o.Revision, "revision", o.Revision, "Pin to a specific revision for showing its status. Defaults to 0 (last revision).")
cmd.Flags().DurationVar(&o.Timeout, "timeout", o.Timeout, "The length of time to wait before ending watch, zero means never. Any other values should contain a corresponding time unit (e.g. 1s, 2m, 3h).")
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
return cmd
}
@ -163,6 +165,7 @@ func (o *RolloutStatusOptions) Run() error {
r := o.Builder().
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
NamespaceParam(o.Namespace).DefaultNamespace().
LabelSelectorParam(o.LabelSelector).
FilenameParam(o.EnforceNamespace, o.FilenameOptions).
ResourceTypeOrNameArgs(true, o.BuilderArgs...).
SingleResourceType().

View File

@ -44,6 +44,7 @@ type UndoOptions struct {
DryRunVerifier *resource.DryRunVerifier
Resources []string
Namespace string
LabelSelector string
EnforceNamespace bool
RESTClientGetter genericclioptions.RESTClientGetter
@ -99,6 +100,7 @@ func NewCmdRolloutUndo(f cmdutil.Factory, streams genericclioptions.IOStreams) *
usage := "identifying the resource to get from a server."
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
cmdutil.AddDryRunFlag(cmd)
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
o.PrintFlags.AddFlags(cmd)
return cmd
}
@ -145,6 +147,7 @@ func (o *UndoOptions) RunUndo() error {
r := o.Builder().
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
NamespaceParam(o.Namespace).DefaultNamespace().
LabelSelectorParam(o.LabelSelector).
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
ResourceTypeOrNameArgs(true, o.Resources...).
ContinueOnError().

View File

@ -463,6 +463,10 @@ func AddChunkSizeFlag(cmd *cobra.Command, value *int64) {
"Return large lists in chunks rather than all at once. Pass 0 to disable. This flag is beta and may change in the future.")
}
func AddLabelSelectorFlagVar(cmd *cobra.Command, p *string) {
cmd.Flags().StringVarP(p, "selector", "l", *p, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
}
type ValidateOptions struct {
EnableValidation bool
}