matsuri 0.1.0 → 0.3.0
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 +5 -5
- data/.envrc +1 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +9 -0
- data/AGENTS.md +18 -0
- data/CHANGELOG.md +642 -0
- data/Gemfile +2 -0
- data/README.md +86 -8
- data/Rakefile +20 -0
- data/bin/console +1 -0
- data/bin/matsuri +6 -0
- data/bin/setup +2 -0
- data/docs/acceptors.md +181 -0
- data/docs/boot.md +101 -0
- data/docs/design/acceptors.md +157 -0
- data/docs/design/boot.md +131 -0
- data/docs/design/ssa.md +142 -0
- data/docs/design/stacks.md +153 -0
- data/docs/diff.md +85 -0
- data/docs/known-issues.md +19 -0
- data/docs/local-dev.md +173 -0
- data/docs/presenters.md +317 -0
- data/docs/resources.md +121 -0
- data/docs/setup.md +97 -0
- data/docs/ssa.md +149 -0
- data/docs/stacks.md +146 -0
- data/flake.lock +61 -0
- data/flake.nix +35 -0
- data/lib/matsuri/acceptor.rb +33 -0
- data/lib/matsuri/acceptors/image_tag.rb +15 -0
- data/lib/matsuri/cmd.rb +46 -0
- data/lib/matsuri/cmds/apply.rb +78 -0
- data/lib/matsuri/cmds/cli.rb +147 -0
- data/lib/matsuri/cmds/create.rb +87 -0
- data/lib/matsuri/cmds/delete.rb +86 -0
- data/lib/matsuri/cmds/diff.rb +105 -0
- data/lib/matsuri/cmds/generate.rb +24 -0
- data/lib/matsuri/cmds/kubectl.rb +16 -0
- data/lib/matsuri/cmds/recreate.rb +65 -0
- data/lib/matsuri/cmds/scale.rb +37 -0
- data/lib/matsuri/cmds/show.rb +123 -0
- data/lib/matsuri/cmds/status.rb +67 -0
- data/lib/matsuri/concerns/awaiting.rb +24 -0
- data/lib/matsuri/concerns/diffable.rb +135 -0
- data/lib/matsuri/concerns/export_manifest.rb +56 -0
- data/lib/matsuri/concerns/import_manifest.rb +64 -0
- data/lib/matsuri/concerns/pod_template.rb +37 -0
- data/lib/matsuri/concerns/pods/spread_pods.rb +47 -0
- data/lib/matsuri/concerns/pods/spread_topology.rb +63 -0
- data/lib/matsuri/concerns/registry_helpers.rb +59 -0
- data/lib/matsuri/config.rb +101 -0
- data/lib/matsuri/core_ext.rb +107 -0
- data/lib/matsuri/dsl/cluster/aggregated_cluster_role.rb +75 -0
- data/lib/matsuri/dsl/cluster/binding.rb +118 -0
- data/lib/matsuri/dsl/cluster/cluster_role.rb +41 -0
- data/lib/matsuri/dsl/cluster/role.rb +49 -0
- data/lib/matsuri/dsl/cluster/scope.rb +245 -0
- data/lib/matsuri/dsl/cluster/service_account.rb +76 -0
- data/lib/matsuri/dsl/concerns/default_binding.rb +34 -0
- data/lib/matsuri/dsl/concerns/manifest_set.rb +21 -0
- data/lib/matsuri/dsl/concerns/metadata.rb +56 -0
- data/lib/matsuri/dsl/concerns/rbac_rules.rb +39 -0
- data/lib/matsuri/initializer.rb +41 -0
- data/lib/matsuri/kubernetes/base.rb +107 -0
- data/lib/matsuri/kubernetes/config_map.rb +100 -0
- data/lib/matsuri/kubernetes/cron_job.rb +115 -0
- data/lib/matsuri/kubernetes/daemon_set.rb +62 -0
- data/lib/matsuri/kubernetes/deployment.rb +65 -0
- data/lib/matsuri/kubernetes/endpoints.rb +57 -0
- data/lib/matsuri/kubernetes/horizontal_pod_autoscaler.rb +116 -0
- data/lib/matsuri/kubernetes/imported_manifest.rb +61 -0
- data/lib/matsuri/kubernetes/ingress.rb +66 -0
- data/lib/matsuri/kubernetes/persistent_volume.rb +56 -0
- data/lib/matsuri/kubernetes/persistent_volume_claim.rb +41 -0
- data/lib/matsuri/kubernetes/pod.rb +355 -0
- data/lib/matsuri/kubernetes/replica_set.rb +46 -0
- data/lib/matsuri/kubernetes/secret.rb +86 -0
- data/lib/matsuri/kubernetes/service.rb +49 -0
- data/lib/matsuri/kubernetes/stateful_set.rb +75 -0
- data/lib/matsuri/kubernetes/storage_class.rb +45 -0
- data/lib/matsuri/platform.rb +38 -0
- data/lib/matsuri/presenter.rb +47 -0
- data/lib/matsuri/presenters/helpers.rb +39 -0
- data/lib/matsuri/presenters/prune_empty.rb +59 -0
- data/lib/matsuri/presenters/seed_image.rb +28 -0
- data/lib/matsuri/presenters/seed_replicas.rb +25 -0
- data/lib/matsuri/presenters/seed_resources.rb +29 -0
- data/lib/matsuri/registry.rb +267 -0
- data/lib/matsuri/shell_out.rb +119 -0
- data/lib/matsuri/stack.rb +276 -0
- data/lib/matsuri/task.rb +20 -0
- data/lib/matsuri/tasks/cluster.rb +79 -0
- data/lib/matsuri/tasks/docker.rb +25 -0
- data/lib/matsuri/tasks/hash_diff.rb +61 -0
- data/lib/matsuri/tasks/kubectl.rb +21 -0
- data/lib/matsuri/tasks/kubectl_ops/apply.rb +24 -0
- data/lib/matsuri/tasks/kubectl_ops/create.rb +27 -0
- data/lib/matsuri/tasks/kubectl_ops/delete.rb +21 -0
- data/lib/matsuri/tasks/kubectl_ops/diff.rb +47 -0
- data/lib/matsuri/tasks/kubectl_ops/get.rb +36 -0
- data/lib/matsuri/tasks/kubectl_ops/recreate.rb +35 -0
- data/lib/matsuri/tasks/kubectl_ops/replace.rb +29 -0
- data/lib/matsuri/tasks/kubectl_ops/rollout_status.rb +22 -0
- data/lib/matsuri/tasks/kubectl_ops/scale.rb +22 -0
- data/lib/matsuri/tasks/kubectl_ops/server_side_apply.rb +32 -0
- data/lib/matsuri/tasks/kubectl_ops/status.rb +20 -0
- data/lib/matsuri/tasks/kubectl_ops.rb +218 -0
- data/lib/matsuri/tasks/pod.rb +56 -0
- data/lib/matsuri/tasks/stacks/seed.rb +93 -0
- data/lib/matsuri/tasks/stacks.rb +31 -0
- data/lib/matsuri/transform/algebra.rb +52 -0
- data/lib/matsuri/transform/callable.rb +54 -0
- data/lib/matsuri/transform/combinators.rb +61 -0
- data/lib/matsuri/transform/env.rb +43 -0
- data/lib/matsuri/transform/kubernetes.rb +89 -0
- data/lib/matsuri/transform.rb +20 -0
- data/lib/matsuri/version.rb +3 -1
- data/lib/matsuri.rb +158 -2
- data/matsuri.gemspec +20 -7
- data/tests/bin/minikube-start.sh +27 -0
- data/tests/bin/probe-save-config.sh +46 -0
- data/tests/bin/probe-ssa.sh +206 -0
- data/tests/example/.gitignore +1 -0
- data/tests/example/Gemfile +7 -0
- data/tests/example/README.md +91 -0
- data/tests/example/bin/matsuri +14 -0
- data/tests/example/config/initializer.rb +12 -0
- data/tests/example/config/matsuri.rb +9 -0
- data/tests/example/config/platform.rb +39 -0
- data/tests/example/lib/acme/nodes/dedicated.rb +26 -0
- data/tests/example/lib/acme/nodes/preemptible.rb +17 -0
- data/tests/example/lib/acme/nodes/required_cores.rb +26 -0
- data/tests/example/lib/acme/nodes/resource_class.rb +26 -0
- data/tests/example/lib/acme/pods/cacerts.rb +15 -0
- data/tests/example/lib/acme/pods/healthcheck.rb +36 -0
- data/tests/example/lib/acme.rb +26 -0
- data/tests/example/platform/config_maps/billing-settings.rb +8 -0
- data/tests/example/platform/config_maps/cacerts.rb +9 -0
- data/tests/example/platform/deployments/billing-web.rb +5 -0
- data/tests/example/platform/deployments/checkout-web.rb +6 -0
- data/tests/example/platform/namespaces/billing-dev.yaml +7 -0
- data/tests/example/platform/namespaces/checkout-dev.yaml +7 -0
- data/tests/example/platform/pods/billing.rb +31 -0
- data/tests/example/platform/pods/checkout.rb +27 -0
- data/tests/example/platform/services/billing-web.rb +6 -0
- data/tests/example/platform/services/checkout-web.rb +4 -0
- data/tests/example/platform/stacks/billing.rb +8 -0
- data/tests/example/platform/stacks/checkout.rb +19 -0
- data/tests/k8s/save-config/deployment-declared.yaml +19 -0
- data/tests/k8s/save-config/deployment-seeded.yaml +20 -0
- data/tests/k8s/save-config/namespace.yaml +4 -0
- data/tests/k8s/ssa/declared-bumped.yaml +22 -0
- data/tests/k8s/ssa/declared.yaml +22 -0
- data/tests/k8s/ssa/namespace.yaml +4 -0
- data/tests/k8s/ssa/seed-via-create.yaml +24 -0
- metadata +288 -20
- data/.travis.yml +0 -4
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'map'
|
|
4
|
+
require 'matsuri/core_ext'
|
|
5
|
+
require 'matsuri/acceptors/image_tag'
|
|
6
|
+
require 'rainbow/ext/string'
|
|
7
|
+
require 'rlet'
|
|
8
|
+
require 'rlet/lazy_options'
|
|
9
|
+
|
|
10
|
+
module Matsuri
|
|
11
|
+
class Stack
|
|
12
|
+
include Let
|
|
13
|
+
include RLet::LazyOptions
|
|
14
|
+
include Matsuri::Concerns::RegistryHelpers
|
|
15
|
+
include Matsuri::Concerns::Awaiting
|
|
16
|
+
|
|
17
|
+
class_attribute :build_order
|
|
18
|
+
class_attribute :acceptors
|
|
19
|
+
class_attribute :presenters
|
|
20
|
+
|
|
21
|
+
class_attribute :save_config_policy
|
|
22
|
+
|
|
23
|
+
# Declaration-surface vocabulary: match / match_kind / match_deployments
|
|
24
|
+
# wrap a present member so it applies selectively; seed_* constructors
|
|
25
|
+
# close a seed concern's hole over a declaration-time value.
|
|
26
|
+
extend Matsuri::Transform::Combinators::Helpers
|
|
27
|
+
extend Matsuri::Presenters::Helpers
|
|
28
|
+
|
|
29
|
+
# The collection name is the options key: each acceptor admits its own
|
|
30
|
+
# component of the invocation options, whole. The built-in :image_tag
|
|
31
|
+
# collection carries the converge positional and owns the 'latest'
|
|
32
|
+
# default; :config carries the stack-config artifact, already loaded —
|
|
33
|
+
# whoever constructs the stack supplies data, never a path.
|
|
34
|
+
let(:image_tag) { accepted(:image_tag).image_tag }
|
|
35
|
+
let(:stack_config) { accepted(:config) }
|
|
36
|
+
|
|
37
|
+
# Defines a list of deps, used to to idempotently converge Kubernetes
|
|
38
|
+
# resources. Generally, you will add a stack as a dep for resources you are
|
|
39
|
+
# sharing with other stacks, while directly referencing Kubernetes resources
|
|
40
|
+
# for things this stack is directly responsible for.
|
|
41
|
+
#
|
|
42
|
+
# Examples:
|
|
43
|
+
# needs :stack, 'postgresql-95'
|
|
44
|
+
# needs :stack, 'redis-28'
|
|
45
|
+
# needs :stack, 'load-balancer'
|
|
46
|
+
# needs :deployment, 'rails-app-worker'
|
|
47
|
+
# needs :deployment, 'rails-app'
|
|
48
|
+
# needs :service, 'rails-app'
|
|
49
|
+
#
|
|
50
|
+
# Hooks:
|
|
51
|
+
# check_only: when set to true, do not create or apply resource. Only check for it's presence
|
|
52
|
+
# on_failure: a symbol of a method to call when check fails.
|
|
53
|
+
#
|
|
54
|
+
# @TODO Consider making this lazy-loaded. We don't need to generate this elaborate data structure
|
|
55
|
+
# unless we are converging. (But it might not really make things that much slower).
|
|
56
|
+
class << self
|
|
57
|
+
def needs(type, name, on_failure: nil, check_only: false)
|
|
58
|
+
self.build_order ||= []
|
|
59
|
+
self.build_order << [type.to_sym, name.to_s, { on_failure: on_failure, check_only: check_only }]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def config_file(path)
|
|
63
|
+
File.join(Matsuri::Config.config_path, path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Registers an acceptor under a named collection. Last declaration
|
|
67
|
+
# per collection wins; a stack overrides a built-in by re-declaring it.
|
|
68
|
+
def accept(collection, acceptor)
|
|
69
|
+
self.acceptors = (acceptors || {}).merge(collection.to_sym => acceptor)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Appends transform members to a scenario's pipeline. Unlike accept,
|
|
73
|
+
# where a collection names a slot and last declaration wins, a
|
|
74
|
+
# scenario accumulates: declaration order is member order. Members
|
|
75
|
+
# are presenter names, presenter classes, Callables, or anything
|
|
76
|
+
# #call-able; scenarios are intent regimes (:on_create brings into
|
|
77
|
+
# existence, :on_apply delivers to something existing) and each
|
|
78
|
+
# delivery verb assembles its own.
|
|
79
|
+
def present(scenario, *steps)
|
|
80
|
+
key = scenario.to_sym
|
|
81
|
+
current = presenters || {}
|
|
82
|
+
self.presenters = current.merge(key => Array(current[key]) + steps)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Declares the save-config policy for every resource member this
|
|
86
|
+
# stack delivers (overriding the repo-wide Platform setting); a
|
|
87
|
+
# resource overrides again with its own let(:save_config). Normal
|
|
88
|
+
# apply path only.
|
|
89
|
+
def save_config(value)
|
|
90
|
+
self.save_config_policy = value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
accept :image_tag, Matsuri::Acceptors::ImageTag
|
|
95
|
+
# Empty acceptor: the collection is total, its surface is empty until
|
|
96
|
+
# a platform re-declares it.
|
|
97
|
+
accept :config, Matsuri::Acceptor
|
|
98
|
+
|
|
99
|
+
# Override this. This commands defines how to display the status
|
|
100
|
+
# Maybe we can find some useful, standardized way of displaying
|
|
101
|
+
# status.
|
|
102
|
+
def status!
|
|
103
|
+
fail NoMethodError, 'Must implement #status!'
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Renders every member manifest as one kubectl-compatible YAML stream.
|
|
107
|
+
def show!(_opt = {})
|
|
108
|
+
members.each { |member| puts member.to_yaml }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def diff!(opt = {})
|
|
112
|
+
members.each do |member|
|
|
113
|
+
# --primary-container is a HashDiff-only projection, so it
|
|
114
|
+
# implies --hash-diff
|
|
115
|
+
if opt[:hash_diff] || opt[:primary_container]
|
|
116
|
+
Matsuri::Tasks::HashDiff.new(resource: member, primary_container: opt[:primary_container],
|
|
117
|
+
presenters: stack_presenters).run!
|
|
118
|
+
else
|
|
119
|
+
Matsuri::Tasks::KubectlOps.diff(member, presenters: stack_presenters)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Override this. This command defines how all the stack depenencies
|
|
125
|
+
# are started, such as services, endpoints, replication controllers, etc.
|
|
126
|
+
def create!
|
|
127
|
+
converge! # Most of the time, we want to converge
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Override this. This command defines how all stack dependencies are stopped
|
|
131
|
+
def delete!
|
|
132
|
+
self.class.build_order.each do |(type, name)|
|
|
133
|
+
if type == :stack
|
|
134
|
+
puts "Skipping stack #{name}"
|
|
135
|
+
next
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Same options as converge, so a member whose identity derives from
|
|
139
|
+
# an accepted value addresses the same resource it created.
|
|
140
|
+
Matsuri::Tasks::KubectlOps.delete(dep(type, name).new(dep_options(type)))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def recreate!
|
|
145
|
+
delete!
|
|
146
|
+
create!
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def rebuild!
|
|
150
|
+
build!
|
|
151
|
+
recreate!
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def converge!(opts = {})
|
|
155
|
+
puts "Converging #{name}".color(:red).bright if config.verbose
|
|
156
|
+
self.class.build_order.each do |(type, name, options)|
|
|
157
|
+
case type
|
|
158
|
+
when :file
|
|
159
|
+
Matsuri.log :info, "Checking for required file #{name}"
|
|
160
|
+
next if File.file?(name)
|
|
161
|
+
Matsuri.log(:fatal, "Cannot find required file #{name}") unless options[:on_failure]
|
|
162
|
+
send(options[:on_failure], type, name) if options[:on_failure]
|
|
163
|
+
when :task
|
|
164
|
+
Matsuri.log :info, "Executing task #{name}"
|
|
165
|
+
send(name, opts)
|
|
166
|
+
next
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
resource = dep(type, name).new(dep_options(type))
|
|
170
|
+
# @TODO if-elsif-end ladder code smell, refactor
|
|
171
|
+
if options[:check_only]
|
|
172
|
+
next if Matsuri::Tasks::KubectlOps.exists?(resource)
|
|
173
|
+
send(options[:on_failure], type, name) || Matsuri.log(:fatal, "#{type}/#{name} not found on cluster")
|
|
174
|
+
elsif type == :stack && !opts[:reboot]
|
|
175
|
+
puts "Shallow converging stack #{name}".color(:red).bright if config.verbose
|
|
176
|
+
resource.converge!
|
|
177
|
+
elsif type == :stack
|
|
178
|
+
resource.converge!(opts)
|
|
179
|
+
else
|
|
180
|
+
Matsuri::Tasks::KubectlOps.converge(resource, opts, presenters: stack_presenters)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Hooks
|
|
186
|
+
def reload!(version, opt={})
|
|
187
|
+
Matsuri.log :fatal, "I don't know how to reload #{name}. Define me in the stack definition."
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def update!(version, opt={})
|
|
191
|
+
Matsuri.log :fatal, "I don't know how to update #{name}. Define me in the stack definition."
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def sh!(opt, args)
|
|
195
|
+
Matsuri.log :fatal, "I don't know how to shell into #{name}. Define me in the stack definition."
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def console!(opt, args)
|
|
199
|
+
Matsuri.log :fatal, "I don't know how to get to the console for #{name}. Define me in the stack definition."
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def build!(opt={})
|
|
203
|
+
Matsuri.log :fatal, "I don't know how to build #{name}. Define me in the stack definition."
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def push!(opt={})
|
|
207
|
+
Matsuri.log :fatal, "I don't know how to push #{name}. Define me in the stack definition."
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The docker mechanism lives in Tasks::Docker; the hook declares, the
|
|
211
|
+
# task delivers. Definition-supplied build!/push! hooks call this
|
|
212
|
+
# unchanged. Stacks themselves no longer shell out.
|
|
213
|
+
def docker!(cmd, options = {})
|
|
214
|
+
Matsuri::Tasks::Docker.new.run!(cmd, options)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Helper functions
|
|
218
|
+
# Like stack-config, present declarations never cross a stack boundary:
|
|
219
|
+
# a nested stack assembles from its own class.
|
|
220
|
+
def stack_presenters
|
|
221
|
+
self.class.presenters || {}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# stack-config never crosses a stack boundary: instance identity is the
|
|
225
|
+
# (stack, stack-config) pair, and a nested stack is a different instance.
|
|
226
|
+
# The save-config policy rides to resource members only — a nested
|
|
227
|
+
# stack declares its own.
|
|
228
|
+
def dep_options(type)
|
|
229
|
+
return { image_tag: image_tag } if type == :stack
|
|
230
|
+
|
|
231
|
+
opts = { image_tag: image_tag, stack_config: stack_config }
|
|
232
|
+
opts[:save_config] = self.class.save_config_policy unless self.class.save_config_policy.nil?
|
|
233
|
+
opts
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def members
|
|
237
|
+
self.class.build_order.filter_map do |(type, name, _)|
|
|
238
|
+
next if type == :file || type == :task
|
|
239
|
+
|
|
240
|
+
if type == :stack
|
|
241
|
+
puts "Skipping stack #{name}"
|
|
242
|
+
next
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
dep(type, name).new(dep_options(type))
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def accepted(collection)
|
|
250
|
+
@accepted ||= {}
|
|
251
|
+
@accepted[collection] ||= acceptor_class_for(self.class.acceptors.fetch(collection)).new(options[collection])
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def acceptor_class_for(name)
|
|
255
|
+
name.is_a?(Class) ? name : Matsuri::Registry.acceptor(name)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def dep(type, name)
|
|
259
|
+
Matsuri::Registry.fetch_or_load type, name
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def stack(name, opt={})
|
|
263
|
+
Matsuri::Registry.stack(name).new(opt)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
class << self
|
|
267
|
+
def load_path
|
|
268
|
+
Matsuri::Config.stacks_path
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def definition_module_name
|
|
272
|
+
'Stacks'
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
data/lib/matsuri/task.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rlet'
|
|
4
|
+
require 'rlet/lazy_options'
|
|
5
|
+
|
|
6
|
+
module Matsuri
|
|
7
|
+
module Task
|
|
8
|
+
extend Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
include Let
|
|
12
|
+
include RLet::LazyOptions
|
|
13
|
+
include Matsuri::ShellOut
|
|
14
|
+
|
|
15
|
+
let(:verbose) { options[:verbose] || config.verbose }
|
|
16
|
+
let(:debug) { options[:debug] || config.debug }
|
|
17
|
+
let(:config) { Matsuri::Config }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'matsuri/core_ext'
|
|
5
|
+
|
|
6
|
+
module Matsuri
|
|
7
|
+
module Tasks
|
|
8
|
+
# Converge cluster
|
|
9
|
+
class Cluster
|
|
10
|
+
include Matsuri::Task
|
|
11
|
+
|
|
12
|
+
let(:root_scope) { Matsuri::DSL::Cluster::Scope.new(namespace: nil) }
|
|
13
|
+
|
|
14
|
+
let(:all_files) { global_files + environment_files }
|
|
15
|
+
let(:global_files) { Dir.glob(File.join(clusters_path, Matsuri.environment, '**', '*.rb')) }
|
|
16
|
+
let(:environment_files) { Dir.glob(File.join(clusters_path, '*.rb')) }
|
|
17
|
+
let(:clusters_path) { Matsuri::Config.clusters_path }
|
|
18
|
+
|
|
19
|
+
let(:all_rbac_manifests) { load_all! && root_scope.rbac_manifests.flatten.compact }
|
|
20
|
+
let(:rbac_manifests_to_yaml) { all_rbac_manifests.map(&:deep_stringify_keys).map(&:to_yaml) }
|
|
21
|
+
let(:rbac_manifests_to_json) { all_rbac_manifests.map(&:to_json) }
|
|
22
|
+
let(:rbac_manifests_to_pretty_json) { all_rbac_manifests.map(&JSON.method(:pretty_generate)) }
|
|
23
|
+
|
|
24
|
+
let(:all_cluster_manifests) { load_all! && root_scope.cluster_manifests.flatten.compact }
|
|
25
|
+
let(:cluster_manifests_to_yaml) { all_cluster_manifests.map(&:deep_stringify_keys).map(&:to_yaml) }
|
|
26
|
+
let(:cluster_manifests_to_json) { all_cluster_manifests.map(&:to_json) }
|
|
27
|
+
let(:cluster_manifests_to_pretty_json) { all_cluster_manifests.map(&JSON.method(:pretty_generate)) }
|
|
28
|
+
|
|
29
|
+
# Memoized so it only evaluates once
|
|
30
|
+
let(:load_all!) { all_files.each(&method(:load_definition!)) }
|
|
31
|
+
|
|
32
|
+
def load_definition!(filename)
|
|
33
|
+
root_scope.import(filename)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def show!(opt = {})
|
|
37
|
+
if opt[:json]
|
|
38
|
+
show_json!
|
|
39
|
+
else
|
|
40
|
+
show_yaml!
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def show_json!
|
|
45
|
+
puts '//--- RBAC resources'
|
|
46
|
+
puts rbac_manifests_to_pretty_json
|
|
47
|
+
puts '', '//--- Other resources'
|
|
48
|
+
puts cluster_manifests_to_pretty_json
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def show_yaml!
|
|
52
|
+
puts '##### RBAC resources'
|
|
53
|
+
puts rbac_manifests_to_yaml
|
|
54
|
+
puts '', '##### Other resources'
|
|
55
|
+
puts cluster_manifests_to_yaml
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reconcile!(opt = {})
|
|
59
|
+
puts 'Converging cluster-wide configuration'.color(:yellow) if config.verbose
|
|
60
|
+
reconcile_rbac!(opt)
|
|
61
|
+
apply_cluster!(opt)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reconcile_rbac!(_opt)
|
|
65
|
+
Matsuri.log :info, 'Reconciling RBAC'
|
|
66
|
+
|
|
67
|
+
# kubectl_options = opt[:dry_run] ? '--dry-run' : ''
|
|
68
|
+
|
|
69
|
+
kubectl! 'auth reconcile -f -', input: rbac_manifests_to_json.join("\n"), kube_options: { namespace: nil }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def apply_cluster!(_opt)
|
|
73
|
+
Matsuri.log :info, 'Applying non-RBAC Cluster Resources'
|
|
74
|
+
|
|
75
|
+
kubectl! 'apply -f -', input: cluster_manifests_to_json.join("\n"), kube_options: { namespace: nil }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matsuri
|
|
4
|
+
module Tasks
|
|
5
|
+
class Docker
|
|
6
|
+
include Matsuri::Task
|
|
7
|
+
|
|
8
|
+
# The delivery surface stack hooks delegate to: the hook declares,
|
|
9
|
+
# this task shells out. (ShellOut's own docker! is private on
|
|
10
|
+
# includers — module_function.)
|
|
11
|
+
def run!(cmd, options = {})
|
|
12
|
+
docker!(cmd, options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def kill_all!
|
|
16
|
+
docker! 'stop $(docker ps -a -q)'
|
|
17
|
+
docker! 'rm -f $(docker ps -a -q)'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fix_pts!
|
|
21
|
+
puts 'sudo mount -o remount,ptmxmode=666 /dev/pts'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rainbow/ext/string'
|
|
4
|
+
|
|
5
|
+
module Matsuri
|
|
6
|
+
module Tasks
|
|
7
|
+
# Client-side diff: fetches the current manifest, asks the resource for
|
|
8
|
+
# the filtered deltas (Concerns::Diffable), and prints them. Sibling to
|
|
9
|
+
# KubectlOps rather than inside it — the comparison happens here, kubectl
|
|
10
|
+
# is only the fetch. The server-side counterpart (KubectlOps::Diff,
|
|
11
|
+
# wrapping kubectl diff) shows what the cluster will make of the
|
|
12
|
+
# manifest, admission injections included; this one shows drift relative
|
|
13
|
+
# to the declaration with the noise filtered. Both are kept.
|
|
14
|
+
class HashDiff
|
|
15
|
+
include Matsuri::Task
|
|
16
|
+
|
|
17
|
+
let(:resource) { options.fetch(:resource) }
|
|
18
|
+
let(:primary_container) { options[:primary_container] }
|
|
19
|
+
let(:presenters) { options[:presenters] || {} }
|
|
20
|
+
|
|
21
|
+
def run!
|
|
22
|
+
current = Matsuri::Tasks::KubectlOps.manifest(resource)
|
|
23
|
+
unless current
|
|
24
|
+
Matsuri.log :fatal, "Cannot fetch current manifest for #{Matsuri::Tasks::KubectlOps.description_for(resource)}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
deltas =
|
|
28
|
+
if primary_container
|
|
29
|
+
resource.primary_container_diff(current, desired: desired_manifest)
|
|
30
|
+
else
|
|
31
|
+
resource.diff(current, desired: desired_manifest)
|
|
32
|
+
end
|
|
33
|
+
print_diff(deltas)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The declaration through the same :on_apply pipeline apply
|
|
37
|
+
# assembles, over the same basis: the applied manifest. Drift reads
|
|
38
|
+
# against what apply would send — never against a step a presenter
|
|
39
|
+
# owns, never against a field the resource restricts out of its
|
|
40
|
+
# applied_manifest. The JSON round trip is the comparison
|
|
41
|
+
# normalization diff always applied to the declaration.
|
|
42
|
+
def desired_manifest
|
|
43
|
+
ops = Matsuri::Tasks::KubectlOps
|
|
44
|
+
presentation = ops.presentation(resource.applied_manifest_for_export,
|
|
45
|
+
ops.presenters_for(resource, presenters), :on_apply)
|
|
46
|
+
JSON.parse(presentation.to_json)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def print_diff(deltas)
|
|
50
|
+
deltas.each do |line|
|
|
51
|
+
color = case line[0]
|
|
52
|
+
when '-' then :red
|
|
53
|
+
when '+' then :green
|
|
54
|
+
when '~' then :yellow
|
|
55
|
+
end
|
|
56
|
+
puts line.join(' ').color(color).bright
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matsuri
|
|
4
|
+
module Tasks
|
|
5
|
+
class Kubectl
|
|
6
|
+
include Matsuri::Task
|
|
7
|
+
|
|
8
|
+
let(:environment) { Matsuri.environment }
|
|
9
|
+
let(:env_config) { Matsuri::Platform.send(environment) }
|
|
10
|
+
let(:namespace) { env_config.namespace || 'default' }
|
|
11
|
+
let(:kube_user) { env_config.user || kube_context }
|
|
12
|
+
let(:kube_cluster) { env_config.kube_cluster || kube_context }
|
|
13
|
+
|
|
14
|
+
# Set up aliases for kubectl. This allows you you to use
|
|
15
|
+
# `kubectl --context=staging` instead of the name given by GKE
|
|
16
|
+
def setup!
|
|
17
|
+
shell_out! "kubectl config set-context #{environment} --cluster=#{kube_cluster} --namespace=#{namespace} --user=#{kube_user}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matsuri
|
|
4
|
+
module Tasks
|
|
5
|
+
module KubectlOps
|
|
6
|
+
# Client-side apply of a rendered manifest payload. The payload is
|
|
7
|
+
# whatever the caller finished rendering — single-doc or multi-doc
|
|
8
|
+
# YAML — so one task serves generated and imported manifests alike.
|
|
9
|
+
class Apply
|
|
10
|
+
include Matsuri::Task
|
|
11
|
+
|
|
12
|
+
let(:input) { options.fetch(:input) }
|
|
13
|
+
let(:namespace) { options.fetch(:namespace) }
|
|
14
|
+
let(:description) { options.fetch(:description) }
|
|
15
|
+
|
|
16
|
+
def run!
|
|
17
|
+
puts "Applying (create or update) #{description}".color(:yellow).bright if verbose
|
|
18
|
+
puts input if debug
|
|
19
|
+
kubectl! 'apply -f -', input: input
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matsuri
|
|
4
|
+
module Tasks
|
|
5
|
+
module KubectlOps
|
|
6
|
+
# Imperative create of a rendered manifest payload. --save-config
|
|
7
|
+
# seeds the last-applied-configuration annotation so a later
|
|
8
|
+
# client-side apply gets its three-way merge; the boundary pins it
|
|
9
|
+
# false (consults_last_applied?) when ongoing delivery never
|
|
10
|
+
# consults the annotation.
|
|
11
|
+
class Create
|
|
12
|
+
include Matsuri::Task
|
|
13
|
+
|
|
14
|
+
let(:input) { options.fetch(:input) }
|
|
15
|
+
let(:namespace) { options.fetch(:namespace) }
|
|
16
|
+
let(:description) { options.fetch(:description) }
|
|
17
|
+
let(:save_config) { options.fetch(:save_config, true) }
|
|
18
|
+
|
|
19
|
+
def run!
|
|
20
|
+
puts "Creating #{description}".color(:yellow).bright if verbose
|
|
21
|
+
puts input if debug
|
|
22
|
+
kubectl! "create --save-config=#{save_config} -f -", input: input
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matsuri
|
|
4
|
+
module Tasks
|
|
5
|
+
module KubectlOps
|
|
6
|
+
# Deletes a live object addressed by type/name.
|
|
7
|
+
class Delete
|
|
8
|
+
include Matsuri::Task
|
|
9
|
+
|
|
10
|
+
let(:resource_type) { options.fetch(:resource_type) }
|
|
11
|
+
let(:name) { options.fetch(:name) }
|
|
12
|
+
let(:namespace) { options.fetch(:namespace) }
|
|
13
|
+
|
|
14
|
+
def run!
|
|
15
|
+
puts "Deleting #{resource_type}/#{name}".color(:yellow).bright if verbose
|
|
16
|
+
kubectl! "delete #{resource_type}/#{name}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rainbow/ext/string'
|
|
4
|
+
|
|
5
|
+
module Matsuri
|
|
6
|
+
module Tasks
|
|
7
|
+
module KubectlOps
|
|
8
|
+
# Server-side diff: the apiserver dry-runs the payload through
|
|
9
|
+
# defaulting and admission, and kubectl prints live-vs-admitted.
|
|
10
|
+
# Companion to Tasks::HashDiff, which shows drift relative to the
|
|
11
|
+
# declaration; this one shows what the cluster will actually do
|
|
12
|
+
# with the manifest, admission injections included.
|
|
13
|
+
#
|
|
14
|
+
# kubectl diff exit codes carry meaning: 0 = no differences,
|
|
15
|
+
# 1 = differences found, >1 = error. Only >1 is a failure, and
|
|
16
|
+
# silence on no-differences is kubectl's own convention.
|
|
17
|
+
class Diff
|
|
18
|
+
include Matsuri::Task
|
|
19
|
+
|
|
20
|
+
let(:input) { options.fetch(:input) }
|
|
21
|
+
let(:namespace) { options.fetch(:namespace) }
|
|
22
|
+
let(:description) { options.fetch(:description) }
|
|
23
|
+
let(:server_side) { options.fetch(:server_side, false) }
|
|
24
|
+
let(:field_manager) { options.fetch(:field_manager, nil) }
|
|
25
|
+
|
|
26
|
+
def run!
|
|
27
|
+
puts "Diffing (live vs admitted) #{description}".color(:yellow).bright if verbose
|
|
28
|
+
puts input if debug
|
|
29
|
+
|
|
30
|
+
cmd = kubectl "diff#{server_side_args} -f -", input: input
|
|
31
|
+
if cmd.exitstatus > 1
|
|
32
|
+
$stderr.print "ERROR: #{cmd.exitstatus}\nSTDOUT:\n#{cmd.stdout}\n\nSTDERR:\n#{cmd.stderr}\n".color(:red).bright
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "No changes for #{description}".color(:yellow) if verbose && cmd.exitstatus.zero?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The manager must be the one apply delivers under, or the
|
|
40
|
+
# preview shows a different conflict frontier than delivery hits.
|
|
41
|
+
def server_side_args
|
|
42
|
+
server_side ? " --server-side --field-manager=#{field_manager}" : ''
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matsuri
|
|
4
|
+
module Tasks
|
|
5
|
+
module KubectlOps
|
|
6
|
+
# Read-side of the delivery mechanism: existence checks and manifest
|
|
7
|
+
# reads against the live cluster, addressed by type/name. Queries,
|
|
8
|
+
# not effects, so it exposes readers instead of run!. Under verbose,
|
|
9
|
+
# exists? narrates what it found.
|
|
10
|
+
class Get
|
|
11
|
+
include Matsuri::Task
|
|
12
|
+
|
|
13
|
+
let(:resource_type) { options.fetch(:resource_type) }
|
|
14
|
+
let(:name) { options.fetch(:name) }
|
|
15
|
+
let(:namespace) { options.fetch(:namespace) }
|
|
16
|
+
|
|
17
|
+
def exists?
|
|
18
|
+
cmd = kubectl "get #{resource_type}/#{name}", echo_level: :debug, no_stdout: true
|
|
19
|
+
if verbose
|
|
20
|
+
status = cmd.status.success? ? 'already started' : 'not started'
|
|
21
|
+
Matsuri.log :info, "#{resource_type}/#{name} #{status}".color(:yellow)
|
|
22
|
+
end
|
|
23
|
+
cmd.status.success?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Plain parsed hash, string keys; nil when not on the cluster.
|
|
27
|
+
def manifest
|
|
28
|
+
cmd = kubectl "get #{resource_type}/#{name} -o json", echo_level: :debug, no_stdout: true
|
|
29
|
+
return nil unless cmd.status.success?
|
|
30
|
+
|
|
31
|
+
JSON.parse(cmd.stdout)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matsuri
|
|
4
|
+
module Tasks
|
|
5
|
+
module KubectlOps
|
|
6
|
+
# Delete-then-create lifecycle: tears down the live object if
|
|
7
|
+
# present, waits until the deletion lands, then runs the creation
|
|
8
|
+
# task the boundary selected — plain create for client-side
|
|
9
|
+
# delivery, the seeding protocol for server-side. The payload must
|
|
10
|
+
# be rendered before this task runs — deletion severs anything the
|
|
11
|
+
# render would have read back.
|
|
12
|
+
class Recreate
|
|
13
|
+
include Matsuri::Task
|
|
14
|
+
include Matsuri::Concerns::Awaiting
|
|
15
|
+
|
|
16
|
+
let(:resource_type) { options.fetch(:resource_type) }
|
|
17
|
+
let(:name) { options.fetch(:name) }
|
|
18
|
+
let(:namespace) { options.fetch(:namespace) }
|
|
19
|
+
let(:description) { options.fetch(:description, "#{resource_type}/#{name}") }
|
|
20
|
+
let(:create) { options.fetch(:create) }
|
|
21
|
+
|
|
22
|
+
let(:identity) { { resource_type: resource_type, name: name, namespace: namespace } }
|
|
23
|
+
|
|
24
|
+
def run!
|
|
25
|
+
if Get.new(identity).exists?
|
|
26
|
+
Delete.new(identity).run!
|
|
27
|
+
awaiting!('resource deletion', interval: 0.5) { !Get.new(identity).exists? }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
create.run!
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|