From 1dfe1b983789b4d6c0fa2fca3fd6c514d176dc38 Mon Sep 17 00:00:00 2001 From: Pavel Shpak Date: Sun, 23 Feb 2025 02:22:45 +0200 Subject: [PATCH] [Feature] Preserve kamal default options. - Add check for default options from `.kamal/options.yml` file. - Add `config_file` option check, with backward-compatible fallback to `config/deploy.yml` value. - Add `.kamal/options.yml` creation on `kamal init` command. Existing option `-c` can be used during initialization to provide default config file. It will be stored into `.kamal/options.yml`. If not provided, backward-compatible `config/deploy.yml` will be stored. - Cover `.kamal/options.yml` with tests. Note: Changes preserve backward-compatibility with current behavior. This feature is extension without breaking stuff. --- lib/kamal/cli/base.rb | 16 ++++- lib/kamal/cli/main.rb | 20 +++++-- lib/kamal/cli/templates/options.yml | 1 + test/cli/main_test.rb | 90 +++++++++++++++++++++-------- 4 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 lib/kamal/cli/templates/options.yml diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 0a6dd6e71..22aa47b12 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -1,5 +1,6 @@ require "thor" require "kamal/sshkit_with_ext" +require "active_support/core_ext/hash/keys" module Kamal::Cli class Base < Thor @@ -17,7 +18,7 @@ def self.dynamic_command_class() Kamal::Cli::Alias::Command end class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma, supports wildcards with *)" class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma, supports wildcards with *)" - class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file" + class_option :config_file, aliases: "-c", desc: "Path to config file. Default set in .kamal/options.yml. Fallback to config/deploy.yml" class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)" class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks" @@ -51,7 +52,7 @@ def initialize_commander end commander.configure \ - config_file: Pathname.new(File.expand_path(options[:config_file])), + config_file: Pathname.new(File.expand_path(option_with_default_from_file(:config_file) || "config/deploy.yml")), destination: options[:destination], version: options[:version] @@ -61,6 +62,17 @@ def initialize_commander end end + def option_with_default_from_file(key) + options[key] || options_file_defaults[key] + end + + def options_file_defaults + @options_file_defaults ||= begin + options_file = Pathname.new(File.expand_path(".kamal/options.yml")) + options_file.exist? ? YAML.load(File.read(options_file)).symbolize_keys : {} + end + end + def print_runtime started_at = Time.now yield diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 2fae36e81..1ef874aba 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -136,17 +136,27 @@ def docs(section = nil) puts "No documentation found for #{section}" end - desc "init", "Create config stub in config/deploy.yml and secrets stub in .kamal" + desc "init", "Create config stub (default config/deploy.yml) and secrets stub in .kamal" option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub" def init require "fileutils" - if (deploy_file = Pathname.new(File.expand_path("config/deploy.yml"))).exist? - puts "Config file already exists in config/deploy.yml (remove first to create a new one)" + config_file = options[:config_file] || "config/deploy.yml" + if (deploy_file = Pathname.new(File.expand_path(config_file))).exist? + puts "Config file #{config_file} already exists (remove first to create a new one)" else FileUtils.mkdir_p deploy_file.dirname - FileUtils.cp_r Pathname.new(File.expand_path("templates/deploy.yml", __dir__)), deploy_file - puts "Created configuration file in config/deploy.yml" + FileUtils.cp Pathname.new(File.expand_path("templates/deploy.yml", __dir__)), deploy_file + puts "Created config file #{config_file}" + end + + unless (options_file = Pathname.new(File.expand_path(".kamal/options.yml"))).exist? + FileUtils.mkdir_p options_file.dirname + FileUtils.cp Pathname.new(File.expand_path("templates/options.yml", __dir__)), options_file + File.open(options_file, "a") do |file| + file.write("config_file: \"#{config_file}\"\n") + end + puts "Created .kamal/options.yml file" end unless (secrets_file = Pathname.new(File.expand_path(".kamal/secrets"))).exist? diff --git a/lib/kamal/cli/templates/options.yml b/lib/kamal/cli/templates/options.yml new file mode 100644 index 000000000..ff06a6c4b --- /dev/null +++ b/lib/kamal/cli/templates/options.yml @@ -0,0 +1 @@ +# Default options to apply on execution of kamal commands. diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index becace43f..5a6f819c7 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -348,24 +348,63 @@ class CliMainTest < CliTestCase end end + test "config and alias commands with defaults from .kamal/options.yml" do + Dir.mktmpdir do |tmpdir| + relative_deploy_file_path = ".kamal/deploy.yml" + + Dir.chdir(tmpdir) do + run_init("-c", relative_deploy_file_path) + end + + config_file = File.join(tmpdir, relative_deploy_file_path) + FileUtils.cp "test/fixtures/deploy_with_aliases.yml", config_file + + Dir.chdir(tmpdir) do + run_main_command("config").tap do |output| + config = YAML.load(output) + assert_equal [ "console", "web", "workers" ], config[:roles] + end + + run_main_command("console", "-r", "workers").tap do |output| + assert_match "docker exec app-workers-999 bin/console on 1.1.1.3", output + assert_match "App Host: 1.1.1.3", output + end + end + end + end + test "init" do in_dummy_git_repo do - run_command("init").tap do |output| - assert_match "Created configuration file in config/deploy.yml", output + run_init.tap do |output| + assert_match "Created config file config/deploy.yml", output + assert_match "Created .kamal/options.yml file", output assert_match "Created .kamal/secrets file", output end assert_file "config/deploy.yml", "service: my-app" + assert_file ".kamal/options.yml", "config_file: \"config/deploy.yml\"" assert_file ".kamal/secrets", "KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD" end end + test "init with config file option" do + in_dummy_git_repo do + run_init("-c", ".kamal/deploy.yml").tap do |output| + assert_match "Created config file .kamal/deploy.yml", output + assert_match "Created .kamal/options.yml file", output + end + + assert_file ".kamal/deploy.yml", "service: my-app" + assert_file ".kamal/options.yml", "config_file: \".kamal/deploy.yml\"" + end + end + test "init with existing config" do in_dummy_git_repo do - run_command("init") + run_init - run_command("init").tap do |output| - assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output + run_init.tap do |output| + assert_match /Config file config\/deploy.yml already exists \(remove first to create a new one\)/, output assert_no_match /Added .kamal\/secrets/, output end end @@ -373,8 +412,8 @@ class CliMainTest < CliTestCase test "init with bundle option" do in_dummy_git_repo do - run_command("init", "--bundle").tap do |output| - assert_match "Created configuration file in config/deploy.yml", output + run_init("--bundle").tap do |output| + assert_match "Created config file config/deploy.yml", output assert_match "Created .kamal/secrets file", output assert_match /Adding Kamal to Gemfile and bundle/, output assert_match /bundle add kamal/, output @@ -385,15 +424,14 @@ class CliMainTest < CliTestCase end test "init with bundle option and existing binstub" do - Pathname.any_instance.expects(:exist?).returns(true).times(4) - Pathname.any_instance.stubs(:mkpath) - FileUtils.stubs(:mkdir_p) - FileUtils.stubs(:cp_r) - FileUtils.stubs(:cp) + in_dummy_git_repo do + binstub = Pathname.new(File.expand_path("bin/kamal")) + FileUtils.mkdir_p binstub.dirname + FileUtils.touch binstub - run_command("init", "--bundle").tap do |output| - assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output - assert_match /Binstub already exists in bin\/kamal \(remove first to create a new one\)/, output + run_init("--bundle").tap do |output| + assert_match /Binstub already exists in bin\/kamal \(remove first to create a new one\)/, output + end end end @@ -484,20 +522,16 @@ class CliMainTest < CliTestCase test "switch config file with an alias" do with_config_files do - with_argv([ "other_config" ]) do - stdouted { Kamal::Cli::Main.start }.tap do |output| - assert_match ":service_with_version: app2-999", output - end + run_main_command("other_config").tap do |output| + assert_match ":service_with_version: app2-999", output end end end test "switch destination with an alias" do with_config_files do - with_argv([ "other_destination_config" ]) do - stdouted { Kamal::Cli::Main.start }.tap do |output| - assert_match ":service_with_version: app3-999", output - end + run_main_command("other_destination_config").tap do |output| + assert_match ":service_with_version: app3-999", output end end end @@ -539,7 +573,15 @@ class CliMainTest < CliTestCase private def run_command(*command, config_file: "deploy_simple") - with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do + run_main_command(*command, "-c", "test/fixtures/#{config_file}.yml") + end + + def run_init(*options) + run_main_command("init", *options) + end + + def run_main_command(*command) + with_argv([ *command ]) do stdouted { Kamal::Cli::Main.start } end end