Merge branch 'main' into allow-bastion-server

This commit is contained in:
David Heinemeier Hansson
2023-02-04 15:33:25 +01:00
committed by GitHub
10 changed files with 125 additions and 70 deletions

View File

@@ -15,12 +15,15 @@ servers:
registry: registry:
username: registry-user-name username: registry-user-name
password: <%= ENV.fetch("MRSK_REGISTRY_PASSWORD") %> password: <%= ENV.fetch("MRSK_REGISTRY_PASSWORD") %>
env:
secret:
- RAILS_MASTER_KEY
``` ```
Now you're ready to deploy a multi-arch image to the servers: Now you're ready to deploy a multi-arch image to the servers:
``` ```
MRSK_REGISTRY_PASSWORD=pw mrsk deploy RAILS_MASTER_KEY=123 MRSK_REGISTRY_PASSWORD=pw mrsk deploy
``` ```
This will: This will:
@@ -282,14 +285,6 @@ ARG RUBY_VERSION
FROM ruby:$RUBY_VERSION-slim as base FROM ruby:$RUBY_VERSION-slim as base
``` ```
### Using without RAILS_MASTER_KEY
If you're using MRSK with older Rails apps that predate RAILS_MASTER_KEY, or with a non-Rails app, you can skip the default usage and reference:
```yaml
skip_master_key: true
```
### Using accessories for database, cache, search services ### Using accessories for database, cache, search services
You can manage your accessory services via MRSK as well. The services will build off public images, and will not be automatically updated when you deploy: You can manage your accessory services via MRSK as well. The services will build off public images, and will not be automatically updated when you deploy:

View File

@@ -7,6 +7,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
directories(name) directories(name)
upload(name) upload(name)
on(accessory.host) do on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} boot"), verbosity: :debug execute *MRSK.auditor.record("accessory #{name} boot"), verbosity: :debug
execute *accessory.run execute *accessory.run

View File

@@ -104,11 +104,6 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) }
end end
desc "current", "Return the current running container ID"
def current
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_container_id) }
end
desc "logs", "Show lines from app on servers" desc "logs", "Show lines from app on servers"
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)" option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server" option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"

View File

@@ -39,10 +39,10 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
end end
def follow_logs(grep: nil) def follow_logs(grep: nil)
run_over_ssh pipe( run_over_ssh \
docker(:logs, service_name, "-t", "-n", "10", "-f", "2>&1"), pipe \
(%(grep "#{grep}") if grep) docker(:logs, service_name, "-t", "-n", "10", "-f", "2>&1"),
).join(" ") (%(grep "#{grep}") if grep)
end end
@@ -64,11 +64,11 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
end end
def execute_in_existing_container_over_ssh(*command) def execute_in_existing_container_over_ssh(*command)
run_over_ssh execute_in_existing_container(*command, interactive: true).join(" ") run_over_ssh execute_in_existing_container(*command, interactive: true)
end end
def execute_in_new_container_over_ssh(*command) def execute_in_new_container_over_ssh(*command)
run_over_ssh execute_in_new_container(*command, interactive: true).join(" ") run_over_ssh execute_in_new_container(*command, interactive: true)
end end
def run_over_ssh(command) def run_over_ssh(command)

View File

@@ -6,7 +6,6 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
"-d", "-d",
"--restart unless-stopped", "--restart unless-stopped",
"--name", service_with_version, "--name", service_with_version,
*rails_master_key_arg,
*role.env_args, *role.env_args,
*config.volume_args, *config.volume_args,
*role.label_args, *role.label_args,
@@ -35,11 +34,13 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end end
def follow_logs(host:, grep: nil) def follow_logs(host:, grep: nil)
run_over_ssh pipe( run_over_ssh \
current_container_id, pipe(
"xargs docker logs -t -n 10 -f 2>&1", current_container_id,
(%(grep "#{grep}") if grep) "xargs docker logs -t -n 10 -f 2>&1",
).join(" "), host: host (%(grep "#{grep}") if grep)
),
host: host
end end
@@ -54,7 +55,6 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
docker :run, docker :run,
("-it" if interactive), ("-it" if interactive),
"--rm", "--rm",
*rails_master_key_arg,
*config.env_args, *config.env_args,
*config.volume_args, *config.volume_args,
config.absolute_image, config.absolute_image,
@@ -62,11 +62,11 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end end
def execute_in_existing_container_over_ssh(*command, host:) def execute_in_existing_container_over_ssh(*command, host:)
run_over_ssh execute_in_existing_container(*command, interactive: true).join(" "), host: host run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
end end
def execute_in_new_container_over_ssh(*command, host:) def execute_in_new_container_over_ssh(*command, host:)
run_over_ssh execute_in_new_container(*command, interactive: true).join(" "), host: host run_over_ssh execute_in_new_container(*command, interactive: true), host: host
end end
@@ -128,12 +128,4 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
def service_filter def service_filter
[ "--filter", "label=service=#{config.service}" ] [ "--filter", "label=service=#{config.service}" ]
end end
def rails_master_key_arg
if master_key = config.master_key
[ "-e", redact("RAILS_MASTER_KEY=#{master_key}") ]
else
[]
end
end
end end

View File

@@ -8,14 +8,11 @@ module Mrsk::Commands
@config = config @config = config
end end
def run_over_ssh(command, host:) def run_over_ssh(*command, host:)
ssh_command = "ssh" "ssh".tap do |cmd|
cmd << " -J #{config.ssh_proxy.jump_proxies}" if config.ssh_proxy
if config.ssh_proxy cmd << " -t #{config.ssh_user}@#{host} '#{command.join(" ")}'"
ssh_command << " -J #{config.ssh_proxy.jump_proxies}"
end end
ssh_command << " -t #{config.ssh_user}@#{host} '#{command}'"
end end
private private

View File

@@ -117,12 +117,6 @@ class Mrsk::Configuration
{ user: ssh_user, proxy: ssh_proxy, auth_methods: [ "publickey" ] }.compact { user: ssh_user, proxy: ssh_proxy, auth_methods: [ "publickey" ] }.compact
end end
def master_key
unless raw_config.skip_master_key
ENV["RAILS_MASTER_KEY"] || File.read(Pathname.new(File.expand_path("config/master.key")))
end
end
def valid? def valid?
ensure_required_keys_present && ensure_env_available ensure_required_keys_present && ensure_env_available

View File

@@ -96,7 +96,12 @@ class Mrsk::Configuration::Role
def merged_env_with_secrets def merged_env_with_secrets
merged_env.tap do |new_env| merged_env.tap do |new_env|
new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"]) new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
new_env["clear"] = (Array(config.env["clear"] || config.env) + Array(specialized_env["clear"] || specialized_env)).uniq
# If there's no secret/clear split, everything is clear
clear_app_env = config.env["secret"] ? Array(config.env["clear"]) : Array(config.env["clear"] || config.env)
clear_role_env = specialized_env["secret"] ? Array(specialized_env["clear"]) : Array(specialized_env["clear"] || specialized_env)
new_env["clear"] = (clear_app_env + clear_role_env).uniq
end end
end end
end end

View File

@@ -81,14 +81,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
end end
test "execute in new container over ssh" do test "execute in new container over ssh" do
@mysql.stub(:run_over_ssh, ->(cmd) { cmd }) do @mysql.stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
assert_match %r|docker run -it --rm -e MYSQL_ROOT_PASSWORD=secret123 -e MYSQL_ROOT_HOST=% mysql:8.0 mysql -u root|, assert_match %r|docker run -it --rm -e MYSQL_ROOT_PASSWORD=secret123 -e MYSQL_ROOT_HOST=% mysql:8.0 mysql -u root|,
@mysql.execute_in_new_container_over_ssh("mysql", "-u", "root") @mysql.execute_in_new_container_over_ssh("mysql", "-u", "root")
end end
end end
test "execute in existing container over ssh" do test "execute in existing container over ssh" do
@mysql.stub(:run_over_ssh, ->(cmd) { cmd }) do @mysql.stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
assert_match %r|docker exec -it app-mysql mysql -u root|, assert_match %r|docker exec -it app-mysql mysql -u root|,
@mysql.execute_in_existing_container_over_ssh("mysql", "-u", "root") @mysql.execute_in_existing_container_over_ssh("mysql", "-u", "root")
end end

View File

@@ -4,59 +4,135 @@ class CommandsAppTest < ActiveSupport::TestCase
setup do setup do
ENV["RAILS_MASTER_KEY"] = "456" ENV["RAILS_MASTER_KEY"] = "456"
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] } @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] } }
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config) @app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config).tap { |c| c.version = "999" }
end end
teardown do teardown do
ENV["RAILS_MASTER_KEY"] = nil ENV.delete("RAILS_MASTER_KEY")
end end
test "run" do test "run" do
assert_equal \ assert_equal \
[:docker, :run, "-d", "--restart unless-stopped", "--name", "app-missing", "-e", "RAILS_MASTER_KEY=456", "--label", "service=app", "--label", "role=web", "--label", "traefik.http.routers.app.rule='PathPrefix(`/`)'", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=/up", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=1s", "--label", "traefik.http.middlewares.app.retry.attempts=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms", "dhh/app:missing"], @app.run "docker run -d --restart unless-stopped --name app-999 -e RAILS_MASTER_KEY=456 --label service=app --label role=web --label traefik.http.routers.app.rule='PathPrefix(`/`)' --label traefik.http.services.app.loadbalancer.healthcheck.path=/up --label traefik.http.services.app.loadbalancer.healthcheck.interval=1s --label traefik.http.middlewares.app.retry.attempts=3 --label traefik.http.middlewares.app.retry.initialinterval=500ms dhh/app:999",
@app.run.join(" ")
end end
test "run with volumes" do test "run with volumes" do
@config[:volumes] = ["/local/path:/container/path" ] @config[:volumes] = ["/local/path:/container/path" ]
assert_equal \ assert_equal \
[:docker, :run, "-d", "--restart unless-stopped", "--name", "app-missing", "-e", "RAILS_MASTER_KEY=456", "--volume", "/local/path:/container/path", "--label", "service=app", "--label", "role=web", "--label", "traefik.http.routers.app.rule='PathPrefix(`/`)'", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=/up", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=1s", "--label", "traefik.http.middlewares.app.retry.attempts=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms", "dhh/app:missing"], @app.run "docker run -d --restart unless-stopped --name app-999 -e RAILS_MASTER_KEY=456 --volume /local/path:/container/path --label service=app --label role=web --label traefik.http.routers.app.rule='PathPrefix(`/`)' --label traefik.http.services.app.loadbalancer.healthcheck.path=/up --label traefik.http.services.app.loadbalancer.healthcheck.interval=1s --label traefik.http.middlewares.app.retry.attempts=3 --label traefik.http.middlewares.app.retry.initialinterval=500ms dhh/app:999",
@app.run.join(" ")
end
test "start" do
assert_equal \
"docker start app-999",
@app.start.join(" ")
end
test "stop" do
assert_equal \
"docker ps -q --filter label=service=app | xargs docker stop",
@app.stop.join(" ")
end
test "info" do
assert_equal \
"docker ps --filter label=service=app",
@app.info.join(" ")
end
test "logs" do
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs 2>&1",
@app.logs.join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs --since 5m 2>&1",
@app.logs(since: "5m").join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs -n 100 2>&1",
@app.logs(lines: "100").join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs --since 5m -n 100 2>&1",
@app.logs(since: "5m", lines: "100").join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs 2>&1 | grep 'my-id'",
@app.logs(grep: "my-id").join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
@app.logs(since: "5m", grep: "my-id").join(" ")
end
test "follow logs" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs -t -n 10 -f 2>&1",
@app.follow_logs(host: "app-1")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs -t -n 10 -f 2>&1 | grep \"Completed\"",
@app.follow_logs(host: "app-1", grep: "Completed")
end
end end
test "execute in new container" do test "execute in new container" do
assert_equal \ assert_equal \
[ :docker, :run, "--rm", "-e", "RAILS_MASTER_KEY=456", "dhh/app:missing", "bin/rails", "db:setup" ], "docker run --rm -e RAILS_MASTER_KEY=456 dhh/app:999 bin/rails db:setup",
@app.execute_in_new_container("bin/rails", "db:setup") @app.execute_in_new_container("bin/rails", "db:setup").join(" ")
end end
test "execute in existing container" do test "execute in existing container" do
assert_equal \ assert_equal \
[ :docker, :exec, "app-missing", "bin/rails", "db:setup" ], "docker exec app-999 bin/rails db:setup",
@app.execute_in_existing_container("bin/rails", "db:setup") @app.execute_in_existing_container("bin/rails", "db:setup").join(" ")
end end
test "execute in new container over ssh" do test "execute in new container over ssh" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd }) do @app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do
assert_match %r|docker run -it --rm -e RAILS_MASTER_KEY=456 dhh/app:missing bin/rails c|, assert_match %r|docker run -it --rm -e RAILS_MASTER_KEY=456 dhh/app:999 bin/rails c|,
@app.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1") @app.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
end end
end end
test "execute in existing container over ssh" do test "execute in existing container over ssh" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd }) do @app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do
assert_match %r|docker exec -it app-missing bin/rails c|, assert_match %r|docker exec -it app-999 bin/rails c|,
@app.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1") @app.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1")
end end
end end
test "run without master key" do test "current_container_id" do
ENV["RAILS_MASTER_KEY"] = nil assert_equal \
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config.tap { |c| c[:skip_master_key] = true }) "docker ps -q --filter label=service=app",
@app.current_container_id.join(" ")
end
assert @app.run.exclude?("RAILS_MASTER_KEY=456") test "container_id_for" do
assert_equal \
"docker container ls -a -f name=app-999 -q",
@app.container_id_for(container_name: "app-999").join(" ")
end
test "current_running_version" do
assert_equal \
"docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1",
@app.current_running_version.join(" ")
end
test "most_recent_version_from_available_images" do
assert_equal \
"docker image ls --format \"{{.Tag}}\" dhh/app | head -n 1",
@app.most_recent_version_from_available_images.join(" ")
end end
test "exec_over_ssh" do test "exec_over_ssh" do