kerbi 0.0.1 → 0.0.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.
- checksums.yaml +4 -4
- data/lib/cli/base_handler.rb +194 -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/release_handler.rb +41 -0
- data/lib/cli/release_serializer.rb +46 -0
- data/lib/cli/root_handler.rb +34 -13
- data/lib/cli/state_handler.rb +88 -0
- data/lib/cli/values_handler.rb +4 -3
- data/{boilerplate → lib/code-gen/new-project}/Gemfile.erb +0 -0
- data/lib/code-gen/new-project/kerbifile.rb.erb +9 -0
- data/lib/code-gen/new-project/values.yaml.erb +1 -0
- data/lib/config/cli_schema.rb +343 -28
- data/lib/config/config_file.rb +60 -0
- data/lib/config/globals.rb +4 -0
- data/lib/config/run_opts.rb +162 -0
- data/lib/config/state_consts.rb +11 -0
- data/lib/kerbi.rb +35 -10
- data/lib/main/code_gen.rb +1 -1
- data/lib/main/errors.rb +115 -0
- data/lib/main/mixer.rb +20 -10
- data/lib/mixins/cli_state_helpers.rb +108 -0
- data/lib/mixins/cm_backend_testing.rb +109 -0
- data/lib/mixins/entry_tag_logic.rb +183 -0
- data/lib/state/base_backend.rb +93 -0
- data/lib/state/config_map_backend.rb +173 -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 +108 -9
- 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/release_handler_spec.rb +127 -0
- data/spec/cli/root_handler_spec.rb +100 -0
- data/spec/cli/state_handler_spec.rb +108 -0
- data/spec/cli/values_handler_spec.rb +17 -0
- data/spec/fixtures/expectations/common/bad-tag.txt +1 -0
- data/spec/fixtures/expectations/config/bad-set.txt +1 -0
- data/spec/fixtures/expectations/config/set.txt +1 -0
- data/spec/fixtures/expectations/config/show-default.yaml +6 -0
- data/spec/fixtures/expectations/release/delete.txt +1 -0
- data/spec/fixtures/expectations/release/init-already-existed.txt +2 -0
- data/spec/fixtures/expectations/release/init-both-created.txt +2 -0
- data/spec/fixtures/expectations/release/list.txt +5 -0
- data/spec/fixtures/expectations/release/status-all-working.txt +5 -0
- data/spec/fixtures/expectations/release/status-data-unreadable.txt +5 -0
- data/spec/fixtures/expectations/release/status-not-provisioned.txt +5 -0
- data/spec/fixtures/expectations/root/template-inlines.yaml +31 -0
- data/spec/fixtures/expectations/root/template-production.yaml +31 -0
- data/spec/fixtures/expectations/root/template-read-inlines.yaml +31 -0
- data/spec/fixtures/expectations/root/template-read.yaml +31 -0
- data/spec/fixtures/expectations/root/template-write.yaml +31 -0
- data/spec/fixtures/expectations/root/template.yaml +31 -0
- data/spec/fixtures/expectations/root/values.json +28 -0
- data/spec/fixtures/expectations/state/delete.txt +1 -0
- data/spec/fixtures/expectations/state/demote.txt +1 -0
- data/spec/fixtures/expectations/state/list.json +51 -0
- data/spec/fixtures/expectations/state/list.txt +6 -0
- data/spec/fixtures/expectations/state/list.yaml +35 -0
- data/spec/fixtures/expectations/state/promote.txt +1 -0
- data/spec/fixtures/expectations/state/prune-candidates.txt +1 -0
- data/spec/fixtures/expectations/state/retag.txt +1 -0
- data/spec/fixtures/expectations/state/set.txt +1 -0
- data/spec/fixtures/expectations/state/show.json +13 -0
- data/spec/fixtures/expectations/state/show.txt +13 -0
- data/spec/fixtures/expectations/state/show.yaml +8 -0
- data/spec/fixtures/expectations/values/order-of-precedence.yaml +4 -0
- data/spec/main/configmap_backend_spec.rb +110 -0
- data/spec/main/project_code_gen_spec.rb +8 -2
- data/spec/main/state_entry_set_spec.rb +112 -0
- data/spec/main/state_entry_spec.rb +109 -0
- data/spec/mini-projects/hello-kerbi/common/metadata.yaml.erb +5 -0
- data/spec/mini-projects/hello-kerbi/consts.rb +5 -0
- data/spec/mini-projects/hello-kerbi/helpers.rb +8 -0
- data/spec/mini-projects/hello-kerbi/kerbifile.rb +18 -0
- data/spec/mini-projects/hello-kerbi/pod-and-service.yaml.erb +23 -0
- data/spec/mini-projects/hello-kerbi/values/production.yaml +2 -0
- data/spec/mini-projects/hello-kerbi/values/v2.yaml +2 -0
- data/spec/mini-projects/hello-kerbi/values/values.yaml +4 -0
- data/spec/spec_helper.rb +143 -1
- data/spec/utils/helm_spec.rb +89 -109
- 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 +143 -16
- data/boilerplate/kerbifile.rb.erb +0 -9
- data/boilerplate/values.yaml.erb +0 -1
- 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
@@ -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,93 @@
|
|
1
|
+
module Kerbi
|
2
|
+
module State
|
3
|
+
class BaseBackend
|
4
|
+
|
5
|
+
attr_reader :is_working
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [Kerbi::State::EntrySet]
|
11
|
+
def entry_set
|
12
|
+
@_entry_set ||= EntrySet.new(read_entries)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Array<Kerbi::State::Entry>]
|
16
|
+
def entries
|
17
|
+
entry_set.entries
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [Kerbi::State::Entry] entry
|
21
|
+
def delete_entry(entry)
|
22
|
+
entries.reject! { |e| e.tag == entry.tag }
|
23
|
+
save
|
24
|
+
@_entry_set = nil
|
25
|
+
@_resource = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def save
|
29
|
+
entry_set.validate!
|
30
|
+
update_resource
|
31
|
+
@_entry_set = nil
|
32
|
+
@_resource = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete
|
36
|
+
delete_resource
|
37
|
+
end
|
38
|
+
|
39
|
+
def resource_signature
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
def prime
|
44
|
+
begin
|
45
|
+
resource
|
46
|
+
entries
|
47
|
+
@is_working = true
|
48
|
+
rescue
|
49
|
+
@is_working = false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [TrueClass|FalseClass]
|
54
|
+
def working?
|
55
|
+
prime if @is_working.nil?
|
56
|
+
@is_working
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.type_signature
|
60
|
+
raise NotImplementedError
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def resource
|
66
|
+
@_resource ||= load_resource
|
67
|
+
end
|
68
|
+
|
69
|
+
def update_resource
|
70
|
+
raise NotImplementedError
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Array<Hash>]
|
74
|
+
def read_entries
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
def load_resource
|
79
|
+
raise NotImplementedError
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete_resource
|
83
|
+
raise NotImplementedError
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def utils
|
89
|
+
Kerbi::Utils
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module Kerbi
|
2
|
+
module State
|
3
|
+
|
4
|
+
##
|
5
|
+
# Treats a Kubernetes configmap in a namespace as a
|
6
|
+
# persistent store for state entries. Reads and writes
|
7
|
+
# to the configmap.
|
8
|
+
class ConfigMapBackend < Kerbi::State::BaseBackend
|
9
|
+
include Kerbi::Mixins::CmBackendTesting
|
10
|
+
|
11
|
+
attr_reader :auth_bundle
|
12
|
+
attr_reader :release_name
|
13
|
+
attr_reader :namespace
|
14
|
+
|
15
|
+
# @param [Hash] auth_bundle generated by Kerbi::Utils::K8sAuth
|
16
|
+
# @param [String] release_name Kubernetes namespace where configmap lives
|
17
|
+
def initialize(auth_bundle, release_name, namespace)
|
18
|
+
@auth_bundle = auth_bundle.freeze
|
19
|
+
@release_name = release_name.freeze
|
20
|
+
@namespace = (namespace || @release_name).freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Checks for the namespace and configmap, creating along
|
25
|
+
# the way if missing. Does not raise if already exists.
|
26
|
+
# @param [Hash] opts for things like verbose
|
27
|
+
def provision_missing_resources(**opts)
|
28
|
+
create_namespace unless (ns_existed = namespace_exists?)
|
29
|
+
echo_init("namespaces/#{namespace}", ns_existed, opts)
|
30
|
+
|
31
|
+
create_resource unless (cm_existed = resource_exists?)
|
32
|
+
echo_init("#{namespace}/configmaps/#{cm_name}", cm_existed, opts)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Creates the configmap with 0 entries.
|
37
|
+
def create_resource
|
38
|
+
apply_resource(template_resource([]))
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Creates the configmap given an exact dict representation
|
43
|
+
# of its contents. This method doesn't actually get used outside
|
44
|
+
# of rspec, but it's super useful there so keeping for time being.
|
45
|
+
# @param [Hash] resource_desc
|
46
|
+
def apply_resource(resource_desc, mode: 'create')
|
47
|
+
if mode == 'create'
|
48
|
+
#noinspection RubyResolve
|
49
|
+
client("v1").create_config_map(resource_desc)
|
50
|
+
elsif mode == 'update'
|
51
|
+
#noinspection RubyResolve
|
52
|
+
client("v1").update_config_map(resource_desc)
|
53
|
+
else
|
54
|
+
raise "What kind of sick mode is #{mode}?"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Outputs the dict representation of the configmap, templated
|
60
|
+
# with the given entries.
|
61
|
+
# @param [Array<Kerbi::State::Entry>] entries
|
62
|
+
# @return [Hash]
|
63
|
+
def template_resource(entries)
|
64
|
+
values = {
|
65
|
+
consts::ENTRIES_ATTR => entries.map(&:to_h),
|
66
|
+
namespace: namespace,
|
67
|
+
cm_name: cm_name
|
68
|
+
}
|
69
|
+
Kerbi::State::ConfigMapMixer.new(values).run.first
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Creates the required namespace resource for this configmap
|
74
|
+
# in the cluster.
|
75
|
+
def create_namespace
|
76
|
+
values = { namespace: namespace }
|
77
|
+
dict = Kerbi::State::NamespaceMixer.new(values).run.first
|
78
|
+
#noinspection RubyResolve
|
79
|
+
client("v1").create_namespace(dict)
|
80
|
+
end
|
81
|
+
|
82
|
+
def resource_name
|
83
|
+
cm_name
|
84
|
+
end
|
85
|
+
|
86
|
+
def resource_signature
|
87
|
+
"configmaps/#{namespace}/#{resource_name}"
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
##
|
93
|
+
# Reads the configmap from Kubernetes, returns its dict representation.
|
94
|
+
def load_resource
|
95
|
+
#noinspection RubyResolve
|
96
|
+
client("v1").get_config_map(cm_name, namespace).to_h
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Reads the configmap from Kubernetes, returns its dict representation.
|
101
|
+
def delete_resource
|
102
|
+
#noinspection RubyResolve
|
103
|
+
client("v1").delete_config_map(cm_name, namespace)
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Templates the updated version of the configmap given the entries
|
108
|
+
# in memory, and uses the new dict to overwrite the last configmap
|
109
|
+
# in the cluster.
|
110
|
+
def update_resource
|
111
|
+
new_resource = template_resource(entries)
|
112
|
+
#noinspection RubyResolve
|
113
|
+
client("v1").update_config_map(new_resource)
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Deserializes the list of entries in the configmap. Calls
|
118
|
+
# #resources, which is memoized, so may trigger a cluster read.
|
119
|
+
# @return [Array<Hash>] entries
|
120
|
+
def read_entries
|
121
|
+
str_entries = resource[:data][consts::ENTRIES_ATTR]
|
122
|
+
JSON.parse(str_entries)
|
123
|
+
end
|
124
|
+
|
125
|
+
## Creates an instance of Kubeclient::Client given
|
126
|
+
# the auth_bundle in the state, and a Kubernetes API name
|
127
|
+
# like appsV1 (defaults to "v1" if not passed).
|
128
|
+
# @return [Kubeclient::Client]
|
129
|
+
def client(api_name="v1")
|
130
|
+
self.class.make_client(auth_bundle, api_name)
|
131
|
+
end
|
132
|
+
|
133
|
+
def consts
|
134
|
+
Kerbi::State::Consts
|
135
|
+
end
|
136
|
+
|
137
|
+
def cm_name
|
138
|
+
self.class.mk_cm_name(release_name)
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.releases(auth_bundle)
|
142
|
+
client = make_client(auth_bundle, "v1")
|
143
|
+
res_dicts = client.get_config_maps.map(&:to_h).select do |res_dict|
|
144
|
+
name = res_dict.dig(:metadata, :name)
|
145
|
+
name =~ Kerbi::State::Consts::CM_REGEX
|
146
|
+
end
|
147
|
+
|
148
|
+
res_dicts.map do |res_dict|
|
149
|
+
name, namespace = res_dict[:metadata].values_at(:name, :namespace)
|
150
|
+
release = name.match(Kerbi::State::Consts::CM_REGEX)[1]
|
151
|
+
self.new(auth_bundle, release, namespace)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.mk_cm_name(release_name)
|
156
|
+
"kerbi-#{release_name}-db"
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.make_client(auth_bundle, api_name)
|
160
|
+
Kubeclient::Client.new(
|
161
|
+
auth_bundle[:endpoint],
|
162
|
+
api_name,
|
163
|
+
**auth_bundle[:options]
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.type_signature
|
168
|
+
"ConfigMap"
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/state/entry.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
module Kerbi
|
2
|
+
module State
|
3
|
+
|
4
|
+
##
|
5
|
+
# Represents a single Kerbi state entry.
|
6
|
+
class Entry
|
7
|
+
|
8
|
+
CANDIDATE_PREFIX = "[cand]-"
|
9
|
+
|
10
|
+
ATTRS = %i[tag message values default_values created_at]
|
11
|
+
SETTABLE_ATTRS = %i[message created_at]
|
12
|
+
|
13
|
+
attr_accessor :set
|
14
|
+
|
15
|
+
attr_accessor :tag
|
16
|
+
attr_accessor :message
|
17
|
+
attr_accessor :default_values
|
18
|
+
attr_accessor :values
|
19
|
+
attr_accessor :created_at
|
20
|
+
|
21
|
+
attr_reader :validation_errors
|
22
|
+
|
23
|
+
def initialize(set, dict)
|
24
|
+
@set = set
|
25
|
+
ATTRS.each do |attr|
|
26
|
+
instance_variable_set("@#{attr}", dict[attr].freeze)
|
27
|
+
end
|
28
|
+
@_was_validated = false
|
29
|
+
@validation_errors = []
|
30
|
+
end
|
31
|
+
|
32
|
+
## A state entry is a 'candidate' if its tag has the
|
33
|
+
# candidate signature - that is it starts with [cand]-.
|
34
|
+
# @return [TrueClass, FalseClass]
|
35
|
+
def candidate?
|
36
|
+
tag.start_with?(CANDIDATE_PREFIX)
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Convenience method that returns the negation of #candidate?
|
41
|
+
# @return [TrueClass, FalseClass]
|
42
|
+
def committed?
|
43
|
+
!candidate?
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Ghetto attribute validation. Pushes a attr => msg hash to the
|
48
|
+
# @validation_errors for every problem found. Does not raise on
|
49
|
+
# problems.
|
50
|
+
# @return [NilClass]
|
51
|
+
def validate
|
52
|
+
@validation_errors.push(
|
53
|
+
attr: 'tag',
|
54
|
+
msg: "Cannot be empty",
|
55
|
+
value: tag
|
56
|
+
) unless tag.present?
|
57
|
+
|
58
|
+
@_was_validated = true
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid?
|
62
|
+
raise "valid? called before #validate" unless @_was_validated
|
63
|
+
validation_errors.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Computes a delta between this state's values and its
|
68
|
+
# default values.
|
69
|
+
# @return [Hash]
|
70
|
+
def overrides_delta
|
71
|
+
if values.is_a?(Hash) & default_values.is_a?(Hash)
|
72
|
+
Kerbi::Utils::Misc.deep_hash_diff(default_values, values)
|
73
|
+
else
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Dynamically assign as a user.
|
80
|
+
# @param [String|Symbol] attr_name
|
81
|
+
# @param [Object] new_value
|
82
|
+
# @return [String] the old value, for convenience
|
83
|
+
def assign_attr(attr_name, new_value)
|
84
|
+
if SETTABLE_ATTRS.include?(attr_name.to_sym)
|
85
|
+
old_value = send(attr_name)
|
86
|
+
send("#{attr_name}=", new_value)
|
87
|
+
old_value
|
88
|
+
else
|
89
|
+
raise Kerbi::NoSuchStateAttrName
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Replace current tag with a new one, where the new
|
95
|
+
# one can contain special interpolatable words like
|
96
|
+
# @candidate.
|
97
|
+
# @param [String] new_tag_expr
|
98
|
+
# @return [String] the old tag, for convenience
|
99
|
+
def retag(new_tag_expr)
|
100
|
+
old_tag = tag
|
101
|
+
self.tag = set.resolve_write_tag_expr(new_tag_expr)
|
102
|
+
old_tag
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Removes the [cand]- part of the tag, making this
|
107
|
+
# entry lose its candidate status.
|
108
|
+
#
|
109
|
+
# Raises an exception if this entry was not a candidate.
|
110
|
+
# @return [String] the old tag, for convenience
|
111
|
+
def promote
|
112
|
+
raise Kerbi::StateNotPromotable unless candidate?
|
113
|
+
old_tag = tag
|
114
|
+
self.tag = tag[CANDIDATE_PREFIX.length..]
|
115
|
+
old_tag
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Adds the [cand]- flag to this entry's tag, making this
|
120
|
+
# entry gain candidate status.
|
121
|
+
#
|
122
|
+
# Raises an exception if this entry was already a candidate.
|
123
|
+
# @return [String] the old tag, for convenience
|
124
|
+
def demote
|
125
|
+
raise Kerbi::StateNotDemotable unless committed?
|
126
|
+
old_tag = tag
|
127
|
+
self.tag = "#{CANDIDATE_PREFIX}#{tag}"
|
128
|
+
old_tag
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Convenience method to get all overridden keys between
|
133
|
+
# the values and default_values dicts.
|
134
|
+
# @return [Array<String>]
|
135
|
+
def overridden_keys
|
136
|
+
(delta = overrides_delta) ? delta.keys.map(&:to_s) : []
|
137
|
+
end
|
138
|
+
|
139
|
+
def to_h
|
140
|
+
special_ser = {
|
141
|
+
values: values || {},
|
142
|
+
default_values: default_values || {},
|
143
|
+
created_at: created_at.to_s
|
144
|
+
}
|
145
|
+
Hash[ATTRS.map{|k|[k, send(k)]}].merge(special_ser)
|
146
|
+
end
|
147
|
+
alias_method :serialize, :to_h
|
148
|
+
|
149
|
+
def to_json
|
150
|
+
JSON.dump(serialize)
|
151
|
+
end
|
152
|
+
|
153
|
+
# @param [Hash] dict
|
154
|
+
# @return [Kerbi::State::Entry]
|
155
|
+
def self.from_dict(set, dict={})
|
156
|
+
dict.deep_symbolize_keys!
|
157
|
+
dict.slice!(*ATTRS)
|
158
|
+
|
159
|
+
self.new(
|
160
|
+
set,
|
161
|
+
**dict,
|
162
|
+
values: dict[:values] || {},
|
163
|
+
default_values: dict[:default_values] || {},
|
164
|
+
created_at: (Time.parse(dict[:created_at]) rescue nil)
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.versioned?(expr)
|
169
|
+
Gem::Version.correct?(expr)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|