k8s-ruby 0.10.5

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