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.
Files changed (157) hide show
  1. checksums.yaml +5 -5
  2. data/.envrc +1 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +9 -0
  5. data/AGENTS.md +18 -0
  6. data/CHANGELOG.md +642 -0
  7. data/Gemfile +2 -0
  8. data/README.md +86 -8
  9. data/Rakefile +20 -0
  10. data/bin/console +1 -0
  11. data/bin/matsuri +6 -0
  12. data/bin/setup +2 -0
  13. data/docs/acceptors.md +181 -0
  14. data/docs/boot.md +101 -0
  15. data/docs/design/acceptors.md +157 -0
  16. data/docs/design/boot.md +131 -0
  17. data/docs/design/ssa.md +142 -0
  18. data/docs/design/stacks.md +153 -0
  19. data/docs/diff.md +85 -0
  20. data/docs/known-issues.md +19 -0
  21. data/docs/local-dev.md +173 -0
  22. data/docs/presenters.md +317 -0
  23. data/docs/resources.md +121 -0
  24. data/docs/setup.md +97 -0
  25. data/docs/ssa.md +149 -0
  26. data/docs/stacks.md +146 -0
  27. data/flake.lock +61 -0
  28. data/flake.nix +35 -0
  29. data/lib/matsuri/acceptor.rb +33 -0
  30. data/lib/matsuri/acceptors/image_tag.rb +15 -0
  31. data/lib/matsuri/cmd.rb +46 -0
  32. data/lib/matsuri/cmds/apply.rb +78 -0
  33. data/lib/matsuri/cmds/cli.rb +147 -0
  34. data/lib/matsuri/cmds/create.rb +87 -0
  35. data/lib/matsuri/cmds/delete.rb +86 -0
  36. data/lib/matsuri/cmds/diff.rb +105 -0
  37. data/lib/matsuri/cmds/generate.rb +24 -0
  38. data/lib/matsuri/cmds/kubectl.rb +16 -0
  39. data/lib/matsuri/cmds/recreate.rb +65 -0
  40. data/lib/matsuri/cmds/scale.rb +37 -0
  41. data/lib/matsuri/cmds/show.rb +123 -0
  42. data/lib/matsuri/cmds/status.rb +67 -0
  43. data/lib/matsuri/concerns/awaiting.rb +24 -0
  44. data/lib/matsuri/concerns/diffable.rb +135 -0
  45. data/lib/matsuri/concerns/export_manifest.rb +56 -0
  46. data/lib/matsuri/concerns/import_manifest.rb +64 -0
  47. data/lib/matsuri/concerns/pod_template.rb +37 -0
  48. data/lib/matsuri/concerns/pods/spread_pods.rb +47 -0
  49. data/lib/matsuri/concerns/pods/spread_topology.rb +63 -0
  50. data/lib/matsuri/concerns/registry_helpers.rb +59 -0
  51. data/lib/matsuri/config.rb +101 -0
  52. data/lib/matsuri/core_ext.rb +107 -0
  53. data/lib/matsuri/dsl/cluster/aggregated_cluster_role.rb +75 -0
  54. data/lib/matsuri/dsl/cluster/binding.rb +118 -0
  55. data/lib/matsuri/dsl/cluster/cluster_role.rb +41 -0
  56. data/lib/matsuri/dsl/cluster/role.rb +49 -0
  57. data/lib/matsuri/dsl/cluster/scope.rb +245 -0
  58. data/lib/matsuri/dsl/cluster/service_account.rb +76 -0
  59. data/lib/matsuri/dsl/concerns/default_binding.rb +34 -0
  60. data/lib/matsuri/dsl/concerns/manifest_set.rb +21 -0
  61. data/lib/matsuri/dsl/concerns/metadata.rb +56 -0
  62. data/lib/matsuri/dsl/concerns/rbac_rules.rb +39 -0
  63. data/lib/matsuri/initializer.rb +41 -0
  64. data/lib/matsuri/kubernetes/base.rb +107 -0
  65. data/lib/matsuri/kubernetes/config_map.rb +100 -0
  66. data/lib/matsuri/kubernetes/cron_job.rb +115 -0
  67. data/lib/matsuri/kubernetes/daemon_set.rb +62 -0
  68. data/lib/matsuri/kubernetes/deployment.rb +65 -0
  69. data/lib/matsuri/kubernetes/endpoints.rb +57 -0
  70. data/lib/matsuri/kubernetes/horizontal_pod_autoscaler.rb +116 -0
  71. data/lib/matsuri/kubernetes/imported_manifest.rb +61 -0
  72. data/lib/matsuri/kubernetes/ingress.rb +66 -0
  73. data/lib/matsuri/kubernetes/persistent_volume.rb +56 -0
  74. data/lib/matsuri/kubernetes/persistent_volume_claim.rb +41 -0
  75. data/lib/matsuri/kubernetes/pod.rb +355 -0
  76. data/lib/matsuri/kubernetes/replica_set.rb +46 -0
  77. data/lib/matsuri/kubernetes/secret.rb +86 -0
  78. data/lib/matsuri/kubernetes/service.rb +49 -0
  79. data/lib/matsuri/kubernetes/stateful_set.rb +75 -0
  80. data/lib/matsuri/kubernetes/storage_class.rb +45 -0
  81. data/lib/matsuri/platform.rb +38 -0
  82. data/lib/matsuri/presenter.rb +47 -0
  83. data/lib/matsuri/presenters/helpers.rb +39 -0
  84. data/lib/matsuri/presenters/prune_empty.rb +59 -0
  85. data/lib/matsuri/presenters/seed_image.rb +28 -0
  86. data/lib/matsuri/presenters/seed_replicas.rb +25 -0
  87. data/lib/matsuri/presenters/seed_resources.rb +29 -0
  88. data/lib/matsuri/registry.rb +267 -0
  89. data/lib/matsuri/shell_out.rb +119 -0
  90. data/lib/matsuri/stack.rb +276 -0
  91. data/lib/matsuri/task.rb +20 -0
  92. data/lib/matsuri/tasks/cluster.rb +79 -0
  93. data/lib/matsuri/tasks/docker.rb +25 -0
  94. data/lib/matsuri/tasks/hash_diff.rb +61 -0
  95. data/lib/matsuri/tasks/kubectl.rb +21 -0
  96. data/lib/matsuri/tasks/kubectl_ops/apply.rb +24 -0
  97. data/lib/matsuri/tasks/kubectl_ops/create.rb +27 -0
  98. data/lib/matsuri/tasks/kubectl_ops/delete.rb +21 -0
  99. data/lib/matsuri/tasks/kubectl_ops/diff.rb +47 -0
  100. data/lib/matsuri/tasks/kubectl_ops/get.rb +36 -0
  101. data/lib/matsuri/tasks/kubectl_ops/recreate.rb +35 -0
  102. data/lib/matsuri/tasks/kubectl_ops/replace.rb +29 -0
  103. data/lib/matsuri/tasks/kubectl_ops/rollout_status.rb +22 -0
  104. data/lib/matsuri/tasks/kubectl_ops/scale.rb +22 -0
  105. data/lib/matsuri/tasks/kubectl_ops/server_side_apply.rb +32 -0
  106. data/lib/matsuri/tasks/kubectl_ops/status.rb +20 -0
  107. data/lib/matsuri/tasks/kubectl_ops.rb +218 -0
  108. data/lib/matsuri/tasks/pod.rb +56 -0
  109. data/lib/matsuri/tasks/stacks/seed.rb +93 -0
  110. data/lib/matsuri/tasks/stacks.rb +31 -0
  111. data/lib/matsuri/transform/algebra.rb +52 -0
  112. data/lib/matsuri/transform/callable.rb +54 -0
  113. data/lib/matsuri/transform/combinators.rb +61 -0
  114. data/lib/matsuri/transform/env.rb +43 -0
  115. data/lib/matsuri/transform/kubernetes.rb +89 -0
  116. data/lib/matsuri/transform.rb +20 -0
  117. data/lib/matsuri/version.rb +3 -1
  118. data/lib/matsuri.rb +158 -2
  119. data/matsuri.gemspec +20 -7
  120. data/tests/bin/minikube-start.sh +27 -0
  121. data/tests/bin/probe-save-config.sh +46 -0
  122. data/tests/bin/probe-ssa.sh +206 -0
  123. data/tests/example/.gitignore +1 -0
  124. data/tests/example/Gemfile +7 -0
  125. data/tests/example/README.md +91 -0
  126. data/tests/example/bin/matsuri +14 -0
  127. data/tests/example/config/initializer.rb +12 -0
  128. data/tests/example/config/matsuri.rb +9 -0
  129. data/tests/example/config/platform.rb +39 -0
  130. data/tests/example/lib/acme/nodes/dedicated.rb +26 -0
  131. data/tests/example/lib/acme/nodes/preemptible.rb +17 -0
  132. data/tests/example/lib/acme/nodes/required_cores.rb +26 -0
  133. data/tests/example/lib/acme/nodes/resource_class.rb +26 -0
  134. data/tests/example/lib/acme/pods/cacerts.rb +15 -0
  135. data/tests/example/lib/acme/pods/healthcheck.rb +36 -0
  136. data/tests/example/lib/acme.rb +26 -0
  137. data/tests/example/platform/config_maps/billing-settings.rb +8 -0
  138. data/tests/example/platform/config_maps/cacerts.rb +9 -0
  139. data/tests/example/platform/deployments/billing-web.rb +5 -0
  140. data/tests/example/platform/deployments/checkout-web.rb +6 -0
  141. data/tests/example/platform/namespaces/billing-dev.yaml +7 -0
  142. data/tests/example/platform/namespaces/checkout-dev.yaml +7 -0
  143. data/tests/example/platform/pods/billing.rb +31 -0
  144. data/tests/example/platform/pods/checkout.rb +27 -0
  145. data/tests/example/platform/services/billing-web.rb +6 -0
  146. data/tests/example/platform/services/checkout-web.rb +4 -0
  147. data/tests/example/platform/stacks/billing.rb +8 -0
  148. data/tests/example/platform/stacks/checkout.rb +19 -0
  149. data/tests/k8s/save-config/deployment-declared.yaml +19 -0
  150. data/tests/k8s/save-config/deployment-seeded.yaml +20 -0
  151. data/tests/k8s/save-config/namespace.yaml +4 -0
  152. data/tests/k8s/ssa/declared-bumped.yaml +22 -0
  153. data/tests/k8s/ssa/declared.yaml +22 -0
  154. data/tests/k8s/ssa/namespace.yaml +4 -0
  155. data/tests/k8s/ssa/seed-via-create.yaml +24 -0
  156. metadata +288 -20
  157. 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
@@ -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