metatron 0.4.2 → 0.6.0

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: fe8d0dcac1a47b872ca203dea9ec4e816e07fdb2bb14fd01f6458ca4b7789cef
4
- data.tar.gz: 255bc8c1efdd1c173455f3b28533ceffd70e34115aad86dba1f685dfceba1318
3
+ metadata.gz: 4dee81a42c5ec6246f3d6a8e9399040dadd9d129b38f1b8b807671f89952fbc9
4
+ data.tar.gz: 1eb701979a19633f39251635b34aaefc6355ddc6174671e68bab40c6ad7216a3
5
5
  SHA512:
6
- metadata.gz: ca1ba6ad7b4b5aa5677b6e7bbd30fe7b643f1f9459232a950d97684430fec3d3c7b710c327aa5a98e8d9324ebe457950df9acb1b50df0678eb875b2f869b6a8d
7
- data.tar.gz: 1c7bc21979e3087dd5e55a381d7fddb9ade45159a72a105ba6d306dd60a8cb34828bad8d9a3ee7db6e1025f5b839e287a3bc21daaa834cb8aa49f31ade19943d
6
+ metadata.gz: 70ae144a7cc7ed33b0c623cc93045439f3b78c4d4e95ee85da2c6bef23aa41c3cb7c0bde2fb4ef51becf7a794b7e8e2899919c9817e54f3f83027a5b51b5e020
7
+ data.tar.gz: 627ccee22056a72d03f04e1150d3f3d315350942e5b6f44d07861e17afd051782444922fd04ac9d71c55fe75472794c45cddc2b12144daf4285713d79b91d916
data/.rubocop.yml CHANGED
@@ -56,3 +56,6 @@ Style/StringLiteralsInInterpolation:
56
56
  Style/StringConcatenation:
57
57
  Exclude:
58
58
  - 'Rakefile'
59
+
60
+ RSpec/ExampleLength:
61
+ Max: 10
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,47 @@
1
+ # Contributing
2
+
3
+ ## How to Contribute Code
4
+
5
+ I'm so glad you're reading this, because we need volunteer developers to help this project grow and thrive!
6
+
7
+ Before you begin, ensure that your contribution is associated with a [GitHub issue](https://github.com/jgnagy/metatron/issues). If there isn't an existing issue, please create one.
8
+
9
+ 1. Fork the repository
10
+ 2. Create a new branch (`git checkout -b feature-branch`)
11
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
12
+ 4. Push to the branch (`git push origin feature-branch`)
13
+ 5. Create a new [Pull Request](http://help.github.com/pull-requests/)
14
+
15
+ ### Testing
16
+
17
+ Please make sure that your code passes the existing tests and add tests for any new functionality. Please write [RSpec](https://rspec.info/) examples for new features you add or modify.
18
+
19
+ To execute the tests, run `bundle exec rake spec`.
20
+
21
+ ### Coding Style
22
+
23
+ This project uses [Rubocop](https://rubocop.org/) to enforce a consistent coding style. Please make sure that your code passes the Rubocop checks.
24
+
25
+ To execute Rubocop, run `bundle exec rake rubocop`.
26
+
27
+ ### Documentation
28
+
29
+ Please make sure that your code is well documented. If you add new features, please add documentation for them. This project uses [YARD](https://yardoc.org/) for documentation, so please add YARD tags to your code when appropriate.
30
+
31
+ ### License
32
+
33
+ By contributing your code, you agree to license your contribution under the terms of the [MIT License](LICENSE.txt).
34
+
35
+ ## Code of Conduct
36
+
37
+ By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
38
+
39
+ ## Reporting Bugs
40
+
41
+ Please use the [GitHub issue tracker](https://github.com/jgnagy/metatron/issues) to report any bugs.
42
+
43
+ ## Suggesting Enhancements
44
+
45
+ If you have ideas for enhancements or improvements please use the [GitHub issue tracker](https://github.com/jgnagy/metatron/issues) to suggest them.
46
+
47
+ Thank you for your interest in contributing!
data/Gemfile.lock CHANGED
@@ -1,11 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- metatron (0.4.2)
4
+ metatron (0.6.0)
5
5
  json (~> 2.6)
6
- puma (~> 6.3)
7
- sinatra (~> 3.1)
8
- sinatra-contrib (~> 3.1)
6
+ rack (>= 2.2.8, < 4)
9
7
 
10
8
  GEM
11
9
  remote: https://rubygems.org/
@@ -24,24 +22,16 @@ GEM
24
22
  kramdown-parser-gfm (1.1.0)
25
23
  kramdown (~> 2.0)
26
24
  language_server-protocol (3.17.0.3)
27
- multi_json (1.15.0)
28
- mustermann (3.0.0)
29
- ruby2_keywords (~> 0.0.1)
30
- nio4r (2.6.0)
31
- nokogiri (1.15.4-arm64-darwin)
25
+ nokogiri (1.15.5-arm64-darwin)
32
26
  racc (~> 1.4)
33
- nokogiri (1.15.4-x86_64-linux)
27
+ nokogiri (1.15.5-x86_64-linux)
34
28
  racc (~> 1.4)
35
29
  parallel (1.23.0)
36
30
  parser (3.2.2.4)
37
31
  ast (~> 2.4.1)
38
32
  racc
39
- puma (6.4.0)
40
- nio4r (~> 2.0)
41
33
  racc (1.7.3)
42
- rack (2.2.8)
43
- rack-protection (3.1.0)
44
- rack (~> 2.2, >= 2.2.4)
34
+ rack (3.0.8)
45
35
  rack-test (2.1.0)
46
36
  rack (>= 1.3)
47
37
  rainbow (3.1.1)
@@ -88,7 +78,6 @@ GEM
88
78
  rubocop-capybara (~> 2.17)
89
79
  rubocop-factory_bot (~> 2.22)
90
80
  ruby-progressbar (1.13.0)
91
- ruby2_keywords (0.0.5)
92
81
  simplecov (0.22.0)
93
82
  docile (~> 1.1)
94
83
  simplecov-html (~> 0.11)
@@ -98,17 +87,6 @@ GEM
98
87
  simplecov (~> 0.19)
99
88
  simplecov-html (0.12.3)
100
89
  simplecov_json_formatter (0.1.4)
101
- sinatra (3.1.0)
102
- mustermann (~> 3.0)
103
- rack (~> 2.2, >= 2.2.4)
104
- rack-protection (= 3.1.0)
105
- tilt (~> 2.0)
106
- sinatra-contrib (3.1.0)
107
- multi_json
108
- mustermann (~> 3.0)
109
- rack-protection (= 3.1.0)
110
- sinatra (= 3.1.0)
111
- tilt (~> 2.0)
112
90
  solargraph (0.49.0)
113
91
  backport (~> 1.2)
114
92
  benchmark
data/README.md CHANGED
@@ -2,359 +2,8 @@
2
2
 
3
3
  Metatron is a Ruby library for creating [Metacontroller](https://metacontroller.github.io/metacontroller/)-based custom Kubernetes controllers.
4
4
 
5
- The intention is to make it as easy as possible to use Ruby to manage [custom resources](https://kubernetes.io/docs/concepts/api-extension/custom-resources/) within your Kubernetes infrastructure. No Golang required to listen for and respond to resources based on your own [CustomResourceDefinition](https://kubernetes.io/docs/tasks/access-kubernetes-api/extend-api-custom-resource-definitions/) or to modify existing kubernetes resources via a [DecoratorController](https://metacontroller.github.io/metacontroller/api/decoratorcontroller.html).
5
+ The intention is to make it as easy as possible to use Ruby to manage [custom resources](https://kubernetes.io/docs/concepts/api-extension/custom-resources/) within your Kubernetes infrastructure. No Golang required!
6
6
 
7
- Your Ruby code doesn't have to have any _real_ knowledge of the Kubernetes environment in which it operates; Metacontroller takes care of all the Kubernetes interactions and Metatron handles providing the JSON interface. Just write a `sync` method that can receive and respond with the appropriate Hashes and you're on your way!
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: /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 Metatron installed:
124
-
125
- ```ruby
126
- # frozen_string_literal: true
127
-
128
- source "https://rubygems.org"
129
-
130
- gem "metatron"
131
- ```
132
-
133
- We'll also need a `config.ru` file to instruct [`rack`](https://github.com/rack/rack) how to route requests:
134
-
135
- ```ruby
136
- # frozen_string_literal: true
137
-
138
- # \ -s puma
139
-
140
- require "metatron"
141
- require_relative "./lib/blog_controller/sync"
142
-
143
- use Rack::ShowExceptions
144
- use Rack::Deflater
145
-
146
- mappings = {
147
- # This one is built-in to Metatron and is useful for monitoring
148
- "/ping" => Metatron::Controllers::Ping.new,
149
- # We'll need to make this one
150
- "/sync" => BlogController::Sync.new
151
- }
152
-
153
- run Rack::URLMap.new(mappings)
154
- ```
155
-
156
- 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:
157
-
158
- ```dockerfile
159
- FROM ruby:3.1
160
-
161
- RUN mkdir -p /app
162
-
163
- COPY config.ru /app/
164
- COPY Gemfile /app/
165
- COPY Gemfile.lock /app/
166
- COPY lib/ /app/lib/
167
-
168
- RUN apt update && apt upgrade -y
169
- RUN useradd appuser -d /app -M -c "App User"
170
- RUN chown appuser /app/Gemfile.lock
171
-
172
- USER appuser
173
- WORKDIR /app
174
- RUN bundle install
175
-
176
- ENTRYPOINT ["bundle", "exec"]
177
- CMD ["puma"]
178
- ```
179
-
180
- *Phew*, ok, with all that out of the way, we can get started with our development. We'll need to create a `Metatron::SyncController` subclass with a `sync` method. We'll put this in `lib/blog_controller/sync.rb`:
181
-
182
- ```ruby
183
- # frozen_string_literal: true
184
-
185
- module BlogController
186
- class Sync < Metatron::SyncController
187
- # This method needs to return a Hash which will be converted to JSON
188
- # It should have the keys "status" (a Hash) and "children" (an Array)
189
- def sync
190
- # request_body is a convenient way to access the data provided by MetaController
191
- parent = request_body["parent"]
192
- existing_children = request_body["children"]
193
- desired_children = []
194
-
195
- # first, let's create the DB and its service
196
- desired_children += construct_db_resources(parent, existing_children)
197
-
198
- # now let's make the app and its parts
199
- db_secret = desired_children.find { |r| r.kind == "Secret" && r.name.end_with?("db") }
200
- desired_children += construct_app_resources(parent, db_secret)
201
-
202
- # We might eventually want a mechanism to build status based on the world:
203
- # status = compare_children(request_body["children"], desired_children)
204
- status = {}
205
-
206
- { status:, children: desired_children }
207
- end
208
-
209
- def construct_app_resources(parent, db_secret)
210
- resources = []
211
- app_db_secret = construct_app_secret(parent["metadata"], db_secret)
212
- resources << app_db_secret
213
- app_deployment = construct_app_deployment(
214
- parent["metadata"], parent["spec"], app_db_secret
215
- )
216
- resources << app_deployment
217
- app_service = construct_service(parent["metadata"], app_deployment)
218
- resources << app_service
219
- resources << construct_ingress(parent["metadata"], app_service)
220
- resources
221
- end
222
-
223
- def construct_db_resources(parent, existing_children)
224
- resources = []
225
- db_secret = construct_db_secret(parent["metadata"], existing_children["Secret.v1"])
226
- resources << db_secret
227
- db_stateful_set = construct_db_stateful_set(db_secret)
228
- resources << db_stateful_set
229
- db_service = construct_service(
230
- parent["metadata"], db_stateful_set, name: "db", port: 3306
231
- )
232
- resources << db_service
233
- resources
234
- end
235
-
236
- def construct_db_stateful_set(secret)
237
- stateful_set = Metatron::Templates::StatefulSet.new("db")
238
- container = Metatron::Templates::Container.new("db")
239
- container.image = "mysql:8.0"
240
- container.envfrom << secret.name
241
- stateful_set.containers << container
242
- stateful_set.additional_pod_labels = { "app.kubernetes.io/component": "db" }
243
- stateful_set
244
- end
245
-
246
- def construct_app_deployment(meta, spec, auth_secret)
247
- deployment = Metatron::Templates::Deployment.new(meta["name"], replicas: spec["replicas"])
248
- container = Metatron::Templates::Container.new("app")
249
- container.image = spec["image"]
250
- container.envfrom << auth_secret.name
251
- container.ports << { name: "web", containerPort: 3000 }
252
-
253
- deployment.containers << container
254
- deployment.additional_pod_labels = { "app.kubernetes.io/component": "app" }
255
- deployment
256
- end
257
-
258
- def construct_ingress(meta, service)
259
- ingress = Metatron::Templates::Ingress.new(meta["name"])
260
- ingress.add_rule(
261
- "#{meta["name"]}.blogs.therubyist.org": { service.name => service.ports.first[:name] }
262
- )
263
- ingress.add_tls("#{meta["name"]}.blogs.therubyist.org")
264
- ingress
265
- end
266
-
267
- def construct_service(meta, resource, name: meta["name"], port: "3000")
268
- service = Metatron::Templates::Service.new(name, port)
269
- service.additional_selector_labels = resource.additional_pod_labels
270
- service
271
- end
272
-
273
- def construct_app_secret(meta, db_secret)
274
- # We'll want to use the password we specified for the DB user
275
- user_pass = db_secret.data["MYSQL_PASSWORD"]
276
- Metatron::Templates::Secret.new(
277
- "#{meta["name"]}app",
278
- {
279
- "DATABASE_URL" => "mysql2://#{meta["name"]}:#{user_pass}@db:3306/#{meta["name"]}"
280
- }
281
- )
282
- end
283
-
284
- def construct_db_secret(meta, existing_secrets)
285
- name = "#{meta["name"]}db"
286
- existing = (existing_secrets || {})[name]
287
- data = if existing
288
- {
289
- "MYSQL_ROOT_PASSWORD" => Base64.decode64(existing.dig("data", "MYSQL_ROOT_PASSWORD")),
290
- "MYSQL_DATABASE" => Base64.decode64(existing.dig("data", "MYSQL_DATABASE")),
291
- "MYSQL_USER" => Base64.decode64(existing.dig("data", "MYSQL_USER")),
292
- "MYSQL_PASSWORD" => Base64.decode64(existing.dig("data", "MYSQL_PASSWORD"))
293
- }
294
- else
295
- {
296
- "MYSQL_ROOT_PASSWORD" => SecureRandom.urlsafe_base64(12),
297
- "MYSQL_DATABASE" => meta["name"],
298
- "MYSQL_USER" => meta["name"],
299
- "MYSQL_PASSWORD" => SecureRandom.urlsafe_base64(8)
300
- }
301
- end
302
- Metatron::Templates::Secret.new(name, data)
303
- end
304
- end
305
- end
306
- ```
307
-
308
- 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.
309
-
310
- To use it, you'll need to create your `Gemfile.lock` file then work on your Docker image:
311
-
312
- ```sh
313
- $ bundle install
314
- $ docker build -t "blogcontroller:latest" .
315
- ```
316
-
317
- You can test your controller locally by running the image:
318
-
319
- ```sh
320
- $ docker run -it --rm -p 9292:9292 "blogcontroller:latest"
321
- ```
322
-
323
- Try POSTing a request via `curl` and inspecting the JSON response to see what your controller is doing for you:
324
-
325
- ```sh
326
- $ curl \
327
- -H "Content-Type: application/json" \
328
- --data '{"parent": {"metadata": {"name": "foo"}, "spec": {"replicas": 1, "image": "nginx:latest"}}}' \
329
- http://localhost:9292/sync
330
- ```
331
-
332
- 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.
333
-
334
- ### Using the New Composite Controller
335
-
336
- 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`):
337
-
338
- ```yaml
339
- apiVersion: therubyist.org/v1
340
- kind: Blog
341
- metadata:
342
- name: test
343
- spec:
344
- image: myapp:tag
345
- replicas: 2
346
- storage:
347
- app: 15Gi
348
- db: 5Gi
349
- ```
350
-
351
- 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.
352
-
353
- Let's make a new namespace for this blog and launch it:
354
-
355
- ```sh
356
- $ kubectl create namespace blog-test
357
- $ kubectl -n blog-test apply -f test-blog.yaml
358
- ```
359
-
360
- 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
@@ -16,4 +16,36 @@ task :demo do
16
16
  system("rackup --host 0.0.0.0 -P #{File.expand_path(".")}/tmp/daemon.pid")
17
17
  end
18
18
 
19
+ desc "automatically bump the gem's version"
20
+ task :bump, [:type] do |_t, args|
21
+ type = args[:type] || ENV["TYPE"] || "patch"
22
+ current_version = Metatron::VERSION
23
+ new_version = calculate_new_version(type)
24
+ puts "Bumping gem version from #{current_version} to #{new_version}"
25
+ update_version(new_version)
26
+ end
27
+
19
28
  task default: %i[spec rubocop yard]
29
+
30
+ def calculate_new_version(type)
31
+ version = Metatron::VERSION.split(".").map(&:to_i)
32
+ case type
33
+ when "patch"
34
+ version[2] += 1
35
+ when "minor"
36
+ version[1] += 1
37
+ version[2] = 0
38
+ when "major"
39
+ version[0] += 1
40
+ version[1] = 0
41
+ version[2] = 0
42
+ end
43
+
44
+ version.join(".")
45
+ end
46
+
47
+ def update_version(new_version)
48
+ file = File.read("lib/metatron/version.rb")
49
+ new_contents = file.gsub(/VERSION = "(.+)"/, %(VERSION = "#{new_version}"))
50
+ File.write("lib/metatron/version.rb", new_contents)
51
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metatron
4
+ # Implementes a Metacontroller CompositeController
5
+ # @see https://metacontroller.github.io/metacontroller/api/compositecontroller.html
6
+ class CompositeController < Controller
7
+ def initialize(env)
8
+ super
9
+ @strategy = nil
10
+ end
11
+
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
17
+
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 = {}
34
+
35
+ return Rack::Response[412, headers, []].to_a if etag_matches?(headers)
36
+
37
+ Rack::Response[200, headers, processed_data].to_a
38
+ end
39
+
40
+ def access_control_allow_methods
41
+ Rack::Response[200, { "access-control-allow-methods" => %w[POST] }, []].to_a
42
+ end
43
+
44
+ def not_found
45
+ Rack::Response[404, { "x-cascade" => "pass" }, []].to_a
46
+ end
47
+
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)
56
+ end
57
+
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 }
63
+ end
64
+
65
+ data.to_json
66
+ end
67
+ end
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
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Metatron
4
- VERSION = [
5
- 0, # major
6
- 4, # minor
7
- 2 # patch
8
- ].join(".")
4
+ VERSION = "0.6.0"
9
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
@@ -46,6 +41,6 @@ require "metatron/templates/service"
46
41
  require "metatron/templates/service_account"
47
42
  require "metatron/templates/stateful_set"
48
43
  require "metatron/controller"
49
- require "metatron/sync_controller"
44
+ require "metatron/composite_controller"
50
45
  require "metatron/controllers/ping"
51
46
  require "metatron/railtie" if defined? Rails::Railtie
data/metatron.gemspec CHANGED
@@ -26,10 +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 "puma", "~> 6.3"
31
- spec.add_runtime_dependency "sinatra", "~> 3.1"
32
- 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"
33
31
 
34
32
  spec.add_development_dependency "bundler", "~> 2.3"
35
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.4.2
4
+ version: 0.6.0
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-16 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,47 +25,25 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.6'
27
27
  - !ruby/object:Gem::Dependency
28
- name: puma
28
+ name: rack
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '6.3'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '6.3'
41
- - !ruby/object:Gem::Dependency
42
- name: sinatra
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
33
+ version: 2.2.8
34
+ - - "<"
46
35
  - !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
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.1'
55
- - !ruby/object:Gem::Dependency
56
- name: sinatra-contrib
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
41
+ - - ">="
60
42
  - !ruby/object:Gem::Version
61
- version: '3.1'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
43
+ version: 2.2.8
44
+ - - "<"
67
45
  - !ruby/object:Gem::Version
68
- version: '3.1'
46
+ version: '4'
69
47
  - !ruby/object:Gem::Dependency
70
48
  name: bundler
71
49
  requirement: !ruby/object:Gem::Requirement
@@ -246,6 +224,8 @@ files:
246
224
  - ".rspec"
247
225
  - ".rubocop.yml"
248
226
  - ".ruby-version"
227
+ - CODE_OF_CONDUCT.md
228
+ - CONTRIBUTING.md
249
229
  - Gemfile
250
230
  - Gemfile.lock
251
231
  - LICENSE.txt
@@ -253,10 +233,10 @@ files:
253
233
  - Rakefile
254
234
  - bin/console
255
235
  - lib/metatron.rb
236
+ - lib/metatron/composite_controller.rb
256
237
  - lib/metatron/controller.rb
257
238
  - lib/metatron/controllers/ping.rb
258
239
  - lib/metatron/railtie.rb
259
- - lib/metatron/sync_controller.rb
260
240
  - lib/metatron/template.rb
261
241
  - lib/metatron/templates/cluster_role.rb
262
242
  - lib/metatron/templates/cluster_role_binding.rb
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Metatron
4
- # Used for "normal" sync requests
5
- class SyncController < Controller
6
- options "/" do
7
- headers "Access-Control-Allow-Methods" => ["POST"]
8
- halt 200
9
- end
10
-
11
- post "/" do
12
- if (provided_etag = calculate_etag)
13
- etag provided_etag
14
- end
15
-
16
- data = sync
17
- data[:children] = data[:children]&.map { |c| c.respond_to?(:render) ? c.render : c }
18
- halt(data.to_json)
19
- end
20
-
21
- def calculate_etag = nil
22
- def sync = raise NotImplementedError
23
- end
24
- end