on_container 0.0.1 → 0.0.7

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
- SHA256:
3
- metadata.gz: f61b63568e23e08af3c0129b1df76cc8a74afa5b8eb9e26ed844e1569329776e
4
- data.tar.gz: a861e643c984f81524645ab76001871da968b5225a7a04d1f3183a186fdd008e
2
+ SHA1:
3
+ metadata.gz: 49a6b84a56344cf0119b822fd9f12277bb66fe70
4
+ data.tar.gz: 6676d161e20a802d3f75c765e04bfd6b0969972c
5
5
  SHA512:
6
- metadata.gz: 29c67a715453468d37fad0651d1e2e44874221d9610487ef201167d918ef30d27fcf0c96377c3ccf449f0c1684027ca8934e8f248fc9c07a5f5a74f03a1b42a6
7
- data.tar.gz: a9487e81209640d5e6927f8f764d51649fcd7d7c06b9d48efa6083e00cdeff78be0804f0f69168c5eb205c76afff98003ce6eba42e6f40e862060110f2fab554
6
+ metadata.gz: 9aa5a059a7ba11f6f658c08d32ead851ab667ad3c894f5442cd795febc3c8e6c9e65f1e8d126708a6a2a0702d18661e043a600768522caadc8cf90ae119f0c90
7
+ data.tar.gz: 6916cb665fa674c90a43634f2cf5bce4632c105c862f44caf8b08200b486440f305fc9f2ca01af4220ba59e87ef15b43969072b900e0c198f290c336ff341580
data/README.md CHANGED
@@ -22,7 +22,71 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ ### Development Routines & Docker Entrypoint Scripts
26
+
27
+ We use some routines included in this gem to create compelling development entrypoint scripts for docker development containers.
28
+
29
+ In this example, we'll be using the `on_container/dev/rails` routine bundle to create our dev entrypoint:
30
+
31
+ ```ruby
32
+ #!/usr/bin/env ruby
33
+
34
+ # frozen_string_literal: true
35
+
36
+ require 'on_container/dev/rails'
37
+
38
+ set_given_or_default_command
39
+
40
+ # `on_setup_lock_acquired` prevents multiple app containers from running
41
+ # the setup process concurrently:
42
+ on_setup_lock_acquired do
43
+ ensure_project_gems_are_installed
44
+ ensure_project_node_packages_are_installed
45
+
46
+ wait_for_service_to_accept_connections 'tcp://postgres:5432'
47
+ setup_activerecord_database unless activerecord_database_ready?
48
+
49
+ remove_rails_pidfile if rails_server?
50
+ end if command_requires_setup?
51
+
52
+ execute_given_or_default_command
53
+ ```
54
+
55
+ ### Loading secrets into environment variables, and inserting credentials into URL environment variables
56
+
57
+ When using Docker Swarm, the secrets are loaded as files mounted into the container's filesystem.
58
+
59
+ The `on_container/load_env_secrets` runs a couple of routines that reads these files into environment variables.
60
+
61
+ For our Rails example app, we added the following line to the `config/boot.rb` file:
62
+
63
+ ```ruby
64
+ # frozen_string_literal: true
65
+
66
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
67
+
68
+ require 'on_container/load_env_secrets' # Load secrets injected by Kubernetes/Swarm
69
+
70
+ require 'bundler/setup' # Set up gems listed in the Gemfile.
71
+ require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
72
+ ```
73
+
74
+ The `on_container/load_env_secrets` also merges any credential available in environment variables into any matching
75
+ `_URL` environment variable. For example, consider the following environment variables:
76
+
77
+ ```shell
78
+ DATABASE_URL=postgres://postgres:5432/?encoding=unicode
79
+ DATABASE_USER=postgres
80
+ DATABASE_PASS=3x4mpl3P455w0rd
81
+ ```
82
+
83
+ The routine will merge `DATABASE_USER` and `DATABASE_PASS` into `DATABASE_URL`:
84
+
85
+ ```ruby
86
+ puts ENV['DATABASE_URL']
87
+ > postgres://postgres:3x4mpl3P455w0rd@postgres:5432/?encoding=unicode
88
+ ```
89
+
26
90
 
27
91
  ## Development
28
92
 
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module OnContainer
5
+ module Dev
6
+ module ActiveRecordOps
7
+ def app_setup_wait
8
+ ENV.fetch('APP_SETUP_WAIT', '5').to_i
9
+ end
10
+
11
+ def parse_activerecord_config_file
12
+ require 'erb'
13
+ require 'yaml'
14
+
15
+ database_yaml = Pathname.new File.expand_path('config/database.yml')
16
+ loaded_yaml = YAML.load(ERB.new(database_yaml.read).result) || {}
17
+ shared = loaded_yaml.delete('shared')
18
+
19
+ loaded_yaml.each { |_k, values| values.reverse_merge!(shared) } if shared
20
+ Hash.new(shared).merge(loaded_yaml)
21
+ end
22
+
23
+ def activerecord_config
24
+ @activerecord_config ||= parse_activerecord_config_file
25
+ .fetch ENV.fetch('RAILS_ENV', 'development')
26
+ end
27
+
28
+ def establish_activerecord_database_connection
29
+ require 'active_record' unless defined?(ActiveRecord)
30
+ ActiveRecord::Base.establish_connection activerecord_config
31
+ ActiveRecord::Base.connection_pool.with_connection { |connection| }
32
+ end
33
+
34
+ def activerecord_database_initialized?
35
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
36
+ connection.data_source_exists? :schema_migrations
37
+ end
38
+ end
39
+
40
+ def activerecord_database_ready?
41
+ connection_tries ||= 3
42
+
43
+ establish_activerecord_database_connection
44
+ activerecord_database_initialized?
45
+
46
+ rescue PG::ConnectionBad
47
+ unless (connection_tries -= 1).zero?
48
+ puts "Retrying DB connection #{connection_tries} more times..."
49
+ sleep app_setup_wait
50
+ retry
51
+ end
52
+ false
53
+
54
+ rescue ActiveRecord::NoDatabaseError
55
+ false
56
+ end
57
+
58
+ def setup_activerecord_database
59
+ system 'rails db:setup'
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnContainer
4
+ module Dev
5
+ module BundlerOps
6
+ def bundle_path
7
+ '/usr/local/bundle'
8
+ end
9
+
10
+ def bundle_owner_id
11
+ File.stat(bundle_path).uid
12
+ end
13
+
14
+ def current_user_id
15
+ Etc.getpwuid.uid
16
+ end
17
+
18
+ def bundle_belongs_to_current_user?
19
+ bundle_owner_id == current_user_id
20
+ end
21
+
22
+ def make_bundle_belong_to_current_user
23
+ target_ownership = "#{current_user_id}:#{current_user_id}"
24
+ system "sudo chown -R #{target_ownership} #{bundle_path}"
25
+ end
26
+
27
+ def ensure_bundle_belongs_to_current_user
28
+ return if bundle_belongs_to_current_user?
29
+
30
+ make_bundle_belong_to_current_user
31
+ end
32
+
33
+ def ensure_project_gems_are_installed
34
+ ensure_bundle_belongs_to_current_user
35
+
36
+ system 'bundle check || bundle install'
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnContainer
4
+ module Dev
5
+ module ContainerCommandOps
6
+ def set_given_or_default_command
7
+ ARGV.concat %w[rails server -p 3000 -b 0.0.0.0] if ARGV.empty?
8
+ end
9
+
10
+ def execute_given_or_default_command
11
+ exec(*ARGV)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etc'
4
+
5
+ module OnContainer
6
+ module Dev
7
+ module NodeModulesOps
8
+ APP_PATH = File.expand_path '.'
9
+
10
+ def node_modules_path
11
+ "#{APP_PATH}/node_modules"
12
+ end
13
+
14
+ def node_modules_owner_id
15
+ File.stat(node_modules_path).uid
16
+ end
17
+
18
+ def current_user_id
19
+ Etc.getpwuid.uid
20
+ end
21
+
22
+ def node_modules_belong_to_current_user?
23
+ node_modules_owner_id == current_user_id
24
+ end
25
+
26
+ def make_node_modules_belong_to_current_user
27
+ target_ownership = "#{current_user_id}:#{current_user_id}"
28
+ system "sudo chown -R #{target_ownership} #{node_modules_path}"
29
+ end
30
+
31
+ def ensure_node_modules_belong_to_current_user
32
+ return if node_modules_belong_to_current_user?
33
+
34
+ make_node_modules_belong_to_current_user
35
+ end
36
+
37
+ def ensure_project_node_packages_are_installed
38
+ ensure_node_modules_belong_to_current_user
39
+
40
+ system 'yarn check --integrity || yarn install'
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'on_container/dev/rails_ops'
4
+ require 'on_container/dev/setup_ops'
5
+ require 'on_container/dev/bundler_ops'
6
+ require 'on_container/dev/node_modules_ops'
7
+ require 'on_container/dev/active_record_ops'
8
+ require 'on_container/dev/container_command_ops'
9
+
10
+ include OnContainer::Dev::RailsOps
11
+ include OnContainer::Dev::SetupOps
12
+ include OnContainer::Dev::BundlerOps
13
+ include OnContainer::Dev::NodeModulesOps
14
+ include OnContainer::Dev::ActiveRecordOps
15
+ include OnContainer::Dev::ContainerCommandOps
16
+
17
+ require 'on_container/ops/service_connection_checks'
18
+
19
+ include OnContainer::Ops::ServiceConnectionChecks
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnContainer
4
+ module Dev
5
+ module RailsOps
6
+ def remove_rails_pidfile
7
+ system "rm -rf #{File.expand_path('tmp/pids/server.pid')}"
8
+ end
9
+
10
+ def rails_server?
11
+ ARGV[0] == 'rails' && %w[server s].include?(ARGV[1])
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnContainer
4
+ module Dev
5
+ module SetupOps
6
+ APP_PATH = File.expand_path '.'
7
+
8
+ def app_temp_path; "#{APP_PATH}/tmp"; end
9
+ def app_setup_wait; ENV.fetch('APP_SETUP_WAIT', '5').to_i; end
10
+ def app_setup_lock_path; "#{app_temp_path}/setup.lock"; end
11
+
12
+ def lock_setup
13
+ system "mkdir -p #{app_temp_path} && touch #{app_setup_lock_path};"
14
+ end
15
+
16
+ def unlock_setup
17
+ system "rm -rf #{app_setup_lock_path}"
18
+ end
19
+
20
+ def wait_setup
21
+ puts 'Waiting for app setup to finish...'
22
+ sleep app_setup_wait
23
+ end
24
+
25
+ def on_setup_lock_acquired
26
+ wait_setup while File.exist?(app_setup_lock_path)
27
+
28
+ lock_setup
29
+
30
+ %w[HUP INT QUIT TERM EXIT].each do |signal_string|
31
+ Signal.trap(signal_string) { unlock_setup }
32
+ end
33
+
34
+ yield
35
+
36
+ ensure
37
+ unlock_setup
38
+ end
39
+
40
+ def command_requires_setup?
41
+ %w[
42
+ rails rspec sidekiq hutch puma rake webpack webpack-dev-server
43
+ ].include?(ARGV[0])
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reads the specified secret paths (i.e. Docker Secrets) into environment
4
+ # variables:
5
+
6
+ require 'active_support'
7
+ require 'active_support/core_ext/object'
8
+
9
+ # Process only a known list of env vars that can filled by reading a file (i.e.
10
+ # a docker secret):
11
+ Dir["#{ENV.fetch('SECRETS_PATH', '/run/secrets/')}*"].each do |secret_filepath|
12
+ next unless File.file?(secret_filepath)
13
+
14
+ secret_envvarname = File.basename(secret_filepath, '.*').upcase
15
+
16
+ # Skip if variable is already set - already-set variables have precedence over
17
+ # the secret files:
18
+ next if ENV.key?(secret_envvarname) && ENV[secret_envvarname].present?
19
+
20
+ ENV[secret_envvarname] = File.read(secret_filepath).strip
21
+ end
22
+
23
+ # For each *_URL environment variable where there's also a *_(USER|USERNAME) or
24
+ # *_(PASS|PASSWORD), update the URL environment variable with the given
25
+ # credentials. For example:
26
+ #
27
+ # DATABASE_URL: postgres://postgres:5432/demo_production
28
+ # DATABASE_USERNAME: lalito
29
+ # DATABASE_PASSWORD: lepass
30
+ #
31
+ # Results in the following updated DATABASE_URL:
32
+ # DATABASE_URL = postgres://lalito:lepass@postgres:5432/demo_production
33
+ require 'uri' if (url_keys = ENV.keys.select { |key| key =~ /_URL/ }).any?
34
+
35
+ url_keys.each do |url_key|
36
+ credential_pattern_string = url_key.gsub('_URL', '_(USER(NAME)?|PASS(WORD)?)')
37
+ credential_pattern = Regexp.new "\\A#{credential_pattern_string}\\z"
38
+ credential_keys = ENV.keys.select { |key| key =~ credential_pattern }
39
+ next unless credential_keys.any?
40
+
41
+ uri = URI(ENV[url_key])
42
+ username = URI.encode_www_form_component ENV[credential_keys.detect { |key| key =~ /USER/ }]
43
+ password = URI.encode_www_form_component ENV[credential_keys.detect { |key| key =~ /PASS/ }]
44
+
45
+ uri.user = username if username
46
+ uri.password = password if password
47
+ ENV[url_key] = uri.to_s
48
+ end
49
+
50
+ # STDERR.puts ENV.inspect
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'timeout'
5
+ require 'uri'
6
+
7
+ module OnContainer
8
+ module Ops
9
+ module ServiceConnectionChecks
10
+ def service_accepts_connections?(service_uri, seconds_to_wait = 30)
11
+ uri = URI(service_uri)
12
+
13
+ Timeout::timeout(seconds_to_wait) do
14
+ TCPSocket.new(uri.host, uri.port).close
15
+ rescue Errno::ECONNREFUSED
16
+ retry
17
+ end
18
+
19
+ true
20
+
21
+ rescue => e
22
+ puts "Connection to #{uri.to_s} failed: '#{e.inspect}'"
23
+ end
24
+
25
+ def wait_for_service_to_accept_connections(service_uri, seconds_to_wait = 30, exit_on_fail = true)
26
+ wait_loop = Thread.new do
27
+ loop do
28
+ sleep(5)
29
+ puts "Waiting for #{service_uri} to accept connections..."
30
+ end
31
+ end
32
+
33
+ if service_accepts_connections?(service_uri, seconds_to_wait)
34
+ return wait_loop.exit
35
+ else
36
+ exit 1 if exit_on_fail
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -26,11 +26,11 @@ module OnContainer
26
26
  end
27
27
 
28
28
  def developer_uid?
29
- developer_uid_as_integer > 0
29
+ developer_uid > 0
30
30
  end
31
31
 
32
32
  def developer_uid
33
- @developer_uid ||= ENV['DEVELOPER_UID'].to_i
33
+ @developer_uid ||= ENV.fetch('DEVELOPER_UID', '').to_i
34
34
  end
35
35
 
36
36
  protected
@@ -38,7 +38,7 @@ module OnContainer
38
38
  def switch_to_developer_user
39
39
  target_user_name = target_user.name
40
40
  puts "Switching from 'root' user to '#{target_user_name}'..."
41
- exec 'su-exec', target_user_name, $0, *$*
41
+ Kernel.exec 'su-exec', target_user_name, $0, *$*
42
42
  end
43
43
 
44
44
  def warn_no_developer_uid
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnContainer
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.7'
5
5
  end
@@ -0,0 +1,5 @@
1
+ RSpec.describe OnContainer do
2
+ it "has a version number" do
3
+ expect(OnContainer::VERSION).not_to be nil
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ require "bundler/setup"
2
+ require "on_container"
3
+
4
+ RSpec.configure do |config|
5
+ # Enable flags like --only-failures and --next-failure
6
+ config.example_status_persistence_file_path = ".rspec_status"
7
+
8
+ # Disable RSpec exposing methods globally on `Module` and `main`
9
+ config.disable_monkey_patching!
10
+
11
+ config.expect_with :rspec do |c|
12
+ c.syntax = :expect
13
+ end
14
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'on_container/step_down_from_root'
4
+
5
+ RSpec.describe OnContainer::StepDownFromRoot do
6
+ let(:root_user) do
7
+ instance_double "struct Etc::Passwd",
8
+ name: 'root',
9
+ passwd: 'x',
10
+ uid: 0,
11
+ gid: 0,
12
+ gecos: 'root',
13
+ dir: '/root',
14
+ shell: '/bin/bash'
15
+ end
16
+
17
+ let(:developer_user) do
18
+ instance_double "struct Etc::Passwd",
19
+ name: 'developer',
20
+ passwd: 'x',
21
+ uid: 1000,
22
+ gid: 1000,
23
+ gecos: 'Developer User,,,',
24
+ dir: '/usr/src',
25
+ shell: '/bin/bash'
26
+ end
27
+
28
+ let(:example_current_user) { root_user }
29
+ let(:example_target_user) { developer_user }
30
+ let(:example_developer_uid) { '1000' }
31
+
32
+ before do
33
+ allow(ENV).to receive(:fetch).with('DEVELOPER_UID', '') { example_developer_uid }
34
+ allow(Etc).to receive(:getpwuid) { example_current_user }
35
+ allow(Etc).to receive(:getpwuid).with(example_target_user.uid) { example_target_user }
36
+ allow(Kernel).to receive(:exec).with('su-exec', example_target_user.name, any_args)
37
+ end
38
+
39
+ describe '#perform' do
40
+ it 'changes to the target user' do
41
+ subject.perform
42
+ expect(Kernel).to have_received(:exec).with 'su-exec', example_target_user.name, any_args
43
+ end
44
+
45
+ context 'without a developer uid' do
46
+ let(:example_developer_uid) { '' }
47
+
48
+ example 'does not change the current user' do
49
+ subject.perform
50
+ expect(Kernel).not_to have_received(:exec)
51
+ end
52
+ end
53
+
54
+ context 'when not as root' do
55
+ let(:example_current_user) { developer_user }
56
+
57
+ example 'does not change the current user' do
58
+ subject.perform
59
+ expect(Kernel).not_to have_received(:exec)
60
+ end
61
+ end
62
+ end
63
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: on_container
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roberto Quintanilla
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-02-17 00:00:00.000000000 Z
11
+ date: 2020-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,9 +38,21 @@ files:
38
38
  - README.md
39
39
  - Rakefile
40
40
  - lib/on_container.rb
41
+ - lib/on_container/dev/active_record_ops.rb
42
+ - lib/on_container/dev/bundler_ops.rb
43
+ - lib/on_container/dev/container_command_ops.rb
44
+ - lib/on_container/dev/node_modules_ops.rb
45
+ - lib/on_container/dev/rails.rb
46
+ - lib/on_container/dev/rails_ops.rb
47
+ - lib/on_container/dev/setup_ops.rb
48
+ - lib/on_container/load_env_secrets.rb
49
+ - lib/on_container/ops/service_connection_checks.rb
41
50
  - lib/on_container/step_down_from_root.rb
42
51
  - lib/on_container/version.rb
43
52
  - on_container.gemspec
53
+ - spec/on_container_spec.rb
54
+ - spec/spec_helper.rb
55
+ - spec/step_down_from_root_spec.rb
44
56
  homepage: https://github.com/IcaliaLabs/on-container-for-ruby
45
57
  licenses:
46
58
  - MIT
@@ -64,9 +76,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
76
  - !ruby/object:Gem::Version
65
77
  version: '0'
66
78
  requirements: []
67
- rubygems_version: 3.0.3
79
+ rubyforge_project:
80
+ rubygems_version: 2.5.2.3
68
81
  signing_key:
69
82
  specification_version: 4
70
83
  summary: A small collection of scripts and routines to help ruby development within
71
84
  containers
72
- test_files: []
85
+ test_files:
86
+ - spec/on_container_spec.rb
87
+ - spec/spec_helper.rb
88
+ - spec/step_down_from_root_spec.rb