k8y 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 967e0fcf49dc5ff373df45925be7c14ee0d8da75d12f348373cc70c72621ff23
4
- data.tar.gz: e02d82838c724f96557d3769421a2ba40557dba6501f012433b1be64ef4204f9
3
+ metadata.gz: 3eff2fb332ede35caba339504a2c23ef9fb860b0741bcaaa89186f7e6e66af42
4
+ data.tar.gz: 39f451e5d2966311dd688efd8f3b8c14a7e8a84caf37038de8446dd4f46f954b
5
5
  SHA512:
6
- metadata.gz: 5937b19b84140f2a81c0935bce8892c92ae709eb8382ce658297911f65790b311e847c92ae71887d33a6920beca1f8f69d535ab32ce125d6f697cbad0dd99c51
7
- data.tar.gz: c4635b0a99227890b7ef4dc7c88ab285771d83ea5926a53b599e90f6e5ff20965c5b51b72ad062f513716b598322422eea53825d908094c9441ae32eac54cc59
6
+ metadata.gz: 408213ee541f715a98fa1c2905110e94a41aea9f6cfa1b88a2908874c766d84264973ecf0c704bd7f4a05e0bc87e3f52dbea618a959a6a07ffec1c3a92155cad
7
+ data.tar.gz: 43ddd9865d31d1b33b5c954b7afbadf2f2d8aae39862a980a2005c78b9b6de97bf69d1cd7ac770444a86783f1a437cb1563a58af20049d90ef5edb544a67ce4a
data/CHANGELOG.md CHANGED
@@ -1,10 +1,20 @@
1
1
  ## next
2
2
 
3
+ ## 0.3.0
4
+
5
+ **Enhancements**
6
+
7
+ - Auth configuration dynamically set according to provided config [#29](https://github.com/tsontario/k8y/pull/29)
8
+
9
+ **Bug fixes**
10
+
11
+ - `Client::Client` API methods (`get_resource`, `update_resource`, etc.) now respects the `as:` parameter [#28](https://github.com/tsontario/k8y/pull/28)
12
+
3
13
  ## 0.2.0
4
14
 
5
15
  **Enhancements**
6
16
 
7
- - K8y::Client::from_in_cluster for easily building in-cluster clients [#23](https://github.com/tsontario/k8y/pull/23)
17
+ - `K8y::Client::from_in_cluster` for easily building in-cluster clients [#23](https://github.com/tsontario/k8y/pull/23)
8
18
 
9
19
  **Testing**
10
20
 
data/README.md CHANGED
@@ -1 +1,88 @@
1
- Kubernetes client for Ruby
1
+ # K8y
2
+
3
+ K8y is (yet another) gem that allows you to talk to your kubernetes clusters!
4
+
5
+ For users, this gem is intended to be simple to use, but with enough flexibility to handle a wide variety of use cases. This is acheived by providing a high-level Client API along with a lower-level REST implementation.
6
+
7
+ For maintainers, the goal is to provide a highly testable, modular, and loosely coupled design to allow for easy change management and confidence in production worthiness.
8
+
9
+ # Basic usage
10
+
11
+ ## From a Config file
12
+
13
+ ```ruby
14
+ # basic client usage
15
+ client = K8y::Client.from_config(K8y::Kubeconfig.from_file)
16
+ client.discover!
17
+ client.get_pods(namespace: "some-namespace")
18
+ ```
19
+
20
+ ## From in-cluster
21
+
22
+ ```ruby
23
+ # basic in-cluster client
24
+ client = K8y::Client.from_in_cluster
25
+ client.discover!
26
+ client.get_pods(namespace: "some-namespace")
27
+ ```
28
+
29
+ ## Client options
30
+
31
+ By default, `Kubeconfig.from_file` will read the file pointed to by `ENV["KUBECONFIG"]`, but a separate filepath can be provided.
32
+
33
+ ```ruby
34
+ # client with custom kubeconfig
35
+ client = K8y::Client.from_config(K8y::Kubeconfig.from_file("path/to/file"))
36
+ client.discover!
37
+ client.get_pods(namespace: "my-namespace")
38
+ ```
39
+
40
+ K8y will use whatever the current context your config is set to. To use a specific context, just supply the `context:` argument. This argument is not available for in-cluster clients.
41
+
42
+ ```ruby
43
+ # explicit context
44
+ client = K8y::Client.from_config(K8y::Kubeconfig.from_file, context: "my-context")
45
+ client.discover!
46
+ client.get_pods(namespace: "some-namespace")
47
+ ```
48
+
49
+ K8y clients also support multiple group versions per instance. By default, clients are loaded with `core/v1` and `apps/v1` group versions. To use a different set, supply the `group_versions:` arg.
50
+
51
+ ```ruby
52
+ # specify group versions
53
+ group_versions = [
54
+ K8y::GroupVersion.new(group: "core", version: "v1"),
55
+ K8y::GroupVersion.new(group: "networking.k8s.io", version: "v1")
56
+ ]
57
+
58
+ client = K8y::Client.from_config(K8y::Kubeconfig.from_file, group_versions: group_versions)
59
+ client.discover!
60
+ client.get_ingress(namespace: "some-namespace", name: "my-ingress")
61
+ ```
62
+
63
+ If a conflict arises between group versions, a `K8y::Client::APINameConflictError` will be raised when trying to access a duplicate-named resource. **A future feature will allow fine-grained access to client group versions. E.g. `client.api_extensions_v1.get_ingresses**
64
+
65
+ # Lower-level access
66
+
67
+ Under the hood, `K8y::Client` makes its requests via a more generic REST client: `K8y::REST::Client`. REST clients can be instantiated much the same as top-level Clients. The `path:` argument will be used as a prefix for all requests made by the client. E.g. given a cluster server of `https://1.2.3.4`, and path `foo`, `rest_client.get("bar")` will make a request to `https://1.2.3.4/foo/bar`
68
+
69
+ ```ruby
70
+ # basic rest client
71
+
72
+ # generate a REST config from a kubeconfig
73
+ rest_config = K8y::REST::Config.from_kubeconfig(Kubeconfig.from_file, path: "/")
74
+ rest_client = K8y::REST::Client.from_config(rest_config)
75
+ rest_client.get("healthz", as: :raw)
76
+ ```
77
+
78
+ # Testing
79
+
80
+ Basic test suite: `bundle exec rake`
81
+
82
+ Integration test suite: `bundle exec rake test_integration` (requires a running cluster. Defaults to searching for a `kind` config, but can be overridden by setting `K8Y_TEST_CONFIG` and `K8Y_TEST_CONTEXT` environment variables)
83
+
84
+ A future goal is to test in-cluster behaviour by running a custom Github action, but that hasn't been done yet.
85
+
86
+ # Contributing
87
+
88
+ Contributions are always welcome!
data/k8y.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency("faraday", "~> 1.6")
31
31
  spec.add_dependency("railties", "~> 6.0")
32
32
  spec.add_dependency("recursive-open-struct", "~>1.1")
33
+ spec.add_dependency("googleauth")
33
34
 
34
35
  spec.add_development_dependency("byebug", "~> 11")
35
36
  spec.add_development_dependency("minitest", "~> 5")
@@ -54,8 +54,9 @@ module K8y
54
54
  namespace = kwargs.fetch(:namespace) if resource_description.namespaced
55
55
  data = kwargs.fetch(:data)
56
56
  headers = kwargs.fetch(:headers, {}).merge(json_content_type)
57
+ as = kwargs.fetch(:as, :ros)
57
58
  rest_client.post(resource_description.path_for_resources(namespace: namespace),
58
- data: JSON.dump(data), headers: headers)
59
+ data: JSON.dump(data), headers: headers, as: as)
59
60
  end
60
61
  register_method(method_name)
61
62
  end
@@ -68,8 +69,9 @@ module K8y
68
69
  namespace = kwargs.fetch(:namespace) if resource_description.namespaced
69
70
  name = kwargs.fetch(:name)
70
71
  headers = kwargs.fetch(:headers, {})
72
+ as = kwargs.fetch(:as, :ros)
71
73
  rest_client.delete(resource_description.path_for_resource(namespace: namespace, name: name),
72
- headers: headers)
74
+ headers: headers, as: as)
73
75
  end
74
76
  register_method(method_name)
75
77
  end
@@ -82,7 +84,9 @@ module K8y
82
84
  namespace = kwargs.fetch(:namespace) if resource_description.namespaced
83
85
  name = kwargs.fetch(:name)
84
86
  headers = kwargs.fetch(:headers, {})
85
- rest_client.get(resource_description.path_for_resource(namespace: namespace, name: name), headers: headers)
87
+ as = kwargs.fetch(:as, :ros)
88
+ rest_client.get(resource_description.path_for_resource(namespace: namespace, name: name),
89
+ headers: headers, as: as)
86
90
  end
87
91
  register_method(method_name)
88
92
  end
@@ -94,7 +98,8 @@ module K8y
94
98
  define_singleton_method(method_name) do |kwargs = {}|
95
99
  namespace = kwargs.fetch(:namespace) if resource_description.namespaced
96
100
  headers = kwargs.fetch(:headers, {})
97
- rest_client.get(resource_description.path_for_resources(namespace: namespace), headers: headers)
101
+ as = kwargs.fetch(:as, :ros)
102
+ rest_client.get(resource_description.path_for_resources(namespace: namespace), headers: headers, as: as)
98
103
  end
99
104
  register_method(method_name)
100
105
  end
@@ -116,8 +121,9 @@ module K8y
116
121
  data = kwargs.fetch(:data)
117
122
  strategy = kwargs.fetch(:strategy)
118
123
  headers = kwargs.fetch(:headers, {}).merge(scoped_content_type_for_patch_strategy.call(strategy))
124
+ as = kwargs.fetch(:as, :ros)
119
125
  rest_client.patch(resource_description.path_for_resource(namespace: namespace, name: name),
120
- strategy: strategy, data: JSON.dump(data), headers: headers)
126
+ strategy: strategy, data: JSON.dump(data), headers: headers, as: as)
121
127
  end
122
128
  register_method(method_name)
123
129
  end
@@ -138,8 +144,9 @@ module K8y
138
144
  name = kwargs.fetch(:name)
139
145
  data = kwargs.fetch(:data)
140
146
  headers = kwargs.fetch(:headers, {}).merge(json_content_type)
147
+ as = kwargs.fetch(:as, :ros)
141
148
  rest_client.put(resource_description.path_for_resource(namespace: namespace, name: name),
142
- data: JSON.dump(data), headers: headers)
149
+ data: JSON.dump(data), headers: headers, as: as)
143
150
  end
144
151
  register_method(method_name)
145
152
  end
@@ -6,9 +6,6 @@ require_relative "apis"
6
6
  module K8y
7
7
  module Client
8
8
  class Client
9
- ContextNotFoundError = Class.new(Error)
10
- APINameConflictError = Class.new(Error)
11
-
12
9
  attr_reader :config, :context, :apis
13
10
 
14
11
  def initialize(config:, context:, group_versions: DEFAULT_GROUP_VERSIONS)
data/lib/k8y/client.rb CHANGED
@@ -4,6 +4,8 @@ require_relative "client/client"
4
4
  module K8y
5
5
  module Client
6
6
  Error = Class.new(Error)
7
+ ContextNotFoundError = Class.new(Error)
8
+ APINameConflictError = Class.new(Error)
7
9
 
8
10
  DEFAULT_GROUP_VERSIONS = [
9
11
  GroupVersion.new(group: "core", version: "v1"),
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8y
4
+ module REST
5
+ module Auth
6
+ class AuthBase
7
+ def configure_connection(connection)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth_base"
4
+
5
+ module K8y
6
+ module REST
7
+ module Auth
8
+ class Basic < AuthBase
9
+ def initialize(username:, password:)
10
+ super()
11
+ @username = username
12
+ @password = password
13
+ end
14
+
15
+ def configure_connection(connection)
16
+ connection.request(:authorization, :basic, username, password)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :username, :password
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "basic"
4
+ require_relative "token"
5
+ require_relative "providers/factory"
6
+
7
+ module K8y
8
+ module REST
9
+ module Auth
10
+ class Factory
11
+ def from_auth_info(auth_info)
12
+ if auth_info.username && auth_info.password
13
+ Basic.new(username: auth_info.username, password: auth_info.password)
14
+ elsif auth_info.token
15
+ Token.new(token: auth_info.token)
16
+ elsif auth_info.auth_provider
17
+ Providers::Factory.new.from_auth_provider(auth_info.auth_provider)
18
+ else
19
+ AuthBase.new
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gcp/factory"
4
+
5
+ module K8y
6
+ module REST
7
+ module Auth
8
+ module Providers
9
+ Error = Class.new(Error)
10
+
11
+ class Factory
12
+ UnnamedProviderError = Class.new(Error)
13
+ def from_auth_provider(provider)
14
+ case provider[:name]
15
+ when "gcp"
16
+ GCP::Factory.new.from_auth_provider(provider)
17
+ when nil
18
+ raise UnnamedProviderError
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "googleauth"
4
+
5
+ require_relative "../provider_base"
6
+
7
+ module K8y
8
+ module REST
9
+ module Auth
10
+ module Providers
11
+ module GCP
12
+ class ApplicationDefaultProvider < ProviderBase
13
+ SCOPES = [
14
+ "https://www.googleapis.com/auth/cloud-platform",
15
+ "https://www.googleapis.com/auth/userinfo.email",
16
+ ]
17
+
18
+ # #get_application_default actually returns a full oauth2 token payload
19
+ # This gives us, among other things, a refresh token, that we should be
20
+ # able to hold on to transparently keep client connections alive and
21
+ # healthy.
22
+ def token
23
+ creds = Google::Auth.get_application_default(SCOPES)
24
+ creds.apply({})
25
+ creds.access_token
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "googleauth"
4
+ require "json"
5
+ require "open3"
6
+ require "shellwords"
7
+
8
+ require_relative "../provider_base"
9
+
10
+ module K8y
11
+ module REST
12
+ module Auth
13
+ module Providers
14
+ module GCP
15
+ class CommandProvider < ProviderBase
16
+ # TODO: this is a shameless copy of abonas/kubeclient. It's worth it to build this from scratch,
17
+ # if only for the better understanding that comes along with it
18
+ def initialize(cmd_path:, access_token: nil, cmd_args: nil, expiry: nil, expiry_key: nil, token_key: nil)
19
+ super
20
+ @access_token = access_token
21
+ @cmd_args = cmd_args
22
+ @cmd_path = cmd_path
23
+ @expiry = expiry
24
+ @expiry_key = expiry_key
25
+ @token_key = token_key
26
+ end
27
+
28
+ def token
29
+ out, err, st = Open3.capture3(cmd, *args.split)
30
+
31
+ raise "exec command failed: #{err}" unless st.success?
32
+
33
+ extract_token(out, token_key)
34
+ end
35
+
36
+ private
37
+
38
+ def extract_token(output, key)
39
+ path =
40
+ key
41
+ .gsub(/\A{(.*)}\z/, '\\1') # {.foo.bar} -> .foo.bar
42
+ .sub(/\A\./, "") # .foo.bar -> foo.bar
43
+ .split(".")
44
+ JSON.parse(output).dig(*path)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "application_default_provider"
4
+ require_relative "command_provider"
5
+
6
+ module K8y
7
+ module REST
8
+ module Auth
9
+ module Providers
10
+ module GCP
11
+ Error = Class.new(Error)
12
+
13
+ class Factory
14
+ MissingConfigError = Class.new(Error)
15
+
16
+ def from_auth_provider(provider)
17
+ config = provider[:config]
18
+ raise MissingConfigError unless config
19
+
20
+ # see https://github.com/kubernetes/client-go/blob/master/plugin/pkg/client/auth/gcp/gcp.go#L58
21
+ if config[:"cmd-path"]
22
+ CommandProvider.new(
23
+ access_token: config[:"access-token"],
24
+ cmd_args: config[:"cmd-args"],
25
+ cmd_path: config[:"cmd-path"],
26
+ expiry: config[:expiry],
27
+ expiry_key: config[:"expiry-key"],
28
+ token_key: config[:"token-key"]
29
+ )
30
+ else
31
+ ApplicationDefaultProvider.new
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8y
4
+ module REST
5
+ module Auth
6
+ module Providers
7
+ class ProviderBase
8
+ # TODO: public API not finalized; subject to change
9
+ def token
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth_base"
4
+
5
+ module K8y
6
+ module REST
7
+ module Auth
8
+ class Token < AuthBase
9
+ def initialize(token:)
10
+ super()
11
+ @token = token
12
+ end
13
+
14
+ def configure_connection(connection)
15
+ connection.headers[:Authorization] = "Bearer #{token}"
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :token
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/k8y/rest/auth.rb CHANGED
@@ -1,77 +1,21 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require_relative "auth/factory"
4
+
2
5
  module K8y
3
6
  module REST
4
- class Auth
5
- InvalidAuthTypeError = Class.new(Error)
6
-
7
+ module Auth
7
8
  class << self
8
9
  def from_kubeconfig(kubeconfig, context: nil)
9
10
  context = context ? context : kubeconfig.current_context
10
11
  auth_info = kubeconfig.user_for_context(context).auth_info
11
-
12
- new(token: token(auth_info), username: auth_info.username, password: auth_info.password,
13
- auth_provider: auth_provider(auth_info), exec_provider: exec_provider(auth_info))
14
- end
15
-
16
- private
17
-
18
- def token(auth_info)
19
- return auth_info.token if auth_info.token
20
- File.read(auth_info.token_file) if auth_info.token_file
21
- end
22
-
23
- def auth_provider(auth_info)
24
- # TODO
12
+ from_auth_info(auth_info)
25
13
  end
26
14
 
27
- def exec_provider(auth_info)
28
- # TODO
15
+ def from_auth_info(auth_info)
16
+ Factory.new.from_auth_info(auth_info)
29
17
  end
30
18
  end
31
-
32
- def initialize(token: nil, username: nil, password: nil, auth_provider: nil, exec_provider: nil)
33
- @token = token
34
- @username = username
35
- @password = password
36
- @auth_provider = auth_provider
37
- @exec_provider = exec_provider
38
- end
39
-
40
- def configure_connection(connection)
41
- case auth_type
42
- when :basic
43
- connection.basic_auth(username, password)
44
- when :token
45
- connection.headers[:Authorization] = "Bearer #{token}"
46
- # TODO...
47
- end
48
- end
49
-
50
- private
51
-
52
- attr_reader :token, :username, :password, :auth_provider, :exec_provider
53
-
54
- def auth_type
55
- if username && password
56
- :basic
57
- elsif token
58
- :token
59
- elsif auth_provider
60
- :auth_provider
61
- elsif exec_provider
62
- :exec_provider
63
- else
64
- :none
65
- end
66
- end
67
-
68
- def basic?
69
- auth_type == :basic
70
- end
71
-
72
- def token?
73
- auth_type == :token
74
- end
75
19
  end
76
20
  end
77
21
  end
data/lib/k8y/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module K8y
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: k8y
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timothy Smith
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-28 00:00:00.000000000 Z
11
+ date: 2021-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: googleauth
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: byebug
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -213,6 +227,15 @@ files:
213
227
  - lib/k8y/kubeconfig/user.rb
214
228
  - lib/k8y/rest.rb
215
229
  - lib/k8y/rest/auth.rb
230
+ - lib/k8y/rest/auth/auth_base.rb
231
+ - lib/k8y/rest/auth/basic.rb
232
+ - lib/k8y/rest/auth/factory.rb
233
+ - lib/k8y/rest/auth/providers/factory.rb
234
+ - lib/k8y/rest/auth/providers/gcp/application_default_provider.rb
235
+ - lib/k8y/rest/auth/providers/gcp/command_provider.rb
236
+ - lib/k8y/rest/auth/providers/gcp/factory.rb
237
+ - lib/k8y/rest/auth/providers/provider_base.rb
238
+ - lib/k8y/rest/auth/token.rb
216
239
  - lib/k8y/rest/client.rb
217
240
  - lib/k8y/rest/config.rb
218
241
  - lib/k8y/rest/config_validator.rb