dockside 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +27 -0
- data/LICENSE.txt +21 -0
- data/README.md +479 -0
- data/lib/dockside/autostart.rb +33 -0
- data/lib/dockside/commands.rb +60 -0
- data/lib/dockside/compose.rb +68 -0
- data/lib/dockside/dependency.rb +216 -0
- data/lib/dockside/docker.rb +61 -0
- data/lib/dockside/errors.rb +42 -0
- data/lib/dockside/override.rb +51 -0
- data/lib/dockside/project.rb +133 -0
- data/lib/dockside/provisioner.rb +81 -0
- data/lib/dockside/railtie.rb +16 -0
- data/lib/dockside/readiness.rb +120 -0
- data/lib/dockside/registry.rb +55 -0
- data/lib/dockside/runner.rb +55 -0
- data/lib/dockside/settings.rb +38 -0
- data/lib/dockside/version.rb +3 -0
- data/lib/dockside.rb +101 -0
- data/lib/generators/dockside/install_generator.rb +15 -0
- data/lib/generators/dockside/templates/dockside.yml.tt +25 -0
- data/lib/tasks/dockside.rake +38 -0
- metadata +97 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Dockside
|
|
4
|
+
# One service of the compose file in one environment: start, stop, reset, exec.
|
|
5
|
+
class Dependency
|
|
6
|
+
LOG_LINES_ON_ERROR = 50
|
|
7
|
+
|
|
8
|
+
attr_reader :name, :project, :settings, :config
|
|
9
|
+
|
|
10
|
+
def initialize(name:, project:, settings:, config:, shared: false)
|
|
11
|
+
@name = name
|
|
12
|
+
@project = project
|
|
13
|
+
@settings = settings
|
|
14
|
+
@config = config
|
|
15
|
+
@shared = shared
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def env
|
|
19
|
+
project.env
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def shared?
|
|
23
|
+
@shared
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def autostart?
|
|
27
|
+
settings.autostart?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def port
|
|
31
|
+
published = config.dig("ports", 0, "published")
|
|
32
|
+
published&.to_s&.[](/\d+/)&.to_i
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def container_port
|
|
36
|
+
config.dig("ports", 0, "target")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def url
|
|
40
|
+
"http://localhost:#{port}" if port
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def container_name
|
|
44
|
+
"#{project.name}-#{name}-1"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def running?
|
|
48
|
+
container&.running? || false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ready?
|
|
52
|
+
running? && probe.ready?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def start(build: false, pull: false)
|
|
56
|
+
with_lock do
|
|
57
|
+
docker.preflight!
|
|
58
|
+
create_bind_mount_directories
|
|
59
|
+
return if !build && !pull && reuse_running_container
|
|
60
|
+
|
|
61
|
+
Dockside.log "starting #{name} (#{env})"
|
|
62
|
+
compose_up(build: build, pull: pull)
|
|
63
|
+
wait_until_ready
|
|
64
|
+
provisioner.run(container.id)
|
|
65
|
+
remember_config
|
|
66
|
+
Dockside.log "#{name} ready at #{url}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ensure_running!
|
|
71
|
+
start
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stop
|
|
75
|
+
compose.stop(name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def reset
|
|
79
|
+
remove
|
|
80
|
+
start
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Removes the container, its volumes and the content of its data folders under tmp/.
|
|
84
|
+
def remove
|
|
85
|
+
compose.remove(name)
|
|
86
|
+
remove_shared_container
|
|
87
|
+
named_volumes.each { |volume| docker.remove_volume(volume) }
|
|
88
|
+
bind_mount_sources.select { |source| source.to_s.start_with?(project.root.join("tmp").to_s) }.each { |source| wipe(source) }
|
|
89
|
+
FileUtils.rm_f(provisioner.marker)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def remove_shared_container
|
|
93
|
+
return unless shared?
|
|
94
|
+
|
|
95
|
+
other = docker.find_container(project: "#{project.app_name}-#{project.other_env}", service: name)
|
|
96
|
+
docker.remove_container(other.id) if other
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def exec(command, environment: {}, stdin: nil, workdir: nil, user: nil, allow_failure: false)
|
|
100
|
+
result = compose.exec(name, command, environment: environment, stdin: stdin, workdir: workdir, user: user)
|
|
101
|
+
raise CommandFailed.new(result) unless result.success? || allow_failure
|
|
102
|
+
|
|
103
|
+
result.stdout
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def exec_succeeds?(command)
|
|
107
|
+
compose.exec(name, command).success?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def logs(tail: 100)
|
|
111
|
+
compose.logs(name, tail: tail).stdout
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def probe
|
|
115
|
+
@probe ||= Readiness.probe_for(self, settings.ready)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def provisioner
|
|
119
|
+
@provisioner ||= Provisioner.new(self)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def compose
|
|
125
|
+
project.compose
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def docker
|
|
129
|
+
project.docker
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def container
|
|
133
|
+
docker.find_container(project: project.name, service: name)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def with_lock
|
|
137
|
+
project.work_dir.mkpath
|
|
138
|
+
File.open(project.work_dir.join(".lock"), File::RDWR | File::CREAT) do |file|
|
|
139
|
+
file.flock(File::LOCK_EX)
|
|
140
|
+
yield
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def reuse_running_container
|
|
145
|
+
if !config_changed? && ready?
|
|
146
|
+
Dockside.log "#{name} already running at #{url}"
|
|
147
|
+
return true
|
|
148
|
+
end
|
|
149
|
+
return false unless shared?
|
|
150
|
+
|
|
151
|
+
other = docker.find_container(project: "#{project.app_name}-#{project.other_env}", service: name)
|
|
152
|
+
return false unless other&.running? && probe.ready?
|
|
153
|
+
|
|
154
|
+
Dockside.log "#{name} shared with #{project.other_env} at #{url}"
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def compose_up(build:, pull:)
|
|
159
|
+
result = compose.up(name, build: build, pull: pull, timeout: settings.timeout)
|
|
160
|
+
return if result.success?
|
|
161
|
+
|
|
162
|
+
output = result.stdout
|
|
163
|
+
if output.match?(/port is already allocated|address already in use/i)
|
|
164
|
+
raise PortInUse, "#{name} (#{env}) could not start because port #{port} is in use. " \
|
|
165
|
+
"Run `docker ps` to see whether another container holds it.\n#{output}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
raise CommandFailed.new(result, "#{name} (#{env}) failed to start:\n#{output}\nLast #{LOG_LINES_ON_ERROR} log lines:\n#{last_logs}")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def wait_until_ready
|
|
172
|
+
Readiness.wait(probe, timeout: settings.timeout)
|
|
173
|
+
rescue ReadyTimeout
|
|
174
|
+
raise ReadyTimeout, "#{name} (#{env}) did not become ready within #{settings.timeout}s " \
|
|
175
|
+
"(waited until #{probe}). Last #{LOG_LINES_ON_ERROR} log lines:\n#{last_logs}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def last_logs
|
|
179
|
+
logs(tail: LOG_LINES_ON_ERROR).chomp
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def config_file
|
|
183
|
+
project.work_dir.join("#{name}.config.json")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def config_changed?
|
|
187
|
+
!config_file.exist? || config_file.read != JSON.generate(config)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def remember_config
|
|
191
|
+
config_file.write(JSON.generate(config))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def create_bind_mount_directories
|
|
195
|
+
bind_mount_sources.select { |source| source.to_s.start_with?(project.root.to_s) }.each do |source|
|
|
196
|
+
FileUtils.mkdir_p(source)
|
|
197
|
+
FileUtils.chmod(0o777, source)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Empties the folder but keeps it: Docker Desktop keeps a deleted folder mounted as a stale mount.
|
|
202
|
+
def wipe(directory)
|
|
203
|
+
return unless directory.directory?
|
|
204
|
+
|
|
205
|
+
directory.children.each { |child| FileUtils.rm_rf(child) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def bind_mount_sources
|
|
209
|
+
Array(config["volumes"]).select { |volume| volume["type"] == "bind" }.map { |volume| Pathname.new(volume["source"]) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def named_volumes
|
|
213
|
+
Array(config["volumes"]).select { |volume| volume["type"] == "volume" }.map { |volume| "#{project.name}_#{volume["source"]}" }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Dockside
|
|
2
|
+
# The docker commands the gem needs besides compose: checks, container lookup, removal.
|
|
3
|
+
class Docker
|
|
4
|
+
Container = Struct.new(:id, :state) do
|
|
5
|
+
def running?
|
|
6
|
+
state == "running"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(runner)
|
|
11
|
+
@runner = runner
|
|
12
|
+
@checked = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def preflight!
|
|
16
|
+
return if @checked
|
|
17
|
+
|
|
18
|
+
check_daemon
|
|
19
|
+
check_compose
|
|
20
|
+
@checked = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The container compose created for the service in the project, or nil.
|
|
24
|
+
def find_container(project:, service:)
|
|
25
|
+
result = @runner.run!([
|
|
26
|
+
"docker", "ps", "--all",
|
|
27
|
+
"--filter", "label=com.docker.compose.project=#{project}",
|
|
28
|
+
"--filter", "label=com.docker.compose.service=#{service}",
|
|
29
|
+
"--format", "{{.ID}} {{.State}}"
|
|
30
|
+
])
|
|
31
|
+
id, state = result.stdout.lines.first&.split
|
|
32
|
+
Container.new(id, state) if id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def remove_container(id)
|
|
36
|
+
@runner.run!(["docker", "rm", "--force", "--volumes", id])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def remove_volume(name)
|
|
40
|
+
@runner.run!(["docker", "volume", "rm", "--force", name])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def check_daemon
|
|
46
|
+
result = @runner.run(["docker", "version", "--format", "{{.Server.Version}}"])
|
|
47
|
+
return if result.success?
|
|
48
|
+
|
|
49
|
+
raise DockerUnavailable, "Docker is installed but the daemon does not answer: #{result.stderr.strip}\n" \
|
|
50
|
+
"Start Docker, or start with DOCKSIDE_AUTOSTART=0 to skip the containers."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def check_compose
|
|
54
|
+
result = @runner.run(["docker", "compose", "version"])
|
|
55
|
+
return if result.success?
|
|
56
|
+
|
|
57
|
+
raise ComposeMissing, "The docker compose plugin is missing. Install it from " \
|
|
58
|
+
"https://docs.docker.com/compose/install/ (docker-compose v1 is not supported)."
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Dockside
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
|
|
4
|
+
class ConfigError < Error; end
|
|
5
|
+
|
|
6
|
+
class UnknownDependency < Error; end
|
|
7
|
+
|
|
8
|
+
class DockerMissing < Error; end
|
|
9
|
+
|
|
10
|
+
class DockerUnavailable < Error; end
|
|
11
|
+
|
|
12
|
+
class ComposeMissing < Error; end
|
|
13
|
+
|
|
14
|
+
class ReadyTimeout < Error; end
|
|
15
|
+
|
|
16
|
+
class PortInUse < Error; end
|
|
17
|
+
|
|
18
|
+
class CommandFailed < Error
|
|
19
|
+
attr_reader :argv, :stdout, :stderr, :exit_status
|
|
20
|
+
|
|
21
|
+
def initialize(result, message = nil)
|
|
22
|
+
@argv = result.argv
|
|
23
|
+
@stdout = result.stdout
|
|
24
|
+
@stderr = result.stderr
|
|
25
|
+
@exit_status = result.exit_status
|
|
26
|
+
super(message || default_message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The last lines the command printed, stderr first.
|
|
30
|
+
def output_tail(lines = 10)
|
|
31
|
+
[stderr, stdout].map(&:strip).reject(&:empty?).join("\n").lines.last(lines).join.strip
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def default_message
|
|
37
|
+
message = "`#{argv.join(" ")}` failed with status #{exit_status}"
|
|
38
|
+
details = [stderr, stdout].map(&:strip).reject(&:empty?).join("\n")
|
|
39
|
+
details.empty? ? message : "#{message}:\n#{details}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Dockside
|
|
2
|
+
# The compose override file the gem generates for one environment: the environment block of every
|
|
3
|
+
# service plus the conventions (image tag for built services, host.docker.internal).
|
|
4
|
+
class Override
|
|
5
|
+
def initialize(project)
|
|
6
|
+
@project = project
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def to_hash
|
|
10
|
+
services = @project.raw_services.to_h do |service, definition|
|
|
11
|
+
[service, service_override(service, definition)]
|
|
12
|
+
end
|
|
13
|
+
{"services" => services}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_yaml
|
|
17
|
+
tree = Psych::Visitors::YAMLTree.create
|
|
18
|
+
tree << to_hash
|
|
19
|
+
stream = tree.tree
|
|
20
|
+
mark_environment_lists_as_override(stream.children.first.root)
|
|
21
|
+
stream.to_yaml
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def service_override(service, definition)
|
|
27
|
+
override = Settings.compose_overrides(definition, @project.env)
|
|
28
|
+
override["extra_hosts"] = Array(override["extra_hosts"] || definition["extra_hosts"]) | [Project::HOST_GATEWAY]
|
|
29
|
+
override["image"] = "#{@project.app_name}-#{service}" if definition.key?("build") && !definition.key?("image")
|
|
30
|
+
override
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def environment_keys(service)
|
|
34
|
+
Settings.compose_overrides(@project.raw_services.fetch(service), @project.env).keys
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Lists in an environment block replace the list of the service instead of being appended to it.
|
|
38
|
+
def mark_environment_lists_as_override(root)
|
|
39
|
+
services = root.children.last
|
|
40
|
+
services.children.each_slice(2) do |name, service|
|
|
41
|
+
replaced = environment_keys(name.value)
|
|
42
|
+
service.children.each_slice(2) do |key, value|
|
|
43
|
+
next unless value.is_a?(Psych::Nodes::Sequence) && replaced.include?(key.value)
|
|
44
|
+
|
|
45
|
+
value.tag = "!override"
|
|
46
|
+
value.implicit = false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module Dockside
|
|
2
|
+
# The app's compose file and the files the gem derives from it for one environment.
|
|
3
|
+
class Project
|
|
4
|
+
ENVIRONMENTS = %w[development test].freeze
|
|
5
|
+
HOST_GATEWAY = "host.docker.internal:host-gateway".freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :root, :env, :app_name, :runner
|
|
8
|
+
|
|
9
|
+
def initialize(root:, env:, app_name:, runner:)
|
|
10
|
+
@root = Pathname.new(root)
|
|
11
|
+
@env = env.to_s
|
|
12
|
+
@app_name = app_name
|
|
13
|
+
@runner = runner
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def name
|
|
17
|
+
"#{app_name}-#{env}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def other_env
|
|
21
|
+
(env == "test") ? "development" : "test"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def compose_file
|
|
25
|
+
root.join("config/dockside.yml")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def env_file
|
|
29
|
+
file = root.join("config/dockside.#{env}.yml")
|
|
30
|
+
file if file.exist?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def work_dir
|
|
34
|
+
root.join("tmp/dockside", env)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def override_file
|
|
38
|
+
work_dir.join("override.yml")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The app's compose file without the x-dockside part, so compose does not interpolate
|
|
42
|
+
# ${...} inside after_start commands or warn about them.
|
|
43
|
+
def plain_file
|
|
44
|
+
work_dir.join("dockside.yml")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def files
|
|
48
|
+
[plain_file, env_file, override_file].compact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def compose_env
|
|
52
|
+
{"RAILS_ENV" => env, "DOCKSIDE_ENV" => env}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def compose
|
|
56
|
+
@compose ||= Compose.new(self)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def docker
|
|
60
|
+
@docker ||= Docker.new(runner)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def service_names
|
|
64
|
+
raw_services.keys
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def settings(service)
|
|
68
|
+
Settings.for(service, raw_services.fetch(service), env)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The first host port the service publishes in the given environment, read from the file
|
|
72
|
+
# before compose resolves it. Used to find out whether two environments share a container.
|
|
73
|
+
def host_port(service, environment)
|
|
74
|
+
definition = raw_services.fetch(service)
|
|
75
|
+
ports = definition.dig("x-dockside", environment, "ports") || definition["ports"] || []
|
|
76
|
+
first = ports.first
|
|
77
|
+
return if first.nil?
|
|
78
|
+
|
|
79
|
+
published = first.is_a?(Hash) ? first["published"] : first.to_s.split(":")[-2]
|
|
80
|
+
published&.to_s
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Writes the gem's files and lets compose merge everything. Returns the resolved services.
|
|
84
|
+
def resolve!
|
|
85
|
+
write_files
|
|
86
|
+
compose.config.fetch("services")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The compose file with everything merged, ready for a plain `docker compose -f`.
|
|
90
|
+
def resolved_yaml
|
|
91
|
+
write_files
|
|
92
|
+
compose.config_yaml
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def write_files
|
|
96
|
+
work_dir.mkpath
|
|
97
|
+
plain_file.write(plain_document.to_yaml)
|
|
98
|
+
override_file.write(Override.new(self).to_yaml)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def raw_services
|
|
102
|
+
raw_document.fetch("services")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def raw_document
|
|
108
|
+
@raw_document ||= load_raw_document
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def load_raw_document
|
|
112
|
+
raise ConfigError, "#{compose_file} does not exist. Run `bin/rails generate dockside:install`." unless compose_file.exist?
|
|
113
|
+
|
|
114
|
+
document = YAML.safe_load(compose_file.read, aliases: true) || {}
|
|
115
|
+
document["services"] ||= {}
|
|
116
|
+
document["services"].each { |service, definition| validate(service, definition) }
|
|
117
|
+
document
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def plain_document
|
|
121
|
+
services = raw_services.transform_values { |definition| definition.except("x-dockside") }
|
|
122
|
+
raw_document.merge("services" => services)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def validate(service, definition)
|
|
126
|
+
if definition.key?("container_name")
|
|
127
|
+
raise ConfigError, "Service #{service} sets container_name. dockside names containers " \
|
|
128
|
+
"#{app_name}-<environment>-#{service}-1, remove container_name."
|
|
129
|
+
end
|
|
130
|
+
Settings.validate(service, definition["x-dockside"])
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Dockside
|
|
2
|
+
# Runs the after_start steps of a dependency once per container.
|
|
3
|
+
class Provisioner
|
|
4
|
+
Step = Struct.new(:exec, :run, :environment, :workdir, :user, :stdin, :allow_failure, :always)
|
|
5
|
+
|
|
6
|
+
class Step
|
|
7
|
+
KEYS = %w[exec run environment workdir user stdin allow_failure always].freeze
|
|
8
|
+
|
|
9
|
+
def self.parse_all(service, steps)
|
|
10
|
+
Array(steps).map { |step| parse(service, step) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.parse(service, step)
|
|
14
|
+
unless step.is_a?(Hash) && (step.keys - KEYS).empty? && [step["exec"], step["run"]].compact.one?
|
|
15
|
+
raise ConfigError, "#{service}: every after_start step needs exec: or run:, " \
|
|
16
|
+
"with the options #{(KEYS - %w[exec run]).join(", ")}. Got #{step.inspect}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
new(**step.transform_keys(&:to_sym))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def description
|
|
23
|
+
exec ? "exec #{Array(exec).join(" ")}" : "run #{Array(run).join(" ")}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(dependency)
|
|
28
|
+
@dependency = dependency
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def marker
|
|
32
|
+
@dependency.project.work_dir.join("#{@dependency.name}.provisioned")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def provisioned?(container_id)
|
|
36
|
+
marker.exist? && marker.read.strip == container_id
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Runs every step for a new container, only the `always` steps for a known one.
|
|
40
|
+
def run(container_id)
|
|
41
|
+
due = !provisioned?(container_id)
|
|
42
|
+
@dependency.settings.after_start.each do |step|
|
|
43
|
+
run_step(step) if due || step.always
|
|
44
|
+
end
|
|
45
|
+
marker.write(container_id) if due
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def run_step(step)
|
|
51
|
+
Dockside.log "#{@dependency.name}: #{step.description}"
|
|
52
|
+
environment = injected_environment.merge((step.environment || {}).transform_keys(&:to_s))
|
|
53
|
+
stdin = step.stdin && @dependency.project.root.join(step.stdin).read
|
|
54
|
+
if step.exec
|
|
55
|
+
@dependency.exec(step.exec, environment: environment, stdin: stdin, workdir: step.workdir, user: step.user)
|
|
56
|
+
else
|
|
57
|
+
run_on_host(step, environment, stdin)
|
|
58
|
+
end
|
|
59
|
+
rescue CommandFailed => error
|
|
60
|
+
raise unless step.allow_failure
|
|
61
|
+
|
|
62
|
+
Dockside.log "#{@dependency.name}: the step failed, moving on because of allow_failure:\n#{error.output_tail}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def run_on_host(step, environment, stdin)
|
|
66
|
+
command = step.run.is_a?(Array) ? step.run : ["sh", "-c", step.run]
|
|
67
|
+
result = @dependency.project.runner.run(command, env: environment, stdin: stdin, chdir: @dependency.project.root)
|
|
68
|
+
raise CommandFailed.new(result) unless result.success?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def injected_environment
|
|
72
|
+
{
|
|
73
|
+
"DOCKSIDE_NAME" => @dependency.name,
|
|
74
|
+
"DOCKSIDE_ENV" => @dependency.env,
|
|
75
|
+
"DOCKSIDE_PORT" => @dependency.port.to_s,
|
|
76
|
+
"DOCKSIDE_URL" => @dependency.url.to_s,
|
|
77
|
+
"DOCKSIDE_CONTAINER" => @dependency.container_name
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require "rails/railtie"
|
|
2
|
+
|
|
3
|
+
module Dockside
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
config.dockside = ActiveSupport::OrderedOptions.new
|
|
6
|
+
config.dockside.autostart = nil
|
|
7
|
+
|
|
8
|
+
rake_tasks do
|
|
9
|
+
load File.expand_path("../tasks/dockside.rake", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
config.after_initialize do |app|
|
|
13
|
+
Autostart.boot(app.config.dockside, env: Rails.env, server_process: defined?(Rails::Server))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|