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

Generate GCP tokens through cmd-path config #410

Merged
merged 1 commit into from
May 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions kubeclient.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'googleauth', '~> 0.5.1'
spec.add_development_dependency('mocha', '~> 1.5')
spec.add_development_dependency 'openid_connect', '~> 1.1'
spec.add_development_dependency 'jsonpath', '~> 1.0'

spec.add_dependency 'rest-client', '~> 2.0'
spec.add_dependency 'recursive-open-struct', '~> 1.0', '>= 1.0.4'
Expand Down
2 changes: 1 addition & 1 deletion lib/kubeclient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
require 'kubeclient/common'
require 'kubeclient/config'
require 'kubeclient/entity_list'
require 'kubeclient/google_application_default_credentials'
require 'kubeclient/exec_credentials'
require 'kubeclient/gcp_auth_provider'
require 'kubeclient/http_error'
require 'kubeclient/missing_kind_compatibility'
require 'kubeclient/oidc_auth_provider'
Expand Down
28 changes: 19 additions & 9 deletions lib/kubeclient/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,23 +150,33 @@ def fetch_user_auth_options(user)
if user.key?('token')
options[:bearer_token] = user['token']
elsif user.key?('exec')
exec_opts = user['exec'].dup
exec_opts['command'] = ext_command_path(exec_opts['command']) if exec_opts['command']
exec_opts = expand_command_option(user['exec'], 'command')
options[:bearer_token] = Kubeclient::ExecCredentials.token(exec_opts)
elsif user.key?('auth-provider')
auth_provider = user['auth-provider']
options[:bearer_token] = case auth_provider['name']
when 'gcp'
then Kubeclient::GoogleApplicationDefaultCredentials.token
when 'oidc'
then Kubeclient::OIDCAuthProvider.token(auth_provider['config'])
end
options[:bearer_token] = fetch_token_from_provider(user['auth-provider'])
else
%w[username password].each do |attr|
options[attr.to_sym] = user[attr] if user.key?(attr)
end
end
options
end

def fetch_token_from_provider(auth_provider)
case auth_provider['name']
when 'gcp'
config = expand_command_option(auth_provider['config'], 'cmd-path')
Kubeclient::GCPAuthProvider.token(config)
when 'oidc'
Kubeclient::OIDCAuthProvider.token(auth_provider['config'])
end
end

def expand_command_option(config, key)
config = config.dup
config[key] = ext_command_path(config[key]) if config[key]

config
end
end
end
19 changes: 19 additions & 0 deletions lib/kubeclient/gcp_auth_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require 'kubeclient/google_application_default_credentials'
require 'kubeclient/gcp_command_credentials'

module Kubeclient
# Handle different ways to get a bearer token for Google Cloud Platform.
class GCPAuthProvider
class << self
def token(config)
if config.key?('cmd-path')
Kubeclient::GCPCommandCredentials.token(config)
else
Kubeclient::GoogleApplicationDefaultCredentials.token
Copy link
Contributor

Choose a reason for hiding this comment

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

In the interests of hardening, would you be opposed to adding retries here? We have begun observing intermittent Signet::RemoteServerError exceptions that, according to Google, we can/should retry on.

I don't think pushing up retryable errors to the caller is the right thing to do; we should handle them here and only throw once we've exhausted our retry limit.

Full error we observe:

"error": {
  "code": 503,
  "message": "The service is currently unavailable.",
  "status": "UNAVAILABLE"
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@timothysmith0609 sounds like something that the googleauth gem should be dealing with, not kubeclient itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

You're absolutely right. It turns out googleauth does retry, but only when using the GCE node service account (no idea why that's the case). It looks like it should be possible to wrap the other case in their retry routine so I'll try to solve this upstream.

end
end
end
end
end
31 changes: 31 additions & 0 deletions lib/kubeclient/gcp_command_credentials.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Kubeclient
# Generates a bearer token for Google Cloud Platform.
class GCPCommandCredentials
class << self
def token(config)
require 'open3'
require 'shellwords'
require 'json'
require 'jsonpath'

cmd = config['cmd-path']
args = config['cmd-args']
token_key = config['token-key']

out, err, st = Open3.capture3(cmd, *args.split(' '))

raise "exec command failed: #{err}" unless st.success?

extract_token(out, token_key)
end

private

def extract_token(output, token_key)
JsonPath.on(output, token_key.gsub(/^{|}$/, '')).first
end
end
end
end
26 changes: 26 additions & 0 deletions test/config/gcpcmdauth.kubeconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: v1
clusters:
- cluster:
server: https://localhost:8443
insecure-skip-tls-verify: true
name: localhost:8443
contexts:
- context:
cluster: localhost:8443
namespace: default
user: application-default-credentials
name: localhost/application-default-credentials
kind: Config
preferences: {}
users:
- name: application-default-credentials
user:
auth-provider:
config:
access-token: <fake_token>
cmd-args: config config-helper --format=json
cmd-path: /path/to/gcloud
expiry: 2019-04-09T19:26:18Z
expiry-key: '{.credential.token_expiry}'
token-key: '{.credential.access_token}'
name: gcp
14 changes: 14 additions & 0 deletions test/test_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ def test_gcp_default_auth_renew
assert_equal({ bearer_token: 'token1' }, context.auth_options)
end

def test_gcp_command_auth
Kubeclient::GCPCommandCredentials.expects(:token)
.with('access-token' => '<fake_token>',
'cmd-args' => 'config config-helper --format=json',
'cmd-path' => '/path/to/gcloud',
'expiry' => '2019-04-09 19:26:18 UTC',
'expiry-key' => '{.credential.token_expiry}',
'token-key' => '{.credential.access_token}')
.returns('token1')
.once
config = Kubeclient::Config.read(config_file('gcpcmdauth.kubeconfig'))
config.context(config.contexts.first)
end

def test_oidc_auth_provider
Kubeclient::OIDCAuthProvider.expects(:token)
.with('client-id' => 'fake-client-id',
Expand Down
27 changes: 27 additions & 0 deletions test/test_gcp_command_credentials.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require_relative 'test_helper'
require 'open3'

# Unit tests for the GCPCommandCredentials token provider
class GCPCommandCredentialsTest < MiniTest::Test
def test_token
opts = { 'cmd-args' => 'config config-helper --format=json',
'cmd-path' => '/path/to/gcloud',
'expiry-key' => '{.credential.token_expiry}',
'token-key' => '{.credential.access_token}' }

creds = JSON.dump(
'credential' => {
'access_token' => '9A3A941836F2458175BE18AA1971EBBF47949B07',
'token_expiry' => '2019-04-12T15:02:51Z'
}
)

st = Minitest::Mock.new
st.expect(:success?, true)

Open3.stub(:capture3, [creds, nil, st]) do
assert_equal('9A3A941836F2458175BE18AA1971EBBF47949B07',
Kubeclient::GCPCommandCredentials.token(opts))
end
end
end