on_container 0.0.1 → 0.0.7

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
- 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