moby-derp 0.3.2 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53b4f6a0a6db392604195b677bdbf933478ec68a389da49d498df62feb950612
4
- data.tar.gz: 36a12fa06801120ff45273bc58771b9e513a75f83ebfc95d803e7eb82545ef11
3
+ metadata.gz: 513a47c0b56a264731bf12050cbc651c7799c4ec60955c06e8cff46377328825
4
+ data.tar.gz: 01d2f26f2fa6300cdddf599b9f544616d1781c4a53879ec009bec1d4dd340b9d
5
5
  SHA512:
6
- metadata.gz: 9b566b1f24b01247b56bf48814866a804f8272055baa80d6e0ca131e158314827a68c56abfe7d669ae1465d17436e8ca5af54f1a916fc51715866503564c40b5
7
- data.tar.gz: 91fb4f476900a4790fc4b96d3b27c488a67de334cc71bd8efb446b9e5282d029194d40dfbd703edd242162bbaf43155249d607767d7fc17d5053bb54fc8b7d7d
6
+ metadata.gz: 307556e56b26e97437df00b9db1277f254554ba88af5a22d9b786de2daba8a30d91f5c932c3b28f4cebb972175958c7c1be81d3cd8ad718a23638d8f696aaa12
7
+ data.tar.gz: eb0c7f7f7514dbb93911cc65037a129aadfc1888454529071f2811e606dd3291dc57e5f2ced377df3896bbaccbac22c163bd5fad4e90cc0435304bb32b65ef6a
data/README.md CHANGED
@@ -134,7 +134,7 @@ wrapper script, like this:
134
134
 
135
135
  set -e
136
136
 
137
- MOBY_DERP_SYSTEM_CONFIG_FILE=/opt/srv/etc/moby-derp/moby-derp.yaml
137
+ export MOBY_DERP_SYSTEM_CONFIG_FILE=/opt/srv/etc/moby-derp/moby-derp.yaml
138
138
 
139
139
  exec /usr/local/bin/moby-derp "$@"
140
140
 
@@ -185,7 +185,7 @@ to `moby-derp`. This means that, yes, different users need to use different
185
185
  filenames. The benefit of this is that the `sudo` configuration becomes a lot
186
186
  easier to audit -- the pod name is right there.
187
187
 
188
- This means that no matter a user does, they cannot have any effect on any
188
+ This means that no matter what a user does, they cannot have any effect on any
189
189
  container which is not named for the pod they're manipulating. There are also
190
190
  safety valves around `moby-derp`-managed containers being labelled as such, so
191
191
  that in the event that someone does inadvertently name a container in such a
@@ -220,7 +220,7 @@ still publish to ephemeral ports (using the `:containerPort` syntax, or
220
220
  `publish_all: true`) if they wish.
221
221
 
222
222
  If a pod *does* need to bind to a specific host port, then that pod/port pair
223
- should be whitelisted in the system configuration file.
223
+ should be whitelisted in the [system configuration file](#system-configuration).
224
224
 
225
225
 
226
226
  # Contributing
@@ -11,7 +11,7 @@ if ARGV.length != 1
11
11
  exit 1
12
12
  end
13
13
 
14
- logger = Logger.new($stdout)
14
+ logger = Logger.new($stderr)
15
15
  logger.formatter = ->(s, t, p, m) { "#{m}\n" }
16
16
 
17
17
  case ENV["MOBY_DERP_LOG_LEVEL"]
@@ -44,6 +44,8 @@ rescue MobyDerp::ConfigurationError => ex
44
44
  exit 1
45
45
  end
46
46
 
47
+ Docker.options = { read_timeout: 86400 }
48
+
47
49
  pod = MobyDerp::Pod.new(pod_config)
48
50
 
49
51
  begin
@@ -213,6 +213,33 @@ containers:
213
213
  ulimit-rttime: 15:16
214
214
  ulimit-stack: 17:18
215
215
 
216
+ # If you're a bit suss as to whether or not one of your containers will
217
+ # successfully start up, you can use the following section to define a
218
+ # start-time health checking regime.
219
+ #
220
+ # How it works is that the defined command is run in the container (using
221
+ # `exec`), and if-and-when that command returns a `0` exit status, the
222
+ # container is considered to be healthy and we're done. If the command
223
+ # returns a non-zero exit status, we wait for `interval` seconds and then
224
+ # retry. If the command executes `attempts` times without receiving a `0`
225
+ # exit status, the container is considered "failed", and no further
226
+ # containers in the pod will be processed, and the `moby-derp` execution
227
+ # will itself exit with a non-zero status.
228
+ startup_health_check:
229
+ # The command to run inside the container via `exec`. You can specify
230
+ # this as a string, or as an array of strings if you prefer to avoid
231
+ # shell quoting hell.
232
+ command: '/usr/local/bin/r-u-ok'
233
+
234
+ # How many seconds to wait between invocations of the command, when
235
+ # it fails. Can be any non-negative number. Defaults to 3.
236
+ interval: 3
237
+
238
+ # How many times to attempt to execute the health-check command before
239
+ # declaring the container hopelessly busticated, and aborting the
240
+ # `moby-derp` run. Must be a positive integer. Defaults to 10.
241
+ attempts: 10
242
+
216
243
  # SECTION 2: POD-LEVEL CONFIGURATION
217
244
  #
218
245
  # The remainder of the configuration items in this file correspond to settings
@@ -1,7 +1,7 @@
1
1
  require_relative "./error"
2
2
  require_relative "./logging_helpers"
3
3
 
4
- require "safe_yaml"
4
+ require "yaml"
5
5
 
6
6
  module MobyDerp
7
7
  class ConfigFile
@@ -12,7 +12,7 @@ module MobyDerp
12
12
  def initialize(filename)
13
13
  begin
14
14
  @logger.debug(logloc) { "Reading configuration file #{filename}" }
15
- @config = SafeYAML.load(File.read(filename))
15
+ @config = YAML.safe_load(File.read(filename))
16
16
  rescue Errno::ENOENT
17
17
  raise ConfigurationError,
18
18
  "file does not exist"
@@ -43,7 +43,35 @@ module MobyDerp
43
43
  end
44
44
 
45
45
  begin
46
- Docker::Container.create(hash_labelled(container_creation_parameters)).start!.id
46
+ c = Docker::Container.create(hash_labelled(container_creation_parameters))
47
+ c.start!.object_id
48
+
49
+ if @config.startup_health_check
50
+ attempts = @config.startup_health_check[:attempts]
51
+
52
+ while attempts > 0
53
+ stdout, stderr, exitstatus = c.exec(@config.startup_health_check[:command])
54
+ if exitstatus > 0
55
+ stdout_lines = stdout.empty? ? [] : ["stdout:"] + stdout.join("\n").split("\n").map { |l| " #{l}" }
56
+ stderr_lines = stderr.empty? ? [] : ["stderr:"] + stderr.join("\n").split("\n").map { |l| " #{l}" }
57
+ output_lines = stdout_lines + stderr_lines
58
+ @logger.warn(logloc) { "Startup health check failed on #{container_name} with status #{exitstatus}." + (output_lines.empty? ? "" : ([" Output:"] + output_lines.join("\n "))) }
59
+
60
+ attempts -= 1
61
+ sleep @config.startup_health_check[:interval]
62
+ else
63
+ @logger.info(logloc) { "Startup health check passed." }
64
+ break
65
+ end
66
+ end
67
+
68
+ if attempts == 0
69
+ raise MobyDerp::StartupHealthCheckError,
70
+ "Container #{container_name} has failed the startup health check command #{@config.startup_health_check[:attempts]} times. Aborting."
71
+ end
72
+ end
73
+
74
+ c.id
47
75
  rescue Docker::Error::ClientError => ex
48
76
  raise MobyDerp::ContainerError,
49
77
  "moby daemon returned error: #{ex.message}"
@@ -71,6 +99,7 @@ module MobyDerp
71
99
  }
72
100
  }
73
101
  end
102
+ params["ExposedPorts"] = Hash[@pod.expose.map { |ex| [ex, {}] }]
74
103
  else
75
104
  params["HostConfig"] = {
76
105
  "NetworkMode" => "container:#{@pod.root_container_id}",
@@ -126,7 +155,9 @@ module MobyDerp
126
155
  params["Labels"] = @pod.common_labels.merge(@config.labels)
127
156
  params["Labels"]["org.hezmatt.moby-derp.pod-name"] = @pod.name
128
157
 
129
- unless @root_container
158
+ if @root_container
159
+ params["Labels"] = @pod.root_labels.merge(params["Labels"])
160
+ else
130
161
  params["Labels"]["org.hezmatt.moby-derp.root-container-id"] = @pod.root_container_id
131
162
  end
132
163
  end
@@ -3,11 +3,13 @@ require_relative "./error"
3
3
  require_relative "./mount"
4
4
 
5
5
  require "docker-api"
6
+ require "shellwords"
6
7
 
7
8
  module MobyDerp
8
9
  class ContainerConfig
9
10
  attr_reader :name, :image, :update_image, :command, :environment, :mounts,
10
- :labels, :readonly, :stop_signal, :stop_timeout, :user, :restart, :limits
11
+ :labels, :readonly, :stop_signal, :stop_timeout, :user, :restart, :limits,
12
+ :startup_health_check
11
13
 
12
14
  def initialize(system_config:,
13
15
  pod_config:,
@@ -23,13 +25,14 @@ module MobyDerp
23
25
  stop_timeout: 10,
24
26
  user: nil,
25
27
  restart: "no",
26
- limits: {}
28
+ limits: {},
29
+ startup_health_check: nil
27
30
  )
28
31
  @system_config, @pod_config, @name, @image = system_config, pod_config, "#{pod_config.name}.#{container_name}", image
29
32
 
30
33
  @update_image, @command, @environment, @mounts, @labels = update_image, command, environment, mounts, labels
31
34
  @readonly, @stop_signal, @stop_timeout, @user, @restart = readonly, stop_signal, stop_timeout, user, restart
32
- @limits = limits
35
+ @limits, @startup_health_check = limits, startup_health_check
33
36
 
34
37
  validate_image
35
38
  validate_update_image
@@ -43,6 +46,7 @@ module MobyDerp
43
46
  validate_user
44
47
  validate_restart
45
48
  validate_limits
49
+ validate_startup_health_check
46
50
  end
47
51
 
48
52
  private
@@ -66,7 +70,10 @@ module MobyDerp
66
70
  def validate_command
67
71
  case @command
68
72
  when String
69
- true
73
+ # Despite the Docker Engine API spec saying you can pass a string,
74
+ # if you do it doesn't get parsed into arguments... so that's pretty
75
+ # fucking useless.
76
+ @command = Shellwords.split(@command)
70
77
  when Array
71
78
  unless @command.all? { |c| String === c }
72
79
  raise ConfigurationError, "all elements of the command array must be strings"
@@ -327,6 +334,51 @@ module MobyDerp
327
334
  end
328
335
  end
329
336
 
337
+ def validate_startup_health_check
338
+ if @startup_health_check.nil?
339
+ # This is fine
340
+ return
341
+ end
342
+
343
+ unless @startup_health_check.is_a?(Hash)
344
+ raise ConfigurationError,
345
+ "startup_health_check must be a hash"
346
+ end
347
+
348
+ case @startup_health_check[:command]
349
+ when String
350
+ @startup_health_check[:command] = Shellwords.split(@startup_health_check[:command])
351
+ when Array
352
+ unless @startup_health_check[:command].all? { |c| String === c }
353
+ raise ConfigurationError, "all elements of the health check command array must be strings"
354
+ end
355
+ when NilClass
356
+ raise ConfigurationError, "health check command must be specified"
357
+ else
358
+ raise ConfigurationError,
359
+ "health check command must be string or array of strings"
360
+ end
361
+
362
+ @startup_health_check[:interval] ||= 3
363
+ @startup_health_check[:attempts] ||= 10
364
+
365
+ unless Numeric === @startup_health_check[:interval]
366
+ raise ConfigurationError, "startup health check interval must be a number"
367
+ end
368
+
369
+ if @startup_health_check[:interval] < 0
370
+ raise ConfigurationError, "startup health check interval cannot be negative"
371
+ end
372
+
373
+ unless Integer === @startup_health_check[:attempts]
374
+ raise ConfigurationError, "startup health check attempt count must be an integer"
375
+ end
376
+
377
+ if @startup_health_check[:attempts] < 1
378
+ raise ConfigurationError, "startup health check attempt count must be a positive integer"
379
+ end
380
+ end
381
+
330
382
  def validate_boolean(name)
331
383
  v = instance_variable_get(:"@#{name}")
332
384
  unless v == true || v == false
@@ -9,6 +9,10 @@ module MobyDerp
9
9
  # Indicates there was a problem manipulating a live container
10
10
  class ContainerError < Error; end
11
11
 
12
+ # Raised when the startup health check has failed spectacularly for
13
+ # a container
14
+ class StartupHealthCheckError < Error; end
15
+
12
16
  # Only appears when an inviolable assertion is invalid, and indicates
13
17
  # there is a bug in the code
14
18
  class BugError < Error; end
@@ -48,6 +48,10 @@ module MobyDerp
48
48
  @config.common_labels
49
49
  end
50
50
 
51
+ def root_labels
52
+ @config.root_labels
53
+ end
54
+
51
55
  def common_environment
52
56
  @config.common_environment
53
57
  end
@@ -68,6 +72,10 @@ module MobyDerp
68
72
  @config.hostname
69
73
  end
70
74
 
75
+ def expose
76
+ @config.expose
77
+ end
78
+
71
79
  private
72
80
 
73
81
  def root_container
@@ -3,13 +3,24 @@ require_relative "./container_config"
3
3
  require_relative "./logging_helpers"
4
4
  require_relative "./mount"
5
5
 
6
- require "safe_yaml"
7
6
  require "socket"
8
7
 
9
8
  module MobyDerp
10
9
  class PodConfig < ConfigFile
11
10
  include LoggingHelpers
12
11
 
12
+ VALID_CONFIG_KEYS = %w{
13
+ containers
14
+ hostname
15
+ common_environment
16
+ common_labels
17
+ root_labels
18
+ common_mounts
19
+ expose
20
+ publish
21
+ publish_all
22
+ }
23
+
13
24
  attr_reader :name,
14
25
  :containers,
15
26
  :hostname,
@@ -21,8 +32,8 @@ module MobyDerp
21
32
  :publish,
22
33
  :publish_all,
23
34
  :mount_root,
24
- :system_config,
25
- :logger
35
+ :system_config,
36
+ :logger
26
37
 
27
38
  def initialize(filename, system_config)
28
39
  @logger = system_config.logger
@@ -34,6 +45,10 @@ module MobyDerp
34
45
  @name = File.basename(filename, ".*")
35
46
  validate_name
36
47
 
48
+ unless (bad_keys = @config.keys - VALID_CONFIG_KEYS).empty?
49
+ raise ConfigurationError,
50
+ "Invalid pod configuration key(s): #{bad_keys.inspect}"
51
+ end
37
52
 
38
53
  unless @config.has_key?("containers")
39
54
  raise ConfigurationError,
@@ -181,18 +196,23 @@ module MobyDerp
181
196
  "expose must be an array"
182
197
  end
183
198
 
184
- @expose.map!(&:to_s)
185
-
186
- @expose.each do |e|
199
+ @expose.map! do |e|
200
+ e = e.to_s
187
201
  unless e.is_a?(String) && e =~ %r{\A\d+(/(tcp|udp))?\z}
188
202
  raise ConfigurationError,
189
203
  "exposed ports must be integers, with an optional protocol specifier (got #{e.inspect})"
190
204
  end
191
205
 
206
+ if $1.nil?
207
+ e += "/tcp"
208
+ end
209
+
192
210
  if e.to_i < 1 || e.to_i > 65535
193
211
  raise ConfigurationError,
194
212
  "exposed port #{e} is out of range (expected 1-65535)"
195
213
  end
214
+
215
+ e
196
216
  end
197
217
  end
198
218
 
@@ -1,16 +1,21 @@
1
1
  require_relative "./config_file"
2
2
 
3
- require "safe_yaml"
4
-
5
3
  module MobyDerp
6
4
  class SystemConfig < ConfigFile
7
5
  attr_reader :mount_root, :port_whitelist, :network_name, :use_host_resolv_conf,
8
6
  :cpu_count, :cpu_bits
9
7
 
10
- def initialize(filename, moby_info, logger)
8
+ def initialize(config_data_or_filename, moby_info, logger)
11
9
  @logger = logger
12
10
 
13
- super(filename)
11
+ case config_data_or_filename
12
+ when String
13
+ super(config_data_or_filename)
14
+ when Hash
15
+ @config = stringify_keys(config_data_or_filename)
16
+ else
17
+ raise ArgumentError, "Unsupported type for config_data_or_filename parameter"
18
+ end
14
19
 
15
20
  @mount_root = @config["mount_root"]
16
21
  @port_whitelist = stringify_keys(@config["port_whitelist"] || {})
@@ -25,7 +25,6 @@ Gem::Specification.new do |s|
25
25
 
26
26
  s.add_runtime_dependency "docker-api"
27
27
  s.add_runtime_dependency "json-canonicalization"
28
- s.add_runtime_dependency "safe_yaml"
29
28
 
30
29
  s.add_development_dependency 'bundler'
31
30
  s.add_development_dependency 'deep_merge'
@@ -0,0 +1,28 @@
1
+ load test_helper
2
+
3
+ @test "Exposed ports" {
4
+ config_file <<-'EOF'
5
+ expose:
6
+ - 80
7
+ - "53/udp"
8
+ containers:
9
+ bob:
10
+ image: busybox:latest
11
+ command: sleep 600
12
+ common_labels:
13
+ moby-derp-smoke-test: ayup
14
+ EOF
15
+
16
+ run $MOBY_DERP_BIN $TEST_CONFIG_FILE
17
+
18
+ echo "status: $status"
19
+ echo "output: $output"
20
+
21
+ [ "$status" = "0" ]
22
+ container_running "mdst"
23
+ container_running "mdst.bob"
24
+
25
+ docker inspect mdst --format='{{.Config.ExposedPorts}}'
26
+ docker inspect mdst --format='{{.Config.ExposedPorts}}' | grep 'map.*53/udp:{}'
27
+ docker inspect mdst --format='{{.Config.ExposedPorts}}' | grep 'map.*80/tcp:{}'
28
+ }
@@ -0,0 +1,27 @@
1
+ load test_helper
2
+
3
+ @test "Core labels" {
4
+ config_file <<-'EOF'
5
+ root_labels:
6
+ foo: booblee
7
+ containers:
8
+ bob:
9
+ image: busybox:latest
10
+ command: sleep 1
11
+ common_labels:
12
+ moby-derp-smoke-test: ayup
13
+ EOF
14
+
15
+ run $MOBY_DERP_BIN $TEST_CONFIG_FILE
16
+
17
+ echo "status: $status"
18
+ echo "output: $output"
19
+
20
+ [ "$status" = "0" ]
21
+ container_running "mdst"
22
+ container_running "mdst.bob"
23
+
24
+ docker inspect mdst --format='{{.Config.Labels}}'
25
+ docker inspect mdst --format='{{.Config.Labels}}' | grep 'map.*foo:booblee'
26
+ ! docker inspect mdst.bob --format='{{.Config.Labels}}' | grep 'map.*foo:booblee'
27
+ }
@@ -23,7 +23,7 @@ EOF
23
23
  teardown() {
24
24
  for i in $(docker ps -a --format='{{.Names}}'); do
25
25
  if docker container inspect $i --format='{{.Config.Labels}}' | grep -q moby-derp-smoke-test; then
26
- docker rm -f $i
26
+ docker rm -f $i >/dev/null
27
27
  fi
28
28
  done
29
29
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moby-derp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Palmer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-03 00:00:00.000000000 Z
11
+ date: 2020-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: docker-api
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: safe_yaml
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: bundler
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -214,14 +200,15 @@ files:
214
200
  - lib/moby_derp/container_config.rb
215
201
  - lib/moby_derp/error.rb
216
202
  - lib/moby_derp/logging_helpers.rb
217
- - lib/moby_derp/moby_info.rb
218
203
  - lib/moby_derp/mount.rb
219
204
  - lib/moby_derp/pod.rb
220
205
  - lib/moby_derp/pod_config.rb
221
206
  - lib/moby_derp/system_config.rb
222
207
  - moby-derp.gemspec
208
+ - smoke_tests/exposed.bats
223
209
  - smoke_tests/minimal.bats
224
210
  - smoke_tests/no_file.bats
211
+ - smoke_tests/root_labels.bats
225
212
  - smoke_tests/test_helper.bash
226
213
  homepage: http://github.com/mpalmer/moby-derp
227
214
  licenses: []
@@ -241,7 +228,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
228
  - !ruby/object:Gem::Version
242
229
  version: '0'
243
230
  requirements: []
244
- rubygems_version: 3.0.1
231
+ rubygems_version: 3.0.3
245
232
  signing_key:
246
233
  specification_version: 4
247
234
  summary: A simple management system for a pod of moby containers
@@ -1,12 +0,0 @@
1
- module MobyDerp
2
- class MobyInfo
3
- attr_reader :cpu_count, :cpu_bits
4
-
5
- def initialize(info)
6
- @cpu_count = info["NCPU"]
7
- # As far as I can tell, the only 32-bit platform Moby supports is
8
- # armhf; if that turns out to be incorrect, amend the list below.
9
- @cpu_bits = %w{armhf}.include?(info["Architecture"]) ? 32 : 64
10
- end
11
- end
12
- end