brut 0.20.2 → 0.21.0.pre.1
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 +4 -4
- data/lib/brut/cli/apps/deploy/deploy_config.rb +62 -0
- data/lib/brut/cli/apps/deploy/git_checks.rb +54 -0
- data/lib/brut/cli/apps/deploy.rb +275 -304
- data/lib/brut/cli/apps/new/add_segment.rb +5 -0
- data/lib/brut/cli/apps/new/app.rb +10 -0
- data/lib/brut/cli/apps/new/segments/docker_deploy.rb +30 -0
- data/lib/brut/cli/apps/new/segments/sidekiq.rb +8 -8
- data/lib/brut/cli/apps/new/segments.rb +1 -0
- data/lib/brut/cli/commands/base_command.rb +6 -2
- data/lib/brut/framework/app.rb +2 -1
- data/lib/brut/framework/config.rb +13 -1
- data/lib/brut/framework/mcp.rb +1 -4
- data/lib/brut/front_end/csrf_protector.rb +10 -6
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +1 -1
- data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +2 -0
- data/lib/brut/front_end/routing.rb +22 -0
- data/lib/brut/instrumentation/open_telemetry.rb +5 -0
- data/lib/brut/sinatra_helpers.rb +16 -4
- data/lib/brut/spec_support/cli_command_support.rb +60 -7
- data/lib/brut/spec_support/matchers/have_executed.rb +2 -0
- data/lib/brut/version.rb +1 -1
- data/templates/Base/dx/exec +10 -3
- data/templates/segments/DockerDeploy/deploy/Dockerfile +126 -0
- data/templates/segments/DockerDeploy/deploy/deploy_config.rb +17 -0
- data/templates/segments/DockerDeploy/deploy/docker-entrypoint +15 -0
- data/templates/segments/Heroku/deploy/deploy_config.rb +21 -0
- metadata +8 -3
- data/lib/brut/cli/apps/new/old_app.rb +0 -81
- data/templates/segments/Heroku/deploy/docker_config.rb +0 -30
|
@@ -161,6 +161,11 @@ class Brut::CLI::Apps::New::App < Brut::CLI::Commands::BaseCommand
|
|
|
161
161
|
project_root: base.project_root,
|
|
162
162
|
templates_dir:
|
|
163
163
|
)
|
|
164
|
+
when "docker-deploy"
|
|
165
|
+
segments << Brut::CLI::Apps::New::Segments::DockerDeploy.new(
|
|
166
|
+
project_root: base.project_root,
|
|
167
|
+
templates_dir:
|
|
168
|
+
)
|
|
164
169
|
when "demo"
|
|
165
170
|
# handled above
|
|
166
171
|
else
|
|
@@ -311,6 +316,11 @@ class Brut::CLI::Apps::New::App < Brut::CLI::Commands::BaseCommand
|
|
|
311
316
|
project_root:,
|
|
312
317
|
templates_dir:
|
|
313
318
|
)
|
|
319
|
+
elsif segment_name == "docker-deploy"
|
|
320
|
+
Brut::CLI::Apps::New::Segments::DockerDeploy.new(
|
|
321
|
+
project_root:,
|
|
322
|
+
templates_dir:
|
|
323
|
+
)
|
|
314
324
|
end
|
|
315
325
|
if !segment
|
|
316
326
|
error "'#{segment_name}' is not a segment. Allowed values: sidekiq, heroku"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class Brut::CLI::Apps::New::Segments::DockerDeploy < Brut::CLI::Apps::New::Base
|
|
2
|
+
def self.friendly_name = "Generic Docker-based Deployment"
|
|
3
|
+
def self.segment_name = "docker-deploy"
|
|
4
|
+
|
|
5
|
+
def initialize(project_root:, templates_dir:)
|
|
6
|
+
@project_root = project_root
|
|
7
|
+
@templates_dir = templates_dir / "segments" / "DockerDeploy"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def add!
|
|
11
|
+
operations = copy_files(@templates_dir, @project_root) +
|
|
12
|
+
other_operations
|
|
13
|
+
|
|
14
|
+
operations.each do |operation|
|
|
15
|
+
operation.call
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def <=>(other)
|
|
20
|
+
if self.class == other.class
|
|
21
|
+
0
|
|
22
|
+
elsif other.class == Brut::CLI::Apps::New::Segments::Sidekiq
|
|
23
|
+
# If both docker and sidekiq segments are activated, we want to do heroku first,
|
|
24
|
+
# since Sidekiq will need to modify it.
|
|
25
|
+
-1
|
|
26
|
+
else
|
|
27
|
+
1
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -47,6 +47,10 @@ class Brut::CLI::Apps::New::Segments::Sidekiq < Brut::CLI::Apps::New::Base
|
|
|
47
47
|
# If both herkou and sidekiq segments are activated, we want to do heroku first,
|
|
48
48
|
# since Sidekiq will need to modify it.
|
|
49
49
|
1
|
|
50
|
+
elsif other.class == Brut::CLI::Apps::New::Segments::DockerDeploy
|
|
51
|
+
# If both herkou and sidekiq segments are activated, we want to do heroku first,
|
|
52
|
+
# since Sidekiq will need to modify it.
|
|
53
|
+
1
|
|
50
54
|
else
|
|
51
55
|
-1
|
|
52
56
|
end
|
|
@@ -146,16 +150,12 @@ SIDEKIQ_BASIC_AUTH_PASSWORD=password
|
|
|
146
150
|
code: "@sidekiq_segment.boot!"
|
|
147
151
|
),
|
|
148
152
|
Brut::CLI::Apps::New::Ops::InsertCodeInMethod.new(
|
|
149
|
-
file: project_root / "deploy" / "
|
|
150
|
-
class_name: "
|
|
151
|
-
method_name: "
|
|
153
|
+
file: project_root / "deploy" / "deploy_config.rb",
|
|
154
|
+
class_name: "AppDeployConfig",
|
|
155
|
+
method_name: "additional_processes",
|
|
152
156
|
ignore_if_file_not_found: true,
|
|
153
157
|
code: %{
|
|
154
|
-
|
|
155
|
-
"sidekiq" => {
|
|
156
|
-
cmd: "bin/run sidekiq",
|
|
157
|
-
}
|
|
158
|
-
}
|
|
158
|
+
process_description("worker", [ "bundle", "exec", "bin/run sidekiq" ])
|
|
159
159
|
},
|
|
160
160
|
where: :end
|
|
161
161
|
),
|
|
@@ -4,5 +4,6 @@ module Brut::CLI::Apps::New
|
|
|
4
4
|
autoload :Demo, "brut/cli/apps/new/segments/demo"
|
|
5
5
|
autoload :Sidekiq, "brut/cli/apps/new/segments/sidekiq"
|
|
6
6
|
autoload :Heroku, "brut/cli/apps/new/segments/heroku"
|
|
7
|
+
autoload :DockerDeploy, "brut/cli/apps/new/segments/docker_deploy"
|
|
7
8
|
end
|
|
8
9
|
end
|
|
@@ -155,13 +155,16 @@ private
|
|
|
155
155
|
end
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
-
# Convienience
|
|
158
|
+
# Convienience method to defer to `Brut::CLI::Commands::ExecutionContext#stdout`'s `puts`.
|
|
159
159
|
# @!visibility public
|
|
160
160
|
def puts(*args)
|
|
161
161
|
if !options.quiet?
|
|
162
162
|
self.execution_context.stdout.puts(*args)
|
|
163
163
|
end
|
|
164
164
|
end
|
|
165
|
+
|
|
166
|
+
# Convienience method to defer to `Brut::CLI::Commands::ExecutionContext#stdout`'s `print`.
|
|
167
|
+
# @!visibility public
|
|
165
168
|
def print(*args)
|
|
166
169
|
if !options.quiet?
|
|
167
170
|
self.execution_context.stdout.print(*args)
|
|
@@ -190,7 +193,8 @@ private
|
|
|
190
193
|
|
|
191
194
|
|
|
192
195
|
# Runs whatever logic this command exists to execute. This is the method you must implement, however `#execute` is the public
|
|
193
|
-
# API and what you should call if you want to programmatically execute a command.
|
|
196
|
+
# API and what you should call if you want to programmatically execute a command. In other words, **do not
|
|
197
|
+
# call this method in a test, call `execute` instead**.
|
|
194
198
|
#
|
|
195
199
|
# The default implementation will generate an error, which is suitable for an app or namespace command that require a subcommand.
|
|
196
200
|
#
|
data/lib/brut/framework/app.rb
CHANGED
|
@@ -47,12 +47,13 @@ class Brut::Framework::App
|
|
|
47
47
|
# Note that Brut will record the exception via OpenTelemetry so you should not do this in your handlers. It
|
|
48
48
|
# would be preferable to instead record an event if you want to have observability from your error handlers.
|
|
49
49
|
#
|
|
50
|
-
# @param [Class|Integer|Range<Integer
|
|
50
|
+
# @param [Class|Integer|Range<Integer>|Symbol] condition if given this specifies the conditions under which the given
|
|
51
51
|
# block will handle the error. If omitted, this block will handle any error that doesn't have a more
|
|
52
52
|
# specific handler configured. Meaning of values:
|
|
53
53
|
# * A class - this is an exception class that, if caught, triggers the handler
|
|
54
54
|
# * An integer - this is an HTTP status code that, if returned, triggers the handler
|
|
55
55
|
# * A range of integers - this is a range of HTTP status codes that, if returned, triggers the handler
|
|
56
|
+
# * If the symbol is `:catch_all`, this will be used to handle any error not handled by a more specific handler.
|
|
56
57
|
# @yield [Exception] the block is given two named parameters: `exception:` and `http_status_code:`. Your block
|
|
57
58
|
# can declare both, either, or none. Any that are declared will be given values. At least one
|
|
58
59
|
# will be non-`nil`, however are encouraged to code defensively inside this block.
|
|
@@ -75,7 +75,10 @@ class Brut::Framework::Config
|
|
|
75
75
|
Object,
|
|
76
76
|
"Handle to the database",
|
|
77
77
|
) do |database_url|
|
|
78
|
-
Sequel.connect(
|
|
78
|
+
Sequel.connect(
|
|
79
|
+
database_url,
|
|
80
|
+
test: true, #XXX
|
|
81
|
+
)
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
c.store_ensured_path(
|
|
@@ -438,6 +441,15 @@ class Brut::Framework::Config
|
|
|
438
441
|
end
|
|
439
442
|
end
|
|
440
443
|
|
|
444
|
+
c.store(
|
|
445
|
+
"webhook_url_path_prefix",
|
|
446
|
+
String,
|
|
447
|
+
"The root url path under which Webhooks are placed.",
|
|
448
|
+
allow_app_override: true
|
|
449
|
+
) do
|
|
450
|
+
"/webhooks"
|
|
451
|
+
end
|
|
452
|
+
|
|
441
453
|
end
|
|
442
454
|
end
|
|
443
455
|
end
|
data/lib/brut/framework/mcp.rb
CHANGED
|
@@ -232,10 +232,7 @@ class Brut::Framework::MCP
|
|
|
232
232
|
[
|
|
233
233
|
{
|
|
234
234
|
allow_if: ->(env) {
|
|
235
|
-
|
|
236
|
-
app_allowed = Brut.container.csrf_protector.allowed?(env)
|
|
237
|
-
|
|
238
|
-
brut_owned_path || app_allowed
|
|
235
|
+
Brut.container.csrf_protector.allowed?(env)
|
|
239
236
|
},
|
|
240
237
|
message: message,
|
|
241
238
|
},
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
1
|
+
# Stores logic around what POST requests should require CSRF protection.
|
|
2
|
+
# Brut ideally wants *all* POST requests to require CSRF protection, however
|
|
3
|
+
# sometimes this is not convienient, notably webhooks. This class includes
|
|
4
|
+
# that logic.
|
|
5
|
+
#
|
|
6
|
+
# You may specify your own implementation via
|
|
4
7
|
# `Brut.container.override("csrf_protector", YourCustomCsrfProtector.new)` in your `App` class' initializer.
|
|
5
8
|
#
|
|
6
9
|
# @example
|
|
7
10
|
# class CsrfProtector < Brut::FrontEnd::CsrfProtector
|
|
8
11
|
# def allowed?(env)
|
|
9
|
-
#
|
|
12
|
+
# super(env) ||
|
|
13
|
+
# !!env["PATH_INFO"].to_s.match?(/^\/api\//)
|
|
10
14
|
# end
|
|
11
15
|
# end
|
|
12
16
|
# # Then, in app.rb
|
|
@@ -23,8 +27,8 @@
|
|
|
23
27
|
#
|
|
24
28
|
class Brut::FrontEnd::CsrfProtector
|
|
25
29
|
|
|
26
|
-
# Return true if the request should be allowed without a CSRF token. This implementation
|
|
30
|
+
# Return true if the request should be allowed without a CSRF token. This implementation allows webhooks and paths that Brut owns explicitly
|
|
27
31
|
def allowed?(env)
|
|
28
|
-
|
|
32
|
+
env["brut.webhook"] || env["brut.owned_path"]
|
|
29
33
|
end
|
|
30
34
|
end
|
|
@@ -75,7 +75,7 @@ class Brut::FrontEnd::Handlers::InstrumentationHandler < Brut::FrontEnd::Handler
|
|
|
75
75
|
|
|
76
76
|
if span.nil? || traceparent.nil?
|
|
77
77
|
SemanticLogger[self.class].info "Missing traceparent or span: #{@http_tracestate}, #{@http_traceparent}"
|
|
78
|
-
return http_status(
|
|
78
|
+
return http_status(200)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
carrier = traceparent.as_carrier
|
|
@@ -9,6 +9,8 @@ class Brut::FrontEnd::Middlewares::AnnotateBrutOwnedPaths < Brut::FrontEnd::Midd
|
|
|
9
9
|
if env["PATH_INFO"] =~ /^\/__brut\//
|
|
10
10
|
Brut.container.instrumentation.add_attributes(prefix: "brut", owned_path: true)
|
|
11
11
|
env["brut.owned_path"] = true
|
|
12
|
+
elsif env["PATH_INFO"] =~ /^#{Regexp.escape(Brut.container.webhook_url_path_prefix)}\//
|
|
13
|
+
env["brut.webhook"] = true
|
|
12
14
|
end
|
|
13
15
|
@app.call(env)
|
|
14
16
|
end
|
|
@@ -97,6 +97,28 @@ class Brut::FrontEnd::Routing
|
|
|
97
97
|
route
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
def register_webhook(path, method:)
|
|
101
|
+
path = path.gsub(/^\//,"")
|
|
102
|
+
full_path = "#{Brut.container.webhook_url_path_prefix}/#{path}"
|
|
103
|
+
|
|
104
|
+
if full_path =~ /^#{Regexp.escape(Brut.container.webhook_url_path_prefix)}#{Regexp.escape(Brut.container.webhook_url_path_prefix)}/
|
|
105
|
+
raise ArgumentError, "webhook '#{path}' should not start with #{Brut.container.webhook_url_path_prefix} - Brut will add that for you"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
route = begin
|
|
109
|
+
Route.new(method, full_path)
|
|
110
|
+
rescue Brut::Framework::Errors::NoClassForPath => ex
|
|
111
|
+
if Brut.container.project_env.development?
|
|
112
|
+
MissingPath.new(method,full_path,ex)
|
|
113
|
+
else
|
|
114
|
+
raise ex
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
@routes << route
|
|
118
|
+
add_routing_method(route)
|
|
119
|
+
route
|
|
120
|
+
end
|
|
121
|
+
|
|
100
122
|
def route(handler_class)
|
|
101
123
|
route = @routes.detect { |route|
|
|
102
124
|
handler_class_match = route.handler_class.name == handler_class.name
|
|
@@ -89,6 +89,11 @@ class Brut::Instrumentation::OpenTelemetry
|
|
|
89
89
|
)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
# Returns a trace ID for the current request/span. This is useful if you
|
|
93
|
+
# need to connect the current OTel request/trace with another system, such
|
|
94
|
+
# as error reporting.
|
|
95
|
+
def current_trace_id = OpenTelemetry::Trace.current_span.context.hex_trace_id
|
|
96
|
+
|
|
92
97
|
private
|
|
93
98
|
|
|
94
99
|
class NormalizedAttributes
|
data/lib/brut/sinatra_helpers.rb
CHANGED
|
@@ -126,7 +126,7 @@ module Brut::SinatraHelpers
|
|
|
126
126
|
#
|
|
127
127
|
def form(path)
|
|
128
128
|
route = Brut.container.routing.register_form(path)
|
|
129
|
-
self.define_handled_route(route
|
|
129
|
+
self.define_handled_route(route)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
# Declare a form action that has no associated form elements. This is used when you need to use a button to submit to the
|
|
@@ -137,7 +137,7 @@ module Brut::SinatraHelpers
|
|
|
137
137
|
# to make sure there is no form defined.
|
|
138
138
|
def action(path)
|
|
139
139
|
route = Brut.container.routing.register_handler_only(path)
|
|
140
|
-
self.define_handled_route(route
|
|
140
|
+
self.define_handled_route(route)
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
# When you need to respond to a given path/method, but it's not a page nor a form. For example, webhooks often
|
|
@@ -146,12 +146,24 @@ module Brut::SinatraHelpers
|
|
|
146
146
|
# This will locate a handler class based on the same naming convention as for forms.
|
|
147
147
|
def path(path, method:)
|
|
148
148
|
route = Brut.container.routing.register_path(path, method: Brut::FrontEnd::HttpMethod.new(method))
|
|
149
|
-
self.define_handled_route(route
|
|
149
|
+
self.define_handled_route(route)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Define a webhook, where another server will post data to your app.
|
|
153
|
+
# This method services two purposes beyond using `path` or `action`:
|
|
154
|
+
#
|
|
155
|
+
# 1. It nests `relative_path` under `/webhooks` as a convention for
|
|
156
|
+
# where all webhooks should go.
|
|
157
|
+
# 2. That convention will be used to skip CSRF protection for a POST
|
|
158
|
+
# made to this endpoint, since you would not want that for a webhook.
|
|
159
|
+
def webhook(relative_path, method: :post)
|
|
160
|
+
route = Brut.container.routing.register_webhook(relative_path, method: Brut::FrontEnd::HttpMethod.new(method))
|
|
161
|
+
self.define_handled_route(route)
|
|
150
162
|
end
|
|
151
163
|
|
|
152
164
|
private
|
|
153
165
|
|
|
154
|
-
def define_handled_route(original_brut_route
|
|
166
|
+
def define_handled_route(original_brut_route)
|
|
155
167
|
|
|
156
168
|
method = original_brut_route.http_method.to_s.upcase
|
|
157
169
|
path = original_brut_route.path_template
|
|
@@ -7,22 +7,62 @@ module Brut::SpecSupport::CLICommandSupport
|
|
|
7
7
|
# A subclass of {Brut::CLI::Executor} that remembers the commands it was asked to execute, instead of
|
|
8
8
|
# actually executing them. This allows inspection later to see if expected commands had been run.
|
|
9
9
|
#
|
|
10
|
-
# This is used by the `have_executed` matcher.
|
|
10
|
+
# This is used by the `have_executed` matcher. It also is useful when yielding a block to
|
|
11
|
+
# `test_execution_context` so that you can configure command line executions to raise errors or
|
|
12
|
+
# provide specific output.
|
|
11
13
|
class CapturingExecutor < Brut::CLI::Executor
|
|
12
14
|
attr_reader :commands_executed
|
|
15
|
+
def initialize(...)
|
|
16
|
+
super
|
|
17
|
+
@on_commands = {}
|
|
18
|
+
end
|
|
13
19
|
def system!(*args)
|
|
14
20
|
@commands_executed ||= []
|
|
15
|
-
if args.length == 1
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
command = if args.length == 1
|
|
22
|
+
args[0]
|
|
23
|
+
else
|
|
24
|
+
args
|
|
25
|
+
end
|
|
26
|
+
@commands_executed << command
|
|
27
|
+
if @on_commands[command]
|
|
28
|
+
if @on_commands[command][:raise_error]
|
|
29
|
+
raise Brut::CLI::SystemExecError.new(command,1)
|
|
30
|
+
elsif @on_commands[command][:output]
|
|
31
|
+
output = @on_commands[command][:output]
|
|
32
|
+
yield(output)
|
|
33
|
+
end
|
|
19
34
|
end
|
|
20
35
|
nil
|
|
21
36
|
end
|
|
37
|
+
|
|
38
|
+
# Configure the behavior of a specific command. This must be called before any
|
|
39
|
+
# command line commands are invoked.
|
|
40
|
+
#
|
|
41
|
+
# @param [String] command the *exact* command line invocation you want to configure.
|
|
42
|
+
# @param [false|true] raise_error if true, an execption is raised when this command is executed.
|
|
43
|
+
# @param [String] output if not-nil, this output is produced by the command.
|
|
44
|
+
def on_command(command, raise_error: false, output: nil)
|
|
45
|
+
if raise_error
|
|
46
|
+
@on_commands[command] = { raise_error: }
|
|
47
|
+
elsif output
|
|
48
|
+
@on_commands[command] = { output: }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
22
51
|
end
|
|
23
52
|
# Create an ExecutionContext suitable for any command, but which allows
|
|
24
53
|
# manipulating the data as needed for your test, or to access the various IO streams
|
|
25
54
|
# used by the command.
|
|
55
|
+
# @param [Array<String>] argv the arguments remaining on the command line after parsing
|
|
56
|
+
# @param [Hash<Symbol|String,String>] options hash of options. The preceding dashes **must be omitted**. If strings are passed as keys, they are converted to symbols.
|
|
57
|
+
# @param [Hash<String,String>] env The UNIX environment to use.
|
|
58
|
+
# @param [StringIO] stdin IO to use as the standard input.
|
|
59
|
+
# @param [StringIO] stdout IO to use as the standard output. Note that you usually do not want to specify this, as the default is to create a `StringIO` which you can access directly from the returned execution context.
|
|
60
|
+
# @param [StringIO] stderr IO to use as the standard error. Note that you usually do not want to specify this, as the default is to create a `StringIO` which you can access directly from the returned execution context.
|
|
61
|
+
# @param [Brut::CLI::Executor] executor executor to use to execute sub commands. Note that you likely
|
|
62
|
+
# don't want to pass in a value for this. The default is an implementation that captures all the
|
|
63
|
+
# commands that were executed for your later inspection via the `have_executed` matcher.
|
|
64
|
+
# @yield [executor] If a block is passed, the executor is yielded to allow for configuration of its behavior.
|
|
65
|
+
# @yieldparam executor [Brut::CLI::Executor|Brut::SpecSupport::CLICommandSupport::CapturingExecutor] the executor. If you used the default and did not provide your own, you'll get a `CapturingExecutor` that you can use to configure responses to command line invocations.
|
|
26
66
|
def test_execution_context(
|
|
27
67
|
argv: [],
|
|
28
68
|
options: {},
|
|
@@ -31,13 +71,26 @@ module Brut::SpecSupport::CLICommandSupport
|
|
|
31
71
|
stdout: StringIO.new,
|
|
32
72
|
stderr: StringIO.new,
|
|
33
73
|
executor: :default,
|
|
34
|
-
logger: :default
|
|
74
|
+
logger: :default,
|
|
75
|
+
&block
|
|
35
76
|
)
|
|
36
77
|
logger = if logger == :default
|
|
37
78
|
Brut::CLI::Logger.new(app_name: $0, stdout:, stderr:)
|
|
38
79
|
else
|
|
39
80
|
logger
|
|
40
81
|
end
|
|
82
|
+
executor = if executor == :default
|
|
83
|
+
CapturingExecutor.new(out: stdout, err: stderr, logger:)
|
|
84
|
+
else
|
|
85
|
+
executor
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if block
|
|
89
|
+
block.(executor)
|
|
90
|
+
end
|
|
91
|
+
options = options.map { |key,value|
|
|
92
|
+
[ key.to_sym, value ]
|
|
93
|
+
}.to_h
|
|
41
94
|
Brut::CLI::Commands::ExecutionContext.new(
|
|
42
95
|
argv:,
|
|
43
96
|
options: Brut::CLI::Options.new({ "log-level": "error" }.merge(options)),
|
|
@@ -45,7 +98,7 @@ module Brut::SpecSupport::CLICommandSupport
|
|
|
45
98
|
stdin:,
|
|
46
99
|
stdout:,
|
|
47
100
|
stderr:,
|
|
48
|
-
executor:
|
|
101
|
+
executor:
|
|
49
102
|
)
|
|
50
103
|
end
|
|
51
104
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
RSpec::Matchers.define :have_executed do |commands|
|
|
2
2
|
match do |execution_context|
|
|
3
|
+
commands = Array(commands)
|
|
3
4
|
commands.all? { |command|
|
|
4
5
|
if command.kind_of?(Regexp)
|
|
5
6
|
execution_context.executor.commands_executed.any? { it.kind_of?(String) && it.match(command) }
|
|
@@ -10,6 +11,7 @@ RSpec::Matchers.define :have_executed do |commands|
|
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
failure_message do |execution_context|
|
|
14
|
+
commands = Array(commands)
|
|
13
15
|
not_executed = commands.reject { |command|
|
|
14
16
|
if command.kind_of?(Regexp)
|
|
15
17
|
execution_context.executor.commands_executed.any? { it.kind_of?(String) && it.match(command) }
|
data/lib/brut/version.rb
CHANGED
data/templates/Base/dx/exec
CHANGED
|
@@ -9,18 +9,22 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
|
|
|
9
9
|
require_command "docker"
|
|
10
10
|
load_docker_compose_env
|
|
11
11
|
|
|
12
|
-
usage_description="Execute a command inside the app's container. Any command other than 'bash' will be run with bash -lc. Use -B to just run the command directly."
|
|
13
|
-
usage_args="[-s service] [-B] command"
|
|
12
|
+
usage_description="Execute a command inside the app's container. Any command other than 'bash' will be run with bash -lc. Use -B to just run the command directly. Use -A to avoid ssh-agent."
|
|
13
|
+
usage_args="[-s service] [-B] [-A] command"
|
|
14
14
|
usage_pre="exec.pre"
|
|
15
15
|
usage_on_help "${usage_description}" "${usage_args}" "${usage_pre}" "" "${@}"
|
|
16
16
|
|
|
17
17
|
SERVICE="${SERVICE_NAME:-${DEFAULT_SERVICE}}"
|
|
18
18
|
INCLUDE_PREFIX_FOR_NON_BASH=true
|
|
19
|
-
|
|
19
|
+
SSH_AGENT=true
|
|
20
|
+
while getopts "s:AB" opt "${@}"; do
|
|
20
21
|
case ${opt} in
|
|
21
22
|
s )
|
|
22
23
|
SERVICE="${OPTARG}"
|
|
23
24
|
;;
|
|
25
|
+
A )
|
|
26
|
+
SSH_AGENT=false
|
|
27
|
+
;;
|
|
24
28
|
B )
|
|
25
29
|
INCLUDE_PREFIX_FOR_NON_BASH=false
|
|
26
30
|
;;
|
|
@@ -53,6 +57,9 @@ elif [ "$INCLUDE_PREFIX_FOR_NON_BASH" = "true" ]; then
|
|
|
53
57
|
else
|
|
54
58
|
COMMAND=("$@")
|
|
55
59
|
fi
|
|
60
|
+
if [ "$SSH_AGENT" = "true" ]; then
|
|
61
|
+
COMMAND=(ssh-agent "${COMMAND[@]}")
|
|
62
|
+
fi
|
|
56
63
|
|
|
57
64
|
log "🚂" "Running '${COMMAND[@]}' inside container with service name '${SERVICE}'"
|
|
58
65
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# This is a multi-stage build, meaning one image will be built that
|
|
2
|
+
# has necessarily dev tools in it, and that image is used to create artifacts
|
|
3
|
+
# that will be copied into the second build, which will create the image
|
|
4
|
+
# you deploy to production.
|
|
5
|
+
#
|
|
6
|
+
# # Maintaing this file
|
|
7
|
+
#
|
|
8
|
+
# You own this file now. While there could be a way to create it from
|
|
9
|
+
# the Dockerfile.dx, for now, you'll need to stay on top of it:
|
|
10
|
+
#
|
|
11
|
+
# 1 - ensure the versions of Ruby match
|
|
12
|
+
# 2 - ensure the versions of NodeJS match
|
|
13
|
+
# 3 - ensure that anything you installed in Dockerfile.dx to make
|
|
14
|
+
# the app work or to pre-generate assets is set up here as well.
|
|
15
|
+
# You are advised to make that setup identical.
|
|
16
|
+
#
|
|
17
|
+
# You can test this locally but using the `--platform` flag to `brut deploy deploy`
|
|
18
|
+
# and running a container locally. You'll need all infrastructure available
|
|
19
|
+
# but it can be done to test things before you deploy. If you need to.
|
|
20
|
+
|
|
21
|
+
# Use Ruby 3.4 as a base.
|
|
22
|
+
FROM ruby:3.4 AS base
|
|
23
|
+
|
|
24
|
+
# bin/deploy will inject this value so that your app's GIT SHA1
|
|
25
|
+
# is in the environment in production, thus allowing you to be more
|
|
26
|
+
# sure of what's actually running.
|
|
27
|
+
ARG app_git_sha1
|
|
28
|
+
|
|
29
|
+
WORKDIR /brut-app
|
|
30
|
+
|
|
31
|
+
# Install base packages
|
|
32
|
+
#
|
|
33
|
+
# - ca-certificates is needed for other installs
|
|
34
|
+
# - curl is needed generally for other installs and by Heroku
|
|
35
|
+
# - gnupg is a PGP replacement used in making sure new APT repos work
|
|
36
|
+
# - libjemalloc2 in theory speeds up Ruby
|
|
37
|
+
# - lsb-release is used to generically access information for this OS's version
|
|
38
|
+
# - wget allows us to copy/paste commands from vendors about how to install
|
|
39
|
+
# software even though it does the same thing as curl
|
|
40
|
+
RUN --mount=type=cache,target=/var/cache/apt \
|
|
41
|
+
apt-get update --quiet --yes && \
|
|
42
|
+
apt-get install --no-install-recommends --quiet --yes \
|
|
43
|
+
ca-certificates \
|
|
44
|
+
curl \
|
|
45
|
+
gnupg \
|
|
46
|
+
libjemalloc2 \
|
|
47
|
+
lsb-release \
|
|
48
|
+
postgresql-common \
|
|
49
|
+
rsync \
|
|
50
|
+
wget
|
|
51
|
+
|
|
52
|
+
# Install the PostgreSQL client. The latest version is not available
|
|
53
|
+
# from Debian, so we set up our own. This should match what's in Dockerfile.dx
|
|
54
|
+
# and ideally the version of Postgres used in production.
|
|
55
|
+
#
|
|
56
|
+
# Incancation is based on: https://www.postgresql.org/download/linux/debian/
|
|
57
|
+
RUN /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && \
|
|
58
|
+
apt-get update --quiet --yes && \
|
|
59
|
+
apt-get --yes --quiet --no-install-recommends install postgresql-client-16 && \
|
|
60
|
+
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
|
61
|
+
|
|
62
|
+
# Set basic env vars for production
|
|
63
|
+
ENV RACK_ENV="production" \
|
|
64
|
+
BUNDLE_DEPLOYMENT="1" \
|
|
65
|
+
BUNDLE_PATH="/usr/local/bundle" \
|
|
66
|
+
BUNDLE_WITHOUT="development"
|
|
67
|
+
|
|
68
|
+
RUN rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
|
69
|
+
|
|
70
|
+
# This makes a new image that we'll throw away after building
|
|
71
|
+
# needed artifacts.
|
|
72
|
+
FROM base AS build
|
|
73
|
+
|
|
74
|
+
# - build-essential is needed for almost any build tool we have to install
|
|
75
|
+
# - git is needed to install some things
|
|
76
|
+
# - libpq-dev is needed by postgres
|
|
77
|
+
# - pkg-config is, I guess, not considered "essential" (as in build-essential),
|
|
78
|
+
# but still needed to install downstream stuff
|
|
79
|
+
RUN --mount=type=cache,target=/var/cache/apt \
|
|
80
|
+
apt-get update --quiet --yes && \
|
|
81
|
+
apt-get install --no-install-recommends --quiet --yes \
|
|
82
|
+
build-essential \
|
|
83
|
+
git \
|
|
84
|
+
libpq-dev \
|
|
85
|
+
pkg-config
|
|
86
|
+
|
|
87
|
+
# Copy NodeJS from the official image. This should be faster than
|
|
88
|
+
# installing it via e.g. nvm
|
|
89
|
+
COPY --from=node:22-slim /usr/local /usr/local
|
|
90
|
+
|
|
91
|
+
# Copy Gemfile only as it may not change between deploys
|
|
92
|
+
COPY Gemfile Gemfile.lock .
|
|
93
|
+
|
|
94
|
+
# Install RubyGems from app's Gemfile
|
|
95
|
+
RUN --mount=type=cache,target=${BUNDLE_PATH}/cache \
|
|
96
|
+
bundle install --verbose && \
|
|
97
|
+
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git
|
|
98
|
+
|
|
99
|
+
COPY package.json package-lock.json ./
|
|
100
|
+
# Install Node Modules from package.json
|
|
101
|
+
RUN npm clean-install --no-audit --no-fund --verbose
|
|
102
|
+
|
|
103
|
+
COPY . .
|
|
104
|
+
|
|
105
|
+
# Build all assets
|
|
106
|
+
RUN BRUT_DEBUG=true bundle exec brut --log-level=debug build-assets all && \
|
|
107
|
+
rm -rf node_modules
|
|
108
|
+
|
|
109
|
+
# We are now switching back to building the image that will be deployed.
|
|
110
|
+
|
|
111
|
+
FROM base
|
|
112
|
+
# Copy built artifacts from the throwaway image: gems, application
|
|
113
|
+
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
|
114
|
+
COPY --from=build /brut-app /brut-app
|
|
115
|
+
|
|
116
|
+
# For security, set directories that will be written to be owned by non-root
|
|
117
|
+
RUN groupadd --system --gid 1000 brut && \
|
|
118
|
+
useradd brut --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
|
119
|
+
chown -R brut:brut logs tmp
|
|
120
|
+
USER 1000:1000
|
|
121
|
+
|
|
122
|
+
ENV APP_GIT_SHA1="${app_git_sha1}"
|
|
123
|
+
|
|
124
|
+
# This is used to execute other commands. When the app is run in production,
|
|
125
|
+
# this script is used to run it.
|
|
126
|
+
ENTRYPOINT ["/brut-app/deploy/docker-entrypoint"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# This contains settings for your production Docker setup.
|
|
2
|
+
# You own and maintain this file. It is `require`d by brut deploy docker
|
|
3
|
+
class AppDeployConfig < Brut::CLI::Apps::Deploy::DeployConfig
|
|
4
|
+
|
|
5
|
+
# Return an array of ProcessDescription instances, that you can
|
|
6
|
+
# create with the `process_description` method.
|
|
7
|
+
#
|
|
8
|
+
# For example, if you have the Sidekiq segment installed, `bin/run sidekiq`
|
|
9
|
+
# runs Sidekiq, so you would implement the method as follows:
|
|
10
|
+
#
|
|
11
|
+
# def additional_processes = [
|
|
12
|
+
# process_description("sidekiq", [ "bundle", "exec", "bin/run sidekiq" ]),
|
|
13
|
+
# ]
|
|
14
|
+
#
|
|
15
|
+
def additional_processes
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash -e
|
|
2
|
+
|
|
3
|
+
# This is the Docker entrypoint, mean this script is used to execute
|
|
4
|
+
# the command given to the RUN directive.
|
|
5
|
+
#
|
|
6
|
+
# The reason this exists is to load jemalloc. If you don't want to use
|
|
7
|
+
# jemalloc, you can delete this script, as well as the ENTRYPOINT directive
|
|
8
|
+
# in deploy/Dockerfile.
|
|
9
|
+
|
|
10
|
+
# Enable jemalloc for reduced memory usage and latency.
|
|
11
|
+
if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then
|
|
12
|
+
export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
exec "${@}"
|