Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@
/log/*
!/log/.keep
/tmp

# Ignore Chef JSON files
/chef/*.json
7 changes: 5 additions & 2 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
147 changes: 147 additions & 0 deletions chef/deploy.rb
Original file line number Diff line number Diff line change
@@ -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}'"
25 changes: 25 additions & 0 deletions chef/remote/cookbooks/stockaid/attributes/default.rb
Original file line number Diff line number Diff line change
@@ -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
108 changes: 108 additions & 0 deletions chef/remote/cookbooks/stockaid/libraries/stockaid_helper.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions chef/remote/cookbooks/stockaid/metadata.rb
Original file line number Diff line number Diff line change
@@ -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"
31 changes: 31 additions & 0 deletions chef/remote/cookbooks/stockaid/recipes/auto_updates.rb
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions chef/remote/cookbooks/stockaid/recipes/database.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions chef/remote/cookbooks/stockaid/recipes/default.rb
Original file line number Diff line number Diff line change
@@ -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"
Loading