kamal-insecure 2.7.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 (130) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/kamal +18 -0
  5. data/lib/kamal/cli/accessory.rb +313 -0
  6. data/lib/kamal/cli/alias/command.rb +10 -0
  7. data/lib/kamal/cli/app/assets.rb +24 -0
  8. data/lib/kamal/cli/app/boot.rb +126 -0
  9. data/lib/kamal/cli/app/error_pages.rb +33 -0
  10. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  11. data/lib/kamal/cli/app.rb +400 -0
  12. data/lib/kamal/cli/base.rb +223 -0
  13. data/lib/kamal/cli/build/clone.rb +61 -0
  14. data/lib/kamal/cli/build.rb +204 -0
  15. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  16. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  17. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  18. data/lib/kamal/cli/lock.rb +45 -0
  19. data/lib/kamal/cli/main.rb +277 -0
  20. data/lib/kamal/cli/proxy.rb +290 -0
  21. data/lib/kamal/cli/prune.rb +34 -0
  22. data/lib/kamal/cli/registry.rb +19 -0
  23. data/lib/kamal/cli/secrets.rb +49 -0
  24. data/lib/kamal/cli/server.rb +50 -0
  25. data/lib/kamal/cli/templates/deploy.yml +101 -0
  26. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  28. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  31. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  35. data/lib/kamal/cli/templates/secrets +17 -0
  36. data/lib/kamal/cli.rb +9 -0
  37. data/lib/kamal/commander/specifics.rb +62 -0
  38. data/lib/kamal/commander.rb +167 -0
  39. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  40. data/lib/kamal/commands/accessory.rb +113 -0
  41. data/lib/kamal/commands/app/assets.rb +51 -0
  42. data/lib/kamal/commands/app/containers.rb +31 -0
  43. data/lib/kamal/commands/app/error_pages.rb +9 -0
  44. data/lib/kamal/commands/app/execution.rb +32 -0
  45. data/lib/kamal/commands/app/images.rb +13 -0
  46. data/lib/kamal/commands/app/logging.rb +28 -0
  47. data/lib/kamal/commands/app/proxy.rb +32 -0
  48. data/lib/kamal/commands/app.rb +124 -0
  49. data/lib/kamal/commands/auditor.rb +39 -0
  50. data/lib/kamal/commands/base.rb +134 -0
  51. data/lib/kamal/commands/builder/base.rb +124 -0
  52. data/lib/kamal/commands/builder/clone.rb +31 -0
  53. data/lib/kamal/commands/builder/cloud.rb +22 -0
  54. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  55. data/lib/kamal/commands/builder/local.rb +14 -0
  56. data/lib/kamal/commands/builder/pack.rb +46 -0
  57. data/lib/kamal/commands/builder/remote.rb +63 -0
  58. data/lib/kamal/commands/builder.rb +48 -0
  59. data/lib/kamal/commands/docker.rb +34 -0
  60. data/lib/kamal/commands/hook.rb +20 -0
  61. data/lib/kamal/commands/lock.rb +70 -0
  62. data/lib/kamal/commands/proxy.rb +127 -0
  63. data/lib/kamal/commands/prune.rb +38 -0
  64. data/lib/kamal/commands/registry.rb +16 -0
  65. data/lib/kamal/commands/server.rb +15 -0
  66. data/lib/kamal/commands.rb +2 -0
  67. data/lib/kamal/configuration/accessory.rb +241 -0
  68. data/lib/kamal/configuration/alias.rb +15 -0
  69. data/lib/kamal/configuration/boot.rb +25 -0
  70. data/lib/kamal/configuration/builder.rb +211 -0
  71. data/lib/kamal/configuration/docs/accessory.yml +128 -0
  72. data/lib/kamal/configuration/docs/alias.yml +26 -0
  73. data/lib/kamal/configuration/docs/boot.yml +19 -0
  74. data/lib/kamal/configuration/docs/builder.yml +132 -0
  75. data/lib/kamal/configuration/docs/configuration.yml +184 -0
  76. data/lib/kamal/configuration/docs/env.yml +116 -0
  77. data/lib/kamal/configuration/docs/logging.yml +21 -0
  78. data/lib/kamal/configuration/docs/proxy.yml +164 -0
  79. data/lib/kamal/configuration/docs/registry.yml +56 -0
  80. data/lib/kamal/configuration/docs/role.yml +53 -0
  81. data/lib/kamal/configuration/docs/servers.yml +27 -0
  82. data/lib/kamal/configuration/docs/ssh.yml +70 -0
  83. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  84. data/lib/kamal/configuration/env/tag.rb +13 -0
  85. data/lib/kamal/configuration/env.rb +38 -0
  86. data/lib/kamal/configuration/logging.rb +33 -0
  87. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  88. data/lib/kamal/configuration/proxy.rb +124 -0
  89. data/lib/kamal/configuration/registry.rb +32 -0
  90. data/lib/kamal/configuration/role.rb +222 -0
  91. data/lib/kamal/configuration/servers.rb +25 -0
  92. data/lib/kamal/configuration/ssh.rb +57 -0
  93. data/lib/kamal/configuration/sshkit.rb +22 -0
  94. data/lib/kamal/configuration/validation.rb +27 -0
  95. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  96. data/lib/kamal/configuration/validator/alias.rb +15 -0
  97. data/lib/kamal/configuration/validator/builder.rb +15 -0
  98. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  99. data/lib/kamal/configuration/validator/env.rb +54 -0
  100. data/lib/kamal/configuration/validator/proxy.rb +25 -0
  101. data/lib/kamal/configuration/validator/registry.rb +25 -0
  102. data/lib/kamal/configuration/validator/role.rb +13 -0
  103. data/lib/kamal/configuration/validator/servers.rb +7 -0
  104. data/lib/kamal/configuration/validator.rb +191 -0
  105. data/lib/kamal/configuration/volume.rb +22 -0
  106. data/lib/kamal/configuration.rb +372 -0
  107. data/lib/kamal/docker.rb +30 -0
  108. data/lib/kamal/env_file.rb +44 -0
  109. data/lib/kamal/git.rb +37 -0
  110. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  111. data/lib/kamal/secrets/adapters/base.rb +33 -0
  112. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  113. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  114. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  115. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  116. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  117. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  118. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  119. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  120. data/lib/kamal/secrets/adapters/test.rb +14 -0
  121. data/lib/kamal/secrets/adapters.rb +16 -0
  122. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
  123. data/lib/kamal/secrets.rb +42 -0
  124. data/lib/kamal/sshkit_with_ext.rb +142 -0
  125. data/lib/kamal/tags.rb +40 -0
  126. data/lib/kamal/utils/sensitive.rb +20 -0
  127. data/lib/kamal/utils.rb +110 -0
  128. data/lib/kamal/version.rb +3 -0
  129. data/lib/kamal.rb +14 -0
  130. metadata +365 -0
@@ -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
+ #!/bin/sh
2
+
3
+ echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
@@ -0,0 +1,17 @@
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
+ # Option 1: Read secrets from the environment
6
+ KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
7
+
8
+ # Option 2: Read secrets via a command
9
+ # RAILS_MASTER_KEY=$(cat config/master.key)
10
+
11
+ # Option 3: Read secrets via kamal secrets helpers
12
+ # These will handle logging in and fetching the secrets in as few calls as possible
13
+ # There are adapters for 1Password, LastPass + Bitwarden
14
+ #
15
+ # SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
16
+ # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
17
+ # 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,167 @@
1
+ require "active_support/core_ext/enumerable"
2
+ require "active_support/core_ext/module/delegation"
3
+ require "active_support/core_ext/object/blank"
4
+
5
+ class Kamal::Commander
6
+ attr_accessor :verbosity, :holding_lock, :connected
7
+ attr_reader :specific_roles, :specific_hosts
8
+ delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics
9
+
10
+ def initialize
11
+ reset
12
+ end
13
+
14
+ def reset
15
+ self.verbosity = :info
16
+ self.holding_lock = ENV["KAMAL_LOCK"] == "true"
17
+ self.connected = false
18
+ @specifics = @specific_roles = @specific_hosts = nil
19
+ @config = @config_kwargs = nil
20
+ @commands = {}
21
+ end
22
+
23
+ def config
24
+ @config ||= Kamal::Configuration.create_from(**@config_kwargs).tap do |config|
25
+ @config_kwargs = nil
26
+ configure_sshkit_with(config)
27
+ end
28
+ end
29
+
30
+ def configure(**kwargs)
31
+ @config, @config_kwargs = nil, kwargs
32
+ end
33
+
34
+ def configured?
35
+ @config || @config_kwargs
36
+ end
37
+
38
+ def specific_primary!
39
+ @specifics = nil
40
+ if specific_roles.present?
41
+ self.specific_hosts = [ specific_roles.first.primary_host ]
42
+ else
43
+ self.specific_hosts = [ config.primary_host ]
44
+ end
45
+ end
46
+
47
+ def specific_roles=(role_names)
48
+ @specifics = nil
49
+ if role_names.present?
50
+ @specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles)
51
+
52
+ if @specific_roles.empty?
53
+ raise ArgumentError, "No --roles match for #{role_names.join(',')}"
54
+ end
55
+
56
+ @specific_roles
57
+ end
58
+ end
59
+
60
+ def specific_hosts=(hosts)
61
+ @specifics = nil
62
+ if hosts.present?
63
+ @specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
64
+
65
+ if @specific_hosts.empty?
66
+ raise ArgumentError, "No --hosts match for #{hosts.join(',')}"
67
+ end
68
+
69
+ @specific_hosts
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
113
+ @commands[:proxy] ||= Kamal::Commands::Proxy.new(config)
114
+ end
115
+
116
+ def prune
117
+ @commands[:prune] ||= Kamal::Commands::Prune.new(config)
118
+ end
119
+
120
+ def registry
121
+ @commands[:registry] ||= Kamal::Commands::Registry.new(config)
122
+ end
123
+
124
+ def server
125
+ @commands[:server] ||= Kamal::Commands::Server.new(config)
126
+ end
127
+
128
+ def alias(name)
129
+ config.aliases[name]
130
+ end
131
+
132
+ def with_verbosity(level)
133
+ old_level = self.verbosity
134
+
135
+ self.verbosity = level
136
+ SSHKit.config.output_verbosity = level
137
+
138
+ yield
139
+ ensure
140
+ self.verbosity = old_level
141
+ SSHKit.config.output_verbosity = old_level
142
+ end
143
+
144
+ def holding_lock?
145
+ self.holding_lock
146
+ end
147
+
148
+ def connected?
149
+ self.connected
150
+ end
151
+
152
+ private
153
+ # Lazy setup of SSHKit
154
+ def configure_sshkit_with(config)
155
+ SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
156
+ SSHKit::Backend::Netssh.configure do |sshkit|
157
+ sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
158
+ sshkit.ssh_options = config.ssh.options
159
+ end
160
+ SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
161
+ SSHKit.config.output_verbosity = verbosity
162
+ end
163
+
164
+ def specifics
165
+ @specifics ||= Kamal::Commander::Specifics.new(config, specific_hosts, specific_roles)
166
+ end
167
+ end
@@ -0,0 +1,16 @@
1
+ module Kamal::Commands::Accessory::Proxy
2
+ delegate :container_name, to: :"config.proxy_boot", prefix: :proxy
3
+
4
+ def deploy(target:)
5
+ proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
6
+ end
7
+
8
+ def remove
9
+ proxy_exec :remove, service_name
10
+ end
11
+
12
+ private
13
+ def proxy_exec(*command)
14
+ docker :exec, proxy_container_name, "kamal-proxy", *command
15
+ end
16
+ end
@@ -0,0 +1,113 @@
1
+ class Kamal::Commands::Accessory < Kamal::Commands::Base
2
+ include Proxy
3
+
4
+ attr_reader :accessory_config
5
+ delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
6
+ :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
7
+ :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry,
8
+ to: :accessory_config
9
+
10
+ def initialize(config, name:)
11
+ super(config)
12
+ @accessory_config = config.accessory(name)
13
+ end
14
+
15
+ def run(host: nil)
16
+ docker :run,
17
+ "--name", service_name,
18
+ "--detach",
19
+ "--restart", "unless-stopped",
20
+ *network_args,
21
+ *config.logging_args,
22
+ *publish_args,
23
+ *([ "--env", "KAMAL_HOST=\"#{host}\"" ] if host),
24
+ *env_args,
25
+ *volume_args,
26
+ *label_args,
27
+ *option_args,
28
+ image,
29
+ cmd
30
+ end
31
+
32
+ def start
33
+ docker :container, :start, service_name
34
+ end
35
+
36
+ def stop
37
+ docker :container, :stop, service_name
38
+ end
39
+
40
+ def info(all: false, quiet: false)
41
+ docker :ps, *("-a" if all), *("-q" if quiet), *service_filter
42
+ end
43
+
44
+ def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
45
+ pipe \
46
+ docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
47
+ ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
48
+ end
49
+
50
+ def follow_logs(timestamps: true, grep: nil, grep_options: nil)
51
+ run_over_ssh \
52
+ pipe \
53
+ docker(:logs, service_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
54
+ (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
55
+ end
56
+
57
+ def execute_in_existing_container(*command, interactive: false)
58
+ docker :exec,
59
+ (docker_interactive_args if interactive),
60
+ service_name,
61
+ *command
62
+ end
63
+
64
+ def execute_in_new_container(*command, interactive: false)
65
+ docker :run,
66
+ (docker_interactive_args if interactive),
67
+ "--rm",
68
+ *network_args,
69
+ *env_args,
70
+ *volume_args,
71
+ image,
72
+ *command
73
+ end
74
+
75
+ def execute_in_existing_container_over_ssh(*command)
76
+ run_over_ssh execute_in_existing_container(*command, interactive: true)
77
+ end
78
+
79
+ def execute_in_new_container_over_ssh(*command)
80
+ run_over_ssh execute_in_new_container(*command, interactive: true)
81
+ end
82
+
83
+ def run_over_ssh(command)
84
+ super command, host: hosts.first
85
+ end
86
+
87
+ def ensure_local_file_present(local_file)
88
+ if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?
89
+ raise "Missing file: #{local_file}"
90
+ end
91
+ end
92
+
93
+ def remove_service_directory
94
+ [ :rm, "-rf", service_name ]
95
+ end
96
+
97
+ def remove_container
98
+ docker :container, :prune, "--force", *service_filter
99
+ end
100
+
101
+ def remove_image
102
+ docker :image, :rm, "--force", image
103
+ end
104
+
105
+ def ensure_env_directory
106
+ make_directory env_directory
107
+ end
108
+
109
+ private
110
+ def service_filter
111
+ [ "--filter", "label=service=#{service_name}" ]
112
+ end
113
+ end
@@ -0,0 +1,51 @@
1
+ module Kamal::Commands::App::Assets
2
+ def extract_assets
3
+ asset_container = "#{role.container_prefix}-assets"
4
+
5
+ combine \
6
+ make_directory(role.asset_extracted_directory),
7
+ [ *docker(:container, :rm, asset_container, "2> /dev/null"), "|| true" ],
8
+ docker(:container, :create, "--name", asset_container, config.absolute_image),
9
+ docker(:container, :cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory),
10
+ docker(:container, :rm, asset_container),
11
+ by: "&&"
12
+ end
13
+
14
+ def sync_asset_volumes(old_version: nil)
15
+ new_extracted_path, new_volume_path = role.asset_extracted_directory(config.version), role.asset_volume.host_path
16
+ if old_version.present?
17
+ old_extracted_path, old_volume_path = role.asset_extracted_directory(old_version), role.asset_volume(old_version).host_path
18
+ end
19
+
20
+ commands = [ make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path) ]
21
+
22
+ if old_version.present?
23
+ commands << copy_contents(new_extracted_path, old_volume_path, continue_on_error: true)
24
+ commands << copy_contents(old_extracted_path, new_volume_path, continue_on_error: true)
25
+ end
26
+
27
+ chain *commands
28
+ end
29
+
30
+ def clean_up_assets
31
+ chain \
32
+ find_and_remove_older_siblings(role.asset_extracted_directory),
33
+ find_and_remove_older_siblings(role.asset_volume_directory)
34
+ end
35
+
36
+ private
37
+ def find_and_remove_older_siblings(path)
38
+ [
39
+ :find,
40
+ Pathname.new(path).dirname.to_s,
41
+ "-maxdepth 1",
42
+ "-name", "'#{role.name}-*'",
43
+ "!", "-name", Pathname.new(path).basename.to_s,
44
+ "-exec rm -rf \"{}\" +"
45
+ ]
46
+ end
47
+
48
+ def copy_contents(source, destination, continue_on_error: false)
49
+ [ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error) ]
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ module Kamal::Commands::App::Containers
2
+ DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
3
+
4
+ def list_containers
5
+ docker :container, :ls, "--all", *container_filter_args
6
+ end
7
+
8
+ def list_container_names
9
+ [ *list_containers, "--format", "'{{ .Names }}'" ]
10
+ end
11
+
12
+ def remove_container(version:)
13
+ pipe \
14
+ container_id_for(container_name: container_name(version)),
15
+ xargs(docker(:container, :rm))
16
+ end
17
+
18
+ def rename_container(version:, new_version:)
19
+ docker :rename, container_name(version), container_name(new_version)
20
+ end
21
+
22
+ def remove_containers
23
+ docker :container, :prune, "--force", *container_filter_args
24
+ end
25
+
26
+ def container_health_log(version:)
27
+ pipe \
28
+ container_id_for(container_name: container_name(version)),
29
+ xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ module Kamal::Commands::App::ErrorPages
2
+ def create_error_pages_directory
3
+ make_directory(config.proxy_boot.error_pages_directory)
4
+ end
5
+
6
+ def clean_up_error_pages
7
+ [ :find, config.proxy_boot.error_pages_directory, "-mindepth", "1", "-maxdepth", "1", "!", "-name", KAMAL.config.version, "-exec", "rm", "-rf", "{} +" ]
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ module Kamal::Commands::App::Execution
2
+ def execute_in_existing_container(*command, interactive: false, env:)
3
+ docker :exec,
4
+ (docker_interactive_args if interactive),
5
+ *argumentize("--env", env),
6
+ container_name,
7
+ *command
8
+ end
9
+
10
+ def execute_in_new_container(*command, interactive: false, detach: false, env:)
11
+ docker :run,
12
+ (docker_interactive_args if interactive),
13
+ ("--detach" if detach),
14
+ ("--rm" unless detach),
15
+ "--network", "kamal",
16
+ *role&.env_args(host),
17
+ *argumentize("--env", env),
18
+ *role.logging_args,
19
+ *config.volume_args,
20
+ *role&.option_args,
21
+ config.absolute_image,
22
+ *command
23
+ end
24
+
25
+ def execute_in_existing_container_over_ssh(*command, env:)
26
+ run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
27
+ end
28
+
29
+ def execute_in_new_container_over_ssh(*command, env:)
30
+ run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ module Kamal::Commands::App::Images
2
+ def list_images
3
+ docker :image, :ls, config.repository
4
+ end
5
+
6
+ def remove_images
7
+ docker :image, :prune, "--all", "--force", *image_filter_args
8
+ end
9
+
10
+ def tag_latest_image
11
+ docker :tag, config.absolute_image, config.latest_image
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module Kamal::Commands::App::Logging
2
+ def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
3
+ pipe \
4
+ container_id_command(container_id),
5
+ "xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
6
+ ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
7
+ end
8
+
9
+ def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil)
10
+ run_over_ssh \
11
+ pipe(
12
+ container_id_command(container_id),
13
+ "xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
14
+ (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
15
+ ),
16
+ host: host
17
+ end
18
+
19
+ private
20
+
21
+ def container_id_command(container_id)
22
+ case container_id
23
+ when Array then container_id
24
+ when String, Symbol then "echo #{container_id}"
25
+ else current_running_container_id
26
+ end
27
+ end
28
+ end