kerbi 0.0.1 → 0.0.2

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/lib/cli/base_handler.rb +180 -0
  3. data/lib/cli/base_serializer.rb +120 -0
  4. data/lib/cli/config_handler.rb +51 -0
  5. data/lib/cli/entry_serializers.rb +99 -0
  6. data/lib/cli/project_handler.rb +2 -2
  7. data/lib/cli/root_handler.rb +32 -13
  8. data/lib/cli/state_handler.rb +95 -0
  9. data/lib/cli/values_handler.rb +4 -3
  10. data/lib/config/cli_schema.rb +299 -27
  11. data/lib/config/config_file.rb +60 -0
  12. data/lib/config/globals.rb +4 -0
  13. data/lib/config/run_opts.rb +150 -0
  14. data/lib/config/state_consts.rb +10 -0
  15. data/lib/kerbi.rb +31 -9
  16. data/lib/main/errors.rb +109 -0
  17. data/lib/main/mixer.rb +12 -8
  18. data/lib/mixins/cli_state_helpers.rb +136 -0
  19. data/lib/mixins/cm_backend_testing.rb +95 -0
  20. data/lib/mixins/entry_tag_logic.rb +183 -0
  21. data/lib/state/base_backend.rb +59 -0
  22. data/lib/state/config_map_backend.rb +119 -0
  23. data/lib/state/entry.rb +173 -0
  24. data/lib/state/entry_set.rb +137 -0
  25. data/lib/state/metadata.yaml.erb +11 -0
  26. data/lib/state/mixers.rb +23 -0
  27. data/lib/state/resources.yaml.erb +17 -0
  28. data/lib/utils/cli.rb +77 -10
  29. data/lib/utils/helm.rb +10 -12
  30. data/lib/utils/k8s_auth.rb +87 -0
  31. data/lib/utils/misc.rb +36 -1
  32. data/lib/utils/mixing.rb +1 -1
  33. data/lib/utils/values.rb +13 -22
  34. data/spec/cli/config_handler_spec.rb +38 -0
  35. data/spec/cli/root_handler_spec.rb +99 -0
  36. data/spec/cli/state_handler_spec.rb +139 -0
  37. data/spec/cli/values_handler_spec.rb +17 -0
  38. data/spec/expectations/common/bad-tag.txt +1 -0
  39. data/spec/expectations/config/bad-set.txt +1 -0
  40. data/spec/expectations/config/set.txt +1 -0
  41. data/spec/expectations/config/show-default.yaml +6 -0
  42. data/spec/expectations/root/template-inlines.yaml +31 -0
  43. data/spec/expectations/root/template-production.yaml +31 -0
  44. data/spec/expectations/root/template-read-inlines.yaml +31 -0
  45. data/spec/expectations/root/template-read.yaml +31 -0
  46. data/spec/expectations/root/template-write.yaml +31 -0
  47. data/spec/expectations/root/template.yaml +31 -0
  48. data/spec/expectations/root/values.json +28 -0
  49. data/spec/expectations/state/delete.txt +1 -0
  50. data/spec/expectations/state/demote.txt +1 -0
  51. data/spec/expectations/state/init-already-existed.txt +2 -0
  52. data/spec/expectations/state/init-both-created.txt +2 -0
  53. data/spec/expectations/state/list.json +51 -0
  54. data/spec/expectations/state/list.txt +6 -0
  55. data/spec/expectations/state/list.yaml +35 -0
  56. data/spec/expectations/state/promote.txt +1 -0
  57. data/spec/expectations/state/prune-candidates.txt +1 -0
  58. data/spec/expectations/state/retag.txt +1 -0
  59. data/spec/expectations/state/set.txt +1 -0
  60. data/spec/expectations/state/show.json +13 -0
  61. data/spec/expectations/state/show.txt +13 -0
  62. data/spec/expectations/state/show.yaml +8 -0
  63. data/spec/expectations/state/status-all-working.txt +4 -0
  64. data/spec/expectations/state/status-not-provisioned.txt +4 -0
  65. data/spec/expectations/values/order-of-precedence.yaml +4 -0
  66. data/spec/main/configmap_backend_spec.rb +109 -0
  67. data/spec/main/state_entry_set_spec.rb +112 -0
  68. data/spec/main/state_entry_spec.rb +109 -0
  69. data/spec/spec_helper.rb +87 -1
  70. data/spec/utils/helm_spec.rb +1 -21
  71. data/spec/utils/k8s_auth_spec.rb +32 -0
  72. data/spec/utils/misc_utils_spec.rb +9 -0
  73. data/spec/utils/values_utils_spec.rb +12 -19
  74. metadata +114 -13
  75. data/lib/cli/base.rb +0 -83
  76. data/lib/config/cli_opts.rb +0 -50
  77. data/lib/config/manager.rb +0 -36
  78. data/lib/main/state_manager.rb +0 -47
  79. data/lib/utils/kubectl.rb +0 -58
  80. data/spec/main/examples_spec.rb +0 -12
  81. data/spec/main/state_manager_spec.rb +0 -84
  82. data/spec/utils/state_utils.rb +0 -15
@@ -0,0 +1,109 @@
1
+ module Kerbi
2
+ class Error < ::StandardError
3
+ end
4
+
5
+ class StateBackendNotReadyError < Error
6
+ MSG = "State-keeping backend not ready. Run 'kerbi state status' for more."
7
+
8
+ def initialize(msg = MSG)
9
+ super
10
+ end
11
+ end
12
+
13
+ class IllegalEntryTag < Error
14
+ def initialize(msg = "State entry tag cannot be 'latest'")
15
+ super
16
+ end
17
+ end
18
+
19
+ class IllegalConfigWrite < Error
20
+ LEGAL = Kerbi::Consts::OptionKeys::LEGAL_CONFIG_FILE_KEYS
21
+ def initialize(key)
22
+ super("Illegal config assignment. '#{key}' not in #{LEGAL}")
23
+ end
24
+ end
25
+
26
+ class BadEntryQueryForWrite < Error
27
+ MSG = "write-state needs an existing entry id/tag, 'candidate', or 'latest'"
28
+ def initialize(msg = MSG)
29
+ super
30
+ end
31
+ end
32
+
33
+ class IllegalWriteStateTagWordError < Error
34
+ MSG = "Tag names for writing cannot contain special words exclusive to tag "
35
+ def initialize(msg = MSG)
36
+ super
37
+ end
38
+ end
39
+
40
+ class StateNotFoundError < Error
41
+ def initialize(tag='')
42
+ super("State given by tag #{tag} not found")
43
+ end
44
+ end
45
+
46
+ class StateNotPromotable < Error
47
+ MSG = "Non-candidate states cannot be promoted"
48
+ def initialize(msg = MSG)
49
+ super
50
+ end
51
+ end
52
+
53
+ class StateNotDemotable < Error
54
+ MSG = "Candidate states cannot be demoted"
55
+ def initialize(msg = MSG)
56
+ super
57
+ end
58
+ end
59
+
60
+ class NoSuchStateAttrName < Error
61
+ MSG = "This attribute does not exist or is not writeable"
62
+ def initialize(msg = MSG)
63
+ super(MSG)
64
+ end
65
+ end
66
+
67
+ class ValuesFileNotFoundError < Error
68
+ def initialize(fname_expr: , root: )
69
+ msg = "Could not resolve values file '#{fname_expr}' in #{root}"
70
+ super(msg)
71
+ end
72
+ end
73
+
74
+ class KerbifileNotFoundError < Error
75
+ def initialize(root: )
76
+ msg = "Could not resolve kerbifile in #{root}"
77
+ super(msg)
78
+ end
79
+ end
80
+
81
+ class EntryValidationError < Error
82
+ MSG = "Cannot write state because of validation errors: "
83
+
84
+ # @param [Hash] errors
85
+ def initialize(errors)
86
+ message = self.class.build_message(errors)
87
+ super(message)
88
+ end
89
+
90
+ # @param [Hash] errors
91
+ def self.error_line(error)
92
+ "#{error[:attr]}['#{error[:value]}']: #{error[:msg]}".indent(1)
93
+ end
94
+
95
+ # @param [String] tag
96
+ # @param [Array<Hash>] errors
97
+ def self.entry_line(tag, error_dicts)
98
+ per_tag_parts = error_dicts.map{ |d| error_line(d) }
99
+ "Entry['#{tag}'] \n #{per_tag_parts.join("\n")}".indent(1)
100
+ end
101
+
102
+ # @param [Hash] errors
103
+ def self.build_message(errors)
104
+ parts = errors.map { |h| entry_line(h[0], h[1]) }
105
+ "#{MSG} \n#{parts.join("\n")}"
106
+ end
107
+ end
108
+
109
+ end
data/lib/main/mixer.rb CHANGED
@@ -45,7 +45,7 @@ module Kerbi
45
45
  def run
46
46
  begin
47
47
  self.mix
48
- rescue Exception => e
48
+ rescue Error => e
49
49
  puts "Exception below caused by mixer #{self.class.name}"
50
50
  raise e
51
51
  end
@@ -202,15 +202,19 @@ module Kerbi
202
202
  subtree.freeze
203
203
  end
204
204
 
205
- def resolve_file_name(fname)
205
+ ## Resolves a user-given short name for a file to interpolate,
206
+ # like 'pod', 'pod.yaml', into an absolute file path.
207
+ # @param [String] fname_expr e.g 'pod', 'pod.yaml'
208
+ # @return [?String]
209
+ def resolve_file_name(fname_expr)
206
210
  dir = self.pwd
207
211
  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"
212
+ fname_expr,
213
+ "#{fname_expr}.yaml",
214
+ "#{fname_expr}.yaml.erb",
215
+ "#{dir}/#{fname_expr}",
216
+ "#{dir}/#{fname_expr}.yaml",
217
+ "#{dir}/#{fname_expr}.yaml.erb"
214
218
  ).first
215
219
  end
216
220
 
@@ -0,0 +1,136 @@
1
+ module Kerbi
2
+ module Mixins
3
+ module CliStateHelpers
4
+
5
+ protected
6
+
7
+ ##
8
+ # Convenience method that returns the state backend's entry set.
9
+ # @return [Kerbi::State::EntrySet]
10
+ def entry_set
11
+ state_backend.entry_set
12
+ end
13
+
14
+ ##
15
+ # For commands that need a working state backend,
16
+ # ask the backend if it is operational, and raise otherwise.
17
+ def raise_unless_backend_ready
18
+ unless state_backend.read_write_ready?
19
+ raise Kerbi::StateBackendNotReadyError
20
+ end
21
+ end
22
+
23
+ ##
24
+ # Convenience method for invoking #find_entry_for_read
25
+ # on the state backend's entry-set.
26
+ # @return [Kerbi::State::Entry]
27
+ def find_readable_entry(tag_expr)
28
+ entry_set.find_entry_for_read(tag_expr)
29
+ end
30
+
31
+ ##
32
+ # Convenience method for updating a state entry's created_at
33
+ # and then persisting the new list of entries via the state backend.
34
+ # Also optionally pretty prints changes.
35
+ # @param [Kerbi::State::Entry] entry
36
+ def touch_and_save_entry(entry, changes={})
37
+ entry.created_at = Time.now
38
+ state_backend.save
39
+ if changes && (change = changes.first)
40
+ key, old_value = change
41
+ new_value = entry.send(key) rescue "ERR"
42
+ name = "state[#{entry.tag}].#{key}"
43
+ change_str = "from #{old_value} => #{new_value}"
44
+ echo "Updated #{name} #{change_str}".colorize(:green)
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Given a tag by read-state [TAG], find the corresponding
50
+ # state entry and return its values dict.
51
+ #
52
+ # If the state entry is NOT found, an empty dict is returned,
53
+ # unless the strict-read option is also passed, in which
54
+ # case it raises a fatal exception.
55
+ # @return [Hash{Symbol->String}]
56
+ def read_state_values
57
+ if run_opts.reads_state?
58
+ expr = run_opts.read_state_from
59
+ begin
60
+ entry = entry_set.find_entry_for_read(expr)
61
+ entry.values.deep_dup.deep_symbolize_keys
62
+ rescue Kerbi::StateNotFoundError => e
63
+ raise e if run_opts.reads_state_strictly?
64
+ {}
65
+ end
66
+ else
67
+ {}
68
+ end
69
+ end
70
+
71
+ ##
72
+ # Given a tag by write-state [TAG], find or create a state entry
73
+ # and assign its values and default_values to the respective
74
+ # just values and default_values just compiled.
75
+ def persist_compiled_values
76
+ if run_opts.writes_state?
77
+ raise_unless_backend_ready
78
+ expr = run_opts.write_state_to
79
+ entry = entry_set.find_or_init_entry_for_write(expr)
80
+
81
+ entry.values = compile_values.deep_dup
82
+ entry.default_values = compile_default_values.deep_dup
83
+ entry.created_at = Time.now
84
+
85
+ state_backend.save
86
+ end
87
+ end
88
+
89
+ ##
90
+ # Given the state-backend parameter or config value,
91
+ # generate an instance of the corresponding backend
92
+ # class.
93
+ # @return [Kerbi::State::Backend]
94
+ def generate_state_backend(namespace=nil)
95
+ if run_opts.state_backend_type == 'configmap'
96
+ auth_bundle = make_k8s_auth_bundle
97
+ Kerbi::State::ConfigMapBackend.new(
98
+ auth_bundle,
99
+ namespace || run_opts.cluster_namespace
100
+ )
101
+ end
102
+ end
103
+
104
+ ##
105
+ # Given the various Kubernetes authentication options and
106
+ # configs, generates a Hash with the necessary data/schema
107
+ # to pass onto the internal k8s authentication logic.
108
+ #
109
+ # This method only delegates. Actual work done is done here at:
110
+ # Kerbi::Utils::K8sAuth.
111
+ # @return [Hash] auth bundle for the k8s authentication logic.
112
+ def make_k8s_auth_bundle
113
+ case run_opts.k8s_auth_type
114
+ when "kube-config"
115
+ Kerbi::Utils::K8sAuth.kube_config_bundle(
116
+ path: run_opts.kube_config_path,
117
+ name: run_opts.kube_context_name
118
+ )
119
+ when "basic"
120
+ Kerbi::Utils::K8sAuth.basic_auth_bundle(
121
+ username: run_opts.k8s_auth_username,
122
+ password: run_opts.k8s_auth_password
123
+ )
124
+ when "token"
125
+ Kerbi::Utils::K8sAuth.token_auth_bundle(
126
+ bearer_token: run_opts.k8s_auth_token,
127
+ )
128
+ when "in-cluster"
129
+ Kerbi::Utils::K8sAuth.in_cluster_auth_bundle
130
+ else
131
+ raise "Bad k8s connect type '#{run_opts.k8s_auth_type}'"
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,95 @@
1
+ module Kerbi
2
+ module Mixins
3
+ module CmBackendTesting
4
+
5
+ # @return [TrueClass, FalseClass]
6
+ def namespace_exists?
7
+ begin
8
+ !!client("v1").get_namespace(namespace)
9
+ rescue Kubeclient::ResourceNotFoundError
10
+ false
11
+ end
12
+ end
13
+
14
+ def resource_exists?
15
+ begin
16
+ !!resource
17
+ rescue Kubeclient::ResourceNotFoundError
18
+ false
19
+ end
20
+ end
21
+
22
+ def read_write_ready?
23
+ namespace_exists? && resource_exists?
24
+ end
25
+
26
+ def test_connection(options={})
27
+ res_name = Kerbi::State::Consts::RESOURCE_NAME
28
+ exceptions = []
29
+
30
+ schema = [
31
+ {
32
+ method: :client,
33
+ message: "1. Create Kubernetes client"
34
+ },
35
+ {
36
+ method: :test_list_namespaces,
37
+ message: "2. List cluster namespaces"
38
+ },
39
+ {
40
+ method: :test_target_ns_exists,
41
+ message: "3. Target namespace #{namespace} exists"
42
+ },
43
+ {
44
+ method: :load_resource,
45
+ message: "4. Resource #{namespace}/cm/#{res_name} exists"
46
+ }
47
+ ]
48
+
49
+ schema.each do |spec|
50
+ begin
51
+ self.send(spec[:method])
52
+ puts_outcome(spec[:message], true)
53
+ rescue StandardError => e
54
+ puts_outcome(spec[:message], false)
55
+ exceptions << { exception: e, test: spec[:message] }
56
+ end
57
+ end
58
+
59
+ if exceptions.any? && options[:verbose]
60
+ puts "\n---EXCEPTIONS---\n".colorize(:red).bold
61
+ exceptions.each do |exc|
62
+ puts "[#{exc[:test]}] #{exc[:exception]}".to_s.colorize(:red).bold
63
+ puts exc[:exception].backtrace
64
+ puts "\n\n"
65
+ end
66
+ end
67
+ end
68
+
69
+ def test_list_namespaces
70
+ client("v1").get_namespaces.any?
71
+ end
72
+
73
+ def test_target_ns_exists
74
+ client.get_namespace namespace
75
+ end
76
+
77
+ #noinspection RubyResolve
78
+ def puts_outcome(msg, result)
79
+ outcome_str = result.present? ? "Success" : "Failure"
80
+ color = result.present? ? :green : :red
81
+ outcome_str = outcome_str.colorize(color)
82
+ puts "#{msg}: #{outcome_str}".bold
83
+ end
84
+
85
+ def echo_init(msg, result, options={})
86
+ unless options[:quiet].present?
87
+ outcome_str = result.present? ? "Already existed" : "Created"
88
+ color = result.present? ? :green : :blue
89
+ outcome_str = outcome_str.colorize(color)
90
+ puts "#{msg}: #{outcome_str}".bold
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,183 @@
1
+ module Kerbi
2
+ module Mixins
3
+
4
+ ##
5
+ # Mixin for handling mission critical state entry tag logic. The logic
6
+ # is most comprised name resolution, i.e turning special words that users
7
+ # pass in place of literal tags, into literal tags.
8
+ module EntryTagLogic
9
+ extend ActiveSupport::Concern
10
+
11
+ SPECIAL_CHAR = "@"
12
+
13
+ CANDIDATE_WORD = "candidate"
14
+ NEW_CANDIDATE_WORD = "new-candidate"
15
+ LATEST_WORD = "latest"
16
+ OLDEST_WORD = "oldest"
17
+ RANDOM_WORD = "random"
18
+
19
+ SPECIAL_READ_WORDS = [
20
+ CANDIDATE_WORD,
21
+ LATEST_WORD,
22
+ OLDEST_WORD
23
+ ]
24
+
25
+ SPECIAL_WRITE_WORDS = [
26
+ LATEST_WORD,
27
+ OLDEST_WORD,
28
+ CANDIDATE_WORD,
29
+ NEW_CANDIDATE_WORD,
30
+ RANDOM_WORD
31
+ ]
32
+
33
+ ##
34
+ # Calls #do_resolve_tag_expr with verb=write in order to turn
35
+ # an entry tag expression like @candidate-new into [cand]purple-forest-new.
36
+ #
37
+ # See documentation for #resolve_word for information on how resolution
38
+ # works at the word level.
39
+ # @param [String] tag_expr
40
+ # @return [String]
41
+ def resolve_write_tag_expr(tag_expr)
42
+ do_resolve_tag_expr(tag_expr, "write")
43
+ end
44
+
45
+ ##
46
+ # Calls #do_resolve_tag_expr with verb=read in order to turn
47
+ # an entry tag expression like @latest into 2.1.1
48
+ #
49
+ # See documentation for #resolve_word for information on how resolution
50
+ # works at the word level.
51
+ # @param [String] tag_expr
52
+ # @return [String]
53
+ def resolve_read_tag_expr(tag_expr)
54
+ do_resolve_tag_expr(tag_expr, "read")
55
+ end
56
+
57
+ ## Main logic to template state entry tag expressions (that users use
58
+ # to identify state entries with) into a final, usable tag.
59
+ #
60
+ # The method finds special words in the tag expression, which start with
61
+ # the SPECIAL_CHAR '@', and substitutes them one at a time with a computed
62
+ # value. For instance, @latest will become the actual tag of the latest state
63
+ # entry, and @random will become a random string.
64
+ #
65
+ # Depending on whether the user's request is for reading or writing an entry,
66
+ # different substitutions are available.
67
+ # @return [String]
68
+ # @param [String] tag_expr
69
+ # @param [String] verb
70
+ def do_resolve_tag_expr(tag_expr, verb)
71
+ raise "Internal error" unless %w[read write].include?(verb)
72
+ words = verb == 'read' ? SPECIAL_READ_WORDS : SPECIAL_WRITE_WORDS
73
+
74
+ resolved_tag = tag_expr
75
+ words.each do |special_word|
76
+ part = "#{SPECIAL_CHAR}#{special_word}"
77
+ if tag_expr.include?(part)
78
+ resolved_word = resolve_word(special_word, verb)
79
+ resolved_tag = resolved_tag.gsub(part, resolved_word)
80
+ end
81
+ end
82
+ resolved_tag
83
+ end
84
+
85
+ ##
86
+ # Performs a special word substitution for an individual special word,
87
+ # like 'latest', 'random', or 'candidate'. Works by looking for a
88
+ # corresponding the word-resolver method in self.
89
+ #
90
+ # E.g if you pass 'random', it expects the method resolve_random_word to
91
+ # exist.
92
+ #
93
+ # Because the same special word can have different interpretations
94
+ # depending on whether the mode (read or write), this method will
95
+ # first look for the mode-specialized version of the word-resolver function,
96
+ # e.g if passed 'candidate' in 'read' mode, it will first look out for
97
+ # the a word-resolver method called 'resolve_candidate_read_word' and call it
98
+ # instead of the less specialized 'resolve_candidate_word' method.
99
+ #
100
+ # @param [String] word a special word ('latest', 'random', 'candidate')
101
+ # @param [Object] verb whether this is a read or write operation
102
+ def resolve_word(word, verb)
103
+ word = word.gsub("-", "_")
104
+ if respond_to?((method = "resolve_#{word}_#{verb}_word"))
105
+ send(method)
106
+ elsif respond_to?((method = "resolve_#{word}_word"))
107
+ send(method)
108
+ else
109
+ raise "What is #{word}??"
110
+ end
111
+ end
112
+
113
+ ##
114
+ # Single word resolver. Looks for the latest candidate state entry
115
+ # and returns its tag or an empty string if there is no
116
+ # latest candidate state.
117
+ # @return [String]
118
+ def resolve_candidate_read_word
119
+ latest_candidate&.tag || ""
120
+ end
121
+
122
+ ##
123
+ # Single word resolver. Outputs a non-taken random tag (given by
124
+ # #generate_random_tag) prefixed with candidate flag prefix [cand]-.
125
+ # @return [String]
126
+ def resolve_candidate_write_word
127
+ resolve_candidate_read_word || resolve_new_candidate_word
128
+ end
129
+
130
+ def resolve_new_candidate_word
131
+ prefix = Kerbi::State::Entry::CANDIDATE_PREFIX
132
+ begin
133
+ tag = "#{prefix}#{self.class.generate_random_tag}"
134
+ end while candidates.find{ |e| e.tag == tag }
135
+ tag
136
+ end
137
+
138
+ ##
139
+ # Single word resolver. Looks for the latest committed state entry
140
+ # and returns its tag or an empty string if there is no
141
+ # latest committed state.
142
+ # @return [String]
143
+ def resolve_latest_word
144
+ latest&.tag || ""
145
+ end
146
+
147
+ ##
148
+ # Single word resolver. Looks for the latest committed state entry
149
+ # and returns its tag or an empty string if there is no
150
+ # latest committed state.
151
+ # @return [String]
152
+ def resolve_oldest_word
153
+ oldest&.tag || ""
154
+ end
155
+
156
+ ##
157
+ # Single word resolver. Outputs a non-taken random tag (given by
158
+ # #generate_random_tag).
159
+ # @return [String]
160
+ def resolve_random_word
161
+ begin
162
+ tag = self.class.generate_random_tag
163
+ end while entries.find{ |e|e.tag == tag }
164
+ tag
165
+ end
166
+
167
+ # private :do_resolve_tag_expr
168
+ # private :resolve_candidate_read_word
169
+
170
+ module ClassMethods
171
+ ##
172
+ # Uses the Spicy::Proton gem to generate a convenient,
173
+ # human-readable random tag for a state entry.
174
+ # @return [String]
175
+ def generate_random_tag
176
+ gen = Spicy::Proton.new
177
+ "#{gen.adjective(max: 5)}-#{gen.noun(max: 5)}"
178
+ end
179
+ end
180
+
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,59 @@
1
+ module Kerbi
2
+ module State
3
+ class BaseBackend
4
+
5
+ def initialize(options={})
6
+ end
7
+
8
+ # @return [Kerbi::State::EntrySet]
9
+ def entry_set
10
+ @_entry_set ||= EntrySet.new(read_entries)
11
+ end
12
+
13
+ # @return [Array<Kerbi::State::Entry>]
14
+ def entries
15
+ entry_set.entries
16
+ end
17
+
18
+ # @param [Kerbi::State::Entry] entry
19
+ def delete_entry(entry)
20
+ entries.reject! { |e| e.tag == entry.tag }
21
+ save
22
+ @_entry_set = nil
23
+ @_resource = nil
24
+ end
25
+
26
+ def save
27
+ entry_set.validate!
28
+ update_resource
29
+ @_entry_set = nil
30
+ @_resource = nil
31
+ end
32
+
33
+ protected
34
+
35
+ def resource
36
+ @_resource ||= load_resource
37
+ end
38
+
39
+ def update_resource
40
+ raise NotImplementedError
41
+ end
42
+
43
+ # @return [Array<Hash>]
44
+ def read_entries
45
+ raise NotImplementedError
46
+ end
47
+
48
+ def load_resource
49
+ raise NotImplementedError
50
+ end
51
+
52
+ private
53
+
54
+ def utils
55
+ Kerbi::Utils
56
+ end
57
+ end
58
+ end
59
+ end