mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 10:51:29 +00:00
kubectl: add --support to get, patch, edit and replace commands
Co-authored-by: Nikhita Raghunath <nikitaraghunath@gmail.com>
This commit is contained in:
parent
f97825e1ce
commit
a5aa858d44
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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{
|
||||||
|
@ -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()).
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
0
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/0.request
vendored
Normal file
0
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/0.request
vendored
Normal file
85
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/0.response
vendored
Normal file
85
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/0.response
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
66
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/1.edited
vendored
Normal file
66
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/1.edited
vendored
Normal 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
|
66
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/1.original
vendored
Normal file
66
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/1.original
vendored
Normal 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
|
5
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/2.request
vendored
Normal file
5
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/2.request
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"status": {
|
||||||
|
"replicas": 4
|
||||||
|
}
|
||||||
|
}
|
85
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/2.response
vendored
Normal file
85
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/2.response
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
27
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/test.yaml
vendored
Normal file
27
staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status/test.yaml
vendored
Normal 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
|
@ -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().
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user