Skip to content

Commit

Permalink
Merge pull request #363 from motymichaely/feature/exec-credential
Browse files Browse the repository at this point in the history
Adds support for exec credential plugin
  • Loading branch information
cben authored Dec 9, 2018
2 parents 4ad560b + 63f1142 commit 336db64
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 0 deletions.
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 @@ -131,6 +131,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']
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?
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

0 comments on commit 336db64

Please sign in to comment.