kubeclient 4.2.2 → 4.7.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of kubeclient might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/.travis.yml +22 -6
- data/CHANGELOG.md +46 -0
- data/README.md +258 -65
- data/RELEASING.md +4 -1
- data/kubeclient.gemspec +6 -3
- data/lib/kubeclient.rb +4 -2
- data/lib/kubeclient/aws_eks_credentials.rb +46 -0
- data/lib/kubeclient/common.rb +49 -17
- data/lib/kubeclient/config.rb +20 -2
- data/lib/kubeclient/gcp_auth_provider.rb +19 -0
- data/lib/kubeclient/gcp_command_credentials.rb +31 -0
- data/lib/kubeclient/google_application_default_credentials.rb +17 -2
- data/lib/kubeclient/oidc_auth_provider.rb +52 -0
- data/lib/kubeclient/version.rb +1 -1
- data/test/config/gcpauth.kubeconfig +22 -0
- data/test/config/gcpcmdauth.kubeconfig +26 -0
- data/test/config/oidcauth.kubeconfig +25 -0
- data/test/json/service_json_patch.json +26 -0
- data/test/json/service_merge_patch.json +26 -0
- data/test/test_config.rb +51 -0
- data/test/test_gcp_command_credentials.rb +27 -0
- data/test/test_helper.rb +1 -0
- data/test/test_kubeclient.rb +16 -0
- data/test/test_oidc_auth_provider.rb +103 -0
- data/test/test_pod_log.rb +37 -3
- data/test/test_service.rb +54 -0
- data/test/test_watch.rb +13 -0
- metadata +77 -12
data/lib/kubeclient/version.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
apiVersion: v1
|
2
|
+
clusters:
|
3
|
+
- cluster:
|
4
|
+
server: https://localhost:8443
|
5
|
+
insecure-skip-tls-verify: true
|
6
|
+
name: localhost:8443
|
7
|
+
contexts:
|
8
|
+
- context:
|
9
|
+
cluster: localhost:8443
|
10
|
+
namespace: default
|
11
|
+
user: application-default-credentials
|
12
|
+
name: localhost/application-default-credentials
|
13
|
+
kind: Config
|
14
|
+
preferences: {}
|
15
|
+
users:
|
16
|
+
- name: application-default-credentials
|
17
|
+
user:
|
18
|
+
auth-provider:
|
19
|
+
config:
|
20
|
+
access-token: <fake_token>
|
21
|
+
expiry: 2019-02-19T11:07:29.827352-05:00
|
22
|
+
name: gcp
|
@@ -0,0 +1,26 @@
|
|
1
|
+
apiVersion: v1
|
2
|
+
clusters:
|
3
|
+
- cluster:
|
4
|
+
server: https://localhost:8443
|
5
|
+
insecure-skip-tls-verify: true
|
6
|
+
name: localhost:8443
|
7
|
+
contexts:
|
8
|
+
- context:
|
9
|
+
cluster: localhost:8443
|
10
|
+
namespace: default
|
11
|
+
user: application-default-credentials
|
12
|
+
name: localhost/application-default-credentials
|
13
|
+
kind: Config
|
14
|
+
preferences: {}
|
15
|
+
users:
|
16
|
+
- name: application-default-credentials
|
17
|
+
user:
|
18
|
+
auth-provider:
|
19
|
+
config:
|
20
|
+
access-token: <fake_token>
|
21
|
+
cmd-args: config config-helper --format=json
|
22
|
+
cmd-path: /path/to/gcloud
|
23
|
+
expiry: 2019-04-09T19:26:18Z
|
24
|
+
expiry-key: '{.credential.token_expiry}'
|
25
|
+
token-key: '{.credential.access_token}'
|
26
|
+
name: gcp
|
@@ -0,0 +1,25 @@
|
|
1
|
+
apiVersion: v1
|
2
|
+
clusters:
|
3
|
+
- cluster:
|
4
|
+
server: https://localhost:8443
|
5
|
+
insecure-skip-tls-verify: true
|
6
|
+
name: localhost:8443
|
7
|
+
contexts:
|
8
|
+
- context:
|
9
|
+
cluster: localhost:8443
|
10
|
+
namespace: default
|
11
|
+
user: oidc-auth-provider
|
12
|
+
name: localhost/oidc-auth-provider
|
13
|
+
kind: Config
|
14
|
+
preferences: {}
|
15
|
+
users:
|
16
|
+
- name: oidc-auth-provider
|
17
|
+
user:
|
18
|
+
auth-provider:
|
19
|
+
config:
|
20
|
+
client-id: fake-client-id
|
21
|
+
client-secret: fake-client-secret
|
22
|
+
id-token: fake-id-token
|
23
|
+
idp-issuer-url: https://accounts.google.com
|
24
|
+
refresh-token: fake-refresh-token
|
25
|
+
name: oidc
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{
|
2
|
+
"status" : {},
|
3
|
+
"kind" : "Service",
|
4
|
+
"apiVersion" : "v1",
|
5
|
+
"spec" : {
|
6
|
+
"ports" : [
|
7
|
+
{
|
8
|
+
"targetPort" : 80,
|
9
|
+
"nodePort" : 0,
|
10
|
+
"port" : 80,
|
11
|
+
"protocol" : "TCP"
|
12
|
+
}
|
13
|
+
],
|
14
|
+
"clusterIP" : "1.2.3.4",
|
15
|
+
"type": "LoadBalancer"
|
16
|
+
},
|
17
|
+
"metadata" : {
|
18
|
+
"name" : "my-service",
|
19
|
+
"creationTimestamp" : null,
|
20
|
+
"namespace" : "development",
|
21
|
+
"resourceVersion" : "2",
|
22
|
+
"annotations" : {
|
23
|
+
"key" : "value"
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{
|
2
|
+
"status" : {},
|
3
|
+
"kind" : "Service",
|
4
|
+
"apiVersion" : "v1",
|
5
|
+
"spec" : {
|
6
|
+
"ports" : [
|
7
|
+
{
|
8
|
+
"targetPort" : 80,
|
9
|
+
"nodePort" : 0,
|
10
|
+
"port" : 80,
|
11
|
+
"protocol" : "TCP"
|
12
|
+
}
|
13
|
+
],
|
14
|
+
"clusterIP" : "1.2.3.4",
|
15
|
+
"type": "NodePort"
|
16
|
+
},
|
17
|
+
"metadata" : {
|
18
|
+
"name" : "my-service",
|
19
|
+
"creationTimestamp" : null,
|
20
|
+
"namespace" : "development",
|
21
|
+
"resourceVersion" : "2",
|
22
|
+
"annotations" : {
|
23
|
+
"key" : "value"
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
data/test/test_config.rb
CHANGED
@@ -123,6 +123,57 @@ class KubeclientConfigTest < MiniTest::Test
|
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
126
|
+
def test_gcp_default_auth
|
127
|
+
Kubeclient::GoogleApplicationDefaultCredentials.expects(:token).returns('token1').once
|
128
|
+
parsed = YAML.safe_load(File.read(config_file('gcpauth.kubeconfig')), [Date, Time])
|
129
|
+
config = Kubeclient::Config.new(parsed, nil)
|
130
|
+
config.context(config.contexts.first)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Each call to .context() obtains a new token, calling .auth_options doesn't change anything.
|
134
|
+
# NOTE: this is not a guarantee, may change, just testing current behavior.
|
135
|
+
def test_gcp_default_auth_renew
|
136
|
+
Kubeclient::GoogleApplicationDefaultCredentials.expects(:token).returns('token1').once
|
137
|
+
parsed = YAML.safe_load(File.read(config_file('gcpauth.kubeconfig')), [Date, Time])
|
138
|
+
config = Kubeclient::Config.new(parsed, nil)
|
139
|
+
context = config.context(config.contexts.first)
|
140
|
+
assert_equal({ bearer_token: 'token1' }, context.auth_options)
|
141
|
+
assert_equal({ bearer_token: 'token1' }, context.auth_options)
|
142
|
+
|
143
|
+
Kubeclient::GoogleApplicationDefaultCredentials.expects(:token).returns('token2').once
|
144
|
+
context2 = config.context(config.contexts.first)
|
145
|
+
assert_equal({ bearer_token: 'token2' }, context2.auth_options)
|
146
|
+
assert_equal({ bearer_token: 'token1' }, context.auth_options)
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_gcp_command_auth
|
150
|
+
Kubeclient::GCPCommandCredentials.expects(:token)
|
151
|
+
.with('access-token' => '<fake_token>',
|
152
|
+
'cmd-args' => 'config config-helper --format=json',
|
153
|
+
'cmd-path' => '/path/to/gcloud',
|
154
|
+
'expiry' => '2019-04-09 19:26:18 UTC',
|
155
|
+
'expiry-key' => '{.credential.token_expiry}',
|
156
|
+
'token-key' => '{.credential.access_token}')
|
157
|
+
.returns('token1')
|
158
|
+
.once
|
159
|
+
config = Kubeclient::Config.read(config_file('gcpcmdauth.kubeconfig'))
|
160
|
+
config.context(config.contexts.first)
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_oidc_auth_provider
|
164
|
+
Kubeclient::OIDCAuthProvider.expects(:token)
|
165
|
+
.with('client-id' => 'fake-client-id',
|
166
|
+
'client-secret' => 'fake-client-secret',
|
167
|
+
'id-token' => 'fake-id-token',
|
168
|
+
'idp-issuer-url' => 'https://accounts.google.com',
|
169
|
+
'refresh-token' => 'fake-refresh-token')
|
170
|
+
.returns('token1')
|
171
|
+
.once
|
172
|
+
parsed = YAML.safe_load(File.read(config_file('oidcauth.kubeconfig')))
|
173
|
+
config = Kubeclient::Config.new(parsed, nil)
|
174
|
+
config.context(config.contexts.first)
|
175
|
+
end
|
176
|
+
|
126
177
|
private
|
127
178
|
|
128
179
|
def check_context(context, ssl: true)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
# Unit tests for the GCPCommandCredentials token provider
|
5
|
+
class GCPCommandCredentialsTest < MiniTest::Test
|
6
|
+
def test_token
|
7
|
+
opts = { 'cmd-args' => 'config config-helper --format=json',
|
8
|
+
'cmd-path' => '/path/to/gcloud',
|
9
|
+
'expiry-key' => '{.credential.token_expiry}',
|
10
|
+
'token-key' => '{.credential.access_token}' }
|
11
|
+
|
12
|
+
creds = JSON.dump(
|
13
|
+
'credential' => {
|
14
|
+
'access_token' => '9A3A941836F2458175BE18AA1971EBBF47949B07',
|
15
|
+
'token_expiry' => '2019-04-12T15:02:51Z'
|
16
|
+
}
|
17
|
+
)
|
18
|
+
|
19
|
+
st = Minitest::Mock.new
|
20
|
+
st.expect(:success?, true)
|
21
|
+
|
22
|
+
Open3.stub(:capture3, [creds, nil, st]) do
|
23
|
+
assert_equal('9A3A941836F2458175BE18AA1971EBBF47949B07',
|
24
|
+
Kubeclient::GCPCommandCredentials.token(opts))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/test/test_helper.rb
CHANGED
data/test/test_kubeclient.rb
CHANGED
@@ -296,6 +296,22 @@ class KubeclientTest < MiniTest::Test
|
|
296
296
|
)
|
297
297
|
end
|
298
298
|
|
299
|
+
def test_entities_with_resource_version
|
300
|
+
version = '329'
|
301
|
+
|
302
|
+
stub_core_api_list
|
303
|
+
stub_get_services
|
304
|
+
|
305
|
+
services = client.get_services(resource_version: version)
|
306
|
+
|
307
|
+
assert_instance_of(Kubeclient::Common::EntityList, services)
|
308
|
+
assert_requested(
|
309
|
+
:get,
|
310
|
+
"http://localhost:8080/api/v1/services?resourceVersion=#{version}",
|
311
|
+
times: 1
|
312
|
+
)
|
313
|
+
end
|
314
|
+
|
299
315
|
def test_entities_with_field_selector
|
300
316
|
selector = 'involvedObject.name=redis-master'
|
301
317
|
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'openid_connect'
|
3
|
+
|
4
|
+
class OIDCAuthProviderTest < MiniTest::Test
|
5
|
+
def setup
|
6
|
+
@client_id = 'client_id'
|
7
|
+
@client_secret = 'client_secret'
|
8
|
+
@idp_issuer_url = 'idp_issuer_url'
|
9
|
+
@refresh_token = 'refresh_token'
|
10
|
+
@id_token = 'id_token'
|
11
|
+
@new_id_token = 'new_id_token'
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_expired_token
|
15
|
+
OpenIDConnect::Discovery::Provider::Config.stub(:discover!, discovery_mock) do
|
16
|
+
OpenIDConnect::ResponseObject::IdToken.stub(:decode, id_token_mock(Time.now.to_i - 7200)) do
|
17
|
+
OpenIDConnect::Client.stub(:new, openid_client_mock) do
|
18
|
+
retrieved_id_token = Kubeclient::OIDCAuthProvider.token(
|
19
|
+
'client-id' => @client_id,
|
20
|
+
'client-secret' => @client_secret,
|
21
|
+
'id-token' => @id_token,
|
22
|
+
'idp-issuer-url' => @idp_issuer_url,
|
23
|
+
'refresh-token' => @refresh_token
|
24
|
+
)
|
25
|
+
assert_equal(@new_id_token, retrieved_id_token)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_valid_token
|
32
|
+
OpenIDConnect::Discovery::Provider::Config.stub(:discover!, discovery_mock) do
|
33
|
+
OpenIDConnect::ResponseObject::IdToken.stub(:decode, id_token_mock(Time.now.to_i + 7200)) do
|
34
|
+
retrieved_id_token = Kubeclient::OIDCAuthProvider.token(
|
35
|
+
'client-id' => @client_id,
|
36
|
+
'client-secret' => @client_secret,
|
37
|
+
'id-token' => @id_token,
|
38
|
+
'idp-issuer-url' => @idp_issuer_url,
|
39
|
+
'refresh-token' => @refresh_token
|
40
|
+
)
|
41
|
+
assert_equal(@id_token, retrieved_id_token)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_missing_id_token
|
47
|
+
OpenIDConnect::Discovery::Provider::Config.stub(:discover!, discovery_mock) do
|
48
|
+
OpenIDConnect::Client.stub(:new, openid_client_mock) do
|
49
|
+
retrieved_id_token = Kubeclient::OIDCAuthProvider.token(
|
50
|
+
'client-id' => @client_id,
|
51
|
+
'client-secret' => @client_secret,
|
52
|
+
'idp-issuer-url' => @idp_issuer_url,
|
53
|
+
'refresh-token' => @refresh_token
|
54
|
+
)
|
55
|
+
assert_equal(@new_id_token, retrieved_id_token)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_token_with_unknown_kid
|
61
|
+
OpenIDConnect::Discovery::Provider::Config.stub(:discover!, discovery_mock) do
|
62
|
+
OpenIDConnect::ResponseObject::IdToken.stub(
|
63
|
+
:decode, ->(_token, _jwks) { raise JSON::JWK::Set::KidNotFound }
|
64
|
+
) do
|
65
|
+
OpenIDConnect::Client.stub(:new, openid_client_mock) do
|
66
|
+
retrieved_id_token = Kubeclient::OIDCAuthProvider.token(
|
67
|
+
'client-id' => @client_id,
|
68
|
+
'client-secret' => @client_secret,
|
69
|
+
'id-token' => @id_token,
|
70
|
+
'idp-issuer-url' => @idp_issuer_url,
|
71
|
+
'refresh-token' => @refresh_token
|
72
|
+
)
|
73
|
+
assert_equal(@new_id_token, retrieved_id_token)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def openid_client_mock
|
82
|
+
access_token = Minitest::Mock.new
|
83
|
+
access_token.expect(@id_token, @new_id_token)
|
84
|
+
|
85
|
+
openid_client = Minitest::Mock.new
|
86
|
+
openid_client.expect(:refresh_token=, nil, [@refresh_token])
|
87
|
+
openid_client.expect(:access_token!, access_token)
|
88
|
+
end
|
89
|
+
|
90
|
+
def id_token_mock(expiry)
|
91
|
+
id_token_mock = Minitest::Mock.new
|
92
|
+
id_token_mock.expect(:exp, expiry)
|
93
|
+
end
|
94
|
+
|
95
|
+
def discovery_mock
|
96
|
+
discovery = Minitest::Mock.new
|
97
|
+
discovery.expect(:jwks, 'jwks')
|
98
|
+
discovery.expect(:authorization_endpoint, 'authz_endpoint')
|
99
|
+
discovery.expect(:token_endpoint, 'token_endpoint')
|
100
|
+
discovery.expect(:userinfo_endpoint, 'userinfo_endpoint')
|
101
|
+
discovery
|
102
|
+
end
|
103
|
+
end
|
data/test/test_pod_log.rb
CHANGED
@@ -69,12 +69,31 @@ class TestPodLog < MiniTest::Test
|
|
69
69
|
times: 1)
|
70
70
|
end
|
71
71
|
|
72
|
+
def test_get_pod_limit_bytes
|
73
|
+
selected_bytes = open_test_file('pod_log.txt').read(10)
|
74
|
+
|
75
|
+
stub_request(:get, %r{/namespaces/default/pods/[a-z0-9-]+/log})
|
76
|
+
.to_return(body: selected_bytes,
|
77
|
+
status: 200)
|
78
|
+
|
79
|
+
client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
|
80
|
+
retrieved_log = client.get_pod_log('redis-master-pod',
|
81
|
+
'default',
|
82
|
+
limit_bytes: 10)
|
83
|
+
|
84
|
+
assert_equal(selected_bytes, retrieved_log)
|
85
|
+
|
86
|
+
assert_requested(:get,
|
87
|
+
'http://localhost:8080/api/v1/namespaces/default/pods/redis-master-pod/log?limitBytes=10',
|
88
|
+
times: 1)
|
89
|
+
end
|
90
|
+
|
72
91
|
def test_watch_pod_log
|
73
|
-
|
92
|
+
file = open_test_file('pod_log.txt')
|
93
|
+
expected_lines = file.read.split("\n")
|
74
94
|
|
75
95
|
stub_request(:get, %r{/namespaces/default/pods/[a-z0-9-]+/log\?.*follow})
|
76
|
-
.to_return(body:
|
77
|
-
status: 200)
|
96
|
+
.to_return(body: file, status: 200)
|
78
97
|
|
79
98
|
client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
|
80
99
|
|
@@ -85,6 +104,21 @@ class TestPodLog < MiniTest::Test
|
|
85
104
|
end
|
86
105
|
end
|
87
106
|
|
107
|
+
def test_watch_pod_log_with_block
|
108
|
+
file = open_test_file('pod_log.txt')
|
109
|
+
first = file.readlines.first.chomp
|
110
|
+
|
111
|
+
stub_request(:get, %r{/namespaces/default/pods/[a-z0-9-]+/log\?.*follow})
|
112
|
+
.to_return(body: file, status: 200)
|
113
|
+
|
114
|
+
client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
|
115
|
+
|
116
|
+
client.watch_pod_log('redis-master-pod', 'default') do |line|
|
117
|
+
assert_equal first, line
|
118
|
+
break
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
88
122
|
def test_watch_pod_log_follow_redirect
|
89
123
|
expected_lines = open_test_file('pod_log.txt').read.split("\n")
|
90
124
|
redirect = 'http://localhost:1234/api/namespaces/default/pods/redis-master-pod/log'
|
data/test/test_service.rb
CHANGED
@@ -273,4 +273,58 @@ class TestService < MiniTest::Test
|
|
273
273
|
data['metadata']['annotations']['key'] == 'value'
|
274
274
|
end
|
275
275
|
end
|
276
|
+
|
277
|
+
def test_json_patch_service
|
278
|
+
service = Kubeclient::Resource.new
|
279
|
+
name = 'my-service'
|
280
|
+
|
281
|
+
service.metadata = {}
|
282
|
+
service.metadata.name = name
|
283
|
+
service.metadata.namespace = 'development'
|
284
|
+
|
285
|
+
stub_core_api_list
|
286
|
+
expected_url = "http://localhost:8080/api/v1/namespaces/development/services/#{name}"
|
287
|
+
stub_request(:patch, expected_url)
|
288
|
+
.to_return(body: open_test_file('service_json_patch.json'), status: 200)
|
289
|
+
|
290
|
+
patch = [
|
291
|
+
{ 'op' => 'add', 'path' => '/spec/type', 'value' => 'LoadBalancer' }
|
292
|
+
]
|
293
|
+
|
294
|
+
client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
|
295
|
+
service = client.json_patch_service(name, patch, 'development')
|
296
|
+
assert_kind_of(RecursiveOpenStruct, service)
|
297
|
+
|
298
|
+
assert_requested(:patch, expected_url, times: 1) do |req|
|
299
|
+
data = JSON.parse(req.body)
|
300
|
+
req.headers['Content-Type'] == 'application/json-patch+json' &&
|
301
|
+
data == patch
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def test_merge_patch_service
|
306
|
+
service = Kubeclient::Resource.new
|
307
|
+
name = 'my-service'
|
308
|
+
|
309
|
+
service.metadata = {}
|
310
|
+
service.metadata.name = name
|
311
|
+
service.metadata.namespace = 'development'
|
312
|
+
|
313
|
+
stub_core_api_list
|
314
|
+
expected_url = "http://localhost:8080/api/v1/namespaces/development/services/#{name}"
|
315
|
+
stub_request(:patch, expected_url)
|
316
|
+
.to_return(body: open_test_file('service_merge_patch.json'), status: 200)
|
317
|
+
|
318
|
+
patch = { spec: { type: 'NodePort' } }
|
319
|
+
|
320
|
+
client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
|
321
|
+
service = client.merge_patch_service(name, patch, 'development')
|
322
|
+
assert_kind_of(RecursiveOpenStruct, service)
|
323
|
+
|
324
|
+
assert_requested(:patch, expected_url, times: 1) do |req|
|
325
|
+
data = JSON.parse(req.body)
|
326
|
+
req.headers['Content-Type'] == 'application/merge-patch+json' &&
|
327
|
+
data['spec']['type'] == 'NodePort'
|
328
|
+
end
|
329
|
+
end
|
276
330
|
end
|