kubectl: add --support to get, patch, edit and replace commands

Co-authored-by: Nikhita Raghunath <nikitaraghunath@gmail.com>

Kubernetes-commit: a5aa858d44651ba48bdce634a39b55be91216614
This commit is contained in:
Yuvaraj Kakaraparthi 2021-07-07 08:30:59 -07:00 committed by Kubernetes Publisher
parent d0173dffba
commit 38487e770c
17 changed files with 546 additions and 11 deletions

View File

@ -63,7 +63,10 @@ var (
kubectl edit job.v1.batch/myjob -o json kubectl edit job.v1.batch/myjob -o json
# Edit the deployment 'mydeployment' in YAML and save the modified config in its annotation # Edit the deployment 'mydeployment' in YAML and save the modified config in its annotation
kubectl edit deployment/mydeployment -o yaml --save-config`)) kubectl edit deployment/mydeployment -o yaml --save-config
# Edit the deployment/mydeployment's status subresource
kubectl edit deployment mydeployment --subresource='status'`))
) )
// NewCmdEdit creates the `edit` command // NewCmdEdit creates the `edit` command
@ -80,6 +83,7 @@ func NewCmdEdit(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra
ValidArgsFunction: util.ResourceTypeAndNameCompletionFunc(f), ValidArgsFunction: util.ResourceTypeAndNameCompletionFunc(f),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(o.Complete(f, args, cmd)) cmdutil.CheckErr(o.Complete(f, args, cmd))
cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.Run()) cmdutil.CheckErr(o.Run())
}, },
} }
@ -96,5 +100,6 @@ func NewCmdEdit(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra
"Defaults to the line ending native to your platform.") "Defaults to the line ending native to your platform.")
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-edit") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-edit")
cmdutil.AddApplyAnnotationVarFlags(cmd, &o.ApplyAnnotation) cmdutil.AddApplyAnnotationVarFlags(cmd, &o.ApplyAnnotation)
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, edit will operate on the subresource of the requested object.", editor.SupportedSubresources...)
return cmd return cmd
} }

View File

@ -53,6 +53,7 @@ type EditTestCase struct {
Output string `yaml:"outputFormat"` Output string `yaml:"outputFormat"`
OutputPatch string `yaml:"outputPatch"` OutputPatch string `yaml:"outputPatch"`
SaveConfig string `yaml:"saveConfig"` SaveConfig string `yaml:"saveConfig"`
Subresource string `yaml:"subresource"`
Namespace string `yaml:"namespace"` Namespace string `yaml:"namespace"`
ExpectedStdout []string `yaml:"expectedStdout"` ExpectedStdout []string `yaml:"expectedStdout"`
ExpectedStderr []string `yaml:"expectedStderr"` ExpectedStderr []string `yaml:"expectedStderr"`
@ -253,6 +254,9 @@ func TestEdit(t *testing.T) {
if len(testcase.SaveConfig) > 0 { if len(testcase.SaveConfig) > 0 {
cmd.Flags().Set("save-config", testcase.SaveConfig) cmd.Flags().Set("save-config", testcase.SaveConfig)
} }
if len(testcase.Subresource) > 0 {
cmd.Flags().Set("subresource", testcase.Subresource)
}
cmdutil.BehaviorOnFatal(func(str string, code int) { cmdutil.BehaviorOnFatal(func(str string, code int) {
errBuf.WriteString(str) errBuf.WriteString(str)

View File

@ -0,0 +1,85 @@
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"annotations": {
"deployment.kubernetes.io/revision": "1"
},
"creationTimestamp": "2021-06-23T17:01:10Z",
"generation": 5,
"labels": {
"app": "nginx"
},
"name": "nginx",
"namespace": "edit-test",
"resourceVersion": "121107",
"uid": "a598ee47-9635-482b-bacb-16c9e3ade05c"
},
"spec": {
"progressDeadlineSeconds": 600,
"replicas": 3,
"revisionHistoryLimit": 10,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"strategy": {
"rollingUpdate": {
"maxSurge": "25%",
"maxUnavailable": "25%"
},
"type": "RollingUpdate"
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [
{
"image": "gcr.io/kakaraparthy-devel/nginx:latest",
"imagePullPolicy": "Always",
"name": "nginx",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 30
}
}
},
"status": {
"availableReplicas": 3,
"conditions": [
{
"lastTransitionTime": "2021-06-23T17:01:10Z",
"lastUpdateTime": "2021-06-23T17:01:18Z",
"message": "ReplicaSet \"nginx-6f5fdbd667\" has successfully progressed.",
"reason": "NewReplicaSetAvailable",
"status": "True",
"type": "Progressing"
},
{
"lastTransitionTime": "2021-06-23T17:59:01Z",
"lastUpdateTime": "2021-06-23T17:59:01Z",
"message": "Deployment has minimum availability.",
"reason": "MinimumReplicasAvailable",
"status": "True",
"type": "Available"
}
],
"observedGeneration": 5,
"readyReplicas": 3,
"replicas": 3,
"updatedReplicas": 3
}
}

View File

@ -0,0 +1,66 @@
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
creationTimestamp: "2021-06-23T17:01:10Z"
generation: 5
labels:
app: nginx
name: nginx
namespace: edit-test
resourceVersion: "121107"
uid: a598ee47-9635-482b-bacb-16c9e3ade05c
spec:
progressDeadlineSeconds: 600
replicas: 3
revisionHistoryLimit: 10
selector:
matchLabels:
app: nginx
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: gcr.io/kakaraparthy-devel/nginx:latest
imagePullPolicy: Always
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status:
availableReplicas: 3
conditions:
- lastTransitionTime: "2021-06-23T17:01:10Z"
lastUpdateTime: "2021-06-23T17:01:18Z"
message: ReplicaSet "nginx-6f5fdbd667" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
- lastTransitionTime: "2021-06-23T17:59:01Z"
lastUpdateTime: "2021-06-23T17:59:01Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
observedGeneration: 5
readyReplicas: 3
replicas: 4
updatedReplicas: 3

View File

@ -0,0 +1,66 @@
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
creationTimestamp: "2021-06-23T17:01:10Z"
generation: 5
labels:
app: nginx
name: nginx
namespace: edit-test
resourceVersion: "121107"
uid: a598ee47-9635-482b-bacb-16c9e3ade05c
spec:
progressDeadlineSeconds: 600
replicas: 3
revisionHistoryLimit: 10
selector:
matchLabels:
app: nginx
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: gcr.io/kakaraparthy-devel/nginx:latest
imagePullPolicy: Always
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status:
availableReplicas: 3
conditions:
- lastTransitionTime: "2021-06-23T17:01:10Z"
lastUpdateTime: "2021-06-23T17:01:18Z"
message: ReplicaSet "nginx-6f5fdbd667" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
- lastTransitionTime: "2021-06-23T17:59:01Z"
lastUpdateTime: "2021-06-23T17:59:01Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
observedGeneration: 5
readyReplicas: 3
replicas: 3
updatedReplicas: 3

View File

@ -0,0 +1,5 @@
{
"status": {
"replicas": 4
}
}

View File

@ -0,0 +1,85 @@
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"annotations": {
"deployment.kubernetes.io/revision": "1"
},
"creationTimestamp": "2021-06-23T17:01:10Z",
"generation": 5,
"labels": {
"app": "nginx"
},
"name": "nginx",
"namespace": "edit-test",
"resourceVersion": "121107",
"uid": "a598ee47-9635-482b-bacb-16c9e3ade05c"
},
"spec": {
"progressDeadlineSeconds": 600,
"replicas": 3,
"revisionHistoryLimit": 10,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"strategy": {
"rollingUpdate": {
"maxSurge": "25%",
"maxUnavailable": "25%"
},
"type": "RollingUpdate"
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [
{
"image": "gcr.io/kakaraparthy-devel/nginx:latest",
"imagePullPolicy": "Always",
"name": "nginx",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 30
}
}
},
"status": {
"availableReplicas": 3,
"conditions": [
{
"lastTransitionTime": "2021-06-23T17:01:10Z",
"lastUpdateTime": "2021-06-23T17:01:18Z",
"message": "ReplicaSet \"nginx-6f5fdbd667\" has successfully progressed.",
"reason": "NewReplicaSetAvailable",
"status": "True",
"type": "Progressing"
},
{
"lastTransitionTime": "2021-06-23T17:59:01Z",
"lastUpdateTime": "2021-06-23T17:59:01Z",
"message": "Deployment has minimum availability.",
"reason": "MinimumReplicasAvailable",
"status": "True",
"type": "Available"
}
],
"observedGeneration": 5,
"readyReplicas": 3,
"replicas": 4,
"updatedReplicas": 3
}
}

View File

@ -0,0 +1,27 @@
description: edit the status subresource
mode: edit
args:
- deployment
- nginx
namespace: edit-test
subresource: status
expectedStdOut:
- deployment.apps/nginx edited
expectedExitCode: 0
steps:
- type: request
expectedMethod: GET
expectedPath: /apis/extensions/v1beta1/namespaces/edit-test/deployments/nginx/status
expectedInput: 0.request
resultingStatusCode: 200
resultingOutput: 0.response
- type: edit
expectedInput: 1.original
resultingOutput: 1.edited
- type: request
expectedMethod: PATCH
expectedPath: /apis/apps/v1/namespaces/edit-test/deployments/nginx/status
expectedContentType: application/strategic-merge-patch+json
expectedInput: 2.request
resultingStatusCode: 200
resultingOutput: 2.response

View File

@ -51,6 +51,7 @@ import (
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/interrupt"
"k8s.io/kubectl/pkg/util/slice"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
utilpointer "k8s.io/utils/pointer" utilpointer "k8s.io/utils/pointer"
) )
@ -78,6 +79,7 @@ type GetOptions struct {
AllNamespaces bool AllNamespaces bool
Namespace string Namespace string
ExplicitNamespace bool ExplicitNamespace bool
Subresource string
ServerPrint bool ServerPrint bool
@ -132,7 +134,10 @@ var (
kubectl get rc,services kubectl get rc,services
# List one or more resources by their type and names # List one or more resources by their type and names
kubectl get rc/web service/frontend pods/web-pod-13je7`)) kubectl get rc/web service/frontend pods/web-pod-13je7
# List status subresource for a single pod.
kubectl get pod web-pod-13je7 --subresource status`))
) )
const ( const (
@ -140,6 +145,8 @@ const (
useServerPrintColumns = "server-print" useServerPrintColumns = "server-print"
) )
var supportedSubresources = []string{"status", "scale"}
// NewGetOptions returns a GetOptions with default chunk size 500. // NewGetOptions returns a GetOptions with default chunk size 500.
func NewGetOptions(parent string, streams genericclioptions.IOStreams) *GetOptions { func NewGetOptions(parent string, streams genericclioptions.IOStreams) *GetOptions {
return &GetOptions{ return &GetOptions{
@ -197,6 +204,7 @@ func NewCmdGet(parent string, f cmdutil.Factory, streams genericclioptions.IOStr
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to get from a server.") cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to get from a server.")
cmdutil.AddChunkSizeFlag(cmd, &o.ChunkSize) cmdutil.AddChunkSizeFlag(cmd, &o.ChunkSize)
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, gets the subresource of the requested object.", supportedSubresources...)
return cmd return cmd
} }
@ -331,6 +339,9 @@ func (o *GetOptions) Validate(cmd *cobra.Command) error {
if o.OutputWatchEvents && !(o.Watch || o.WatchOnly) { if o.OutputWatchEvents && !(o.Watch || o.WatchOnly) {
return cmdutil.UsageErrorf(cmd, "--output-watch-events option can only be used with --watch or --watch-only") return cmdutil.UsageErrorf(cmd, "--output-watch-events option can only be used with --watch or --watch-only")
} }
if len(o.Subresource) > 0 && !slice.ContainsString(supportedSubresources, o.Subresource, nil) {
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, supportedSubresources)
}
return nil return nil
} }
@ -484,6 +495,7 @@ func (o *GetOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) e
FilenameParam(o.ExplicitNamespace, &o.FilenameOptions). FilenameParam(o.ExplicitNamespace, &o.FilenameOptions).
LabelSelectorParam(o.LabelSelector). LabelSelectorParam(o.LabelSelector).
FieldSelectorParam(o.FieldSelector). FieldSelectorParam(o.FieldSelector).
Subresource(o.Subresource).
RequestChunksOf(chunkSize). RequestChunksOf(chunkSize).
ResourceTypeOrNameArgs(true, args...). ResourceTypeOrNameArgs(true, args...).
ContinueOnError(). ContinueOnError().

View File

@ -242,6 +242,60 @@ foo <unknown>
} }
} }
func TestGetObjectSubresourceStatus(t *testing.T) {
_, _, replicationcontrollers := 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: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &replicationcontrollers.Items[0])},
}
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
cmd := NewCmdGet("kubectl", tf, streams)
cmd.SetOutput(buf)
cmd.Flags().Set("subresource", "status")
cmd.Run(cmd, []string{"replicationcontrollers", "rc1"})
expected := `NAME AGE
rc1 <unknown>
`
if e, a := expected, buf.String(); e != a {
t.Errorf("expected\n%v\ngot\n%v", e, a)
}
}
func TestGetObjectSubresourceScale(t *testing.T) {
_, _, replicationcontrollers := 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: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: replicationControllersScaleSubresourceTableObjBody(codec, replicationcontrollers.Items[0])},
}
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
cmd := NewCmdGet("kubectl", tf, streams)
cmd.SetOutput(buf)
cmd.Flags().Set("subresource", "scale")
cmd.Run(cmd, []string{"replicationcontrollers", "rc1"})
expected := `NAME DESIRED AVAILABLE
rc1 1 0
`
if e, a := expected, buf.String(); e != a {
t.Errorf("expected\n%v\ngot\n%v", e, a)
}
}
func TestGetTableObjects(t *testing.T) { func TestGetTableObjects(t *testing.T) {
pods, _, _ := cmdtesting.TestData() pods, _, _ := cmdtesting.TestData()
@ -2902,3 +2956,23 @@ func emptyTableObjBody(codec runtime.Codec) io.ReadCloser {
} }
return cmdtesting.ObjBody(codec, table) return cmdtesting.ObjBody(codec, table)
} }
func replicationControllersScaleSubresourceTableObjBody(codec runtime.Codec, replicationControllers ...corev1.ReplicationController) io.ReadCloser {
table := &metav1.Table{
ColumnDefinitions: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "Desired", Type: "integer", Description: autoscalingv1.ScaleSpec{}.SwaggerDoc()["replicas"]},
{Name: "Available", Type: "integer", Description: autoscalingv1.ScaleStatus{}.SwaggerDoc()["replicas"]},
},
}
for i := range replicationControllers {
b := bytes.NewBuffer(nil)
codec.Encode(&replicationControllers[i], b)
table.Rows = append(table.Rows, metav1.TableRow{
Object: runtime.RawExtension{Raw: b.Bytes()},
Cells: []interface{}{replicationControllers[i].Name, replicationControllers[i].Spec.Replicas, replicationControllers[i].Status.Replicas},
})
}
return cmdtesting.ObjBody(codec, table)
}

View File

@ -41,6 +41,7 @@ import (
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/slice"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
) )
@ -56,10 +57,11 @@ type PatchOptions struct {
ToPrinter func(string) (printers.ResourcePrinter, error) ToPrinter func(string) (printers.ResourcePrinter, error)
Recorder genericclioptions.Recorder Recorder genericclioptions.Recorder
Local bool Local bool
PatchType string PatchType string
Patch string Patch string
PatchFile string PatchFile string
Subresource string
namespace string namespace string
enforceNamespace bool enforceNamespace bool
@ -94,9 +96,14 @@ var (
kubectl patch pod valid-pod -p '{"spec":{"containers":[{"name":"kubernetes-serve-hostname","image":"new image"}]}}' kubectl patch pod valid-pod -p '{"spec":{"containers":[{"name":"kubernetes-serve-hostname","image":"new image"}]}}'
# Update a container's image using a JSON patch with positional arrays # Update a container's image using a JSON patch with positional arrays
kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"new image"}]'`)) kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"new image"}]'
# Update a deployment's replicas through the scale subresource using a merge patch.
kubectl patch deployment nginx-deployment --subresource='scale' --type='merge' -p '{"spec":{"replicas":2}}'`))
) )
var supportedSubresources = []string{"status", "scale"}
func NewPatchOptions(ioStreams genericclioptions.IOStreams) *PatchOptions { func NewPatchOptions(ioStreams genericclioptions.IOStreams) *PatchOptions {
return &PatchOptions{ return &PatchOptions{
RecordFlags: genericclioptions.NewRecordFlags(), RecordFlags: genericclioptions.NewRecordFlags(),
@ -133,6 +140,7 @@ func NewCmdPatch(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobr
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update") cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update")
cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, patch will operate on the content of the file, not the server-side resource.") cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, patch will operate on the content of the file, not the server-side resource.")
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-patch") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-patch")
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, patch will operate on the subresource of the requested object.", supportedSubresources...)
return cmd return cmd
} }
@ -192,7 +200,9 @@ func (o *PatchOptions) Validate() error {
return fmt.Errorf("--type must be one of %v, not %q", sets.StringKeySet(patchTypes).List(), o.PatchType) return fmt.Errorf("--type must be one of %v, not %q", sets.StringKeySet(patchTypes).List(), o.PatchType)
} }
} }
if len(o.Subresource) > 0 && !slice.ContainsString(supportedSubresources, o.Subresource, nil) {
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, supportedSubresources)
}
return nil return nil
} }
@ -224,6 +234,7 @@ func (o *PatchOptions) RunPatch() error {
LocalParam(o.Local). LocalParam(o.Local).
NamespaceParam(o.namespace).DefaultNamespace(). NamespaceParam(o.namespace).DefaultNamespace().
FilenameParam(o.enforceNamespace, &o.FilenameOptions). FilenameParam(o.enforceNamespace, &o.FilenameOptions).
Subresource(o.Subresource).
ResourceTypeOrNameArgs(false, o.args...). ResourceTypeOrNameArgs(false, o.args...).
Flatten(). Flatten().
Do() Do()
@ -255,7 +266,8 @@ func (o *PatchOptions) RunPatch() error {
helper := resource. helper := resource.
NewHelper(client, mapping). NewHelper(client, mapping).
DryRun(o.dryRunStrategy == cmdutil.DryRunServer). DryRun(o.dryRunStrategy == cmdutil.DryRunServer).
WithFieldManager(o.fieldManager) WithFieldManager(o.fieldManager).
WithSubresource(o.Subresource)
patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil) patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil)
if err != nil { if err != nil {
return err return err

View File

@ -21,6 +21,8 @@ import (
"strings" "strings"
"testing" "testing"
jsonpath "github.com/exponent-io/jsonpath"
corev1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/rest/fake" "k8s.io/client-go/rest/fake"
@ -190,3 +192,51 @@ func TestPatchObjectFromFileOutput(t *testing.T) {
t.Errorf("unexpected output: %s", buf.String()) t.Errorf("unexpected output: %s", buf.String())
} }
} }
func TestPatchSubresource(t *testing.T) {
pod := cmdtesting.SubresourceTestData()
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
expectedStatus := corev1.PodRunning
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/foo/status" && (m == "PATCH" || m == "GET"):
obj := pod
// ensure patched object reflects successful
// patch edits from the client
if m == "PATCH" {
obj.Status.Phase = expectedStatus
}
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
stream, _, buf, _ := genericclioptions.NewTestIOStreams()
cmd := NewCmdPatch(tf, stream)
cmd.Flags().Set("namespace", "test")
cmd.Flags().Set("patch", `{"status":{"phase":"Running"}}`)
cmd.Flags().Set("output", "json")
cmd.Flags().Set("subresource", "status")
cmd.Run(cmd, []string{"pod/foo"})
decoder := jsonpath.NewDecoder(buf)
var actualStatus corev1.PodPhase
decoder.SeekTo("status", "phase")
decoder.Decode(&actualStatus)
// check the status.phase value is updated in the response
if actualStatus != expectedStatus {
t.Errorf("unexpected pod status to be set to %s got: %s", expectedStatus, actualStatus)
}
}

View File

@ -40,6 +40,7 @@ import (
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/slice"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
"k8s.io/kubectl/pkg/validation" "k8s.io/kubectl/pkg/validation"
) )
@ -67,6 +68,8 @@ var (
kubectl replace --force -f ./pod.json`)) kubectl replace --force -f ./pod.json`))
) )
var supportedSubresources = []string{"status", "scale"}
type ReplaceOptions struct { type ReplaceOptions struct {
PrintFlags *genericclioptions.PrintFlags PrintFlags *genericclioptions.PrintFlags
RecordFlags *genericclioptions.RecordFlags RecordFlags *genericclioptions.RecordFlags
@ -92,6 +95,8 @@ type ReplaceOptions struct {
Recorder genericclioptions.Recorder Recorder genericclioptions.Recorder
Subresource string
genericclioptions.IOStreams genericclioptions.IOStreams
fieldManager string fieldManager string
@ -132,6 +137,7 @@ func NewCmdReplace(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr
cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to PUT to the server. Uses the transport specified by the kubeconfig file.") cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to PUT to the server. Uses the transport specified by the kubeconfig file.")
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-replace") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-replace")
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, replace will operate on the subresource of the requested object.", supportedSubresources...)
return cmd return cmd
} }
@ -238,6 +244,10 @@ func (o *ReplaceOptions) Validate(cmd *cobra.Command) error {
} }
} }
if len(o.Subresource) > 0 && !slice.ContainsString(supportedSubresources, o.Subresource, nil) {
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, supportedSubresources)
}
return nil return nil
} }
@ -262,6 +272,7 @@ func (o *ReplaceOptions) Run(f cmdutil.Factory) error {
ContinueOnError(). ContinueOnError().
NamespaceParam(o.Namespace).DefaultNamespace(). NamespaceParam(o.Namespace).DefaultNamespace().
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
Subresource(o.Subresource).
Flatten(). Flatten().
Do() Do()
if err := r.Err(); err != nil { if err := r.Err(); err != nil {
@ -295,6 +306,7 @@ func (o *ReplaceOptions) Run(f cmdutil.Factory) error {
NewHelper(info.Client, info.Mapping). NewHelper(info.Client, info.Mapping).
DryRun(o.DryRunStrategy == cmdutil.DryRunServer). DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
WithFieldManager(o.fieldManager). WithFieldManager(o.fieldManager).
WithSubresource(o.Subresource).
Replace(info.Namespace, info.Name, true, info.Object) Replace(info.Namespace, info.Name, true, info.Object)
if err != nil { if err != nil {
return cmdutil.AddSourceToErr("replacing", info.Source, err) return cmdutil.AddSourceToErr("replacing", info.Source, err)
@ -330,6 +342,7 @@ func (o *ReplaceOptions) forceReplace() error {
NamespaceParam(o.Namespace).DefaultNamespace(). NamespaceParam(o.Namespace).DefaultNamespace().
ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false). ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false).
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
Subresource(o.Subresource).
Flatten() Flatten()
if stdinInUse { if stdinInUse {
b = b.StdinInUse() b = b.StdinInUse()
@ -369,6 +382,7 @@ func (o *ReplaceOptions) forceReplace() error {
ContinueOnError(). ContinueOnError().
NamespaceParam(o.Namespace).DefaultNamespace(). NamespaceParam(o.Namespace).DefaultNamespace().
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
Subresource(o.Subresource).
Flatten() Flatten()
if stdinInUse { if stdinInUse {
b = b.StdinInUse() b = b.StdinInUse()

View File

@ -150,6 +150,22 @@ func EmptyTestData() (*corev1.PodList, *corev1.ServiceList, *corev1.ReplicationC
return pods, svc, rc return pods, svc, rc
} }
func SubresourceTestData() *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
DNSPolicy: corev1.DNSClusterFirst,
TerminationGracePeriodSeconds: &grace,
SecurityContext: &corev1.PodSecurityContext{},
EnableServiceLinks: &enableServiceLinks,
},
Status: corev1.PodStatus{
Phase: corev1.PodPending,
},
}
}
func GenResponseWithJsonEncodedBody(bodyStruct interface{}) (*http.Response, error) { func GenResponseWithJsonEncodedBody(bodyStruct interface{}) (*http.Response, error) {
jsonBytes, err := json.Marshal(bodyStruct) jsonBytes, err := json.Marshal(bodyStruct)
if err != nil { if err != nil {

View File

@ -51,8 +51,11 @@ import (
"k8s.io/kubectl/pkg/cmd/util/editor/crlf" "k8s.io/kubectl/pkg/cmd/util/editor/crlf"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
"k8s.io/kubectl/pkg/util/slice"
) )
var SupportedSubresources = []string{"status"}
// EditOptions contains all the options for running edit cli command. // EditOptions contains all the options for running edit cli command.
type EditOptions struct { type EditOptions struct {
resource.FilenameOptions resource.FilenameOptions
@ -84,6 +87,8 @@ type EditOptions struct {
updatedResultGetter func(data []byte) *resource.Result updatedResultGetter func(data []byte) *resource.Result
FieldManager string FieldManager string
Subresource string
} }
// NewEditOptions returns an initialized EditOptions instance // NewEditOptions returns an initialized EditOptions instance
@ -184,6 +189,7 @@ func (o *EditOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Comm
} }
r := b.NamespaceParam(cmdNamespace).DefaultNamespace(). r := b.NamespaceParam(cmdNamespace).DefaultNamespace().
FilenameParam(enforceNamespace, &o.FilenameOptions). FilenameParam(enforceNamespace, &o.FilenameOptions).
Subresource(o.Subresource).
ContinueOnError(). ContinueOnError().
Flatten(). Flatten().
Do() Do()
@ -198,6 +204,7 @@ func (o *EditOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Comm
return f.NewBuilder(). return f.NewBuilder().
Unstructured(). Unstructured().
Stream(bytes.NewReader(data), "edited-file"). Stream(bytes.NewReader(data), "edited-file").
Subresource(o.Subresource).
ContinueOnError(). ContinueOnError().
Flatten(). Flatten().
Do() Do()
@ -216,6 +223,9 @@ func (o *EditOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Comm
// Validate checks the EditOptions to see if there is sufficient information to run the command. // Validate checks the EditOptions to see if there is sufficient information to run the command.
func (o *EditOptions) Validate() error { func (o *EditOptions) Validate() error {
if len(o.Subresource) > 0 && !slice.ContainsString(SupportedSubresources, o.Subresource, nil) {
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, SupportedSubresources)
}
return nil return nil
} }
@ -561,7 +571,7 @@ func (o *EditOptions) annotationPatch(update *resource.Info) error {
if err != nil { if err != nil {
return err return err
} }
helper := resource.NewHelper(client, mapping).WithFieldManager(o.FieldManager) helper := resource.NewHelper(client, mapping).WithFieldManager(o.FieldManager).WithSubresource(o.Subresource)
_, err = helper.Patch(o.CmdNamespace, update.Name, patchType, patch, nil) _, err = helper.Patch(o.CmdNamespace, update.Name, patchType, patch, nil)
if err != nil { if err != nil {
return err return err
@ -699,7 +709,7 @@ func (o *EditOptions) visitToPatch(originalInfos []*resource.Info, patchVisitor
} }
patched, err := resource.NewHelper(info.Client, info.Mapping). patched, err := resource.NewHelper(info.Client, info.Mapping).
WithFieldManager(o.FieldManager). WithFieldManager(o.FieldManager).WithSubresource(o.Subresource).
Patch(info.Namespace, info.Name, patchType, patch, nil) Patch(info.Namespace, info.Name, patchType, patch, nil)
if err != nil { if err != nil {
fmt.Fprintln(o.ErrOut, results.addError(err, info)) fmt.Fprintln(o.ErrOut, results.addError(err, info))

View File

@ -470,6 +470,10 @@ 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). Matching objects must satisfy all of the specified label constraints.") cmd.Flags().StringVarP(p, "selector", "l", *p, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.")
} }
func AddSubresourceFlags(cmd *cobra.Command, subresource *string, usage string, allowedSubresources ...string) {
cmd.Flags().StringVar(subresource, "subresource", "", fmt.Sprintf("%s Must be one of %v. This flag is alpha and may change in the future.", usage, allowedSubresources))
}
type ValidateOptions struct { type ValidateOptions struct {
EnableValidation bool EnableValidation bool
} }