diff --git a/Dockerfile b/Dockerfile index 3d8af252..d8346804 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ -# Use the official Ruby 3.2.0 Alpine image as the base image -FROM ruby:3.2.0-alpine +FROM ruby:3.3-alpine # Install docker/buildx-bin COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx diff --git a/Gemfile.lock b/Gemfile.lock index 093ef9b7..c057d7f6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,24 +1,24 @@ PATH remote: . specs: - kamal (2.2.2) + kamal (2.3.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) concurrent-ruby (~> 1.2) dotenv (~> 3.1) ed25519 (~> 1.2) - net-ssh (~> 7.0) + net-ssh (~> 7.3) sshkit (>= 1.23.0, < 2.0) thor (~> 1.3) - zeitwerk (~> 2.5) + zeitwerk (>= 2.6.18, < 3.0) GEM remote: https://rubygems.org/ specs: - actionpack (7.1.3.4) - actionview (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionpack (7.1.4.1) + actionview (= 7.1.4.1) + activesupport (= 7.1.4.1) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -26,13 +26,13 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actionview (7.1.3.4) - activesupport (= 7.1.3.4) + actionview (7.1.4.1) + activesupport (= 7.1.4.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activesupport (7.1.3.4) + activesupport (7.1.4.1) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -49,7 +49,7 @@ GEM bcrypt_pbkdf (1.1.1-x86_64-darwin) bigdecimal (3.1.8) builder (3.3.0) - concurrent-ruby (1.3.3) + concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) debug (1.9.2) @@ -59,7 +59,7 @@ GEM drb (2.2.1) ed25519 (1.3.0) erubi (1.13.0) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) io-console (0.7.2) irb (1.14.0) @@ -70,7 +70,7 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - minitest (5.24.1) + minitest (5.25.1) mocha (2.4.5) ruby2_keywords (>= 0.0.5) mutex_m (0.2.0) @@ -78,7 +78,7 @@ GEM net-ssh (>= 2.6.5, < 8.0.0) net-sftp (4.0.0) net-ssh (>= 5.0.0, < 8.0.0) - net-ssh (7.2.3) + net-ssh (7.3.0) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) nokogiri (1.16.7-x86_64-darwin) @@ -92,7 +92,7 @@ GEM psych (5.1.2) stringio racc (1.8.1) - rack (3.1.7) + rack (3.1.8) rack-session (2.0.0) rack (>= 3.0.0) rack-test (2.1.0) @@ -107,9 +107,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) + railties (7.1.4.1) + actionpack (= 7.1.4.1) + activesupport (= 7.1.4.1) irb rackup (>= 1.0.0) rake (>= 12.2) @@ -122,8 +122,7 @@ GEM regexp_parser (2.9.2) reline (0.5.9) io-console (~> 0.5) - rexml (3.3.4) - strscan + rexml (3.3.9) rubocop (1.65.1) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -161,13 +160,12 @@ GEM net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) stringio (3.1.1) - strscan (3.1.0) thor (1.3.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) - webrick (1.8.1) - zeitwerk (2.6.17) + webrick (1.8.2) + zeitwerk (2.7.1) PLATFORMS arm64-darwin diff --git a/kamal.gemspec b/kamal.gemspec index 0dfab60b..7218f865 100644 --- a/kamal.gemspec +++ b/kamal.gemspec @@ -13,10 +13,10 @@ Gem::Specification.new do |spec| spec.add_dependency "activesupport", ">= 7.0" spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0" - spec.add_dependency "net-ssh", "~> 7.0" + spec.add_dependency "net-ssh", "~> 7.3" spec.add_dependency "thor", "~> 1.3" spec.add_dependency "dotenv", "~> 3.1" - spec.add_dependency "zeitwerk", "~> 2.5" + spec.add_dependency "zeitwerk", ">= 2.6.18", "< 3.0" spec.add_dependency "ed25519", "~> 1.2" spec.add_dependency "bcrypt_pbkdf", "~> 1.0" spec.add_dependency "concurrent-ruby", "~> 1.2" diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 2ce7c2ac..d0e9ba2b 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -14,7 +14,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base version = capture_with_info(*KAMAL.proxy.version).strip.presence if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION) - raise "kamal-proxy version #{version} is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" + raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" end execute *KAMAL.proxy.start_or_run end diff --git a/lib/kamal/cli/templates/deploy.yml b/lib/kamal/cli/templates/deploy.yml index e278caa8..fc44b6b9 100644 --- a/lib/kamal/cli/templates/deploy.yml +++ b/lib/kamal/cli/templates/deploy.yml @@ -13,13 +13,14 @@ servers: # - 192.168.0.1 # cmd: bin/jobs -# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server). -# If using something like Cloudflare, it is recommended to set encryption mode -# in Cloudflare's SSL/TLS setting to "Full" to enable end-to-end encryption. +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. proxy: ssl: true host: app.example.com - # kamal-proxy connects to your container over port 80, use `app_port` to specify a different port. + # Proxy connects to your container on port 80 by default. # app_port: 3000 # Credentials for your image host. @@ -90,7 +91,7 @@ builder: # directories: # - data:/var/lib/mysql # redis: -# image: redis:7.0 +# image: valkey/valkey:8 # host: 192.168.0.2 # port: 6379 # directories: diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index 0c1b9009..9abb6dfc 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -1,7 +1,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base attr_reader :accessory_config delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd, - :publish_args, :env_args, :volume_args, :label_args, :option_args, + :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args, :secrets_io, :secrets_path, :env_directory, to: :accessory_config @@ -15,7 +15,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base "--name", service_name, "--detach", "--restart", "unless-stopped", - "--network", "kamal", + *network_args, *config.logging_args, *publish_args, *env_args, @@ -64,7 +64,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base docker :run, ("-it" if interactive), "--rm", - "--network", "kamal", + *network_args, *env_args, *volume_args, image, diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index 6d8f44c6..6c4df0e4 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -47,7 +47,7 @@ class Kamal::Commands::App < Kamal::Commands::Base end def info - docker :ps, *filter_args + docker :ps, *container_filter_args end @@ -67,7 +67,7 @@ class Kamal::Commands::App < Kamal::Commands::Base def list_versions(*docker_args, statuses: nil) pipe \ - docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'), + docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'), extract_version_from_name end @@ -91,11 +91,15 @@ class Kamal::Commands::App < Kamal::Commands::Base end def latest_container(format:, filters: nil) - docker :ps, "--latest", *format, *filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters) + docker :ps, "--latest", *format, *container_filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters) end - def filter_args(statuses: nil) - argumentize "--filter", filters(statuses: statuses) + def container_filter_args(statuses: nil) + argumentize "--filter", container_filters(statuses: statuses) + end + + def image_filter_args + argumentize "--filter", image_filters end def extract_version_from_name @@ -103,13 +107,17 @@ class Kamal::Commands::App < Kamal::Commands::Base %(while read line; do echo ${line##{role.container_prefix}-}; done) end - def filters(statuses: nil) + def container_filters(statuses: nil) [ "label=service=#{config.service}" ].tap do |filters| - filters << "label=destination=#{config.destination}" if config.destination + filters << "label=destination=#{config.destination}" filters << "label=role=#{role}" if role statuses&.each do |status| filters << "status=#{status}" end end end + + def image_filters + [ "label=service=#{config.service}" ] + end end diff --git a/lib/kamal/commands/app/containers.rb b/lib/kamal/commands/app/containers.rb index 0bab388b..a83d83ca 100644 --- a/lib/kamal/commands/app/containers.rb +++ b/lib/kamal/commands/app/containers.rb @@ -2,7 +2,7 @@ module Kamal::Commands::App::Containers DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'" def list_containers - docker :container, :ls, "--all", *filter_args + docker :container, :ls, "--all", *container_filter_args end def list_container_names @@ -20,7 +20,7 @@ module Kamal::Commands::App::Containers end def remove_containers - docker :container, :prune, "--force", *filter_args + docker :container, :prune, "--force", *container_filter_args end def container_health_log(version:) diff --git a/lib/kamal/commands/app/images.rb b/lib/kamal/commands/app/images.rb index e20e83e1..db596a31 100644 --- a/lib/kamal/commands/app/images.rb +++ b/lib/kamal/commands/app/images.rb @@ -4,7 +4,7 @@ module Kamal::Commands::App::Images end def remove_images - docker :image, :prune, "--all", "--force", *filter_args + docker :image, :prune, "--all", "--force", *image_filter_args end def tag_latest_image diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 636fe4f4..d551520b 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -6,7 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base delegate :argumentize, to: Kamal::Utils delegate \ :args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote, - :cache_from, :cache_to, :ssh, :driver, :docker_driver?, + :cache_from, :cache_to, :ssh, :provenance, :driver, :docker_driver?, to: :builder_config def clean @@ -37,7 +37,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base end def build_options - [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ] + [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance ] end def build_context @@ -97,6 +97,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base argumentize "--ssh", ssh if ssh.present? end + def builder_provenance + argumentize "--provenance", provenance unless provenance.nil? + end + def builder_config config.builder end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 86602fe1..021e5e49 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -14,7 +14,7 @@ class Kamal::Configuration include Validation - PROXY_MINIMUM_VERSION = "v0.8.1" + PROXY_MINIMUM_VERSION = "v0.8.2" PROXY_HTTP_PORT = 80 PROXY_HTTPS_PORT = 443 PROXY_LOG_MAX_SIZE = "10m" @@ -254,7 +254,7 @@ class Kamal::Configuration end def proxy_logging_args(max_size) - argumentize "--log-opt", "max-size=#{max_size}" + argumentize "--log-opt", "max-size=#{max_size}" if max_size.present? end def proxy_options_default diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 804a1502..3258c9d0 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -1,6 +1,8 @@ class Kamal::Configuration::Accessory include Kamal::Configuration::Validation + DEFAULT_NETWORK = "kamal" + delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :name, :accessory_config, :env @@ -38,6 +40,10 @@ class Kamal::Configuration::Accessory end end + def network_args + argumentize "--network", network + end + def publish_args argumentize "--publish", port if port end @@ -173,4 +179,8 @@ class Kamal::Configuration::Accessory accessory_config["roles"].flat_map { |role| config.role(role).hosts } end end + + def network + accessory_config["network"] || DEFAULT_NETWORK + end end diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index a395e228..4c0dc603 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -111,6 +111,10 @@ class Kamal::Configuration::Builder builder_config["ssh"] end + def provenance + builder_config["provenance"] + end + def git_clone? Kamal::Git.used? && builder_config["context"].nil? end @@ -166,7 +170,7 @@ class Kamal::Configuration::Builder end def cache_to_config_for_registry - [ "type=registry", builder_config["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",") + [ "type=registry", "ref=#{cache_image_ref}", builder_config["cache"]&.fetch("options", nil) ].compact.join(",") end def repo_basename diff --git a/lib/kamal/configuration/docs/accessory.yml b/lib/kamal/configuration/docs/accessory.yml index 86d49b77..e77bf754 100644 --- a/lib/kamal/configuration/docs/accessory.yml +++ b/lib/kamal/configuration/docs/accessory.yml @@ -90,3 +90,11 @@ accessories: # They are not created or copied before mounting: volumes: - /path/to/mysql-logs:/var/log/mysql + + # Network + # + # The network the accessory will be attached to. + # + # Defaults to kamal: + network: custom + diff --git a/lib/kamal/configuration/docs/builder.yml b/lib/kamal/configuration/docs/builder.yml index eccda407..b6e639f3 100644 --- a/lib/kamal/configuration/docs/builder.yml +++ b/lib/kamal/configuration/docs/builder.yml @@ -102,3 +102,9 @@ builder: # # The build driver to use, defaults to `docker-container`: driver: docker + + # Provenance + # + # It is used to configure provenance attestations for the build result. + # The value can also be a boolean to enable or disable provenance attestations. + provenance: mode=max diff --git a/lib/kamal/env_file.rb b/lib/kamal/env_file.rb index 6a4a80e3..36c251be 100644 --- a/lib/kamal/env_file.rb +++ b/lib/kamal/env_file.rb @@ -37,6 +37,8 @@ class Kamal::EnvFile def escape_docker_env_file_ascii_value(value) # Doublequotes are treated literally in docker env files # so remove leading and trailing ones and unescape any others - value.to_s.dump[1..-2].gsub(/\\"/, "\"") + value.to_s.dump[1..-2] + .gsub(/\\"/, "\"") + .gsub(/\\#/, "#") end end diff --git a/lib/kamal/secrets.rb b/lib/kamal/secrets.rb index c7d4cc03..7a382f98 100644 --- a/lib/kamal/secrets.rb +++ b/lib/kamal/secrets.rb @@ -1,13 +1,10 @@ require "dotenv" class Kamal::Secrets - attr_reader :secrets_files - Kamal::Secrets::Dotenv::InlineCommandSubstitution.install! def initialize(destination: nil) - @secrets_files = \ - [ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) } + @destination = destination @mutex = Mutex.new end @@ -17,10 +14,10 @@ class Kamal::Secrets secrets.fetch(key) end rescue KeyError - if secrets_files + if secrets_files.present? raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}" else - raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided" + raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files (#{secrets_filenames.join(", ")}) provided" end end @@ -28,10 +25,18 @@ class Kamal::Secrets secrets end + def secrets_files + @secrets_files ||= secrets_filenames.select { |f| File.exist?(f) } + end + private def secrets @secrets ||= secrets_files.inject({}) do |secrets, secrets_file| secrets.merge!(::Dotenv.parse(secrets_file)) end end + + def secrets_filenames + [ ".kamal/secrets-common", ".kamal/secrets#{(".#{@destination}" if @destination)}" ] + end end diff --git a/lib/kamal/secrets/adapters/base.rb b/lib/kamal/secrets/adapters/base.rb index 97b2a458..579414af 100644 --- a/lib/kamal/secrets/adapters/base.rb +++ b/lib/kamal/secrets/adapters/base.rb @@ -2,6 +2,7 @@ class Kamal::Secrets::Adapters::Base delegate :optionize, to: Kamal::Utils def fetch(secrets, account:, from: nil) + check_dependencies! session = login(account) full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") } fetch_secrets(full_secrets, account: account, session: session) @@ -15,4 +16,8 @@ class Kamal::Secrets::Adapters::Base def fetch_secrets(...) raise NotImplementedError end + + def check_dependencies! + raise NotImplementedError + end end diff --git a/lib/kamal/secrets/adapters/bitwarden.rb b/lib/kamal/secrets/adapters/bitwarden.rb index e84a0d93..5dfb72db 100644 --- a/lib/kamal/secrets/adapters/bitwarden.rb +++ b/lib/kamal/secrets/adapters/bitwarden.rb @@ -25,18 +25,15 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base {}.tap do |results| items_fields(secrets).each do |item, fields| item_json = run_command("get item #{item.shellescape}", session: session, raw: true) - raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success? + raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success? item_json = JSON.parse(item_json) - if fields.any? - fields.each do |field| - item_field = item_json["fields"].find { |f| f["name"] == field } - raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field - value = item_field["value"] - results["#{item}/#{field}"] = value - end + results.merge! fetch_secrets_from_fields(fields, item, item_json) elsif item_json.dig("login", "password") results[item] = item_json.dig("login", "password") + elsif item_json["fields"]&.any? + fields = item_json["fields"].pluck("name") + results.merge! fetch_secrets_from_fields(fields, item, item_json) else raise RuntimeError, "Item #{item} is not a login type item and no fields were specified" end @@ -44,6 +41,15 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base end end + def fetch_secrets_from_fields(fields, item, item_json) + fields.to_h do |field| + item_field = item_json["fields"].find { |f| f["name"] == field } + raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field + value = item_field["value"] + [ "#{item}/#{field}", value ] + end + end + def items_fields(secrets) {}.tap do |items| secrets.each do |secret| @@ -63,4 +69,13 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base result = `#{full_command}`.strip raw ? result : JSON.parse(result) end + + def check_dependencies! + raise RuntimeError, "Bitwarden CLI is not installed" unless cli_installed? + end + + def cli_installed? + `bw --version 2> /dev/null` + $?.success? + end end diff --git a/lib/kamal/secrets/adapters/last_pass.rb b/lib/kamal/secrets/adapters/last_pass.rb index 390e84ed..2f95148b 100644 --- a/lib/kamal/secrets/adapters/last_pass.rb +++ b/lib/kamal/secrets/adapters/last_pass.rb @@ -27,4 +27,13 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base end end end + + def check_dependencies! + raise RuntimeError, "LastPass CLI is not installed" unless cli_installed? + end + + def cli_installed? + `lpass --version 2> /dev/null` + $?.success? + end end diff --git a/lib/kamal/secrets/adapters/one_password.rb b/lib/kamal/secrets/adapters/one_password.rb index c7e9b28d..c7f9d5ab 100644 --- a/lib/kamal/secrets/adapters/one_password.rb +++ b/lib/kamal/secrets/adapters/one_password.rb @@ -58,4 +58,13 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success? end end + + def check_dependencies! + raise RuntimeError, "1Password CLI is not installed" unless cli_installed? + end + + def cli_installed? + `op --version 2> /dev/null` + $?.success? + end end diff --git a/lib/kamal/secrets/adapters/test.rb b/lib/kamal/secrets/adapters/test.rb index fc0903d9..82577a76 100644 --- a/lib/kamal/secrets/adapters/test.rb +++ b/lib/kamal/secrets/adapters/test.rb @@ -7,4 +7,8 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base def fetch_secrets(secrets, account:, session:) secrets.to_h { |secret| [ secret, secret.reverse ] } end + + def check_dependencies! + # no op + end end diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index ab8dd50e..58daaa91 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -12,6 +12,8 @@ module Kamal::Utils attr = "#{key}=#{escape_shell_value(value)}" attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive [ argument, attr ] + elsif value == false + [ argument, "#{key}=false" ] else [ argument, key ] end diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 3f5ef1fc..2f9a86b6 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.2.2" + VERSION = "2.3.0" end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 32b37456..5e76179c 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -19,7 +19,7 @@ class CliAppTest < CliTestCase .returns("12345678") # running version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("123") # old version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) @@ -63,7 +63,7 @@ class CliAppTest < CliTestCase .returns("12345678") # running version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("123").twice # old version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) @@ -92,7 +92,7 @@ class CliAppTest < CliTestCase .returns("12345678") # running version SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("123") # old version run_command("boot", config: :with_env_tags).tap do |output| @@ -196,17 +196,17 @@ class CliAppTest < CliTestCase test "stop" do run_command("stop").tap do |output| - assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", output + assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", output end end test "stale_containers" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=destination=", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("12345678\n87654321\n") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("12345678\n") run_command("stale_containers").tap do |output| @@ -216,11 +216,11 @@ class CliAppTest < CliTestCase test "stop stale_containers" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=destination=", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("12345678\n87654321\n") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("12345678\n") run_command("stale_containers", "--stop").tap do |output| @@ -231,13 +231,13 @@ class CliAppTest < CliTestCase test "details" do run_command("details").tap do |output| - assert_match "docker ps --filter label=service=app --filter label=role=web", output + assert_match "docker ps --filter label=service=app --filter label=destination= --filter label=role=web", output end end test "remove" do run_command("remove").tap do |output| - assert_match /#{Regexp.escape("sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop")}/, output + assert_match /#{Regexp.escape("sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop")}/, output assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output end @@ -275,7 +275,7 @@ class CliAppTest < CliTestCase test "exec with reuse" do run_command("exec", "--reuse", "ruby -v").tap do |output| - assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version + assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version assert_match "docker exec app-web-999 ruby -v", output end end @@ -294,7 +294,7 @@ class CliAppTest < CliTestCase .with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'") run_command("exec", "-i", "--reuse", "ruby -v").tap do |output| assert_match "Get current version of running container...", output - assert_match "Running /usr/bin/env sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output + assert_match "Running /usr/bin/env sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output end end @@ -313,46 +313,46 @@ class CliAppTest < CliTestCase test "logs" do SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'") + .with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'") - assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", run_command("logs") + assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", run_command("logs") - assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey") + assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey") - assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2") + assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2") end test "logs with follow" do SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1'") + .with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1'") - assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow") + assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow") end test "logs with follow and grep" do SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"'") + .with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"'") - assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"", run_command("logs", "--follow", "--grep", "hey") + assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"", run_command("logs", "--follow", "--grep", "hey") end test "logs with follow, grep and grep options" do SSHKit::Backend::Abstract.any_instance.stubs(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\" -C 2'") + .with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\" -C 2'") - assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\" -C 2", run_command("logs", "--follow", "--grep", "hey", "--grep-options", "-C 2") + assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\" -C 2", run_command("logs", "--follow", "--grep", "hey", "--grep-options", "-C 2") end test "version" do run_command("version").tap do |output| - assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output + assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output end end test "version through main" do stdouted { Kamal::Cli::Main.start([ "app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1" ]) }.tap do |output| - assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output + assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 4b111bad..cd0efe1f 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -250,7 +250,7 @@ class CliMainTest < CliTestCase .with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet") .returns("version-to-rollback\n").at_least_once SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-#{role}-}; done", raise_on_non_zero_exit: false) + .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=#{role} --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=#{role} --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-#{role}-}; done", raise_on_non_zero_exit: false) .returns("version-to-rollback\n").at_least_once end @@ -280,7 +280,7 @@ class CliMainTest < CliTestCase .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") .returns("123").at_least_once SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) + .with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) .returns("").at_least_once run_command("rollback", "123").tap do |output| diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index c62589fe..0a890451 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -22,7 +22,7 @@ class CliProxyTest < CliTestCase end end - assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" + assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" ensure Thread.report_on_exception = false end @@ -263,6 +263,15 @@ class CliProxyTest < CliTestCase end end + test "boot_config set no log max size" do + run_command("boot_config", "set", "--log-max-size=").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output + end + end + end + test "boot_config set custom ports" do run_command("boot_config", "set", "--http-port", "8080", "--https-port", "8443").tap do |output| %w[ 1.1.1.1 1.1.1.2 ].each do |host| diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index f3d71ffd..1befd9e6 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -71,6 +71,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase new_command(:busybox).run.join(" ") end + test "run in custom network" do + @config[:accessories]["mysql"]["network"] = "custom" + + assert_equal \ + "docker run --name app-mysql --detach --restart unless-stopped --network custom --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0", + new_command(:mysql).run.join(" ") + end + test "start" do assert_equal \ "docker container start app-mysql", diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 0e5cad79..1fb59e8a 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -79,18 +79,18 @@ class CommandsAppTest < ActiveSupport::TestCase test "stop" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", new_command.stop.join(" ") end test "stop with custom drain timeout" do @config[:drain_timeout] = 20 assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", new_command.stop.join(" ") assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=workers --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=workers --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 20", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=workers --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=workers --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 20", new_command(role: "workers").stop.join(" ") end @@ -102,7 +102,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "info" do assert_equal \ - "docker ps --filter label=service=app --filter label=role=web", + "docker ps --filter label=service=app --filter label=destination= --filter label=role=web", new_command.info.join(" ") end @@ -153,71 +153,71 @@ class CommandsAppTest < ActiveSupport::TestCase test "logs" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1", new_command.logs.join(" ") end test "logs with since" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1", new_command.logs(since: "5m").join(" ") end test "logs with lines" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", new_command.logs(lines: "100").join(" ") end test "logs with since and lines" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1", new_command.logs(since: "5m", lines: "100").join(" ") end test "logs with grep" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'", new_command.logs(grep: "my-id").join(" ") end test "logs with grep and grep options" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2", new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ") end test "logs with since, grep and grep options" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2", new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ") end test "logs with since and grep" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'", new_command.logs(since: "5m", grep: "my-id").join(" ") end test "follow logs" do assert_equal \ - "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1'", + "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1'", new_command.follow_logs(host: "app-1") assert_equal \ - "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"Completed\"'", + "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"Completed\"'", new_command.follow_logs(host: "app-1", grep: "Completed") assert_equal \ - "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1'", + "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1'", new_command.follow_logs(host: "app-1", lines: 123) assert_equal \ - "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'", + "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'", new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed") assert_equal \ - "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'", + "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'", new_command.follow_logs(host: "app-1", timestamps: false, lines: 123, grep: "Completed") end @@ -322,7 +322,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "current_running_container_id" do assert_equal \ - "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1", + "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1", new_command.current_running_container_id.join(" ") end @@ -341,23 +341,23 @@ class CommandsAppTest < ActiveSupport::TestCase test "current_running_version" do assert_equal \ - "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", + "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", new_command.current_running_version.join(" ") end test "list_versions" do assert_equal \ - "docker ps --filter label=service=app --filter label=role=web --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done", + "docker ps --filter label=service=app --filter label=destination= --filter label=role=web --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done", new_command.list_versions.join(" ") assert_equal \ - "docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done", + "docker ps --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done", new_command.list_versions("--latest", statuses: [ :running, :restarting ]).join(" ") end test "list_containers" do assert_equal \ - "docker container ls --all --filter label=service=app --filter label=role=web", + "docker container ls --all --filter label=service=app --filter label=destination= --filter label=role=web", new_command.list_containers.join(" ") end @@ -370,7 +370,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "list_container_names" do assert_equal \ - "docker container ls --all --filter label=service=app --filter label=role=web --format '{{ .Names }}'", + "docker container ls --all --filter label=service=app --filter label=destination= --filter label=role=web --format '{{ .Names }}'", new_command.list_container_names.join(" ") end @@ -389,7 +389,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "remove_containers" do assert_equal \ - "docker container prune --force --filter label=service=app --filter label=role=web", + "docker container prune --force --filter label=service=app --filter label=destination= --filter label=role=web", new_command.remove_containers.join(" ") end @@ -408,14 +408,14 @@ class CommandsAppTest < ActiveSupport::TestCase test "remove_images" do assert_equal \ - "docker image prune --all --force --filter label=service=app --filter label=role=web", + "docker image prune --all --force --filter label=service=app", new_command.remove_images.join(" ") end test "remove_images with destination" do @destination = "staging" assert_equal \ - "docker image prune --all --force --filter label=service=app --filter label=destination=staging --filter label=role=web", + "docker image prune --all --force --filter label=service=app", new_command.remove_images.join(" ") end diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 1fc224da..86b2f573 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -144,6 +144,20 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder.push.join(" ") end + test "push with provenance" do + builder = new_builder_command(builder: { "provenance" => "mode=max" }) + assert_equal \ + "docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance mode=max .", + builder.push.join(" ") + end + + test "push with provenance false" do + builder = new_builder_command(builder: { "provenance" => false }) + assert_equal \ + "docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance false .", + builder.push.join(" ") + end + test "mirror count" do command = new_builder_command assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ") diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index 2615dab6..f5220902 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -152,4 +152,13 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase test "options" do assert_equal [ "--cpus", "\"4\"", "--memory", "\"2GB\"" ], @config.accessory(:redis).option_args end + + test "network_args default" do + assert_equal [ "--network", "kamal" ], @config.accessory(:mysql).network_args + end + + test "network_args with configured options" do + @deploy[:accessories]["mysql"]["network"] = "database" + assert_equal [ "--network", "database" ], @config.accessory(:mysql).network_args + end end diff --git a/test/configuration/builder_test.rb b/test/configuration/builder_test.rb index 53740ca8..5fef465a 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -64,7 +64,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase @deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } } assert_equal "type=registry,ref=dhh/app-build-cache", config.builder.cache_from - assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config.builder.cache_to + assert_equal "type=registry,ref=dhh/app-build-cache,mode=max,image-manifest=true,oci-mediatypes=true", config.builder.cache_to end test "setting registry cache when using a custom registry" do @@ -72,14 +72,14 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase @deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } } assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_from - assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_to + assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache,mode=max,image-manifest=true,oci-mediatypes=true", config.builder.cache_to end test "setting registry cache with image" do @deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } } assert_equal "type=registry,ref=kamal", config.builder.cache_from - assert_equal "type=registry,mode=max,ref=kamal", config.builder.cache_to + assert_equal "type=registry,ref=kamal,mode=max", config.builder.cache_to end test "args" do @@ -134,6 +134,16 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase assert_equal "default=$SSH_AUTH_SOCK", config.builder.ssh end + test "provenance" do + assert_nil config.builder.provenance + end + + test "setting provenance" do + @deploy[:builder]["provenance"] = "mode=max" + + assert_equal "mode=max", config.builder.provenance + end + test "local disabled but no remote set" do @deploy[:builder]["local"] = false diff --git a/test/env_file_test.rb b/test/env_file_test.rb index c6b9e66e..34d31ad6 100644 --- a/test/env_file_test.rb +++ b/test/env_file_test.rb @@ -11,6 +11,16 @@ class EnvFileTest < ActiveSupport::TestCase Kamal::EnvFile.new(env).to_s end + test "to_s won't escape '#'" do + env = { + "foo" => '#$foo', + "bar" => '#{bar}' + } + + assert_equal "foo=\#$foo\nbar=\#{bar}\n", \ + Kamal::EnvFile.new(env).to_s + end + test "to_str won't escape chinese characters" do env = { "foo" => '你好 means hello, "欢迎" means welcome, that\'s simple! 😃 {smile}' diff --git a/test/secrets/bitwarden_adapter_test.rb b/test/secrets/bitwarden_adapter_test.rb index e2a3ac37..ad280791 100644 --- a/test/secrets/bitwarden_adapter_test.rb +++ b/test/secrets/bitwarden_adapter_test.rb @@ -2,6 +2,8 @@ require "test_helper" class BitwardenAdapterTest < SecretAdapterTestCase test "fetch" do + stub_ticks.with("bw --version 2> /dev/null") + stub_unlocked stub_ticks.with("bw sync").returns("") stub_mypassword @@ -14,6 +16,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch with no login" do + stub_ticks.with("bw --version 2> /dev/null") + stub_unlocked stub_ticks.with("bw sync").returns("") stub_noteitem @@ -25,6 +29,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch with from" do + stub_ticks.with("bw --version 2> /dev/null") + stub_unlocked stub_ticks.with("bw sync").returns("") stub_myitem @@ -38,7 +44,26 @@ class BitwardenAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch all with from" do + stub_ticks.with("bw --version 2> /dev/null") + + stub_unlocked + stub_ticks.with("bw sync").returns("") + stub_noteitem_with_fields + + json = JSON.parse(shellunescape(run_command("fetch", "mynotefields"))) + + expected_json = { + "mynotefields/field1"=>"secret1", "mynotefields/field2"=>"blam", "mynotefields/field3"=>"fewgrwjgk", + "mynotefields/field4"=>"auto" + } + + assert_equal expected_json, json + end + test "fetch with multiple items" do + stub_ticks.with("bw --version 2> /dev/null") + stub_unlocked stub_ticks.with("bw sync").returns("") @@ -80,6 +105,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch unauthenticated" do + stub_ticks.with("bw --version 2> /dev/null") + stub_ticks .with("bw status") .returns( @@ -101,6 +128,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch locked" do + stub_ticks.with("bw --version 2> /dev/null") + stub_ticks .with("bw status") .returns( @@ -126,6 +155,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase end test "fetch locked with session" do + stub_ticks.with("bw --version 2> /dev/null") + stub_ticks .with("bw status") .returns( @@ -150,6 +181,15 @@ class BitwardenAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch without CLI installed" do + stub_ticks_with("bw --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "mynote"))) + end + assert_equal "Bitwarden CLI is not installed", error.message + end + private def run_command(*command) stdouted do @@ -214,7 +254,37 @@ class BitwardenAdapterTest < SecretAdapterTestCase "collectionIds":[] } JSON - end + end + + def stub_noteitem_with_fields(session: nil) + stub_ticks + .with("#{"BW_SESSION=#{session} " if session}bw get item mynotefields") + .returns(<<~JSON) + { + "passwordHistory":null, + "revisionDate":"2024-09-28T09:07:27.461Z", + "creationDate":"2024-09-28T09:07:00.740Z", + "deletedDate":null, + "object":"item", + "id":"aaaaaaaa-cccc-eeee-0000-222222222222", + "organizationId":null, + "folderId":null, + "type":2, + "reprompt":0, + "name":"noteitem", + "notes":"NOTES", + "favorite":false, + "fields":[ + {"name":"field1","value":"secret1","type":1,"linkedId":null}, + {"name":"field2","value":"blam","type":1,"linkedId":null}, + {"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null}, + {"name":"field4","value":"auto","type":1,"linkedId":null} + ], + "secureNote":{"type":0}, + "collectionIds":[] + } + JSON + end def stub_myitem stub_ticks @@ -237,7 +307,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase "fields":[ {"name":"field1","value":"secret1","type":1,"linkedId":null}, {"name":"field2","value":"blam","type":1,"linkedId":null}, - {"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null} + {"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null}, + {"name":"field4","value":"auto","type":1,"linkedId":null} ], "login":{"fido2Credentials":[],"uris":[],"username":null,"password":null,"totp":null,"passwordRevisionDate":null},"collectionIds":[] } diff --git a/test/secrets/last_pass_adapter_test.rb b/test/secrets/last_pass_adapter_test.rb index 3801d486..ca1f346c 100644 --- a/test/secrets/last_pass_adapter_test.rb +++ b/test/secrets/last_pass_adapter_test.rb @@ -6,6 +6,7 @@ class LastPassAdapterTest < SecretAdapterTestCase end test "fetch" do + stub_ticks.with("lpass --version 2> /dev/null") stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.") stub_ticks @@ -63,6 +64,7 @@ class LastPassAdapterTest < SecretAdapterTestCase end test "fetch with from" do + stub_ticks.with("lpass --version 2> /dev/null") stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.") stub_ticks @@ -107,6 +109,8 @@ class LastPassAdapterTest < SecretAdapterTestCase end test "fetch with signin" do + stub_ticks.with("lpass --version 2> /dev/null") + stub_ticks_with("lpass status --color never", succeed: false).returns("Not logged in.") stub_ticks_with("lpass login email@example.com", succeed: true).returns("") stub_ticks.with("lpass show SECRET1 --json").returns(single_item_json) @@ -120,6 +124,15 @@ class LastPassAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch without CLI installed" do + stub_ticks_with("lpass --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2"))) + end + assert_equal "LastPass CLI is not installed", error.message + end + private def run_command(*command) stdouted do diff --git a/test/secrets/one_password_adapter_test.rb b/test/secrets/one_password_adapter_test.rb index 59ad511d..36fab7c3 100644 --- a/test/secrets/one_password_adapter_test.rb +++ b/test/secrets/one_password_adapter_test.rb @@ -2,6 +2,7 @@ require "test_helper" class SecretsOnePasswordAdapterTest < SecretAdapterTestCase test "fetch" do + stub_ticks.with("op --version 2> /dev/null") stub_ticks.with("op account get --account myaccount 2> /dev/null") stub_ticks @@ -56,6 +57,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase end test "fetch with multiple items" do + stub_ticks.with("op --version 2> /dev/null") stub_ticks.with("op account get --account myaccount 2> /dev/null") stub_ticks @@ -115,6 +117,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase end test "fetch with signin, no session" do + stub_ticks.with("op --version 2> /dev/null") + stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false) stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("") @@ -132,6 +136,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase end test "fetch with signin and session" do + stub_ticks.with("op --version 2> /dev/null") + stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false) stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("1234567890") @@ -148,6 +154,15 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch without CLI installed" do + stub_ticks_with("op --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3"))) + end + assert_equal "1Password CLI is not installed", error.message + end + private def run_command(*command) stdouted do diff --git a/test/secrets_test.rb b/test/secrets_test.rb index bb77a196..aca9cebe 100644 --- a/test/secrets_test.rb +++ b/test/secrets_test.rb @@ -31,4 +31,18 @@ class SecretsTest < ActiveSupport::TestCase assert_equal "JKL", Kamal::Secrets.new(destination: "nodest")["SECRET2"] end end + + test "no secrets files" do + with_test_secrets do + error = assert_raises(Kamal::ConfigurationError) do + Kamal::Secrets.new["SECRET"] + end + assert_equal "Secret 'SECRET' not found, no secret files (.kamal/secrets-common, .kamal/secrets) provided", error.message + + error = assert_raises(Kamal::ConfigurationError) do + Kamal::Secrets.new(destination: "dest")["SECRET"] + end + assert_equal "Secret 'SECRET' not found, no secret files (.kamal/secrets-common, .kamal/secrets.dest) provided", error.message + end + end end diff --git a/test/utils_test.rb b/test/utils_test.rb index e292e980..a8fa0797 100644 --- a/test/utils_test.rb +++ b/test/utils_test.rb @@ -2,8 +2,8 @@ require "test_helper" class UtilsTest < ActiveSupport::TestCase test "argumentize" do - assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux ], \ - Kamal::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil }) + assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux, "--label", "quuz=false" ], \ + Kamal::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil, quuz: false }) end test "argumentize with redacted" do