-
Notifications
You must be signed in to change notification settings - Fork 702
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
Utilize user credentials to invoke the new kubeapps-api #2908
Conversation
Still a hardcoded config, pending using the existing config file
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.
WIP, there is a couple of things I wanna re-check and thoughts:
* As it is a sort of transversal functionality, we should provide a unified auth experience for plugin developers. Can plugins invoke main program functions?
No - I don't think they should be able to do so, but open to suggestions.
Another way round: can the main program share info with the plugins?
Yes, we can definitely do this. For example, we could include configuration data in the signature of the RegisterWithGRPCServer
function, or we could define a set of env vars that are available. Or, it may be useful (as I think you suggested) to even pass a function or something to the plugins when registering that returns a k8s config (so each plugin doesn't have to do this, or care about pinniped etc.)
Currently, I just came with using envars to share the config so that it can be accessed by the plugins.
One thing: I didn't expect this initial PR to require cluster config or anything to do with pinniped etc. Is that just because your local dev server is setup with pinniped? I'd recommend first just using a normal non-pinniped setup and verifying the way to ensure plugins have a valid k8s config. Then we can update to support pinniped etc. But up to you.
Glad the crux of it is straight-forward (extracting the token from the context, which is translated from the http header).
func extractToken(ctx context.Context) (string, error) { | ||
md, ok := metadata.FromIncomingContext(ctx) | ||
if !ok { | ||
return "", status.Errorf(codes.Unauthenticated, "no authorization metadata found") |
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.
We may have some anonymous endpoionts? Why not defer to the endpoint (which will fail if RBAC doesn't allow it).
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.
Ooops, that's true. I've updated that function accordingly.
However, I still have a doubt here: do anonymous endpoints (eg getConfiguredPlugins
) still require the k8s authentication? In this PR I just added that logic in those which are not anonymous.
I am not 100% sure about this, but I think I am hitting some RBAC-related issues, possibly related to this PR |
Correct, yes that's the bit that Antonio is working on in this branch, but it won't be affecting your experience yet (nor when it lands, I hope :) ).
My first thought was that our development RBAC may not have you covered, but it seems to (since all the flux CRDs appear to be for My next guess is that it's a namespace-scoped object (a HelmChart)? So perhaps you need to do
Currently for the ease of development, all calls are using the in-cluster config which is the service account linked to the above RBAC. Once Antonio finishes, we'll additionally have the option of switching off the development-only RBAC so the user (JWT) identity is used, which I guess we could parse and use the username. The
Right, that might be more evidence for my hypothesis above, given the |
Yep, that was exactly it, I needed to set the namespace, as I was dealing with Namespaced-scoped object. Thanks, Michael!. Your help is always spot on and very much appreciated. Still would like to see an example of piece of go code that logs the user identity on behalf of which the code is executed. I know how to do this in Java (using thread-locals) in spring or EE container but no clue in go |
It holds the cluster info so that other components can read it. 6f6e39
5c4e05b
to
466478c
Compare
Conflicts: cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/main.go cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go cmd/kubeapps-apis/plugins/kapp_controller/packages/v1alpha1/main.go cmd/kubeapps-apis/server/plugins.go
I've tried to add as many unit tests as possible, but some functions are really coupled to a k8s cluster, so, even if I tried to mock some objects, the coverage is far from being ideal. fwiw, I've tested it locally with a single cluster using pinniped and csp, so... let's mark it as ready for review (finally... 😁 ) |
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.
Yep, I got it. After refactoring a little bit for writing the tests, I just improved the b) option, creating two functions w/ and w/o the required params (still don't know why golang is against overloading functions...)
The (b) option is using a global var I think, see inline comments about using function closures to avoid the globals. Regarding the two functions w/ and w/o the required params, I didn't see them? Perhaps you figured that out? (as I see you're using the raw signature now rather than a type alias defined in a package).
@@ -232,3 +253,140 @@ func listSOFiles(fsys fs.FS, pluginDirs []string) ([]string, error) { | |||
} | |||
return matches, nil | |||
} | |||
|
|||
// parseClusterConfig returns a kube.ClustersConfig struct after parsing the raw `clusters` object provided by the user | |||
// TODO(agamez): this fn is the same as in kubeapps/cmd/kubeops/main.go, export it and use it instead (unit test available at: cmd/kubeops/main_test.go) |
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.
+1, let's move it into pkg/kube and re-use from there (guessing that's what you intended, just being explicit).
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.
But let's do it in another PR, no? I already have it planned once we got this PR landed
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.
Yes, I didn't mean it needs to be moved here.
cmd/kubeapps-apis/server/plugins.go
Outdated
|
||
// dynClientGetterForContext returns a k8s client for use during interactions with the cluster. | ||
// It is invoked by dynClientGetterForContext and unit tests passing the appropriate configuration | ||
func dynClientGetterForContextWithConfig(ctx context.Context, inClusterConfig *rest.Config, serveOpts ServeOptions, config kube.ClustersConfig) (dynamic.Interface, 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.
Nice, so this function is not only testable, but doesn't require any global vars... great, as this enables us to avoid using global vars altogether...
cmd/kubeapps-apis/server/plugins.go
Outdated
return fmt.Errorf("unable to use %q in plugin %v due to mismatched signature.\nwant: %T\ngot: %T", grpcRegisterFunction, pluginDetail, dummyFn, grpcRegFn) | ||
} | ||
server := grpcFn(registrar) | ||
|
||
server := grpcFn(registrar, dynClientGetterForContext) |
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.
So I think ^^ is the reason that you've needed global variables: you're passing in a static function to each plugin and so that statically defined function needs access to the server options, without them being passed in (due to the function signature)?
The solution is to not use a static function, but instead, we can define a function closure (a function which captures state from the scope where it is defined), probably not here in registerGRPC
as then we'd be doing it for every plugin, but above in registerPlugins
where we can create it once for all plugins:
var dynClientGetterForContext := func(ctx context.Context) (dynamic.Interfoce, error) {
...
}
You should only need to ensure that the server options are passed down to registerPlugins
. Then you can pass the closure to each plugin.
Or if you don't want to define the function closure within the registerPlugins
, you can instead define a function that takes the server options as an arg and returns the function closure, such as:
func createClientGetter(serveOpts ServeOptions) func(context.Context) (dynamic.Client, error) {
// parse the cluster config etc.
return func(ctx context.Context) (dynamic.Client, error) {
// In here you can refer to both serveOpts and the parsed cluster config
...
}
}
You can then call this function once in registerPlugins
to create the dynamic client getter, then pass that down to each registerGRPC
call.
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've tried to add as many unit tests as possible, but some functions are really coupled to a k8s cluster, so, even if I tried to mock some objects, the coverage is far from being ideal.
Yep, you've got the main thing tested (the extraction of the token from context) - great. Testing the function for the dynamic client is tricky, what you've done is fine for now. I've left an idea for a future improvement that we can try at some point.
fwiw, I've tested it locally with a single cluster using pinniped and csp, so... let's mark it as ready for review (finally... grin )
Great. I'll hold off approving until you've had a chance to look at the function closure to get rid of the global vars.
contextKey string | ||
contextValue string | ||
expectedToken string | ||
expectedErr string |
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 may also be able to use the errors
package to instead define an error type here (ie. nil, for no error, or an actual error if it is, then use errors.Is. But fine as is.
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've changed it to fmt.Errorf(...)
and status.Errorf
(passing the expected codes.Unauthenticated
instead of relying on the string msg). Thanks!
expectedErr string | ||
}{ | ||
{ | ||
name: "Good token", |
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.
So this function is an easy one to test - great. I try to make the name of the test follow the test spec, like "it returns the expected token without error for a valid context value", but that's just a personal preference.
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.
Yup, totally right. I also like having meaningfulñ names for the tests, but I was too focused on writing them that I forgot to edit them before pushing. Thanks for the heads up!
} | ||
dynamicInterface, err := dynClientGetterForContextWithConfig(context, inClusterConfig, serveOpts, config) | ||
|
||
if tc.shouldCreate != (dynamicInterface != 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.
So right, this one is harder to test. Worth asking what is the purpose of the test? I assume we want to ensure that the returned client is valid when it should be, but it's tricky to do so, since the returned result doesn't have much to check in the way of public fields.
One thing that may be possible (not recommending you do this, just a thought) would be to use the http_test
package to create a fake http server and use it's address as the endpoint you expect (such as the pinnipedProxyURL). It's possible then that you could actually use the client to request something (anything) and verify that the token was sent with the request to the expected IP. But again, no need to do this, just thinking out aloud. I'd be happy to give it a go as a follow-up.
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.
Exactly, I initally was trying to -use- the dynamic interface, but it was more tricky than expected, so I just checked if the creation was being performed.
I've added a //TODO there, but I'd rather leave the current test. At least we are checking the closure fn is being called with the proper params so that it doesn't throw any error.
Co-authored-by: Michael Nelson <absoludity@gmail.com>
Conflicts: chart/kubeapps/Chart.yaml
I don't know why, but the e2e tests are not passing because of a CI issue 😭 . Why it's happening here? Dunno, the changes herein are totally unrelated... I've sent a fix in #2950 |
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.
Great work Antonio! Small note inline about hard-coding the "default" cluster, but otherwise +1
cmd/kubeapps-apis/server/plugins.go
Outdated
|
||
var client dynamic.Interface | ||
if !serveOpts.UnsafeUseDemoSA { | ||
restConfig, err := kube.NewClusterConfig(inClusterConfig, token, "default", config) |
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.
Guessing you didn't mean to hard-code the "default" cluster here?
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.
Oops, leftover I didn't notice! It should retrieve the cluster from ClustersConfig.KubeappsClusterName
(since we are not using cluster-scoped endpoints yet, afaik). Changing it straight away. Thanks!
Conflicts: cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go
Well, it was not the CI who blame 100%, but a minor kind-of syntax bug (always forget Helm doesn't have lazy eval...). The issue is that it should have been caught by the Thanks again for your valuable suggestions and ideas! |
Description of the change
This PR tries to replace the current hardcoded SA with the actual user's credentials, including the proper configuration for the Pinniped scenario.
Currently, it also adds a new chart flag for dev purposes (
kubeappsapis.UnsafeUseDemoSA: true
) that allow having the same behavior prior to this PR.Benefits
A consistent auth approach with the rest of the Kubeapps components.
Possible drawbacks
No SA for quick testing, but it will be get solved with a flag soon.Applicable issues
Additional information
WIP, there is a couple of things I wanna re-check and thoughts:
Currently, I just came with using envars to share the config so that it can be accessed by the plugins.
For subsequent PRs
pkg/..
to avoid duplicated code: Extract ParseClusterConfig fn to /pkg #2958