dash 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/dash +18 -0
  5. data/bin/kamal +18 -0
  6. data/lib/kamal/cli/accessory.rb +342 -0
  7. data/lib/kamal/cli/alias/command.rb +10 -0
  8. data/lib/kamal/cli/app/assets.rb +24 -0
  9. data/lib/kamal/cli/app/boot.rb +126 -0
  10. data/lib/kamal/cli/app/error_pages.rb +33 -0
  11. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  12. data/lib/kamal/cli/app.rb +368 -0
  13. data/lib/kamal/cli/base.rb +324 -0
  14. data/lib/kamal/cli/build/clone.rb +59 -0
  15. data/lib/kamal/cli/build/port_forwarding.rb +66 -0
  16. data/lib/kamal/cli/build.rb +242 -0
  17. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  18. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  19. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  20. data/lib/kamal/cli/lock.rb +34 -0
  21. data/lib/kamal/cli/main.rb +299 -0
  22. data/lib/kamal/cli/proxy.rb +419 -0
  23. data/lib/kamal/cli/prune.rb +34 -0
  24. data/lib/kamal/cli/registry.rb +49 -0
  25. data/lib/kamal/cli/secrets.rb +50 -0
  26. data/lib/kamal/cli/server.rb +70 -0
  27. data/lib/kamal/cli/templates/deploy.yml +102 -0
  28. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  31. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  35. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  36. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  37. data/lib/kamal/cli/templates/secrets +22 -0
  38. data/lib/kamal/cli.rb +9 -0
  39. data/lib/kamal/commander/specifics.rb +62 -0
  40. data/lib/kamal/commander.rb +230 -0
  41. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  42. data/lib/kamal/commands/accessory.rb +118 -0
  43. data/lib/kamal/commands/app/assets.rb +51 -0
  44. data/lib/kamal/commands/app/containers.rb +31 -0
  45. data/lib/kamal/commands/app/error_pages.rb +9 -0
  46. data/lib/kamal/commands/app/execution.rb +38 -0
  47. data/lib/kamal/commands/app/images.rb +13 -0
  48. data/lib/kamal/commands/app/logging.rb +28 -0
  49. data/lib/kamal/commands/app/proxy.rb +32 -0
  50. data/lib/kamal/commands/app.rb +125 -0
  51. data/lib/kamal/commands/auditor.rb +39 -0
  52. data/lib/kamal/commands/base.rb +147 -0
  53. data/lib/kamal/commands/builder/base.rb +143 -0
  54. data/lib/kamal/commands/builder/clone.rb +32 -0
  55. data/lib/kamal/commands/builder/cloud.rb +22 -0
  56. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  57. data/lib/kamal/commands/builder/local.rb +20 -0
  58. data/lib/kamal/commands/builder/pack.rb +46 -0
  59. data/lib/kamal/commands/builder/remote.rb +75 -0
  60. data/lib/kamal/commands/builder.rb +54 -0
  61. data/lib/kamal/commands/docker.rb +50 -0
  62. data/lib/kamal/commands/hook.rb +20 -0
  63. data/lib/kamal/commands/loadbalancer.rb +130 -0
  64. data/lib/kamal/commands/lock.rb +70 -0
  65. data/lib/kamal/commands/proxy.rb +150 -0
  66. data/lib/kamal/commands/prune.rb +38 -0
  67. data/lib/kamal/commands/registry.rb +38 -0
  68. data/lib/kamal/commands/server.rb +15 -0
  69. data/lib/kamal/commands.rb +2 -0
  70. data/lib/kamal/configuration/accessory.rb +280 -0
  71. data/lib/kamal/configuration/alias.rb +15 -0
  72. data/lib/kamal/configuration/boot.rb +29 -0
  73. data/lib/kamal/configuration/builder.rb +218 -0
  74. data/lib/kamal/configuration/docs/accessory.yml +160 -0
  75. data/lib/kamal/configuration/docs/alias.yml +29 -0
  76. data/lib/kamal/configuration/docs/boot.yml +21 -0
  77. data/lib/kamal/configuration/docs/builder.yml +132 -0
  78. data/lib/kamal/configuration/docs/configuration.yml +228 -0
  79. data/lib/kamal/configuration/docs/env.yml +118 -0
  80. data/lib/kamal/configuration/docs/logging.yml +21 -0
  81. data/lib/kamal/configuration/docs/output.yml +25 -0
  82. data/lib/kamal/configuration/docs/proxy.yml +207 -0
  83. data/lib/kamal/configuration/docs/registry.yml +64 -0
  84. data/lib/kamal/configuration/docs/role.yml +54 -0
  85. data/lib/kamal/configuration/docs/servers.yml +27 -0
  86. data/lib/kamal/configuration/docs/ssh.yml +81 -0
  87. data/lib/kamal/configuration/docs/sshkit.yml +31 -0
  88. data/lib/kamal/configuration/env/tag.rb +13 -0
  89. data/lib/kamal/configuration/env.rb +42 -0
  90. data/lib/kamal/configuration/loadbalancer.rb +34 -0
  91. data/lib/kamal/configuration/logging.rb +33 -0
  92. data/lib/kamal/configuration/output.rb +34 -0
  93. data/lib/kamal/configuration/proxy/boot.rb +124 -0
  94. data/lib/kamal/configuration/proxy/run.rb +152 -0
  95. data/lib/kamal/configuration/proxy.rb +156 -0
  96. data/lib/kamal/configuration/registry.rb +40 -0
  97. data/lib/kamal/configuration/role.rb +247 -0
  98. data/lib/kamal/configuration/servers.rb +25 -0
  99. data/lib/kamal/configuration/ssh.rb +76 -0
  100. data/lib/kamal/configuration/sshkit.rb +26 -0
  101. data/lib/kamal/configuration/validation.rb +27 -0
  102. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  103. data/lib/kamal/configuration/validator/alias.rb +15 -0
  104. data/lib/kamal/configuration/validator/builder.rb +15 -0
  105. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  106. data/lib/kamal/configuration/validator/env.rb +54 -0
  107. data/lib/kamal/configuration/validator/proxy.rb +47 -0
  108. data/lib/kamal/configuration/validator/registry.rb +27 -0
  109. data/lib/kamal/configuration/validator/role.rb +13 -0
  110. data/lib/kamal/configuration/validator/servers.rb +7 -0
  111. data/lib/kamal/configuration/validator.rb +251 -0
  112. data/lib/kamal/configuration/volume.rb +29 -0
  113. data/lib/kamal/configuration.rb +465 -0
  114. data/lib/kamal/docker.rb +30 -0
  115. data/lib/kamal/env_file.rb +44 -0
  116. data/lib/kamal/git.rb +37 -0
  117. data/lib/kamal/otel_shipper.rb +176 -0
  118. data/lib/kamal/output/base_logger.rb +29 -0
  119. data/lib/kamal/output/file_logger.rb +51 -0
  120. data/lib/kamal/output/formatter.rb +36 -0
  121. data/lib/kamal/output/otel_logger.rb +70 -0
  122. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +59 -0
  123. data/lib/kamal/secrets/adapters/base.rb +33 -0
  124. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  125. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  126. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  127. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  128. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  129. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  130. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  131. data/lib/kamal/secrets/adapters/passbolt.rb +129 -0
  132. data/lib/kamal/secrets/adapters/test.rb +16 -0
  133. data/lib/kamal/secrets/adapters.rb +16 -0
  134. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +47 -0
  135. data/lib/kamal/secrets.rb +53 -0
  136. data/lib/kamal/sshkit_with_ext.rb +273 -0
  137. data/lib/kamal/tags.rb +40 -0
  138. data/lib/kamal/utils/sensitive.rb +20 -0
  139. data/lib/kamal/utils.rb +110 -0
  140. data/lib/kamal/version.rb +3 -0
  141. data/lib/kamal.rb +15 -0
  142. metadata +388 -0
@@ -0,0 +1,102 @@
1
+ # Name of your application. Used to uniquely configure containers.
2
+ service: my-app
3
+
4
+ # Name of the container image.
5
+ image: my-user/my-app
6
+
7
+ # Deploy to these servers.
8
+ servers:
9
+ web:
10
+ - 192.168.0.1
11
+ # job:
12
+ # hosts:
13
+ # - 192.168.0.1
14
+ # cmd: bin/jobs
15
+
16
+ # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
17
+ # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
18
+ #
19
+ # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
20
+ proxy:
21
+ ssl: true
22
+ host: app.example.com
23
+ # Proxy connects to your container on port 80 by default.
24
+ # app_port: 3000
25
+
26
+ # Credentials for your image host.
27
+ registry:
28
+ server: localhost:5555
29
+ # Specify the registry server, if you're not using Docker Hub
30
+ # server: registry.digitalocean.com / ghcr.io / ...
31
+ # username: my-user
32
+
33
+ # Always use an access token rather than real password (pulled from .kamal/secrets).
34
+ # password:
35
+ # - KAMAL_REGISTRY_PASSWORD
36
+
37
+ # Configure builder setup.
38
+ builder:
39
+ arch: amd64
40
+ # Pass in additional build args needed for your Dockerfile.
41
+ # args:
42
+ # RUBY_VERSION: <%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %>
43
+
44
+ # Inject ENV variables into containers (secrets come from .kamal/secrets).
45
+ #
46
+ # env:
47
+ # clear:
48
+ # DB_HOST: 192.168.0.2
49
+ # secret:
50
+ # - RAILS_MASTER_KEY
51
+
52
+ # Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
53
+ # "bin/kamal app logs -r job" will tail logs from the first server in the job section.
54
+ #
55
+ # aliases:
56
+ # shell: app exec --interactive --reuse "bash"
57
+
58
+ # Use a different ssh user than root
59
+ #
60
+ # ssh:
61
+ # user: app
62
+
63
+ # Use a persistent storage volume.
64
+ #
65
+ # volumes:
66
+ # - "app_storage:/app/storage"
67
+
68
+ # Bridge fingerprinted assets, like JS and CSS, between versions to avoid
69
+ # hitting 404 on in-flight requests. Combines all files from new and old
70
+ # version inside the asset_path.
71
+ #
72
+ # asset_path: /app/public/assets
73
+
74
+ # Configure rolling deploys by setting a wait time between batches of restarts.
75
+ #
76
+ # boot:
77
+ # limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
78
+ # wait: 2
79
+
80
+ # Use accessory services (secrets come from .kamal/secrets).
81
+ #
82
+ # accessories:
83
+ # db:
84
+ # image: mysql:8.0
85
+ # host: 192.168.0.2
86
+ # port: 3306
87
+ # env:
88
+ # clear:
89
+ # MYSQL_ROOT_HOST: '%'
90
+ # secret:
91
+ # - MYSQL_ROOT_PASSWORD
92
+ # files:
93
+ # - config/mysql/production.cnf:/etc/mysql/my.cnf
94
+ # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
95
+ # directories:
96
+ # - data:/var/lib/mysql
97
+ # redis:
98
+ # image: valkey/valkey:8
99
+ # host: 192.168.0.2
100
+ # port: 6379
101
+ # directories:
102
+ # - data:/data
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env sh
2
+
3
+ echo "Docker set up on $KAMAL_HOSTS..."
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env sh
2
+
3
+ echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env sh
2
+
3
+ # A sample post-deploy hook
4
+ #
5
+ # These environment variables are available:
6
+ # KAMAL_RECORDED_AT
7
+ # KAMAL_PERFORMER
8
+ # KAMAL_VERSION
9
+ # KAMAL_HOSTS
10
+ # KAMAL_ROLES (if set)
11
+ # KAMAL_DESTINATION (if set)
12
+ # KAMAL_RUNTIME
13
+
14
+ echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env sh
2
+
3
+ echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env sh
2
+
3
+ echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env sh
2
+
3
+ # A sample pre-build hook
4
+ #
5
+ # Checks:
6
+ # 1. We have a clean checkout
7
+ # 2. A remote is configured
8
+ # 3. The branch has been pushed to the remote
9
+ # 4. The version we are deploying matches the remote
10
+ #
11
+ # These environment variables are available:
12
+ # KAMAL_RECORDED_AT
13
+ # KAMAL_PERFORMER
14
+ # KAMAL_VERSION
15
+ # KAMAL_HOSTS
16
+ # KAMAL_ROLES (if set)
17
+ # KAMAL_DESTINATION (if set)
18
+
19
+ if [ -n "$(git status --porcelain)" ]; then
20
+ echo "Git checkout is not clean, aborting..." >&2
21
+ git status --porcelain >&2
22
+ exit 1
23
+ fi
24
+
25
+ first_remote=$(git remote)
26
+
27
+ if [ -z "$first_remote" ]; then
28
+ echo "No git remote set, aborting..." >&2
29
+ exit 1
30
+ fi
31
+
32
+ current_branch=$(git branch --show-current)
33
+
34
+ if [ -z "$current_branch" ]; then
35
+ echo "Not on a git branch, aborting..." >&2
36
+ exit 1
37
+ fi
38
+
39
+ remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
40
+
41
+ if [ -z "$remote_head" ]; then
42
+ echo "Branch not pushed to remote, aborting..." >&2
43
+ exit 1
44
+ fi
45
+
46
+ if [ "$KAMAL_VERSION" != "$remote_head" ]; then
47
+ echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
48
+ exit 1
49
+ fi
50
+
51
+ exit 0
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A sample pre-connect check
4
+ #
5
+ # Warms DNS before connecting to hosts in parallel
6
+ #
7
+ # These environment variables are available:
8
+ # KAMAL_RECORDED_AT
9
+ # KAMAL_PERFORMER
10
+ # KAMAL_VERSION
11
+ # KAMAL_HOSTS
12
+ # KAMAL_ROLES (if set)
13
+ # KAMAL_DESTINATION (if set)
14
+ # KAMAL_RUNTIME
15
+
16
+ hosts = ENV["KAMAL_HOSTS"].split(",")
17
+ results = nil
18
+ max = 3
19
+
20
+ elapsed = Benchmark.realtime do
21
+ results = hosts.map do |host|
22
+ Thread.new do
23
+ tries = 1
24
+
25
+ begin
26
+ Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
27
+ rescue SocketError
28
+ if tries < max
29
+ puts "Retrying DNS warmup: #{host}"
30
+ tries += 1
31
+ sleep rand
32
+ retry
33
+ else
34
+ puts "DNS warmup failed: #{host}"
35
+ host
36
+ end
37
+ end
38
+
39
+ tries
40
+ end
41
+ end.map(&:value)
42
+ end
43
+
44
+ retries = results.sum - hosts.size
45
+ nopes = results.count { |r| r == max }
46
+
47
+ puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A sample pre-deploy hook
4
+ #
5
+ # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
6
+ #
7
+ # Fails unless the combined status is "success"
8
+ #
9
+ # These environment variables are available:
10
+ # KAMAL_RECORDED_AT
11
+ # KAMAL_PERFORMER
12
+ # KAMAL_VERSION
13
+ # KAMAL_HOSTS
14
+ # KAMAL_COMMAND
15
+ # KAMAL_SUBCOMMAND
16
+ # KAMAL_ROLES (if set)
17
+ # KAMAL_DESTINATION (if set)
18
+
19
+ # Only check the build status for production deployments
20
+ if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
21
+ exit 0
22
+ end
23
+
24
+ require "bundler/inline"
25
+
26
+ # true = install gems so this is fast on repeat invocations
27
+ gemfile(true, quiet: true) do
28
+ source "https://rubygems.org"
29
+
30
+ gem "octokit"
31
+ gem "faraday-retry"
32
+ end
33
+
34
+ MAX_ATTEMPTS = 72
35
+ ATTEMPTS_GAP = 10
36
+
37
+ def exit_with_error(message)
38
+ $stderr.puts message
39
+ exit 1
40
+ end
41
+
42
+ class GithubStatusChecks
43
+ attr_reader :remote_url, :git_sha, :github_client, :combined_status
44
+
45
+ def initialize
46
+ @remote_url = github_repo_from_remote_url
47
+ @git_sha = `git rev-parse HEAD`.strip
48
+ @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
49
+ refresh!
50
+ end
51
+
52
+ def refresh!
53
+ @combined_status = github_client.combined_status(remote_url, git_sha)
54
+ end
55
+
56
+ def state
57
+ combined_status[:state]
58
+ end
59
+
60
+ def first_status_url
61
+ first_status = combined_status[:statuses].find { |status| status[:state] == state }
62
+ first_status && first_status[:target_url]
63
+ end
64
+
65
+ def complete_count
66
+ combined_status[:statuses].count { |status| status[:state] != "pending"}
67
+ end
68
+
69
+ def total_count
70
+ combined_status[:statuses].count
71
+ end
72
+
73
+ def current_status
74
+ if total_count > 0
75
+ "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
76
+ else
77
+ "Build not started..."
78
+ end
79
+ end
80
+
81
+ private
82
+ def github_repo_from_remote_url
83
+ url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
84
+ if url.start_with?("https://github.com/")
85
+ url.delete_prefix("https://github.com/")
86
+ elsif url.start_with?("git@github.com:")
87
+ url.delete_prefix("git@github.com:")
88
+ else
89
+ url
90
+ end
91
+ end
92
+ end
93
+
94
+
95
+ $stdout.sync = true
96
+
97
+ begin
98
+ puts "Checking build status..."
99
+
100
+ attempts = 0
101
+ checks = GithubStatusChecks.new
102
+
103
+ loop do
104
+ case checks.state
105
+ when "success"
106
+ puts "Checks passed, see #{checks.first_status_url}"
107
+ exit 0
108
+ when "failure"
109
+ exit_with_error "Checks failed, see #{checks.first_status_url}"
110
+ when "pending"
111
+ attempts += 1
112
+ end
113
+
114
+ exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
115
+
116
+ puts checks.current_status
117
+ sleep(ATTEMPTS_GAP)
118
+ checks.refresh!
119
+ end
120
+ rescue Octokit::NotFound
121
+ exit_with_error "Build status could not be found"
122
+ end
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env sh
2
+
3
+ echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
@@ -0,0 +1,22 @@
1
+ # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
2
+ # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
3
+ # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
4
+ #
5
+ # When deploying with destinations, shared secrets can go in .kamal/secrets-common and
6
+ # destination-specific secrets in .kamal/secrets.<destination>. This .kamal/secrets file is used
7
+ # only when no destination is selected.
8
+
9
+ # Option 1: Read secrets from the environment
10
+ # KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
11
+
12
+ # Option 2: Read secrets via a command
13
+ # RAILS_MASTER_KEY=$(cat config/master.key)
14
+ # KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
15
+
16
+ # Option 3: Read secrets via kamal secrets helpers
17
+ # These will handle logging in and fetching the secrets in as few calls as possible
18
+ # There are adapters for 1Password, LastPass + Bitwarden
19
+ #
20
+ # SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
21
+ # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
22
+ # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
data/lib/kamal/cli.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Kamal::Cli
2
+ class BootError < StandardError; end
3
+ class HookError < StandardError; end
4
+ class LockError < StandardError; end
5
+ class DependencyError < StandardError; end
6
+ end
7
+
8
+ # SSHKit uses instance eval, so we need a global const for ergonomics
9
+ KAMAL = Kamal::Commander.new
@@ -0,0 +1,62 @@
1
+ class Kamal::Commander::Specifics
2
+ attr_reader :primary_host, :primary_role, :hosts, :roles
3
+ delegate :stable_sort!, to: Kamal::Utils
4
+
5
+ def initialize(config, specific_hosts, specific_roles)
6
+ @config, @specific_hosts, @specific_roles = config, specific_hosts, specific_roles
7
+
8
+ @roles, @hosts = specified_roles, specified_hosts
9
+
10
+ @primary_host = specific_hosts&.first || primary_specific_role&.primary_host || config.primary_host
11
+ @primary_role = primary_or_first_role(roles_on(primary_host))
12
+
13
+ stable_sort!(roles) { |role| role == primary_role ? 0 : 1 }
14
+ sort_primary_role_hosts_first!(hosts)
15
+ end
16
+
17
+ def roles_on(host)
18
+ roles.select { |role| role.hosts.include?(host.to_s) }
19
+ end
20
+
21
+ def app_hosts
22
+ @app_hosts ||= sort_primary_role_hosts_first!(config.app_hosts & specified_hosts)
23
+ end
24
+
25
+ def proxy_hosts
26
+ config.proxy_hosts & specified_hosts
27
+ end
28
+
29
+ def accessory_hosts
30
+ config.accessories.flat_map(&:hosts) & specified_hosts
31
+ end
32
+
33
+ private
34
+ attr_reader :config, :specific_hosts, :specific_roles
35
+
36
+ def primary_specific_role
37
+ primary_or_first_role(specific_roles) if specific_roles.present?
38
+ end
39
+
40
+ def primary_or_first_role(roles)
41
+ roles.detect { |role| role == config.primary_role } || roles.first
42
+ end
43
+
44
+ def specified_roles
45
+ (specific_roles || config.roles) \
46
+ .select { |role| ((specific_hosts || config.all_hosts) & role.hosts).any? }
47
+ end
48
+
49
+ def specified_hosts
50
+ specified_hosts = specific_hosts || config.all_hosts
51
+
52
+ if (specific_role_hosts = specific_roles&.flat_map(&:hosts)).present?
53
+ specified_hosts.select { |host| specific_role_hosts.include?(host) }
54
+ else
55
+ specified_hosts
56
+ end
57
+ end
58
+
59
+ def sort_primary_role_hosts_first!(hosts)
60
+ stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
61
+ end
62
+ end
@@ -0,0 +1,230 @@
1
+ require "active_support/core_ext/enumerable"
2
+ require "active_support/core_ext/module/delegation"
3
+ require "active_support/core_ext/object/blank"
4
+ require "active_support/broadcast_logger"
5
+ require "active_support/notifications"
6
+
7
+ class Kamal::Commander
8
+ attr_accessor :verbosity, :holding_lock, :connected, :logging, :lock_wait, :lock_wait_timeout, :lock_wait_interval
9
+ attr_reader :specific_roles, :specific_hosts
10
+ delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics
11
+
12
+ def initialize
13
+ reset
14
+ end
15
+
16
+ def reset
17
+ self.verbosity = :info
18
+ self.holding_lock = ENV["KAMAL_LOCK"] == "true"
19
+ self.connected = false
20
+ self.logging = false
21
+ self.lock_wait = false
22
+ self.lock_wait_timeout = 900
23
+ self.lock_wait_interval = 15
24
+ @modify_depth = 0
25
+ @specifics = @specific_roles = @specific_hosts = nil
26
+ @config = @config_kwargs = nil
27
+ @output_logger = nil
28
+ @commands = {}
29
+ end
30
+
31
+ def config
32
+ @config ||= Kamal::Configuration.create_from(**@config_kwargs.to_h).tap do |config|
33
+ @config_kwargs = nil
34
+ configure_sshkit_with(config)
35
+ end
36
+ end
37
+
38
+ def configure(**kwargs)
39
+ @config, @config_kwargs = nil, kwargs
40
+ end
41
+
42
+ def configured?
43
+ @config || @config_kwargs
44
+ end
45
+
46
+ def specific_primary!
47
+ @specifics = nil
48
+ if specific_roles.present?
49
+ self.specific_hosts = [ specific_roles.first.primary_host ]
50
+ else
51
+ self.specific_hosts = [ config.primary_host ]
52
+ end
53
+ end
54
+
55
+ def specific_roles=(role_names)
56
+ @specifics = nil
57
+ @specific_roles = if role_names.present?
58
+ filtered = Kamal::Utils.filter_specific_items(role_names, config.roles)
59
+ raise ArgumentError, "No --roles match for #{role_names.join(',')}" if filtered.empty?
60
+ filtered
61
+ end
62
+ end
63
+
64
+ def specific_hosts=(hosts)
65
+ @specifics = nil
66
+ @specific_hosts = if hosts.present?
67
+ filtered = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
68
+ raise ArgumentError, "No --hosts match for #{hosts.join(',')}" if filtered.empty?
69
+ filtered
70
+ end
71
+ end
72
+
73
+ def with_specific_hosts(hosts)
74
+ original_hosts, self.specific_hosts = specific_hosts, hosts
75
+ yield
76
+ ensure
77
+ self.specific_hosts = original_hosts
78
+ end
79
+
80
+ def accessory_names
81
+ config.accessories&.collect(&:name) || []
82
+ end
83
+
84
+ def app(role: nil, host: nil)
85
+ Kamal::Commands::App.new(config, role: role, host: host)
86
+ end
87
+
88
+ def accessory(name)
89
+ Kamal::Commands::Accessory.new(config, name: name)
90
+ end
91
+
92
+ def auditor(**details)
93
+ Kamal::Commands::Auditor.new(config, **details)
94
+ end
95
+
96
+ def builder
97
+ @commands[:builder] ||= Kamal::Commands::Builder.new(config)
98
+ end
99
+
100
+ def docker
101
+ @commands[:docker] ||= Kamal::Commands::Docker.new(config)
102
+ end
103
+
104
+ def hook
105
+ @commands[:hook] ||= Kamal::Commands::Hook.new(config)
106
+ end
107
+
108
+ def lock
109
+ @commands[:lock] ||= Kamal::Commands::Lock.new(config)
110
+ end
111
+
112
+ def proxy(host)
113
+ Kamal::Commands::Proxy.new(config, host: host)
114
+ end
115
+
116
+ def loadbalancer_config
117
+ @loadbalancer_config ||= Kamal::Configuration::Loadbalancer.new(config: config, proxy_config: config.proxy.proxy_config, secrets: config.secrets)
118
+ end
119
+
120
+ def loadbalancer
121
+ @commands[:loadbalancer] ||= Kamal::Commands::Loadbalancer.new(config, loadbalancer_config: loadbalancer_config)
122
+ end
123
+
124
+ def prune
125
+ @commands[:prune] ||= Kamal::Commands::Prune.new(config)
126
+ end
127
+
128
+ def registry
129
+ @commands[:registry] ||= Kamal::Commands::Registry.new(config)
130
+ end
131
+
132
+ def server
133
+ @commands[:server] ||= Kamal::Commands::Server.new(config)
134
+ end
135
+
136
+ def alias(name)
137
+ config.aliases[name]
138
+ end
139
+
140
+ def resolve_alias(name)
141
+ if @config
142
+ @config.aliases[name]&.command
143
+ else
144
+ raw_config = Kamal::Configuration.load_raw_config(**@config_kwargs.to_h.slice(:config_file, :destination))
145
+ raw_config[:aliases]&.dig(name)
146
+ end
147
+ end
148
+
149
+ def with_verbosity(level)
150
+ old_level = self.verbosity
151
+
152
+ self.verbosity = level
153
+ SSHKit.config.output_verbosity = level
154
+
155
+ yield
156
+ ensure
157
+ self.verbosity = old_level
158
+ SSHKit.config.output_verbosity = old_level
159
+ end
160
+
161
+ def modify(command:, subcommand:)
162
+ @logging = true
163
+ if modify_started
164
+ ActiveSupport::Notifications.instrument("modify.kamal",
165
+ command: command, subcommand: subcommand, destination: config.destination, hosts: hosts) { yield }
166
+ else
167
+ yield
168
+ end
169
+ ensure
170
+ output_logger.close if modify_finished
171
+ end
172
+
173
+ def log(line)
174
+ output_logger << "#{line}\n" if logging
175
+ end
176
+
177
+ def holding_lock?
178
+ self.holding_lock
179
+ end
180
+
181
+ def connected?
182
+ self.connected
183
+ end
184
+
185
+ private
186
+ def output_logger
187
+ @output_logger ||= ActiveSupport::BroadcastLogger.new
188
+ end
189
+
190
+ def modify_started
191
+ @modify_depth += 1
192
+ @modify_depth == 1
193
+ end
194
+
195
+ def modify_finished
196
+ @modify_depth -= 1
197
+ @modify_depth == 0
198
+ end
199
+
200
+ # Lazy setup of SSHKit
201
+ def configure_sshkit_with(config)
202
+ SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
203
+ SSHKit::Backend::Netssh.configure do |sshkit|
204
+ sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
205
+ sshkit.dns_retries = config.sshkit.dns_retries
206
+ sshkit.ssh_options = config.ssh.options
207
+ end
208
+ SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
209
+ SSHKit.config.output_verbosity = verbosity
210
+
211
+ configure_output_with(config)
212
+ end
213
+
214
+ def configure_output_with(config)
215
+ return unless config.output.enabled?
216
+
217
+ config.output.loggers.each { |logger| output_logger.broadcast_to(logger) }
218
+
219
+ SSHKit.config.output = Kamal::Output::Formatter.new($stdout, output_logger)
220
+
221
+ at_exit { @output_logger&.close }
222
+ rescue => e
223
+ $stderr.puts "Output logger setup failed: #{e.class}: #{e.message}"
224
+ $stderr.puts e.backtrace.join("\n") if ENV["VERBOSE"]
225
+ end
226
+
227
+ def specifics
228
+ @specifics ||= Kamal::Commander::Specifics.new(config, specific_hosts, specific_roles)
229
+ end
230
+ end