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.
@@ -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
@@ -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
@@ -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
@@ -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