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:
parent
d0173dffba
commit
38487e770c
|
@ -63,7 +63,10 @@ var (
|
|||
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`))
|
||||
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
|
||||
|
@ -80,6 +83,7 @@ func NewCmdEdit(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra
|
|||
ValidArgsFunction: util.ResourceTypeAndNameCompletionFunc(f),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, args, cmd))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
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.")
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-edit")
|
||||
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
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ type EditTestCase struct {
|
|||
Output string `yaml:"outputFormat"`
|
||||
OutputPatch string `yaml:"outputPatch"`
|
||||
SaveConfig string `yaml:"saveConfig"`
|
||||
Subresource string `yaml:"subresource"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
ExpectedStdout []string `yaml:"expectedStdout"`
|
||||
ExpectedStderr []string `yaml:"expectedStderr"`
|
||||
|
@ -253,6 +254,9 @@ func TestEdit(t *testing.T) {
|
|||
if len(testcase.SaveConfig) > 0 {
|
||||
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) {
|
||||
errBuf.WriteString(str)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"status": {
|
||||
"replicas": 4
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -51,6 +51,7 @@ import (
|
|||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/interrupt"
|
||||
"k8s.io/kubectl/pkg/util/slice"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
)
|
||||
|
@ -78,6 +79,7 @@ type GetOptions struct {
|
|||
AllNamespaces bool
|
||||
Namespace string
|
||||
ExplicitNamespace bool
|
||||
Subresource string
|
||||
|
||||
ServerPrint bool
|
||||
|
||||
|
@ -132,7 +134,10 @@ var (
|
|||
kubectl get rc,services
|
||||
|
||||
# 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 (
|
||||
|
@ -140,6 +145,8 @@ const (
|
|||
useServerPrintColumns = "server-print"
|
||||
)
|
||||
|
||||
var supportedSubresources = []string{"status", "scale"}
|
||||
|
||||
// NewGetOptions returns a GetOptions with default chunk size 500.
|
||||
func NewGetOptions(parent string, streams genericclioptions.IOStreams) *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.AddChunkSizeFlag(cmd, &o.ChunkSize)
|
||||
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, gets the subresource of the requested object.", supportedSubresources...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
@ -331,6 +339,9 @@ func (o *GetOptions) Validate(cmd *cobra.Command) error {
|
|||
if o.OutputWatchEvents && !(o.Watch || o.WatchOnly) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -484,6 +495,7 @@ func (o *GetOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) e
|
|||
FilenameParam(o.ExplicitNamespace, &o.FilenameOptions).
|
||||
LabelSelectorParam(o.LabelSelector).
|
||||
FieldSelectorParam(o.FieldSelector).
|
||||
Subresource(o.Subresource).
|
||||
RequestChunksOf(chunkSize).
|
||||
ResourceTypeOrNameArgs(true, args...).
|
||||
ContinueOnError().
|
||||
|
|
|
@ -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) {
|
||||
pods, _, _ := cmdtesting.TestData()
|
||||
|
||||
|
@ -2902,3 +2956,23 @@ func emptyTableObjBody(codec runtime.Codec) io.ReadCloser {
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import (
|
|||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/slice"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
|
@ -56,10 +57,11 @@ type PatchOptions struct {
|
|||
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||
Recorder genericclioptions.Recorder
|
||||
|
||||
Local bool
|
||||
PatchType string
|
||||
Patch string
|
||||
PatchFile string
|
||||
Local bool
|
||||
PatchType string
|
||||
Patch string
|
||||
PatchFile string
|
||||
Subresource string
|
||||
|
||||
namespace string
|
||||
enforceNamespace bool
|
||||
|
@ -94,9 +96,14 @@ var (
|
|||
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
|
||||
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 {
|
||||
return &PatchOptions{
|
||||
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")
|
||||
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.AddSubresourceFlags(cmd, &o.Subresource, "If specified, patch will operate on the subresource of the requested object.", supportedSubresources...)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -224,6 +234,7 @@ func (o *PatchOptions) RunPatch() error {
|
|||
LocalParam(o.Local).
|
||||
NamespaceParam(o.namespace).DefaultNamespace().
|
||||
FilenameParam(o.enforceNamespace, &o.FilenameOptions).
|
||||
Subresource(o.Subresource).
|
||||
ResourceTypeOrNameArgs(false, o.args...).
|
||||
Flatten().
|
||||
Do()
|
||||
|
@ -255,7 +266,8 @@ func (o *PatchOptions) RunPatch() error {
|
|||
helper := resource.
|
||||
NewHelper(client, mapping).
|
||||
DryRun(o.dryRunStrategy == cmdutil.DryRunServer).
|
||||
WithFieldManager(o.fieldManager)
|
||||
WithFieldManager(o.fieldManager).
|
||||
WithSubresource(o.Subresource)
|
||||
patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -21,6 +21,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
jsonpath "github.com/exponent-io/jsonpath"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
|
@ -190,3 +192,51 @@ func TestPatchObjectFromFileOutput(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import (
|
|||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/slice"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
)
|
||||
|
@ -67,6 +68,8 @@ var (
|
|||
kubectl replace --force -f ./pod.json`))
|
||||
)
|
||||
|
||||
var supportedSubresources = []string{"status", "scale"}
|
||||
|
||||
type ReplaceOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
|
@ -92,6 +95,8 @@ type ReplaceOptions struct {
|
|||
|
||||
Recorder genericclioptions.Recorder
|
||||
|
||||
Subresource string
|
||||
|
||||
genericclioptions.IOStreams
|
||||
|
||||
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.")
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -262,6 +272,7 @@ func (o *ReplaceOptions) Run(f cmdutil.Factory) error {
|
|||
ContinueOnError().
|
||||
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
|
||||
Subresource(o.Subresource).
|
||||
Flatten().
|
||||
Do()
|
||||
if err := r.Err(); err != nil {
|
||||
|
@ -295,6 +306,7 @@ func (o *ReplaceOptions) Run(f cmdutil.Factory) error {
|
|||
NewHelper(info.Client, info.Mapping).
|
||||
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
|
||||
WithFieldManager(o.fieldManager).
|
||||
WithSubresource(o.Subresource).
|
||||
Replace(info.Namespace, info.Name, true, info.Object)
|
||||
if err != nil {
|
||||
return cmdutil.AddSourceToErr("replacing", info.Source, err)
|
||||
|
@ -330,6 +342,7 @@ func (o *ReplaceOptions) forceReplace() error {
|
|||
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||
ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false).
|
||||
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
|
||||
Subresource(o.Subresource).
|
||||
Flatten()
|
||||
if stdinInUse {
|
||||
b = b.StdinInUse()
|
||||
|
@ -369,6 +382,7 @@ func (o *ReplaceOptions) forceReplace() error {
|
|||
ContinueOnError().
|
||||
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||
FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
|
||||
Subresource(o.Subresource).
|
||||
Flatten()
|
||||
if stdinInUse {
|
||||
b = b.StdinInUse()
|
||||
|
|
|
@ -150,6 +150,22 @@ func EmptyTestData() (*corev1.PodList, *corev1.ServiceList, *corev1.ReplicationC
|
|||
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) {
|
||||
jsonBytes, err := json.Marshal(bodyStruct)
|
||||
if err != nil {
|
||||
|
|
|
@ -51,8 +51,11 @@ import (
|
|||
"k8s.io/kubectl/pkg/cmd/util/editor/crlf"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"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.
|
||||
type EditOptions struct {
|
||||
resource.FilenameOptions
|
||||
|
@ -84,6 +87,8 @@ type EditOptions struct {
|
|||
updatedResultGetter func(data []byte) *resource.Result
|
||||
|
||||
FieldManager string
|
||||
|
||||
Subresource string
|
||||
}
|
||||
|
||||
// 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().
|
||||
FilenameParam(enforceNamespace, &o.FilenameOptions).
|
||||
Subresource(o.Subresource).
|
||||
ContinueOnError().
|
||||
Flatten().
|
||||
Do()
|
||||
|
@ -198,6 +204,7 @@ func (o *EditOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Comm
|
|||
return f.NewBuilder().
|
||||
Unstructured().
|
||||
Stream(bytes.NewReader(data), "edited-file").
|
||||
Subresource(o.Subresource).
|
||||
ContinueOnError().
|
||||
Flatten().
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -561,7 +571,7 @@ func (o *EditOptions) annotationPatch(update *resource.Info) error {
|
|||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -699,7 +709,7 @@ func (o *EditOptions) visitToPatch(originalInfos []*resource.Info, patchVisitor
|
|||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
fmt.Fprintln(o.ErrOut, results.addError(err, info))
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
||||
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 {
|
||||
EnableValidation bool
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue