Back to Repositories

Validating Accessory Service Configuration in Kamal

This test suite validates the configuration functionality for accessory services in Kamal, focusing on container deployment settings like MySQL, Redis, and monitoring services. It ensures proper handling of service configurations including networking, environment variables, and volume management.

Test Coverage Overview

The test suite provides comprehensive coverage of accessory service configuration in Kamal deployment.

Key areas tested include:
  • Service naming and port mapping
  • Host configuration and validation
  • Environment variable handling and secrets management
  • Volume and directory mounting
  • Network configuration and proxy settings

Implementation Analysis

The testing approach uses ActiveSupport::TestCase with a detailed setup method that creates a mock deployment configuration. Tests utilize assertion-based validation to verify configuration object behavior and error handling.

Key patterns include:
  • Configuration validation testing
  • Error condition verification
  • Dynamic file expansion testing
  • Service integration validation

Technical Details

Testing infrastructure includes:
  • Minitest framework integration
  • Mock configuration data structure
  • File fixture handling
  • Environment variable management
  • Docker-style configuration validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Comprehensive setup and teardown management
  • Isolated test cases for specific functionality
  • Clear error condition handling
  • Thorough validation of configuration options
  • Proper separation of concerns in test organization

basecamp/kamal

test/configuration/accessory_test.rb

            
require "test_helper"

class ConfigurationAccessoryTest < ActiveSupport::TestCase
  setup do
    @deploy = {
      service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
      servers: {
        "web" => [ "1.1.1.1", "1.1.1.2" ],
        "workers" => [ "1.1.1.3", "1.1.1.4" ]
      },
      builder: { "arch" => "amd64" },
      env: { "REDIS_URL" => "redis://x/y" },
      accessories: {
        "mysql" => {
          "image" => "mysql:8.0",
          "host" => "1.1.1.5",
          "port" => "3306",
          "env" => {
            "clear" => {
              "MYSQL_ROOT_HOST" => "%"
            },
            "secret" => [
              "MYSQL_ROOT_PASSWORD"
            ]
          },
          "files" => [
            "config/mysql/my.cnf:/etc/mysql/my.cnf",
            "db/structure.sql:/docker-entrypoint-initdb.d/structure.sql"
          ],
          "directories" => [
            "data:/var/lib/mysql"
          ]
        },
        "redis" => {
          "image" => "redis:latest",
          "hosts" => [ "1.1.1.6", "1.1.1.7" ],
          "port" => "6379:6379",
          "labels" => {
            "cache" => "true"
          },
          "env" => {
            "SOMETHING" => "else"
          },
          "volumes" => [
            "/var/lib/redis:/data"
          ],
          "options" => {
            "cpus" => "4",
            "memory" => "2GB"
          }
        },
        "monitoring" => {
          "service" => "custom-monitoring",
          "image" => "monitoring:latest",
          "roles" => [ "web" ],
          "port" => "4321:4321",
          "labels" => {
            "cache" => "true"
          },
          "env" => {
            "STATSD_PORT" => "8126"
          },
          "options" => {
            "cpus" => "4",
            "memory" => "2GB"
          },
          "proxy" => {
            "host" => "monitoring.example.com"
          }
        }
      }
    }

    @config = Kamal::Configuration.new(@deploy)
  end

  test "service name" do
    assert_equal "app-mysql", @config.accessory(:mysql).service_name
    assert_equal "app-redis", @config.accessory(:redis).service_name
    assert_equal "custom-monitoring", @config.accessory(:monitoring).service_name
  end

  test "port" do
    assert_equal "3306:3306", @config.accessory(:mysql).port
    assert_equal "6379:6379", @config.accessory(:redis).port
  end

  test "host" do
    assert_equal [ "1.1.1.5" ], @config.accessory(:mysql).hosts
    assert_equal [ "1.1.1.6", "1.1.1.7" ], @config.accessory(:redis).hosts
    assert_equal [ "1.1.1.1", "1.1.1.2" ], @config.accessory(:monitoring).hosts
  end

  test "missing host" do
    @deploy[:accessories]["mysql"]["host"] = nil

    assert_raises(Kamal::ConfigurationError) do
      Kamal::Configuration.new(@deploy)
    end
  end

  test "setting host, hosts and roles" do
    @deploy[:accessories]["mysql"]["hosts"] = [ "mysql-db1" ]
    @deploy[:accessories]["mysql"]["roles"] = [ "db" ]

    exception = assert_raises(Kamal::ConfigurationError) do
      Kamal::Configuration.new(@deploy)
    end
    assert_equal "accessories/mysql: specify one of `host`, `hosts` or `roles`", exception.message
  end

  test "all hosts" do
    assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4", "1.1.1.5", "1.1.1.6", "1.1.1.7" ], @config.all_hosts
  end

  test "label args" do
    assert_equal [ "--label", "service=\"app-mysql\"" ], @config.accessory(:mysql).label_args
    assert_equal [ "--label", "service=\"app-redis\"", "--label", "cache=\"true\"" ], @config.accessory(:redis).label_args
  end

  test "env args" do
    with_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123") do
      config = Kamal::Configuration.new(@deploy)

      assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/apps/app/env/accessories/mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s)
      assert_equal "MYSQL_ROOT_PASSWORD=secret123\n", config.accessory(:mysql).secrets_io.string
      assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/apps/app/env/accessories/redis.env" ], @config.accessory(:redis).env_args
      assert_equal "\n", config.accessory(:redis).secrets_io.string
    end
  end

  test "volume args" do
    assert_equal [ "--volume", "$PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf", "--volume", "$PWD/app-mysql/docker-entrypoint-initdb.d/structure.sql:/docker-entrypoint-initdb.d/structure.sql", "--volume", "$PWD/app-mysql/data:/var/lib/mysql" ], @config.accessory(:mysql).volume_args
    assert_equal [ "--volume", "/var/lib/redis:/data" ], @config.accessory(:redis).volume_args
  end

  test "dynamic file expansion" do
    @deploy[:accessories]["mysql"]["files"] << "test/fixtures/files/structure.sql.erb:/docker-entrypoint-initdb.d/structure.sql"
    @config = Kamal::Configuration.new(@deploy)

    assert_match "This was dynamically expanded", @config.accessory(:mysql).files.keys[2].read
    assert_match "%", @config.accessory(:mysql).files.keys[2].read
  end

  test "directory with a relative path" do
    @deploy[:accessories]["mysql"]["directories"] = [ "data:/var/lib/mysql" ]
    assert_equal({ "$PWD/app-mysql/data"=>"/var/lib/mysql" }, @config.accessory(:mysql).directories)
  end

  test "directory with an absolute path" do
    @deploy[:accessories]["mysql"]["directories"] = [ "/var/data/mysql:/var/lib/mysql" ]
    assert_equal({ "/var/data/mysql"=>"/var/lib/mysql" }, @config.accessory(:mysql).directories)
  end

  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

  test "proxy" do
    assert @config.accessory(:monitoring).running_proxy?
    assert_equal [ "monitoring.example.com" ], @config.accessory(:monitoring).proxy.hosts
  end
end