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

Adds support for exec credential plugin #363

Merged
merged 2 commits into from
Dec 9, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/kubeclient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'kubeclient/config'
require 'kubeclient/entity_list'
require 'kubeclient/google_application_default_credentials'
require 'kubeclient/exec_credentials'
require 'kubeclient/http_error'
require 'kubeclient/missing_kind_compatibility'
require 'kubeclient/resource'
Expand Down
2 changes: 2 additions & 0 deletions lib/kubeclient/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ def fetch_user_auth_options(user)
options = {}
if user.key?('token')
options[:bearer_token] = user['token']
elsif user.key?('exec')
options[:bearer_token] = Kubeclient::ExecCredentials.token(user['exec'])
else
%w[username password].each do |attr|
options[attr.to_sym] = user[attr] if user.key?(attr)
Expand Down
60 changes: 60 additions & 0 deletions lib/kubeclient/exec_credentials.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module Kubeclient
# An exec-based client auth provide
# https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration
# Inspired by https://github.com/kubernetes/client-go/blob/master/plugin/pkg/client/auth/exec/exec.go
class ExecCredentials
class << self
def token(opts)
require 'open3'
require 'json'

raise ArgumentError, 'exec options are required' if opts.nil?

cmd = opts['command']
Copy link
Collaborator

Choose a reason for hiding this comment

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

note for future (not blocker): if command is relative path, the Go implemetation resolves it relative to the kubeconfig file
https://github.com/kubernetes/kubernetes/pull/59495/files/6463e9efd9ba552e60d2555a3e6526ef90196473#diff-4c107b9e9f7f10a98e5c52f66b952e01R562

args = opts['args']
env = map_env(opts['env'])

# Validate exec options
validate_opts(opts)

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

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

creds = JSON.parse(out)
validate_credentials(opts, creds)
creds['status']['token']
end

private

def validate_opts(opts)
raise KeyError, 'exec command is required' unless opts['command']
end

def validate_credentials(opts, creds)
# out should have ExecCredential structure
raise 'invalid credentials' if creds.nil?

# Verify apiVersion?
api_version = opts['apiVersion']
if api_version && api_version != creds['apiVersion']
raise "exec plugin is configured to use API version #{api_version}, " \
"plugin returned version #{creds['apiVersion']}"
end

raise 'exec plugin didn\'t return a status field' if creds['status'].nil?
raise 'exec plugin didn\'t return a token' if creds['status']['token'].nil?
Copy link
Collaborator

Choose a reason for hiding this comment

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

These string exceptions should become some exception class.
KubeException is deprecated because it's in global namespace, we've documented "The gem raises Kubeclient::HttpError or subclasses now." But Kubeclient::HttpError is inappropriate here.
This is not very important, until we actually try to remove KubeException.

What's more interesting is understand when this can be raised. If we'll support auto-renewal, then ANY request such as get_pods would potentially also raise these!
What exceptions does Config raise presently? I see KeyError, and one string 'Unknown kubeconfig version'.
You're adding some ArgumentError and strings.

Perhaps add Kubeclient::ConfigError for all config problems?

BTW, if we add opt-out, need to choose error for refusing to exec commands too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cben I agree, KubeClient::ConfigError can be a good candidate for configuration errors.

end

# Transform name/value pairs to hash
def map_env(env)
return {} unless env

Hash[env.map { |e| [e['name'], e['value']] }]
end
end
end
end
40 changes: 40 additions & 0 deletions test/config/execauth.kubeconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
apiVersion: v1
clusters:
- cluster:
server: https://localhost:8443
insecure-skip-tls-verify: true
name: localhost:8443
contexts:
- context:
cluster: localhost:8443
namespace: default
user: system:admin:exec
name: localhost/system:admin:exec
current-context: localhost/system:admin:exec
kind: Config
preferences: {}
users:
- name: system:admin:exec
user:
exec:
# Command to execute. Required.
command: "example-exec-plugin"

# API version to use when decoding the ExecCredentials resource. Required.
#
# The API version returned by the plugin MUST match the version listed here.
#
# To integrate with tools that support multiple versions (such as client.authentication.k8s.io/v1alpha1),
# set an environment variable or pass an argument to the tool that indicates which version the exec plugin expects.
apiVersion: "client.authentication.k8s.io/v1beta1"

# Environment variables to set when executing the plugin. Optional.
env:
- name: "FOO"
value: "bar"

# Arguments to pass when executing the plugin. Optional.
args:
- "arg1"
- "arg2"

22 changes: 22 additions & 0 deletions test/test_config.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require_relative 'test_helper'
require 'yaml'
require 'open3'

# Testing Kubernetes client configuration
class KubeclientConfigTest < MiniTest::Test
Expand Down Expand Up @@ -73,6 +74,27 @@ def test_timestamps
Kubeclient::Config.read(config_file('timestamps.kubeconfig'))
end

def test_user_exec
creds = JSON.dump(
'apiVersion': 'client.authentication.k8s.io/v1beta1',
'status': {
'token': '0123456789ABCDEF0123456789ABCDEF'
}
)

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

Open3.stub(:capture3, [creds, nil, st]) do
config = Kubeclient::Config.read(config_file('execauth.kubeconfig'))
assert_equal(['localhost/system:admin:exec'], config.contexts)
context = config.context('localhost/system:admin:exec')
check_context(context, ssl: false)

assert_equal('0123456789ABCDEF0123456789ABCDEF', context.auth_options[:bearer_token])
end
end

private

def check_context(context, ssl: true)
Expand Down
125 changes: 125 additions & 0 deletions test/test_exec_credentials.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
require_relative 'test_helper'
require 'open3'

# Unit tests for the ExecCredentials token provider
class ExecCredentialsTest < MiniTest::Test
def test_exec_opts_missing
expected_msg =
'exec options are required'
exception = assert_raises(ArgumentError) do
Kubeclient::ExecCredentials.token(nil)
end
assert_equal(expected_msg, exception.message)
end

def test_exec_command_missing
expected_msg =
'exec command is required'
exception = assert_raises(KeyError) do
Kubeclient::ExecCredentials.token({})
end
assert_equal(expected_msg, exception.message)
end

def test_exec_command_failure
err = 'Error'
expected_msg =
"exec command failed: #{err}"

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

opts = { 'command' => 'dummy' }

Open3.stub(:capture3, [nil, err, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.token(opts)
end
assert_equal(expected_msg, exception.message)
end
end

def test_token
opts = { 'command' => 'dummy' }

creds = JSON.dump(
'apiVersion': 'client.authentication.k8s.io/v1alpha1',
'status': {
'token': '0123456789ABCDEF0123456789ABCDEF'
}
)

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

Open3.stub(:capture3, [creds, nil, st]) do
assert_equal('0123456789ABCDEF0123456789ABCDEF', Kubeclient::ExecCredentials.token(opts))
end
end

def test_status_missing
opts = { 'command' => 'dummy' }

creds = JSON.dump('apiVersion': 'client.authentication.k8s.io/v1alpha1')

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

expected_msg = 'exec plugin didn\'t return a status field'

Open3.stub(:capture3, [creds, nil, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.token(opts)
end
assert_equal(expected_msg, exception.message)
end
end

def test_token_missing
opts = { 'command' => 'dummy' }

creds = JSON.dump(
'apiVersion': 'client.authentication.k8s.io/v1alpha1',
'status': {}
)

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

expected_msg = 'exec plugin didn\'t return a token'

Open3.stub(:capture3, [creds, nil, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.token(opts)
end
assert_equal(expected_msg, exception.message)
end
end

def test_api_version_mismatch
api_version = 'client.authentication.k8s.io/v1alpha1'
expected_version = 'client.authentication.k8s.io/v1beta1'

opts = {
'command' => 'dummy',
'apiVersion' => expected_version
}

creds = JSON.dump(
'apiVersion': api_version
)

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

expected_msg = "exec plugin is configured to use API version #{expected_version}," \
" plugin returned version #{api_version}"

Open3.stub(:capture3, [creds, nil, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.token(opts)
end
assert_equal(expected_msg, exception.message)
end
end
end