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,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,118 @@
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
+ :restart_policy, :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", restart_policy,
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
+ *option_args,
72
+ image,
73
+ *command
74
+ end
75
+
76
+ def execute_in_existing_container_over_ssh(*command)
77
+ run_over_ssh execute_in_existing_container(*command, interactive: true)
78
+ end
79
+
80
+ def execute_in_new_container_over_ssh(*command)
81
+ run_over_ssh execute_in_new_container(*command, interactive: true)
82
+ end
83
+
84
+ def run_over_ssh(command)
85
+ super command, host: hosts.first
86
+ end
87
+
88
+ def ensure_local_file_present(local_file)
89
+ if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?
90
+ raise "Missing file: #{local_file}"
91
+ end
92
+ end
93
+
94
+ def pull_image
95
+ docker :image, :pull, image
96
+ end
97
+
98
+ def remove_service_directory
99
+ [ :rm, "-rf", service_name ]
100
+ end
101
+
102
+ def remove_container
103
+ docker :container, :prune, "--force", *service_filter
104
+ end
105
+
106
+ def remove_image
107
+ docker :image, :rm, "--force", image
108
+ end
109
+
110
+ def ensure_env_directory
111
+ make_directory env_directory
112
+ end
113
+
114
+ private
115
+ def service_filter
116
+ [ "--filter", "label=service=#{service_name}" ]
117
+ end
118
+ 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,38 @@
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
+ "--name", container_name_for_exec,
16
+ "--network", "kamal",
17
+ *role&.env_args(host),
18
+ *argumentize("--env", env),
19
+ *role.logging_args,
20
+ *config.volume_args,
21
+ *role&.option_args,
22
+ config.absolute_image,
23
+ *command
24
+ end
25
+
26
+ def execute_in_existing_container_over_ssh(*command, env:)
27
+ run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
28
+ end
29
+
30
+ def execute_in_new_container_over_ssh(*command, env:)
31
+ run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
32
+ end
33
+
34
+ private
35
+ def container_name_for_exec
36
+ [ role.container_prefix, "exec", config.version, SecureRandom.hex(3) ].compact.join("-")
37
+ end
38
+ 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 shell([ "echo #{container_id}" ])
25
+ else current_running_container_id
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ module Kamal::Commands::App::Proxy
2
+ delegate :container_name, to: :"config.proxy_boot", prefix: :proxy
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
+ def live
13
+ proxy_exec :resume, role.container_prefix
14
+ end
15
+
16
+ def maintenance(**options)
17
+ proxy_exec :stop, role.container_prefix, *role.proxy.stop_command_args(**options)
18
+ end
19
+
20
+ def remove_proxy_app_directory
21
+ remove_directory config.proxy_boot.app_directory
22
+ end
23
+
24
+ def create_ssl_directory
25
+ make_directory(File.join(config.proxy_boot.tls_directory, role.name))
26
+ end
27
+
28
+ private
29
+ def proxy_exec(*command)
30
+ docker :exec, proxy_container_name, "kamal-proxy", *command
31
+ end
32
+ end
@@ -0,0 +1,125 @@
1
+ class Kamal::Commands::App < Kamal::Commands::Base
2
+ include Assets, Containers, ErrorPages, Execution, Images, Logging, Proxy
3
+
4
+ ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
5
+
6
+ attr_reader :role, :host
7
+
8
+ delegate :container_name, to: :role
9
+
10
+ def initialize(config, role: nil, host: nil)
11
+ super(config)
12
+ @role = role
13
+ @host = host
14
+ end
15
+
16
+ def run(hostname: nil)
17
+ docker :run,
18
+ "--detach",
19
+ "--restart", role.restart_policy,
20
+ "--name", container_name,
21
+ "--network", "kamal",
22
+ *([ "--hostname", hostname ] if hostname),
23
+ "--env", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
24
+ "--env", "KAMAL_VERSION=\"#{config.version}\"",
25
+ "--env", "KAMAL_HOST=\"#{host}\"",
26
+ *([ "--env", "KAMAL_DESTINATION=\"#{config.destination}\"" ] if config.destination),
27
+ *role.env_args(host),
28
+ *role.logging_args,
29
+ *config.volume_args,
30
+ *role.asset_volume_args,
31
+ *role.label_args,
32
+ *role.option_args,
33
+ config.absolute_image,
34
+ role.cmd
35
+ end
36
+
37
+ def start
38
+ docker :start, container_name
39
+ end
40
+
41
+ def status(version:)
42
+ pipe container_id_for_version(version), xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
43
+ end
44
+
45
+ def stop(version: nil)
46
+ pipe \
47
+ version ? container_id_for_version(version) : current_running_container_id,
48
+ xargs(docker(:stop, *role.stop_args))
49
+ end
50
+
51
+ def info
52
+ docker :ps, *container_filter_args
53
+ end
54
+
55
+
56
+ def current_running_container_id
57
+ current_running_container(format: "--quiet")
58
+ end
59
+
60
+ def container_id_for_version(version, only_running: false)
61
+ container_id_for(container_name: container_name(version), only_running: only_running)
62
+ end
63
+
64
+ def current_running_version
65
+ pipe \
66
+ current_running_container(format: "--format '{{.Names}}'"),
67
+ extract_version_from_name
68
+ end
69
+
70
+ def list_versions(*docker_args, statuses: nil)
71
+ pipe \
72
+ docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
73
+ extract_version_from_name
74
+ end
75
+
76
+ def ensure_env_directory
77
+ make_directory role.env_directory
78
+ end
79
+
80
+ private
81
+ def latest_image_id
82
+ docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'"
83
+ end
84
+
85
+ def current_running_container(format:)
86
+ pipe \
87
+ shell(chain(latest_image_container(format: format), latest_container(format: format))),
88
+ [ :head, "-1" ]
89
+ end
90
+
91
+ def latest_image_container(format:)
92
+ latest_container format: format, filters: [ "ancestor=$(#{latest_image_id.join(" ")})" ]
93
+ end
94
+
95
+ def latest_container(format:, filters: nil)
96
+ docker :ps, "--latest", *format, *container_filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
97
+ end
98
+
99
+ def container_filter_args(statuses: nil)
100
+ argumentize "--filter", container_filters(statuses: statuses)
101
+ end
102
+
103
+ def image_filter_args
104
+ argumentize "--filter", image_filters
105
+ end
106
+
107
+ def extract_version_from_name
108
+ # Extract SHA from "service-role-dest-SHA"
109
+ %(while read line; do echo ${line##{role.container_prefix}-}; done)
110
+ end
111
+
112
+ def container_filters(statuses: nil)
113
+ [ "label=service=#{config.service}" ].tap do |filters|
114
+ filters << "label=destination=#{config.destination}"
115
+ filters << "label=role=#{role}" if role
116
+ statuses&.each do |status|
117
+ filters << "status=#{status}"
118
+ end
119
+ end
120
+ end
121
+
122
+ def image_filters
123
+ [ "label=service=#{config.service}" ]
124
+ end
125
+ end
@@ -0,0 +1,39 @@
1
+ class Kamal::Commands::Auditor < Kamal::Commands::Base
2
+ attr_reader :details
3
+ delegate :escape_shell_value, to: Kamal::Utils
4
+
5
+ def initialize(config, **details)
6
+ super(config)
7
+ @details = details
8
+ end
9
+
10
+ # Runs remotely
11
+ def record(line, **details)
12
+ combine \
13
+ make_run_directory,
14
+ append([ :echo, escape_shell_value(audit_line(line, **details)) ], audit_log_file)
15
+ end
16
+
17
+ def reveal
18
+ [ :tail, "-n", 50, audit_log_file ]
19
+ end
20
+
21
+ private
22
+ def audit_log_file
23
+ file = [ config.service, config.destination, "audit.log" ].compact.join("-")
24
+
25
+ File.join(config.run_directory, file)
26
+ end
27
+
28
+ def audit_tags(**details)
29
+ tags(**self.details, **details)
30
+ end
31
+
32
+ def make_run_directory
33
+ [ :mkdir, "-p", config.run_directory ]
34
+ end
35
+
36
+ def audit_line(line, **details)
37
+ "#{audit_tags(**details).except(:version, :service_version, :service)} #{line}"
38
+ end
39
+ end
@@ -0,0 +1,147 @@
1
+ module Kamal::Commands
2
+ class Base
3
+ delegate :sensitive, :argumentize, to: Kamal::Utils
4
+
5
+ DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"
6
+
7
+ attr_accessor :config
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def run_over_ssh(*command, host:)
14
+ "ssh#{ssh_config_args}#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
15
+ end
16
+
17
+ def container_id_for(container_name:, only_running: false)
18
+ docker :container, :ls, *("--all" unless only_running), "--filter", "'name=^#{container_name}$'", "--quiet"
19
+ end
20
+
21
+ def make_directory_for(remote_file)
22
+ make_directory Pathname.new(remote_file).dirname.to_s
23
+ end
24
+
25
+ def make_directory(path)
26
+ [ :mkdir, "-p", path ]
27
+ end
28
+
29
+ def remove_directory(path)
30
+ [ :rm, "-r", path ]
31
+ end
32
+
33
+ def remove_file(path)
34
+ [ :rm, path ]
35
+ end
36
+
37
+ def ensure_docker_installed
38
+ combine \
39
+ ensure_local_docker_installed,
40
+ ensure_local_buildx_installed
41
+ end
42
+
43
+ private
44
+ def combine(*commands, by: "&&")
45
+ commands
46
+ .compact
47
+ .collect { |command| Array(command) + [ by ] }.flatten # Join commands
48
+ .tap { |commands| commands.pop } # Remove trailing combiner
49
+ end
50
+
51
+ def chain(*commands)
52
+ combine *commands, by: ";"
53
+ end
54
+
55
+ def pipe(*commands)
56
+ combine *commands, by: "|"
57
+ end
58
+
59
+ def append(*commands)
60
+ combine *commands, by: ">>"
61
+ end
62
+
63
+ def write(*commands)
64
+ combine *commands, by: ">"
65
+ end
66
+
67
+ def any(*commands)
68
+ combine *commands, by: "||"
69
+ end
70
+
71
+ def substitute(*commands)
72
+ "\$\(#{commands.join(" ")}\)"
73
+ end
74
+
75
+ def xargs(command)
76
+ [ :xargs, command ].flatten
77
+ end
78
+
79
+ def shell(command)
80
+ [ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\\\''")}'" ]
81
+ end
82
+
83
+ def docker(*args)
84
+ args.compact.unshift :docker
85
+ end
86
+
87
+ def pack(*args)
88
+ args.compact.unshift :pack
89
+ end
90
+
91
+ def git(*args, path: nil)
92
+ [ :git, *([ "-C", path ] if path), *args.compact ]
93
+ end
94
+
95
+ def grep(*args)
96
+ args.compact.unshift :grep
97
+ end
98
+
99
+ def tags(**details)
100
+ Kamal::Tags.from_config(config, **details)
101
+ end
102
+
103
+ def ssh_config_args
104
+ case config.ssh.config
105
+ when Array
106
+ config.ssh.config.map { |file| " -F #{file}" }.join
107
+ when String
108
+ " -F #{config.ssh.config}"
109
+ when true
110
+ "" # Use default SSH config
111
+ when false
112
+ " -F /dev/null" # Ignore SSH config
113
+ end
114
+ end
115
+
116
+ def ssh_proxy_args
117
+ case config.ssh.proxy
118
+ when Net::SSH::Proxy::Jump
119
+ " -J #{config.ssh.proxy.jump_proxies}"
120
+ when Net::SSH::Proxy::Command
121
+ " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
122
+ end
123
+ end
124
+
125
+ def ssh_keys_args
126
+ "#{ ssh_keys.join("") if ssh_keys}" + "#{" -o IdentitiesOnly=yes" if config.ssh&.keys_only}"
127
+ end
128
+
129
+ def ssh_keys
130
+ config.ssh.keys&.map do |key|
131
+ " -i #{key}"
132
+ end
133
+ end
134
+
135
+ def ensure_local_docker_installed
136
+ docker "--version"
137
+ end
138
+
139
+ def ensure_local_buildx_installed
140
+ docker :buildx, "version"
141
+ end
142
+
143
+ def docker_interactive_args
144
+ STDIN.isatty ? "-it" : "-i"
145
+ end
146
+ end
147
+ end