Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic client supports subresources #56717

Merged
merged 1 commit into from Feb 14, 2018

Conversation

roycaihw
Copy link
Member

@roycaihw roycaihw commented Dec 1, 2017

What this PR does / why we need it:
Allows resource.name to be a subresource which contains "/" in

resource *metav1.APIResource

Which issue(s) this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close the issue(s) when PR gets merged):
Fixes #49588

Special notes for your reviewer:
The change is backward compatible.

Release note:

NONE

/sig api-machinery

@k8s-ci-robot k8s-ci-robot added release-note-none Denotes a PR that doesn't merit a release note. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Dec 1, 2017
@roycaihw roycaihw changed the title Dynamic client support subresource Dynamic client supports subresources Dec 1, 2017
@@ -151,9 +151,18 @@ func (rc *ResourceClient) List(opts metav1.ListOptions) (runtime.Object, error)
if parameterEncoder == nil {
parameterEncoder = defaultParameterEncoder
}
resourceName := ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this block of code a private method of ResourceClient to avoid code duplication.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. I also put a if logic into Create() instead of having two Create functions.

Copy link
Member

@caesarxuchao caesarxuchao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users usually creates a client_pool. I think it client_pool will only work with some subresources, e.g., it will work with apps/v1/replicaset/status, but not apps/v1/replicaset/scale, because in the latter case, the gvk of scale is autoscaling/v1/scale, different from the group version of the main resource. Could you manually test these cases, and then write e2e tests?

@@ -114,10 +117,47 @@ func TestList(t *testing.T) {
},
},
},
{
resource: "rtest/srtest",
name: "normal_list",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name should be unique for each test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non_namespaced_list? "normal" doesn't make sense. That could be another PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have updated Name to be unique for each test.

NamespaceIfScoped(rc.ns, rc.resource.Namespaced).
Resource(resourceName).
SubResource(subresourceName...).
Name(obj.GetName()).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obj here is the subresource, but the name you want is the name of the parent resource, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems subresource name is implicitly always the same as the resource name.

@@ -66,6 +66,8 @@ type ResourceInterface interface {
DeleteCollection(deleteOptions *metav1.DeleteOptions, listOptions metav1.ListOptions) error
// Create creates the provided resource.
Create(obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
// CreateSubresource creates the provided resource.
CreateSubresource(name string, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remind me why CreateSubresource requires a name, instead of using obj.GetName()? And why we don't need an UpdateSubresource as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use obj.GetName(), as it seems subresource name is implicitly always the same as the resource name. CreateSubresource has more code change because it's adding both {name} and subresourceName into the request:

Create posts to .../resourceName
CreateSubresource posts to .../resourceName/{name}/subresourceName
compared with other operations:
Update puts to .../resourceName/{name}
UpdateSubresource puts to .../resourceName/{name}/subresourceName

We have a separate CreateSubresource instead of merging the logic into Create, because we want the change to be more backward compatible.
(FYI, subresources that support create operation are: pods/binding, pods/eviction and some deployments/rollback)

Currently there are subresources supporting create/get/patch/update operations. Probably we don't need to change code in list/delete/deletecollection/watch operations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently there are subresources supporting create/get/patch/update operations. Probably we don't need to change code in list/delete/deletecollection/watch operations.

I prefer not changing the methods that we are not supporting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify why something like CreateSubresource is not a good idea?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a separate CreateSubresource is for allowing user to input a resource {name} (resourceName/{name}/subresourceName) that is different from obj.GetName(). We think currently our system assumes every subresource object has the same name as the parent resource object. E.g. a pods/binding object having metadada.name "foo" means pod "foo" is being bound. So we can keep the dynamic client concise and restricted.

cc @lavalamp

@roycaihw
Copy link
Member Author

roycaihw commented Feb 8, 2018

@caesarxuchao As we discussed offline, our system assumes every subresource object has the same name as the parent resource object. E.g. a pods/binding object having metadada.name "foo" means pod "foo" is being bound. This PR only changes the verbs create/get/update/patch.

Copy link
Member

@caesarxuchao caesarxuchao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address the nits and then LGTM.

cc @sttts if you care about dynamic client supporting subresources.

@@ -145,6 +145,19 @@ type ResourceClient struct {
parameterCodec runtime.ParameterCodec
}

func (rc *ResourceClient) parseResourceSubresourceName() (string, []string) {
resourceName := ""
subresourceName := []string{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: change to

var  resourceName
var subresourceName []string

to avoid the memory allocation caused by []string{}.

if len(subresourceName) > 0 {
// If the provided resource is a subresource, the POST request should contain
// object name. Examples of subresources that support Create operation:
// core/v1/pods/binding
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about updating it to core/v1/pods/{name}/binding so it's clear where the name is in the url?

// core/v1/pods/eviction
// extensions/v1beta1/deployments/rollback
// apps/v1beta1/deployments/rollback
// NOTE: Currently our system assumes every subresource object has the same
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Update() function has the same assumption. Could you copy-paste this NOTE to there as well?

@roycaihw
Copy link
Member Author

/retest

@@ -166,9 +179,11 @@ func (rc *ResourceClient) Get(name string, opts metav1.GetOptions) (*unstructure
parameterEncoder = defaultParameterEncoder
}
result := new(unstructured.Unstructured)
resourceName, subresourceName := rc.parseResourceSubresourceName()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which subresources support List?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware of any based on api discovery. This change is in Get (github has some weird display problem :) ).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, yes. :)

err := rc.cl.Put().
NamespaceIfScoped(rc.ns, rc.resource.Namespaced).
Resource(rc.resource.Name).
Resource(resourceName).
SubResource(subresourceName...).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding explicit methods like UpdateStatus(), etc instead of deriving the name? It would keep it backward compatible too.

I had added UpdateStatus and modified Patch to support subresources in c961ac4#diff-781d9a2143af0fe2be85e527bf53626a.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how we do in typed clients:

// StatefulSetInterface has methods to work with StatefulSet resources.
type StatefulSetInterface interface {
Create(*apps.StatefulSet) (*apps.StatefulSet, error)
Update(*apps.StatefulSet) (*apps.StatefulSet, error)
UpdateStatus(*apps.StatefulSet) (*apps.StatefulSet, error)
Delete(name string, options *v1.DeleteOptions) error
DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
Get(name string, options v1.GetOptions) (*apps.StatefulSet, error)
List(opts v1.ListOptions) (*apps.StatefulSetList, error)
Watch(opts v1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *apps.StatefulSet, err error)
GetScale(statefulSetName string, options v1.GetOptions) (*autoscaling.Scale, error)
UpdateScale(statefulSetName string, scale *autoscaling.Scale) (*autoscaling.Scale, error)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to support Update for other subresources like certificates.k8s.io/v1beta1/certificatesigningrequests/{name}/approval and v1/namespaces/{name}/finalize.

err := rc.cl.Patch(pt).
NamespaceIfScoped(rc.ns, rc.resource.Namespaced).
Resource(rc.resource.Name).
Resource(resourceName).
SubResource(subresourceName...).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to have subresources as an argument...that's how we do in typed clients.

// Patch applies the patch and returns the patched statefulSet.
func (c *statefulSets) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *apps.StatefulSet, err error) {
result = &apps.StatefulSet{}
err = c.client.Patch(pt).
Namespace(c.ns).
Resource("statefulsets").
SubResource(subresources...).
Name(name).
Body(data).
Do().
Into(result)
return
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we should emulate typed clients behavior in dynamic client.

However, I think rc.resource.Name (which is a metav1.APIResource.Name) should allow names containing '/', e.g. pods/binding, to reflect what we are serving in api discovery:
{
"name": "pods/binding",
"singularName": "",
"namespaced": true,
"kind": "Binding",
"verbs": [
"create"
]
},

In that case, should we parse rc.resource.Name and change Resource(rc.resource.Name). into Resource(resourceName).?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roycaihw can you try what happens if user creates a clientpool, and then call clientpool.ClientForGroupVersionResource() with a subresource? Can the restmapper handle subresource?

Copy link
Member Author

@roycaihw roycaihw Feb 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the restmapper can handle subresource: 9f9ec17#diff-b9e0984a44b8dbba37804e9a762ea6cbR89. It seems to return a client for GV if Group and Version exist

return c.ClientForGroupVersionKind(schema.GroupVersionKind{Group: resource.Group, Version: resource.Version})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikhita a common use case is constructing the dynamic client with the result returned by the discovery client. In this case, it's convenient if the client.Resource(resource, namespace) allows resource to be in the form of resource/subresource. Typed clients don't have this use case so I think we can tolerate the discrepancy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@caesarxuchao
Copy link
Member

All the comments from @nikhita have been resolved. Thank you for reviewing it.

/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Feb 14, 2018
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: caesarxuchao, roycaihw

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these OWNERS Files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Feb 14, 2018
@k8s-github-robot
Copy link

/test all [submit-queue is verifying that this PR is safe to merge]

@k8s-github-robot
Copy link

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions here.

@k8s-github-robot k8s-github-robot merged commit f33e0b3 into kubernetes:master Feb 14, 2018
@k8s-ci-robot
Copy link
Contributor

@roycaihw: The following test failed, say /retest to rerun them all:

Test name Commit Details Rerun command
pull-kubernetes-e2e-gce-device-plugin-gpu e10cdb3 link /test pull-kubernetes-e2e-gce-device-plugin-gpu

Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

@roycaihw roycaihw deleted the dynamic_subresource branch February 15, 2018 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. release-note-none Denotes a PR that doesn't merit a release note. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dynamic client does not support subresources
6 participants