metatron 0.5.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9252f0668ef9d7bb533e87c696ee1c19968bfc2029b9c9992c2defe42360e8d7
4
- data.tar.gz: f0570ad8ffd6ba0256316dce95f0143e5972117851f0e6080d4ff6a773fa8461
3
+ metadata.gz: d3938ca86027d00b6e1f12ce310b5d120240f4741c54c2d250ec7dd661d532f5
4
+ data.tar.gz: c7f9e703fda8a9113f99f5d2b39e9fcd05970fcefe68fd0389a37a43cff0aacb
5
5
  SHA512:
6
- metadata.gz: 8b80a57ab9b8deccfe861e08baedf2c220319443a89d38f3607dc20f8654243b29cb84d35c0da0a4c795f4619be6d32d1c8cb9ee799705046799456f79e5b7ba
7
- data.tar.gz: 0d29f8ad2e9b74338ddf8ff2aaaf219541e069e5ae9cbad6cc2bc8ad6e30d65c9c723231d2cd86fc55778ae19bd9161cd60f1e5ac700a9159ebb166fe34fbf15
6
+ metadata.gz: 72575f10175d98b9ee1d62f36b97ac2872c8683e93fb635efd17f4ca11c61187403102d0a1e0e0591980d658346e2d2129060f56a1dc2186d60ae9725569e6c3
7
+ data.tar.gz: 608ad100ae3a883ebfab8235ad0c9674d757c47996ca2f1189a72d9b37fbcfc01ab9858f095b4137f653a6d31fcf1eb3c2c1aeba77eade46c3e4de0787e01f4b
data/Gemfile.lock CHANGED
@@ -1,10 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- metatron (0.5.0)
4
+ metatron (0.6.1)
5
5
  json (~> 2.6)
6
- sinatra (~> 3.1)
7
- sinatra-contrib (~> 3.1)
6
+ rack (>= 2.2.8, < 4)
8
7
 
9
8
  GEM
10
9
  remote: https://rubygems.org/
@@ -23,21 +22,16 @@ GEM
23
22
  kramdown-parser-gfm (1.1.0)
24
23
  kramdown (~> 2.0)
25
24
  language_server-protocol (3.17.0.3)
26
- multi_json (1.15.0)
27
- mustermann (3.0.0)
28
- ruby2_keywords (~> 0.0.1)
29
- nokogiri (1.15.4-arm64-darwin)
25
+ nokogiri (1.15.5-arm64-darwin)
30
26
  racc (~> 1.4)
31
- nokogiri (1.15.4-x86_64-linux)
27
+ nokogiri (1.15.5-x86_64-linux)
32
28
  racc (~> 1.4)
33
29
  parallel (1.23.0)
34
30
  parser (3.2.2.4)
35
31
  ast (~> 2.4.1)
36
32
  racc
37
33
  racc (1.7.3)
38
- rack (2.2.8)
39
- rack-protection (3.1.0)
40
- rack (~> 2.2, >= 2.2.4)
34
+ rack (3.0.8)
41
35
  rack-test (2.1.0)
42
36
  rack (>= 1.3)
43
37
  rainbow (3.1.1)
@@ -84,7 +78,6 @@ GEM
84
78
  rubocop-capybara (~> 2.17)
85
79
  rubocop-factory_bot (~> 2.22)
86
80
  ruby-progressbar (1.13.0)
87
- ruby2_keywords (0.0.5)
88
81
  simplecov (0.22.0)
89
82
  docile (~> 1.1)
90
83
  simplecov-html (~> 0.11)
@@ -94,17 +87,6 @@ GEM
94
87
  simplecov (~> 0.19)
95
88
  simplecov-html (0.12.3)
96
89
  simplecov_json_formatter (0.1.4)
97
- sinatra (3.1.0)
98
- mustermann (~> 3.0)
99
- rack (~> 2.2, >= 2.2.4)
100
- rack-protection (= 3.1.0)
101
- tilt (~> 2.0)
102
- sinatra-contrib (3.1.0)
103
- multi_json
104
- mustermann (~> 3.0)
105
- rack-protection (= 3.1.0)
106
- sinatra (= 3.1.0)
107
- tilt (~> 2.0)
108
90
  solargraph (0.49.0)
109
91
  backport (~> 1.2)
110
92
  benchmark
data/README.md CHANGED
@@ -6,359 +6,4 @@ The intention is to make it as easy as possible to use Ruby to manage [custom re
6
6
 
7
7
  For more information, see the [Metatron Wiki on GitHub](https://github.com/jgnagy/metatron/wiki)!
8
8
 
9
- ## Usage
10
-
11
- For a complete walk-through, check out my [blog mini-series](https://therubyist.org/2022/10/25/kubernetes-controllers-via-metatron-part-1/) about Metatron!
12
- ### Getting Started
13
-
14
- To use Metatron, first decide what type of Metacontroller you'd like to create, mostly based on the type(s) of resource(s) you'll manage. Most of the time, what you want is a Custom Resource that has child resources, which means you'll want a [Composite Controller](https://metacontroller.github.io/metacontroller/api/compositecontroller.html).
15
-
16
- Reading the [Metacontroller user's guide](https://metacontroller.github.io/metacontroller/guide.html) will be pretty helpful but isn't strictly required.
17
-
18
- You'll need to [install Metacontroller](https://metacontroller.github.io/metacontroller/guide/install.html) into your cluster before proceeding. This guide doesn't provide a recommendation on how to do that, but it isn't very difficult.
19
-
20
- ### Creating a Composite Controller
21
-
22
- As an example, let's suppose we want to simplify launching blogs for users. Each `Blog` resource should have its own application server (as a `Deployment`), a database (as a `StatefulSet`), a Kubernetes `Service`, and an `Ingress`. A `Blog` will probably have a `name`, an `hostname` (which we'll derive based on its name), and a `username` and `password` (as a `Secret`) to restrict who can author content.
23
-
24
- This means we'll want a `Blog` custom resource and it'll need a few basic properties, like those listed above. It'll also need to specify a container image, and a number of replicas (so we can scale it up and down).
25
-
26
- Here's how that CRD (let's call it `blog-crd.yaml`) might look:
27
-
28
- ```yaml
29
- apiVersion: apiextensions.k8s.io/v1
30
- kind: CustomResourceDefinition
31
- metadata:
32
- name: blogs.therubyist.org
33
- spec:
34
- group: therubyist.org
35
- names:
36
- kind: Blog
37
- plural: blogs
38
- singular: blog
39
- scope: Namespaced
40
- versions:
41
- - name: v1
42
- served: true
43
- storage: true
44
- subresources:
45
- status: {}
46
- schema:
47
- openAPIV3Schema:
48
- type: object
49
- properties:
50
- spec:
51
- type: object
52
- properties:
53
- image:
54
- type: string
55
- replicas:
56
- type: integer
57
- minimum: 1
58
- storage:
59
- type: object
60
- properties:
61
- app:
62
- type: string
63
- db:
64
- type: string
65
- ```
66
-
67
- This means we'll be able to query our Kubernetes cluster for `blogs`. You might eventually want to expand the field list, or simplify it and defer to your controller for validating it. You'll also probably want to make `metadata.name` and `spec.group` use a real domain to avoid potential conflicts. This should be safe to `kubectl apply` as the CRD doesn't do much on its own.
68
-
69
- Now, you'll need to define a `CompositeController` resource (let's call this `blog-controller.yaml`) that instructs Metacontroller where to send sync requests:
70
-
71
- ```yaml
72
- apiVersion: metacontroller.k8s.io/v1alpha1
73
- kind: CompositeController
74
- metadata:
75
- name: blog-controller
76
- spec:
77
- generateSelector: true
78
- parentResource:
79
- apiVersion: therubyist.org/v1
80
- resource: blogs
81
- childResources:
82
- - apiVersion: apps/v1
83
- resource: deployments
84
- updateStrategy:
85
- method: InPlace
86
- - apiVersion: apps/v1
87
- resource: statefulsets
88
- updateStrategy:
89
- method: InPlace
90
- - apiVersion: v1
91
- resource: services
92
- updateStrategy:
93
- method: InPlace
94
- - apiVersion: networking.k8s.io/v1
95
- resource: ingresses
96
- updateStrategy:
97
- method: InPlace
98
- - apiVersion: v1
99
- resource: secrets
100
- updateStrategy:
101
- method: InPlace
102
- hooks:
103
- sync:
104
- webhook:
105
- service:
106
- name: blog-controller
107
- namespace: blog-controller
108
- port: 9292
109
- protocol: http
110
- path: /blogs/sync
111
- ```
112
-
113
- Before applying the above though, we'll need to actually create a service that can response to sync requests. That's where Metatron comes in!
114
-
115
- ### Creating a Sync Controller with Metatron
116
-
117
- As Metatron is a tool for creating Ruby projects, you'll need a few prerequistes. First, make a directory (and git repo) for your controller:
118
-
119
- ```sh
120
- $ git init blog_controller && cd blog_controller
121
- ```
122
-
123
- We'll need a `Gemfile` to ensure we have installed both Metatron and a
124
- [`rack`][] compatible server:
125
-
126
- ```ruby
127
- # frozen_string_literal: true
128
-
129
- source "https://rubygems.org"
130
-
131
- gem "metatron"
132
- gem "puma"
133
- ```
134
-
135
- [`rack`]: https://github.com/rack/rack
136
-
137
- We'll also need a `config.ru` file to instruct [`rack`][] how to route requests:
138
-
139
- ```ruby
140
- # frozen_string_literal: true
141
-
142
- # \ -s puma
143
-
144
- require "metatron"
145
- require_relative "./lib/blog_controller/composite_controller"
146
-
147
- use Rack::ShowExceptions
148
- use Rack::Deflater
149
-
150
- mappings = {
151
- # This one is built-in to Metatron and is useful for monitoring
152
- "/ping" => Metatron::Controllers::Ping.new,
153
- # We'll need to make this one
154
- "/blogs" => BlogController::CompositeController.new
155
- }
156
-
157
- run Rack::URLMap.new(mappings)
158
- ```
159
-
160
- Finally, before we start hacking on some actual Metatron-related code, we'll need a `Dockerfile` to create an image that we can deploy to Kubernetes:
161
-
162
- ```dockerfile
163
- FROM ruby:3.1
164
-
165
- RUN mkdir -p /app
166
-
167
- COPY config.ru /app/
168
- COPY Gemfile /app/
169
- COPY Gemfile.lock /app/
170
- COPY lib/ /app/lib/
171
-
172
- RUN apt update && apt upgrade -y
173
- RUN useradd appuser -d /app -M -c "App User"
174
- RUN chown appuser /app/Gemfile.lock
175
-
176
- USER appuser
177
- WORKDIR /app
178
- RUN bundle install
179
-
180
- ENTRYPOINT ["bundle", "exec"]
181
- CMD ["puma"]
182
- ```
183
-
184
- *Phew*, ok, with all that out of the way, we can get started with our development. We'll need to create a `Metatron::CompositeController` subclass with a `sync` method. We'll put this in `lib/blog_controller/composite_controller.rb`:
185
-
186
- ```ruby
187
- # frozen_string_literal: true
188
-
189
- module BlogController
190
- class CompositeController < Metatron::CompositeController
191
- # This method needs to return a Hash which will be converted to JSON
192
- # It should have the keys "status" (a Hash) and "children" (an Array)
193
- def sync
194
- # request_body is a convenient way to access the data provided by MetaController
195
- parent = request_body["parent"]
196
- existing_children = request_body["children"]
197
- desired_children = []
198
-
199
- # first, let's create the DB and its service
200
- desired_children += construct_db_resources(parent, existing_children)
201
-
202
- # now let's make the app and its parts
203
- db_secret = desired_children.find { |r| r.kind == "Secret" && r.name.end_with?("db") }
204
- desired_children += construct_app_resources(parent, db_secret)
205
-
206
- # We might eventually want a mechanism to build status based on the world:
207
- # status = compare_children(request_body["children"], desired_children)
208
- status = {}
209
-
210
- { status:, children: desired_children }
211
- end
212
-
213
- def construct_app_resources(parent, db_secret)
214
- resources = []
215
- app_db_secret = construct_app_secret(parent["metadata"], db_secret)
216
- resources << app_db_secret
217
- app_deployment = construct_app_deployment(
218
- parent["metadata"], parent["spec"], app_db_secret
219
- )
220
- resources << app_deployment
221
- app_service = construct_service(parent["metadata"], app_deployment)
222
- resources << app_service
223
- resources << construct_ingress(parent["metadata"], app_service)
224
- resources
225
- end
226
-
227
- def construct_db_resources(parent, existing_children)
228
- resources = []
229
- db_secret = construct_db_secret(parent["metadata"], existing_children["Secret.v1"])
230
- resources << db_secret
231
- db_stateful_set = construct_db_stateful_set(db_secret)
232
- resources << db_stateful_set
233
- db_service = construct_service(
234
- parent["metadata"], db_stateful_set, name: "db", port: 3306
235
- )
236
- resources << db_service
237
- resources
238
- end
239
-
240
- def construct_db_stateful_set(secret)
241
- stateful_set = Metatron::Templates::StatefulSet.new("db")
242
- container = Metatron::Templates::Container.new("db")
243
- container.image = "mysql:8.0"
244
- container.envfrom << secret.name
245
- stateful_set.containers << container
246
- stateful_set.additional_pod_labels = { "app.kubernetes.io/component": "db" }
247
- stateful_set
248
- end
249
-
250
- def construct_app_deployment(meta, spec, auth_secret)
251
- deployment = Metatron::Templates::Deployment.new(meta["name"], replicas: spec["replicas"])
252
- container = Metatron::Templates::Container.new("app")
253
- container.image = spec["image"]
254
- container.envfrom << auth_secret.name
255
- container.ports << { name: "web", containerPort: 3000 }
256
-
257
- deployment.containers << container
258
- deployment.additional_pod_labels = { "app.kubernetes.io/component": "app" }
259
- deployment
260
- end
261
-
262
- def construct_ingress(meta, service)
263
- ingress = Metatron::Templates::Ingress.new(meta["name"])
264
- ingress.add_rule(
265
- "#{meta["name"]}.blogs.therubyist.org": { service.name => service.ports.first[:name] }
266
- )
267
- ingress.add_tls("#{meta["name"]}.blogs.therubyist.org")
268
- ingress
269
- end
270
-
271
- def construct_service(meta, resource, name: meta["name"], port: "3000")
272
- service = Metatron::Templates::Service.new(name, port)
273
- service.additional_selector_labels = resource.additional_pod_labels
274
- service
275
- end
276
-
277
- def construct_app_secret(meta, db_secret)
278
- # We'll want to use the password we specified for the DB user
279
- user_pass = db_secret.data["MYSQL_PASSWORD"]
280
- Metatron::Templates::Secret.new(
281
- "#{meta["name"]}app",
282
- {
283
- "DATABASE_URL" => "mysql2://#{meta["name"]}:#{user_pass}@db:3306/#{meta["name"]}"
284
- }
285
- )
286
- end
287
-
288
- def construct_db_secret(meta, existing_secrets)
289
- name = "#{meta["name"]}db"
290
- existing = (existing_secrets || {})[name]
291
- data = if existing
292
- {
293
- "MYSQL_ROOT_PASSWORD" => Base64.decode64(existing.dig("data", "MYSQL_ROOT_PASSWORD")),
294
- "MYSQL_DATABASE" => Base64.decode64(existing.dig("data", "MYSQL_DATABASE")),
295
- "MYSQL_USER" => Base64.decode64(existing.dig("data", "MYSQL_USER")),
296
- "MYSQL_PASSWORD" => Base64.decode64(existing.dig("data", "MYSQL_PASSWORD"))
297
- }
298
- else
299
- {
300
- "MYSQL_ROOT_PASSWORD" => SecureRandom.urlsafe_base64(12),
301
- "MYSQL_DATABASE" => meta["name"],
302
- "MYSQL_USER" => meta["name"],
303
- "MYSQL_PASSWORD" => SecureRandom.urlsafe_base64(8)
304
- }
305
- end
306
- Metatron::Templates::Secret.new(name, data)
307
- end
308
- end
309
- end
310
- ```
311
-
312
- That might seem like a lot of code, but it does a **lot** of heavy lifting for you in creating Kubernetes resources. Try creating all the above Kubernetes resources by hand and you'll see what Metatron is doing for you. It is pretty likely you'll want to adjust a lot of the above code, but it should be a decent starting point.
313
-
314
- To use it, you'll need to create your `Gemfile.lock` file then work on your Docker image:
315
-
316
- ```sh
317
- $ bundle install
318
- $ docker build -t "blogcontroller:latest" .
319
- ```
320
-
321
- You can test your controller locally by running the image:
322
-
323
- ```sh
324
- $ docker run -it --rm -p 9292:9292 "blogcontroller:latest"
325
- ```
326
-
327
- Try POSTing a request via `curl` and inspecting the JSON response to see what your controller is doing for you:
328
-
329
- ```sh
330
- $ curl \
331
- -H "Content-Type: application/json" \
332
- --data '{"parent": {"metadata": {"name": "foo"}, "spec": {"replicas": 1, "image": "nginx:latest"}}}' \
333
- http://localhost:9292/blogs/sync
334
- ```
335
-
336
- Once we've confirmed this works, we'll need to publish our image somewhere and run it. Make sure you update the Service details in `blog-controller.yaml` to reflect its actual location.
337
-
338
- ### Using the New Composite Controller
339
-
340
- After your Metatron controller is up and running in your Kubernetes cluster, you'll need to actually `kubectl apply` your `blog-controller.yaml` file we created way above. Once that is deployed, you can create new `Blog` resources that look something like this (let's call it `test-blog.yaml`):
341
-
342
- ```yaml
343
- apiVersion: therubyist.org/v1
344
- kind: Blog
345
- metadata:
346
- name: test
347
- spec:
348
- image: myapp:tag
349
- replicas: 2
350
- storage:
351
- app: 15Gi
352
- db: 5Gi
353
- ```
354
-
355
- Note that `myapp:tag` should point to some image that is ready to run a blog. This is just an example and, much like the other resources we've created in this guide, it will almost certainly not work as-is. The `DATABASE_URL` secret we create in our Metatron controller should work well for a [Ruby on Rails](https://rubyonrails.org/) app though.
356
-
357
- Let's make a new namespace for this blog and launch it:
358
-
359
- ```sh
360
- $ kubectl create namespace blog-test
361
- $ kubectl -n blog-test apply -f test-blog.yaml
362
- ```
363
-
364
- You should be able to inspect the pods, services, etc. in the `blog-test` namespace and see your resources running!
9
+ For help on how to get started, take a look at the [User Guide](https://github.com/jgnagy/metatron/wiki/User-Guide) in the Wiki!
data/Rakefile CHANGED
@@ -23,6 +23,7 @@ task :bump, [:type] do |_t, args|
23
23
  new_version = calculate_new_version(type)
24
24
  puts "Bumping gem version from #{current_version} to #{new_version}"
25
25
  update_version(new_version)
26
+ update_gem_lock
26
27
  end
27
28
 
28
29
  task default: %i[spec rubocop yard]
@@ -44,6 +45,10 @@ def calculate_new_version(type)
44
45
  version.join(".")
45
46
  end
46
47
 
48
+ def update_gem_lock
49
+ system("bundle lock --update")
50
+ end
51
+
47
52
  def update_version(new_version)
48
53
  file = File.read("lib/metatron/version.rb")
49
54
  new_contents = file.gsub(/VERSION = "(.+)"/, %(VERSION = "#{new_version}"))
@@ -4,51 +4,65 @@ module Metatron
4
4
  # Implementes a Metacontroller CompositeController
5
5
  # @see https://metacontroller.github.io/metacontroller/api/compositecontroller.html
6
6
  class CompositeController < Controller
7
- options "/sync" do
8
- headers "Access-Control-Allow-Methods" => ["POST"]
9
- halt 200
7
+ def initialize(env)
8
+ super
9
+ @strategy = nil
10
10
  end
11
11
 
12
- post "/sync" do
13
- if (provided_etag = calculate_sync_etag)
14
- etag provided_etag
15
- end
12
+ def calculate_customize_etag = nil
13
+ def calculate_sync_etag = nil
14
+ def customize = raise NotImplementedError
15
+ def finalize = raise NotImplementedError
16
+ def sync = raise NotImplementedError
16
17
 
17
- data = sync
18
- data[:children] = data[:children]&.map { |c| c.respond_to?(:render) ? c.render : c }
19
- halt(data.to_json)
20
- end
18
+ private
19
+
20
+ STRATEGY = {
21
+ "/customize" => { data: :customize, etag: :calculate_customize_etag },
22
+ # finalize calls should be rare and unique enough that we don't need to worry about ETags
23
+ "/finalize" => { data: :finalize },
24
+ "/sync" => { data: :sync, etag: :calculate_sync_etag }
25
+ }.freeze
26
+
27
+ def _call
28
+ return access_control_allow_methods if request.options?
29
+ return not_found unless request.post?
30
+
31
+ @strategy = STRATEGY.fetch(request.path_info) { return not_found }
32
+
33
+ headers = {}
21
34
 
22
- options "/finalize" do
23
- headers "Access-Control-Allow-Methods" => ["POST"]
24
- halt 200
35
+ return Rack::Response[412, headers, []].to_a if etag_matches?(headers)
36
+
37
+ Rack::Response[200, headers, processed_data].to_a
25
38
  end
26
39
 
27
- post "/finalize" do
28
- # finalize calls should be rare and unique enough that we don't need to worry about ETags
40
+ def access_control_allow_methods
41
+ Rack::Response[200, { "access-control-allow-methods" => %w[POST] }, []].to_a
42
+ end
29
43
 
30
- data = finalize
31
- data[:children] = data[:children]&.map { |c| c.respond_to?(:render) ? c.render : c }
32
- halt(data.to_json)
44
+ def not_found
45
+ Rack::Response[404, { "x-cascade" => "pass" }, []].to_a
33
46
  end
34
47
 
35
- options "/customize" do
36
- headers "Access-Control-Allow-Methods" => ["POST"]
37
- halt 200
48
+ def etag_matches?(headers)
49
+ return false unless (calculator = @strategy[:etag])
50
+ return false unless (raw_etag = public_send(calculator))
51
+
52
+ etag = +'"' << raw_etag << '"'
53
+ headers["etag"] = etag
54
+
55
+ (none_match = request.get_header("HTTP_IF_NONE_MATCH")) && none_match.include?(etag)
38
56
  end
39
57
 
40
- post "/customize" do
41
- if (provided_etag = calculate_customize_etag)
42
- etag provided_etag
58
+ def processed_data
59
+ data = public_send(@strategy[:data])
60
+
61
+ if data[:children]
62
+ data[:children] = data[:children].map { |c| c.respond_to?(:render) ? c.render : c }
43
63
  end
44
64
 
45
- halt(customize.to_json)
65
+ data.to_json
46
66
  end
47
-
48
- def calculate_customize_etag = nil
49
- def calculate_sync_etag = nil
50
- def customize = raise NotImplementedError
51
- def finalize = raise NotImplementedError
52
- def sync = raise NotImplementedError
53
67
  end
54
68
  end
@@ -2,37 +2,37 @@
2
2
 
3
3
  module Metatron
4
4
  # Base class for API services
5
- class Controller < Sinatra::Base
6
- helpers Sinatra::CustomLogger
7
-
8
- configure do
9
- set :protection, except: :http_origin
10
- set :logging, true
11
- set :logger, Metatron.logger
12
- set :show_exceptions, false
13
- end
14
-
15
- before do
16
- # Sets up a useful variable (@json_body) for accessing a parsed request body
17
- if request.content_type&.include?("json") && !request.body.read.empty?
18
- request.body.rewind
19
- @json_body = JSON.parse(request.body.read)
5
+ class Controller
6
+ class << self
7
+ def call(env)
8
+ new(env).call
20
9
  end
21
- rescue StandardError => e
22
- halt(400, { error: "Request must be JSON: #{e.message}}" }.to_json)
23
10
  end
24
11
 
25
- error do
26
- content_type :json
12
+ attr_accessor :params
27
13
 
28
- e = env["sinatra.error"]
29
- resp = { result: "error", message: e.message }
30
- resp[:trace] = e.full_message if settings.environment.to_s != "production"
31
- resp.to_json
14
+ def initialize(env)
15
+ @env = env
16
+ @request = Rack::Request.new(env)
32
17
  end
33
18
 
34
- def request_body
35
- @json_body
19
+ def call
20
+ begin
21
+ if request&.content_type&.include?("json")
22
+ body = request.body.read
23
+ request.body.rewind if request.body.respond_to?(:rewind)
24
+
25
+ self.params = JSON.parse(body) unless body.empty?
26
+ end
27
+ rescue JSON::ParserError => e
28
+ return [400, {}, [{ error: "Request must be JSON: #{e.message}" }.to_json]]
29
+ end
30
+
31
+ _call
36
32
  end
33
+
34
+ private
35
+
36
+ attr_reader :request
37
37
  end
38
38
  end
@@ -3,33 +3,26 @@
3
3
  module Metatron
4
4
  module Controllers
5
5
  # Healthcheck service
6
- class Ping < Sinatra::Application
7
- configure do
8
- set :logging, true
9
- set :logger, Metatron.logger
10
- end
11
-
12
- before do
13
- content_type "application/json"
6
+ class Ping
7
+ RESPONSE = { status: "up" }.to_json
14
8
 
15
- halt 403 unless request.get? || request.options?
9
+ def call(env)
10
+ req = Rack::Request.new(env)
16
11
 
17
- if request.get?
18
- headers "X-Frame-Options" => "SAMEORIGIN"
19
- headers "X-XSS-Protection" => "1; mode=block"
20
- end
21
- end
12
+ return access_control_allow_methods if req.options?
13
+ return [403, { Rack::CONTENT_TYPE => "application/json" }, []] unless req.get?
22
14
 
23
- after do
24
- headers "Access-Control-Allow-Methods" => %w[GET] if request.options?
15
+ Rack::Response[200, {
16
+ "content-type" => "application/json",
17
+ "x-frame-options" => "SAMEORIGIN",
18
+ "x-xss-protection" => "1; mode=block"
19
+ }, [RESPONSE]].to_a
25
20
  end
26
21
 
27
- get "/" do
28
- '{ "status": "up" }'
29
- end
22
+ private
30
23
 
31
- options "/" do
32
- halt 200
24
+ def access_control_allow_methods
25
+ Rack::Response[200, { "access-control-allow-methods" => %w[GET] }, []].to_a
33
26
  end
34
27
  end
35
28
  end
@@ -12,6 +12,28 @@ module Metatron
12
12
  def label_namespace
13
13
  @label_namespace ||= "metatron.therubyist.org"
14
14
  end
15
+
16
+ def initializer(*args)
17
+ @initializers ||= []
18
+ @initializers += args
19
+ end
20
+
21
+ def initializers
22
+ @initializers ||= []
23
+ end
24
+
25
+ def nearest_metatron_ancestor
26
+ return self if metatron_template_class?
27
+
28
+ ancestors.find { _1.respond_to?(:metatron_template_class?) && _1.metatron_template_class? }
29
+ end
30
+
31
+ def metatron_template_class?
32
+ return true if name == "Metatron::Template"
33
+ return false if name.start_with?("Metatron::Templates::Concerns")
34
+
35
+ name.start_with?("Metatron::Templates::")
36
+ end
15
37
  end
16
38
 
17
39
  def initialize(name)
@@ -25,27 +47,7 @@ module Metatron
25
47
 
26
48
  alias apiVersion api_version
27
49
 
28
- def self.initializer(*args)
29
- @initializers ||= []
30
- @initializers += args
31
- end
32
-
33
- def self.initializers
34
- @initializers ||= []
35
- end
36
-
37
- def self.nearest_metatron_ancestor
38
- return self if metatron_template_class?
39
-
40
- ancestors.find { _1.respond_to?(:metatron_template_class?) && _1.metatron_template_class? }
41
- end
42
-
43
- def self.metatron_template_class?
44
- return true if name == "Metatron::Template"
45
- return false if name.start_with?("Metatron::Templates::Concerns")
46
-
47
- name.start_with?("Metatron::Templates::")
48
- end
50
+ def base_labels = { "#{label_namespace}/name": name }
49
51
 
50
52
  private
51
53
 
@@ -22,7 +22,7 @@ module Metatron
22
22
  kind:,
23
23
  metadata: {
24
24
  name:,
25
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
25
+ labels: base_labels.merge(additional_labels)
26
26
  }.merge(formatted_annotations).compact,
27
27
  aggregationRule:,
28
28
  rules: formatted_rules
@@ -28,7 +28,7 @@ module Metatron
28
28
  kind:,
29
29
  metadata: {
30
30
  name:,
31
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
31
+ labels: base_labels.merge(additional_labels)
32
32
  }.merge(formatted_annotations).compact,
33
33
  roleRef:,
34
34
  subjects:
@@ -104,7 +104,7 @@ module Metatron
104
104
  def pod_metadata
105
105
  {
106
106
  metadata: {
107
- labels: { "#{label_namespace}/name": name }.merge(additional_pod_labels)
107
+ labels: base_labels.merge(additional_pod_labels)
108
108
  }.merge(formatted_pod_annotations)
109
109
  }
110
110
  end
@@ -23,7 +23,7 @@ module Metatron
23
23
  kind:,
24
24
  metadata: {
25
25
  name:,
26
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
26
+ labels: base_labels.merge(additional_labels)
27
27
  }.merge(formatted_annotations).merge(formatted_namespace).compact,
28
28
  data:,
29
29
  immutable:
@@ -30,7 +30,7 @@ module Metatron
30
30
  apiVersion:,
31
31
  kind:,
32
32
  metadata: {
33
- labels: { "#{label_namespace}/name": name }.merge(additional_labels),
33
+ labels: base_labels.merge(additional_labels),
34
34
  name:
35
35
  }.merge(formatted_annotations).merge(formatted_namespace),
36
36
  spec: {
@@ -21,11 +21,11 @@ module Metatron
21
21
  kind:,
22
22
  metadata: {
23
23
  name:,
24
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
24
+ labels: base_labels.merge(additional_labels)
25
25
  }.merge(formatted_annotations).merge(formatted_namespace),
26
26
  spec: {
27
27
  selector: {
28
- matchLabels: { "#{label_namespace}/name": name }.merge(additional_pod_labels)
28
+ matchLabels: base_labels.merge(additional_pod_labels)
29
29
  }
30
30
  }.merge(pod_template)
31
31
  }
@@ -22,13 +22,13 @@ module Metatron
22
22
  kind:,
23
23
  metadata: {
24
24
  name:,
25
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
25
+ labels: base_labels.merge(additional_labels)
26
26
  }.merge(formatted_annotations).merge(formatted_namespace),
27
27
  spec: {
28
28
  replicas:,
29
29
  strategy:,
30
30
  selector: {
31
- matchLabels: { "#{label_namespace}/name": name }.merge(additional_pod_labels)
31
+ matchLabels: base_labels.merge(additional_pod_labels)
32
32
  }
33
33
  }.merge(pod_template).compact
34
34
  }
@@ -77,7 +77,7 @@ module Metatron
77
77
  kind:,
78
78
  metadata: {
79
79
  name:,
80
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
80
+ labels: base_labels.merge(additional_labels)
81
81
  }.merge(formatted_annotations).merge(formatted_namespace),
82
82
  spec: formatted_rules.merge(formatted_tls)
83
83
  }
@@ -28,7 +28,7 @@ module Metatron
28
28
  apiVersion:,
29
29
  kind:,
30
30
  metadata: {
31
- labels: { "#{label_namespace}/name": name }.merge(additional_labels),
31
+ labels: base_labels.merge(additional_labels),
32
32
  name:
33
33
  }.merge(formatted_annotations).merge(formatted_namespace),
34
34
  spec: {
@@ -19,7 +19,7 @@ module Metatron
19
19
  kind:,
20
20
  metadata: {
21
21
  name:,
22
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
22
+ labels: base_labels.merge(additional_labels)
23
23
  }.merge(formatted_annotations)
24
24
  }
25
25
  end
@@ -31,7 +31,7 @@ module Metatron
31
31
  kind:,
32
32
  metadata: {
33
33
  name:,
34
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
34
+ labels: base_labels.merge(additional_labels)
35
35
  }.merge(formatted_annotations).merge(formatted_namespace),
36
36
  spec: {
37
37
  accessModes:,
@@ -13,7 +13,7 @@ module Metatron
13
13
  apiVersion:,
14
14
  kind:,
15
15
  metadata: {
16
- labels: { "#{label_namespace}/name": name }.merge(additional_labels),
16
+ labels: base_labels.merge(additional_labels),
17
17
  name:
18
18
  }.merge(formatted_annotations).merge(formatted_namespace)
19
19
  }.merge(pod_spec)
@@ -22,12 +22,12 @@ module Metatron
22
22
  kind:,
23
23
  metadata: {
24
24
  name:,
25
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
25
+ labels: base_labels.merge(additional_labels)
26
26
  }.merge(formatted_annotations).merge(formatted_namespace),
27
27
  spec: {
28
28
  replicas:,
29
29
  selector: {
30
- matchLabels: { "#{label_namespace}/name": name }.merge(additional_pod_labels)
30
+ matchLabels: base_labels.merge(additional_pod_labels)
31
31
  }
32
32
  }.merge(pod_template)
33
33
  }
@@ -21,7 +21,7 @@ module Metatron
21
21
  kind:,
22
22
  metadata: {
23
23
  name:,
24
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
24
+ labels: base_labels.merge(additional_labels)
25
25
  }.merge(formatted_annotations).merge(formatted_namespace).compact,
26
26
  rules: formatted_rules
27
27
  }.compact
@@ -29,7 +29,7 @@ module Metatron
29
29
  kind:,
30
30
  metadata: {
31
31
  name:,
32
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
32
+ labels: base_labels.merge(additional_labels)
33
33
  }.merge(formatted_annotations).merge(formatted_namespace).compact,
34
34
  roleRef:,
35
35
  subjects:
@@ -22,7 +22,7 @@ module Metatron
22
22
  kind:,
23
23
  metadata: {
24
24
  name:,
25
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
25
+ labels: base_labels.merge(additional_labels)
26
26
  }.merge(formatted_annotations).merge(formatted_namespace).compact,
27
27
  type:,
28
28
  stringData: data
@@ -13,7 +13,7 @@ module Metatron
13
13
  def initialize(name, port = nil)
14
14
  super(name)
15
15
  @type = "ClusterIP"
16
- @selector = { "#{label_namespace}/name": name }
16
+ @selector = base_labels
17
17
  @additional_labels = {}
18
18
  @additional_selector_labels = {}
19
19
  @publish_not_ready_addresses = false
@@ -41,7 +41,7 @@ module Metatron
41
41
  kind:,
42
42
  metadata: {
43
43
  name:,
44
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
44
+ labels: base_labels.merge(additional_labels)
45
45
  }.merge(formatted_annotations).merge(formatted_namespace),
46
46
  spec: {
47
47
  type:,
@@ -18,7 +18,7 @@ module Metatron
18
18
  automountServiceAccountToken:,
19
19
  metadata: {
20
20
  name:,
21
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
21
+ labels: base_labels.merge(additional_labels)
22
22
  }.merge(formatted_annotations).merge(formatted_namespace).compact
23
23
  }.compact
24
24
  end
@@ -25,20 +25,20 @@ module Metatron
25
25
  alias strategy= update_strategy=
26
26
  alias updateStrategy update_strategy
27
27
 
28
- def render # rubocop:disable Metrics/AbcSize
28
+ def render
29
29
  {
30
30
  apiVersion:,
31
31
  kind:,
32
32
  metadata: {
33
33
  name:,
34
- labels: { "#{label_namespace}/name": name }.merge(additional_labels)
34
+ labels: base_labels.merge(additional_labels)
35
35
  }.merge(formatted_annotations).merge(formatted_namespace),
36
36
  spec: {
37
37
  replicas:,
38
38
  serviceName:,
39
39
  updateStrategy:,
40
40
  selector: {
41
- matchLabels: { "#{label_namespace}/name": name }.merge(additional_pod_labels)
41
+ matchLabels: base_labels.merge(additional_pod_labels)
42
42
  }
43
43
  }.merge(pod_template).merge(volume_claim_templates).compact
44
44
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Metatron
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.1"
5
5
  end
data/lib/metatron.rb CHANGED
@@ -1,16 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Standard Library requirements
4
- require "base64"
5
4
  require "resolv"
6
5
  require "securerandom"
7
6
  require "time"
8
7
  require "logger"
9
8
 
10
- # External requirements
11
- require "sinatra/base"
12
- require "sinatra/custom_logger"
13
-
14
9
  # The top-level module for Metatron
15
10
  module Metatron
16
11
  class Error < StandardError; end
data/metatron.gemspec CHANGED
@@ -26,9 +26,8 @@ Gem::Specification.new do |spec|
26
26
 
27
27
  spec.required_ruby_version = "~> 3.1"
28
28
 
29
- spec.add_runtime_dependency "json", "~> 2.6"
30
- spec.add_runtime_dependency "sinatra", "~> 3.1"
31
- spec.add_runtime_dependency "sinatra-contrib", "~> 3.1"
29
+ spec.add_runtime_dependency "json", "~> 2.6"
30
+ spec.add_runtime_dependency "rack", ">= 2.2.8", "< 4"
32
31
 
33
32
  spec.add_development_dependency "bundler", "~> 2.3"
34
33
  spec.add_development_dependency "byebug", "~> 11"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metatron
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gnagy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-18 00:00:00.000000000 Z
11
+ date: 2023-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -25,33 +25,25 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.6'
27
27
  - !ruby/object:Gem::Dependency
28
- name: sinatra
28
+ name: rack
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '3.1'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
33
+ version: 2.2.8
34
+ - - "<"
39
35
  - !ruby/object:Gem::Version
40
- version: '3.1'
41
- - !ruby/object:Gem::Dependency
42
- name: sinatra-contrib
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.1'
36
+ version: '4'
48
37
  type: :runtime
49
38
  prerelease: false
50
39
  version_requirements: !ruby/object:Gem::Requirement
51
40
  requirements:
52
- - - "~>"
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.2.8
44
+ - - "<"
53
45
  - !ruby/object:Gem::Version
54
- version: '3.1'
46
+ version: '4'
55
47
  - !ruby/object:Gem::Dependency
56
48
  name: bundler
57
49
  requirement: !ruby/object:Gem::Requirement