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.
- checksums.yaml +4 -4
- data/lib/cli/base_handler.rb +180 -0
- data/lib/cli/base_serializer.rb +120 -0
- data/lib/cli/config_handler.rb +51 -0
- data/lib/cli/entry_serializers.rb +99 -0
- data/lib/cli/project_handler.rb +2 -2
- data/lib/cli/root_handler.rb +32 -13
- data/lib/cli/state_handler.rb +95 -0
- data/lib/cli/values_handler.rb +4 -3
- data/lib/config/cli_schema.rb +299 -27
- data/lib/config/config_file.rb +60 -0
- data/lib/config/globals.rb +4 -0
- data/lib/config/run_opts.rb +150 -0
- data/lib/config/state_consts.rb +10 -0
- data/lib/kerbi.rb +31 -9
- data/lib/main/errors.rb +109 -0
- data/lib/main/mixer.rb +12 -8
- data/lib/mixins/cli_state_helpers.rb +136 -0
- data/lib/mixins/cm_backend_testing.rb +95 -0
- data/lib/mixins/entry_tag_logic.rb +183 -0
- data/lib/state/base_backend.rb +59 -0
- data/lib/state/config_map_backend.rb +119 -0
- data/lib/state/entry.rb +173 -0
- data/lib/state/entry_set.rb +137 -0
- data/lib/state/metadata.yaml.erb +11 -0
- data/lib/state/mixers.rb +23 -0
- data/lib/state/resources.yaml.erb +17 -0
- data/lib/utils/cli.rb +77 -10
- data/lib/utils/helm.rb +10 -12
- data/lib/utils/k8s_auth.rb +87 -0
- data/lib/utils/misc.rb +36 -1
- data/lib/utils/mixing.rb +1 -1
- data/lib/utils/values.rb +13 -22
- data/spec/cli/config_handler_spec.rb +38 -0
- data/spec/cli/root_handler_spec.rb +99 -0
- data/spec/cli/state_handler_spec.rb +139 -0
- data/spec/cli/values_handler_spec.rb +17 -0
- data/spec/expectations/common/bad-tag.txt +1 -0
- data/spec/expectations/config/bad-set.txt +1 -0
- data/spec/expectations/config/set.txt +1 -0
- data/spec/expectations/config/show-default.yaml +6 -0
- data/spec/expectations/root/template-inlines.yaml +31 -0
- data/spec/expectations/root/template-production.yaml +31 -0
- data/spec/expectations/root/template-read-inlines.yaml +31 -0
- data/spec/expectations/root/template-read.yaml +31 -0
- data/spec/expectations/root/template-write.yaml +31 -0
- data/spec/expectations/root/template.yaml +31 -0
- data/spec/expectations/root/values.json +28 -0
- data/spec/expectations/state/delete.txt +1 -0
- data/spec/expectations/state/demote.txt +1 -0
- data/spec/expectations/state/init-already-existed.txt +2 -0
- data/spec/expectations/state/init-both-created.txt +2 -0
- data/spec/expectations/state/list.json +51 -0
- data/spec/expectations/state/list.txt +6 -0
- data/spec/expectations/state/list.yaml +35 -0
- data/spec/expectations/state/promote.txt +1 -0
- data/spec/expectations/state/prune-candidates.txt +1 -0
- data/spec/expectations/state/retag.txt +1 -0
- data/spec/expectations/state/set.txt +1 -0
- data/spec/expectations/state/show.json +13 -0
- data/spec/expectations/state/show.txt +13 -0
- data/spec/expectations/state/show.yaml +8 -0
- data/spec/expectations/state/status-all-working.txt +4 -0
- data/spec/expectations/state/status-not-provisioned.txt +4 -0
- data/spec/expectations/values/order-of-precedence.yaml +4 -0
- data/spec/main/configmap_backend_spec.rb +109 -0
- data/spec/main/state_entry_set_spec.rb +112 -0
- data/spec/main/state_entry_spec.rb +109 -0
- data/spec/spec_helper.rb +87 -1
- data/spec/utils/helm_spec.rb +1 -21
- data/spec/utils/k8s_auth_spec.rb +32 -0
- data/spec/utils/misc_utils_spec.rb +9 -0
- data/spec/utils/values_utils_spec.rb +12 -19
- metadata +114 -13
- data/lib/cli/base.rb +0 -83
- data/lib/config/cli_opts.rb +0 -50
- data/lib/config/manager.rb +0 -36
- data/lib/main/state_manager.rb +0 -47
- data/lib/utils/kubectl.rb +0 -58
- data/spec/main/examples_spec.rb +0 -12
- data/spec/main/state_manager_spec.rb +0 -84
- data/spec/utils/state_utils.rb +0 -15
data/lib/main/errors.rb
ADDED
@@ -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
|
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
|
-
|
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
|
-
|
209
|
-
"#{
|
210
|
-
"#{
|
211
|
-
"#{dir}/#{
|
212
|
-
"#{dir}/#{
|
213
|
-
"#{dir}/#{
|
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
|