diff --git a/lib/kamal/commands/base.rb b/lib/kamal/commands/base.rb index c0eac91cc..e71470e42 100644 --- a/lib/kamal/commands/base.rb +++ b/lib/kamal/commands/base.rb @@ -74,6 +74,10 @@ def docker(*args) args.compact.unshift :docker end + def pack(*args) + args.compact.unshift :pack + end + def git(*args, path: nil) [ :git, *([ "-C", path ] if path), *args.compact ] end diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index cd2980fbf..c50bc3c5e 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -2,7 +2,7 @@ class Kamal::Commands::Builder < Kamal::Commands::Base delegate :create, :remove, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target - delegate :local?, :remote?, to: "config.builder" + delegate :local?, :remote?, :pack?, to: "config.builder" include Clone @@ -17,6 +17,8 @@ def target else remote end + elsif pack? + pack else local end @@ -34,6 +36,9 @@ def hybrid @hybrid ||= Kamal::Commands::Builder::Hybrid.new(config) end + def pack + @pack ||= Kamal::Commands::Builder::Pack.new(config) + end def ensure_local_dependencies_installed if name.native? diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index dea04a3a3..9fc019384 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -6,6 +6,7 @@ class BuilderError < StandardError; end delegate :argumentize, to: Kamal::Utils delegate \ :args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote, + :pack?, :pack_builder, :pack_buildpacks, :cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?, to: :builder_config diff --git a/lib/kamal/commands/builder/pack.rb b/lib/kamal/commands/builder/pack.rb new file mode 100644 index 000000000..672051b46 --- /dev/null +++ b/lib/kamal/commands/builder/pack.rb @@ -0,0 +1,35 @@ +class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base + def push + combine \ + pack(:build, + config.repository, + "--platform", platform, + "--creation-time", "now", + "--builder", pack_builder, + buildpacks, + "-t", config.absolute_image, + "-t", config.latest_image, + "--env", "BP_IMAGE_LABELS=service=#{config.service}", + *argumentize("--env", args), + *argumentize("--env", secrets, sensitive: true), + "--path", build_context), + docker(:push, config.absolute_image), + docker(:push, config.latest_image) + end + + def remove;end + + def info + pack :builder, :inspect, pack_builder + end + alias_method :inspect_builder, :info + + private + def platform + "linux/#{local_arches.first}" + end + + def buildpacks + (pack_buildpacks << "paketo-buildpacks/image-labels").map { |buildpack| [ "--buildpack", buildpack ] } + end +end diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index 970c47d18..bb8d60582 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -57,6 +57,10 @@ def cached? !!builder_config["cache"] end + def pack? + !!builder_config["pack"] + end + def args builder_config["args"] || {} end @@ -81,6 +85,14 @@ def driver builder_config.fetch("driver", "docker-container") end + def pack_builder + builder_config["pack"]["builder"] if pack? + end + + def pack_buildpacks + builder_config["pack"]["buildpacks"] if pack? + end + def local_disabled? builder_config["local"] == false end diff --git a/lib/kamal/configuration/docs/builder.yml b/lib/kamal/configuration/docs/builder.yml index 230b39eef..6549937d4 100644 --- a/lib/kamal/configuration/docs/builder.yml +++ b/lib/kamal/configuration/docs/builder.yml @@ -8,7 +8,6 @@ # # Options go under the builder key in the root configuration. builder: - # Arch # # The architectures to build for — you can set an array or just a single value. @@ -31,6 +30,19 @@ builder: # Defaults to true: local: true + # Buildpack configuration + # + # The build configuration for using pack to build a Cloud Native Buildpack image. + # + # For additional buildpack customization options you can create a project descriptor + # file(project.toml) that the Pack CLI will automatically use. + # See https://buildpacks.io/docs/for-app-developers/how-to/build-inputs/use-project-toml/ for more information. + pack: + builder: heroku/builder:24 + buildpacks: + - heroku/ruby + - heroku/procfile + # Builder cache # # The type must be either 'gha' or 'registry'. diff --git a/lib/kamal/configuration/validator/builder.rb b/lib/kamal/configuration/validator/builder.rb index 8c8fe1f01..0115d437c 100644 --- a/lib/kamal/configuration/validator/builder.rb +++ b/lib/kamal/configuration/validator/builder.rb @@ -8,6 +8,8 @@ def validate! error "Builder arch not set" unless config["arch"].present? + error "buildpacks only support building for one arch" if config["pack"] && config["arch"].is_a?(Array) && config["arch"].size > 1 + error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank? end end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 85703f546..598f8a18e 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -61,6 +61,32 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder.push.join(" ") end + test "target pack when pack is set" do + builder = new_builder_command(image: "dhh/app", builder: { "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } }) + assert_equal "pack", builder.name + assert_equal \ + "pack build dhh/app --platform linux/amd64 --creation-time now --builder heroku/builder:24 --buildpack heroku/ruby --buildpack heroku/procfile --buildpack paketo-buildpacks/image-labels -t dhh/app:123 -t dhh/app:latest --env BP_IMAGE_LABELS=service=app --path . && docker push dhh/app:123 && docker push dhh/app:latest", + builder.push.join(" ") + end + + test "pack build args passed as env" do + builder = new_builder_command(image: "dhh/app", builder: { "args" => { "a" => 1, "b" => 2 }, "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } }) + + assert_equal \ + "pack build dhh/app --platform linux/amd64 --creation-time now --builder heroku/builder:24 --buildpack heroku/ruby --buildpack heroku/procfile --buildpack paketo-buildpacks/image-labels -t dhh/app:123 -t dhh/app:latest --env BP_IMAGE_LABELS=service=app --env a=\"1\" --env b=\"2\" --path . && docker push dhh/app:123 && docker push dhh/app:latest", + builder.push.join(" ") + end + + test "pack build secrets as env" do + with_test_secrets("secrets" => "token_a=foo\ntoken_b=bar") do + builder = new_builder_command(image: "dhh/app", builder: { "secrets" => [ "token_a", "token_b" ], "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } }) + + assert_equal \ + "pack build dhh/app --platform linux/amd64 --creation-time now --builder heroku/builder:24 --buildpack heroku/ruby --buildpack heroku/procfile --buildpack paketo-buildpacks/image-labels -t dhh/app:123 -t dhh/app:latest --env BP_IMAGE_LABELS=service=app --env token_a=\"foo\" --env token_b=\"bar\" --path . && docker push dhh/app:123 && docker push dhh/app:latest", + builder.push.join(" ") + end + end + test "build args" do builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) assert_equal \ diff --git a/test/configuration/builder_test.rb b/test/configuration/builder_test.rb index 123878009..46e9504aa 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -16,6 +16,23 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase assert_equal false, config.builder.remote? end + test "pack?" do + assert_not config.builder.pack? + end + + test "pack? with pack builder" do + @deploy[:builder] = { "arch" => "arm64", "pack" => { "builder" => "heroku/builder:24" } } + + assert config.builder.pack? + end + + test "pack details" do + @deploy[:builder] = { "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } } + + assert_equal "heroku/builder:24", config.builder.pack_builder + assert_equal [ "heroku/ruby", "heroku/procfile" ], config.builder.pack_buildpacks + end + test "remote" do assert_nil config.builder.remote end diff --git a/test/configuration/validation_test.rb b/test/configuration/validation_test.rb index 179aacfe8..d16b2de20 100644 --- a/test/configuration/validation_test.rb +++ b/test/configuration/validation_test.rb @@ -94,6 +94,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase assert_error "builder/arch: should be an array or a string", builder: { "arch" => {} } assert_error "builder/args: should be a hash", builder: { "args" => [ "foo" ] } assert_error "builder/cache/options: should be a string", builder: { "cache" => { "options" => [] } } + assert_error "builder: buildpacks only support building for one arch", builder: { "arch" => [ "amd64", "arm64" ], "pack" => { "builder" => "heroku/builder:24" } } end private