Added support for multistage build cache
This commit is contained in:
31
README.md
31
README.md
@@ -463,6 +463,37 @@ builder:
|
||||
context: ".."
|
||||
```
|
||||
|
||||
### Using multistage builder cache
|
||||
|
||||
Docker multistage build cache can singlehandedly speed up your builds by a lot. Currently MRSK only supports using the GHA cache or the Registry cache:
|
||||
|
||||
```yaml
|
||||
# Using GHA cache
|
||||
builder:
|
||||
cache:
|
||||
type: gha
|
||||
|
||||
# Using Registry cache
|
||||
builder:
|
||||
cache:
|
||||
type: registry
|
||||
|
||||
# Using Registry cache with different cache image
|
||||
builder:
|
||||
cache:
|
||||
type: registry
|
||||
# default image name is <image>-build-cache
|
||||
image: application-cache-image
|
||||
|
||||
# Using Registry cache with additinonal cache-to options
|
||||
builder:
|
||||
cache:
|
||||
type: registry
|
||||
options: mode=max,image-manifest=true,oci-mediatypes=true
|
||||
```
|
||||
|
||||
For further insights into build cache optimization, check out documentation on Docker's official website: https://docs.docker.com/build/cache/.
|
||||
|
||||
### Using build secrets for new images
|
||||
|
||||
Some images need a secret passed in during build time, like a GITHUB_TOKEN, to give access to private gem repositories. This can be done by having the secret in ENV, then referencing it in the builder configuration:
|
||||
|
||||
@@ -7,11 +7,11 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
|
||||
|
||||
def target
|
||||
case
|
||||
when config.builder && config.builder["multiarch"] == false
|
||||
when !config.builder.multiarch?
|
||||
native
|
||||
when config.builder && config.builder["local"] && config.builder["remote"]
|
||||
when config.builder.local? && config.builder.remote?
|
||||
multiarch_remote
|
||||
when config.builder && config.builder["remote"]
|
||||
when config.builder.remote?
|
||||
native_remote
|
||||
else
|
||||
multiarch
|
||||
|
||||
@@ -13,11 +13,11 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
||||
end
|
||||
|
||||
def build_options
|
||||
[ *build_tags, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
|
||||
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
|
||||
end
|
||||
|
||||
def build_context
|
||||
context
|
||||
config.builder.context
|
||||
end
|
||||
|
||||
|
||||
@@ -26,39 +26,30 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
||||
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
||||
end
|
||||
|
||||
def build_cache
|
||||
if config.builder.cache?
|
||||
["--cache-to", config.builder.cache_to,
|
||||
"--cache-from", config.builder.cache_from]
|
||||
end
|
||||
end
|
||||
|
||||
def build_labels
|
||||
argumentize "--label", { service: config.service }
|
||||
end
|
||||
|
||||
def build_args
|
||||
argumentize "--build-arg", args, sensitive: true
|
||||
argumentize "--build-arg", config.builder.args, sensitive: true
|
||||
end
|
||||
|
||||
def build_secrets
|
||||
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
|
||||
argumentize "--secret", config.builder.secrets.collect { |secret| [ "id", secret ] }
|
||||
end
|
||||
|
||||
def build_dockerfile
|
||||
if Pathname.new(File.expand_path(dockerfile)).exist?
|
||||
argumentize "--file", dockerfile
|
||||
if Pathname.new(File.expand_path(config.builder.dockerfile)).exist?
|
||||
argumentize "--file", config.builder.dockerfile
|
||||
else
|
||||
raise BuilderError, "Missing #{dockerfile}"
|
||||
raise BuilderError, "Missing #{config.builder.dockerfile}"
|
||||
end
|
||||
end
|
||||
|
||||
def args
|
||||
(config.builder && config.builder["args"]) || {}
|
||||
end
|
||||
|
||||
def secrets
|
||||
(config.builder && config.builder["secrets"]) || []
|
||||
end
|
||||
|
||||
def dockerfile
|
||||
(config.builder && config.builder["dockerfile"]) || "Dockerfile"
|
||||
end
|
||||
|
||||
def context
|
||||
(config.builder && config.builder["context"]) || "."
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,17 +22,17 @@ class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Mult
|
||||
end
|
||||
|
||||
def create_local_buildx
|
||||
docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local["arch"]), "--platform", "linux/#{local["arch"]}"
|
||||
docker :buildx, :create, "--name", builder_name, builder_name_with_arch(config.builder.local_arch), "--platform", "linux/#{config.builder.local_arch}"
|
||||
end
|
||||
|
||||
def append_remote_buildx
|
||||
docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(remote["arch"]), "--platform", "linux/#{remote["arch"]}"
|
||||
docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(config.builder.remote_arch), "--platform", "linux/#{config.builder.remote_arch}"
|
||||
end
|
||||
|
||||
def create_contexts
|
||||
combine \
|
||||
create_context(local["arch"], local["host"]),
|
||||
create_context(remote["arch"], remote["host"])
|
||||
create_context(config.builder.local_arch, config.builder.local_host),
|
||||
create_context(config.builder.remote_arch, config.builder.remote_host)
|
||||
end
|
||||
|
||||
def create_context(arch, host)
|
||||
@@ -41,19 +41,11 @@ class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Mult
|
||||
|
||||
def remove_contexts
|
||||
combine \
|
||||
remove_context(local["arch"]),
|
||||
remove_context(remote["arch"])
|
||||
remove_context(config.builder.local_arch),
|
||||
remove_context(config.builder.remote_arch)
|
||||
end
|
||||
|
||||
def remove_context(arch)
|
||||
docker :context, :rm, builder_name_with_arch(arch)
|
||||
end
|
||||
|
||||
def local
|
||||
config.builder["local"]
|
||||
end
|
||||
|
||||
def remote
|
||||
config.builder["remote"]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
|
||||
def create
|
||||
# No-op on native
|
||||
# No-op on native without cache
|
||||
|
||||
if config.builder.cache?
|
||||
docker :buildx, :create, "--use", "--driver=docker-container"
|
||||
end
|
||||
end
|
||||
|
||||
def remove
|
||||
# No-op on native
|
||||
# No-op on native without cache
|
||||
|
||||
if config.builder.cache?
|
||||
docker :buildx, :rm, builder_name
|
||||
end
|
||||
end
|
||||
|
||||
def push
|
||||
if config.builder.cache?
|
||||
docker :buildx, :build,
|
||||
"--push",
|
||||
*build_options,
|
||||
build_context
|
||||
else
|
||||
combine \
|
||||
docker(:build, *build_options, build_context),
|
||||
docker(:push, config.absolute_image),
|
||||
docker(:push, config.latest_image)
|
||||
end
|
||||
end
|
||||
|
||||
def info
|
||||
# No-op on native
|
||||
|
||||
@@ -28,29 +28,21 @@ class Mrsk::Commands::Builder::Native::Remote < Mrsk::Commands::Builder::Native
|
||||
|
||||
|
||||
private
|
||||
def arch
|
||||
config.builder["remote"]["arch"]
|
||||
end
|
||||
|
||||
def host
|
||||
config.builder["remote"]["host"]
|
||||
end
|
||||
|
||||
def builder_name
|
||||
"mrsk-#{config.service}-native-remote"
|
||||
end
|
||||
|
||||
def builder_name_with_arch
|
||||
"#{builder_name}-#{arch}"
|
||||
"#{builder_name}-#{config.builder.remote_arch}"
|
||||
end
|
||||
|
||||
def platform
|
||||
"linux/#{arch}"
|
||||
"linux/#{config.builder.remote_arch}"
|
||||
end
|
||||
|
||||
def create_context
|
||||
docker :context, :create,
|
||||
builder_name_with_arch, "--description", "'#{builder_name} #{arch} native host'", "--docker", "'host=#{host}'"
|
||||
builder_name_with_arch, "--description", "'#{builder_name} #{config.builder.remote_arch} native host'", "--docker", "'host=#{config.builder.remote_host}'"
|
||||
end
|
||||
|
||||
def remove_context
|
||||
|
||||
@@ -6,7 +6,7 @@ require "erb"
|
||||
require "net/ssh/proxy/jump"
|
||||
|
||||
class Mrsk::Configuration
|
||||
delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
||||
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
|
||||
|
||||
attr_accessor :destination
|
||||
@@ -182,7 +182,7 @@ class Mrsk::Configuration
|
||||
env_args: env_args,
|
||||
volume_args: volume_args,
|
||||
ssh_options: ssh_options,
|
||||
builder: raw_config.builder,
|
||||
builder: builder.to_h,
|
||||
accessories: raw_config.accessories,
|
||||
logging: logging_args,
|
||||
healthcheck: healthcheck
|
||||
@@ -197,6 +197,10 @@ class Mrsk::Configuration
|
||||
raw_config.hooks_path || ".mrsk/hooks"
|
||||
end
|
||||
|
||||
def builder
|
||||
Mrsk::Configuration::Builder.new(config: self)
|
||||
end
|
||||
|
||||
private
|
||||
# Will raise ArgumentError if any required config keys are missing
|
||||
def ensure_required_keys_present
|
||||
|
||||
113
lib/mrsk/configuration/builder.rb
Normal file
113
lib/mrsk/configuration/builder.rb
Normal file
@@ -0,0 +1,113 @@
|
||||
class Mrsk::Configuration::Builder
|
||||
def initialize(config:)
|
||||
@options = config.raw_config.builder || {}
|
||||
@image = config.image
|
||||
@server = config.registry["server"]
|
||||
|
||||
valid?
|
||||
end
|
||||
|
||||
def to_h
|
||||
@options
|
||||
end
|
||||
|
||||
def multiarch?
|
||||
@options["multiarch"] != false
|
||||
end
|
||||
|
||||
def local?
|
||||
!!@options["local"]
|
||||
end
|
||||
|
||||
def remote?
|
||||
!!@options["remote"]
|
||||
end
|
||||
|
||||
def cache?
|
||||
!!@options["cache"]
|
||||
end
|
||||
|
||||
def args
|
||||
@options["args"] || {}
|
||||
end
|
||||
|
||||
def secrets
|
||||
@options["secrets"] || []
|
||||
end
|
||||
|
||||
def dockerfile
|
||||
@options["dockerfile"] || "Dockerfile"
|
||||
end
|
||||
|
||||
def context
|
||||
@options["context"] || "."
|
||||
end
|
||||
|
||||
def local_arch
|
||||
@options["local"]["arch"] if local?
|
||||
end
|
||||
|
||||
def local_host
|
||||
@options["local"]["host"] if local?
|
||||
end
|
||||
|
||||
def remote_arch
|
||||
@options["remote"]["arch"] if remote?
|
||||
end
|
||||
|
||||
def remote_host
|
||||
@options["remote"]["host"] if remote?
|
||||
end
|
||||
|
||||
def cache_from
|
||||
if cache?
|
||||
case @options["cache"]["type"]
|
||||
when 'gha'
|
||||
"type=gha"
|
||||
when 'registry'
|
||||
[
|
||||
"type=registry",
|
||||
"ref=#{@server}/#{cache_image}"
|
||||
].compact.join(",")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cache_to
|
||||
if cache?
|
||||
case @options["cache"]["type"]
|
||||
when "gha"
|
||||
[
|
||||
"type=gha",
|
||||
@options["cache"]&.fetch("options", nil),
|
||||
].compact.join(",")
|
||||
when "registry"
|
||||
[
|
||||
"type=registry",
|
||||
@options["cache"]&.fetch("options", nil),
|
||||
"ref=#{@server}/#{cache_image}"
|
||||
].compact.join(",")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid?
|
||||
if @options["local"] && !@options["remote"]
|
||||
raise ArgumentError, "You must specify both local and remote builder config for remote multiarch builds"
|
||||
end
|
||||
|
||||
if @options["cache"] && @options["cache"]["type"]
|
||||
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless ["gha", "registry"].include?(@options["cache"]["type"])
|
||||
end
|
||||
end
|
||||
|
||||
def cache_image
|
||||
@options["cache"]&.fetch("image", nil) || "#{@image}-build-cache"
|
||||
end
|
||||
|
||||
def current_branch
|
||||
`git rev-parse --abbrev-ref HEAD`.strip
|
||||
end
|
||||
end
|
||||
151
test/configuration/builder_test.rb
Normal file
151
test/configuration/builder_test.rb
Normal file
@@ -0,0 +1,151 @@
|
||||
require "test_helper"
|
||||
|
||||
class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@deploy = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
servers: [ "1.1.1.1" ]
|
||||
}
|
||||
|
||||
@config = Mrsk::Configuration.new(@deploy)
|
||||
|
||||
@deploy_with_builder_option = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||
servers: [ "1.1.1.1" ],
|
||||
builder: {}
|
||||
}
|
||||
|
||||
@config_with_builder_option = Mrsk::Configuration.new(@deploy_with_builder_option)
|
||||
end
|
||||
|
||||
test "multiarch?" do
|
||||
assert_equal true, @config.builder.multiarch?
|
||||
end
|
||||
|
||||
test "setting multiarch to false" do
|
||||
@deploy_with_builder_option[:builder] = { "multiarch" => false }
|
||||
|
||||
assert_equal false, @config_with_builder_option.builder.multiarch?
|
||||
end
|
||||
|
||||
test "local?" do
|
||||
assert_equal false, @config.builder.local?
|
||||
end
|
||||
|
||||
test "remote?" do
|
||||
assert_equal false, @config.builder.remote?
|
||||
end
|
||||
|
||||
test "remote_arch" do
|
||||
assert_nil @config.builder.remote_arch
|
||||
end
|
||||
|
||||
test "remote_host" do
|
||||
assert_nil @config.builder.remote_host
|
||||
end
|
||||
|
||||
test "remote config is missing when local is specified" do
|
||||
@deploy_with_builder_option[:builder] = { "local" => { "arch" => "arm64", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" } }
|
||||
|
||||
assert_raises(ArgumentError) do
|
||||
@config_with_builder_option.builder
|
||||
end
|
||||
end
|
||||
|
||||
test "setting both local and remote configs" do
|
||||
@deploy_with_builder_option[:builder] = {
|
||||
"local" => { "arch" => "arm64", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" },
|
||||
"remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" }
|
||||
}
|
||||
|
||||
assert_equal true, @config_with_builder_option.builder.local?
|
||||
assert_equal true, @config_with_builder_option.builder.remote?
|
||||
|
||||
assert_equal "amd64", @config_with_builder_option.builder.remote_arch
|
||||
assert_equal "ssh://root@192.168.0.1", @config_with_builder_option.builder.remote_host
|
||||
|
||||
assert_equal "arm64", @config_with_builder_option.builder.local_arch
|
||||
assert_equal "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock", @config_with_builder_option.builder.local_host
|
||||
end
|
||||
|
||||
test "cache?" do
|
||||
assert_equal false, @config.builder.cache?
|
||||
end
|
||||
|
||||
test "invalid cache type specified" do
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "invalid" } }
|
||||
|
||||
assert_raises(ArgumentError) do
|
||||
@config_with_builder_option.builder
|
||||
end
|
||||
end
|
||||
|
||||
test "cache_from" do
|
||||
assert_nil @config.builder.cache_from
|
||||
end
|
||||
|
||||
test "cache_to" do
|
||||
assert_nil @config.builder.cache_to
|
||||
end
|
||||
|
||||
test "setting gha cache" do
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "gha", "options" => "mode=max" } }
|
||||
|
||||
assert_equal "type=gha", @config_with_builder_option.builder.cache_from
|
||||
assert_equal "type=gha,mode=max", @config_with_builder_option.builder.cache_to
|
||||
end
|
||||
|
||||
test "setting registry cache" do
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||
|
||||
assert_equal "type=registry,ref=/dhh/app-build-cache", @config_with_builder_option.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=/dhh/app-build-cache", @config_with_builder_option.builder.cache_to
|
||||
end
|
||||
|
||||
test "setting registry cache with image" do
|
||||
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "image" => "mrsk", "options" => "mode=max" } }
|
||||
|
||||
assert_equal "type=registry,ref=/mrsk", @config_with_builder_option.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,ref=/mrsk", @config_with_builder_option.builder.cache_to
|
||||
end
|
||||
|
||||
test "args" do
|
||||
assert_equal({}, @config.builder.args)
|
||||
end
|
||||
|
||||
test "setting args" do
|
||||
@deploy_with_builder_option[:builder] = { "args" => { "key" => "value" } }
|
||||
|
||||
assert_equal({ "key" => "value" }, @config_with_builder_option.builder.args)
|
||||
end
|
||||
|
||||
test "secrets" do
|
||||
assert_equal [], @config.builder.secrets
|
||||
end
|
||||
|
||||
test "setting secrets" do
|
||||
@deploy_with_builder_option[:builder] = { "secrets" => ["GITHUB_TOKEN"] }
|
||||
|
||||
assert_equal ["GITHUB_TOKEN"], @config_with_builder_option.builder.secrets
|
||||
end
|
||||
|
||||
test "dockerfile" do
|
||||
assert_equal "Dockerfile", @config.builder.dockerfile
|
||||
end
|
||||
|
||||
test "setting dockerfile" do
|
||||
@deploy_with_builder_option[:builder] = { "dockerfile" => "Dockerfile.dev" }
|
||||
|
||||
assert_equal "Dockerfile.dev", @config_with_builder_option.builder.dockerfile
|
||||
end
|
||||
|
||||
test "context" do
|
||||
assert_equal ".", @config.builder.context
|
||||
end
|
||||
|
||||
test "setting context" do
|
||||
@deploy_with_builder_option[:builder] = { "context" => ".." }
|
||||
|
||||
assert_equal "..", @config_with_builder_option.builder.context
|
||||
end
|
||||
end
|
||||
@@ -266,6 +266,6 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "to_h" do
|
||||
assert_equal({ :roles=>["web"], :hosts=>["1.1.1.1", "1.1.1.2"], :primary_host=>"1.1.1.1", :version=>"missing", :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", :env_args=>["-e", "REDIS_URL=\"redis://x/y\""], :ssh_options=>{:user=>"root", :auth_methods=>["publickey"]}, :volume_args=>["--volume", "/local/path:/container/path"], :logging=>["--log-opt", "max-size=\"10m\""], :healthcheck=>{"path"=>"/up", "port"=>3000, "max_attempts" => 7 }}, @config.to_h)
|
||||
assert_equal({ :roles=>["web"], :hosts=>["1.1.1.1", "1.1.1.2"], :primary_host=>"1.1.1.1", :version=>"missing", :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", :env_args=>["-e", "REDIS_URL=\"redis://x/y\""], :ssh_options=>{:user=>"root", :auth_methods=>["publickey"]}, :volume_args=>["--volume", "/local/path:/container/path"], :builder=>{}, :logging=>["--log-opt", "max-size=\"10m\""], :healthcheck=>{"path"=>"/up", "port"=>3000, "max_attempts" => 7 }}, @config.to_h)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user