k8y 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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