moby-derp 0.4.1 → 0.7.2

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: 4c1246d1a7563d646c3e0626cf32e4ab35d425890d0cf61645f94e56ef11be68
4
- data.tar.gz: f51695848c2d42402c3664d514e6f6870279b40658fa2ff90253c31ab2977f36
3
+ metadata.gz: e1d04c23ce729679e9e1cd34fa7fbb63dac8672a2212cbed442cdaef698ac20e
4
+ data.tar.gz: f7cfa0d0fa9f21e6f02766669cd2a763614828a75e60ba6b35146c9c2b3d5877
5
5
  SHA512:
6
- metadata.gz: 92c6fdb5e9fff6ed7eb06ad3bdbcc0c881c174c94058ab97ab6e3ae67e0236b7f60cea48cf4814ea50a6b4684c00f47803c4182ebdaad3c486b78a529d56bb0e
7
- data.tar.gz: e1d08cf1af42df813f7a2bef9f318bafb2af7d2ee8b18f9de9e40978040023d3fb1160257271ca2fdfd9422d56dd04f1254516e0fc773bcc9ee6d83dd4f15711
6
+ metadata.gz: 877ef1db2ccb0f5b32cdb2bca422edc2bb3995fa650d65fad0fa581fc89c5ae4b3873759f2da706700c0a3a2b8b8392cc315659d4cf58b480efd5fcc1e91fc18
7
+ data.tar.gz: bf4261f7c31c01628df29e43cc42b84346525802225999e790e0268c6246b2730f9716dc087c8270cde96a75730a345276cb176e78a5d38603517532fd9c07ee
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ Gemfile.lock
4
4
  /.yardoc
5
5
  /.bundle
6
6
  /coverage
7
+ /test
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+
3
+ cache: bundler
4
+
5
+ rvm:
6
+ - 2.6
7
+ - 2.5
8
+ - 2.4
9
+ - 2.3
10
+
11
+ gemfile:
12
+ - Gemfile
data/README.md CHANGED
@@ -84,7 +84,7 @@ removed. This is used as the prefix for the name of all containers in the pod.
84
84
  Some aspects of `moby-derp`'s operation are security-sensitive, and thus shouldn't
85
85
  be able to be modified by the ordinary user. There is a
86
86
  system-wide configuration file for this purpose, by default located at
87
- `/etc/moby-derp.yml`.
87
+ `/etc/moby-derp.conf`.
88
88
 
89
89
  Its structure is quite simple. A full example looks like this:
90
90
 
@@ -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
@@ -295,8 +322,8 @@ expose:
295
322
  # in the `moby-derp` README).
296
323
  #
297
324
  publish:
298
- - :80
299
- - :1234-1237
325
+ - ":80"
326
+ - ":1234-1237"
300
327
 
301
328
  # If you have a burning desire to have all exposed ports automatically published
302
329
  # to (not-so-)randomly chosen ephemeral ports, you can set this option to `true`.
@@ -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 => {
@@ -224,6 +254,10 @@ module MobyDerp
224
254
  docker_network.info["EnableIPv6"]
225
255
  end
226
256
 
257
+ def user_defined_network?
258
+ !%w{bridge host none}.include?(@pod.network_name)
259
+ end
260
+
227
261
  def container_ipv6_address
228
262
  network, masklen = ipv6_network.split("/", 2)
229
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
@@ -58,6 +61,10 @@ module MobyDerp
58
61
  raise ConfigurationError,
59
62
  "image is not a valid image reference"
60
63
  end
64
+
65
+ if @image.match(Docker::Image::IMAGE_REFERENCE)[9].nil?
66
+ @image += ":latest"
67
+ end
61
68
  end
62
69
 
63
70
  def validate_update_image
@@ -331,6 +338,51 @@ module MobyDerp
331
338
  end
332
339
  end
333
340
 
341
+ def validate_startup_health_check
342
+ if @startup_health_check.nil?
343
+ # This is fine
344
+ return
345
+ end
346
+
347
+ unless @startup_health_check.is_a?(Hash)
348
+ raise ConfigurationError,
349
+ "startup_health_check must be a hash"
350
+ end
351
+
352
+ case @startup_health_check[:command]
353
+ when String
354
+ @startup_health_check[:command] = Shellwords.split(@startup_health_check[:command])
355
+ when Array
356
+ unless @startup_health_check[:command].all? { |c| String === c }
357
+ raise ConfigurationError, "all elements of the health check command array must be strings"
358
+ end
359
+ when NilClass
360
+ raise ConfigurationError, "health check command must be specified"
361
+ else
362
+ raise ConfigurationError,
363
+ "health check command must be string or array of strings"
364
+ end
365
+
366
+ @startup_health_check[:interval] ||= 3
367
+ @startup_health_check[:attempts] ||= 10
368
+
369
+ unless Numeric === @startup_health_check[:interval]
370
+ raise ConfigurationError, "startup health check interval must be a number"
371
+ end
372
+
373
+ if @startup_health_check[:interval] < 0
374
+ raise ConfigurationError, "startup health check interval cannot be negative"
375
+ end
376
+
377
+ unless Integer === @startup_health_check[:attempts]
378
+ raise ConfigurationError, "startup health check attempt count must be an integer"
379
+ end
380
+
381
+ if @startup_health_check[:attempts] < 1
382
+ raise ConfigurationError, "startup health check attempt count must be a positive integer"
383
+ end
384
+ end
385
+
334
386
  def validate_boolean(name)
335
387
  v = instance_variable_get(:"@#{name}")
336
388
  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 = cred["auth"]&.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)
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
 
@@ -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'
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.1
4
+ version: 0.7.2
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-11 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
@@ -201,6 +187,7 @@ extensions: []
201
187
  extra_rdoc_files: []
202
188
  files:
203
189
  - ".gitignore"
190
+ - ".travis.yml"
204
191
  - ".yardopts"
205
192
  - CODE_OF_CONDUCT.md
206
193
  - CONTRIBUTING.md
@@ -208,13 +195,13 @@ files:
208
195
  - README.md
209
196
  - bin/moby-derp
210
197
  - example.yml
211
- - lib/freedom_patches/docker/image.rb
212
198
  - lib/moby_derp/config_file.rb
213
199
  - lib/moby_derp/container.rb
214
200
  - lib/moby_derp/container_config.rb
215
201
  - lib/moby_derp/error.rb
202
+ - lib/moby_derp/freedom_patches/docker/credential.rb
203
+ - lib/moby_derp/freedom_patches/docker/image.rb
216
204
  - lib/moby_derp/logging_helpers.rb
217
- - lib/moby_derp/moby_info.rb
218
205
  - lib/moby_derp/mount.rb
219
206
  - lib/moby_derp/pod.rb
220
207
  - lib/moby_derp/pod_config.rb
@@ -243,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
230
  - !ruby/object:Gem::Version
244
231
  version: '0'
245
232
  requirements: []
246
- rubygems_version: 3.0.1
233
+ rubygems_version: 3.0.3
247
234
  signing_key:
248
235
  specification_version: 4
249
236
  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