k8s-ruby 0.10.5
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 +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.relaxed.yml +176 -0
- data/.rubocop.yml +57 -0
- data/Dockerfile +11 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +214 -0
- data/Rakefile +16 -0
- data/bin/k8s-client +337 -0
- data/docker-compose.yaml +10 -0
- data/k8s-ruby.gemspec +40 -0
- data/lib/k8s-ruby.rb +4 -0
- data/lib/k8s/api.rb +31 -0
- data/lib/k8s/api/metav1.rb +26 -0
- data/lib/k8s/api/metav1/api_group.rb +26 -0
- data/lib/k8s/api/metav1/api_resource.rb +26 -0
- data/lib/k8s/api/metav1/list.rb +20 -0
- data/lib/k8s/api/metav1/object.rb +53 -0
- data/lib/k8s/api/metav1/status.rb +34 -0
- data/lib/k8s/api/metav1/watch_event.rb +20 -0
- data/lib/k8s/api/version.rb +17 -0
- data/lib/k8s/api_client.rb +116 -0
- data/lib/k8s/client.rb +283 -0
- data/lib/k8s/config.rb +208 -0
- data/lib/k8s/error.rb +66 -0
- data/lib/k8s/logging.rb +87 -0
- data/lib/k8s/resource.rb +117 -0
- data/lib/k8s/resource_client.rb +349 -0
- data/lib/k8s/ruby/version.rb +8 -0
- data/lib/k8s/stack.rb +186 -0
- data/lib/k8s/transport.rb +385 -0
- data/lib/k8s/util.rb +139 -0
- metadata +265 -0
data/docker-compose.yaml
ADDED
data/k8s-ruby.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "k8s/ruby/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "k8s-ruby"
|
8
|
+
spec.version = K8s::Ruby::VERSION
|
9
|
+
spec.authors = ["rdx.net", "Kontena, Inc."]
|
10
|
+
spec.email = ["firstname.lastname@rdx.net"]
|
11
|
+
spec.license = "Apache-2.0"
|
12
|
+
|
13
|
+
spec.summary = "Kubernetes client library for Ruby"
|
14
|
+
spec.homepage = "https://github.com/rdxnet/k8s-ruby"
|
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.71"
|
27
|
+
spec.add_runtime_dependency "dry-struct", "~> 0.5.0"
|
28
|
+
spec.add_runtime_dependency "dry-types", "~> 0.13.0"
|
29
|
+
spec.add_runtime_dependency "recursive-open-struct", "~> 1.1.0"
|
30
|
+
spec.add_runtime_dependency 'hashdiff', '~> 1.0.0'
|
31
|
+
spec.add_runtime_dependency 'jsonpath', '~> 0.9.5'
|
32
|
+
spec.add_runtime_dependency 'yajl-ruby', '~> 1.4.0'
|
33
|
+
spec.add_runtime_dependency "yaml-safe_load_stream", "~> 0.1"
|
34
|
+
|
35
|
+
spec.add_development_dependency "bundler", ">= 1.17", "< 3.0"
|
36
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.7"
|
38
|
+
spec.add_development_dependency "webmock", "~> 3.6.2"
|
39
|
+
spec.add_development_dependency "rubocop", "~> 0.59"
|
40
|
+
end
|
data/lib/k8s-ruby.rb
ADDED
data/lib/k8s/api.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-types'
|
4
|
+
require 'dry-struct'
|
5
|
+
|
6
|
+
module K8s
|
7
|
+
# Kube API definitions
|
8
|
+
module API
|
9
|
+
# Common Dry::Types used in the API
|
10
|
+
module Types
|
11
|
+
include Dry::Types.module
|
12
|
+
end
|
13
|
+
|
14
|
+
# Common API struct type, handling JSON transforms with symbol keys
|
15
|
+
class Struct < Dry::Struct
|
16
|
+
# input from JSON with string keys
|
17
|
+
transform_keys(&:to_sym)
|
18
|
+
|
19
|
+
# @param data [Hash]
|
20
|
+
# @return [self]
|
21
|
+
def self.from_json(data)
|
22
|
+
new(Yajl::Parser.parse(data))
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
def to_json(*_args)
|
27
|
+
to_hash.to_json
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'k8s/api'
|
4
|
+
|
5
|
+
module K8s
|
6
|
+
module API
|
7
|
+
# API types common to all apigroups/versions
|
8
|
+
#
|
9
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1
|
10
|
+
module MetaV1
|
11
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta
|
12
|
+
class Resource < Struct
|
13
|
+
# XXX: making these optional seems dangerous, but some APIs (GET /api/v1) are missing these
|
14
|
+
attribute :kind, Types::Strict::String.optional.default(nil)
|
15
|
+
attribute :apiVersion, Types::Strict::String.optional.default(nil)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'k8s/api/metav1/api_group'
|
22
|
+
require 'k8s/api/metav1/api_resource'
|
23
|
+
require 'k8s/api/metav1/list'
|
24
|
+
require 'k8s/api/metav1/object'
|
25
|
+
require 'k8s/api/metav1/status'
|
26
|
+
require 'k8s/api/metav1/watch_event'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
module API
|
5
|
+
module MetaV1
|
6
|
+
# structured list of available APIGroup versions
|
7
|
+
# groupVersion provided for convenience
|
8
|
+
class APIGroupVersion < Struct
|
9
|
+
attribute :groupVersion, Types::Strict::String
|
10
|
+
attribute :version, Types::Strict::String
|
11
|
+
end
|
12
|
+
|
13
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIGroup
|
14
|
+
class APIGroup < Struct
|
15
|
+
attribute :name, Types::Strict::String
|
16
|
+
attribute :versions, Types::Strict::Array.of(APIGroupVersion)
|
17
|
+
attribute :preferredVersion, APIGroupVersion
|
18
|
+
end
|
19
|
+
|
20
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIGroupList
|
21
|
+
class APIGroupList < Resource
|
22
|
+
attribute :groups, Types::Strict::Array.of(APIGroup)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
module API
|
5
|
+
module MetaV1
|
6
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIResource
|
7
|
+
class APIResource < Struct
|
8
|
+
attribute :name, Types::Strict::String
|
9
|
+
attribute :singularName, Types::Strict::String
|
10
|
+
attribute :namespaced, Types::Strict::Bool
|
11
|
+
attribute :group, Types::Strict::String.optional.default(nil)
|
12
|
+
attribute :version, Types::Strict::String.optional.default(nil)
|
13
|
+
attribute :kind, Types::Strict::String
|
14
|
+
attribute :verbs, Types::Strict::Array.of(Types::Strict::String)
|
15
|
+
attribute :shortNames, Types::Strict::Array.of(Types::Strict::String).optional.default(proc { [] })
|
16
|
+
attribute :categories, Types::Strict::Array.of(Types::Strict::String).optional.default(proc { [] })
|
17
|
+
end
|
18
|
+
|
19
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIResourceList
|
20
|
+
class APIResourceList < Resource
|
21
|
+
attribute :groupVersion, Types::Strict::String
|
22
|
+
attribute :resources, Types::Strict::Array.of(APIResource)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
module API
|
5
|
+
module MetaV1
|
6
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ListMeta
|
7
|
+
class ListMeta < Struct
|
8
|
+
attribute :selfLink, Types::Strict::String.optional.default(nil)
|
9
|
+
attribute :resourceVersion, Types::Strict::String.optional.default(nil)
|
10
|
+
attribute :continue, Types::Strict::String.optional.default(nil)
|
11
|
+
end
|
12
|
+
|
13
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#List
|
14
|
+
class List < Resource
|
15
|
+
attribute :metadata, ListMeta
|
16
|
+
attribute :items, Types::Strict::Array # untyped
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'k8s/api/metav1/status'
|
4
|
+
|
5
|
+
module K8s
|
6
|
+
module API
|
7
|
+
module MetaV1
|
8
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#OwnerReference
|
9
|
+
class OwnerReference < Resource
|
10
|
+
attribute :name, Types::Strict::String
|
11
|
+
attribute :uid, Types::Strict::String
|
12
|
+
attribute :controller, Types::Strict::Bool.optional.default(nil)
|
13
|
+
attribute :blockOwnerDeletion, Types::Strict::Bool.optional.default(nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Initializer
|
17
|
+
class Initializer < Struct
|
18
|
+
attribute :name, Types::Strict::String
|
19
|
+
end
|
20
|
+
|
21
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Initializers
|
22
|
+
class Initializers < Struct
|
23
|
+
attribute :pending, Initializer
|
24
|
+
attribute :result, Status.optional.default(nil)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMeta
|
28
|
+
class ObjectMeta < Resource
|
29
|
+
attribute :name, Types::Strict::String.optional.default(nil)
|
30
|
+
attribute :generateName, Types::Strict::String.optional.default(nil)
|
31
|
+
attribute :namespace, Types::Strict::String.optional.default(nil)
|
32
|
+
attribute :selfLink, Types::Strict::String.optional.default(nil)
|
33
|
+
attribute :uid, Types::Strict::String.optional.default(nil)
|
34
|
+
attribute :resourceVersion, Types::Strict::String.optional.default(nil)
|
35
|
+
attribute :generation, Types::Strict::Integer.optional.default(nil)
|
36
|
+
attribute :creationTimestamp, Types::DateTime.optional.default(nil)
|
37
|
+
attribute :deletionTimestamp, Types::DateTime.optional.default(nil)
|
38
|
+
attribute :deletionGracePeriodSeconds, Types::Strict::Integer.optional.default(nil)
|
39
|
+
attribute :labels, Types::Strict::Hash.map(Types::Strict::String, Types::Strict::String).optional.default(nil)
|
40
|
+
attribute :annotations, Types::Strict::Hash.map(Types::Strict::String, Types::Strict::String).optional.default(nil)
|
41
|
+
attribute :ownerReferences, Types::Strict::Array.of(OwnerReference).optional.default([])
|
42
|
+
attribute :initializers, Initializers.optional.default(nil)
|
43
|
+
attribute :finalizers, Types::Strict::Array.of(Types::Strict::String).optional.default([])
|
44
|
+
attribute :clusterName, Types::Strict::String.optional.default(nil)
|
45
|
+
end
|
46
|
+
|
47
|
+
# common attributes shared by all object types
|
48
|
+
class ObjectCommon < Resource
|
49
|
+
attribute :metadata, ObjectMeta.optional.default(nil)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
module API
|
5
|
+
module MetaV1
|
6
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Status
|
7
|
+
class Status < Resource
|
8
|
+
# structured cause
|
9
|
+
class Cause < Struct
|
10
|
+
attribute :reason, Types::Strict::String.optional.default(nil)
|
11
|
+
attribute :message, Types::Strict::String.optional.default(nil) # human-readable
|
12
|
+
attribute :field, Types::Strict::String.optional.default(nil) # human-readable
|
13
|
+
end
|
14
|
+
|
15
|
+
# structured details
|
16
|
+
class Details < Struct
|
17
|
+
attribute :name, Types::Strict::String.optional.default(nil)
|
18
|
+
attribute :group, Types::Strict::String.optional.default(nil)
|
19
|
+
attribute :kind, Types::Strict::String.optional.default(nil)
|
20
|
+
attribute :uid, Types::Strict::String.optional.default(nil)
|
21
|
+
attribute :causes, Types::Strict::Array.of(Cause).optional.default(nil)
|
22
|
+
attribute :retryAfterSeconds, Types::Strict::Integer.optional.default(nil)
|
23
|
+
end
|
24
|
+
|
25
|
+
attribute :metadata, ListMeta
|
26
|
+
attribute :status, Types::Strict::String.enum('Success', 'Failure').optional.default(nil)
|
27
|
+
attribute :message, Types::Strict::String.optional.default(nil) # human-readable
|
28
|
+
attribute :reason, Types::Strict::String.optional.default(nil) # machine-readable
|
29
|
+
attribute :details, Details.optional.default(nil)
|
30
|
+
attribute :code, Types::Strict::Integer.optional.default(nil)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../resource'
|
4
|
+
|
5
|
+
module K8s
|
6
|
+
module API
|
7
|
+
module MetaV1
|
8
|
+
# @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#WatchEvent
|
9
|
+
class WatchEvent < Resource
|
10
|
+
attribute :type, Types::Strict::String
|
11
|
+
attribute :object, Types::Strict::Hash
|
12
|
+
|
13
|
+
# @return [K8s::Resource]
|
14
|
+
def resource
|
15
|
+
@resource ||= K8s::Resource.new(object)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
module API
|
5
|
+
# GET /version
|
6
|
+
class Version < Struct
|
7
|
+
attribute :buildDate, Types::Strict::String # TODO: parse datetime?
|
8
|
+
attribute :compiler, Types::Strict::String
|
9
|
+
attribute :gitCommit, Types::Strict::String
|
10
|
+
attribute :gitTreeState, Types::Strict::String
|
11
|
+
attribute :gitVersion, Types::Strict::String
|
12
|
+
attribute :major, Types::Strict::String
|
13
|
+
attribute :minor, Types::Strict::String
|
14
|
+
attribute :platform, Types::Strict::String
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
# Per-APIGroup/version client.
|
5
|
+
#
|
6
|
+
# Offers access to {ResourceClient} instances for the APIResource types defined in this apigroup/version
|
7
|
+
class APIClient
|
8
|
+
# @param api_version [String] either core version (v1) or apigroup/apiversion (apps/v1)
|
9
|
+
# @return [String]
|
10
|
+
def self.path(api_version)
|
11
|
+
if api_version.include? '/'
|
12
|
+
File.join('/apis', api_version)
|
13
|
+
else
|
14
|
+
File.join('/api', api_version)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param transport [K8s::Transport]
|
19
|
+
# @param api_version [String] "group/version" or "version" (core)
|
20
|
+
def initialize(transport, api_version)
|
21
|
+
@transport = transport
|
22
|
+
@api_version = api_version
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
attr_reader :api_version
|
27
|
+
|
28
|
+
# @param path [Array<String>] join path from parts
|
29
|
+
# @return [String]
|
30
|
+
def path(*path)
|
31
|
+
@transport.path(self.class.path(@api_version), *path)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Bool] loaded yet?
|
35
|
+
def api_resources?
|
36
|
+
!!@api_resources
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param api_resources [Array<K8s::API::MetaV1::APIResource>]
|
40
|
+
attr_writer :api_resources
|
41
|
+
|
42
|
+
# Force-update APIResources
|
43
|
+
#
|
44
|
+
# @return [Array<K8s::API::MetaV1::APIResource>]
|
45
|
+
def api_resources!
|
46
|
+
@api_resources = @transport.get(path,
|
47
|
+
response_class: K8s::API::MetaV1::APIResourceList).resources
|
48
|
+
end
|
49
|
+
|
50
|
+
# Cached APIResources
|
51
|
+
#
|
52
|
+
# @return [Array<K8s::API::MetaV1::APIResource>]
|
53
|
+
def api_resources
|
54
|
+
@api_resources || api_resources!
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param resource_name [String]
|
58
|
+
# @raise [K8s::Error::UndefinedResource]
|
59
|
+
# @return [K8s::API::MetaV1::APIResource]
|
60
|
+
def find_api_resource(resource_name)
|
61
|
+
found_resource = api_resources.find{ |api_resource| api_resource.name == resource_name }
|
62
|
+
found_resource ||= api_resources!.find{ |api_resource| api_resource.name == resource_name }
|
63
|
+
raise K8s::Error::UndefinedResource, "Unknown resource #{resource_name} for #{@api_version}" unless found_resource
|
64
|
+
|
65
|
+
found_resource
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param resource_name [String]
|
69
|
+
# @param namespace [String, nil]
|
70
|
+
# @raise [K8s::Error::UndefinedResource]
|
71
|
+
# @return [K8s::ResourceClient]
|
72
|
+
def resource(resource_name, namespace: nil)
|
73
|
+
ResourceClient.new(@transport, self, find_api_resource(resource_name), namespace: namespace)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param resource [K8s::Resource]
|
77
|
+
# @param namespace [String, nil] default if resource is missing namespace
|
78
|
+
# @raise [K8s::Error::UndefinedResource]
|
79
|
+
# @return [K8s::ResourceClient]
|
80
|
+
def client_for_resource(resource, namespace: nil)
|
81
|
+
unless @api_version == resource.apiVersion
|
82
|
+
raise K8s::Error::UndefinedResource, "Invalid apiVersion=#{resource.apiVersion} for #{@api_version} client"
|
83
|
+
end
|
84
|
+
|
85
|
+
found_resource = api_resources.find{ |api_resource| api_resource.kind == resource.kind }
|
86
|
+
found_resource ||= api_resources!.find{ |api_resource| api_resource.kind == resource.kind }
|
87
|
+
raise K8s::Error::UndefinedResource, "Unknown resource kind=#{resource.kind} for #{@api_version}" unless found_resource
|
88
|
+
|
89
|
+
ResourceClient.new(@transport, self, found_resource, namespace: resource.metadata.namespace || namespace)
|
90
|
+
end
|
91
|
+
|
92
|
+
# TODO: skip non-namespaced resources if namespace is given, or ignore namespace?
|
93
|
+
#
|
94
|
+
# @param namespace [String, nil]
|
95
|
+
# @return [Array<K8s::ResourceClient>]
|
96
|
+
def resources(namespace: nil)
|
97
|
+
api_resources.map{ |api_resource|
|
98
|
+
ResourceClient.new(@transport, self, api_resource,
|
99
|
+
namespace: namespace)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Pipeline list requests for multiple resource types.
|
104
|
+
#
|
105
|
+
# Returns flattened array with mixed resource kinds.
|
106
|
+
#
|
107
|
+
# @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
|
108
|
+
# @param options @see [K8s::ResourceClient#list]
|
109
|
+
# @return [Array<K8s::Resource>]
|
110
|
+
def list_resources(resources = nil, **options)
|
111
|
+
resources ||= self.resources.select(&:list?)
|
112
|
+
|
113
|
+
ResourceClient.list(resources, @transport, **options)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/k8s/client.rb
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'base64'
|
5
|
+
require 'yajl'
|
6
|
+
require 'monitor'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
require 'k8s/util'
|
10
|
+
|
11
|
+
require 'k8s/api/metav1'
|
12
|
+
require 'k8s/api/version'
|
13
|
+
require 'k8s/config'
|
14
|
+
require 'k8s/logging'
|
15
|
+
require 'k8s/api_client'
|
16
|
+
require "k8s/error"
|
17
|
+
require 'k8s/resource'
|
18
|
+
require 'k8s/resource_client'
|
19
|
+
require 'k8s/stack'
|
20
|
+
require 'k8s/transport'
|
21
|
+
|
22
|
+
module K8s
|
23
|
+
# @param server [String] http/s URL
|
24
|
+
# @param options [Hash] @see Transport.new
|
25
|
+
# @return [K8s::Client]
|
26
|
+
def self.client(server, **options)
|
27
|
+
Client.new(Transport.new(server, **options))
|
28
|
+
end
|
29
|
+
|
30
|
+
# Top-level client wrapper.
|
31
|
+
# Uses a {Transport} instance to talk to the kube API.
|
32
|
+
# Offers access to {APIClient} and {ResourceClient} instances.
|
33
|
+
class Client
|
34
|
+
# @param config [Phraos::Kube::Config]
|
35
|
+
# @param namespace [String] @see #initialize
|
36
|
+
# @param options [Hash] @see Transport.config
|
37
|
+
# @return [K8s::Client]
|
38
|
+
def self.config(config, namespace: nil, **options)
|
39
|
+
new(
|
40
|
+
Transport.config(config, **options),
|
41
|
+
namespace: namespace
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# An K8s::Client instance from in-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
|
46
|
+
# @see K8s::Transport#in_cluster_config
|
47
|
+
#
|
48
|
+
# @param namespace [String] default namespace for all operations
|
49
|
+
# @param options [Hash] options passed to transport, @see Transport#in_cluster_config
|
50
|
+
# @return [K8s::Client]
|
51
|
+
# @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
|
52
|
+
def self.in_cluster_config(namespace: nil, **options)
|
53
|
+
new(Transport.in_cluster_config(**options), namespace: namespace)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Attempts to create a K8s::Client instance automatically using environment variables, existing configuration
|
57
|
+
# files or in cluster configuration.
|
58
|
+
#
|
59
|
+
# Look-up order:
|
60
|
+
# - KUBE_TOKEN, KUBE_CA, KUBE_SERVER environment variables
|
61
|
+
# - KUBECONFIG environment variable
|
62
|
+
# - $HOME/.kube/config file
|
63
|
+
# - In cluster configuration
|
64
|
+
#
|
65
|
+
# Will raise when no means of configuration is available
|
66
|
+
#
|
67
|
+
# @param options [Hash] default namespace for all operations
|
68
|
+
# @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
|
69
|
+
# @return [K8s::Client]
|
70
|
+
def self.autoconfig(namespace: nil, **options)
|
71
|
+
if ENV.values_at('KUBE_TOKEN', 'KUBE_CA', 'KUBE_SERVER').none? { |v| v.nil? || v.empty? }
|
72
|
+
unless Base64.decode64(ENV['KUBE_CA']).match?(/CERTIFICATE/)
|
73
|
+
raise ArgumentError, 'KUBE_CA does not seem to be base64 encoded'
|
74
|
+
end
|
75
|
+
|
76
|
+
begin
|
77
|
+
token = options[:auth_token] || Base64.strict_decode64(ENV['KUBE_TOKEN'])
|
78
|
+
rescue ArgumentError
|
79
|
+
raise ArgumentError, 'KUBE_TOKEN does not seem to be base64 encoded'
|
80
|
+
end
|
81
|
+
|
82
|
+
configuration = K8s::Config.build(server: ENV['KUBE_SERVER'], ca: ENV['KUBE_CA'], auth_token: token)
|
83
|
+
elsif !ENV['KUBECONFIG'].to_s.empty?
|
84
|
+
configuration = K8s::Config.from_kubeconfig_env(ENV['KUBECONFIG'])
|
85
|
+
else
|
86
|
+
found_config = [
|
87
|
+
File.join(Dir.home, '.kube', 'config'),
|
88
|
+
'/etc/kubernetes/admin.conf',
|
89
|
+
'/etc/kubernetes/kubelet.conf'
|
90
|
+
].find { |f| File.exist?(f) && File.readable?(f) }
|
91
|
+
|
92
|
+
configuration = K8s::Config.load_file(found_config) if found_config
|
93
|
+
end
|
94
|
+
|
95
|
+
if configuration
|
96
|
+
config(configuration, namespace: namespace, **options)
|
97
|
+
else
|
98
|
+
in_cluster_config(namespace: namespace, **options)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
include MonitorMixin
|
103
|
+
|
104
|
+
attr_reader :transport
|
105
|
+
|
106
|
+
# @param transport [K8s::Transport]
|
107
|
+
# @param namespace [String, nil] default namespace for all operations
|
108
|
+
def initialize(transport, namespace: nil)
|
109
|
+
@transport = transport
|
110
|
+
@namespace = namespace
|
111
|
+
|
112
|
+
@api_clients = {}
|
113
|
+
super()
|
114
|
+
end
|
115
|
+
|
116
|
+
# @raise [K8s::Error]
|
117
|
+
# @return [K8s::API::Version]
|
118
|
+
def version
|
119
|
+
@version ||= @transport.version
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param api_version [String] "group/version" or "version" (core)
|
123
|
+
# @return [APIClient]
|
124
|
+
def api(api_version = 'v1')
|
125
|
+
@api_clients[api_version] ||= APIClient.new(@transport, api_version)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Force-update /apis cache.
|
129
|
+
# Required if creating new CRDs/apiservices.
|
130
|
+
#
|
131
|
+
# @return [Array<String>]
|
132
|
+
def api_groups!
|
133
|
+
synchronize do
|
134
|
+
@api_groups = @transport.get(
|
135
|
+
'/apis',
|
136
|
+
response_class: K8s::API::MetaV1::APIGroupList
|
137
|
+
).groups.flat_map{ |api_group| api_group.versions.map(&:groupVersion) }
|
138
|
+
|
139
|
+
@api_clients.clear
|
140
|
+
end
|
141
|
+
|
142
|
+
@api_groups
|
143
|
+
end
|
144
|
+
|
145
|
+
# Cached /apis preferred group apiVersions
|
146
|
+
# @return [Array<String>]
|
147
|
+
def api_groups
|
148
|
+
@api_groups || api_groups!
|
149
|
+
end
|
150
|
+
|
151
|
+
# @param api_versions [Array<String>] defaults to all APIs
|
152
|
+
# @param prefetch_resources [Boolean] prefetch any missing api_resources for each api_version
|
153
|
+
# @param skip_missing [Boolean] return APIClient without api_resources? if 404
|
154
|
+
# @return [Array<APIClient>]
|
155
|
+
def apis(api_versions = nil, prefetch_resources: false, skip_missing: false)
|
156
|
+
api_versions ||= ['v1'] + api_groups
|
157
|
+
|
158
|
+
if prefetch_resources
|
159
|
+
# api groups that are missing their api_resources
|
160
|
+
api_paths = api_versions
|
161
|
+
.uniq
|
162
|
+
.reject{ |api_version| api(api_version).api_resources? }
|
163
|
+
.map{ |api_version| APIClient.path(api_version) }
|
164
|
+
|
165
|
+
# load into APIClient.api_resources=
|
166
|
+
begin
|
167
|
+
@transport.gets(*api_paths, response_class: K8s::API::MetaV1::APIResourceList, skip_missing: skip_missing).each do |api_resource_list|
|
168
|
+
api(api_resource_list.groupVersion).api_resources = api_resource_list.resources if api_resource_list
|
169
|
+
end
|
170
|
+
rescue K8s::Error::NotFound, K8s::Error::ServiceUnavailable # rubocop:disable Lint/HandleExceptions
|
171
|
+
# kubernetes api is in unstable state
|
172
|
+
# because this is only performance optimization, better to skip prefetch and move on
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
api_versions.map{ |api_version| api(api_version) }
|
177
|
+
end
|
178
|
+
|
179
|
+
# @param namespace [String, nil]
|
180
|
+
# @return [Array<K8s::ResourceClient>]
|
181
|
+
def resources(namespace: nil)
|
182
|
+
apis(prefetch_resources: true).map { |api|
|
183
|
+
begin
|
184
|
+
api.resources(namespace: namespace)
|
185
|
+
rescue K8s::Error::ServiceUnavailable, K8s::Error::NotFound
|
186
|
+
[]
|
187
|
+
end
|
188
|
+
}.flatten
|
189
|
+
end
|
190
|
+
|
191
|
+
# Pipeline list requests for multiple resource types.
|
192
|
+
#
|
193
|
+
# Returns flattened array with mixed resource kinds.
|
194
|
+
#
|
195
|
+
# @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
|
196
|
+
# @param options @see K8s::ResourceClient#list
|
197
|
+
# @return [Array<K8s::Resource>]
|
198
|
+
def list_resources(resources = nil, **options)
|
199
|
+
cached_clients = @api_clients.size.positive?
|
200
|
+
resources ||= self.resources.select(&:list?)
|
201
|
+
|
202
|
+
begin
|
203
|
+
ResourceClient.list(resources, @transport, **options)
|
204
|
+
rescue K8s::Error::NotFound
|
205
|
+
raise unless cached_clients
|
206
|
+
|
207
|
+
cached_clients = false
|
208
|
+
api_groups!
|
209
|
+
retry
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# @param resource [K8s::Resource]
|
214
|
+
# @param namespace [String, nil] default if resource is missing namespace
|
215
|
+
# @raise [K8s::Error::NotFound] API Group does not exist
|
216
|
+
# @raise [K8s::Error::UndefinedResource]
|
217
|
+
# @return [K8s::ResourceClient]
|
218
|
+
def client_for_resource(resource, namespace: nil)
|
219
|
+
api(resource.apiVersion).client_for_resource(resource, namespace: namespace)
|
220
|
+
end
|
221
|
+
|
222
|
+
# @param resource [K8s::Resource]
|
223
|
+
# @return [K8s::Resource]
|
224
|
+
def create_resource(resource)
|
225
|
+
client_for_resource(resource).create_resource(resource)
|
226
|
+
end
|
227
|
+
|
228
|
+
# @param resource [K8s::Resource]
|
229
|
+
# @return [K8s::Resource]
|
230
|
+
def get_resource(resource)
|
231
|
+
client_for_resource(resource).get_resource(resource)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Returns nils for any resources that do not exist.
|
235
|
+
# This includes custom resources that were not yet defined.
|
236
|
+
#
|
237
|
+
# @param resources [Array<K8s::Resource>]
|
238
|
+
# @return [Array<K8s::Resource, nil>] matching resources array 1:1
|
239
|
+
def get_resources(resources)
|
240
|
+
# prefetch api resources, skip missing APIs
|
241
|
+
resource_apis = apis(resources.map(&:apiVersion), prefetch_resources: true, skip_missing: true)
|
242
|
+
|
243
|
+
# map each resource to excon request options, or nil if resource is not (yet) defined
|
244
|
+
requests = resources.zip(resource_apis).map{ |resource, api_client|
|
245
|
+
next nil unless api_client.api_resources?
|
246
|
+
|
247
|
+
resource_client = api_client.client_for_resource(resource)
|
248
|
+
|
249
|
+
{
|
250
|
+
method: 'GET',
|
251
|
+
path: resource_client.path(resource.metadata.name, namespace: resource.metadata.namespace),
|
252
|
+
response_class: resource_client.resource_class
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
256
|
+
# map non-nil requests to response objects, or nil for nil request options
|
257
|
+
Util.compact_map(requests) { |reqs|
|
258
|
+
@transport.requests(*reqs, skip_missing: true)
|
259
|
+
}
|
260
|
+
end
|
261
|
+
|
262
|
+
# @param resource [K8s::Resource]
|
263
|
+
# @return [K8s::Resource]
|
264
|
+
def update_resource(resource)
|
265
|
+
client_for_resource(resource).update_resource(resource)
|
266
|
+
end
|
267
|
+
|
268
|
+
# @param resource [K8s::Resource]
|
269
|
+
# @param options [Hash]
|
270
|
+
# @see ResourceClient#delete for options
|
271
|
+
# @return [K8s::Resource]
|
272
|
+
def delete_resource(resource, **options)
|
273
|
+
client_for_resource(resource).delete_resource(resource, **options)
|
274
|
+
end
|
275
|
+
|
276
|
+
# @param resource [K8s::Resource]
|
277
|
+
# @param attrs [Hash]
|
278
|
+
# @return [K8s::Client]
|
279
|
+
def patch_resource(resource, attrs)
|
280
|
+
client_for_resource(resource).json_patch(resource.metadata.name, attrs)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|