katalyst-thermite 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +41 -0
- data/lib/generators/thermite/install/active_storage/USAGE +28 -0
- data/lib/generators/thermite/install/active_storage/active_storage_generator.rb +79 -0
- data/lib/generators/thermite/install/active_storage/templates/app/controllers/active_storage/authenticated_direct_uploads_controller.rb +29 -0
- data/lib/generators/thermite/install/active_storage/templates/config/routes/overrides.rb +15 -0
- data/lib/generators/thermite/install/active_storage/templates/config/storage.yml.tt +12 -0
- data/lib/generators/thermite/install/active_storage/templates/spec/requests/active_storage/authenticated_direct_uploads_controller_spec.rb +70 -0
- data/lib/generators/thermite/install/content_security_policy/USAGE +21 -0
- data/lib/generators/thermite/install/content_security_policy/content_security_policy_generator.rb +18 -0
- data/lib/generators/thermite/install/content_security_policy/templates/config/initializers/content_security_policy.rb +72 -0
- data/lib/generators/thermite/install/docker/USAGE +8 -0
- data/lib/generators/thermite/install/docker/docker_generator.rb +35 -0
- data/lib/generators/thermite/install/docker/templates/Dockerfile.tt +119 -0
- data/lib/generators/thermite/install/docker/templates/bin/docker-entrypoint +21 -0
- data/lib/generators/thermite/install/permissions_policy/USAGE +11 -0
- data/lib/generators/thermite/install/permissions_policy/permissions_policy_generator.rb +18 -0
- data/lib/generators/thermite/install/permissions_policy/templates/config/initializers/permissions_policy.rb +11 -0
- data/lib/generators/thermite/install/sentry/USAGE +18 -0
- data/lib/generators/thermite/install/sentry/sentry_generator.rb +46 -0
- data/lib/generators/thermite/install/sentry/templates/config/initializers/sentry.rb.tt +50 -0
- data/lib/generators/thermite/install/solid_cable/USAGE +16 -0
- data/lib/generators/thermite/install/solid_cable/solid_cable_generator.rb +98 -0
- data/lib/generators/thermite/install/solid_cable/templates/add_solid_cable.rb.tt +7 -0
- data/lib/generators/thermite/install/solid_cable/templates/config/cable.yml.tt +16 -0
- data/lib/generators/thermite/install/solid_queue/USAGE +18 -0
- data/lib/generators/thermite/install/solid_queue/solid_queue_generator.rb +94 -0
- data/lib/generators/thermite/install/solid_queue/templates/add_solid_queue.rb.tt +7 -0
- data/lib/generators/thermite/install/solid_queue/templates/config/queue.yml.tt +14 -0
- data/lib/generators/thermite/install/solid_queue/templates/config/recurring.yml.tt +9 -0
- data/lib/katalyst-thermite.rb +3 -0
- data/lib/thermite/engine.rb +11 -0
- data/lib/thermite/tasks.rb +20 -0
- data/lib/thermite.rb +17 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '02089b926bee271fd703e7ac9aab79a4adf886f627f212d518427a4d3263bfe8'
|
|
4
|
+
data.tar.gz: 4782edfbf124fd1b63f8bc959072de74c2e2bb49ec9f2a0e4c87196d2dcd28d3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d3971994ede5200b2798773970002b161b3a62beb0c6e2d79c04078f63913bd3639b81dc1cc66a9cf39970bf948fbcba9a7e2cf1dc0524a0176b61365f6421e2
|
|
7
|
+
data.tar.gz: 804df729abc85ba29fe70a859a1e7000c0c6b9b4719c045fc7d2ccd958fd4705cdca2a7740c0ff22b4eed0c30f2ed1315736619f7de1357503da19c21395a7d5
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Katalyst Interactive
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Katalyst::Thermite
|
|
2
|
+
|
|
3
|
+
Katalyst Thermite provides tools for adding and updating libraries and frameworks following Katalyst best practices.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to the `development` group in your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
group :development do
|
|
11
|
+
gem "katalyst-thermite"
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Run installers to add common frameworks and libraries to your Rails app, for example:
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
bin/rails thermite:install:koi
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Available installers:
|
|
24
|
+
|
|
25
|
+
```shell
|
|
26
|
+
bin/rails thermite:install:active_storage
|
|
27
|
+
bin/rails thermite:install:content_security_policy
|
|
28
|
+
bin/rails thermite:install:docker
|
|
29
|
+
bin/rails thermite:install:permissions_policy
|
|
30
|
+
bin/rails thermite:install:sentry
|
|
31
|
+
bin/rails thermite:install:solid_cable
|
|
32
|
+
bin/rails thermite:install:solid_queue
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Contributing
|
|
36
|
+
|
|
37
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/thermite.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs Active Storage following Katalyst conventions, on fresh or existing apps.
|
|
3
|
+
|
|
4
|
+
Writes a Katalyst-flavoured config/storage.yml (Disk locally, S3 elsewhere) and
|
|
5
|
+
points config.active_storage.service at :s3 for every non dev/test environment.
|
|
6
|
+
|
|
7
|
+
Also secures direct uploads: the POST /rails/active_storage/direct_uploads endpoint
|
|
8
|
+
is routed through an authenticated controller, and the disk service routes are
|
|
9
|
+
disabled unless storage is served from local disk. Authentication is app-specific, so
|
|
10
|
+
the controller ships locked down (every upload rejected) and the generated request
|
|
11
|
+
spec leaves the authenticated examples pending for you to complete.
|
|
12
|
+
|
|
13
|
+
To use the S3 service in deployed environments, add `aws-sdk-s3` to your Gemfile.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
bin/rails thermite:install:active_storage
|
|
17
|
+
|
|
18
|
+
This will:
|
|
19
|
+
Create config/storage.yml
|
|
20
|
+
Install the Active Storage migrations
|
|
21
|
+
Set the Active Storage service to :s3 (non dev/test environments)
|
|
22
|
+
Add app/controllers/active_storage/authenticated_direct_uploads_controller.rb
|
|
23
|
+
Add config/routes/overrides.rb and draw it from config/routes.rb
|
|
24
|
+
Add spec/requests/active_storage/authenticated_direct_uploads_controller_spec.rb
|
|
25
|
+
|
|
26
|
+
After running, enable uploads for your authenticated users:
|
|
27
|
+
Permit your principals in #current_user in the authenticated direct uploads controller
|
|
28
|
+
Complete the pending examples in the generated request spec
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
|
|
6
|
+
# Installs Active Storage following Katalyst conventions, on fresh or existing apps.
|
|
7
|
+
module Thermite
|
|
8
|
+
module Install
|
|
9
|
+
class ActiveStorageGenerator < Rails::Generators::Base
|
|
10
|
+
def self.source_root
|
|
11
|
+
File.expand_path("templates", __dir__)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def copy_storage_config
|
|
15
|
+
template "config/storage.yml"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Lock the direct upload endpoint behind authentication and disable the disk
|
|
19
|
+
# service routes when storage isn't served from local disk. The authentication
|
|
20
|
+
# stubs are left for the app to complete (see the generated controller and spec).
|
|
21
|
+
def secure_direct_uploads
|
|
22
|
+
copy_file "app/controllers/active_storage/authenticated_direct_uploads_controller.rb"
|
|
23
|
+
copy_file "config/routes/overrides.rb"
|
|
24
|
+
copy_file "spec/requests/active_storage/authenticated_direct_uploads_controller_spec.rb"
|
|
25
|
+
draw_overrides
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# update tops up an existing install; install lays the base for a new one.
|
|
29
|
+
def install_migrations
|
|
30
|
+
rails_command active_storage_installed? ? "active_storage:update" : "active_storage:install"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def configure_services
|
|
34
|
+
environments.each do |pathname|
|
|
35
|
+
next if local?(pathname)
|
|
36
|
+
|
|
37
|
+
gsub_file pathname, /(# )?config\.active_storage\.service\s*=.*/,
|
|
38
|
+
"config.active_storage.service = :s3", verbose: false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Wire config/routes/overrides.rb into the app's router with `draw :overrides`,
|
|
45
|
+
# inserted just inside the routes block. Idempotent and a no-op without routes.rb.
|
|
46
|
+
def draw_overrides
|
|
47
|
+
routes = Pathname(destination_root).join("config/routes.rb")
|
|
48
|
+
return unless routes.exist?
|
|
49
|
+
return if routes.read.include?("draw :overrides")
|
|
50
|
+
|
|
51
|
+
inject_into_file "config/routes.rb", " draw :overrides\n",
|
|
52
|
+
after: /^\s*Rails\.application\.routes\.draw do\n/, verbose: false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def environments
|
|
56
|
+
Pathname(destination_root).join("config/environments").glob("*.rb")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def active_storage_installed?
|
|
60
|
+
Pathname(destination_root).join("db/migrate").glob("*create_active_storage_tables*").any?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def local?(pathname)
|
|
64
|
+
%w[development test].include?(pathname.basename(".rb").to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Overridable at runtime via RAILS_ASSETS_BUCKET_ID (see storage.yml).
|
|
68
|
+
def asset_bucket_name
|
|
69
|
+
name = if defined?(Rails.application) && Rails.application
|
|
70
|
+
Rails.application.class.module_parent_name
|
|
71
|
+
else
|
|
72
|
+
File.basename(destination_root)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
"#{name.to_s.underscore.dasherize}-web-assets"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveStorage
|
|
4
|
+
# Active Storage's default direct upload endpoint (POST /rails/active_storage/direct_uploads)
|
|
5
|
+
# mints a signed URL that lets the caller PUT a file straight into the storage service.
|
|
6
|
+
# Left unauthenticated, it is an open door for anyone to fill the asset bucket.
|
|
7
|
+
# @see https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers
|
|
8
|
+
#
|
|
9
|
+
# This controller restricts the endpoint to authenticated principals and rate limits
|
|
10
|
+
# requests using #current_user to identify a database-backed principal for authorisation and
|
|
11
|
+
# request throttling. If no principal is returned requests fail with :unauthorized.
|
|
12
|
+
# @see config/routes/overrides.rb
|
|
13
|
+
class AuthenticatedDirectUploadsController < ActiveStorage::DirectUploadsController
|
|
14
|
+
before_action :authenticate_user!
|
|
15
|
+
rate_limit to: 20, within: 10.seconds, by: -> { current_user.to_gid }
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# Reject any request we can't attribute to a #current_user.
|
|
20
|
+
def authenticate_user!
|
|
21
|
+
head(:unauthorized) if current_user.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The authenticated principal for the current request, or nil when the request is anonymous.
|
|
25
|
+
def current_user
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Drawn from config/routes.rb via `draw :overrides`.
|
|
4
|
+
|
|
5
|
+
# Only authenticated users can post files.
|
|
6
|
+
# @see app/controllers/active_storage/authenticated_direct_uploads_controller.rb
|
|
7
|
+
post "/rails/active_storage/direct_uploads", to: "active_storage/authenticated_direct_uploads#create"
|
|
8
|
+
|
|
9
|
+
# Disable the disk service routes unless files are actually served from local disk.
|
|
10
|
+
unless %i[local test].include?(Rails.application.config.active_storage.service)
|
|
11
|
+
no_route = ->(req) { raise ActionController::RoutingError, "No route matches #{req['REQUEST_PATH']}" }
|
|
12
|
+
|
|
13
|
+
get "/rails/active_storage/disk/:encoded_key/*filename", to: no_route
|
|
14
|
+
put "/rails/active_storage/disk/:encoded_token", to: no_route
|
|
15
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
test:
|
|
2
|
+
service: Disk
|
|
3
|
+
root: <%%= Rails.root.join("tmp/storage") %>
|
|
4
|
+
|
|
5
|
+
local:
|
|
6
|
+
service: Disk
|
|
7
|
+
root: <%%= Rails.root.join("storage") %>
|
|
8
|
+
|
|
9
|
+
s3:
|
|
10
|
+
service: S3
|
|
11
|
+
region: ap-southeast-2
|
|
12
|
+
bucket: <%%= ENV.fetch("RAILS_ASSETS_BUCKET_ID", "<%= asset_bucket_name %>") %>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe ActiveStorage::AuthenticatedDirectUploadsController do
|
|
6
|
+
let(:action) { post(rails_direct_uploads_path, params:) }
|
|
7
|
+
let(:file) { StringIO.new("example") }
|
|
8
|
+
let(:params) do
|
|
9
|
+
{
|
|
10
|
+
blob: {
|
|
11
|
+
filename: "example.txt",
|
|
12
|
+
content_type: "text/plain",
|
|
13
|
+
byte_size: file.string.bytesize,
|
|
14
|
+
checksum: OpenSSL::Digest::MD5.new(file.string).base64digest,
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "rejects uploads from anonymous users" do
|
|
20
|
+
action
|
|
21
|
+
|
|
22
|
+
expect(response).to have_http_status(:unauthorized)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context "as a signed-in admin", pending: "requires admin session setup" do
|
|
26
|
+
# include_context "with admin session"
|
|
27
|
+
|
|
28
|
+
it "permits the upload" do
|
|
29
|
+
action
|
|
30
|
+
|
|
31
|
+
expect(response).to have_http_status(:ok)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context "as a signed-in user", pending: "requires user session setup" do
|
|
36
|
+
# before { sign_in(create(:user)) }
|
|
37
|
+
|
|
38
|
+
it "permits the upload" do
|
|
39
|
+
action
|
|
40
|
+
|
|
41
|
+
expect(response).to have_http_status(:ok)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "rate limiting", pending: "requires a user session" do
|
|
46
|
+
# rate_limit captures the cache in a lambda at class-load time, so stub the call not the object.
|
|
47
|
+
let(:cache) { ActiveSupport::Cache::MemoryStore.new }
|
|
48
|
+
let(:rate_limit) { 20 }
|
|
49
|
+
|
|
50
|
+
before do
|
|
51
|
+
allow(Rails.cache).to receive(:increment) do |name, amount = 1, **options|
|
|
52
|
+
cache.increment(name, amount, **options)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# include_context "with admin session"
|
|
57
|
+
|
|
58
|
+
it "throttles bursts of uploads with 429 Too Many Requests" do
|
|
59
|
+
aggregate_failures do
|
|
60
|
+
rate_limit.times do
|
|
61
|
+
post(rails_direct_uploads_path, params:, headers:)
|
|
62
|
+
expect(response).to have_http_status(:ok)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
post(rails_direct_uploads_path, params:, headers:)
|
|
66
|
+
expect(response).to have_http_status(:too_many_requests)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs the Katalyst content security policy configuration.
|
|
3
|
+
|
|
4
|
+
Generates config/initializers/content_security_policy.rb with a
|
|
5
|
+
nonce-based policy controlled per deployment via CONTENT_SECURITY_POLICY:
|
|
6
|
+
|
|
7
|
+
- enforce (default): the policy is enforced
|
|
8
|
+
- report: policy violations are reported but not enforced — use this to
|
|
9
|
+
trial policy changes and review violations in Sentry before enforcing
|
|
10
|
+
- none: no policy header is sent — set this on existing apps that have
|
|
11
|
+
not yet reviewed the policy against their content
|
|
12
|
+
|
|
13
|
+
Violation reports are delivered to Sentry when SENTRY_DSN is set and the
|
|
14
|
+
Sentry initializer registers the report URI (see thermite:install:sentry).
|
|
15
|
+
|
|
16
|
+
See https://docs.sentry.io/platforms/ruby/guides/rails/security-policy-reporting/
|
|
17
|
+
and the project's Sentry Project Settings > SDK Setup > Security Headers page
|
|
18
|
+
(https://<org>.sentry.io/settings/projects/<project>/security-headers/).
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
bin/rails generate thermite:install:content_security_policy
|
data/lib/generators/thermite/install/content_security_policy/content_security_policy_generator.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
|
|
6
|
+
module Thermite
|
|
7
|
+
module Install
|
|
8
|
+
class ContentSecurityPolicyGenerator < Rails::Generators::Base
|
|
9
|
+
def self.source_root
|
|
10
|
+
File.expand_path("templates", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def copy_files
|
|
14
|
+
copy_file "config/initializers/content_security_policy.rb"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Be sure to restart your server when you modify this file.
|
|
4
|
+
|
|
5
|
+
# Define an application-wide content security policy.
|
|
6
|
+
# See the Securing Rails Applications Guide for more information:
|
|
7
|
+
# https://guides.rubyonrails.org/security.html#content-security-policy-header
|
|
8
|
+
|
|
9
|
+
# Enforced by default. Set CONTENT_SECURITY_POLICY=report per deployment to
|
|
10
|
+
# trial policy changes and review violations in Sentry, or =none for existing
|
|
11
|
+
# apps that have not yet reviewed the policy against their content.
|
|
12
|
+
CSP_MODE = ENV.fetch("CONTENT_SECURITY_POLICY", "enforce").freeze
|
|
13
|
+
|
|
14
|
+
warn "Unknown configuration CONTENT_SECURITY_POLICY=#{CSP_MODE}" unless CSP_MODE.in?(%w[none report enforce])
|
|
15
|
+
|
|
16
|
+
return if CSP_MODE == "none"
|
|
17
|
+
|
|
18
|
+
# Deliver violation reports to Sentry (see config/initializers/sentry.rb)
|
|
19
|
+
# https://docs.sentry.io/platforms/ruby/guides/rails/security-policy-reporting/
|
|
20
|
+
# Sentry's report endpoint is shown under Project Settings > SDK Setup > Security Headers,
|
|
21
|
+
# e.g. https://<org>.sentry.io/settings/projects/<project>/security-headers/
|
|
22
|
+
reporting = if ENV.key?("SENTRY_DSN")
|
|
23
|
+
sentry_uri = "https://#{URI(ENV.fetch('SENTRY_DSN')).hostname}"
|
|
24
|
+
[sentry_uri, sentry_uri.gsub("ingest.sentry", "ingest.us.sentry")]
|
|
25
|
+
else
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Rails.application.configure do
|
|
30
|
+
config.content_security_policy do |policy|
|
|
31
|
+
policy.default_src(:self)
|
|
32
|
+
policy.frame_ancestors(:self)
|
|
33
|
+
|
|
34
|
+
# Permissive policy, many browser plugins use custom fonts
|
|
35
|
+
policy.font_src(:self, :data, :https)
|
|
36
|
+
|
|
37
|
+
# Permissive policy, allow data URLs and https (hotlinking, tracking pixels)
|
|
38
|
+
policy.img_src(:self, :data, :https)
|
|
39
|
+
|
|
40
|
+
# Block legacy embeds, such as flash
|
|
41
|
+
policy.object_src(:none)
|
|
42
|
+
|
|
43
|
+
# Restricted policy, all scripts should have nonce or SRI. strict-dynamic
|
|
44
|
+
# allows scripts to load children (CSP-3); unsafe-inline and https are
|
|
45
|
+
# fallbacks for legacy browsers, ignored when nonces are supported.
|
|
46
|
+
policy.script_src(:self, :strict_dynamic, :wasm_unsafe_eval, :unsafe_inline, :https)
|
|
47
|
+
|
|
48
|
+
# Permissive policy, browser plugins inject inline styles.
|
|
49
|
+
# Safari < 26.0 does not support -attr and -elem (https://bugs.webkit.org/show_bug.cgi?id=276931)
|
|
50
|
+
policy.style_src(:self, :unsafe_inline, :https)
|
|
51
|
+
|
|
52
|
+
# Permissive policy, allows external embeds and user browser plugins
|
|
53
|
+
policy.frame_src(:self, :https)
|
|
54
|
+
|
|
55
|
+
# Permissive policy, many browser plugins use connect
|
|
56
|
+
policy.connect_src(:self, :data, :https, *reporting)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Generate nonces for permitted importmap, inline scripts, and inline styles.
|
|
60
|
+
# A fresh nonce per request is incompatible with conditional GET caching; apps
|
|
61
|
+
# that create a session on every request can use the session id instead, see
|
|
62
|
+
# https://guides.rubyonrails.org/security.html#adding-a-nonce
|
|
63
|
+
config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
|
|
64
|
+
config.content_security_policy_nonce_directives = %w[script-src style-src-elem]
|
|
65
|
+
|
|
66
|
+
# Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
|
|
67
|
+
# if the corresponding directives are specified in `content_security_policy_nonce_directives`.
|
|
68
|
+
config.content_security_policy_nonce_auto = true
|
|
69
|
+
|
|
70
|
+
# Report violations without enforcing the policy.
|
|
71
|
+
config.content_security_policy_report_only = true if CSP_MODE == "report"
|
|
72
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
|
|
6
|
+
module Thermite
|
|
7
|
+
module Install
|
|
8
|
+
class DockerGenerator < Rails::Generators::Base
|
|
9
|
+
def self.source_root
|
|
10
|
+
File.expand_path("templates", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def copy_docker_files
|
|
14
|
+
template "Dockerfile.tt", "Dockerfile"
|
|
15
|
+
copy_file ".dockerignore"
|
|
16
|
+
copy_file "bin/docker-entrypoint"
|
|
17
|
+
chmod "bin/docker-entrypoint", 0o755 & ~File.umask, verbose: false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def remove_legacy_docker_directory
|
|
21
|
+
remove_dir "docker" if Pathname(destination_root).join("docker").directory?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def application_name
|
|
27
|
+
if defined?(Rails.application) && Rails.application
|
|
28
|
+
Rails.application.class.module_parent_name.to_s.underscore.dasherize
|
|
29
|
+
else
|
|
30
|
+
File.basename(destination_root).underscore.dasherize
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
# check=error=true
|
|
3
|
+
|
|
4
|
+
ARG APPLICATION_NAME="<%= application_name %>"
|
|
5
|
+
ARG RUBY_VERSION=4.0
|
|
6
|
+
FROM ruby:$RUBY_VERSION-slim AS base
|
|
7
|
+
|
|
8
|
+
ARG PACKAGES="curl libvips libjemalloc2 postgresql-client"
|
|
9
|
+
|
|
10
|
+
# Rails app lives here
|
|
11
|
+
WORKDIR /rails
|
|
12
|
+
|
|
13
|
+
# Install base packages
|
|
14
|
+
RUN apt-get update -qq && \
|
|
15
|
+
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $PACKAGES && \
|
|
16
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
17
|
+
|
|
18
|
+
ARG APPLICATION_NAME
|
|
19
|
+
ARG APPLICATION_VERSION
|
|
20
|
+
ARG APPLICATION_REVISION
|
|
21
|
+
|
|
22
|
+
# Set production environment
|
|
23
|
+
ENV APPLICATION_NAME="${APPLICATION_NAME}" \
|
|
24
|
+
BUNDLE_DEPLOYMENT="1" \
|
|
25
|
+
BUNDLE_PATH="/usr/local/bundle" \
|
|
26
|
+
BUNDLE_WITHOUT="development:test" \
|
|
27
|
+
EXECJS_RUNTIME="Disabled" \
|
|
28
|
+
RAILS_ENV="production" \
|
|
29
|
+
PUMA_SSL="1" \
|
|
30
|
+
TZ="Australia/Adelaide"
|
|
31
|
+
|
|
32
|
+
# Create a rails group and user for non-privileged access
|
|
33
|
+
RUN groupadd --system --gid 1000 rails && \
|
|
34
|
+
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
|
35
|
+
echo "$APPLICATION_NAME ($APPLICATION_VERSION) [$APPLICATION_REVISION]" > /etc/motd && \
|
|
36
|
+
echo "PS1='\u@\${APPLICATION_NAME}-\${RAILS_ENV}:\w\$ '" >> /etc/bash.bashrc && \
|
|
37
|
+
echo "cat /etc/motd" >> /etc/bash.bashrc
|
|
38
|
+
|
|
39
|
+
# Build rails gems and assets in a throw-away build stage to reduce size of final image
|
|
40
|
+
FROM base AS build
|
|
41
|
+
|
|
42
|
+
# Install packages needed to build gems
|
|
43
|
+
ARG BUILD_PACKAGES="build-essential git libcurl4-openssl-dev libpq-dev libyaml-dev pkg-config zlib1g-dev"
|
|
44
|
+
RUN apt-get update -qq && \
|
|
45
|
+
apt-get install --no-install-recommends -y $BUILD_PACKAGES && \
|
|
46
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
47
|
+
|
|
48
|
+
# Install application gems
|
|
49
|
+
COPY Gemfile Gemfile.lock ./
|
|
50
|
+
RUN bundle install && \
|
|
51
|
+
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
|
|
52
|
+
bundle exec bootsnap precompile --gemfile
|
|
53
|
+
|
|
54
|
+
# Copy application code
|
|
55
|
+
COPY --link . .
|
|
56
|
+
|
|
57
|
+
# Precompile bootsnap code for faster boot times
|
|
58
|
+
RUN bundle exec bootsnap precompile app/ lib/
|
|
59
|
+
|
|
60
|
+
# If set, assets will be compiled into a subdirectory of public/ with this prefix
|
|
61
|
+
ARG CDN_ASSET_PREFIX=""
|
|
62
|
+
|
|
63
|
+
# Precompile assets for production
|
|
64
|
+
RUN DATABASE_URL="postgresql://localhost" \
|
|
65
|
+
SECRET_KEY_BASE_DUMMY=1 \
|
|
66
|
+
./bin/rails assets:precompile
|
|
67
|
+
|
|
68
|
+
# END OF BUILD ##
|
|
69
|
+
|
|
70
|
+
# Copy assets to the CDN
|
|
71
|
+
FROM amazon/aws-cli:latest AS cdn
|
|
72
|
+
|
|
73
|
+
ARG AWS_DEFAULT_REGION="ap-southeast-2"
|
|
74
|
+
ARG CDN_ASSET_PREFIX=""
|
|
75
|
+
ARG S3_ASSETS_BUCKET=""
|
|
76
|
+
|
|
77
|
+
COPY --from=build /rails/public public
|
|
78
|
+
RUN --mount=type=secret,id=aws-key-id,env=AWS_ACCESS_KEY_ID \
|
|
79
|
+
--mount=type=secret,id=aws-secret-key,env=AWS_SECRET_ACCESS_KEY \
|
|
80
|
+
--mount=type=secret,id=aws-session-token,env=AWS_SESSION_TOKEN \
|
|
81
|
+
if [ -n "$CDN_ASSET_PREFIX" ] && [ -n "$S3_ASSETS_BUCKET" ]; then \
|
|
82
|
+
aws s3 sync --only-show-errors --cache-control "public, max-age=31536000, immutable" \
|
|
83
|
+
public/$CDN_ASSET_PREFIX s3://$S3_ASSETS_BUCKET/$CDN_ASSET_PREFIX; \
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# END OF CDN ##
|
|
87
|
+
|
|
88
|
+
# Assemble everything into a single layer
|
|
89
|
+
FROM base AS assemble
|
|
90
|
+
|
|
91
|
+
ARG APPLICATION_VERSION
|
|
92
|
+
ARG APPLICATION_REVISION
|
|
93
|
+
|
|
94
|
+
COPY --link . .
|
|
95
|
+
COPY --from=build /rails/tmp/cache/bootsnap tmp/cache/bootsnap
|
|
96
|
+
COPY --from=cdn /aws/public public
|
|
97
|
+
RUN echo $APPLICATION_VERSION > VERSION && \
|
|
98
|
+
echo $APPLICATION_REVISION > REVISION && \
|
|
99
|
+
mkdir -p db log ssl storage tmp/pids tmp/sockets && \
|
|
100
|
+
chown -R rails:rails db log ssl storage tmp
|
|
101
|
+
|
|
102
|
+
# Final stage for app image
|
|
103
|
+
FROM base
|
|
104
|
+
|
|
105
|
+
ARG CDN_ASSET_PREFIX
|
|
106
|
+
|
|
107
|
+
# Puma and Rails configuration for a small server setup with puma in single mode with 5 workers
|
|
108
|
+
ENV CDN_ASSET_PREFIX=$CDN_ASSET_PREFIX \
|
|
109
|
+
WEB_CONCURRENCY=0 \
|
|
110
|
+
RAILS_MAX_THREADS=5
|
|
111
|
+
|
|
112
|
+
# Assemble layers
|
|
113
|
+
COPY --from=build --link "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
|
114
|
+
COPY --from=assemble --link /rails .
|
|
115
|
+
|
|
116
|
+
USER 1000:1000
|
|
117
|
+
EXPOSE 4443
|
|
118
|
+
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
|
119
|
+
CMD ["./bin/rails", "server"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/bash -e
|
|
2
|
+
|
|
3
|
+
# Enable jemalloc for reduced memory usage and latency.
|
|
4
|
+
if [ -z "${LD_PRELOAD+x}" ]; then
|
|
5
|
+
LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit)
|
|
6
|
+
export LD_PRELOAD
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
# If running the rails server then create or migrate existing database
|
|
10
|
+
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
|
|
11
|
+
./bin/rails db:prepare
|
|
12
|
+
|
|
13
|
+
# create a self-signed certificate for puma
|
|
14
|
+
mkdir -p ~/.ssl
|
|
15
|
+
openssl req -x509 -newkey rsa:2048 -nodes -days 730 \
|
|
16
|
+
-subj '/CN=docker/' \
|
|
17
|
+
-keyout ~/.ssl/docker.key \
|
|
18
|
+
-out ~/.ssl/docker.crt
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
exec "${@}"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs the Katalyst permissions policy configuration.
|
|
3
|
+
|
|
4
|
+
Generates config/initializers/permissions_policy.rb with a restrictive
|
|
5
|
+
Permissions-Policy header: camera, microphone, geolocation, payment,
|
|
6
|
+
gyroscope, and usb are denied, and fullscreen is limited to the app
|
|
7
|
+
itself. Allow specific features (e.g. geolocation, payment) only for apps
|
|
8
|
+
that use them. The header applies as soon as it is deployed.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
bin/rails generate thermite:install:permissions_policy
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
|
|
6
|
+
module Thermite
|
|
7
|
+
module Install
|
|
8
|
+
class PermissionsPolicyGenerator < Rails::Generators::Base
|
|
9
|
+
def self.source_root
|
|
10
|
+
File.expand_path("templates", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def copy_files
|
|
14
|
+
copy_file "config/initializers/permissions_policy.rb"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Rails.application.config.permissions_policy do |policy|
|
|
4
|
+
policy.camera :none
|
|
5
|
+
policy.fullscreen :self
|
|
6
|
+
policy.geolocation :none
|
|
7
|
+
policy.gyroscope :none
|
|
8
|
+
policy.microphone :none
|
|
9
|
+
policy.payment :none
|
|
10
|
+
policy.usb :none
|
|
11
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs the Katalyst Sentry configuration.
|
|
3
|
+
|
|
4
|
+
Generates config/initializers/sentry.rb with the standard errors-only
|
|
5
|
+
data posture for sentry-ruby/sentry-rails 7.x:
|
|
6
|
+
|
|
7
|
+
- structured logging disabled (no request/SQL logs sent to Sentry)
|
|
8
|
+
- explicit data_collection settings (no user info, cookies, bodies,
|
|
9
|
+
query params, or database query values)
|
|
10
|
+
- error events sanitized with the app's parameter filter
|
|
11
|
+
- CSP violations reported to Sentry when a content security policy is
|
|
12
|
+
configured (see thermite:install:content_security_policy and
|
|
13
|
+
https://docs.sentry.io/platforms/ruby/guides/rails/security-policy-reporting/)
|
|
14
|
+
- tracing and profiling off unless enabled per deployment via
|
|
15
|
+
SENTRY_TRACES_SAMPLE_RATE / SENTRY_PROFILES_SAMPLE_RATE
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
bin/rails generate thermite:install:sentry
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
|
|
6
|
+
module Thermite
|
|
7
|
+
module Install
|
|
8
|
+
class SentryGenerator < Rails::Generators::Base
|
|
9
|
+
def self.source_root
|
|
10
|
+
File.expand_path("templates", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Fail early with a helpful message when sentry-rails isn't available.
|
|
14
|
+
def verify_sentry!
|
|
15
|
+
require "sentry-rails"
|
|
16
|
+
rescue LoadError
|
|
17
|
+
raise Thor::Error, <<~MSG.strip
|
|
18
|
+
thermite:install:sentry requires the sentry-rails gem, which is not available.
|
|
19
|
+
Add it to your Gemfile and run `bundle install`:
|
|
20
|
+
|
|
21
|
+
gem "sentry-rails"
|
|
22
|
+
MSG
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def copy_files
|
|
26
|
+
template "config/initializers/sentry.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# The environments Sentry should report from: every deployed environment,
|
|
32
|
+
# derived from config/environments/*.rb so we don't hard-code names like
|
|
33
|
+
# "staging" / "uat".
|
|
34
|
+
def enabled_environments
|
|
35
|
+
environments
|
|
36
|
+
.map { |pathname| pathname.basename(".rb").to_s }
|
|
37
|
+
.reject { |environment| %w[development test].include?(environment) }
|
|
38
|
+
.sort_by { |environment| environment == "production" ? 1 : 0 }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def environments
|
|
42
|
+
Pathname(destination_root).join("config/environments").glob("*.rb")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Sanitize error events with the application's parameter filter
|
|
4
|
+
filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
|
5
|
+
sanitize = lambda do |event, _hint|
|
|
6
|
+
event.extra = filter.filter(event.extra) if event.extra
|
|
7
|
+
event.user = filter.filter(event.user) if event.user
|
|
8
|
+
event.contexts = filter.filter(event.contexts) if event.contexts
|
|
9
|
+
event
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
ActiveSupport.on_load(:active_record) do
|
|
13
|
+
version = Rails.root.join("VERSION")
|
|
14
|
+
|
|
15
|
+
# DSN is set via SENTRY_DSN in ENV
|
|
16
|
+
Sentry.init do |config|
|
|
17
|
+
config.enabled_environments = %w[<%= enabled_environments.join(" ") %>]
|
|
18
|
+
config.release = File.read(version).strip.split("/").last if version.exist?
|
|
19
|
+
|
|
20
|
+
# Errors only: structured logging sends a log event for every request and
|
|
21
|
+
# SQL query. Collecting logs is a per-client data decision.
|
|
22
|
+
config.rails.structured_logging.enabled = false
|
|
23
|
+
|
|
24
|
+
# Data collection posture, based on sentry-ruby 7's `send_default_pii`
|
|
25
|
+
pii_headers = %w[forwarded -ip _ip remote via _user -user]
|
|
26
|
+
|
|
27
|
+
config.data_collection.user_info = false
|
|
28
|
+
config.data_collection.cookies.mode = :off
|
|
29
|
+
config.data_collection.http_headers.request.terms = pii_headers
|
|
30
|
+
config.data_collection.http_headers.response.terms = pii_headers
|
|
31
|
+
config.data_collection.http_bodies = []
|
|
32
|
+
config.data_collection.url_query_params.mode = :off
|
|
33
|
+
config.data_collection.database_query_data = false
|
|
34
|
+
config.data_collection.queues = false
|
|
35
|
+
|
|
36
|
+
config.before_send = sanitize
|
|
37
|
+
|
|
38
|
+
# Report CSP violations to Sentry (requires a content security policy)
|
|
39
|
+
# https://docs.sentry.io/platforms/ruby/guides/rails/security-policy-reporting/
|
|
40
|
+
if (policy = Rails.application.config.content_security_policy).present? && config.csp_report_uri.present?
|
|
41
|
+
policy.report_uri(config.csp_report_uri)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Tracing and profiling, off unless enabled per deployment.
|
|
45
|
+
# Profiling requires the vernier gem.
|
|
46
|
+
config.traces_sample_rate = ENV.fetch("SENTRY_TRACES_SAMPLE_RATE", "0").to_f
|
|
47
|
+
config.profiles_sample_rate = ENV.fetch("SENTRY_PROFILES_SAMPLE_RATE", "0").to_f
|
|
48
|
+
config.profiler_class = Sentry::Vernier::Profiler
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs Solid Cable using a single, shared database (Katalyst's preferred setup).
|
|
3
|
+
|
|
4
|
+
Unlike `solid_cable:install`, this does not provision a separate cable
|
|
5
|
+
database. The solid_cable tables are added to the primary database via a
|
|
6
|
+
normal migration in db/migrate, and config/cable.yml uses the solid_cable
|
|
7
|
+
adapter without a `connects_to` configuration.
|
|
8
|
+
|
|
9
|
+
Requires the solid_cable gem to be present in the host application.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
bin/rails thermite:install:solid_cable
|
|
13
|
+
|
|
14
|
+
This will:
|
|
15
|
+
Create config/cable.yml
|
|
16
|
+
Create a db/migrate migration for the solid_cable tables
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
require "rails/generators/active_record/migration"
|
|
6
|
+
|
|
7
|
+
require "active_record"
|
|
8
|
+
require "yaml"
|
|
9
|
+
|
|
10
|
+
# For a single-database install, solid cable's documented setup is to copy the contents
|
|
11
|
+
# of db/cable_schema.rb into a normal migration, delete db/cable_schema.rb,
|
|
12
|
+
# remove connects_to from config/cable.yml, then run db:migrate.
|
|
13
|
+
# Thermite follows that path by generating the migration directly.
|
|
14
|
+
#
|
|
15
|
+
# solid_cable is an optional dependency: it is required lazily in #verify_solid_cable!
|
|
16
|
+
# so the generator still loads (and lists) when solid_cable is absent.
|
|
17
|
+
module Thermite
|
|
18
|
+
module Install
|
|
19
|
+
class SolidCableGenerator < Rails::Generators::Base
|
|
20
|
+
# Provides migration_template, next_migration_number (timestamped) and the
|
|
21
|
+
# CreateMigration action that makes re-runs safe: identical content is
|
|
22
|
+
# skipped, a same-named migration with different content raises a conflict.
|
|
23
|
+
include ActiveRecord::Generators::Migration
|
|
24
|
+
|
|
25
|
+
# Reuse solid_cable's own templates. Resolved lazily so the class loads
|
|
26
|
+
# without solid_cable present; populated once #verify_solid_cable! requires it.
|
|
27
|
+
def self.source_root
|
|
28
|
+
::SolidCable::InstallGenerator.source_root if defined?(::SolidCable::InstallGenerator)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Search our own templates (the migration) first, then solid_cable's
|
|
32
|
+
# (cable.yml).
|
|
33
|
+
def self.source_paths
|
|
34
|
+
[File.expand_path("templates", __dir__), source_root].compact
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Our source_root points into solid_cable, so the inherited USAGE lookup
|
|
38
|
+
# would resolve to solid_cable's USAGE. Point it at ours instead.
|
|
39
|
+
def self.usage_path
|
|
40
|
+
File.expand_path("USAGE", __dir__)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Fail early with a helpful message when solid_cable isn't available.
|
|
44
|
+
def verify_solid_cable!
|
|
45
|
+
require "solid_cable/version"
|
|
46
|
+
require "generators/solid_cable/install/install_generator"
|
|
47
|
+
rescue LoadError
|
|
48
|
+
raise Thor::Error, <<~MSG.strip
|
|
49
|
+
thermite:install:solid_cable requires the solid_cable gem, which is not available.
|
|
50
|
+
Add it to your Gemfile and run `bundle install`:
|
|
51
|
+
|
|
52
|
+
gem "solid_cable"
|
|
53
|
+
MSG
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Based on SolidCable::InstallGenerator#copy_files but writing schema to a migration instead
|
|
57
|
+
# @see https://github.com/rails/solid_cable#single-database-configuration
|
|
58
|
+
def copy_files
|
|
59
|
+
template "config/cable.yml"
|
|
60
|
+
migration_template "add_solid_cable.rb", "db/migrate/add_solid_cable.rb", skip: true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# The application's extra environments, derived from config/environments/*.rb so
|
|
66
|
+
# we don't hard-code names like "staging" / "uat".
|
|
67
|
+
def extra_environments
|
|
68
|
+
environments
|
|
69
|
+
.reject { |pathname| local?(pathname) || production?(pathname) }
|
|
70
|
+
.map { |path| path.basename(".rb").to_s }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def environments
|
|
74
|
+
Pathname(destination_root).join("config/environments").glob("*.rb")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def local?(pathname)
|
|
78
|
+
%w[development test].include?(pathname.basename(".rb").to_s)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def production?(pathname)
|
|
82
|
+
pathname.basename(".rb").to_s == "production"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def solid_cable_schema_body
|
|
86
|
+
schema = File.read(File.join(self.class.source_root, "db/cable_schema.rb"))
|
|
87
|
+
|
|
88
|
+
schema
|
|
89
|
+
# Keep only the schema block body.
|
|
90
|
+
.sub(%r{\A.*ActiveRecord::Schema[^\n]+do\n}, "")
|
|
91
|
+
.sub(/\nend\n?\z/, "")
|
|
92
|
+
.lines
|
|
93
|
+
.map { |line| line == "\n" ? line : " #{line}" }
|
|
94
|
+
.join
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
development:
|
|
2
|
+
adapter: async
|
|
3
|
+
|
|
4
|
+
test:
|
|
5
|
+
adapter: test
|
|
6
|
+
|
|
7
|
+
production: &production
|
|
8
|
+
adapter: solid_cable
|
|
9
|
+
polling_interval: 0.1.seconds
|
|
10
|
+
message_retention: 1.day
|
|
11
|
+
|
|
12
|
+
<% extra_environments.each do |environment| -%>
|
|
13
|
+
|
|
14
|
+
<%= environment %>:
|
|
15
|
+
<<: *production
|
|
16
|
+
<% end -%>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs Solid Queue using a single, shared database (Katalyst's preferred setup).
|
|
3
|
+
|
|
4
|
+
Unlike `solid_queue:install`, this does not provision a separate queue
|
|
5
|
+
database. The solid_queue tables are added to the primary database via a
|
|
6
|
+
normal migration in db/migrate, and `config.active_job.queue_adapter` is set
|
|
7
|
+
to :solid_queue without a `connects_to` configuration.
|
|
8
|
+
|
|
9
|
+
Requires the solid_queue gem to be present in the host application.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
bin/rails thermite:install:solid_queue
|
|
13
|
+
|
|
14
|
+
This will:
|
|
15
|
+
Create config/queue.yml and config/recurring.yml
|
|
16
|
+
Create a db/migrate migration for the solid_queue tables
|
|
17
|
+
Install the bin/jobs binstub
|
|
18
|
+
Set the Active Job queue adapter to :solid_queue (non dev/test environments)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
require "rails/generators/active_record/migration"
|
|
6
|
+
|
|
7
|
+
require "active_record"
|
|
8
|
+
|
|
9
|
+
# For a single-database install, solid queue's documented setup is to copy the contents
|
|
10
|
+
# of db/queue_schema.rb into a normal migration, delete db/queue_schema.rb,
|
|
11
|
+
# remove config.solid_queue.connects_to, then run db:migrate.
|
|
12
|
+
module Thermite
|
|
13
|
+
module Install
|
|
14
|
+
class SolidQueueGenerator < Rails::Generators::Base
|
|
15
|
+
include ActiveRecord::Generators::Migration
|
|
16
|
+
|
|
17
|
+
# Reuse solid_queue's own templates. Resolved lazily so the class loads
|
|
18
|
+
# without solid_queue present; populated once #verify_solid_queue! requires it.
|
|
19
|
+
def self.source_root
|
|
20
|
+
::SolidQueue::InstallGenerator.source_root if defined?(::SolidQueue::InstallGenerator)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Search our own templates (the migration) first, then solid_queue's
|
|
24
|
+
# (queue.yml, recurring.yml, bin/jobs).
|
|
25
|
+
def self.source_paths
|
|
26
|
+
[File.expand_path("templates", __dir__), source_root].compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Our source_root points into solid_queue, so the inherited USAGE lookup
|
|
30
|
+
# would resolve to solid_queue's USAGE. Point it at ours instead.
|
|
31
|
+
def self.usage_path
|
|
32
|
+
File.expand_path("USAGE", __dir__)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Fail early with a helpful message when solid_queue isn't available.
|
|
36
|
+
def verify_solid_queue!
|
|
37
|
+
require "solid_queue/version"
|
|
38
|
+
require "generators/solid_queue/install/install_generator"
|
|
39
|
+
rescue LoadError
|
|
40
|
+
raise Thor::Error, <<~MSG.strip
|
|
41
|
+
thermite:install:solid_queue requires the solid_queue gem, which is not available.
|
|
42
|
+
Add it to your Gemfile and run `bundle install`:
|
|
43
|
+
|
|
44
|
+
gem "solid_queue"
|
|
45
|
+
MSG
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Based on SolidQueue::InstallGenerator#copy_files but writing schema to a migration instead
|
|
49
|
+
# @see https://github.com/rails/solid_queue#single-database-configuration
|
|
50
|
+
def copy_files
|
|
51
|
+
template "config/queue.yml"
|
|
52
|
+
template "config/recurring.yml"
|
|
53
|
+
migration_template "add_solid_queue.rb", "db/migrate/add_solid_queue.rb", skip: true
|
|
54
|
+
template "bin/jobs"
|
|
55
|
+
chmod "bin/jobs", 0o755 & ~File.umask, verbose: false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Based on SolidQueue::InstallGenerator#configure_adapter but without connects_to configuration
|
|
59
|
+
# @see https://github.com/rails/solid_queue#single-database-configuration
|
|
60
|
+
def configure_adapter
|
|
61
|
+
Pathname(destination_root).join("config/environments").glob("*.rb").each do |pathname|
|
|
62
|
+
next if %w[development.rb test.rb].include?(pathname.basename.to_s)
|
|
63
|
+
|
|
64
|
+
gsub_file pathname, /\n\s*config\.solid_queue\.connects_to\s+=.*\n/, "\n", verbose: false
|
|
65
|
+
gsub_file pathname, /(# )?config\.active_job\.queue_adapter\s+=.*\n/,
|
|
66
|
+
"config.active_job.queue_adapter = :solid_queue\n"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# The application's environments, derived from config/environments/*.rb so
|
|
73
|
+
# we don't hard-code names like "staging" / "uat".
|
|
74
|
+
def application_environments
|
|
75
|
+
Pathname(destination_root).join("config/environments")
|
|
76
|
+
.glob("*.rb")
|
|
77
|
+
.map { |path| path.basename(".rb").to_s }
|
|
78
|
+
.sort
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def solid_queue_schema_body
|
|
82
|
+
schema = File.read(File.join(self.class.source_root, "db/queue_schema.rb"))
|
|
83
|
+
|
|
84
|
+
schema
|
|
85
|
+
# Keep only the schema block body.
|
|
86
|
+
.sub(%r{\A.*ActiveRecord::Schema[^\n]+do\n}, "")
|
|
87
|
+
.sub(/\nend\n?\z/, "")
|
|
88
|
+
.lines
|
|
89
|
+
.map { |line| line == "\n" ? line : " #{line}" }
|
|
90
|
+
.join
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
default: &default
|
|
2
|
+
dispatchers:
|
|
3
|
+
- polling_interval: 1
|
|
4
|
+
batch_size: 500
|
|
5
|
+
workers:
|
|
6
|
+
- queues: "*"
|
|
7
|
+
threads: 3
|
|
8
|
+
processes: <%%= ENV.fetch("JOB_CONCURRENCY", 1) %>
|
|
9
|
+
polling_interval: 0.1
|
|
10
|
+
<% application_environments.each do |environment| -%>
|
|
11
|
+
|
|
12
|
+
<%= environment %>:
|
|
13
|
+
<<: *default
|
|
14
|
+
<% end -%>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
production: &production
|
|
2
|
+
clear_solid_queue_finished_jobs:
|
|
3
|
+
command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
|
|
4
|
+
schedule: every hour at minute 12
|
|
5
|
+
<% (application_environments - %w[development test production]).each do |environment| -%>
|
|
6
|
+
|
|
7
|
+
<%= environment %>:
|
|
8
|
+
<<: *production
|
|
9
|
+
<% end -%>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :thermite do
|
|
4
|
+
namespace :install do
|
|
5
|
+
desc "Install Solid Queue"
|
|
6
|
+
task :solid_queue do
|
|
7
|
+
Rails::Command.invoke :generate, ["thermite:install:solid_queue"]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
desc "Install Active Storage"
|
|
11
|
+
task :active_storage do
|
|
12
|
+
Rails::Command.invoke :generate, ["thermite:install:active_storage"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Install Solid Cable"
|
|
16
|
+
task :solid_cable do
|
|
17
|
+
Rails::Command.invoke :generate, ["thermite:install:solid_cable"]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/thermite.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thermite/engine"
|
|
4
|
+
|
|
5
|
+
require "active_support"
|
|
6
|
+
|
|
7
|
+
require "zeitwerk"
|
|
8
|
+
|
|
9
|
+
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
10
|
+
loader.ignore("#{__dir__}/generators")
|
|
11
|
+
loader.ignore("#{__dir__}/katalyst-thermite.rb")
|
|
12
|
+
loader.ignore("#{__dir__}/thermite/tasks.rb")
|
|
13
|
+
loader.setup
|
|
14
|
+
|
|
15
|
+
module Thermite
|
|
16
|
+
extend self
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: katalyst-thermite
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Katalyst Interactive
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: railties
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
email:
|
|
41
|
+
- developers@katalyst.com.au
|
|
42
|
+
executables: []
|
|
43
|
+
extensions: []
|
|
44
|
+
extra_rdoc_files: []
|
|
45
|
+
files:
|
|
46
|
+
- LICENSE
|
|
47
|
+
- README.md
|
|
48
|
+
- lib/generators/thermite/install/active_storage/USAGE
|
|
49
|
+
- lib/generators/thermite/install/active_storage/active_storage_generator.rb
|
|
50
|
+
- lib/generators/thermite/install/active_storage/templates/app/controllers/active_storage/authenticated_direct_uploads_controller.rb
|
|
51
|
+
- lib/generators/thermite/install/active_storage/templates/config/routes/overrides.rb
|
|
52
|
+
- lib/generators/thermite/install/active_storage/templates/config/storage.yml.tt
|
|
53
|
+
- lib/generators/thermite/install/active_storage/templates/spec/requests/active_storage/authenticated_direct_uploads_controller_spec.rb
|
|
54
|
+
- lib/generators/thermite/install/content_security_policy/USAGE
|
|
55
|
+
- lib/generators/thermite/install/content_security_policy/content_security_policy_generator.rb
|
|
56
|
+
- lib/generators/thermite/install/content_security_policy/templates/config/initializers/content_security_policy.rb
|
|
57
|
+
- lib/generators/thermite/install/docker/USAGE
|
|
58
|
+
- lib/generators/thermite/install/docker/docker_generator.rb
|
|
59
|
+
- lib/generators/thermite/install/docker/templates/Dockerfile.tt
|
|
60
|
+
- lib/generators/thermite/install/docker/templates/bin/docker-entrypoint
|
|
61
|
+
- lib/generators/thermite/install/permissions_policy/USAGE
|
|
62
|
+
- lib/generators/thermite/install/permissions_policy/permissions_policy_generator.rb
|
|
63
|
+
- lib/generators/thermite/install/permissions_policy/templates/config/initializers/permissions_policy.rb
|
|
64
|
+
- lib/generators/thermite/install/sentry/USAGE
|
|
65
|
+
- lib/generators/thermite/install/sentry/sentry_generator.rb
|
|
66
|
+
- lib/generators/thermite/install/sentry/templates/config/initializers/sentry.rb.tt
|
|
67
|
+
- lib/generators/thermite/install/solid_cable/USAGE
|
|
68
|
+
- lib/generators/thermite/install/solid_cable/solid_cable_generator.rb
|
|
69
|
+
- lib/generators/thermite/install/solid_cable/templates/add_solid_cable.rb.tt
|
|
70
|
+
- lib/generators/thermite/install/solid_cable/templates/config/cable.yml.tt
|
|
71
|
+
- lib/generators/thermite/install/solid_queue/USAGE
|
|
72
|
+
- lib/generators/thermite/install/solid_queue/solid_queue_generator.rb
|
|
73
|
+
- lib/generators/thermite/install/solid_queue/templates/add_solid_queue.rb.tt
|
|
74
|
+
- lib/generators/thermite/install/solid_queue/templates/config/queue.yml.tt
|
|
75
|
+
- lib/generators/thermite/install/solid_queue/templates/config/recurring.yml.tt
|
|
76
|
+
- lib/katalyst-thermite.rb
|
|
77
|
+
- lib/thermite.rb
|
|
78
|
+
- lib/thermite/engine.rb
|
|
79
|
+
- lib/thermite/tasks.rb
|
|
80
|
+
homepage: https://github.com/katalyst/thermite
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata:
|
|
84
|
+
rubygems_mfa_required: 'true'
|
|
85
|
+
rdoc_options: []
|
|
86
|
+
require_paths:
|
|
87
|
+
- lib
|
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '4.0'
|
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '0'
|
|
98
|
+
requirements: []
|
|
99
|
+
rubygems_version: 4.0.16
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: Katalyst tools for integrating common patterns into Rails apps
|
|
102
|
+
test_files: []
|