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

Co-authored-by: Nikhita Raghunath <nikitaraghunath@gmail.com>
This commit is contained in:
Yuvaraj Kakaraparthi 2021-07-07 08:30:59 -07:00 committed by Nikhita Raghunath
parent f97825e1ce
commit a5aa858d44
25 changed files with 775 additions and 36 deletions

View File

@ -588,6 +588,7 @@ func AddHandlers(h printers.PrintHandler) {
{Name: "Name", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, {Name: "Name", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "Desired", Type: "integer", Description: autoscalingv1.ScaleSpec{}.SwaggerDoc()["replicas"]}, {Name: "Desired", Type: "integer", Description: autoscalingv1.ScaleSpec{}.SwaggerDoc()["replicas"]},
{Name: "Available", Type: "integer", Description: autoscalingv1.ScaleStatus{}.SwaggerDoc()["replicas"]}, {Name: "Available", Type: "integer", Description: autoscalingv1.ScaleStatus{}.SwaggerDoc()["replicas"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
} }
h.TableHandler(scaleColumnDefinitions, printScale) h.TableHandler(scaleColumnDefinitions, printScale)
} }
@ -2627,7 +2628,7 @@ func printScale(obj *autoscaling.Scale, options printers.GenerateOptions) ([]met
row := metav1.TableRow{ row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj}, Object: runtime.RawExtension{Object: obj},
} }
row.Cells = append(row.Cells, obj.Name, obj.Spec.Replicas, obj.Status.Replicas) row.Cells = append(row.Cells, obj.Name, obj.Spec.Replicas, obj.Status.Replicas, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil return []metav1.TableRow{row}, nil
} }

View File

@ -5841,7 +5841,7 @@ func TestPrintScale(t *testing.T) {
}, },
expected: []metav1.TableRow{ expected: []metav1.TableRow{
{ {
Cells: []interface{}{"test-autoscaling", int32(2), int32(1)}, Cells: []interface{}{"test-autoscaling", int32(2), int32(1), string("0s")},
}, },
}, },
}, },

View File

@ -83,7 +83,8 @@ type Builder struct {
limitChunks int64 limitChunks int64
requestTransforms []RequestTransform requestTransforms []RequestTransform
resources []string resources []string
subresource string
namespace string namespace string
allNamespace bool allNamespace bool
@ -555,6 +556,13 @@ func (b *Builder) TransformRequests(opts ...RequestTransform) *Builder {
return b return b
} }
// Subresource instructs the builder to retrieve the object at the
// subresource path instead of the main resource path.
func (b *Builder) Subresource(subresource string) *Builder {
b.subresource = subresource
return b
}
// SelectEverythingParam // SelectEverythingParam
func (b *Builder) SelectAllParam(selectAll bool) *Builder { func (b *Builder) SelectAllParam(selectAll bool) *Builder {
if selectAll && (b.labelSelector != nil || b.fieldSelector != nil) { if selectAll && (b.labelSelector != nil || b.fieldSelector != nil) {
@ -886,6 +894,10 @@ func (b *Builder) visitBySelector() *Result {
if len(b.resources) == 0 { if len(b.resources) == 0 {
return result.withError(fmt.Errorf("at least one resource must be specified to use a selector")) return result.withError(fmt.Errorf("at least one resource must be specified to use a selector"))
} }
if len(b.subresource) != 0 {
return result.withError(fmt.Errorf("subresource cannot be used when bulk resources are specified"))
}
mappings, err := b.resourceMappings() mappings, err := b.resourceMappings()
if err != nil { if err != nil {
result.err = err result.err = err
@ -1007,10 +1019,11 @@ func (b *Builder) visitByResource() *Result {
} }
info := &Info{ info := &Info{
Client: client, Client: client,
Mapping: mapping, Mapping: mapping,
Namespace: selectorNamespace, Namespace: selectorNamespace,
Name: tuple.Name, Name: tuple.Name,
Subresource: b.subresource,
} }
items = append(items, info) items = append(items, info)
} }
@ -1071,10 +1084,11 @@ func (b *Builder) visitByName() *Result {
visitors := []Visitor{} visitors := []Visitor{}
for _, name := range b.names { for _, name := range b.names {
info := &Info{ info := &Info{
Client: client, Client: client,
Mapping: mapping, Mapping: mapping,
Namespace: selectorNamespace, Namespace: selectorNamespace,
Name: name, Name: name,
Subresource: b.subresource,
} }
visitors = append(visitors, info) visitors = append(visitors, info)
} }

View File

@ -150,6 +150,14 @@ func streamTestData() (io.Reader, *v1.PodList, *v1.ServiceList) {
return r, pods, svc return r, pods, svc
} }
func subresourceTestData(name string) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test", ResourceVersion: "10"},
Spec: V1DeepEqualSafePodSpec(),
Status: V1DeepEqualSafePodStatus(),
}
}
func JSONToYAMLOrDie(in []byte) []byte { func JSONToYAMLOrDie(in []byte) []byte {
data, err := yaml.JSONToYAML(in) data, err := yaml.JSONToYAML(in)
if err != nil { if err != nil {
@ -915,6 +923,37 @@ func TestResourceByName(t *testing.T) {
t.Errorf("unexpected resource mapping: %#v", mapping) t.Errorf("unexpected resource mapping: %#v", mapping)
} }
} }
func TestSubresourceByName(t *testing.T) {
pod := subresourceTestData("foo")
b := newDefaultBuilderWith(fakeClientWith("", t, map[string]string{
"/namespaces/test/pods/foo/status": runtime.EncodeOrDie(corev1Codec, pod),
})).NamespaceParam("test")
test := &testVisitor{}
singleItemImplied := false
if b.Do().Err() == nil {
t.Errorf("unexpected non-error")
}
b.ResourceTypeOrNameArgs(true, "pods", "foo").Subresource("status")
err := b.Do().IntoSingleItemImplied(&singleItemImplied).Visit(test.Handle)
if err != nil || !singleItemImplied || len(test.Infos) != 1 {
t.Fatalf("unexpected response: %v %t %#v", err, singleItemImplied, test.Infos)
}
if !apiequality.Semantic.DeepEqual(pod, test.Objects()[0]) {
t.Errorf("unexpected object: %#v", test.Objects()[0])
}
mapping, err := b.Do().ResourceMapping()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mapping.Resource != (schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}) {
t.Errorf("unexpected resource mapping: %#v", mapping)
}
}
func TestRestMappingErrors(t *testing.T) { func TestRestMappingErrors(t *testing.T) {
pods, _ := testData() pods, _ := testData()
@ -1260,8 +1299,9 @@ func TestResourceTuple(t *testing.T) {
expectNoErr := func(err error) bool { return err == nil } expectNoErr := func(err error) bool { return err == nil }
expectErr := func(err error) bool { return err != nil } expectErr := func(err error) bool { return err != nil }
testCases := map[string]struct { testCases := map[string]struct {
args []string args []string
errFn func(error) bool subresource string
errFn func(error) bool
}{ }{
"valid": { "valid": {
args: []string{"pods/foo"}, args: []string{"pods/foo"},
@ -1303,6 +1343,16 @@ func TestResourceTuple(t *testing.T) {
args: []string{"bar/"}, args: []string{"bar/"},
errFn: expectErr, errFn: expectErr,
}, },
"valid status subresource": {
args: []string{"pods/foo"},
subresource: "status",
errFn: expectNoErr,
},
"valid status subresource for multiple with name indirection": {
args: []string{"pods/foo", "pod/bar"},
subresource: "status",
errFn: expectNoErr,
},
} }
for k, tt := range testCases { for k, tt := range testCases {
t.Run("using default namespace", func(t *testing.T) { t.Run("using default namespace", func(t *testing.T) {
@ -1311,14 +1361,18 @@ func TestResourceTuple(t *testing.T) {
if requireObject { if requireObject {
pods, _ := testData() pods, _ := testData()
expectedRequests = map[string]string{ expectedRequests = map[string]string{
"/namespaces/test/pods/foo": runtime.EncodeOrDie(corev1Codec, &pods.Items[0]), "/namespaces/test/pods/foo": runtime.EncodeOrDie(corev1Codec, &pods.Items[0]),
"/namespaces/test/pods/bar": runtime.EncodeOrDie(corev1Codec, &pods.Items[0]), "/namespaces/test/pods/bar": runtime.EncodeOrDie(corev1Codec, &pods.Items[0]),
"/nodes/foo": runtime.EncodeOrDie(corev1Codec, &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}), "/namespaces/test/pods/foo/status": runtime.EncodeOrDie(corev1Codec, subresourceTestData("foo")),
"/namespaces/test/pods/bar/status": runtime.EncodeOrDie(corev1Codec, subresourceTestData("bar")),
"/nodes/foo": runtime.EncodeOrDie(corev1Codec, &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}),
} }
} }
b := newDefaultBuilderWith(fakeClientWith(k, t, expectedRequests)). b := newDefaultBuilderWith(fakeClientWith(k, t, expectedRequests)).
NamespaceParam("test").DefaultNamespace(). NamespaceParam("test").DefaultNamespace().
ResourceTypeOrNameArgs(true, tt.args...).RequireObject(requireObject) ResourceTypeOrNameArgs(true, tt.args...).
RequireObject(requireObject).
Subresource(tt.subresource)
r := b.Do() r := b.Do()
@ -1557,6 +1611,23 @@ func TestListObjectWithDifferentVersions(t *testing.T) {
} }
} }
func TestListObjectSubresource(t *testing.T) {
pods, _ := testData()
labelKey := metav1.LabelSelectorQueryParam(corev1GV.String())
b := newDefaultBuilderWith(fakeClientWith("", t, map[string]string{
"/namespaces/test/pods?" + labelKey: runtime.EncodeOrDie(corev1Codec, pods),
})).
NamespaceParam("test").
ResourceTypeOrNameArgs(true, "pods").
Subresource("status").
Flatten()
_, err := b.Do().Object()
if err == nil || !strings.Contains(err.Error(), "subresource cannot be used when bulk resources are specified") {
t.Fatalf("unexpected response: %v", err)
}
}
func TestWatch(t *testing.T) { func TestWatch(t *testing.T) {
_, svc := testData() _, svc := testData()
w, err := newDefaultBuilderWith(fakeClientWith("", t, map[string]string{ w, err := newDefaultBuilderWith(fakeClientWith("", t, map[string]string{

View File

@ -37,6 +37,8 @@ var metadataAccessor = meta.NewAccessor()
type Helper struct { type Helper struct {
// The name of this resource as the server would recognize it // The name of this resource as the server would recognize it
Resource string Resource string
// The name of the subresource as the server would recognize it
Subresource string
// A RESTClient capable of mutating this resource. // A RESTClient capable of mutating this resource.
RESTClient RESTClient RESTClient RESTClient
// True if the resource type is scoped to namespaces // True if the resource type is scoped to namespaces
@ -77,11 +79,18 @@ func (m *Helper) WithFieldManager(fieldManager string) *Helper {
return m return m
} }
// Subresource sets the helper to access (<resource>/[ns/<namespace>/]<name>/<subresource>)
func (m *Helper) WithSubresource(subresource string) *Helper {
m.Subresource = subresource
return m
}
func (m *Helper) Get(namespace, name string) (runtime.Object, error) { func (m *Helper) Get(namespace, name string) (runtime.Object, error) {
req := m.RESTClient.Get(). req := m.RESTClient.Get().
NamespaceIfScoped(namespace, m.NamespaceScoped). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
Name(name) Name(name).
SubResource(m.Subresource)
return req.Do(context.TODO()).Get() return req.Do(context.TODO()).Get()
} }
@ -237,6 +246,7 @@ func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte,
NamespaceIfScoped(namespace, m.NamespaceScoped). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
Name(name). Name(name).
SubResource(m.Subresource).
VersionedParams(options, metav1.ParameterCodec). VersionedParams(options, metav1.ParameterCodec).
Body(data). Body(data).
Do(context.TODO()). Do(context.TODO()).
@ -261,7 +271,7 @@ func (m *Helper) Replace(namespace, name string, overwrite bool, obj runtime.Obj
} }
if version == "" && overwrite { if version == "" && overwrite {
// Retrieve the current version of the object to overwrite the server object // Retrieve the current version of the object to overwrite the server object
serverObj, err := c.Get().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(m.Resource).Name(name).Do(context.TODO()).Get() serverObj, err := c.Get().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(m.Resource).Name(name).SubResource(m.Subresource).Do(context.TODO()).Get()
if err != nil { if err != nil {
// The object does not exist, but we want it to be created // The object does not exist, but we want it to be created
return m.replaceResource(c, m.Resource, namespace, name, obj, options) return m.replaceResource(c, m.Resource, namespace, name, obj, options)
@ -283,6 +293,7 @@ func (m *Helper) replaceResource(c RESTClient, resource, namespace, name string,
NamespaceIfScoped(namespace, m.NamespaceScoped). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(resource). Resource(resource).
Name(name). Name(name).
SubResource(m.Subresource).
VersionedParams(options, metav1.ParameterCodec). VersionedParams(options, metav1.ParameterCodec).
Body(obj). Body(obj).
Do(context.TODO()). Do(context.TODO()).

View File

@ -68,6 +68,17 @@ func V1DeepEqualSafePodSpec() corev1.PodSpec {
} }
} }
func V1DeepEqualSafePodStatus() corev1.PodStatus {
return corev1.PodStatus{
Conditions: []corev1.PodCondition{
{
Status: corev1.ConditionTrue,
Type: corev1.PodReady,
},
},
}
}
func TestHelperDelete(t *testing.T) { func TestHelperDelete(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -257,11 +268,12 @@ func TestHelperCreate(t *testing.T) {
func TestHelperGet(t *testing.T) { func TestHelperGet(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
Err bool subresource string
Req func(*http.Request) bool Err bool
Resp *http.Response Req func(*http.Request) bool
HttpErr error Resp *http.Response
HttpErr error
}{ }{
{ {
name: "test1", name: "test1",
@ -301,6 +313,35 @@ func TestHelperGet(t *testing.T) {
return true return true
}, },
}, },
{
name: "test with subresource",
subresource: "status",
Resp: &http.Response{
StatusCode: http.StatusOK,
Header: header(),
Body: objBody(&corev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}),
},
Req: func(req *http.Request) bool {
if req.Method != "GET" {
t.Errorf("unexpected method: %#v", req)
return false
}
parts := splitPath(req.URL.Path)
if parts[1] != "bar" {
t.Errorf("url doesn't contain namespace: %#v", req)
return false
}
if parts[2] != "foo" {
t.Errorf("url doesn't contain name: %#v", req)
return false
}
if parts[3] != "status" {
t.Errorf("url doesn't contain subresource: %#v", req)
return false
}
return true
},
},
} }
for i, tt := range tests { for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -313,6 +354,7 @@ func TestHelperGet(t *testing.T) {
modifier := &Helper{ modifier := &Helper{
RESTClient: client, RESTClient: client,
NamespaceScoped: true, NamespaceScoped: true,
Subresource: tt.subresource,
} }
obj, err := modifier.Get("bar", "foo") obj, err := modifier.Get("bar", "foo")
@ -382,6 +424,34 @@ func TestHelperList(t *testing.T) {
return true return true
}, },
}, },
{
name: "test with",
Resp: &http.Response{
StatusCode: http.StatusOK,
Header: header(),
Body: objBody(&corev1.PodList{
Items: []corev1.Pod{{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
},
},
}),
},
Req: func(req *http.Request) bool {
if req.Method != "GET" {
t.Errorf("unexpected method: %#v", req)
return false
}
if req.URL.Path != "/namespaces/bar" {
t.Errorf("url doesn't contain name: %#v", req.URL)
return false
}
if req.URL.Query().Get(metav1.LabelSelectorQueryParam(corev1GV.String())) != labels.SelectorFromSet(labels.Set{"foo": "baz"}).String() {
t.Errorf("url doesn't contain query parameters: %#v", req.URL)
return false
}
return true
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -501,6 +571,7 @@ func TestHelperReplace(t *testing.T) {
Object runtime.Object Object runtime.Object
Namespace string Namespace string
NamespaceScoped bool NamespaceScoped bool
Subresource string
ExpectPath string ExpectPath string
ExpectObject runtime.Object ExpectObject runtime.Object
@ -592,6 +663,29 @@ func TestHelperReplace(t *testing.T) {
Resp: &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})}, Resp: &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})},
Req: expectPut, Req: expectPut,
}, },
{
Name: "test7 - with status subresource",
Namespace: "bar",
NamespaceScoped: true,
Subresource: "status",
Object: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Status: V1DeepEqualSafePodStatus(),
},
ExpectPath: "/namespaces/bar/foo/status",
ExpectObject: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"},
Status: V1DeepEqualSafePodStatus(),
},
Overwrite: true,
HTTPClient: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
if req.Method == "PUT" {
return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})}, nil
}
return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil
}),
Req: expectPut,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) { t.Run(tt.Name, func(t *testing.T) {
@ -605,6 +699,7 @@ func TestHelperReplace(t *testing.T) {
modifier := &Helper{ modifier := &Helper{
RESTClient: client, RESTClient: client,
NamespaceScoped: tt.NamespaceScoped, NamespaceScoped: tt.NamespaceScoped,
Subresource: tt.Subresource,
} }
_, err := modifier.Replace(tt.Namespace, "foo", tt.Overwrite, tt.Object) _, err := modifier.Replace(tt.Namespace, "foo", tt.Overwrite, tt.Object)
if (err != nil) != tt.Err { if (err != nil) != tt.Err {

View File

@ -78,12 +78,16 @@ type Info struct {
// defined. If retrieved from the server, the Builder expects the mapping client to // defined. If retrieved from the server, the Builder expects the mapping client to
// decide the final form. Use the AsVersioned, AsUnstructured, and AsInternal helpers // decide the final form. Use the AsVersioned, AsUnstructured, and AsInternal helpers
// to alter the object versions. // to alter the object versions.
// If Subresource is specified, this will be the object for the subresource.
Object runtime.Object Object runtime.Object
// Optional, this is the most recent resource version the server knows about for // Optional, this is the most recent resource version the server knows about for
// this type of resource. It may not match the resource version of the object, // this type of resource. It may not match the resource version of the object,
// but if set it should be equal to or newer than the resource version of the // but if set it should be equal to or newer than the resource version of the
// object (however the server defines resource version). // object (however the server defines resource version).
ResourceVersion string ResourceVersion string
// Optional, if specified, the object is the most recent value of the subresource
// returned by the server if available.
Subresource string
} }
// Visit implements Visitor // Visit implements Visitor
@ -93,7 +97,7 @@ func (i *Info) Visit(fn VisitorFunc) error {
// Get retrieves the object from the Namespace and Name fields // Get retrieves the object from the Namespace and Name fields
func (i *Info) Get() (err error) { func (i *Info) Get() (err error) {
obj, err := NewHelper(i.Client, i.Mapping).Get(i.Namespace, i.Name) obj, err := NewHelper(i.Client, i.Mapping).WithSubresource(i.Subresource).Get(i.Namespace, i.Name)
if err != nil { if err != nil {
if errors.IsNotFound(err) && len(i.Namespace) > 0 && i.Namespace != metav1.NamespaceDefault && i.Namespace != metav1.NamespaceAll { if errors.IsNotFound(err) && len(i.Namespace) > 0 && i.Namespace != metav1.NamespaceDefault && i.Namespace != metav1.NamespaceAll {
err2 := i.Client.Get().AbsPath("api", "v1", "namespaces", i.Namespace).Do(context.TODO()).Error() err2 := i.Client.Get().AbsPath("api", "v1", "namespaces", i.Namespace).Do(context.TODO()).Error()

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

@ -468,6 +468,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
} }

View File

@ -175,6 +175,14 @@ run_kubectl_get_tests() {
output_message=$(! kubectl get pod valid-pod --allow-missing-template-keys=false -o go-template='{{.missing}}' "${kube_flags[@]}") output_message=$(! kubectl get pod valid-pod --allow-missing-template-keys=false -o go-template='{{.missing}}' "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'map has no entry for key "missing"' kube::test::if_has_string "${output_message}" 'map has no entry for key "missing"'
## check --subresource=status works
output_message=$(kubectl get "${kube_flags[@]}" pod valid-pod --subresource=status)
kube::test::if_has_string "${output_message}" 'valid-pod'
## check --subresource=scale returns an error for pods
output_message=$(! kubectl get pod valid-pod --subresource=scale 2>&1 "${kube_flags[@]:?}")
kube::test::if_has_string "${output_message}" 'the server could not find the requested resource'
### Test kubectl get watch ### Test kubectl get watch
output_message=$(kubectl get pods -w --request-timeout=1 "${kube_flags[@]}") output_message=$(kubectl get pods -w --request-timeout=1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'STATUS' # headers kube::test::if_has_string "${output_message}" 'STATUS' # headers