kamal 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
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