diff --git a/.gitignore b/.gitignore index 276b1092..da1ea537 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ /log/* !/log/.keep /tmp + +# Ignore Chef JSON files +/chef/*.json diff --git a/.rubocop.yml b/.rubocop.yml index ae736962..6c15f6e5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,11 +15,14 @@ Metrics/AbcSize: Max: 16 Metrics/ClassLength: Max: 150 -Metrics/LineLength: - Max: 120 Metrics/MethodLength: Max: 15 +Metrics/LineLength: + Max: 120 + Exclude: + - chef/**/* + Rails: Enabled: true diff --git a/chef/deploy.rb b/chef/deploy.rb new file mode 100755 index 00000000..da3fdc96 --- /dev/null +++ b/chef/deploy.rb @@ -0,0 +1,147 @@ +#!/usr/bin/env ruby +require "json" +require "optparse" + +# This script is for deploying to the StockAid instance. Unless configured via +# options, it expects to connect via the "chef" user to the "stockaid" host (you +# can set up this host in your ~/.ssh/config file, or provide the real host via +# a command line option). + +REMOTE_DIR = File.expand_path("../remote", __FILE__) +CHEF_VERSION = "12.19.36".freeze +SSH_DEFAULTS = { batch: true }.freeze + +DEFAULT_OPTIONS = { + user: "chef", + host: "stockaid", + port: 22, + identity: File.expand_path("~/.ssh/stockaid_rsa"), + files_dir: File.expand_path("../files", __FILE__), + repos_dir: File.expand_path("../repos", __FILE__), + json: File.expand_path("../stockaid.json", __FILE__), + clear_host: false, + ssh: false +}.freeze + +OPTIONS = Hash.new { |_hash, key| DEFAULT_OPTIONS[key] } + +def system_exec(cmd, options = {}) + puts cmd + + if options[:exec] + exec cmd + else + system cmd + end +end + +def ssh_options(options = {}) + options = SSH_DEFAULTS.merge(options) + result = [] + result << "-o 'BatchMode yes'" if options[:batch] + result.join(" ") +end + +def ssh(command, options = {}) + if command.is_a?(Hash) + options = options.merge(command) + options[:batch] = false if options[:exec] + command = nil + end + + user = OPTIONS[:user] + host = OPTIONS[:host] + port = OPTIONS[:port] + identity_file = OPTIONS[:identity] + + if options[:exec] + system_exec "ssh '#{user}@#{host}' -p #{port} -i #{identity_file} #{ssh_options(options)}", exec: true + else + force_tty = "-t -t" unless options[:batch] + system_exec "ssh '#{user}@#{host}' -p #{port} -i #{identity_file} #{force_tty} #{ssh_options(options)} '#{command}'" + end +end + +def scp(from, to, options = {}) + user = OPTIONS[:user] + host = OPTIONS[:host] + port = OPTIONS[:port] + identity_file = OPTIONS[:identity] + server = "#{user}@#{host}" + from.sub!(/\Aserver:/, "#{server}:") + to.sub!(/\Aserver:/, "#{server}:") + recursive = "-r" if options[:recursive] + system_exec "scp #{recursive} -P #{port} -i #{identity_file} #{ssh_options(options)} '#{from}' '#{to}'" +end + +def rsync(from, to, options = {}) + user = OPTIONS[:user] + host = OPTIONS[:host] + port = OPTIONS[:port] + identity_file = OPTIONS[:identity] + server = "#{user}@#{host}" + from.sub!(/\Aserver:/, "#{server}:") + to.sub!(/\Aserver:/, "#{server}:") + system_exec %(rsync -e "ssh -p #{port} -i #{identity_file} #{ssh_options(options)}" -av '#{from}' '#{to}') +end + +OptionParser.new do |opts| + opts.banner = "Usage: ./deploy.rb [options]" + + opts.on "-s", "--ssh", "Just ssh to the server, don't actually deploy" do |_ssh| + OPTIONS[:ssh] = true + end + + opts.on "-c", "--clear-host", "Clear the host from known hosts" do |clear| + OPTIONS[:clear_host] = clear + end + + opts.on "-u", "--user USER", "User to ssh with" do |user| + OPTIONS[:user] = user + end + + opts.on "-h", "--host HOST", "Host to ssh with" do |host| + OPTIONS[:host] = host + end + + opts.on "-p", "--port PORT", "Port to ssh with" do |port| + OPTIONS[:port] = port.to_i + end + + opts.on "-i", "--identity FILE", "Identity file to ssh with" do |identity| + OPTIONS[:identity] = File.expand_path(identity) + end + + opts.on "-j", "--json FILE", "Chef json config file" do |config| + OPTIONS[:json] = File.expand_path(config) + end +end.parse! + +JSON.parse(File.read(OPTIONS[:json])).fetch("meta", {}).each do |key, value| + next if OPTIONS.include?(key.to_sym) + puts "Using JSON default #{key}: #{value.inspect}" + value = File.expand_path(value) if %w(identity files_dir repos_dir).include?(key) + OPTIONS[key.to_sym] = value +end + +ssh exec: true if OPTIONS[:ssh] + +system_exec "ssh-keygen -R '#{OPTIONS[:host]}'" if OPTIONS[:clear_host] + +unless ssh "echo ssh key is working" + pub_file = "#{OPTIONS[:identity]}.pub" + public_key = File.read(pub_file) + saved_key = ssh %(mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && echo "#{public_key}" >> ~/.ssh/authorized_keys), batch: false + abort "Failed to save authorized key!" unless saved_key +end + +rsync "#{REMOTE_DIR}/", "server:~/next-stockaid-chef" +rsync OPTIONS[:files_dir], "server:~/chef_data/" +rsync OPTIONS[:repos_dir], "server:~/chef_data/" + +if OPTIONS[:json] + puts "Using json file: #{OPTIONS[:json]}" + scp OPTIONS[:json], "server:~/next-stockaid-chef/solo.json" +end + +ssh "sudo ~/next-stockaid-chef/install.sh '#{OPTIONS[:user]}' '#{CHEF_VERSION}'" diff --git a/chef/remote/cookbooks/stockaid/attributes/default.rb b/chef/remote/cookbooks/stockaid/attributes/default.rb new file mode 100644 index 00000000..1bf36b22 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/attributes/default.rb @@ -0,0 +1,25 @@ +default[:stockaid][:github_url] = "https://github.com/on-site/StockAid.git" +default[:stockaid][:home] = "/home/stockaid" +default[:stockaid][:user] = "stockaid" +default[:stockaid][:group] = "stockaid" +default[:stockaid][:dir] = node[:stockaid][:home] +default[:stockaid][:repo_dir] = File.join(node[:stockaid][:dir], "StockAid") +default[:stockaid][:domain] = "orders.gratefulgarment.org" +default[:stockaid][:site_name] = "The Grateful Garment Project" + +default[:stockaid][:google][:api_key] = nil # Set for some Google integrations +default[:stockaid][:google][:drive_json] = nil # Set for automatic DB backups to Google Drive + +default[:stockaid][:mailer][:default_from] = "noreply@gratefulgarment.org" +default[:stockaid][:mailer][:default_host] = "orders.gratefulgarment.org" + +default[:stockaid][:mailgun][:enabled] = true +default[:stockaid][:mailgun][:domain] = "mg.gratefulgarment.org" +default[:stockaid][:mailgun][:api_key] = nil # This must be set for mailgun to work + +default[:stockaid][:letsencrypt][:enabled] = true +default[:stockaid][:letsencrypt][:email] = nil # This must be set for letsencrypt to work + +default[:stockaid][:newrelic][:enabled] = true +default[:stockaid][:newrelic][:app_name] = "grateful-garment" +default[:stockaid][:newrelic][:license_key] = nil # This must be set for newrelic to work diff --git a/chef/remote/cookbooks/stockaid/libraries/stockaid_helper.rb b/chef/remote/cookbooks/stockaid/libraries/stockaid_helper.rb new file mode 100644 index 00000000..d6ee6d21 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/libraries/stockaid_helper.rb @@ -0,0 +1,108 @@ +require "json" + +module StockAid + class Environment + attr_reader :node + + def initialize(node) + @node = node + end + + def to_h + simple_env.merge(google_env).merge(mailgun_env).merge(newrelic_env) + end + + private + + def simple_env + { + "STOCKAID_DATABASE_HOST" => "localhost", + "STOCKAID_DATABASE_USERNAME" => "stockaid", + "STOCKAID_DATABASE_PASSWORD" => database_password, + "STOCKAID_SECRET_KEY_BASE" => secret_key_base, + "STOCKAID_DEVISE_PEPPER" => devise_pepper, + "STOCKAID_ENV_SETUP" => "3", + "STOCKAID_SITE_NAME" => node[:stockaid][:site_name], + "STOCKAID_ACTION_MAILER_DEFAULT_FROM" => node[:stockaid][:mailer][:default_from], + "STOCKAID_ACTION_MAILER_DEFAULT_HOST" => node[:stockaid][:mailer][:default_host] + } + end + + def database_password + File.read(File.join(node[:stockaid][:dir], ".stockaid-db-password")).strip + end + + def secret_key_base + File.read(File.join(node[:stockaid][:dir], ".stockaid-secret-key-base")) + end + + def devise_pepper + File.read(File.join(node[:stockaid][:dir], ".stockaid-devise-pepper")) + end + + def google_env + google_api_env.merge(google_drive_env) + end + + def google_api_env + if node[:stockaid][:google][:api_key] + { "STOCKAID_GOOGLE_API_KEY" => node[:stockaid][:google][:api_key] } + else + {} + end + end + + def google_drive_env + if node[:stockaid][:google][:drive_json] + { "STOCKAID_GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON" => node[:stockaid][:google][:drive_json].to_json } + else + {} + end + end + + def mailgun_env + if node[:stockaid][:mailgun][:enabled] + { + "STOCKAID_MAILGUN_DOMAIN" => node[:stockaid][:mailgun][:domain], + "STOCKAID_MAILGUN_API_KEY" => node[:stockaid][:mailgun][:api_key] + } + else + {} + end + end + + def newrelic_env + if node[:stockaid][:newrelic][:enabled] + { + "NEW_RELIC_APP_NAME" => node[:stockaid][:newrelic][:app_name], + "NEW_RELIC_LICENSE_KEY" => node[:stockaid][:newrelic][:license_key] + } + else + {} + end + end + end + + module Helper + module_function def systemd_env_variable(key, value) + { + "\\" => "\\\\", + "\n" => "\\n" + }.each do |string, replacement| + value = value.gsub(string, replacement) + end + + if value.include?('"') && value.include?("'") + raise "Please avoid using both ' and \" in an environment variable: #{value.inspect}" + elsif value.include?('"') + "Environment='#{key}=#{value}'" + else + %(Environment="#{key}=#{value}") + end + end + + module_function def stockaid_environment(node) + StockAid::Environment.new(node).to_h + end + end +end diff --git a/chef/remote/cookbooks/stockaid/metadata.rb b/chef/remote/cookbooks/stockaid/metadata.rb new file mode 100644 index 00000000..adafdb3e --- /dev/null +++ b/chef/remote/cookbooks/stockaid/metadata.rb @@ -0,0 +1,6 @@ +name "stockaid" +maintainer "Mike Virata-Stone" +maintainer_email "mike@virata-stone.com" +license "All rights reserved" +description "Sets up and configures StockAid" +version "0.0.1" diff --git a/chef/remote/cookbooks/stockaid/recipes/auto_updates.rb b/chef/remote/cookbooks/stockaid/recipes/auto_updates.rb new file mode 100644 index 00000000..64c57192 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/auto_updates.rb @@ -0,0 +1,31 @@ +directory "/var/log" do + owner "root" + group "root" + mode "0775" + recursive true +end + +file "/etc/cron.weekly/apt-auto-updates" do + content %(# This file is managed by Chef +echo "**************" >> /var/log/apt-auto-updates.log +date >> /var/log/apt-auto-updates.log +apt-get update >> /var/log/apt-auto-updates.log +apt-get upgrade --assume-yes >> /var/log/apt-auto-updates.log +echo "Updates (if any) installed" +) + owner "root" + group "root" + mode "0755" +end + +file "/etc/logrotate.d/apt-auto-updates" do + content %(# This file is managed by Chef +/var/log/apt-auto-updates.log { + rotate 20 + weekly + size 500k + compress + notifempty +} +) +end diff --git a/chef/remote/cookbooks/stockaid/recipes/database.rb b/chef/remote/cookbooks/stockaid/recipes/database.rb new file mode 100644 index 00000000..985101d4 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/database.rb @@ -0,0 +1,36 @@ +%w( + postgresql + libpq-dev +).each do |pkg| + package pkg +end + +password_file = File.join(node[:stockaid][:dir], ".stockaid-db-password") + +file password_file do + content lazy { `openssl rand -base64 18` } + user node[:stockaid][:user] + group node[:stockaid][:group] + mode "0600" + action :create_if_missing +end + +execute "create-postgres-user" do + command lazy { %(psql -c "CREATE USER \\"stockaid\\" WITH LOGIN PASSWORD '#{File.read(password_file).strip}'") } + user "postgres" + not_if %{psql postgres -tAc "SELECT 1 FROM pg_roles WHERE LOWER(rolname) = 'stockaid'" | grep 1}, user: "postgres" +end + +execute "create-postgres-database" do + command lazy { %(psql -c "CREATE DATABASE \\"stockaid_production\\" WITH OWNER \\"stockaid\\" ENCODING 'unicode'") } + user "postgres" + not_if %{psql postgres -tAc "SELECT 1 FROM pg_database WHERE LOWER(datname) = 'stockaid_production'" | grep 1}, + user: "postgres" + notifies :run, "execute[grant-postgres-database-to-user]", :immediately +end + +execute "grant-postgres-database-to-user" do + command %(psql -c "GRANT ALL PRIVILEGES ON DATABASE \\"stockaid_production\\" TO \\"stockaid\\"") + user "postgres" + action :nothing +end diff --git a/chef/remote/cookbooks/stockaid/recipes/default.rb b/chef/remote/cookbooks/stockaid/recipes/default.rb new file mode 100644 index 00000000..efdbfa4f --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/default.rb @@ -0,0 +1,9 @@ +include_recipe "stockaid::repo" +include_recipe "stockaid::rvm" +include_recipe "stockaid::database" +include_recipe "stockaid::rails" +include_recipe "stockaid::sidekiq" +include_recipe "stockaid::self_signed_ssl" +include_recipe "stockaid::nginx" +include_recipe "stockaid::letsencrypt" if node[:stockaid][:letsencrypt][:enabled] +include_recipe "stockaid::auto_updates" diff --git a/chef/remote/cookbooks/stockaid/recipes/letsencrypt.rb b/chef/remote/cookbooks/stockaid/recipes/letsencrypt.rb new file mode 100644 index 00000000..15c32015 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/letsencrypt.rb @@ -0,0 +1,34 @@ +package "letsencrypt" + +raise "node[:stockaid][:letsencrypt][:email] is not set!" unless node[:stockaid][:letsencrypt][:email] + +template "/usr/bin/stockaid_letsencrypt_renew" do + source "letsencrypt/stockaid_letsencrypt_renew.erb" + owner "root" + group "root" + mode "0744" +end + +file "/etc/cron.d/letsencrypt_renew" do + content "# This file is managed by chef +0 5 * * * root /usr/bin/stockaid_letsencrypt_renew >> /var/log/letsencrypt-renew.log +" + owner "root" + group "root" + mode "0644" +end + +directory "/var/www-letsencrypt/#{node[:stockaid][:domain]}" do + owner "www-data" + group "www-data" + mode "0744" + recursive true +end + +execute "setup-letsencrypt" do + command "letsencrypt certonly --non-interactive --agree-tos --email '#{node[:stockaid][:letsencrypt][:email]}' " \ + "--webroot -w '/var/www-letsencrypt/#{node[:stockaid][:domain]}' -d '#{node[:stockaid][:domain]}'" + creates "/etc/letsencrypt/live/#{node[:stockaid][:domain]}" + notifies :run, "execute[reload-nginx]", :before + notifies :create, "template[/etc/nginx/sites-available/stockaid]", :immediately +end diff --git a/chef/remote/cookbooks/stockaid/recipes/nginx.rb b/chef/remote/cookbooks/stockaid/recipes/nginx.rb new file mode 100644 index 00000000..239e1db2 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/nginx.rb @@ -0,0 +1,106 @@ +%w( + apt-transport-https + ca-certificates +).each do |pkg| + package pkg +end + +apt_key = "561F9B9CAC40B2F7" + +execute "add-passenger-apt-key" do + command "apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-keys '#{apt_key}'" + + not_if do + fingerprints = `apt-key finger`.split("\n").map do |line| + line[/Key fingerprint = ([0-9A-F ]+)/, 1] + end + + fingerprints.compact! + fingerprints.map! { |fingerprint| fingerprint.split.join } + fingerprints.any? { |fingerprint| fingerprint.end_with? apt_key } + end +end + +file "/etc/apt/sources.list.d/passenger.list" do + content lazy { + ubuntu_codename = `lsb_release -s -c`.strip + "deb https://oss-binaries.phusionpassenger.com/apt/passenger #{ubuntu_codename} main" + } + + owner "root" + group "root" + mode "0644" +end + +execute "update-apt" do + command "apt-get update" + action :nothing +end + +%w( + nginx-extras + passenger +).each do |pkg| + package pkg do + options "--force-yes" + notifies :run, "execute[update-apt]", :before + notifies :run, "execute[reload-nginx]", :immediately + end +end + +template "/etc/nginx/sites-available/default" do + source "nginx/default.nginx.conf.erb" + owner "root" + group "root" + mode "0644" + notifies :run, "execute[reload-nginx]" +end + +link "/etc/nginx/conf.d/passenger.conf" do + to "/etc/nginx/passenger.conf" + notifies :run, "execute[reload-nginx]" +end + +template "/etc/nginx/sites-available/stockaid" do + source "nginx/stockaid.nginx.conf.erb" + owner "root" + group "root" + mode "0644" + + variables( + lazy do + stockaid_ruby_file = File.join(node[:stockaid][:repo_dir], ".ruby-version") + ruby = File.read(stockaid_ruby_file).strip + + if File.exist?("/etc/letsencrypt/live/#{node[:stockaid][:domain]}/fullchain.pem") + certificate = "/etc/letsencrypt/live/#{node[:stockaid][:domain]}/fullchain.pem" + certificate_key = "/etc/letsencrypt/live/#{node[:stockaid][:domain]}/privkey.pem" + else + certificate = "/etc/self-signed-ssl/stockaid.crt" + certificate_key = "/etc/self-signed-ssl/stockaid.key" + end + + { + domain: node[:stockaid][:domain], + passenger_ruby: File.join(node[:stockaid][:home], ".rvm/wrappers/#{ruby}/ruby"), + rails_root: node[:stockaid][:repo_dir], + certificate: certificate, + certificate_key: certificate_key, + env: StockAid::Helper.stockaid_environment(node) + } + end + ) + + notifies :run, "execute[reload-nginx]", :immediately +end + +link "/etc/nginx/sites-enabled/stockaid" do + to "/etc/nginx/sites-available/stockaid" + notifies :run, "execute[reload-nginx]", :immediately +end + +execute "reload-nginx" do + command "systemctl reload nginx" + ignore_failure true + action :nothing +end diff --git a/chef/remote/cookbooks/stockaid/recipes/rails.rb b/chef/remote/cookbooks/stockaid/recipes/rails.rb new file mode 100644 index 00000000..5dcbf028 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/rails.rb @@ -0,0 +1,88 @@ +# For execjs to work without using the ruby racer gem +%w( + nodejs + redis-server +).each do |pkg| + package pkg +end + +rvm_binary = File.join(node[:stockaid][:home], ".rvm/bin/rvm") +rvm_environment = { + "USER" => node[:stockaid][:user], + "HOME" => node[:stockaid][:home], + "TERM" => "dumb", + "RAILS_ENV" => "production" +} + +execute "install-bundler" do + command "#{rvm_binary} in '#{node[:stockaid][:repo_dir]}' do gem install bundler --no-ri --no-rdoc" + user node[:stockaid][:user] + group node[:stockaid][:group] + cwd node[:stockaid][:repo_dir] + not_if "#{rvm_binary} in '#{node[:stockaid][:repo_dir]}' do gem list | grep ^bundler\\\\s", + user: node[:stockaid][:user], environment: rvm_environment +end + +execute "bundle-install" do + command "#{rvm_binary} in '#{node[:stockaid][:repo_dir]}' do bundle install --frozen --with production" + user node[:stockaid][:user] + group node[:stockaid][:group] + cwd node[:stockaid][:repo_dir] + not_if "#{rvm_binary} in '#{node[:stockaid][:repo_dir]}' do bundle check", + user: node[:stockaid][:user], environment: rvm_environment + notifies :run, "execute[reload-nginx]" +end + +require "securerandom" +secret_key_base_file = File.join(node[:stockaid][:dir], ".stockaid-secret-key-base") + +file secret_key_base_file do + content SecureRandom.hex(64) + user node[:stockaid][:user] + group node[:stockaid][:group] + mode "0600" + action :create_if_missing +end + +devise_pepper_file = File.join(node[:stockaid][:dir], ".stockaid-devise-pepper") + +file devise_pepper_file do + content SecureRandom.hex(64) + user node[:stockaid][:user] + group node[:stockaid][:group] + mode "0600" + action :create_if_missing +end + +file File.join(node[:stockaid][:repo_dir], ".ruby-env") do + content lazy { + [].tap do |lines| + lines << "# This file is generated by Chef" + lines << "RAILS_ENV=production" + + StockAid::Helper.stockaid_environment(node).each do |key, value| + lines << "#{key}=#{value}" + end + end.join("\n") + } +end + +asset_check_path = File.join(node[:stockaid][:dir], ".stockaid-assets-precompiled") + +execute "rake-assets-precompile" do + command "#{rvm_binary} in '#{node[:stockaid][:repo_dir]}' do rake assets:precompile && git log -1 --format=format:%H%n >> '#{asset_check_path}'" + user node[:stockaid][:user] + group node[:stockaid][:group] + cwd node[:stockaid][:repo_dir] + not_if { File.exist?(asset_check_path) && File.read(asset_check_path)[`cd '#{node[:stockaid][:repo_dir]}' && git log -1 --format=format:%H`] } + notifies :run, "execute[reload-nginx]" +end + +execute "rake-db-migrate" do + command "#{rvm_binary} in '#{node[:stockaid][:repo_dir]}' do bundle exec rake db:migrate" + user node[:stockaid][:user] + group node[:stockaid][:group] + cwd node[:stockaid][:repo_dir] + not_if "#{rvm_binary} in '#{node[:stockaid][:repo_dir]}' do bundle exec rake db:abort_if_pending_migrations", + user: node[:stockaid][:user], environment: rvm_environment +end diff --git a/chef/remote/cookbooks/stockaid/recipes/repo.rb b/chef/remote/cookbooks/stockaid/recipes/repo.rb new file mode 100644 index 00000000..a1f5aaa3 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/repo.rb @@ -0,0 +1,14 @@ +package "git" + +directory node[:stockaid][:dir] do + owner node[:stockaid][:user] + group node[:stockaid][:group] + recursive true +end + +execute "git-clone-stockaid" do + command "git clone '#{node[:stockaid][:github_url]}' '#{node[:stockaid][:repo_dir]}'" + user node[:stockaid][:user] + group node[:stockaid][:group] + not_if { File.exist?(File.join(node[:stockaid][:repo_dir], ".git")) } +end diff --git a/chef/remote/cookbooks/stockaid/recipes/rvm.rb b/chef/remote/cookbooks/stockaid/recipes/rvm.rb new file mode 100644 index 00000000..3955fac2 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/rvm.rb @@ -0,0 +1,53 @@ +%w( + autoconf + automake + bison + curl + gawk + libffi-dev + libgdbm-dev + libncurses5-dev + libreadline6-dev + libsqlite3-dev + libssl-dev + libtool + libyaml-dev + pkg-config + sqlite3 + zlib1g-dev +).each do |pkg| + package pkg +end + +gpg_key = "409B6B1796C275462A1703113804BB82D39DC0E3" +stockaid_ruby_file = File.join(node[:stockaid][:repo_dir], ".ruby-version") +rvm_binary = File.join node[:stockaid][:home], ".rvm/bin/rvm" +rvm_environment = { + "USER" => node[:stockaid][:user], + "HOME" => node[:stockaid][:home], + "TERM" => "dumb" +} + +execute "install-rvm-key" do + command "gpg --keyserver hkp://keys.gnupg.net --recv-keys #{gpg_key}" + user node[:stockaid][:user] + group node[:stockaid][:group] + environment rvm_environment + not_if "gpg -k #{gpg_key}", user: node[:stockaid][:user], environment: rvm_environment +end + +execute "install-rvm" do + command "curl -sSL https://get.rvm.io | bash -s stable --autolibs=read-fail" + user node[:stockaid][:user] + group node[:stockaid][:group] + environment rvm_environment + not_if { File.exist?(File.join(node[:stockaid][:home], ".rvm")) } +end + +execute "rvm-install-ruby" do + command lazy { "#{rvm_binary} install #{File.read(stockaid_ruby_file).strip}" } + user node[:stockaid][:user] + group node[:stockaid][:group] + environment rvm_environment + not_if { File.exist?(File.join(node[:stockaid][:home], ".rvm/rubies/#{File.read(stockaid_ruby_file).strip}")) } +end diff --git a/chef/remote/cookbooks/stockaid/recipes/self_signed_ssl.rb b/chef/remote/cookbooks/stockaid/recipes/self_signed_ssl.rb new file mode 100644 index 00000000..f8fb551a --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/self_signed_ssl.rb @@ -0,0 +1,12 @@ +# The self signed cert will be used until lets encrypt is ready +directory "/etc/self-signed-ssl" do + owner "root" + group "root" + mode "0700" + recursive true +end + +execute "generate-self-signed-cert" do + command %(openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/self-signed-ssl/stockaid.key -out /etc/self-signed-ssl/stockaid.crt -subj "/C=/ST=/L=/O=/OU=/CN=#{node[:stockaid][:domain]}") + not_if { File.exist?("/etc/self-signed-ssl/stockaid.key") && File.exist?("/etc/self-signed-ssl/stockaid.crt") } +end diff --git a/chef/remote/cookbooks/stockaid/recipes/sidekiq.rb b/chef/remote/cookbooks/stockaid/recipes/sidekiq.rb new file mode 100644 index 00000000..d4acc573 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/recipes/sidekiq.rb @@ -0,0 +1,22 @@ +template "/etc/systemd/system/sidekiq.service" do + source "sidekiq/sidekiq.service.erb" + owner "root" + group "root" + mode "0600" + + variables lazy { + require "yaml" + procfile_file = File.join(node[:stockaid][:repo_dir], "Procfile") + procfile = YAML.load_file(procfile_file) + + { + command: procfile["worker"], + env: StockAid::Helper.stockaid_environment(node) + } + } +end + +service "sidekiq" do + action [:enable, :start] + subscribes :reload, "template[/etc/systemd/system/sidekiq.service]", :immediately +end diff --git a/chef/remote/cookbooks/stockaid/templates/default/letsencrypt/stockaid_letsencrypt_renew.erb b/chef/remote/cookbooks/stockaid/templates/default/letsencrypt/stockaid_letsencrypt_renew.erb new file mode 100644 index 00000000..9580f6f1 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/templates/default/letsencrypt/stockaid_letsencrypt_renew.erb @@ -0,0 +1,48 @@ +#!/bin/bash + +sleep_before_renew=true + +while test $# -gt 0 +do + case "$1" in + --no-sleep) sleep_before_renew=false + ;; + --*) echo "invalid option $1" + ;; + *) echo "invalid argument $1" + ;; + esac + shift +done + +ping_and_check_url() { + echo "*** Pinging $1 at $(date) ***" + + wget -qO- $1 &> /dev/null + if [ $? -ne 0 ]; then + echo "$1 FAILED!!!" + else + echo "$1 succeeded" + fi +} + +echo "*************************************************************************" +echo "*** Starting letsencrypt renew script at $(date) ***" + +if [ "$sleep_before_renew" = "true" ]; then + random_sleep=${RANDOM:0:2} + echo "*** Sleeping for $random_sleep minutes ***" + sleep ${random_sleep}m +fi + +echo "*** Renewing certificates at $(date) ***" +letsencrypt renew --non-interactive --agree-tos --email <%= node[:stockaid][:letsencrypt][:email] %> +echo "*** Reloading nginx at $(date) ***" +systemctl reload nginx.service +echo "*** Sleeping to let nginx reload successfully ***" +sleep 5 + +# Ping the domains to make sure they came back up ok, and also to spin up the +# Rails processes +echo "*** Pinging urls at $(date) ***" +ping_and_check_url <%= node[:stockaid][:domain] %> diff --git a/chef/remote/cookbooks/stockaid/templates/default/nginx/default.nginx.conf.erb b/chef/remote/cookbooks/stockaid/templates/default/nginx/default.nginx.conf.erb new file mode 100644 index 00000000..ef0990bb --- /dev/null +++ b/chef/remote/cookbooks/stockaid/templates/default/nginx/default.nginx.conf.erb @@ -0,0 +1,12 @@ +# A blank server for the default site +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + server_tokens off; + + location / { + add_header Content-Type text/plain; + return 200 'Hello!'; + } +} diff --git a/chef/remote/cookbooks/stockaid/templates/default/nginx/stockaid.nginx.conf.erb b/chef/remote/cookbooks/stockaid/templates/default/nginx/stockaid.nginx.conf.erb new file mode 100644 index 00000000..01b196a9 --- /dev/null +++ b/chef/remote/cookbooks/stockaid/templates/default/nginx/stockaid.nginx.conf.erb @@ -0,0 +1,37 @@ +server { + listen 443 ssl; + server_name <%= @domain %>; + server_tokens off; + root <%= File.join(@rails_root, "public") %>; + gzip_static on; + passenger_enabled on; + passenger_ruby <%= @passenger_ruby %>; + + <% @env.each do |key, value| %> + <% if value.is_a?(String) && value =~ /\s/ %> + passenger_env_var <%= key %> <%= value.inspect %>; + <% else %> + passenger_env_var <%= key %> <%= value %>; + <% end %> + <% end %> + + rails_env production; + ssl on; + ssl_certificate <%= @certificate %>; + ssl_certificate_key <%= @certificate_key %>; + client_max_body_size 4m; +} + +server { + listen 80; + server_name <%= @domain %>; + server_tokens off; + + location / { + return 301 https://<%= @domain %>$request_uri; + } + + location ^~ /.well-known { + alias /var/www-letsencrypt/<%= @domain %>/.well-known; + } +} diff --git a/chef/remote/cookbooks/stockaid/templates/default/sidekiq/sidekiq.service.erb b/chef/remote/cookbooks/stockaid/templates/default/sidekiq/sidekiq.service.erb new file mode 100644 index 00000000..3db5966d --- /dev/null +++ b/chef/remote/cookbooks/stockaid/templates/default/sidekiq/sidekiq.service.erb @@ -0,0 +1,34 @@ +# This file is managed by Chef +# +# Based on: +# https://github.com/mperham/sidekiq/blob/e2f588d8bd66481e9fb2f7a7fd1378344c4709a9/examples/systemd/sidekiq.service +[Unit] +Description=sidekiq +# start us only once the network and logging subsystems are available, +# consider adding redis-server.service if Redis is local and systemd-managed. +After=network.target + +# See these pages for lots of options: +# http://0pointer.de/public/systemd-man/systemd.service.html +# http://0pointer.de/public/systemd-man/systemd.exec.html +[Service] +Type=simple +WorkingDirectory=<%= node[:stockaid][:repo_dir] %> +ExecStart=<%= File.join(node[:stockaid][:home], ".rvm/bin/rvm") %> in <%= node[:stockaid][:repo_dir] %> do <%= @command %> +User=<%= node[:stockaid][:user] %> +Group=<%= node[:stockaid][:user] %> +UMask=0002 + +<% @env.each do |key, value| -%> +<%= StockAid::Helper.systemd_env_variable(key, value) %> +<% end -%> + +# if we crash, restart +RestartSec=1 +Restart=on-failure + +# This will default to "bundler" if we don't specify it +SyslogIdentifier=sidekiq + +[Install] +WantedBy=multi-user.target diff --git a/chef/remote/install.sh b/chef/remote/install.sh new file mode 100755 index 00000000..f0753bab --- /dev/null +++ b/chef/remote/install.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +USERNAME="$1" +CHEF_VERSION="$2" + +if ! command -v ruby >/dev/null 2>&1 +then + echo "Installing Ruby" + DEBIAN_FRONTEND=noninteractive apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y ruby ruby-dev build-essential +fi + +if ! command -v chef-solo >/dev/null 2>&1 +then + echo "Installing Chef" + gem install --no-rdoc --no-ri chef --version $CHEF_VERSION +fi + +rm -rf /home/$USERNAME/stockaid-chef +mv /home/$USERNAME/next-stockaid-chef /home/$USERNAME/stockaid-chef +cd /home/$USERNAME/stockaid-chef +chef-solo -c solo.rb -j solo.json diff --git a/chef/remote/solo.rb b/chef/remote/solo.rb new file mode 100644 index 00000000..8a546892 --- /dev/null +++ b/chef/remote/solo.rb @@ -0,0 +1,3 @@ +root = File.absolute_path File.dirname(__FILE__) +file_cache_path root +cookbook_path File.join(root, "cookbooks") diff --git a/config/database.yml b/config/database.yml index 06f054a4..c97d9a39 100644 --- a/config/database.yml +++ b/config/database.yml @@ -2,6 +2,7 @@ default: &default adapter: postgresql encoding: unicode pool: 5 + host: <%= ENV['STOCKAID_DATABASE_HOST'] %> username: <%= ENV['STOCKAID_DATABASE_USERNAME'] %> password: <%= ENV['STOCKAID_DATABASE_PASSWORD'] %>