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/lib/k8s/config.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-struct'
|
4
|
+
require 'dry-types'
|
5
|
+
require 'base64'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module K8s
|
9
|
+
# Common struct type for kubeconfigs:
|
10
|
+
#
|
11
|
+
# * converts string keys to symbols
|
12
|
+
# * normalizes foo-bar to foo_bar
|
13
|
+
class ConfigStruct < Dry::Struct
|
14
|
+
transform_keys do |key|
|
15
|
+
case key
|
16
|
+
when String
|
17
|
+
key.tr('-', '_').to_sym
|
18
|
+
else
|
19
|
+
key
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @see https://godoc.org/k8s.io/client-go/tools/clientcmd/api/v1#Config
|
25
|
+
class Config < ConfigStruct
|
26
|
+
# Common dry-types for config
|
27
|
+
class Types
|
28
|
+
include Dry::Types.module
|
29
|
+
end
|
30
|
+
|
31
|
+
# structured cluster
|
32
|
+
class Cluster < ConfigStruct
|
33
|
+
attribute :server, Types::String
|
34
|
+
attribute :insecure_skip_tls_verify, Types::Bool.optional.default(nil)
|
35
|
+
attribute :certificate_authority, Types::String.optional.default(nil)
|
36
|
+
attribute :certificate_authority_data, Types::String.optional.default(nil)
|
37
|
+
attribute :extensions, Types::Strict::Array.optional.default(nil)
|
38
|
+
end
|
39
|
+
|
40
|
+
# structured cluster with name
|
41
|
+
class NamedCluster < ConfigStruct
|
42
|
+
attribute :name, Types::String
|
43
|
+
attribute :cluster, Cluster
|
44
|
+
end
|
45
|
+
|
46
|
+
# structured user auth provider
|
47
|
+
class UserAuthProvider < ConfigStruct
|
48
|
+
attribute :name, Types::String
|
49
|
+
attribute :config, Types::Strict::Hash
|
50
|
+
end
|
51
|
+
|
52
|
+
# structured user exec
|
53
|
+
class UserExec < ConfigStruct
|
54
|
+
attribute :command, Types::String
|
55
|
+
attribute :apiVersion, Types::String
|
56
|
+
attribute :env, Types::Strict::Array.of(Types::Hash).optional.default(nil)
|
57
|
+
attribute :args, Types::Strict::Array.of(Types::String).optional.default(nil)
|
58
|
+
end
|
59
|
+
|
60
|
+
# structured user
|
61
|
+
class User < ConfigStruct
|
62
|
+
attribute :client_certificate, Types::String.optional.default(nil)
|
63
|
+
attribute :client_certificate_data, Types::String.optional.default(nil)
|
64
|
+
attribute :client_key, Types::String.optional.default(nil)
|
65
|
+
attribute :client_key_data, Types::String.optional.default(nil)
|
66
|
+
attribute :token, Types::String.optional.default(nil)
|
67
|
+
attribute :tokenFile, Types::String.optional.default(nil)
|
68
|
+
attribute :as, Types::String.optional.default(nil)
|
69
|
+
attribute :as_groups, Types::Array.of(Types::String).optional.default(nil)
|
70
|
+
attribute :as_user_extra, Types::Hash.optional.default(nil)
|
71
|
+
attribute :username, Types::String.optional.default(nil)
|
72
|
+
attribute :password, Types::String.optional.default(nil)
|
73
|
+
attribute :auth_provider, UserAuthProvider.optional.default(nil)
|
74
|
+
attribute :exec, UserExec.optional.default(nil)
|
75
|
+
attribute :extensions, Types::Strict::Array.optional.default(nil)
|
76
|
+
end
|
77
|
+
|
78
|
+
# structured user with name
|
79
|
+
class NamedUser < ConfigStruct
|
80
|
+
attribute :name, Types::String
|
81
|
+
attribute :user, User
|
82
|
+
end
|
83
|
+
|
84
|
+
# structured context
|
85
|
+
#
|
86
|
+
# Referrs to other named User/cluster objects within the same config.
|
87
|
+
class Context < ConfigStruct
|
88
|
+
attribute :cluster, Types::Strict::String
|
89
|
+
attribute :user, Types::Strict::String
|
90
|
+
attribute :namespace, Types::Strict::String.optional.default(nil)
|
91
|
+
attribute :extensions, Types::Strict::Array.optional.default(nil)
|
92
|
+
end
|
93
|
+
|
94
|
+
# named context
|
95
|
+
class NamedContext < ConfigStruct
|
96
|
+
attribute :name, Types::String
|
97
|
+
attribute :context, Context
|
98
|
+
end
|
99
|
+
|
100
|
+
attribute :kind, Types::Strict::String.optional.default(nil)
|
101
|
+
attribute :apiVersion, Types::Strict::String.optional.default(nil)
|
102
|
+
attribute :preferences, Types::Strict::Hash.optional.default(proc { {} })
|
103
|
+
attribute :clusters, Types::Strict::Array.of(NamedCluster).optional.default(proc { [] })
|
104
|
+
attribute :users, Types::Strict::Array.of(NamedUser).optional.default(proc { [] })
|
105
|
+
attribute :contexts, Types::Strict::Array.of(NamedContext).optional.default(proc { [] })
|
106
|
+
attribute :current_context, Types::Strict::String.optional.default(nil)
|
107
|
+
attribute :extensions, Types::Strict::Array.optional.default(proc { [] })
|
108
|
+
|
109
|
+
# Loads a configuration from a YAML file
|
110
|
+
#
|
111
|
+
# @param path [String]
|
112
|
+
# @return [K8s::Config]
|
113
|
+
def self.load_file(path)
|
114
|
+
new(YAML.safe_load(File.read(File.expand_path(path)), [Time, DateTime, Date], [], true))
|
115
|
+
end
|
116
|
+
|
117
|
+
# Loads configuration files listed in KUBE_CONFIG environment variable and
|
118
|
+
# merge using the configuration merge rules, @see K8s::Config.merge
|
119
|
+
#
|
120
|
+
# @param kubeconfig [String] by default read from ENV['KUBECONFIG']
|
121
|
+
def self.from_kubeconfig_env(kubeconfig = nil)
|
122
|
+
kubeconfig ||= ENV.fetch('KUBECONFIG', '')
|
123
|
+
raise ArgumentError, "KUBECONFIG not set" if kubeconfig.empty?
|
124
|
+
|
125
|
+
paths = kubeconfig.split(/(?!\\):/)
|
126
|
+
|
127
|
+
paths.inject(load_file(paths.shift)) do |memo, other_cfg|
|
128
|
+
memo.merge(load_file(other_cfg))
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Build a minimal configuration from at least a server address, server certificate authority data and an access token.
|
133
|
+
#
|
134
|
+
# @param server [String] kubernetes server address
|
135
|
+
# @param ca [String] server certificate authority data (base64 encoded)
|
136
|
+
# @param token [String] access token
|
137
|
+
# @param cluster_name [String] cluster name
|
138
|
+
# @param user [String] user name
|
139
|
+
# @param context [String] context name
|
140
|
+
# @param options [Hash] (see #initialize)
|
141
|
+
def self.build(server:, ca:, auth_token:, cluster_name: 'kubernetes', user: 'k8s-client', context: 'k8s-client', **options)
|
142
|
+
new(
|
143
|
+
{
|
144
|
+
clusters: [{ name: cluster_name, cluster: { server: server, certificate_authority_data: ca } }],
|
145
|
+
users: [{ name: user, user: { token: auth_token } }],
|
146
|
+
contexts: [{ name: context, context: { cluster: cluster_name, user: user } }],
|
147
|
+
current_context: context
|
148
|
+
}.merge(options)
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Merges configuration according to the rules specified in
|
153
|
+
# https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files
|
154
|
+
#
|
155
|
+
# @param other [Hash, K8s::Config]
|
156
|
+
# @return [K8s::Config]
|
157
|
+
def merge(other)
|
158
|
+
old_attributes = attributes
|
159
|
+
other_attributes = other.is_a?(Hash) ? other : other.attributes
|
160
|
+
|
161
|
+
old_attributes.merge!(other_attributes) do |key, old_value, new_value|
|
162
|
+
case key
|
163
|
+
when :clusters, :contexts, :users
|
164
|
+
old_value + new_value.reject do |new_mapping|
|
165
|
+
old_value.any? { |old_mapping| old_mapping[:name] == new_mapping[:name] }
|
166
|
+
end
|
167
|
+
else
|
168
|
+
case old_value
|
169
|
+
when Array
|
170
|
+
(old_value + new_value).uniq
|
171
|
+
when Hash
|
172
|
+
old_value.merge(new_value) do |_key, inner_old_value, inner_new_value|
|
173
|
+
inner_old_value.nil? ? inner_new_value : inner_old_value
|
174
|
+
end
|
175
|
+
when NilClass
|
176
|
+
new_value
|
177
|
+
else
|
178
|
+
old_value
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
self.class.new(old_attributes)
|
184
|
+
end
|
185
|
+
|
186
|
+
# @param name [String]
|
187
|
+
# @raise [K8s::Error::Configuration]
|
188
|
+
# @return [K8s::Config::Context]
|
189
|
+
def context(name = current_context)
|
190
|
+
found = contexts.find{ |context| context.name == name }
|
191
|
+
raise K8s::Error::Configuration, "context not found: #{name.inspect}" unless found
|
192
|
+
|
193
|
+
found.context
|
194
|
+
end
|
195
|
+
|
196
|
+
# @param name [String]
|
197
|
+
# @return [K8s::Config::Cluster]
|
198
|
+
def cluster(name = context.cluster)
|
199
|
+
clusters.find{ |cluster| cluster.name == name }.cluster
|
200
|
+
end
|
201
|
+
|
202
|
+
# @param name [String]
|
203
|
+
# @return [K8s::Config::User]
|
204
|
+
def user(name = context.user)
|
205
|
+
users.find{ |user| user.name == name }.user
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
data/lib/k8s/error.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module K8s
|
6
|
+
# Top-level class for all errors raised by this gem.
|
7
|
+
class Error < StandardError
|
8
|
+
# Kube API error, related to a HTTP response with a non-2xx code
|
9
|
+
class API < Error
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :method, :path, :code, :reason, :status
|
13
|
+
|
14
|
+
# @param method [Integer] HTTP request method
|
15
|
+
# @param path [Integer] HTTP request path
|
16
|
+
# @param code [Integer] HTTP response code
|
17
|
+
# @param reason [String] HTTP response reason
|
18
|
+
# @param status [K8s::API::MetaV1::Status]
|
19
|
+
def initialize(method, path, code, reason, status = nil)
|
20
|
+
@method = method
|
21
|
+
@path = path
|
22
|
+
@code = code
|
23
|
+
@reason = reason
|
24
|
+
@status = status
|
25
|
+
|
26
|
+
if status
|
27
|
+
super("#{@method} #{@path} => HTTP #{@code} #{@reason}: #{@status.message}")
|
28
|
+
else
|
29
|
+
super("#{@method} #{@path} => HTTP #{@code} #{@reason}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
BadRequest = Class.new(API).freeze
|
35
|
+
Unauthorized = Class.new(API).freeze
|
36
|
+
Forbidden = Class.new(API).freeze
|
37
|
+
NotFound = Class.new(API).freeze
|
38
|
+
MethodNotAllowed = Class.new(API).freeze
|
39
|
+
Conflict = Class.new(API).freeze # XXX: also AlreadyExists?
|
40
|
+
Invalid = Class.new(API).freeze
|
41
|
+
Timeout = Class.new(API).freeze
|
42
|
+
InternalError = Class.new(API).freeze
|
43
|
+
ServiceUnavailable = Class.new(API).freeze
|
44
|
+
ServerTimeout = Class.new(API).freeze
|
45
|
+
|
46
|
+
HTTP_STATUS_ERRORS = {
|
47
|
+
400 => BadRequest,
|
48
|
+
401 => Unauthorized,
|
49
|
+
403 => Forbidden,
|
50
|
+
404 => NotFound,
|
51
|
+
405 => MethodNotAllowed,
|
52
|
+
409 => Conflict,
|
53
|
+
422 => Invalid,
|
54
|
+
429 => Timeout,
|
55
|
+
500 => InternalError,
|
56
|
+
503 => ServiceUnavailable,
|
57
|
+
504 => ServerTimeout
|
58
|
+
}.freeze
|
59
|
+
|
60
|
+
# Attempt to create a ResourceClient for an unknown resource type.
|
61
|
+
# The client cannot construct the correct API URL without having the APIResource definition.
|
62
|
+
UndefinedResource = Class.new(Error)
|
63
|
+
|
64
|
+
Configuration = Class.new(Error)
|
65
|
+
end
|
66
|
+
end
|
data/lib/k8s/logging.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module K8s
|
6
|
+
# Logging utilities
|
7
|
+
#
|
8
|
+
# This provides a per-class Logger that uses the class name as a logging prefix.
|
9
|
+
# Instances can optionally also use logger! to define a per-instance Logger using a custom prefix.
|
10
|
+
module Logging
|
11
|
+
# Default log target
|
12
|
+
LOG_TARGET = $stderr
|
13
|
+
|
14
|
+
# Default log level: show warnings.
|
15
|
+
#
|
16
|
+
# Use K8s::Logging.quiet! to supress warnings.
|
17
|
+
# Note that the K8s::Transport defaults to quiet!
|
18
|
+
LOG_LEVEL = Logger::WARN
|
19
|
+
|
20
|
+
# methods defined on both the global K8s::Logging module, as well as class methods on each class including K8s::Logging
|
21
|
+
module ModuleMethods
|
22
|
+
# global log_level shared across all including classes
|
23
|
+
# @return Logger::*
|
24
|
+
def log_level
|
25
|
+
@log_level
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param level Logger::*
|
29
|
+
def log_level=(level)
|
30
|
+
@log_level = level
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set log_level to Logger::DEBUG
|
34
|
+
def debug!
|
35
|
+
self.log_level = Logger::DEBUG
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set log_level to Logger::INFO
|
39
|
+
def verbose!
|
40
|
+
self.log_level = Logger::INFO
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set log_level to Logger::ERROR, surpressing any warnings logged by default
|
44
|
+
def quiet!
|
45
|
+
self.log_level = Logger::ERROR
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
extend ModuleMethods # global @log_level
|
50
|
+
|
51
|
+
# methods defined on each class including K8s::Logging
|
52
|
+
module ClassMethods
|
53
|
+
# @return [Logger]
|
54
|
+
def logger(target: LOG_TARGET, level: nil)
|
55
|
+
@logger ||= Logger.new(target).tap do |logger|
|
56
|
+
logger.progname = name
|
57
|
+
logger.level = level || log_level || K8s::Logging.log_level || LOG_LEVEL
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# extend class/intance methods for per-class logger
|
63
|
+
def self.included(base)
|
64
|
+
base.extend(ModuleMethods) # per-class @log_level
|
65
|
+
base.extend(ClassMethods)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Use per-instance logger instead of the default per-class logger
|
69
|
+
#
|
70
|
+
# Sets the instance variable returned by #logger
|
71
|
+
#
|
72
|
+
# @return [Logger]
|
73
|
+
def logger!(progname: self.class.name, target: LOG_TARGET, level: nil, debug: false)
|
74
|
+
@logger = Logger.new(target).tap do |logger|
|
75
|
+
level = Logger::DEBUG if debug
|
76
|
+
|
77
|
+
logger.progname = "#{self.class.name}<#{progname}>"
|
78
|
+
logger.level = level || self.class.log_level || K8s::Logging.log_level || LOG_LEVEL
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Logger]
|
83
|
+
def logger
|
84
|
+
@logger || self.class.logger
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/k8s/resource.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'recursive-open-struct'
|
4
|
+
require 'hashdiff'
|
5
|
+
require 'forwardable'
|
6
|
+
require 'yaml/safe_load_stream'
|
7
|
+
|
8
|
+
module K8s
|
9
|
+
# generic untyped resource
|
10
|
+
class Resource < RecursiveOpenStruct
|
11
|
+
extend Forwardable
|
12
|
+
include Comparable
|
13
|
+
|
14
|
+
using YAMLSafeLoadStream
|
15
|
+
using K8s::Util::HashDeepMerge
|
16
|
+
|
17
|
+
# @param data [String]
|
18
|
+
# @return [self]
|
19
|
+
def self.from_json(data)
|
20
|
+
new(Yajl::Parser.parse(data))
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param filename [String] file path
|
24
|
+
# @return [K8s::Resource]
|
25
|
+
def self.from_file(filename)
|
26
|
+
new(YAML.safe_load(File.read(filename), [], [], true, filename))
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param path [String] file path
|
30
|
+
# @return [Array<K8s::Resource>]
|
31
|
+
def self.from_files(path)
|
32
|
+
stat = File.stat(path)
|
33
|
+
|
34
|
+
if stat.directory?
|
35
|
+
# recurse
|
36
|
+
Dir.glob("#{path}/*.{yml,yaml}").sort.map { |dir| from_files(dir) }.flatten
|
37
|
+
else
|
38
|
+
YAML.safe_load_stream(File.read(path), path).map{ |doc| new(doc) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param hash [Hash]
|
43
|
+
# @param recurse_over_arrays [Boolean]
|
44
|
+
# @param options [Hash] see RecursiveOpenStruct#initialize
|
45
|
+
def initialize(hash, recurse_over_arrays: true, **options)
|
46
|
+
super(hash,
|
47
|
+
recurse_over_arrays: recurse_over_arrays,
|
48
|
+
**options
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param options [Hash] see Hash#to_json
|
53
|
+
# @return [String]
|
54
|
+
def to_json(**options)
|
55
|
+
to_hash.to_json(**options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param other [K8s::Resource]
|
59
|
+
# @return [Boolean]
|
60
|
+
def <=>(other)
|
61
|
+
to_hash <=> other.to_hash
|
62
|
+
end
|
63
|
+
|
64
|
+
# merge in fields
|
65
|
+
#
|
66
|
+
# @param attrs [Hash, K8s::Resource]
|
67
|
+
# @return [K8s::Resource]
|
68
|
+
def merge(attrs)
|
69
|
+
# deep clone of attrs
|
70
|
+
h = to_hash
|
71
|
+
|
72
|
+
# merge in-place
|
73
|
+
h.deep_merge!(attrs.to_hash, overwrite_arrays: true, merge_nil_values: true)
|
74
|
+
|
75
|
+
self.class.new(h)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [String]
|
79
|
+
def checksum
|
80
|
+
@checksum ||= Digest::MD5.hexdigest(Marshal.dump(to_hash))
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param attrs [Hash]
|
84
|
+
# @param config_annotation [String]
|
85
|
+
# @return [Hash]
|
86
|
+
def merge_patch_ops(attrs, config_annotation)
|
87
|
+
Util.json_patch(current_config(config_annotation), stringify_hash(attrs))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Gets the existing resources (on kube api) configuration, an empty hash if not present
|
91
|
+
#
|
92
|
+
# @param config_annotation [String]
|
93
|
+
# @return [Hash]
|
94
|
+
def current_config(config_annotation)
|
95
|
+
current_cfg = metadata.annotations&.dig(config_annotation)
|
96
|
+
return {} unless current_cfg
|
97
|
+
|
98
|
+
current_hash = Yajl::Parser.parse(current_cfg)
|
99
|
+
# kubectl adds empty metadata.namespace, let's fix it
|
100
|
+
current_hash['metadata'].delete('namespace') if current_hash.dig('metadata', 'namespace').to_s.empty?
|
101
|
+
|
102
|
+
current_hash
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param config_annotation [String]
|
106
|
+
# @return [Boolean]
|
107
|
+
def can_patch?(config_annotation)
|
108
|
+
!!metadata.annotations&.dig(config_annotation)
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param hash [Hash]
|
112
|
+
# @return [Hash]
|
113
|
+
def stringify_hash(hash)
|
114
|
+
Yajl::Parser.parse(JSON.dump(hash))
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|