k8s-client 0.1.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.
@@ -0,0 +1,10 @@
1
+ rspec:
2
+ build: .
3
+ volumes:
4
+ - .:/app
5
+ entrypoint: bundle exec rspec
6
+ k8s-client:
7
+ build: .
8
+ volumes:
9
+ - .:/app
10
+ entrypoint: bundle exec bin/k8s-client
@@ -0,0 +1,35 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "k8s/client/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "k8s-client"
8
+ spec.version = K8s::Client::VERSION
9
+ spec.authors = ["Kontena, Inc."]
10
+ spec.email = ["info@kontena.io"]
11
+ spec.license = "Apache-2.0"
12
+
13
+ spec.summary = "Kubernetes client library"
14
+ spec.homepage = "https://github.com/kontena/k8s-client"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "bin"
22
+ spec.executables = []
23
+ spec.require_paths = ["lib"]
24
+ spec.required_ruby_version = '~> 2.4'
25
+
26
+ spec.add_runtime_dependency "excon", "~> 0.62.0"
27
+ spec.add_runtime_dependency "dry-struct", "~> 0.5.0"
28
+ spec.add_runtime_dependency "deep_merge", "~> 1.2.1"
29
+ spec.add_runtime_dependency "recursive-open-struct", "~> 1.1.0"
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.16"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec", "~> 3.7"
34
+ spec.add_development_dependency "webmock", "~> 3.4.2"
35
+ end
@@ -0,0 +1,13 @@
1
+ require 'k8s/api/metav1'
2
+ require 'k8s/api/version'
3
+
4
+ require 'k8s/config'
5
+ require 'k8s/logging'
6
+
7
+ require 'k8s/api_client'
8
+ require "k8s/client"
9
+ require "k8s/error"
10
+ require 'k8s/resource'
11
+ require 'k8s/resource_client'
12
+ require 'k8s/stack'
13
+ require 'k8s/transport'
@@ -0,0 +1,26 @@
1
+ require 'dry-types'
2
+ require 'dry-struct'
3
+
4
+ module K8s
5
+ module API
6
+ module Types
7
+ include Dry::Types.module
8
+ end
9
+
10
+ class Struct < Dry::Struct
11
+ # input from JSON with string keys
12
+ transform_keys(&:to_sym)
13
+
14
+ # @param data [Hash]
15
+ # @return [self]
16
+ def self.from_json(data)
17
+ return new(data)
18
+ end
19
+
20
+ # @return [String]
21
+ def to_json
22
+ to_hash.to_json
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ require 'k8s/api'
2
+
3
+ module K8s
4
+ module API
5
+ module MetaV1
6
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta
7
+ class Resource < Struct
8
+ # XXX: making these optional seems dangerous, but some APIs (GET /api/v1) are missing these
9
+ attribute :kind, Types::Strict::String.optional.default(nil)
10
+ attribute :apiVersion, Types::Strict::String.optional.default(nil)
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ require 'k8s/api/metav1/api_group'
17
+ require 'k8s/api/metav1/api_resource'
18
+ require 'k8s/api/metav1/list'
19
+ require 'k8s/api/metav1/object'
20
+ require 'k8s/api/metav1/status'
@@ -0,0 +1,22 @@
1
+ module K8s
2
+ module API
3
+ module MetaV1
4
+ class APIGroupVersion < Struct
5
+ attribute :groupVersion, Types::Strict::String
6
+ attribute :version, Types::Strict::String
7
+ end
8
+
9
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIGroup
10
+ class APIGroup < Struct
11
+ attribute :name, Types::Strict::String
12
+ attribute :versions, Types::Strict::Array.of(APIGroupVersion)
13
+ attribute :preferredVersion, APIGroupVersion
14
+ end
15
+
16
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIGroupList
17
+ class APIGroupList < Resource
18
+ attribute :groups, Types::Strict::Array.of(APIGroup)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module K8s
2
+ module API
3
+ module MetaV1
4
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIResource
5
+ class APIResource < Struct
6
+ attribute :name, Types::Strict::String
7
+ attribute :singularName, Types::Strict::String
8
+ attribute :namespaced, Types::Strict::Bool
9
+ attribute :group, Types::Strict::String.optional.default(nil)
10
+ attribute :version, Types::Strict::String.optional.default(nil)
11
+ attribute :kind, Types::Strict::String
12
+ attribute :verbs, Types::Strict::Array.of(Types::Strict::String)
13
+ attribute :shortNames, Types::Strict::Array.of(Types::Strict::String).optional.default([])
14
+ attribute :categories, Types::Strict::Array.of(Types::Strict::String).optional.default([])
15
+ end
16
+
17
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIResourceList
18
+ class APIResourceList < Resource
19
+ attribute :groupVersion, Types::Strict::String
20
+ attribute :resources, Types::Strict::Array.of(APIResource)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ module K8s
2
+ module API
3
+ module MetaV1
4
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ListMeta
5
+ class ListMeta < Struct
6
+ attribute :selfLink, Types::Strict::String.optional.default(nil)
7
+ attribute :resourceVersion, Types::Strict::String.optional.default(nil)
8
+ attribute :continue, Types::Strict::String.optional.default(nil)
9
+ end
10
+
11
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#List
12
+ class List < Resource
13
+ attribute :metadata, ListMeta
14
+ attribute :items, Types::Strict::Array # untyped
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ require 'k8s/api/metav1/status'
2
+
3
+ module K8s
4
+ module API
5
+ module MetaV1
6
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#OwnerReference
7
+ class OwnerReference < Resource
8
+ attribute :name, Types::Strict::String
9
+ attribute :uid, Types::Strict::String
10
+ attribute :controller, Types::Strict::Bool.optional.default(nil)
11
+ attribute :blockOwnerDeletion, Types::Strict::Bool.optional.default(nil)
12
+ end
13
+
14
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Initializer
15
+ class Initializer < Struct
16
+ attribute :name, Types::Strict::String
17
+ end
18
+
19
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Initializers
20
+ class Initializers < Struct
21
+ attribute :pending, Initializer
22
+ attribute :result, Status.optional.default(nil)
23
+ end
24
+
25
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMeta
26
+ class ObjectMeta < Resource
27
+ attribute :name, Types::Strict::String.optional.default(nil)
28
+ attribute :generateName, Types::Strict::String.optional.default(nil)
29
+ attribute :namespace, Types::Strict::String.optional.default(nil)
30
+ attribute :selfLink, Types::Strict::String.optional.default(nil)
31
+ attribute :uid, Types::Strict::String.optional.default(nil)
32
+ attribute :resourceVersion, Types::Strict::String.optional.default(nil)
33
+ attribute :generation, Types::Strict::Integer.optional.default(nil)
34
+ attribute :creationTimestamp, Types::DateTime.optional.default(nil)
35
+ attribute :deletionTimestamp, Types::DateTime.optional.default(nil)
36
+ attribute :deletionGracePeriodSeconds, Types::Strict::Integer.optional.default(nil)
37
+ attribute :labels, Types::Strict::Hash.map(Types::Strict::String, Types::Strict::String).optional.default(nil)
38
+ attribute :annotations, Types::Strict::Hash.map(Types::Strict::String, Types::Strict::String).optional.default(nil)
39
+ attribute :ownerReferences, Types::Strict::Array.of(OwnerReference).optional.default([])
40
+ attribute :initializers, Initializers.optional.default(nil)
41
+ attribute :finalizers, Types::Strict::Array.of(Types::Strict::String).optional.default([])
42
+ attribute :clusterName, Types::Strict::String.optional.default(nil)
43
+ end
44
+
45
+ # common attributes shared by all object types
46
+ class ObjectCommon < Resource
47
+ attribute :metadata, ObjectMeta.optional.default(nil)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ module K8s
2
+ module API
3
+ module MetaV1
4
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Status
5
+ class Status < Resource
6
+
7
+ class Cause < Struct
8
+ attribute :reason, Types::Strict::String.optional.default(nil)
9
+ attribute :message, Types::Strict::String.optional.default(nil) # human-readable
10
+ attribute :field, Types::Strict::String.optional.default(nil) # human-readable
11
+ end
12
+
13
+ class Details < Struct
14
+ attribute :name, Types::Strict::String.optional.default(nil)
15
+ attribute :group, Types::Strict::String.optional.default(nil)
16
+ attribute :kind, Types::Strict::String.optional.default(nil)
17
+ attribute :uid, Types::Strict::String.optional.default(nil)
18
+ attribute :causes, Types::Strict::Array.of(Cause).optional.default(nil)
19
+ attribute :retryAfterSeconds, Types::Strict::Integer.optional.default(nil)
20
+ end
21
+
22
+ attribute :metadata, ListMeta
23
+ attribute :status, Types::Strict::String.enum('Success', 'Failure').optional.default(nil)
24
+ attribute :message, Types::Strict::String.optional.default(nil) # human-readable
25
+ attribute :reason, Types::Strict::String.optional.default(nil) # machine-readable
26
+ attribute :details, Details.optional.default(nil)
27
+ attribute :code, Types::Strict::Integer.optional.default(nil)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ module K8s
2
+ module API
3
+ class Version < Struct
4
+ attribute :buildDate, Types::Strict::String # TODO: parse datetime?
5
+ attribute :compiler, Types::Strict::String
6
+ attribute :gitCommit, Types::Strict::String
7
+ attribute :gitTreeState, Types::Strict::String
8
+ attribute :gitVersion, Types::Strict::String
9
+ attribute :major, Types::Strict::String
10
+ attribute :minor, Types::Strict::String
11
+ attribute :platform, Types::Strict::String
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,109 @@
1
+ module K8s
2
+ class APIClient
3
+ def self.path(api_version)
4
+ if api_version.include? '/'
5
+ File.join('/apis', api_version)
6
+ else
7
+ File.join('/api', api_version)
8
+ end
9
+
10
+ end
11
+
12
+ # @param transport [K8s::Transport]
13
+ # @param api_version [String] "group/version" or "version" (core)
14
+ def initialize(transport, api_version)
15
+ @transport = transport
16
+ @api_version = api_version
17
+ end
18
+
19
+ # @return [String]
20
+ def api_version
21
+ @api_version
22
+ end
23
+
24
+ def path(*path)
25
+ @transport.path(self.class.path(@api_version), *path)
26
+ end
27
+
28
+ # @return [Bool] loaded yet?
29
+ def api_resources?
30
+ !!@api_resources
31
+ end
32
+
33
+ # @param api_resources [Array<K8s::API::MetaV1::APIResource>]
34
+ def api_resources=(api_resources)
35
+ @api_resources = api_resources
36
+ end
37
+
38
+ # Force-update APIResources
39
+ #
40
+ # @return [Array<K8s::API::MetaV1::APIResource>]
41
+ def api_resources!
42
+ @api_resources = @transport.get(self.path,
43
+ response_class: K8s::API::MetaV1::APIResourceList,
44
+ ).resources
45
+ end
46
+
47
+ # Cached APIResources
48
+ #
49
+ # @return [Array<K8s::API::MetaV1::APIResource>]
50
+ def api_resources
51
+ @api_resources || api_resources!
52
+ end
53
+
54
+ # @param resource_name [String]
55
+ # @param namespace [String, nil]
56
+ # @raise [K8s::Error] unknown resource
57
+ # @return [K8s::ResourceClient]
58
+ def resource(resource_name, namespace: nil)
59
+ unless api_resource = api_resources.find{ |api_resource| api_resource.name == resource_name }
60
+ raise K8s::Error, "Unknown resource #{resource_name} for #{@api_version}"
61
+ end
62
+
63
+ ResourceClient.new(@transport, self, api_resource,
64
+ namespace: namespace,
65
+ )
66
+ end
67
+
68
+ # @param resource [K8s::Resource]
69
+ # @param namespace [String, nil] default if resource is missing namespace
70
+ # @raise [K8s::Error] unknown resource
71
+ # @return [K8s::ResourceClient]
72
+ def client_for_resource(resource, namespace: nil)
73
+ unless @api_version == resource.apiVersion
74
+ raise K8s::Error, "Invalid apiVersion=#{resource.apiVersion} for #{@api_version} client"
75
+ end
76
+
77
+ unless api_resource = api_resources.find{ |api_resource| api_resource.kind == resource.kind }
78
+ raise K8s::Error, "Unknown resource kind=#{api_resource.kind} for #{@api_version}"
79
+ end
80
+
81
+ ResourceClient.new(@transport, self, api_resource,
82
+ namespace: resource.metadata.namespace || namespace,
83
+ )
84
+ end
85
+
86
+ # TODO: skip non-namespaced resources if namespace is given, or ignore namespace?
87
+ #
88
+ # @param namespace [String, nil]
89
+ # @return [Array<K8s::ResourceClient>]
90
+ def resources(namespace: nil)
91
+ api_resources.map{ |api_resource| ResourceClient.new(@transport, self, api_resource,
92
+ namespace: namespace,
93
+ ) }
94
+ end
95
+
96
+ # Pipeline list requests for multiple resource types.
97
+ #
98
+ # Returns flattened array with mixed resource kinds.
99
+ #
100
+ # @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
101
+ # @param **options @see [K8s::ResourceClient#list]
102
+ # @return [Array<K8s::Resource>]
103
+ def list_resources(resources = nil, **options)
104
+ resources ||= self.resources.select{|resource| resource.list? }
105
+
106
+ ResourceClient.list(resource, @transport, **options)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,148 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module K8s
5
+ # @return [K8s::Client]
6
+ def self.client(server, **options)
7
+ Client.new(Transport.new(server, **options))
8
+ end
9
+
10
+ class Client
11
+ # @param config [Phraos::Kube::Config]
12
+ # @return [K8s::Client]
13
+ def self.config(config)
14
+ new(Transport.config(config))
15
+ end
16
+
17
+ # @return [K8s::Client]
18
+ def self.in_cluster_config
19
+ new(Transport.in_cluster_config)
20
+ end
21
+
22
+ # @param transport [K8s::Transport]
23
+ def initialize(transport, namespace: nil)
24
+ @transport = transport
25
+ @namespace = namespace
26
+
27
+ @api_clients = {}
28
+ end
29
+
30
+ # @raise [K8s::Error]
31
+ # @return [K8s::API::Version]
32
+ def version
33
+ @transport.get('/version',
34
+ response_class: K8s::API::Version,
35
+ )
36
+ end
37
+
38
+ # @param api_version [String] "group/version" or "version" (core)
39
+ # @return [APIClient]
40
+ def api(api_version = 'v1')
41
+ @api_clients[api_version] ||= APIClient.new(@transport, api_version)
42
+ end
43
+
44
+ # Force-update /apis cache.
45
+ # Required if creating new CRDs/apiservices.
46
+ #
47
+ # @return [Array<String>]
48
+ def api_groups!
49
+ @api_groups = @transport.get('/apis',
50
+ response_class: K8s::API::MetaV1::APIGroupList,
51
+ ).groups.map{|api_group| api_group.preferredVersion.groupVersion }
52
+ end
53
+
54
+ # Cached /apis preferred group apiVersions
55
+ # @return [Array<String>]
56
+ def api_groups
57
+ @api_groups || api_groups!
58
+ end
59
+
60
+ # @param api_versions [Array<String>] defaults to all APIs
61
+ # @return [Array<APIClient>]
62
+ def apis(api_versions = nil, prefetch_resources: false)
63
+ api_versions ||= ['v1'] + self.api_groups
64
+
65
+ if prefetch_resources
66
+ # api groups that are missing their api_resources
67
+ api_paths = api_versions
68
+ .select{|api_version| !api(api_version).api_resources? }
69
+ .map{|api_version| APIClient.path(api_version) }
70
+
71
+ # load into APIClient.api_resources=
72
+ @transport.gets(*api_paths, response_class: K8s::API::MetaV1::APIResourceList).each do |api_resource_list|
73
+ api(api_resource_list.groupVersion).api_resources = api_resource_list.resources
74
+ end
75
+ end
76
+
77
+ api_versions.map{|api_version| api(api_version) }
78
+ end
79
+
80
+ # @param namespace [String, nil]
81
+ # @return [Array<K8s::ResourceClient>]
82
+ def resources(namespace: nil)
83
+ apis(prefetch_resources: true).map { |api| api.resources(namespace: namespace) }.flatten
84
+ end
85
+
86
+ # Pipeline list requests for multiple resource types.
87
+ #
88
+ # Returns flattened array with mixed resource kinds.
89
+ #
90
+ # @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
91
+ # @param **options @see [K8s::ResourceClient#list]
92
+ # @return [Array<K8s::Resource>]
93
+ def list_resources(resources = nil, **options)
94
+ resources ||= self.resources.select{|resource| resource.list? }
95
+
96
+ ResourceClient.list(resources, @transport, **options)
97
+ end
98
+
99
+ # @param resource [K8s::Resource]
100
+ # @param namespace [String, nil] default if resource is missing namespace
101
+ # @raise [K8s::Error] unknown resource
102
+ # @return [K8s::ResourceClient]
103
+ def client_for_resource(resource, namespace: nil)
104
+ api(resource.apiVersion).client_for_resource(resource, namespace: namespace)
105
+ end
106
+
107
+ # @param resource [K8s::Resource]
108
+ # @return [K8s::Resource]
109
+ def create_resource(resource)
110
+ client_for_resource(resource).create_resource(resource)
111
+ end
112
+
113
+ # @param resource [K8s::Resource]
114
+ # @return [K8s::Resource]
115
+ def get_resource(resource)
116
+ client_for_resource(resource).get_resource(resource)
117
+ end
118
+
119
+ # @param resources [Array<K8s::Resource>]
120
+ # @return [Array<K8s::Resource, nil>]
121
+ def get_resources(resources)
122
+ # prefetch api resources
123
+ apis(resources.map{|resource| resource.apiVersion }.uniq, prefetch_resources: true)
124
+
125
+ resource_clients = resources.map{|resource| client_for_resource(resource) }
126
+ requests = resources.zip(resource_clients).map{|resource, resource_client|
127
+ {
128
+ method: 'GET',
129
+ path: resource_client.path(resource.metadata.name, namespace: resource.metadata.namespace),
130
+ response_class: resource_client.resource_class,
131
+ }
132
+ }
133
+ responses = @transport.requests(*requests, skip_missing: true)
134
+ end
135
+
136
+ # @param resource [K8s::Resource]
137
+ # @return [K8s::Resource]
138
+ def update_resource(resource)
139
+ client_for_resource(resource).update_resource(resource)
140
+ end
141
+
142
+ # @param resource [K8s::Resource]
143
+ # @return [K8s::Resource]
144
+ def delete_resource(resource)
145
+ client_for_resource(resource).delete_resource(resource)
146
+ end
147
+ end
148
+ end