moby-derp 0.4.0 → 0.7.1

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: 31c2c89cac1b196c48dc2c2183367d61578b1e4e1bae1ff7d698dd7ef6c23cb5
4
- data.tar.gz: c7b83663a971a01f3e8b5e8a5420b12b6192d207107ce1cd3047539644051f3e
3
+ metadata.gz: f4f3f67740bc9579d1b4e2af6e95801e2f019f65b2fddcc495e5ef1c24084565
4
+ data.tar.gz: a78bf5f24219288581f7bc24137b372e2c9738a9ba2245266b176c800e8db0f8
5
5
  SHA512:
6
- metadata.gz: 34c40eb8d1d8cfa668a3048c6a8ceaf231c4fae6a607d389bf073d3a3874a74a10a31e728c0c322476a154c9c425fd92869d0fb5849707c1dee2b4c0970f15af
7
- data.tar.gz: 01d15f3f197b48fe5748f27718a8e8c7035ff0713113742bf1ae7a43a480197ab7a97559e4ae19fbec8ca5711d22ebd38f02865194e9ce73a817b11a0899bf9a
6
+ metadata.gz: 724548d882433981999f8c6ba64ba48b3e227de6697a23fb2bd01819acf50a711fd880f685825099e25864aad0736fdc55cd7098ed4618eae83a38bf6a63d472
7
+ data.tar.gz: be5f791cc9b56efb0be63fc56bd6d788ad4d6617aed29360452a8c5c44d7d83228734edb3c0ad0c8bbbfda58c680e71bf65503ced36db2547876efa5e95bff93
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
@@ -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"
@@ -5,6 +5,8 @@ require "docker-api"
5
5
  require "ipaddr"
6
6
  require "json/canonicalization"
7
7
 
8
+ require_relative "./freedom_patches/docker/credential"
9
+
8
10
  module MobyDerp
9
11
  class Container
10
12
  include LoggingHelpers
@@ -43,7 +45,35 @@ module MobyDerp
43
45
  end
44
46
 
45
47
  begin
46
- Docker::Container.create(hash_labelled(container_creation_parameters)).start!.id
48
+ c = Docker::Container.create(hash_labelled(container_creation_parameters))
49
+ c.start!.object_id
50
+
51
+ if @config.startup_health_check
52
+ attempts = @config.startup_health_check[:attempts]
53
+
54
+ while attempts > 0
55
+ stdout, stderr, exitstatus = c.exec(@config.startup_health_check[:command])
56
+ if exitstatus > 0
57
+ stdout_lines = stdout.empty? ? [] : ["stdout:"] + stdout.join("\n").split("\n").map { |l| " #{l}" }
58
+ stderr_lines = stderr.empty? ? [] : ["stderr:"] + stderr.join("\n").split("\n").map { |l| " #{l}" }
59
+ output_lines = stdout_lines + stderr_lines
60
+ @logger.warn(logloc) { "Startup health check failed on #{container_name} with status #{exitstatus}." + (output_lines.empty? ? "" : ([" Output:"] + output_lines.join("\n "))) }
61
+
62
+ attempts -= 1
63
+ sleep @config.startup_health_check[:interval]
64
+ else
65
+ @logger.info(logloc) { "Startup health check passed." }
66
+ break
67
+ end
68
+ end
69
+
70
+ if attempts == 0
71
+ raise MobyDerp::StartupHealthCheckError,
72
+ "Container #{container_name} has failed the startup health check command #{@config.startup_health_check[:attempts]} times. Aborting."
73
+ end
74
+ end
75
+
76
+ c.id
47
77
  rescue Docker::Error::ClientError => ex
48
78
  raise MobyDerp::ContainerError,
49
79
  "moby daemon returned error: #{ex.message}"
@@ -60,7 +90,7 @@ module MobyDerp
60
90
  "Init" => true,
61
91
  }
62
92
  params["MacAddress"] = container_mac_address
63
- if network_uses_ipv6?
93
+ if network_uses_ipv6? && user_defined_network?
64
94
  params["NetworkingConfig"] = {
65
95
  "EndpointsConfig" => {
66
96
  @pod.network_name => {
@@ -127,7 +157,9 @@ module MobyDerp
127
157
  params["Labels"] = @pod.common_labels.merge(@config.labels)
128
158
  params["Labels"]["org.hezmatt.moby-derp.pod-name"] = @pod.name
129
159
 
130
- unless @root_container
160
+ if @root_container
161
+ params["Labels"] = @pod.root_labels.merge(params["Labels"])
162
+ else
131
163
  params["Labels"]["org.hezmatt.moby-derp.root-container-id"] = @pod.root_container_id
132
164
  end
133
165
  end
@@ -222,6 +254,10 @@ module MobyDerp
222
254
  docker_network.info["EnableIPv6"]
223
255
  end
224
256
 
257
+ def user_defined_network?
258
+ !%w{bridge host none}.include?(@pod.network_name)
259
+ end
260
+
225
261
  def container_ipv6_address
226
262
  network, masklen = ipv6_network.split("/", 2)
227
263
  network = IPAddr.new(network)
@@ -1,4 +1,4 @@
1
- require_relative "../freedom_patches/docker/image"
1
+ require_relative "./freedom_patches/docker/image"
2
2
  require_relative "./error"
3
3
  require_relative "./mount"
4
4
 
@@ -8,7 +8,8 @@ require "shellwords"
8
8
  module MobyDerp
9
9
  class ContainerConfig
10
10
  attr_reader :name, :image, :update_image, :command, :environment, :mounts,
11
- :labels, :readonly, :stop_signal, :stop_timeout, :user, :restart, :limits
11
+ :labels, :readonly, :stop_signal, :stop_timeout, :user, :restart, :limits,
12
+ :startup_health_check
12
13
 
13
14
  def initialize(system_config:,
14
15
  pod_config:,
@@ -24,13 +25,14 @@ module MobyDerp
24
25
  stop_timeout: 10,
25
26
  user: nil,
26
27
  restart: "no",
27
- limits: {}
28
+ limits: {},
29
+ startup_health_check: nil
28
30
  )
29
31
  @system_config, @pod_config, @name, @image = system_config, pod_config, "#{pod_config.name}.#{container_name}", image
30
32
 
31
33
  @update_image, @command, @environment, @mounts, @labels = update_image, command, environment, mounts, labels
32
34
  @readonly, @stop_signal, @stop_timeout, @user, @restart = readonly, stop_signal, stop_timeout, user, restart
33
- @limits = limits
35
+ @limits, @startup_health_check = limits, startup_health_check
34
36
 
35
37
  validate_image
36
38
  validate_update_image
@@ -44,6 +46,7 @@ module MobyDerp
44
46
  validate_user
45
47
  validate_restart
46
48
  validate_limits
49
+ validate_startup_health_check
47
50
  end
48
51
 
49
52
  private
@@ -331,6 +334,51 @@ module MobyDerp
331
334
  end
332
335
  end
333
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
+
334
382
  def validate_boolean(name)
335
383
  v = instance_variable_get(:"@#{name}")
336
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
@@ -0,0 +1,91 @@
1
+ require "json"
2
+ require "open3"
3
+ require "pathname"
4
+ require "uri"
5
+
6
+ module Docker
7
+ module Credential
8
+ #:nocov:
9
+ def self.for(ref)
10
+ image_cred(ref)
11
+ end
12
+
13
+ private
14
+
15
+ def self.image_cred(ref)
16
+ cred_helper = hunt_for_image_domain_cred(ref, docker_config.fetch("credHelpers", {}))
17
+
18
+ if cred_helper
19
+ out, rv = Open3.capture2e("docker-credential-#{cred_helper}", "get", stdin_data: image_domain(ref))
20
+
21
+ if rv.exitstatus == 0
22
+ cred_data = JSON.parse(out)
23
+
24
+ { username: cred_data["Username"], password: cred_data["Secret"], serveraddress: image_domain(ref) }
25
+ else
26
+ raise RuntimeError, "Credential helper docker-credential-#{cred_helper} exited with #{rv.exitstatus}: #{out}"
27
+ end
28
+ else
29
+ cred = hunt_for_image_domain_cred(ref, docker_config.fetch("auths", {}))
30
+
31
+ if cred
32
+ user, pass = JSON.parse(cred.fetch("auth", "null"))&.unpack("m")&.first&.split(":", 2)
33
+
34
+ if user && pass
35
+ { username: user, password: pass, serveraddress: image_domain(ref) }
36
+ else
37
+ {}
38
+ end
39
+ else
40
+ {}
41
+ end
42
+ end
43
+ end
44
+
45
+ def self.hunt_for_image_domain_cred(ref, section)
46
+ section.find do |k, v|
47
+ if k =~ /:\/\//
48
+ # Doin' it URL style
49
+ URI(k).host == image_domain(ref)
50
+ else
51
+ k == image_domain(ref)
52
+ end
53
+ end&.last
54
+ end
55
+
56
+ def self.image_domain(ref)
57
+ if match_data = ref.match(Docker::Image::IMAGE_REFERENCE)
58
+ if match_data[1] =~ /[.:]/
59
+ match_data[1].gsub(/\/\z/, '')
60
+ else
61
+ "index.docker.io"
62
+ end
63
+ else
64
+ raise ArgumentError, "Could not parse image ref #{ref.inspect}"
65
+ end
66
+ end
67
+
68
+ def self.docker_config
69
+ if (f = Pathname.new(ENV.fetch("DOCKER_CONFIG", "~/.docker")).expand_path.join("config.json")).exist?
70
+ JSON.parse(f.read)
71
+ else
72
+ {}
73
+ end
74
+ end
75
+
76
+ module ImageClassMixin
77
+ def create(opts = {}, creds = nil, conn = Docker.connection, &block)
78
+ if creds.nil?
79
+ image = opts["fromImage"] || opts[:fromImage]
80
+
81
+ creds = Docker::Credential.for(image)
82
+ end
83
+
84
+ super(opts, creds, conn, &block)
85
+ end
86
+ end
87
+ #:nocov:
88
+ end
89
+ end
90
+
91
+ Docker::Image.singleton_class.prepend(Docker::Credential::ImageClassMixin)
@@ -18,6 +18,20 @@ module MobyDerp
18
18
 
19
19
  @logger.debug(logloc) { "Root container ID is #{@root_container_id}" }
20
20
 
21
+ desired_container_names = @config.containers.map(&:name)
22
+
23
+ Docker::Container.all(all: true).each do |c|
24
+ c_name = c.info["Names"].first.sub(/^\//, '')
25
+
26
+ if c.info["Labels"]["org.hezmatt.moby-derp.pod-name"] == name &&
27
+ !c.info["Labels"]["org.hezmatt.moby-derp.root-container-id"].nil? &&
28
+ !desired_container_names.include?(c_name.split(".", 2).last)
29
+ @logger.info(logloc) { "Removing stale container #{c_name}" }
30
+ c.stop
31
+ c.delete
32
+ end
33
+ end
34
+
21
35
  @config.containers.each do |cfg|
22
36
  @logger.info(logloc) { "Checking container #{cfg.name}" }
23
37
 
@@ -48,6 +62,10 @@ module MobyDerp
48
62
  @config.common_labels
49
63
  end
50
64
 
65
+ def root_labels
66
+ @config.root_labels
67
+ end
68
+
51
69
  def common_environment
52
70
  @config.common_environment
53
71
  end
@@ -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,
@@ -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,
@@ -100,6 +115,11 @@ module MobyDerp
100
115
  raise ConfigurationError,
101
116
  "container name #{name.inspect} is invalid (must contain only alphanumerics, underscores, and hyphens)"
102
117
  end
118
+
119
+ unless data.is_a?(Hash)
120
+ raise ConfigurationError,
121
+ "container data must be a hash"
122
+ end
103
123
  end
104
124
 
105
125
  begin
@@ -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,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
+ }
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.4.0
4
+ version: 0.7.1
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-20 00:00:00.000000000 Z
11
+ date: 2020-07-01 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
@@ -208,13 +194,13 @@ files:
208
194
  - README.md
209
195
  - bin/moby-derp
210
196
  - example.yml
211
- - lib/freedom_patches/docker/image.rb
212
197
  - lib/moby_derp/config_file.rb
213
198
  - lib/moby_derp/container.rb
214
199
  - lib/moby_derp/container_config.rb
215
200
  - lib/moby_derp/error.rb
201
+ - lib/moby_derp/freedom_patches/docker/credential.rb
202
+ - lib/moby_derp/freedom_patches/docker/image.rb
216
203
  - lib/moby_derp/logging_helpers.rb
217
- - lib/moby_derp/moby_info.rb
218
204
  - lib/moby_derp/mount.rb
219
205
  - lib/moby_derp/pod.rb
220
206
  - lib/moby_derp/pod_config.rb
@@ -223,6 +209,7 @@ files:
223
209
  - smoke_tests/exposed.bats
224
210
  - smoke_tests/minimal.bats
225
211
  - smoke_tests/no_file.bats
212
+ - smoke_tests/root_labels.bats
226
213
  - smoke_tests/test_helper.bash
227
214
  homepage: http://github.com/mpalmer/moby-derp
228
215
  licenses: []
@@ -242,7 +229,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
229
  - !ruby/object:Gem::Version
243
230
  version: '0'
244
231
  requirements: []
245
- rubygems_version: 3.0.1
232
+ rubygems_version: 3.0.3
246
233
  signing_key:
247
234
  specification_version: 4
248
235
  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