-
Notifications
You must be signed in to change notification settings - Fork 167
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
Allow authorization with Google application default credentials #213
Conversation
@moolitayer can you review this? Especially the |
Hmm.. the travis failure is because it's running on a system that does not have any default application credentials configured. So I need to figure out how to test around that. I didn't see any stubbing in the existing tests except with |
lib/kubeclient/common.rb
Outdated
@@ -70,6 +72,9 @@ def initialize_client( | |||
elsif auth_options[:bearer_token_file] | |||
validate_bearer_token_file | |||
bearer_token(File.read(@auth_options[:bearer_token_file])) | |||
elsif auth_options[:default_credentials] |
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.
Please initialize this variable (line 23) with 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.
You will also need to add it to the input validation in line 442
README.md
Outdated
|
||
```ruby | ||
auth_options = { | ||
default_credentials: 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.
I would name this use_default since its boolean. It seems more consistent with the other auth keys that all contain string values. Also maybe use_default_gce or use_gce is better since it's currently only gce?
Naming is hard 🐏
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.
Naming is hard. And often having more people looking at it helps. :)
How about use_default_gcp
because this applies to GKE as well.
lib/kubeclient/common.rb
Outdated
|
||
def default_credentials_token | ||
scopes = ['https://www.googleapis.com/auth/cloud-platform'] | ||
authorization = Google::Auth.get_application_default(scopes) |
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.
Google::Auth.get_application_default(scopes).apply({}).access_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.
#access_token
is a method on the object returned by #get_application_default
, not on the #apply
method result (it's not chainable). I could use #tap
if you don't want the interim local variable but I tend to find that harder to read.
test/test_kubeclient.rb
Outdated
.to_return(body: open_test_file('pod_list.json'), | ||
status: 200) | ||
|
||
client = Kubeclient::Client.new 'http://localhost:8080/api/', |
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.
Parenthesis around parameter list please
test/test_kubeclient.rb
Outdated
default_credentials: true | ||
} | ||
end | ||
assert_equal expected_msg, exception.message |
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.
Parenthesis around parameter list please
test/test_kubeclient.rb
Outdated
Kubeclient::Client.new 'http://localhost:8080', | ||
auth_options: { | ||
default_credentials: 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.
Parenthesis around parameter list please
I think for consistency we would need to catch Signet::AuthorizationError and throw a KubeException. This seems like a good direction. Thanks for the patch @jeremywadsack 👏 |
@simon3z about the dependency honestly I hope it won't break anything like the dependency problems we have been having lately. Maybe if we encounter problems in the future we can define such dependencies as optional to mitigate the problem. |
@moolitayer I made most of the changes you requested (except can't inline Also:
I also thought about making the dependency optional, especially for those not on Google platforms. Let me know if you want to to take a stab at that. I can squash and rebase if you want once you are done reviewing. |
test/test_kubeclient.rb
Outdated
auth = Minitest::Mock.new | ||
auth.expect(:apply, nil, [{}]) | ||
auth.expect(:access_token, 'valid_token') | ||
Google::Auth.stub(:get_application_default, auth) 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.
👍
That's good but it makes this change harder to review. If you will submit that in a separate pr we can get it merged fast. You can wait for that or you can use the old style here for consistency.
Cool, don't invest time in that just yet. Go ahead and squash your changes |
test/test_kubeclient.rb
Outdated
@@ -389,16 +400,18 @@ def test_api_bearer_token_success | |||
def test_api_bearer_token_failure | |||
error_message = '"/api/v1" is forbidden because ' \ | |||
'system:anonymous cannot list on pods in' | |||
response = OpenStruct.new(code: 401, message: error_message) | |||
response = RestClientResponseStub.new('', 403) |
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.
Can you explain the change in expectation?
Also is there a functional need in using a class extending string instead of OpenStruct.?
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 previous code was raising a KubeException
in the request, which meant that #handle_exception
was just passing through the exception. So all the expectations in this test were just testing the OpenStruct
, not testing the code within #handle_exception
that retains the status code and response body.
Instead of raising a KubeException
this raises the RestClient::Exception
which is what #handle_exception
is rescuing and parsing. The response is required by RestClient::Exception
. The stub extends String
because the RestClient::Response
object extends String
and is expected to behave the same way (that is, the response is the body).
@moolitayer I was on holiday for a week and just now catching up. I'll try to push the style changes as a separate PR today. Rolling back is harder. Once that's merged I can squash and rebase this on top of that. |
LGTM +1 once squashed @simon3z please review. Also please comment if you would like us to explore the path of optional dependencies. I didn't find any built in solution in gemspec but ad hock ones. I saw Keenan comment on those issues so he might be able to help us down that path. Some stats: without googleauth kubeclinet depends on 29 gems. Adding it takes us up to 39. Those dependencies are (I'm using 2.3):
|
One solution for optional dependency is to add a note in the Readme that
says, "to use the default Google account with GCP include googleauth gem in
your Gemfile as well." And then we just require with a rescue.
On Sun, Dec 4, 2016 at 4:22 AM Mooli Tayer ***@***.***> wrote:
LGTM +1 once squashed
@simon3z <https://github.com/simon3z> please review. Also please comment
if you would like us to explore the path of optional dependencies. I didn't
find any built in solution in gemspec but ad hock ones. I saw Keenan
comment on those issues so he might be able to help us down that path.
Some stats: without googleauth kubeclinet depends on 29 gems. Adding it
takes us up to 39. Those dependencies are (I'm using 2.3):
faraday-0.10.0
googleauth-0.5.1
jwt-1.5.6
little-plugger-1.1.4
logging-2.1.0
memoist-0.15.0
multi_json-1.12.1
multipart-post-2.0.0
os-0.9.6
signet-0.7.3
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#213 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAOurIq-s1y_xr6jjbUWCl4sAKwO_PNiks5rErB5gaJpZM4K2BcX>
.
--
Jeremy Wadsack
|
lib/kubeclient/common.rb
Outdated
{} | ||
end | ||
err_message = json_error_msg['message'] || e.message | ||
err_message = json_error_message(e.response) || e.message |
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.
@jeremywadsack is this change related? Why is it needed?
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.
@simon3z Because I used the same code for parsing the error message from the body of a Signet::AuthorizationError
, so I encapsulated the code 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.
👍
@jeremywadsack I am wondering... isn't this something that could wrap |
@simon3z I could certainly create a gem that adds this. Let me know which way you prefer to go. |
1a840ef
to
4f5f5ed
Compare
@simon3z @moolitayer Rebased and squashed. |
What's the status of these changes? Is there a chance they will get merged in the near future? |
@jeremywadsack Do you have time to work on this? Or should someone else take over? |
That's very disappointing, it would be useful to support GCE/GKE. Thanks for the update though. |
That's totally possible. If you'd like I'll whip up a patch merging my config wrapper with the main one and conditionally guarding the behaviour around a gem presence check. |
@xldenis Thank you for picking this up. I would also love to have this supported natively in the gem for the same reason: GKE is a major platform for Kubernetes. As I posted before we can just add a README entry that says that add the googleauth gem for GCE/GKE support. @bglusman's suggestion of checking for |
@xldenis sounds good to me. And I'm OK with the dependency in dev. |
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.
Hi, I'd like to somehow move this along.
I'm again questioning whether obtaining the token belongs in kubeclient :-)
Please don't read this as objection — I see there was consensus on "optional dependency" approach, and I'm good with that!
I'm just trying to understand whether it's the most convenient thing for GCP users, or maybe obtaining the token yourself and passing it to Client gives much more flexibility... (I don't know anything about GCP)
P.S. thanks everyone for #210, #211 and Shopify/krane#88, the multiple implementations are very helpful for understand the problem space 🙇♂️
@@ -509,22 +524,22 @@ def test_init_username_and_bearer_token | |||
assert_equal(expected_msg, exception.message) | |||
end | |||
|
|||
def test_init_user_and_bearer_token | |||
def test_init_username_and_bearer_token_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.
At first I wanted to ask why we need both bearer_token and bearer_token_file but then noticed it's already supported and documented, thanks for adding tests! 👏
lib/kubeclient/common.rb
Outdated
@@ -70,6 +73,9 @@ def initialize_client( | |||
elsif auth_options[:bearer_token_file] | |||
validate_bearer_token_file | |||
bearer_token(File.read(@auth_options[:bearer_token_file])) | |||
elsif auth_options[:use_default_gcp] |
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 wonder if this should be an option for the Client.
It's resolved to a token on initialization, and after that the Client doesn't care.
Could it be responsibility of Kubeclient::Config instead? (like in Shopify/krane#88)
Well, it's also useful for people who don't use kubeconfig.
But maybe expose a class method gcp_auth_options
or something like that, also used by Kubeclient::Config?
- README recommends calling
config.context
several times in Client instantiation, and it's silly if that will result in obtaining many tokens :-(- Seems solvable by moving it from
fetch_user_auth_options
now called duringconfig.context
to only the callcontext.auth_options
.
- Seems solvable by moving it from
- It should be possible to use a single Config instance for many Client instantiations, obtaining a new token for each one.
- Conversely it should be possible to reuse one token for many Client instances (e.g. it's currently common to create several instances for each API group/version you use).
lib/kubeclient/common.rb
Outdated
|
||
authorization.apply({}) | ||
|
||
authorization.access_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.
This is not a lot of code, so I'm wondering if kubeclient needs to include it at all.
- Does this cover most uses with GCP, or will people need other variations?
- Add google cloud configuration object for kubeclient Shopify/krane#88 says it's valid for 1 hour. Is that tunable? Should a long-lived Client object be able to renew it?
The alternative I'm thinking about is:
- Document example code in README.
- Those directly passing
auth_options:
just write this or similar code and pass the token. - For those reading a config file, Kubeclient::Config could have a hook to plug your own handling of 'auth-provider'. E.g.
auth-provider: gcp
would callfetch_auth_provider_options_gcp
method one could override in a subclass.
See also my other comment about one Config / many context
calls / many Client instances / one or many tokens. Perhaps for gcp you better take matters into your own hands and not call config.context.auth_options
at all? (for that, Config should let user access the raw auth-provider
portion of 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.
I'm sure there are other cases with GCP but I can think of the two:
-
User has authorized
gcloud
for their GCP project (and by extensionkubectl
). In that case, the token is only good for an hour as I pointed out in Authentication from "gcp" provider #210. I tried to address this in Enable "gcp" authentication structure for bearer token in .kube/config #211 but I couldn't get it to work for more than an hour and I'm not sure if we can. -
This is on a GCP server (GKE, GCE, App Engine, Cloud Functions), where Google has installed "default credentials". That's what this PR addresses. (You can always install default credentials on your local dev machine as "long-running" credentials.)
I see your point that this could just be documented. It would be really nice if kubeclient
just worked out-of-the-box for gcloud
-configured machines (e.g. development machine) and GCE/GKE machines. I can see that sometimes users are going to want to modify the scope and may eventually want to use specific keys for role-based-access (rather than authorizing the default credentials on a machine for this permission). So maybe there's a place for basic "out-of-the-box" support and anything beyond that is documented.
It does make more sense to use Config
than Client
. However, the shopify solution depends on the presence of auth-provider.name
in the configuration file but there is no such file in a GCP server. So it wouldn't solve for that case.
lib/kubeclient/common.rb
Outdated
authorization.apply({}) | ||
|
||
authorization.access_token | ||
rescue Signet::AuthorizationError => e |
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 wrapping helping? It was suggested "for consistency" but perhaps it's nicer to be able to rescue original Signet exceptions rather than a kubeclient exception?
Plus, we rebranded our main exception as Kubeclient::HttpError
this makes less sense.
A lot of my questions above are around Config vs tokens vs Client lifespans, which are not one-size-fits-all because tokens expire. |
However, the shopify solution depends on the presence of auth-provider.name in the configuration file but there is no such file in a GCP server. So it wouldn't solve for that case.
OK, so how do you detect/activate GCP support for use case 2?
Or do you mean you manually specify `use_default_gcp: true`?
|
@cben I think either have a configuration option that tells the config/client to look for the default Google authorization (and require From the comments here, my guess is this team is partial to an explicit selection. |
Any idea if this can be merged soon? Anyone still actively working on it? Anyone need help with anything? |
@moolitayer what do you think? You've LGTM'd this in the past. |
@cben It's been so long since I looked at this that it's hard to recall where my head was at the time. As it happens, I'm just looking into this again (or into the code that we built that depends on this) and so it's a good time to rethink. In production in our GKE cluster, we're using this:
That's basically what's described in the README for "using kubeclient inside a Kubernetes cluster" (with the addition of a well-known location for the API host and CA file). Given that it's documented, I don't know that we need to do anything to "make this work" in GKE. Although adding additional details for GKE might be helpful. (Incidentally, it's documented in Kubernetes as the recommended way to access the API server.) I have no experience running Here's a proposal, let me know what you think:
Does that sound good? *Using
|
@jeremywadsack Sorry for delay, I was hoping @moolitayer would respond too (nag nag 😉)... |
4f5f5ed
to
4c672a4
Compare
@cben @moolitayer: I've rewritten this and I think it should make more sense and be simpler. First, note that the "application default credentials" aren't related to having I think this is a much simpler and more useful approach. I removed the dependency on Let me know if your thoughts and any changes you'd like. If this looks good, I can squash commits. |
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! few last minor questions.
@@ -0,0 +1,17 @@ | |||
# frozen_string_literal: true | |||
|
|||
require 'googleauth' |
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.
With this file required in kubeclient.rb
, I think this means you can't require kubeclient without having googleauth gem. Can you move this into token
method so it's only done if you use 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.
Good catch. Fixed in 774177b.
|
||
module Kubeclient | ||
# Get a bearer token from the Google's application default credentials. | ||
class ApplicationDefaultCredentials |
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 name sounds pretty generic, perhaps should somehow include "Google".
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 24145e9. I thought that the name was already pretty long... ;) But I agree.
`kubeclient` dependencies so you should add it to your bundle. | ||
|
||
```ruby | ||
require 'googleauth' |
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 not strictly neccessary here, just to emphasize the dependency on the gem, right?
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.
Originally I think I was thinking of not having a require
in the library code at all, in which case this would be necessary. As it is now it's not necessary, but it follows the example below this for Celluloid::IO
.
However, when the googleauth
gem is not installed, and you try to use get a token, it raises a load error:
2.3.1 :002 > Kubeclient::ApplicationDefaultCredentials.token
LoadError: cannot load such file -- googleauth
I think this is obscure (even though it's documented in the README). What do you think about adding a message here that tells what to do. One of these options:
- Do not include the
require
(because presumably it's already been done by bundler) and rescueNameError
aroundGoogle::Auth.get_application_default
and provide a message. - Retain the
require
(because it is a dependency) and rescue theLoadError
around therequire
and provide a message.
Message would be something like this: "GoogleApplicationDefaultCredentials requires the googleauth
gem. Please make sure this is included in your bundle."
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'd rather not assume bundler does require
. Some people practice require: false
in Gemfile. And I suppose some people [citation needed ;-] use gem install
outside bundler.
=> if we don't require in the code, keep this require in README.
Personally I'd do require
on demand inside the token
method. Because it is a run time dependency of this code, only not a bundler dependency. So user is responsible to install it, but if installed we can use it.
But I don't care much, your call.
IMHO LoadError is clear & actionable enough, tells you what gem you need.
(But an uncatched NameError would be too obscure to my taste.)
If you want a rescue for a friendlier message, that's OK too...
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.
Sounds good. I'll leave it as is. It accomplishes the job and I don't want to add more code.
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.
LGTM 👍
Just to be sure, I confirmed manually that kubeclient works with this PR without googleauth in the bundle. (Not checked by Travis, due to googleauth dev dependency. I think that's OK.)
@moolitayer can we merge?
lib/kubeclient.rb
Outdated
@@ -1,6 +1,7 @@ | |||
require 'json' | |||
require 'rest-client' | |||
|
|||
require 'kubeclient/google_application_default_credentials' |
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: please keep this list sorted
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 meant to do that. Then I renamed the file. I've fixed that and will squash and rebase (to address merge conflicts in README).
Nice and clean, thanks! |
This adds support for reading the application default credentials installed on a system by Google. For example, on a development system where gcloud is configured or on a GCP instance.
4737898
to
e6e10d0
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.
rebase looks good, merging.
Thanks for the perseverance! 🍰
This adds support for reading the application default
credentials installed on a system by Google. For example, on a
development system where gcloud is configured or on a GCP
instance.
I think this is a better solution to #210.