kamal 0.16.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1021 -0
  4. data/bin/kamal +18 -0
  5. data/lib/kamal/cli/accessory.rb +239 -0
  6. data/lib/kamal/cli/app.rb +296 -0
  7. data/lib/kamal/cli/base.rb +171 -0
  8. data/lib/kamal/cli/build.rb +106 -0
  9. data/lib/kamal/cli/healthcheck.rb +20 -0
  10. data/lib/kamal/cli/lock.rb +37 -0
  11. data/lib/kamal/cli/main.rb +249 -0
  12. data/lib/kamal/cli/prune.rb +30 -0
  13. data/lib/kamal/cli/registry.rb +18 -0
  14. data/lib/kamal/cli/server.rb +21 -0
  15. data/lib/kamal/cli/templates/deploy.yml +74 -0
  16. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  17. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  18. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  19. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
  20. data/lib/kamal/cli/templates/template.env +2 -0
  21. data/lib/kamal/cli/traefik.rb +111 -0
  22. data/lib/kamal/cli.rb +7 -0
  23. data/lib/kamal/commander.rb +154 -0
  24. data/lib/kamal/commands/accessory.rb +113 -0
  25. data/lib/kamal/commands/app.rb +175 -0
  26. data/lib/kamal/commands/auditor.rb +28 -0
  27. data/lib/kamal/commands/base.rb +65 -0
  28. data/lib/kamal/commands/builder/base.rb +60 -0
  29. data/lib/kamal/commands/builder/multiarch/remote.rb +51 -0
  30. data/lib/kamal/commands/builder/multiarch.rb +29 -0
  31. data/lib/kamal/commands/builder/native/cached.rb +16 -0
  32. data/lib/kamal/commands/builder/native/remote.rb +59 -0
  33. data/lib/kamal/commands/builder/native.rb +20 -0
  34. data/lib/kamal/commands/builder.rb +62 -0
  35. data/lib/kamal/commands/docker.rb +21 -0
  36. data/lib/kamal/commands/healthcheck.rb +57 -0
  37. data/lib/kamal/commands/hook.rb +14 -0
  38. data/lib/kamal/commands/lock.rb +63 -0
  39. data/lib/kamal/commands/prune.rb +38 -0
  40. data/lib/kamal/commands/registry.rb +20 -0
  41. data/lib/kamal/commands/traefik.rb +104 -0
  42. data/lib/kamal/commands.rb +2 -0
  43. data/lib/kamal/configuration/accessory.rb +169 -0
  44. data/lib/kamal/configuration/boot.rb +20 -0
  45. data/lib/kamal/configuration/builder.rb +114 -0
  46. data/lib/kamal/configuration/role.rb +155 -0
  47. data/lib/kamal/configuration/ssh.rb +38 -0
  48. data/lib/kamal/configuration/sshkit.rb +20 -0
  49. data/lib/kamal/configuration.rb +251 -0
  50. data/lib/kamal/sshkit_with_ext.rb +104 -0
  51. data/lib/kamal/tags.rb +39 -0
  52. data/lib/kamal/utils/healthcheck_poller.rb +39 -0
  53. data/lib/kamal/utils/sensitive.rb +19 -0
  54. data/lib/kamal/utils.rb +100 -0
  55. data/lib/kamal/version.rb +3 -0
  56. data/lib/kamal.rb +10 -0
  57. metadata +266 -0
@@ -0,0 +1,74 @@
1
+ # Name of your application. Used to uniquely configure containers.
2
+ service: my-app
3
+
4
+ # Name of the container image.
5
+ image: user/my-app
6
+
7
+ # Deploy to these servers.
8
+ servers:
9
+ - 192.168.0.1
10
+
11
+ # Credentials for your image host.
12
+ registry:
13
+ # Specify the registry server, if you're not using Docker Hub
14
+ # server: registry.digitalocean.com / ghcr.io / ...
15
+ username: my-user
16
+
17
+ # Always use an access token rather than real password when possible.
18
+ password:
19
+ - KAMAL_REGISTRY_PASSWORD
20
+
21
+ # Inject ENV variables into containers (secrets come from .env).
22
+ # env:
23
+ # clear:
24
+ # DB_HOST: 192.168.0.2
25
+ # secret:
26
+ # - RAILS_MASTER_KEY
27
+
28
+ # Use a different ssh user than root
29
+ # ssh:
30
+ # user: app
31
+
32
+ # Configure builder setup.
33
+ # builder:
34
+ # args:
35
+ # RUBY_VERSION: 3.2.0
36
+ # secrets:
37
+ # - GITHUB_TOKEN
38
+ # remote:
39
+ # arch: amd64
40
+ # host: ssh://app@192.168.0.1
41
+
42
+ # Use accessory services (secrets come from .env).
43
+ # accessories:
44
+ # db:
45
+ # image: mysql:8.0
46
+ # host: 192.168.0.2
47
+ # port: 3306
48
+ # env:
49
+ # clear:
50
+ # MYSQL_ROOT_HOST: '%'
51
+ # secret:
52
+ # - MYSQL_ROOT_PASSWORD
53
+ # files:
54
+ # - config/mysql/production.cnf:/etc/mysql/my.cnf
55
+ # - db/production.sql.erb:/docker-entrypoint-initdb.d/setup.sql
56
+ # directories:
57
+ # - data:/var/lib/mysql
58
+ # redis:
59
+ # image: redis:7.0
60
+ # host: 192.168.0.2
61
+ # port: 6379
62
+ # directories:
63
+ # - data:/data
64
+
65
+ # Configure custom arguments for Traefik
66
+ # traefik:
67
+ # args:
68
+ # accesslog: true
69
+ # accesslog.format: json
70
+
71
+ # Configure a custom healthcheck (default is /up on port 3000)
72
+ # healthcheck:
73
+ # path: /healthz
74
+ # port: 4000
@@ -0,0 +1,14 @@
1
+ #!/bin/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_ROLE (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,51 @@
1
+ #!/bin/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_ROLE (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 "No git remote set, 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_ROLE (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,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,2 @@
1
+ KAMAL_REGISTRY_PASSWORD=change-this
2
+ RAILS_MASTER_KEY=another-env
@@ -0,0 +1,111 @@
1
+ class Kamal::Cli::Traefik < Kamal::Cli::Base
2
+ desc "boot", "Boot Traefik on servers"
3
+ def boot
4
+ mutating do
5
+ on(KAMAL.traefik_hosts) do
6
+ execute *KAMAL.registry.login
7
+ execute *KAMAL.traefik.start_or_run
8
+ end
9
+ end
10
+ end
11
+
12
+ desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
13
+ option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
14
+ def reboot
15
+ mutating do
16
+ on(KAMAL.traefik_hosts, in: options[:rolling] ? :sequence : :parallel) do
17
+ execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
18
+ execute *KAMAL.registry.login
19
+ execute *KAMAL.traefik.stop
20
+ execute *KAMAL.traefik.remove_container
21
+ execute *KAMAL.traefik.run
22
+ end
23
+ end
24
+ end
25
+
26
+ desc "start", "Start existing Traefik container on servers"
27
+ def start
28
+ mutating do
29
+ on(KAMAL.traefik_hosts) do
30
+ execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
31
+ execute *KAMAL.traefik.start
32
+ end
33
+ end
34
+ end
35
+
36
+ desc "stop", "Stop existing Traefik container on servers"
37
+ def stop
38
+ mutating do
39
+ on(KAMAL.traefik_hosts) do
40
+ execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
41
+ execute *KAMAL.traefik.stop
42
+ end
43
+ end
44
+ end
45
+
46
+ desc "restart", "Restart existing Traefik container on servers"
47
+ def restart
48
+ mutating do
49
+ stop
50
+ start
51
+ end
52
+ end
53
+
54
+ desc "details", "Show details about Traefik container from servers"
55
+ def details
56
+ on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
57
+ end
58
+
59
+ desc "logs", "Show log lines from Traefik on servers"
60
+ option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
61
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
62
+ option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
63
+ option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
64
+ def logs
65
+ grep = options[:grep]
66
+
67
+ if options[:follow]
68
+ run_locally do
69
+ info "Following logs on #{KAMAL.primary_host}..."
70
+ info KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep)
71
+ exec KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep)
72
+ end
73
+ else
74
+ since = options[:since]
75
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
76
+
77
+ on(KAMAL.traefik_hosts) do |host|
78
+ puts_by_host host, capture(*KAMAL.traefik.logs(since: since, lines: lines, grep: grep)), type: "Traefik"
79
+ end
80
+ end
81
+ end
82
+
83
+ desc "remove", "Remove Traefik container and image from servers"
84
+ def remove
85
+ mutating do
86
+ stop
87
+ remove_container
88
+ remove_image
89
+ end
90
+ end
91
+
92
+ desc "remove_container", "Remove Traefik container from servers", hide: true
93
+ def remove_container
94
+ mutating do
95
+ on(KAMAL.traefik_hosts) do
96
+ execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
97
+ execute *KAMAL.traefik.remove_container
98
+ end
99
+ end
100
+ end
101
+
102
+ desc "remove_image", "Remove Traefik image from servers", hide: true
103
+ def remove_image
104
+ mutating do
105
+ on(KAMAL.traefik_hosts) do
106
+ execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
107
+ execute *KAMAL.traefik.remove_image
108
+ end
109
+ end
110
+ end
111
+ end
data/lib/kamal/cli.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Kamal::Cli
2
+ class LockError < StandardError; end
3
+ class HookError < StandardError; end
4
+ end
5
+
6
+ # SSHKit uses instance eval, so we need a global const for ergonomics
7
+ KAMAL = Kamal::Commander.new
@@ -0,0 +1,154 @@
1
+ require "active_support/core_ext/enumerable"
2
+ require "active_support/core_ext/module/delegation"
3
+
4
+ class Kamal::Commander
5
+ attr_accessor :verbosity, :holding_lock, :hold_lock_on_error
6
+
7
+ def initialize
8
+ self.verbosity = :info
9
+ self.holding_lock = false
10
+ self.hold_lock_on_error = false
11
+ end
12
+
13
+ def config
14
+ @config ||= Kamal::Configuration.create_from(**@config_kwargs).tap do |config|
15
+ @config_kwargs = nil
16
+ configure_sshkit_with(config)
17
+ end
18
+ end
19
+
20
+ def configure(**kwargs)
21
+ @config, @config_kwargs = nil, kwargs
22
+ end
23
+
24
+ attr_reader :specific_roles, :specific_hosts
25
+
26
+ def specific_primary!
27
+ self.specific_hosts = [ config.primary_web_host ]
28
+ end
29
+
30
+ def specific_roles=(role_names)
31
+ @specific_roles = config.roles.select { |r| role_names.include?(r.name) } if role_names.present?
32
+ end
33
+
34
+ def specific_hosts=(hosts)
35
+ @specific_hosts = config.all_hosts & hosts if hosts.present?
36
+ end
37
+
38
+ def primary_host
39
+ specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host
40
+ end
41
+
42
+ def roles
43
+ (specific_roles || config.roles).select do |role|
44
+ ((specific_hosts || config.all_hosts) & role.hosts).any?
45
+ end
46
+ end
47
+
48
+ def hosts
49
+ (specific_hosts || config.all_hosts).select do |host|
50
+ (specific_roles || config.roles).flat_map(&:hosts).include?(host)
51
+ end
52
+ end
53
+
54
+ def boot_strategy
55
+ if config.boot.limit.present?
56
+ { in: :groups, limit: config.boot.limit, wait: config.boot.wait }
57
+ else
58
+ {}
59
+ end
60
+ end
61
+
62
+ def roles_on(host)
63
+ roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
64
+ end
65
+
66
+ def traefik_hosts
67
+ specific_hosts || config.traefik_hosts
68
+ end
69
+
70
+ def accessory_hosts
71
+ specific_hosts || config.accessories.flat_map(&:hosts)
72
+ end
73
+
74
+ def accessory_names
75
+ config.accessories&.collect(&:name) || []
76
+ end
77
+
78
+
79
+ def app(role: nil)
80
+ Kamal::Commands::App.new(config, role: role)
81
+ end
82
+
83
+ def accessory(name)
84
+ Kamal::Commands::Accessory.new(config, name: name)
85
+ end
86
+
87
+ def auditor(**details)
88
+ Kamal::Commands::Auditor.new(config, **details)
89
+ end
90
+
91
+ def builder
92
+ @builder ||= Kamal::Commands::Builder.new(config)
93
+ end
94
+
95
+ def docker
96
+ @docker ||= Kamal::Commands::Docker.new(config)
97
+ end
98
+
99
+ def healthcheck
100
+ @healthcheck ||= Kamal::Commands::Healthcheck.new(config)
101
+ end
102
+
103
+ def hook
104
+ @hook ||= Kamal::Commands::Hook.new(config)
105
+ end
106
+
107
+ def lock
108
+ @lock ||= Kamal::Commands::Lock.new(config)
109
+ end
110
+
111
+ def prune
112
+ @prune ||= Kamal::Commands::Prune.new(config)
113
+ end
114
+
115
+ def registry
116
+ @registry ||= Kamal::Commands::Registry.new(config)
117
+ end
118
+
119
+ def traefik
120
+ @traefik ||= Kamal::Commands::Traefik.new(config)
121
+ end
122
+
123
+ def with_verbosity(level)
124
+ old_level = self.verbosity
125
+
126
+ self.verbosity = level
127
+ SSHKit.config.output_verbosity = level
128
+
129
+ yield
130
+ ensure
131
+ self.verbosity = old_level
132
+ SSHKit.config.output_verbosity = old_level
133
+ end
134
+
135
+ def holding_lock?
136
+ self.holding_lock
137
+ end
138
+
139
+ def hold_lock_on_error?
140
+ self.hold_lock_on_error
141
+ end
142
+
143
+ private
144
+ # Lazy setup of SSHKit
145
+ def configure_sshkit_with(config)
146
+ SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
147
+ SSHKit::Backend::Netssh.configure do |sshkit|
148
+ sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
149
+ sshkit.ssh_options = config.ssh.options
150
+ end
151
+ SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
152
+ SSHKit.config.output_verbosity = verbosity
153
+ end
154
+ 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
+ :publish_args, :env_args, :volume_args, :label_args, :option_args, to: :accessory_config
5
+
6
+ def initialize(config, name:)
7
+ super(config)
8
+ @accessory_config = config.accessory(name)
9
+ end
10
+
11
+ def run
12
+ docker :run,
13
+ "--name", service_name,
14
+ "--detach",
15
+ "--restart", "unless-stopped",
16
+ *config.logging_args,
17
+ *publish_args,
18
+ *env_args,
19
+ *volume_args,
20
+ *label_args,
21
+ *option_args,
22
+ image,
23
+ cmd
24
+ end
25
+
26
+ def start
27
+ docker :container, :start, service_name
28
+ end
29
+
30
+ def stop
31
+ docker :container, :stop, service_name
32
+ end
33
+
34
+ def info
35
+ docker :ps, *service_filter
36
+ end
37
+
38
+
39
+ def logs(since: nil, lines: nil, grep: nil)
40
+ pipe \
41
+ docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
42
+ ("grep '#{grep}'" if grep)
43
+ end
44
+
45
+ def follow_logs(grep: nil)
46
+ run_over_ssh \
47
+ pipe \
48
+ docker(:logs, service_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
49
+ (%(grep "#{grep}") if grep)
50
+ end
51
+
52
+
53
+ def execute_in_existing_container(*command, interactive: false)
54
+ docker :exec,
55
+ ("-it" if interactive),
56
+ service_name,
57
+ *command
58
+ end
59
+
60
+ def execute_in_new_container(*command, interactive: false)
61
+ docker :run,
62
+ ("-it" if interactive),
63
+ "--rm",
64
+ *env_args,
65
+ *volume_args,
66
+ image,
67
+ *command
68
+ end
69
+
70
+ def execute_in_existing_container_over_ssh(*command)
71
+ run_over_ssh execute_in_existing_container(*command, interactive: true)
72
+ end
73
+
74
+ def execute_in_new_container_over_ssh(*command)
75
+ run_over_ssh execute_in_new_container(*command, interactive: true)
76
+ end
77
+
78
+ def run_over_ssh(command)
79
+ super command, host: hosts.first
80
+ end
81
+
82
+
83
+ def ensure_local_file_present(local_file)
84
+ if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?
85
+ raise "Missing file: #{local_file}"
86
+ end
87
+ end
88
+
89
+ def make_directory_for(remote_file)
90
+ make_directory Pathname.new(remote_file).dirname.to_s
91
+ end
92
+
93
+ def make_directory(path)
94
+ [ :mkdir, "-p", path ]
95
+ end
96
+
97
+ def remove_service_directory
98
+ [ :rm, "-rf", service_name ]
99
+ end
100
+
101
+ def remove_container
102
+ docker :container, :prune, "--force", *service_filter
103
+ end
104
+
105
+ def remove_image
106
+ docker :image, :rm, "--force", image
107
+ end
108
+
109
+ private
110
+ def service_filter
111
+ [ "--filter", "label=service=#{service_name}" ]
112
+ end
113
+ end