-
Notifications
You must be signed in to change notification settings - Fork 263
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
Improve 'source apiserver update --resource' #590
Improve 'source apiserver update --resource' #590
Conversation
0254441
to
847a337
Compare
847a337
to
8564205
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contributions @daisy-ycguo
See my comments.
/ok-to-test.
@@ -26,7 +26,7 @@ kn source apiserver create NAME --resource RESOURCE --service-account ACCOUNTNAM | |||
"Ref" sends only the reference to the resource, | |||
"Resource" send the full resource. (default "Ref") | |||
-n, --namespace string Specify the namespace to operate in. | |||
--resource strings Specification for which events to listen, in the format Kind:APIVersion:isController, e.g. Deployment:apps/v1:true. | |||
--resource stringArray Specification for which events to listen, in the format Kind:APIVersion:isController, e.g. Deployment:apps/v1:true. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example shows a /
but the format does not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deployment:apps/v1:true
is just the format of Kind:APIVersion:isController
. APIVersion usually looks like:
serving.knative.dev/v1
or- simply
v1
, only means kubernetes API V1.
I could change it to v1
if it looks confused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would keep apps/v1
as v1
without an apigroup is more the exception than the rule. Giving a full example makes more sense.
@@ -26,7 +26,7 @@ kn source apiserver update NAME --resource RESOURCE --service-account ACCOUNTNAM | |||
"Ref" sends only the reference to the resource, | |||
"Resource" send the full resource. (default "Ref") | |||
-n, --namespace string Specify the namespace to operate in. | |||
--resource strings Specification for which events to listen, in the format Kind:APIVersion:isController, e.g. Deployment:apps/v1:true. | |||
--resource stringArray Specification for which events to listen, in the format Kind:APIVersion:isController, e.g. Deployment:apps/v1:true. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as previous comment. Not /
in example but not the format
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see my previous comments
Controller: isController, | ||
} | ||
b.apiServerSource.Spec.Resources = append(resources, resourceRef) | ||
return b |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this return of b
done to stack the calls to add resource? e.g., AddResource(...).AddResource(...)
otherwise it seems weird ad Go code, as matter of style, does not have a return of the instance in a method...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I follow the same style in other eventing clients, e.g. cronjob_client and eventing_client.
I agree with you that the return is unmeaning, unless @rhuss could give an explanation which we didn't think about. If we all agree to remove the return, I will suggest to create a separate issue to track and we can change them all together.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is classical pattern used in many languages which allows for chained calls for elegant, DSL-like APIs. Do you remember the review comment where there was a check for a nil-receiver in a method (in some dependency) ? That is exactly for that use case of chained calls, where an intermediate call could return nil. So its also used in Go quite a bit (and btw is very popular in Java as well).
Some references illustrating that chaining is quite popular in Go, too (and they all return the object itself):
- https://github.com/mauricioklein/go-chainable
- https://vikash1976.wordpress.com/2017/02/03/method-chaining-in-go-lang-through-interface/
- https://riptutorial.com/go/example/13465/chaining-methods
- https://stackoverflow.com/questions/27297702/go-method-chaining-and-error-handling
- ....
BTW, you can still ignore the returned value if you like.
return b, nil | ||
} | ||
} | ||
return b, fmt.Errorf("cannot find resource %s:%s:%t to remove", version, kind, isController) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case because of the error you would not able able to stack calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the resource could be found, the function will return at the line 193 without error.
If the resource could not be found after traversing the whole structure, the function will return at line 196 with an error.
Sorry I don't get your point. Could you help to explain more of your comment ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maximilien is right, chaining works on methods which return a single return value, so returning two values makes no sense in this pattern.
But as we discussed in the other PR having mutating methods on a builder is an anti-pattern (as then the builder turns more into a regular domain objects). Maybe we can solve this use case differently, too ?
If not, we could agree that removing a non-existing resource is just a no-op and gets ignored, so that you can return directly the builder again.
Controller: isController, | ||
} | ||
b.apiServerSource.Spec.Resources = append(resources, resourceRef) | ||
return b |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit... Also b
while short is not very descriptive... perhaps builder
is better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
b is short for builder. Again, I will suggest to create a separate issue to change all client codes, including cronjob_client, eventing_client and apiserver_client.
} | ||
return &resourceList, nil | ||
return added, removed, nil | ||
} | ||
|
||
func getValidResource(resource string) (*resourceSpec, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should add comment for all code. I would rather see for public methods and functions but since you added above then should be consistent...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment.
@@ -95,9 +114,9 @@ func (f *APIServerSourceUpdateFlags) Add(cmd *cobra.Command) { | |||
`The mode the receive adapter controller runs under:, | |||
"Ref" sends only the reference to the resource, | |||
"Resource" send the full resource.`) | |||
cmd.Flags().StringSliceVar(&f.Resources, | |||
cmd.Flags().StringArrayVar(&f.Resources, | |||
"resource", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does the name resource
not match the variable f.Resources
? Should this not be "resources"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resource
is a flag. Resources
is an array of resource
.
User uses --resource abc --resource def
to set multiple resources, which will be saved in f.Resources
.
Mode: "Ref", | ||
Resources: []string{"Service:serving.knative.dev/v1alpha1:true"}, | ||
} | ||
created, _ := createFlag.getAPIServerResourceArray() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are you ignoring errors? Just assert that they are nil
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will add some tests to test errors. Thank you for pointing out.
) | ||
|
||
func TestGetAPIServerResourceArray(t *testing.T) { | ||
t.Run("get single apiserver resource", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think since there are many combination you are testing you should offer a small comment summary of the table and what combination you are testing. This would even help you if you need to add or remove tests. Or seperate them into their own t.Run
calls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simple comments are added before test cases. e.g. line 38 and line 52.
But I think maybe separating them into a single t.Run is more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
pkg/util/parsing_helper_test.go
Outdated
@@ -92,3 +92,9 @@ func TestParseMinusSuffix(t *testing.T) { | |||
assert.DeepEqual(t, expectedMap, inputMap) | |||
assert.DeepEqual(t, expectedStringToRemove, stringToRemove) | |||
} | |||
|
|||
func TestAddListAndRemovalListFromArray(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add tests for corner cases. For instance,
- add one item and remove none,
- add no items, remove one,
- add one item and remove one,
- add none and remove none.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
Is this ready? Also, looks like you have some conflicts to resolve. Thanks. |
467edd9
to
fc74cbb
Compare
} | ||
for _, k := range added { | ||
b.AddResource(k.ApiVersion, k.Kind, k.IsController) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would follow the same scheme here as we had for trigger filters in #603 , I think it covers the same use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resource is not a string map, but an array. I cannot leverage stringmap here.
I have to think about how to handle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right. Unfortunately, there are no generics in golang, that would make things much easier. Its also not easy to simulate with interfaces ;(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks ! lgtm but I would remove the 'remove' method from the builder and model it the same as in #603
fc74cbb
to
6edd8b7
Compare
} | ||
|
||
// updateExistingAPIServerResourceArray is to update an array of resources. | ||
func (f *APIServerSourceUpdateFlags) updateExistingAPIServerResourceArray(existing []v1alpha1.ApiServerResource) ([]v1alpha1.ApiServerResource, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to move this method to parsing_helper.go to provide a general method UpdateArray
to update existing array.
func UpdateArray(existing []interface{}, added []interface{}, removed []interface{}) ([]interface{}, error)
My question is which type (or key word) I should use to describe a general array here ?
I tried to use []interface{} but I get an error:
cannot use existing (type []"knative.dev/eventing/pkg/apis/sources/v1alpha1".ApiServerResource) as type []interface {} in argument to util.UpdateArray
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not easily possible, see https://golang.org/doc/faq#convert_slice_of_interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you could do is to introduce a simple utility method which converts an []ApiServerResource
to a []interface{}
and feed that into the generic method like in
fromISlice(UpdateArray(toISlice(existing), toISlice(added), toISlice(removed)))
and do the conversion between ApiServerSource and interfaces slices before and after calling the function. But I don't think its worth the hassle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, ok. Then I keep as it is.
Please review the latest version. I have removed AddResource and RemoveResource from Builder.
Yes, it's ready for a review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor comments, otherwise looks good to me.
} | ||
added = append(added, *resourceSpec) | ||
} | ||
for _, r := range removedArray { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could put the loop into a seperate function and call it twice, for adds and removals. That would reduce duplicated code a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
parts := strings.Split(resource, apiVersionSplitChar) | ||
parts := strings.SplitN(resource, apiVersionSplitChar, 3) | ||
if len(parts[0]) == 0 { | ||
return nil, fmt.Errorf("cannot find KIND in resource specification %s", resource) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil, fmt.Errorf("cannot find KIND in resource specification %s", resource) | |
return nil, fmt.Errorf("cannot find 'Kind' part in resource specification %s (expected: <Kind:ApiVersion[:controllerFlag]>", resource) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
if len(parts) >= 2 && len(parts[1]) > 0 { | ||
version = parts[1] | ||
} else { | ||
version = "v1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want a default to "v1" ? I would prefer to be always explicit with the ApiVersion and make it mandatory (as you have to do it for any k8s resource, too, when deploying to the server)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just want to make a simple format for ApiServerSource, e.g. Event or Pod or Deployment can be described as --resource Event --resource Pod --resource Deployment
which look more artistic, other than --resource Event:v1 --resource Pod:v1 --resource Deployment:v1
So if no ApiVersion provides, I use v1 instead. If user want to use default version but set a controller flag, it could be describe as Event::true
.
If you don't agree with this kind of contraction rule, I could delete this feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point is that this is a bit surprising for Kubernetes users, as the apiVersion is a mandatory field in a resource descriptors. Also, imagine now that the Event or Pod spec evolves to a v2 version. Would you then also update the default to get to this newest version ? Will a user still using v1 and, more important, is she aware of that not the latest event version is used ? Maybe even v1 is getting removed later on then, making the default useless.
I understand that you want to make it as easy and concise as possible. However, its one of key principle of Kubernetes (and hence Knative, too) that to identify a resource you alway need apiVersion + kind.
So I really would be explicit here to (a) be on par with the Kubernetes spec and (b) to avoid future surprises (and (c) to avoid 'hacks' like '::' to get to the third option).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, Deployment
is in apps/v1
, not v1
. There are only a handful of resources that are really in v1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh. Got your point. Let me remove the default value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. The latest version is submitted and ready for review. @rhuss
} | ||
version := parts[1] | ||
if len(parts) >= 3 { | ||
if len(parts) >= 3 && len(parts[2]) > 0 { | ||
isController, err = strconv.ParseBool(parts[2]) | ||
if err != nil { | ||
return nil, fmt.Errorf("cannot parse controller flage in resource specification %s", resource) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil, fmt.Errorf("cannot parse controller flage in resource specification %s", resource) | |
return nil, fmt.Errorf("controller flag is not a boolean in resource specification %s (expected: <Kind:ApiVersion[:controllerFlag]>)", resource) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
cf02e79
to
b812464
Compare
/test pull-knative-client-integration-tests |
@maximilien @rhuss Are we ready to merge this ? I'm on vacation. I could still address the comments if you have. Thank you. |
b812464
to
1276fbf
Compare
The following is the coverage report on the affected files.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, lgtm !
/lgtm
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: daisy-ycguo, maximilien, rhuss 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 files:
Approvers can indicate their approval by writing |
* Update kn-plugin-source-kafka to v0.19.0 * Enable Kafka for E2E tests * Update vendor dir
Fixes #565
Proposed Changes
source apiserver update --resource
to follow the common update semantics-