kerbi 0.0.1

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.
data/lib/main/mixer.rb ADDED
@@ -0,0 +1,235 @@
1
+ module Kerbi
2
+ class Mixer
3
+ include Kerbi::Mixins::Mixer
4
+
5
+ ##
6
+ # Values hash available to subclasses
7
+ # @return [Immutable::Hash] symbol-keyed hash
8
+ attr_reader :values
9
+
10
+ ##
11
+ # Release name available for templating
12
+ # @return [String] symbol-keyed hash
13
+ attr_reader :release_name
14
+
15
+ ##
16
+ # Array of res-hashes being aggregated
17
+ # @return [Array<Hash>] list of hashes
18
+ attr_reader :output
19
+
20
+ ##
21
+ # Array of patches to be applied to results
22
+ # @return [Array<Hash>] list of hashes
23
+ attr_accessor :patch_stack
24
+
25
+ ##
26
+ # Constructor
27
+ # @param [Hash] values the values tree that will be accessible to the subclass
28
+ def initialize(values, opts={})
29
+ @output = []
30
+ @release_name = opts[:release_name] || "default"
31
+ @patch_stack = []
32
+ @values = self.class.compute_own_values_subtree(
33
+ values,
34
+ opts[:overwrite_values_root]
35
+ )
36
+ end
37
+
38
+ ##
39
+ # Where users should return a hash or
40
+ # an array of hashes representing Kubernetes resources
41
+ # @yield [bucket] Exec context in which hashes are collected into one bucket
42
+ # @yieldparam [Kerbi::ResBucket] g Bucket object with essential methods
43
+ # @yieldreturn [Array<Hash>] array of hashes representing Kubernetes resources
44
+ # @return [Array<Hash>] array of hashes representing Kubernetes resources
45
+ def run
46
+ begin
47
+ self.mix
48
+ rescue Exception => e
49
+ puts "Exception below caused by mixer #{self.class.name}"
50
+ raise e
51
+ end
52
+ self.output
53
+ end
54
+
55
+ def mix
56
+ end
57
+
58
+ ##
59
+ # Registers a dict or an array of dicts that will part of the
60
+ # mixers's final output, which is an Array<Hash>.
61
+ # @param [Hash | Array<Hash>] dict the hash to be added
62
+ def push(dicts)
63
+ final_list = Utils::Mixing.sanitize_res_dict_list(dicts)
64
+ self.output.append(*final_list)
65
+ end
66
+
67
+ ##
68
+ # Normalizes, sanitizes and filters a dict or an array of
69
+ # dicts.
70
+ # @param [Hash | Array<Hash>] dict the hash to be added
71
+ def dicts(dict, **opts)
72
+ output = Utils::Mixing.clean_and_filter_dicts(dict, **opts)
73
+ should_patch = opts[:no_patch].blank?
74
+ should_patch ? apply_patch_context(output) : output
75
+ end
76
+ alias_method :dict, :dicts
77
+
78
+ ##
79
+ # Loads a YAML/JSON/ERB file, parses it, interpolates it,
80
+ # and returns processed and filtered list of dicts via #dicts.
81
+ # @param [String] fname with or without extension, relative to self
82
+ # @param [Hash] opts filtering and other options for #dicts
83
+ # @return [Array<Hash>] processed dicts read from file
84
+ def file(fname, **opts)
85
+ output = Utils::Mixing.yaml_file_to_dicts(
86
+ self.class.resolve_file_name(fname),
87
+ **opts.merge({src_binding: binding})
88
+ )
89
+ dicts(output)
90
+ end
91
+
92
+ # @param [String] fname
93
+ # @param [Hash] opts filtering and other options for #dicts
94
+ # @return [Array]
95
+ def dir(fname, **opts)
96
+ output = Utils::Mixing.yamls_in_dir_to_dicts(
97
+ self.class.pwd,
98
+ resolve_file_name(fname),
99
+ **opts
100
+ )
101
+ dicts(output)
102
+ end
103
+
104
+ ##
105
+ # Run 'helm template' on Helm project, parse the output into dicts,
106
+ # return processed and filtered list via #dicts.
107
+ # @param [String] chart_id using format 'jetstack/cert-manager'
108
+ # @param [Hash] opts filtering and other options for #dicts
109
+ # @return [Array<Hash>] processed and filtered dicts
110
+ def chart(chart_id, **opts)
111
+ release = opts[:release] || release_name
112
+ helm_output = Utils::Helm.template(release, chart_id, **opts)
113
+ dicts(helm_output)
114
+ end
115
+
116
+ ##
117
+ # Run another mixer given by klass, return processed and
118
+ # filtered list via #dicts.
119
+ # @param [Class<Kerbi::Mixer>] klass other mixer's class
120
+ # @param [Hash] opts filtering and other options for #dicts
121
+ # @return [Array<Hash>] processed and filtered dicts
122
+ def mixer(klass, **opts)
123
+ force_subtree = opts.delete(:values)
124
+ mixer_inst = klass.new(
125
+ force_subtree.nil? ? values : force_subtree,
126
+ release_name: release_name,
127
+ overwrite_values_subtree: !force_subtree.nil?
128
+ )
129
+ output = mixer_inst.run
130
+ dicts(output)
131
+ end
132
+
133
+ ##
134
+ # Any x-to-dict statements (e.g #dicts, #dir, #chart) executed
135
+ # in the &block passed to this method will have their return values
136
+ # deep merged with the dict(s) passed.
137
+ # @param [Array<Hash>|Hash] dict
138
+ # @param [Proc] block
139
+ # @return [Array<Hash>, Hash]
140
+ def patched_with(dict, &block)
141
+ new_patches = extract_patches(dict)
142
+ patch_stack.push(new_patches)
143
+ yield(block)
144
+ patch_stack.pop
145
+ end
146
+
147
+ private
148
+
149
+ def extract_patches(obj)
150
+ (obj.is_a?(Hash) ? [obj] : obj).map(&:deep_dup)
151
+ end
152
+
153
+ def apply_patch_context(output)
154
+ return output if patch_stack.blank?
155
+ output.map do |res|
156
+ patch_stack.flatten.inject(res) do |whole, patch|
157
+ whole.deep_merge(patch)
158
+ end
159
+ end
160
+ end
161
+
162
+ ##
163
+ # Coerces filename of unknown format to an absolute path
164
+ # @param [String] fname simplified or absolute path of file
165
+ # @return [String] a variation of the filename that exists
166
+ ##
167
+ # Convenience instance method for accessing class level pwd
168
+ # @return [String] the subclass' pwd as defined by the user
169
+
170
+ class << self
171
+
172
+ ##
173
+ # Pass a deep key that will be used to dig into the values
174
+ # dict the mixer gets upon initialization. For example if
175
+ # deep_key is "x", then if the mixer is initialized with
176
+ # values as x: {y: 'z'}, then its final values attribute
177
+ # will be {y: 'z'}.
178
+ def values_root(deep_key)
179
+ @vals_root_deep_key = deep_key
180
+ end
181
+
182
+ def compute_own_values_subtree(values_root, override)
183
+ self.compute_values_subtree(
184
+ values_root,
185
+ override ? nil : @vals_root_deep_key
186
+ )
187
+ end
188
+
189
+ ##
190
+ # Given a values_root dict and a deep key (e.g "x.y.z"), outputs
191
+ # a frozen, deep-cloned, subtree corresponding to the deep
192
+ # key's position in the values_root.
193
+ # @param [Hash] values_root dict from which to extract subtree
194
+ # @param [String] deep_key key in dict in "x.y.z" format
195
+ # @return [Hash] frozen and deep-cloned subtree
196
+ def compute_values_subtree(values_root, deep_key)
197
+ subtree = values_root.deep_dup
198
+ if deep_key.present?
199
+ deep_key_parts = deep_key.split(".")
200
+ subtree = subtree.dig(*deep_key_parts)
201
+ end
202
+ subtree.freeze
203
+ end
204
+
205
+ def resolve_file_name(fname)
206
+ dir = self.pwd
207
+ Kerbi::Utils::Misc.real_files_for(
208
+ fname,
209
+ "#{fname}.yaml",
210
+ "#{fname}.yaml.erb",
211
+ "#{dir}/#{fname}",
212
+ "#{dir}/#{fname}.yaml",
213
+ "#{dir}/#{fname}.yaml.erb"
214
+ ).first
215
+ end
216
+
217
+ ##
218
+ # Sets the absolute path of the directory where
219
+ # yamls used by this Gen can be found, usually "__dir__"
220
+ # @param [String] dirname absolute path of the directory
221
+ # @return [void]
222
+ def locate_self(dirname)
223
+ @dir_location = dirname
224
+ end
225
+
226
+ ##
227
+ # Returns the value set by locate_self
228
+ # @return [String] the subclass' pwd as defined by the user
229
+ def pwd
230
+ @dir_location
231
+ end
232
+ end
233
+
234
+ end
235
+ end
@@ -0,0 +1,47 @@
1
+ module Kerbi
2
+ class StateManager
3
+ def self.patch
4
+ self.create_configmap_if_missing
5
+ patch_values = self.compile_patch
6
+ config_map = utils::State.kubectl_get_cm("state", raise_on_err: false)
7
+ crt_values = utils::State.read_cm_data(config_map)
8
+ merged_vars = crt_values.deep_merge(patch_values)
9
+ new_body = { **config_map, data: { variables: JSON.dump(merged_vars) } }
10
+ yaml_body = YAML.dump(new_body.deep_stringify_keys)
11
+ Utils::Kubectl.apply_tmpfile(yaml_body, args_manager.get_kmd_arg_str)
12
+ end
13
+
14
+ def compile_patch
15
+ values = {}
16
+
17
+ args_manager.get_fnames.each do |fname|
18
+ new_values = YAML.load_file(fname).deep_symbolize_keys
19
+ values.deep_merge!(new_values)
20
+ end
21
+
22
+ args_manager.get_inlines.each do |assignment_str|
23
+ assignment = Utils::Utils.str_assign_to_h(assignment_str)
24
+ values.deep_merge!(assignment)
25
+ end
26
+ values
27
+ end
28
+
29
+ def get_crt_vars
30
+ create_configmap_if_missing
31
+ get_configmap_values(get_configmap)
32
+ end
33
+
34
+ def create_configmap_if_missing
35
+ unless get_configmap(raise_on_er: false)
36
+ kmd = "create cm state #{args_manager.get_kmd_arg_str}"
37
+ Utils::Kubectl.kmd(kmd)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def utils
44
+ Kerbi::Utils
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,79 @@
1
+ module Kerbi
2
+ module Mixins
3
+ module Mixer
4
+
5
+ # @param [Hash] dict hash or array
6
+ # @return [String] encoded string
7
+ def embed(dict, indent: 25)
8
+ _dict = dict.is_a?(Array) ? dict.first : dict
9
+ raw = YAML.dump(_dict).sub("---", "")
10
+ indented_lines = raw.split("\n").map do |line|
11
+ line.indent(indent)
12
+ end
13
+ "\n#{indented_lines.join("\n")}"
14
+ end
15
+
16
+ # @param [Array|Hash] dicts hash or array
17
+ # @return [String] encoded string
18
+ def embed_array(dicts, indent: 25)
19
+ return "[]" unless dicts.present?
20
+
21
+ unless dicts.is_a?(Array)
22
+ raise "embed_array called with non-array #{dicts.class} #{dicts}"
23
+ end
24
+
25
+ raw = YAML.dump(dicts).sub("---", "")
26
+ indented_lines = raw.split("\n").map do |line|
27
+ line.indent(indent)
28
+ end
29
+
30
+ "\n#{indented_lines.join("\n")}"
31
+ end
32
+
33
+ # @param [String] string string to be base64 encoded
34
+ # @return [String] encoded string
35
+ def b64enc(string)
36
+ if string
37
+ Base64.strict_encode64(string)
38
+ else
39
+ ''
40
+ end
41
+ end
42
+
43
+ # @param [String] string string to be base64 encoded
44
+ # @return [String] encoded string
45
+ def b64dec(string)
46
+ if string
47
+ Base64.decode64(string).strip
48
+ else
49
+ ''
50
+ end
51
+ end
52
+
53
+ # @param [String] fname absolute path of file to be encoded
54
+ # @return [String] encoded string
55
+ def b64enc_file(fname)
56
+ file_contents = File.read(fname) rescue nil
57
+ b64enc(file_contents)
58
+ end
59
+
60
+ ##
61
+ # @param [Hash] opts options
62
+ # @option opts [String] url full URL to raw yaml file contents on the web
63
+ # @option opts [String] from one of [github]
64
+ # @option opts [String] except list of filenames to avoid
65
+ # @raise [Exception] if project-id/file missing in github hash
66
+ def http_descriptor_to_url(**opts)
67
+ return opts[:url] if opts[:url]
68
+
69
+ if opts[:from] == 'github'
70
+ base = "https://raw.githubusercontent.com"
71
+ branch = opts[:branch] || 'master'
72
+ project, file = (opts[:project] || opts[:id]), opts[:file]
73
+ raise "Project and/or file not found" unless project && file
74
+ "#{base}/#{project}/#{branch}/#{file}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
data/lib/utils/cli.rb ADDED
@@ -0,0 +1,59 @@
1
+ module Kerbi
2
+ module Utils
3
+ module Cli
4
+
5
+ ##
6
+ # Convenience method for running and compiling the output
7
+ # of several mixers. Returns all result dicts in a flat array
8
+ # preserving the order they were created in.
9
+ # @param [Array<Class<Kerbi::Mixer>] mixer_classes mixers to run
10
+ # @param [Hash] values root values hash to pass to all mixers
11
+ # @param [Object] release_name helm-like release_name for mixers
12
+ # @return [List<Hash>] all dicts emitted by mixers
13
+ def self.run_mixers(mixer_classes, values, release_name)
14
+ mixer_classes.inject([]) do |whole, gen_class|
15
+ mixer_instance = gen_class.new(values, release_name: release_name)
16
+ whole + mixer_instance.run.flatten
17
+ end
18
+ end
19
+
20
+ ##
21
+ # Turns list of key-symbol dicts into their
22
+ # pretty YAML representation.
23
+ # @param [Array<Hash>] dicts dicts to YAMLify
24
+ # @return [String] pretty YAML representation of input
25
+ def self.dicts_to_yaml(dicts)
26
+ if dicts.is_a?(Array)
27
+ dicts.each_with_index.map do |h, i|
28
+ raw = YAML.dump(h.deep_stringify_keys)
29
+ raw.gsub("---\n", i.zero? ? '' : "---\n\n")
30
+ end.join("\n")
31
+ else
32
+ as_yaml = YAML.dump(dicts.deep_stringify_keys)
33
+ as_yaml.gsub("---\n", "")
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Turns list of key-symbol dicts into their
39
+ # pretty JSON representation.
40
+ # @param [Array<Hash>] dicts dicts to YAMLify
41
+ # @return [String] pretty JSON representation of input
42
+ def self.dicts_to_json(dicts)
43
+ JSON.pretty_generate(dicts)
44
+ end
45
+
46
+ ##
47
+ # Searches the expected paths for the kerbifile and ruby-loads it.
48
+ # @param [String] root directory to search
49
+ def self.load_kerbifile(fname_expr)
50
+ fname_expr ||= Dir.pwd
51
+ abs_path = "#{fname_expr}/kerbifile.rb"
52
+ if File.exists?(abs_path)
53
+ #noinspection RubyResolve
54
+ load(abs_path)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/utils/helm.rb ADDED
@@ -0,0 +1,64 @@
1
+ module Kerbi
2
+ module Utils
3
+ module Helm
4
+
5
+ def self.config
6
+ Kerbi::Config::Manager
7
+ end
8
+
9
+ ##
10
+ # Tests whether Kerbi can invoke Helm commands
11
+ # @return [Boolean] true if helm commands succeed locally
12
+ def self.can_exec?
13
+ !!system(config.helm_exec, out: File::NULL, err: File::NULL)
14
+ end
15
+
16
+ ##
17
+ # Writes a hash of values to a YAML to a temp file
18
+ # @param [Hash] values a hash of values
19
+ # @return [String] the path of the file
20
+ def self.make_tmp_values_file(values)
21
+ File.open(config.tmp_helm_values_path, 'w') do |f|
22
+ f.write(YAML.dump((values || {}).deep_stringify_keys))
23
+ end
24
+ config.tmp_helm_values_path
25
+ end
26
+
27
+ ##
28
+ # Deletes the temp file
29
+ # @return [void]
30
+ def self.del_tmp_values_file
31
+ if File.exists?(config.tmp_helm_values_path)
32
+ File.delete(config.tmp_helm_values_path)
33
+ end
34
+ end
35
+
36
+ ##
37
+ # Joins assignments in flat hash into list of --set flags
38
+ # @param [Hash] inline_assigns flat Hash of deep_key: val
39
+ # @return [String] corresponding space-separated --set flags
40
+ def self.encode_inline_assigns(inline_assigns)
41
+ (inline_assigns || []).map do |key, value|
42
+ raise "Assignments must be flat" if value.is_a?(Hash)
43
+ "--set #{key}=#{value}"
44
+ end.join(" ")
45
+ end
46
+
47
+ ##
48
+ # Runs the helm template command
49
+ # @param [String] release release name to pass to Helm
50
+ # @param [String] project <org>/<chart> string identifying helm chart
51
+ # @return [Array<Hash>]
52
+ def self.template(release, project, opts={})
53
+ raise "Helm executable not working" unless can_exec?
54
+ tmp_file = make_tmp_values_file(opts[:values])
55
+ inline_flags = encode_inline_assigns(opts[:inline_assigns])
56
+ command = "#{config.helm_exec} template #{release} #{project}"
57
+ command += " -f #{tmp_file} #{inline_flags} #{opts[:cli_args]}"
58
+ output = `#{command}`
59
+ del_tmp_values_file
60
+ YAML.load_stream(output)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,58 @@
1
+ module Kerbi
2
+ module Utils
3
+ module Kubectl
4
+ def self.kmd(cmd, options = {})
5
+ cmd = "kubectl #{cmd}"
6
+ self.eval_shell_cmd(cmd, options)
7
+ end
8
+
9
+ def self.jkmd(cmd, options={})
10
+ cmd = "kubectl #{cmd} -o json"
11
+ begin
12
+ output = self.eval_shell_cmd(cmd, options)
13
+ as_hash = JSON.parse(output).deep_symbolize_keys
14
+ if as_hash.has_key?(:items)
15
+ as_hash[:items]
16
+ else
17
+ as_hash
18
+ end
19
+ rescue
20
+ nil
21
+ end
22
+ end
23
+
24
+ def self.eval_shell_cmd(cmd, options={})
25
+ print_err = options[:print_err]
26
+ raise_on_err = options[:raise_on_err]
27
+ begin
28
+ output, status = Open3.capture2e(cmd)
29
+ if status.success?
30
+ output = output[0..output.length - 2] if output.end_with?("\n")
31
+ output
32
+ else
33
+ if print_err
34
+ puts "Command \"#{cmd}\" error status #{status} with message:"
35
+ puts output
36
+ puts "---"
37
+ end
38
+ nil
39
+ end
40
+ rescue Exception => e
41
+ if print_err
42
+ puts "Command \"#{cmd}\" failed with message:"
43
+ puts e.message
44
+ puts "---"
45
+ end
46
+ nil
47
+ end
48
+ end
49
+
50
+ def self.apply_tmpfile(yaml_str, append)
51
+ tmp_fname = "/tmp/man-#{SecureRandom.hex(32)}.yaml"
52
+ File.write(tmp_fname, yaml_str)
53
+ kmd("apply -f #{tmp_fname} #{append}", print_err: true)
54
+ File.delete(tmp_fname)
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/utils/misc.rb ADDED
@@ -0,0 +1,41 @@
1
+ module Kerbi
2
+ module Utils
3
+ module Misc
4
+ def self.one_to_array(item)
5
+ return [] if item.nil?
6
+ if item.is_a?(Array)
7
+ item
8
+ else
9
+ [item]
10
+ end
11
+ end
12
+
13
+ ## Given a list of filenames, returns the subset that
14
+ # are real files.
15
+ # @param [Array] candidates filenames to try
16
+ # @return [Array] subset of candidate filenames that are real filenames
17
+ def self.real_files_for(*candidates)
18
+ candidates.select do |fname|
19
+ File.exists?(fname)
20
+ end
21
+ end
22
+
23
+ ##
24
+ # Turns a nested dict into a deep-keyed dict. For example
25
+ # {x: {y: 'z'}} becomes {'x.y': 'z'}
26
+ # @param [Hash] hash input nested dict
27
+ # @return [Hash] flattened dict
28
+ def self.flatten_hash(hash)
29
+ hash.each_with_object({}) do |(k, v), h|
30
+ if v.is_a? Hash
31
+ flatten_hash(v).map do |h_k, h_v|
32
+ h["#{k}.#{h_k}".to_sym] = h_v
33
+ end
34
+ else
35
+ h[k] = v
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end