k8s-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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