Skip to content

Commit

Permalink
Merge pull request #828 from basecamp/configuration-validation
Browse files Browse the repository at this point in the history
Configuration validation
  • Loading branch information
djmb authored Jun 18, 2024
2 parents 0a6b0b7 + 6bf3f48 commit da599d9
Show file tree
Hide file tree
Showing 59 changed files with 1,939 additions and 475 deletions.
134 changes: 134 additions & 0 deletions bin/docs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env ruby
require "stringio"

def usage
puts "Usage: #{$0} <kamal_site_repo>"
exit 1
end

usage if ARGV.size != 1

kamal_site_repo = ARGV[0]

if !File.directory?(kamal_site_repo)
puts "Error: #{kamal_site_repo} is not a directory"
exit 1
end

DOCS = {
"accessory" => "Accessories",
"boot" => "Booting",
"builder" => "Builders",
"configuration" => "Configuration overview",
"env" => "Environment variables",
"healthcheck" => "Healthchecks",
"logging" => "Logging",
"registry" => "Docker Registry",
"role" => "Roles",
"servers" => "Servers",
"ssh" => "SSH",
"sshkit" => "SSHKit",
"traefik" => "Traefik"
}

class DocWriter
attr_reader :from_file, :to_file, :key, :heading, :body, :output, :in_yaml

def initialize(from_file, to_dir)
@from_file = from_file
@key = File.basename(from_file, ".yml")
@to_file = File.join(to_dir, "#{linkify(DOCS[key])}.md")
@body = File.readlines(from_file)
@heading = body.shift.chomp("\n")
@output = nil
end

def write
puts "Writing #{to_file}"
generate_markdown
File.write(to_file, output.string)
end

private
def generate_markdown
@output = StringIO.new

generate_header

place = :in_section

loop do
line = body.shift&.chomp("\n")
break if line.nil?

case place
when :new_section, :in_section
if line.empty?
output.puts
place = :new_section
elsif line =~ /^ *#/
generate_line(line, place: place)
place = :in_section
else
output.puts "```yaml"
output.print line
place = :in_yaml
end
when :in_yaml
if line =~ /^ *#/
output.puts "```"
generate_line(line, place: :new_section)
place = :in_section
else
output.puts
output.print line
end
end
end

output.puts "\n```" if place == :in_yaml
end

def generate_header
output.puts "---"
output.puts "title: #{heading[2..-1]}"
output.puts "---"
output.puts
output.puts heading
output.puts
end

def generate_line(line, place: :in_section)
line = line.gsub(/^ *#\s?/, "")

if line =~ /(.*)kamal docs ([a-z]*)(.*)/
line = "#{$1}[#{DOCS[$2]}](../#{linkify(DOCS[$2])})#{$3}"
end

if line =~ /(.*)https:\/\/kamal-deploy.org([a-z\/-]*)(.*)/
line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}"
end

if place == :new_section
output.puts "## [#{line}](##{linkify(line)})"
else
output.puts line
end
end

def linkify(text)
text.downcase.gsub(" ", "-")
end

def titlify(text)
text.capitalize.gsub("-", " ")
end
end

from_dir = File.join(File.dirname(__FILE__), "../lib/kamal/configuration/docs")
to_dir = File.join(kamal_site_repo, "docs/configuration")
Dir.glob("#{from_dir}/*") do |from_file|
key = File.basename(from_file, ".yml")

DocWriter.new(from_file, to_dir).write
end
2 changes: 2 additions & 0 deletions lib/kamal.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
module Kamal
class ConfigurationError < StandardError; end
end

require "active_support"
require "zeitwerk"
require "yaml"

loader = Zeitwerk::Loader.for_gem
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/cli.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Kamal::Cli
class LockError < StandardError; end
class HookError < StandardError; end
class LockError < StandardError; end
end

# SSHKit uses instance eval, so we need a global const for ergonomics
Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/cli/healthcheck/poller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Kamal::Cli::Healthcheck::Poller

def wait_for_healthy(pause_after_ready: false, &block)
attempt = 1
max_attempts = KAMAL.config.healthcheck["max_attempts"]
max_attempts = KAMAL.config.healthcheck.max_attempts

begin
case status = block.call
Expand All @@ -33,7 +33,7 @@ def wait_for_healthy(pause_after_ready: false, &block)

def wait_for_unhealthy(pause_after_ready: false, &block)
attempt = 1
max_attempts = KAMAL.config.healthcheck["max_attempts"]
max_attempts = KAMAL.config.healthcheck.max_attempts

begin
case status = block.call
Expand Down
12 changes: 12 additions & 0 deletions lib/kamal/cli/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@ def config
end
end

desc "docs", "Show Kamal documentation for configuration setting"
def docs(section = nil)
case section
when NilClass
puts Kamal::Configuration.validation_doc
else
puts Kamal::Configuration.const_get(section.titlecase.to_sym).validation_doc
end
rescue NameError
puts "No documentation found for #{section}"
end

desc "init", "Create config stub in config/deploy.yml and env stub in .env"
option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
def init
Expand Down
25 changes: 15 additions & 10 deletions lib/kamal/commands/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ def name
end

def target
case
when !config.builder.multiarch? && !config.builder.cached?
native
when !config.builder.multiarch? && config.builder.cached?
native_cached
when config.builder.local? && config.builder.remote?
multiarch_remote
when config.builder.remote?
native_remote
if config.builder.multiarch?
if config.builder.remote?
if config.builder.local?
multiarch_remote
else
native_remote
end
else
multiarch
end
else
multiarch
if config.builder.cached?
native_cached
else
native
end
end
end

Expand Down
17 changes: 4 additions & 13 deletions lib/kamal/commands/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,12 @@ class Kamal::Commands::Registry < Kamal::Commands::Base

def login
docker :login,
registry["server"],
"-u", sensitive(Kamal::Utils.escape_shell_value(lookup("username"))),
"-p", sensitive(Kamal::Utils.escape_shell_value(lookup("password")))
registry.server,
"-u", sensitive(Kamal::Utils.escape_shell_value(registry.username)),
"-p", sensitive(Kamal::Utils.escape_shell_value(registry.password))
end

def logout
docker :logout, registry["server"]
docker :logout, registry.server
end

private
def lookup(key)
if registry[key].is_a?(Array)
ENV.fetch(registry[key].first).dup
else
registry[key]
end
end
end
47 changes: 4 additions & 43 deletions lib/kamal/commands/traefik.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
class Kamal::Commands::Traefik < Kamal::Commands::Base
delegate :argumentize, :optionize, to: Kamal::Utils

DEFAULT_IMAGE = "traefik:v2.10"
CONTAINER_PORT = 80
DEFAULT_ARGS = {
"log.level" => "DEBUG"
}
DEFAULT_LABELS = {
# These ensure we serve a 502 rather than a 404 if no containers are available
"traefik.http.routers.catchall.entryPoints" => "http",
"traefik.http.routers.catchall.rule" => "PathPrefix(`/`)",
"traefik.http.routers.catchall.service" => "unavailable",
"traefik.http.routers.catchall.priority" => 1,
"traefik.http.services.unavailable.loadbalancer.server.port" => "0"
}
delegate :port, :publish?, :labels, :env, :image, :options, :args, to: :"config.traefik"

def run
docker :run, "--name traefik",
Expand Down Expand Up @@ -67,16 +54,6 @@ def remove_image
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
end

def port
"#{host_port}:#{CONTAINER_PORT}"
end

def env
Kamal::Configuration::Env.from_config \
config: config.traefik.fetch("env", {}),
secrets_file: File.join(config.host_env_directory, "traefik", "traefik.env")
end

def make_env_directory
make_directory(env.secrets_directory)
end
Expand All @@ -87,7 +64,7 @@ def remove_env_file

private
def publish_args
argumentize "--publish", port unless config.traefik["publish"] == false
argumentize "--publish", port if publish?
end

def label_args
Expand All @@ -98,27 +75,11 @@ def env_args
env.args
end

def labels
DEFAULT_LABELS.merge(config.traefik["labels"] || {})
end

def image
config.traefik.fetch("image") { DEFAULT_IMAGE }
end

def docker_options_args
optionize(config.traefik["options"] || {})
optionize(options)
end

def cmd_option_args
if args = config.traefik["args"]
optionize DEFAULT_ARGS.merge(args), with: "="
else
optionize DEFAULT_ARGS, with: "="
end
end

def host_port
config.traefik["host_port"] || CONTAINER_PORT
optionize args, with: "="
end
end
Loading

0 comments on commit da599d9

Please sign in to comment.