nocoffee-kamal 2.3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) 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 +287 -0
  6. data/lib/kamal/cli/alias/command.rb +9 -0
  7. data/lib/kamal/cli/app/boot.rb +125 -0
  8. data/lib/kamal/cli/app/prepare_assets.rb +24 -0
  9. data/lib/kamal/cli/app.rb +335 -0
  10. data/lib/kamal/cli/base.rb +198 -0
  11. data/lib/kamal/cli/build/clone.rb +61 -0
  12. data/lib/kamal/cli/build.rb +162 -0
  13. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  14. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  15. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  16. data/lib/kamal/cli/lock.rb +45 -0
  17. data/lib/kamal/cli/main.rb +279 -0
  18. data/lib/kamal/cli/proxy.rb +257 -0
  19. data/lib/kamal/cli/prune.rb +34 -0
  20. data/lib/kamal/cli/registry.rb +17 -0
  21. data/lib/kamal/cli/secrets.rb +43 -0
  22. data/lib/kamal/cli/server.rb +48 -0
  23. data/lib/kamal/cli/templates/deploy.yml +98 -0
  24. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  25. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  26. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  28. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  29. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
  30. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  31. data/lib/kamal/cli/templates/secrets +17 -0
  32. data/lib/kamal/cli.rb +8 -0
  33. data/lib/kamal/commander/specifics.rb +54 -0
  34. data/lib/kamal/commander.rb +176 -0
  35. data/lib/kamal/commands/accessory.rb +113 -0
  36. data/lib/kamal/commands/app/assets.rb +51 -0
  37. data/lib/kamal/commands/app/containers.rb +31 -0
  38. data/lib/kamal/commands/app/execution.rb +30 -0
  39. data/lib/kamal/commands/app/images.rb +13 -0
  40. data/lib/kamal/commands/app/logging.rb +18 -0
  41. data/lib/kamal/commands/app/proxy.rb +16 -0
  42. data/lib/kamal/commands/app.rb +115 -0
  43. data/lib/kamal/commands/auditor.rb +33 -0
  44. data/lib/kamal/commands/base.rb +98 -0
  45. data/lib/kamal/commands/builder/base.rb +111 -0
  46. data/lib/kamal/commands/builder/clone.rb +31 -0
  47. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  48. data/lib/kamal/commands/builder/local.rb +14 -0
  49. data/lib/kamal/commands/builder/remote.rb +63 -0
  50. data/lib/kamal/commands/builder.rb +56 -0
  51. data/lib/kamal/commands/docker.rb +34 -0
  52. data/lib/kamal/commands/hook.rb +20 -0
  53. data/lib/kamal/commands/lock.rb +70 -0
  54. data/lib/kamal/commands/proxy.rb +87 -0
  55. data/lib/kamal/commands/prune.rb +38 -0
  56. data/lib/kamal/commands/registry.rb +14 -0
  57. data/lib/kamal/commands/server.rb +15 -0
  58. data/lib/kamal/commands.rb +2 -0
  59. data/lib/kamal/configuration/accessory.rb +186 -0
  60. data/lib/kamal/configuration/alias.rb +15 -0
  61. data/lib/kamal/configuration/boot.rb +25 -0
  62. data/lib/kamal/configuration/builder.rb +191 -0
  63. data/lib/kamal/configuration/docs/accessory.yml +100 -0
  64. data/lib/kamal/configuration/docs/alias.yml +26 -0
  65. data/lib/kamal/configuration/docs/boot.yml +19 -0
  66. data/lib/kamal/configuration/docs/builder.yml +110 -0
  67. data/lib/kamal/configuration/docs/configuration.yml +178 -0
  68. data/lib/kamal/configuration/docs/env.yml +85 -0
  69. data/lib/kamal/configuration/docs/logging.yml +21 -0
  70. data/lib/kamal/configuration/docs/proxy.yml +110 -0
  71. data/lib/kamal/configuration/docs/registry.yml +52 -0
  72. data/lib/kamal/configuration/docs/role.yml +53 -0
  73. data/lib/kamal/configuration/docs/servers.yml +27 -0
  74. data/lib/kamal/configuration/docs/ssh.yml +70 -0
  75. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  76. data/lib/kamal/configuration/env/tag.rb +13 -0
  77. data/lib/kamal/configuration/env.rb +29 -0
  78. data/lib/kamal/configuration/logging.rb +33 -0
  79. data/lib/kamal/configuration/proxy.rb +63 -0
  80. data/lib/kamal/configuration/registry.rb +32 -0
  81. data/lib/kamal/configuration/role.rb +220 -0
  82. data/lib/kamal/configuration/servers.rb +18 -0
  83. data/lib/kamal/configuration/ssh.rb +57 -0
  84. data/lib/kamal/configuration/sshkit.rb +22 -0
  85. data/lib/kamal/configuration/validation.rb +27 -0
  86. data/lib/kamal/configuration/validator/accessory.rb +9 -0
  87. data/lib/kamal/configuration/validator/alias.rb +15 -0
  88. data/lib/kamal/configuration/validator/builder.rb +13 -0
  89. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  90. data/lib/kamal/configuration/validator/env.rb +54 -0
  91. data/lib/kamal/configuration/validator/proxy.rb +15 -0
  92. data/lib/kamal/configuration/validator/registry.rb +25 -0
  93. data/lib/kamal/configuration/validator/role.rb +11 -0
  94. data/lib/kamal/configuration/validator/servers.rb +7 -0
  95. data/lib/kamal/configuration/validator.rb +171 -0
  96. data/lib/kamal/configuration/volume.rb +22 -0
  97. data/lib/kamal/configuration.rb +393 -0
  98. data/lib/kamal/env_file.rb +44 -0
  99. data/lib/kamal/git.rb +27 -0
  100. data/lib/kamal/secrets/adapters/base.rb +23 -0
  101. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  102. data/lib/kamal/secrets/adapters/last_pass.rb +39 -0
  103. data/lib/kamal/secrets/adapters/one_password.rb +70 -0
  104. data/lib/kamal/secrets/adapters/test.rb +14 -0
  105. data/lib/kamal/secrets/adapters.rb +14 -0
  106. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
  107. data/lib/kamal/secrets.rb +42 -0
  108. data/lib/kamal/sshkit_with_ext.rb +142 -0
  109. data/lib/kamal/tags.rb +40 -0
  110. data/lib/kamal/utils/sensitive.rb +20 -0
  111. data/lib/kamal/utils.rb +110 -0
  112. data/lib/kamal/version.rb +3 -0
  113. data/lib/kamal.rb +14 -0
  114. metadata +349 -0
@@ -0,0 +1,109 @@
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_ROLE (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 = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
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
+ end
81
+
82
+
83
+ $stdout.sync = true
84
+
85
+ puts "Checking build status..."
86
+ attempts = 0
87
+ checks = GithubStatusChecks.new
88
+
89
+ begin
90
+ loop do
91
+ case checks.state
92
+ when "success"
93
+ puts "Checks passed, see #{checks.first_status_url}"
94
+ exit 0
95
+ when "failure"
96
+ exit_with_error "Checks failed, see #{checks.first_status_url}"
97
+ when "pending"
98
+ attempts += 1
99
+ end
100
+
101
+ exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
102
+
103
+ puts checks.current_status
104
+ sleep(ATTEMPTS_GAP)
105
+ checks.refresh!
106
+ end
107
+ rescue Octokit::NotFound
108
+ exit_with_error "Build status could not be found"
109
+ 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,8 @@
1
+ module Kamal::Cli
2
+ class BootError < StandardError; end
3
+ class HookError < StandardError; end
4
+ class LockError < StandardError; end
5
+ end
6
+
7
+ # SSHKit uses instance eval, so we need a global const for ergonomics
8
+ KAMAL = Kamal::Commander.new
@@ -0,0 +1,54 @@
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
+ stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
15
+ end
16
+
17
+ def roles_on(host)
18
+ roles.select { |role| role.hosts.include?(host.to_s) }
19
+ end
20
+
21
+ def proxy_hosts
22
+ config.proxy_hosts & specified_hosts
23
+ end
24
+
25
+ def accessory_hosts
26
+ config.accessories.flat_map(&:hosts) & specified_hosts
27
+ end
28
+
29
+ private
30
+ attr_reader :config, :specific_hosts, :specific_roles
31
+
32
+ def primary_specific_role
33
+ primary_or_first_role(specific_roles) if specific_roles.present?
34
+ end
35
+
36
+ def primary_or_first_role(roles)
37
+ roles.detect { |role| role == config.primary_role } || roles.first
38
+ end
39
+
40
+ def specified_roles
41
+ (specific_roles || config.roles) \
42
+ .select { |role| ((specific_hosts || config.all_hosts) & role.hosts).any? }
43
+ end
44
+
45
+ def specified_hosts
46
+ specified_hosts = specific_hosts || config.all_hosts
47
+
48
+ if (specific_role_hosts = specific_roles&.flat_map(&:hosts)).present?
49
+ specified_hosts.select { |host| specific_role_hosts.include?(host) }
50
+ else
51
+ specified_hosts
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,176 @@
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
+ delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :proxy_hosts, :accessory_hosts, to: :specifics
8
+
9
+ def initialize
10
+ self.verbosity = :info
11
+ self.holding_lock = false
12
+ self.connected = false
13
+ @specifics = nil
14
+ end
15
+
16
+ def config
17
+ @config ||= Kamal::Configuration.create_from(**@config_kwargs).tap do |config|
18
+ @config_kwargs = nil
19
+ configure_sshkit_with(config)
20
+ end
21
+ end
22
+
23
+ def configure(**kwargs)
24
+ @config, @config_kwargs = nil, kwargs
25
+ end
26
+
27
+ def configured?
28
+ @config || @config_kwargs
29
+ end
30
+
31
+ attr_reader :specific_roles, :specific_hosts
32
+
33
+ def specific_primary!
34
+ @specifics = nil
35
+ if specific_roles.present?
36
+ self.specific_hosts = [ specific_roles.first.primary_host ]
37
+ else
38
+ self.specific_hosts = [ config.primary_host ]
39
+ end
40
+ end
41
+
42
+ def specific_roles=(role_names)
43
+ @specifics = nil
44
+ if role_names.present?
45
+ @specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles)
46
+
47
+ if @specific_roles.empty?
48
+ raise ArgumentError, "No --roles match for #{role_names.join(',')}"
49
+ end
50
+
51
+ @specific_roles
52
+ end
53
+ end
54
+
55
+ def specific_hosts=(hosts)
56
+ @specifics = nil
57
+ if hosts.present?
58
+ @specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
59
+
60
+ if @specific_hosts.empty?
61
+ raise ArgumentError, "No --hosts match for #{hosts.join(',')}"
62
+ end
63
+
64
+ @specific_hosts
65
+ end
66
+ end
67
+
68
+ def with_specific_hosts(hosts)
69
+ original_hosts, self.specific_hosts = specific_hosts, hosts
70
+ yield
71
+ ensure
72
+ self.specific_hosts = original_hosts
73
+ end
74
+
75
+ def accessory_names
76
+ config.accessories&.collect(&:name) || []
77
+ end
78
+
79
+ def accessories_on(host)
80
+ config.accessories.select { |accessory| accessory.hosts.include?(host.to_s) }.map(&:name)
81
+ end
82
+
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
+ @builder ||= Kamal::Commands::Builder.new(config)
98
+ end
99
+
100
+ def docker
101
+ @docker ||= Kamal::Commands::Docker.new(config)
102
+ end
103
+
104
+ def hook
105
+ @hook ||= Kamal::Commands::Hook.new(config)
106
+ end
107
+
108
+ def lock
109
+ @lock ||= Kamal::Commands::Lock.new(config)
110
+ end
111
+
112
+ def proxy
113
+ @proxy ||= Kamal::Commands::Proxy.new(config)
114
+ end
115
+
116
+ def prune
117
+ @prune ||= Kamal::Commands::Prune.new(config)
118
+ end
119
+
120
+ def registry
121
+ @registry ||= Kamal::Commands::Registry.new(config)
122
+ end
123
+
124
+ def server
125
+ @server ||= Kamal::Commands::Server.new(config)
126
+ end
127
+
128
+ def alias(name)
129
+ config.aliases[name]
130
+ end
131
+
132
+
133
+ def with_verbosity(level)
134
+ old_level = self.verbosity
135
+
136
+ self.verbosity = level
137
+ SSHKit.config.output_verbosity = level
138
+
139
+ yield
140
+ ensure
141
+ self.verbosity = old_level
142
+ SSHKit.config.output_verbosity = old_level
143
+ end
144
+
145
+ def boot_strategy
146
+ if config.boot.limit.present?
147
+ { in: :groups, limit: config.boot.limit, wait: config.boot.wait }
148
+ else
149
+ {}
150
+ end
151
+ end
152
+
153
+ def holding_lock?
154
+ self.holding_lock
155
+ end
156
+
157
+ def connected?
158
+ self.connected
159
+ end
160
+
161
+ private
162
+ # Lazy setup of SSHKit
163
+ def configure_sshkit_with(config)
164
+ SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
165
+ SSHKit::Backend::Netssh.configure do |sshkit|
166
+ sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
167
+ sshkit.ssh_options = config.ssh.options
168
+ end
169
+ SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
170
+ SSHKit.config.output_verbosity = verbosity
171
+ end
172
+
173
+ def specifics
174
+ @specifics ||= Kamal::Commander::Specifics.new(config, specific_hosts, specific_roles)
175
+ end
176
+ end
@@ -0,0 +1,113 @@
1
+ class Kamal::Commands::Accessory < Kamal::Commands::Base
2
+ attr_reader :accessory_config
3
+ delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
4
+ :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
5
+ :secrets_io, :secrets_path, :env_directory,
6
+ to: :accessory_config
7
+
8
+ def initialize(config, name:)
9
+ super(config)
10
+ @accessory_config = config.accessory(name)
11
+ end
12
+
13
+ def run
14
+ docker :run,
15
+ "--name", service_name,
16
+ "--detach",
17
+ "--restart", "unless-stopped",
18
+ *network_args,
19
+ *config.logging_args,
20
+ *publish_args,
21
+ *env_args,
22
+ *volume_args,
23
+ *label_args,
24
+ *option_args,
25
+ image,
26
+ cmd
27
+ end
28
+
29
+ def start
30
+ docker :container, :start, service_name
31
+ end
32
+
33
+ def stop
34
+ docker :container, :stop, service_name
35
+ end
36
+
37
+ def info
38
+ docker :ps, *service_filter
39
+ end
40
+
41
+
42
+ def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
43
+ pipe \
44
+ docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
45
+ ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
46
+ end
47
+
48
+ def follow_logs(timestamps: true, grep: nil, grep_options: nil)
49
+ run_over_ssh \
50
+ pipe \
51
+ docker(:logs, service_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
52
+ (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
53
+ end
54
+
55
+
56
+ def execute_in_existing_container(*command, interactive: false)
57
+ docker :exec,
58
+ ("-it" if interactive),
59
+ service_name,
60
+ *command
61
+ end
62
+
63
+ def execute_in_new_container(*command, interactive: false)
64
+ docker :run,
65
+ ("-it" if interactive),
66
+ "--rm",
67
+ *network_args,
68
+ *env_args,
69
+ *volume_args,
70
+ image,
71
+ *command
72
+ end
73
+
74
+ def execute_in_existing_container_over_ssh(*command)
75
+ run_over_ssh execute_in_existing_container(*command, interactive: true)
76
+ end
77
+
78
+ def execute_in_new_container_over_ssh(*command)
79
+ run_over_ssh execute_in_new_container(*command, interactive: true)
80
+ end
81
+
82
+ def run_over_ssh(command)
83
+ super command, host: hosts.first
84
+ end
85
+
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(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true" ],
8
+ docker(:run, "--name", asset_container, "--detach", "--rm", "--entrypoint", "sleep", config.absolute_image, "1000000"),
9
+ docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory),
10
+ docker(:stop, "-t 1", 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", *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", *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,30 @@
1
+ module Kamal::Commands::App::Execution
2
+ def execute_in_existing_container(*command, interactive: false, env:)
3
+ docker :exec,
4
+ ("-it" if interactive),
5
+ *argumentize("--env", env),
6
+ container_name,
7
+ *command
8
+ end
9
+
10
+ def execute_in_new_container(*command, interactive: false, env:)
11
+ docker :run,
12
+ ("-it" if interactive),
13
+ "--rm",
14
+ "--network", "kamal",
15
+ *role&.env_args(host),
16
+ *argumentize("--env", env),
17
+ *config.volume_args,
18
+ *role&.option_args,
19
+ config.absolute_image,
20
+ *command
21
+ end
22
+
23
+ def execute_in_existing_container_over_ssh(*command, env:)
24
+ run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
25
+ end
26
+
27
+ def execute_in_new_container_over_ssh(*command, env:)
28
+ run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
29
+ end
30
+ 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", *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,18 @@
1
+ module Kamal::Commands::App::Logging
2
+ def logs(version: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
3
+ pipe \
4
+ version ? container_id_for_version(version) : current_running_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:, timestamps: true, lines: nil, grep: nil, grep_options: nil)
10
+ run_over_ssh \
11
+ pipe(
12
+ current_running_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
+ end
@@ -0,0 +1,16 @@
1
+ module Kamal::Commands::App::Proxy
2
+ delegate :proxy_container_name, to: :config
3
+
4
+ def deploy(target:)
5
+ proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
6
+ end
7
+
8
+ def remove
9
+ proxy_exec :remove, role.container_prefix
10
+ end
11
+
12
+ private
13
+ def proxy_exec(*command)
14
+ docker :exec, proxy_container_name, "kamal-proxy", *command
15
+ end
16
+ end