moby-derp 0.4.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
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