on_container 0.0.2 → 0.0.8

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: e6c6b69a780d2c6b4f874f5e5ba73c5cd466cefaa12417387e17c56450feba6d
4
- data.tar.gz: 3c0ad63961d07fdc00f3903346680270b51f26c5d30c3f2cd4fb53e63a065190
3
+ metadata.gz: 9b925ace8fb6fd6a98bf34a8ece37b21e18d25db9d348c7473dc68c8941b6769
4
+ data.tar.gz: 60a7a5747314918c66233d9e0847869e67c16887f46aae0a5de9aebaaa228a60
5
5
  SHA512:
6
- metadata.gz: f250e92ac740599c98efbce04aefd437fae5a29d59bf3e0ff3334e7672391088c72ff4f7119878d7eb36dbe55cefaa594286095731c09247f97107cc4c04d572
7
- data.tar.gz: 7816fdf82eef22a8fd907bf89dbfaf3e1866f68b131529638e94f8bd661a0d51401ac36095cfaeb124cde2fe90109d30b96bd67202fa117513f810b18f6aa00b
6
+ metadata.gz: 878956892f1cdf7f9ae0509b76b3262ec5d6b4d2bfb606ac315e15ed69be6a7bb57259f994a13cfb3ffa55756a2b7612b40d4c6b3c8023bbe81f8c065166da9c
7
+ data.tar.gz: 33dd25d152e4fb8631c814ef17c7582c63ff9c3cf50d57f384b7f73c8b21eed800a1b73e0f2782066d7f73c963c4b2f0b9b0096215c7a199de2b96e17af990b8
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnContainer
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.8'
5
5
  end
@@ -24,5 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.bindir = 'exe'
25
25
  spec.require_paths = ['lib']
26
26
 
27
- spec.add_development_dependency 'bundler', '~> 1.17'
27
+ spec.add_development_dependency 'bundler', '~> 2.1'
28
28
  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.2
4
+ version: 0.0.8
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-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.17'
19
+ version: '2.1'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.17'
26
+ version: '2.1'
27
27
  description: A small collection of scripts and routines to help ruby development within
28
28
  containers
29
29
  email:
@@ -38,7 +38,15 @@ files:
38
38
  - README.md
39
39
  - Rakefile
40
40
  - lib/on_container.rb
41
- - lib/on_container/step_down_from_root.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
42
50
  - lib/on_container/version.rb
43
51
  - on_container.gemspec
44
52
  homepage: https://github.com/IcaliaLabs/on-container-for-ruby
@@ -64,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
72
  - !ruby/object:Gem::Version
65
73
  version: '0'
66
74
  requirements: []
67
- rubygems_version: 3.0.3
75
+ rubygems_version: 3.1.4
68
76
  signing_key:
69
77
  specification_version: 4
70
78
  summary: A small collection of scripts and routines to help ruby development within
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'etc'
4
-
5
- module OnContainer
6
- class StepDownFromRoot
7
- attr_reader :curent_user, :target_user
8
-
9
- def initialize
10
- @curent_user = Etc.getpwuid
11
- end
12
-
13
- def target_user
14
- @target_user ||= Etc.getpwuid(developer_uid)
15
- end
16
-
17
- def perform
18
- return unless root_user?
19
- return warn_no_developer_uid unless developer_uid?
20
-
21
- switch_to_developer_user
22
- end
23
-
24
- def root_user?
25
- curent_user.name == 'root'
26
- end
27
-
28
- def developer_uid?
29
- developer_uid > 0
30
- end
31
-
32
- def developer_uid
33
- @developer_uid ||= ENV.fetch('DEVELOPER_UID', '').to_i
34
- end
35
-
36
- protected
37
-
38
- def switch_to_developer_user
39
- target_user_name = target_user.name
40
- puts "Switching from 'root' user to '#{target_user_name}'..."
41
- Kernel.exec 'su-exec', target_user_name, $0, *$*
42
- end
43
-
44
- def warn_no_developer_uid
45
- puts "The 'DEVELOPER_UID' environment variable is not set... " \
46
- 'still running as root!'
47
- end
48
-
49
- def self.perform
50
- new.perform
51
- end
52
- end
53
- end
54
-
55
- OnContainer::StepDownFromRoot.perform