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 +4 -4
- data/.rubocop.yml +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +47 -0
- data/Gemfile.lock +5 -27
- data/README.md +3 -354
- data/Rakefile +32 -0
- data/lib/metatron/composite_controller.rb +68 -0
- data/lib/metatron/controller.rb +25 -25
- data/lib/metatron/controllers/ping.rb +14 -21
- data/lib/metatron/version.rb +1 -5
- data/lib/metatron.rb +1 -6
- data/metatron.gemspec +2 -4
- metadata +14 -34
- data/lib/metatron/sync_controller.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4dee81a42c5ec6246f3d6a8e9399040dadd9d129b38f1b8b807671f89952fbc9
|
4
|
+
data.tar.gz: 1eb701979a19633f39251635b34aaefc6355ddc6174671e68bab40c6ad7216a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70ae144a7cc7ed33b0c623cc93045439f3b78c4d4e95ee85da2c6bef23aa41c3cb7c0bde2fb4ef51becf7a794b7e8e2899919c9817e54f3f83027a5b51b5e020
|
7
|
+
data.tar.gz: 627ccee22056a72d03f04e1150d3f3d315350942e5b6f44d07861e17afd051782444922fd04ac9d71c55fe75472794c45cddc2b12144daf4285713d79b91d916
|
data/.rubocop.yml
CHANGED
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
|
+
metatron (0.6.0)
|
5
5
|
json (~> 2.6)
|
6
|
-
|
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
|
-
|
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.
|
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 (
|
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
|
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
|
-
|
7
|
+
For more information, see the [Metatron Wiki on GitHub](https://github.com/jgnagy/metatron/wiki)!
|
8
8
|
|
9
|
-
|
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
|
data/lib/metatron/controller.rb
CHANGED
@@ -2,37 +2,37 @@
|
|
2
2
|
|
3
3
|
module Metatron
|
4
4
|
# Base class for API services
|
5
|
-
class Controller
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
26
|
-
content_type :json
|
12
|
+
attr_accessor :params
|
27
13
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
resp.to_json
|
14
|
+
def initialize(env)
|
15
|
+
@env = env
|
16
|
+
@request = Rack::Request.new(env)
|
32
17
|
end
|
33
18
|
|
34
|
-
def
|
35
|
-
|
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
|
7
|
-
|
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
|
-
|
9
|
+
def call(env)
|
10
|
+
req = Rack::Request.new(env)
|
16
11
|
|
17
|
-
if
|
18
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
28
|
-
'{ "status": "up" }'
|
29
|
-
end
|
22
|
+
private
|
30
23
|
|
31
|
-
|
32
|
-
|
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
|
data/lib/metatron/version.rb
CHANGED
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/
|
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",
|
30
|
-
spec.add_runtime_dependency "
|
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
|
+
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-
|
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:
|
28
|
+
name: rack
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
-
|
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: '
|
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:
|
62
|
-
|
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: '
|
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
|