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,175 @@
1
+ class Kamal::Commands::App < Kamal::Commands::Base
2
+ ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
3
+
4
+ attr_reader :role
5
+
6
+ def initialize(config, role: nil)
7
+ super(config)
8
+ @role = role
9
+ end
10
+
11
+ def start_or_run(hostname: nil)
12
+ combine start, run(hostname: hostname), by: "||"
13
+ end
14
+
15
+ def run(hostname: nil)
16
+ role = config.role(self.role)
17
+
18
+ docker :run,
19
+ "--detach",
20
+ "--restart unless-stopped",
21
+ "--name", container_name,
22
+ *(["--hostname", hostname] if hostname),
23
+ "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
24
+ *role.env_args,
25
+ *role.health_check_args,
26
+ *config.logging_args,
27
+ *config.volume_args,
28
+ *role.label_args,
29
+ *role.option_args,
30
+ config.absolute_image,
31
+ role.cmd
32
+ end
33
+
34
+ def start
35
+ docker :start, container_name
36
+ end
37
+
38
+ def status(version:)
39
+ pipe container_id_for_version(version), xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
40
+ end
41
+
42
+ def stop(version: nil)
43
+ pipe \
44
+ version ? container_id_for_version(version) : current_running_container_id,
45
+ xargs(config.stop_wait_time ? docker(:stop, "-t", config.stop_wait_time) : docker(:stop))
46
+ end
47
+
48
+ def info
49
+ docker :ps, *filter_args
50
+ end
51
+
52
+
53
+ def logs(since: nil, lines: nil, grep: nil)
54
+ pipe \
55
+ current_running_container_id,
56
+ "xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
57
+ ("grep '#{grep}'" if grep)
58
+ end
59
+
60
+ def follow_logs(host:, grep: nil)
61
+ run_over_ssh \
62
+ pipe(
63
+ current_running_container_id,
64
+ "xargs docker logs --timestamps --tail 10 --follow 2>&1",
65
+ (%(grep "#{grep}") if grep)
66
+ ),
67
+ host: host
68
+ end
69
+
70
+
71
+ def execute_in_existing_container(*command, interactive: false)
72
+ docker :exec,
73
+ ("-it" if interactive),
74
+ container_name,
75
+ *command
76
+ end
77
+
78
+ def execute_in_new_container(*command, interactive: false)
79
+ role = config.role(self.role)
80
+
81
+ docker :run,
82
+ ("-it" if interactive),
83
+ "--rm",
84
+ *config.env_args,
85
+ *config.volume_args,
86
+ *role&.option_args,
87
+ config.absolute_image,
88
+ *command
89
+ end
90
+
91
+ def execute_in_existing_container_over_ssh(*command, host:)
92
+ run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
93
+ end
94
+
95
+ def execute_in_new_container_over_ssh(*command, host:)
96
+ run_over_ssh execute_in_new_container(*command, interactive: true), host: host
97
+ end
98
+
99
+
100
+ def current_running_container_id
101
+ docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--latest"
102
+ end
103
+
104
+ def container_id_for_version(version, only_running: false)
105
+ container_id_for(container_name: container_name(version), only_running: only_running)
106
+ end
107
+
108
+ def current_running_version
109
+ list_versions("--latest", statuses: ACTIVE_DOCKER_STATUSES)
110
+ end
111
+
112
+ def list_versions(*docker_args, statuses: nil)
113
+ pipe \
114
+ docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
115
+ %(while read line; do echo ${line##{service_role_dest}-}; done) # Extract SHA from "service-role-dest-SHA"
116
+ end
117
+
118
+ def list_containers
119
+ docker :container, :ls, "--all", *filter_args
120
+ end
121
+
122
+ def list_container_names
123
+ [ *list_containers, "--format", "'{{ .Names }}'" ]
124
+ end
125
+
126
+ def remove_container(version:)
127
+ pipe \
128
+ container_id_for(container_name: container_name(version)),
129
+ xargs(docker(:container, :rm))
130
+ end
131
+
132
+ def rename_container(version:, new_version:)
133
+ docker :rename, container_name(version), container_name(new_version)
134
+ end
135
+
136
+ def remove_containers
137
+ docker :container, :prune, "--force", *filter_args
138
+ end
139
+
140
+ def list_images
141
+ docker :image, :ls, config.repository
142
+ end
143
+
144
+ def remove_images
145
+ docker :image, :prune, "--all", "--force", *filter_args
146
+ end
147
+
148
+ def tag_current_as_latest
149
+ docker :tag, config.absolute_image, config.latest_image
150
+ end
151
+
152
+
153
+ private
154
+ def container_name(version = nil)
155
+ [ config.service, role, config.destination, version || config.version ].compact.join("-")
156
+ end
157
+
158
+ def filter_args(statuses: nil)
159
+ argumentize "--filter", filters(statuses: statuses)
160
+ end
161
+
162
+ def service_role_dest
163
+ [config.service, role, config.destination].compact.join("-")
164
+ end
165
+
166
+ def filters(statuses: nil)
167
+ [ "label=service=#{config.service}" ].tap do |filters|
168
+ filters << "label=destination=#{config.destination}" if config.destination
169
+ filters << "label=role=#{role}" if role
170
+ statuses&.each do |status|
171
+ filters << "status=#{status}"
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,28 @@
1
+ class Kamal::Commands::Auditor < Kamal::Commands::Base
2
+ attr_reader :details
3
+
4
+ def initialize(config, **details)
5
+ super(config)
6
+ @details = details
7
+ end
8
+
9
+ # Runs remotely
10
+ def record(line, **details)
11
+ append \
12
+ [ :echo, audit_tags(**details).except(:version, :service_version).to_s, line ],
13
+ audit_log_file
14
+ end
15
+
16
+ def reveal
17
+ [ :tail, "-n", 50, audit_log_file ]
18
+ end
19
+
20
+ private
21
+ def audit_log_file
22
+ [ "kamal", config.service, config.destination, "audit.log" ].compact.join("-")
23
+ end
24
+
25
+ def audit_tags(**details)
26
+ tags(**self.details, **details)
27
+ end
28
+ end
@@ -0,0 +1,65 @@
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
+ DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
7
+
8
+ attr_accessor :config
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def run_over_ssh(*command, host:)
15
+ "ssh".tap do |cmd|
16
+ if config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Jump)
17
+ cmd << " -J #{config.ssh.proxy.jump_proxies}"
18
+ elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
19
+ cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
20
+ end
21
+ cmd << " -t #{config.ssh.user}@#{host} '#{command.join(" ")}'"
22
+ end
23
+ end
24
+
25
+ def container_id_for(container_name:, only_running: false)
26
+ docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
27
+ end
28
+
29
+ private
30
+ def combine(*commands, by: "&&")
31
+ commands
32
+ .compact
33
+ .collect { |command| Array(command) + [ by ] }.flatten # Join commands
34
+ .tap { |commands| commands.pop } # Remove trailing combiner
35
+ end
36
+
37
+ def chain(*commands)
38
+ combine *commands, by: ";"
39
+ end
40
+
41
+ def pipe(*commands)
42
+ combine *commands, by: "|"
43
+ end
44
+
45
+ def append(*commands)
46
+ combine *commands, by: ">>"
47
+ end
48
+
49
+ def write(*commands)
50
+ combine *commands, by: ">"
51
+ end
52
+
53
+ def xargs(command)
54
+ [ :xargs, command ].flatten
55
+ end
56
+
57
+ def docker(*args)
58
+ args.compact.unshift :docker
59
+ end
60
+
61
+ def tags(**details)
62
+ Kamal::Tags.from_config(config, **details)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,60 @@
1
+
2
+ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
3
+ class BuilderError < StandardError; end
4
+
5
+ delegate :argumentize, to: Kamal::Utils
6
+ delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, to: :builder_config
7
+
8
+ def clean
9
+ docker :image, :rm, "--force", config.absolute_image
10
+ end
11
+
12
+ def pull
13
+ docker :pull, config.absolute_image
14
+ end
15
+
16
+ def build_options
17
+ [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
18
+ end
19
+
20
+ def build_context
21
+ config.builder.context
22
+ end
23
+
24
+
25
+ private
26
+ def build_tags
27
+ [ "-t", config.absolute_image, "-t", config.latest_image ]
28
+ end
29
+
30
+ def build_cache
31
+ if cache_to && cache_from
32
+ ["--cache-to", cache_to,
33
+ "--cache-from", cache_from]
34
+ end
35
+ end
36
+
37
+ def build_labels
38
+ argumentize "--label", { service: config.service }
39
+ end
40
+
41
+ def build_args
42
+ argumentize "--build-arg", args, sensitive: true
43
+ end
44
+
45
+ def build_secrets
46
+ argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
47
+ end
48
+
49
+ def build_dockerfile
50
+ if Pathname.new(File.expand_path(dockerfile)).exist?
51
+ argumentize "--file", dockerfile
52
+ else
53
+ raise BuilderError, "Missing #{dockerfile}"
54
+ end
55
+ end
56
+
57
+ def builder_config
58
+ config.builder
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Multiarch
2
+ def create
3
+ combine \
4
+ create_contexts,
5
+ create_local_buildx,
6
+ append_remote_buildx
7
+ end
8
+
9
+ def remove
10
+ combine \
11
+ remove_contexts,
12
+ super
13
+ end
14
+
15
+ private
16
+ def builder_name
17
+ super + "-remote"
18
+ end
19
+
20
+ def builder_name_with_arch(arch)
21
+ "#{builder_name}-#{arch}"
22
+ end
23
+
24
+ def create_local_buildx
25
+ docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local_arch), "--platform", "linux/#{local_arch}"
26
+ end
27
+
28
+ def append_remote_buildx
29
+ docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(remote_arch), "--platform", "linux/#{remote_arch}"
30
+ end
31
+
32
+ def create_contexts
33
+ combine \
34
+ create_context(local_arch, local_host),
35
+ create_context(remote_arch, remote_host)
36
+ end
37
+
38
+ def create_context(arch, host)
39
+ docker :context, :create, builder_name_with_arch(arch), "--description", "'#{builder_name} #{arch} native host'", "--docker", "'host=#{host}'"
40
+ end
41
+
42
+ def remove_contexts
43
+ combine \
44
+ remove_context(local_arch),
45
+ remove_context(remote_arch)
46
+ end
47
+
48
+ def remove_context(arch)
49
+ docker :context, :rm, builder_name_with_arch(arch)
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
2
+ def create
3
+ docker :buildx, :create, "--use", "--name", builder_name
4
+ end
5
+
6
+ def remove
7
+ docker :buildx, :rm, builder_name
8
+ end
9
+
10
+ def push
11
+ docker :buildx, :build,
12
+ "--push",
13
+ "--platform", "linux/amd64,linux/arm64",
14
+ "--builder", builder_name,
15
+ *build_options,
16
+ build_context
17
+ end
18
+
19
+ def info
20
+ combine \
21
+ docker(:context, :ls),
22
+ docker(:buildx, :ls)
23
+ end
24
+
25
+ private
26
+ def builder_name
27
+ "kamal-#{config.service}-multiarch"
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Native
2
+ def create
3
+ docker :buildx, :create, "--use", "--driver=docker-container"
4
+ end
5
+
6
+ def remove
7
+ docker :buildx, :rm, builder_name
8
+ end
9
+
10
+ def push
11
+ docker :buildx, :build,
12
+ "--push",
13
+ *build_options,
14
+ build_context
15
+ end
16
+ end
@@ -0,0 +1,59 @@
1
+ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Native
2
+ def create
3
+ chain \
4
+ create_context,
5
+ create_buildx
6
+ end
7
+
8
+ def remove
9
+ chain \
10
+ remove_context,
11
+ remove_buildx
12
+ end
13
+
14
+ def push
15
+ docker :buildx, :build,
16
+ "--push",
17
+ "--platform", platform,
18
+ "--builder", builder_name,
19
+ *build_options,
20
+ build_context
21
+ end
22
+
23
+ def info
24
+ chain \
25
+ docker(:context, :ls),
26
+ docker(:buildx, :ls)
27
+ end
28
+
29
+
30
+ private
31
+ def builder_name
32
+ "kamal-#{config.service}-native-remote"
33
+ end
34
+
35
+ def builder_name_with_arch
36
+ "#{builder_name}-#{remote_arch}"
37
+ end
38
+
39
+ def platform
40
+ "linux/#{remote_arch}"
41
+ end
42
+
43
+ def create_context
44
+ docker :context, :create,
45
+ builder_name_with_arch, "--description", "'#{builder_name} #{remote_arch} native host'", "--docker", "'host=#{remote_host}'"
46
+ end
47
+
48
+ def remove_context
49
+ docker :context, :rm, builder_name_with_arch
50
+ end
51
+
52
+ def create_buildx
53
+ docker :buildx, :create, "--name", builder_name, builder_name_with_arch, "--platform", platform
54
+ end
55
+
56
+ def remove_buildx
57
+ docker :buildx, :rm, builder_name
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base
2
+ def create
3
+ # No-op on native without cache
4
+ end
5
+
6
+ def remove
7
+ # No-op on native without cache
8
+ end
9
+
10
+ def push
11
+ combine \
12
+ docker(:build, *build_options, build_context),
13
+ docker(:push, config.absolute_image),
14
+ docker(:push, config.latest_image)
15
+ end
16
+
17
+ def info
18
+ # No-op on native
19
+ end
20
+ end
@@ -0,0 +1,62 @@
1
+ class Kamal::Commands::Builder < Kamal::Commands::Base
2
+ delegate :create, :remove, :push, :clean, :pull, :info, to: :target
3
+
4
+ def name
5
+ target.class.to_s.remove("Kamal::Commands::Builder::").underscore.inquiry
6
+ end
7
+
8
+ def target
9
+ case
10
+ when !config.builder.multiarch? && !config.builder.cached?
11
+ native
12
+ when !config.builder.multiarch? && config.builder.cached?
13
+ native_cached
14
+ when config.builder.local? && config.builder.remote?
15
+ multiarch_remote
16
+ when config.builder.remote?
17
+ native_remote
18
+ else
19
+ multiarch
20
+ end
21
+ end
22
+
23
+ def native
24
+ @native ||= Kamal::Commands::Builder::Native.new(config)
25
+ end
26
+
27
+ def native_cached
28
+ @native ||= Kamal::Commands::Builder::Native::Cached.new(config)
29
+ end
30
+
31
+ def native_remote
32
+ @native ||= Kamal::Commands::Builder::Native::Remote.new(config)
33
+ end
34
+
35
+ def multiarch
36
+ @multiarch ||= Kamal::Commands::Builder::Multiarch.new(config)
37
+ end
38
+
39
+ def multiarch_remote
40
+ @multiarch_remote ||= Kamal::Commands::Builder::Multiarch::Remote.new(config)
41
+ end
42
+
43
+
44
+ def ensure_local_dependencies_installed
45
+ if name.native?
46
+ ensure_local_docker_installed
47
+ else
48
+ combine \
49
+ ensure_local_docker_installed,
50
+ ensure_local_buildx_installed
51
+ end
52
+ end
53
+
54
+ private
55
+ def ensure_local_docker_installed
56
+ docker "--version"
57
+ end
58
+
59
+ def ensure_local_buildx_installed
60
+ docker :buildx, "version"
61
+ end
62
+ end
@@ -0,0 +1,21 @@
1
+ class Kamal::Commands::Docker < Kamal::Commands::Base
2
+ # Install Docker using the https://github.com/docker/docker-install convenience script.
3
+ def install
4
+ pipe [ :curl, "-fsSL", "https://get.docker.com" ], :sh
5
+ end
6
+
7
+ # Checks the Docker client version. Fails if Docker is not installed.
8
+ def installed?
9
+ docker "-v"
10
+ end
11
+
12
+ # Checks the Docker server version. Fails if Docker is not running.
13
+ def running?
14
+ docker :version
15
+ end
16
+
17
+ # Do we have superuser access to install Docker and start system services?
18
+ def superuser?
19
+ [ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
2
+ EXPOSED_PORT = 3999
3
+
4
+ def run
5
+ web = config.role(:web)
6
+
7
+ docker :run,
8
+ "--detach",
9
+ "--name", container_name_with_version,
10
+ "--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
11
+ "--label", "service=#{container_name}",
12
+ "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
13
+ *web.env_args,
14
+ *web.health_check_args,
15
+ *config.volume_args,
16
+ *web.option_args,
17
+ config.absolute_image,
18
+ web.cmd
19
+ end
20
+
21
+ def status
22
+ pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
23
+ end
24
+
25
+ def container_health_log
26
+ pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
27
+ end
28
+
29
+ def logs
30
+ pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
31
+ end
32
+
33
+ def stop
34
+ pipe container_id, xargs(docker(:stop))
35
+ end
36
+
37
+ def remove
38
+ pipe container_id, xargs(docker(:container, :rm))
39
+ end
40
+
41
+ private
42
+ def container_name
43
+ [ "healthcheck", config.service, config.destination ].compact.join("-")
44
+ end
45
+
46
+ def container_name_with_version
47
+ "#{container_name}-#{config.version}"
48
+ end
49
+
50
+ def container_id
51
+ container_id_for(container_name: container_name_with_version)
52
+ end
53
+
54
+ def health_url
55
+ "http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}"
56
+ end
57
+ end
@@ -0,0 +1,14 @@
1
+ class Kamal::Commands::Hook < Kamal::Commands::Base
2
+ def run(hook, **details)
3
+ [ hook_file(hook), env: tags(**details).env ]
4
+ end
5
+
6
+ def hook_exists?(hook)
7
+ Pathname.new(hook_file(hook)).exist?
8
+ end
9
+
10
+ private
11
+ def hook_file(hook)
12
+ "#{config.hooks_path}/#{hook}"
13
+ end
14
+ end