diff --git a/.rubocop.yml b/.rubocop.yml index aa49f43c..4922ce18 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ AllCops: - lib/templates/rails/scaffold_controller/controller.rb - db/schema.rb - vendor/**/* + - lib/capistrano/**/* Rails: Enabled: true diff --git a/Capfile b/Capfile new file mode 100644 index 00000000..343d60b9 --- /dev/null +++ b/Capfile @@ -0,0 +1,35 @@ +# frozen_string_literal: true +# Load DSL and Setup Up Stages +require 'capistrano/setup' + +# Includes default deployment tasks +require 'capistrano/deploy' + +require 'capistrano/safe_deploy_to' + +require 'capistrano/postgresql' + +require 'capistrano/unicorn_nginx' + +# require 'capistrano/memcached' + +# require 'whenever/capistrano' + +# Includes tasks from other gems included in your Gemfile +require 'capistrano/bundler' +require 'capistrano/rails' + +require 'rvm1/capistrano3' + +# require 'capistrano/faster_assets' + +# require 'capistrano/wal_e' + +require 'capistrano/sidekiq' +require 'capistrano/sidekiq/monit' + +require 'capistrano/scm/git' +install_plugin Capistrano::SCM::Git + +# Loads custom tasks from `lib/capistrano/tasks' if you have any defined. +Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } diff --git a/Gemfile b/Gemfile index 155fb669..a21ffb08 100644 --- a/Gemfile +++ b/Gemfile @@ -10,8 +10,8 @@ end gem 'rails', '~> 5.0.1' # Use sqlite3 as the database for Active Record gem 'pg' -# Use Puma as the app server -gem 'puma', '~> 3.0' +# Use Unicorn as the app server +gem 'unicorn' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0' # Use Uglifier as compressor for JavaScript assets @@ -31,6 +31,7 @@ gem 'rubyzip' gem 'jquery-rails' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.5' +gem 'redis-namespace' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 3.0' # Use ActiveModel has_secure_password @@ -51,8 +52,10 @@ gem 'rdiscount' gem 'git_stats' -gem 'resque' -gem 'resque-web', require: 'resque_web' +# gem 'resque' +# gem 'resque-web', require: 'resque_web' + +gem 'sidekiq' group :development, :test do gem 'pry' @@ -73,6 +76,20 @@ group :development do gem 'rubocop', require: false gem 'brakeman', require: false + + # capistrano + + gem 'capistrano', '~> 3.8', require: false, group: :development + # Deploy with capistrano, see config/deploy.rb + gem 'capistrano-rails', '~> 1.1', require: false + gem 'capistrano-bundler', '~> 1.1', require: false + gem 'capistrano-safe-deploy-to', '~> 1.1.1', require: false + gem 'rvm1-capistrano3', '~> 1.3.2', require: false + gem 'capistrano-postgresql', '~> 4.2.1', require: false + # gem 'capistrano-memcached', '~> 1.2.0', require: false + gem 'capistrano-unicorn-nginx', '~> 4.1', require: false + # gem 'capistrano-faster-assets', '~> 1.0.2', require: false + gem 'capistrano-sidekiq', require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index a00da153..929b94f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,6 +126,8 @@ GEM tzinfo (~> 1.1) addressable (2.5.0) public_suffix (~> 2.0, >= 2.0.2) + airbrussh (1.3.0) + sshkit (>= 1.6.1, != 1.7.0) arbre (1.1.1) activesupport (>= 3.0.0) arel (7.1.4) @@ -152,6 +154,27 @@ GEM buftok (0.2.0) builder (3.2.3) cancancan (1.15.0) + capistrano (3.8.2) + airbrussh (>= 1.0.0) + i18n + rake (>= 10.0.0) + sshkit (>= 1.9.0) + capistrano-bundler (1.1.4) + capistrano (~> 3.1) + sshkit (~> 1.2) + capistrano-postgresql (4.2.1) + capistrano (>= 3.0) + capistrano-rails (1.1.3) + capistrano (~> 3.1) + capistrano-bundler (~> 1.1) + capistrano-safe-deploy-to (1.1.1) + capistrano (>= 3.0) + capistrano-sidekiq (0.10.0) + capistrano + sidekiq (>= 3.4) + capistrano-unicorn-nginx (4.1.0) + capistrano (>= 3.1) + sshkit (>= 1.2.0) capybara (2.11.0) addressable mime-types (>= 1.16) @@ -174,8 +197,8 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - commonjs (0.2.7) concurrent-ruby (1.0.4) + connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) css_parser (1.4.7) @@ -262,17 +285,11 @@ GEM json (1.8.3) json_pure (2.0.2) jwt (1.5.6) + kgio (2.11.0) launchy (2.4.3) addressable (~> 2.3) lazy_high_charts (1.5.6) hash-deep-merge - less (2.6.0) - commonjs (~> 0.2.7) - less-rails (2.8.0) - actionpack (>= 4.0) - less (~> 2.6.0) - sprockets (> 2, < 4) - tilt letter_opener (1.4.0) launchy (~> 2.2) listen (3.0.8) @@ -294,11 +311,13 @@ GEM mini_portile2 (2.1.0) minitest (5.10.1) modernizr-rails (2.7.1) - mono_logger (1.1.0) multi_json (1.12.1) multi_xml (0.6.0) multipart-post (2.0.0) naught (1.1.0) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-ssh (4.1.0) nio4r (1.2.1) nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) @@ -357,8 +376,9 @@ GEM method_source (~> 0.8.1) slop (~> 3.4) public_suffix (2.0.5) - puma (3.6.2) rack (2.0.1) + rack-protection (2.0.0) + rack rack-test (0.6.3) rack (>= 1.0) rails (5.0.1) @@ -389,29 +409,18 @@ GEM rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) + raindrops (0.19.0) rake (12.0.0) rb-fsevent (0.9.8) rb-inotify (0.9.7) ffi (>= 0.5.0) rdiscount (2.2.0.1) - redis (3.3.2) + redis (3.3.3) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) request_store (1.3.2) responders (2.3.0) railties (>= 4.2.0, < 5.1) - resque (1.26.0) - mono_logger (~> 1.0) - multi_json (~> 1.0) - redis-namespace (~> 1.3) - sinatra (>= 0.9.2) - vegas (~> 0.1.2) - resque-web (0.0.9) - coffee-rails - jquery-rails - resque - sass-rails - twitter-bootstrap-rails rspec-collection_matchers (1.1.2) rspec-expectations (>= 2.99.0.beta1) rspec-core (3.5.4) @@ -439,6 +448,9 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.8.1) rubyzip (1.1.7) + rvm1-capistrano3 (1.3.2.2) + capistrano (~> 3.0) + sshkit (>= 1.2) safe_yaml (1.0.4) sass (3.4.23) sass-rails (5.0.6) @@ -450,12 +462,15 @@ GEM sawyer (0.8.1) addressable (>= 2.3.5, < 2.6) faraday (~> 0.8, < 1.0) + sidekiq (5.0.4) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.2, >= 2.2.0) + rack-protection (>= 1.5.0) + redis (~> 3.3, >= 3.3.3) simple_form (3.3.1) actionpack (> 4, < 5.1) activemodel (> 4, < 5.1) simple_oauth (0.3.1) - sinatra (1.0) - rack (>= 1.0) slop (3.6.0) spring (2.0.0) activesupport (>= 4.2) @@ -471,6 +486,9 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + sshkit (1.13.1) + net-scp (>= 1.1.2) + net-ssh (>= 2.8.0) thor (0.19.4) thread_safe (0.3.5) tilt (2.0.5) @@ -484,11 +502,6 @@ GEM memoizable (~> 0.4.2) naught (~> 1.1) simple_oauth (~> 0.3.1) - twitter-bootstrap-rails (3.2.2) - actionpack (>= 3.1) - execjs (>= 2.2.2, >= 2.2) - less-rails (>= 2.5.0) - railties (>= 3.1) tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (3.0.4) @@ -497,9 +510,10 @@ GEM unf_ext unf_ext (0.0.7.2) unicode-display_width (1.1.1) + unicorn (5.3.0) + kgio (~> 2.6) + raindrops (~> 0.7) vcr (3.0.3) - vegas (0.1.11) - rack (>= 1.0.0) warden (1.2.6) rack (>= 1.0) web-console (3.4.0) @@ -528,6 +542,13 @@ DEPENDENCIES bootstrap-sass brakeman cancancan + capistrano (~> 3.8) + capistrano-bundler (~> 1.1) + capistrano-postgresql (~> 4.2.1) + capistrano-rails (~> 1.1) + capistrano-safe-deploy-to (~> 1.1.1) + capistrano-sidekiq + capistrano-unicorn-nginx (~> 4.1) capybara ckeditor coffee-rails (~> 4.2) @@ -563,21 +584,21 @@ DEPENDENCIES pg premailer-rails pry - puma (~> 3.0) rails (~> 5.0.1) rails-controller-testing ransack! rdiscount + redis-namespace responders (~> 2.0) - resque - resque-web rich_pluralization! rspec-collection_matchers rspec-mocks rspec-rails rubocop rubyzip + rvm1-capistrano3 (~> 1.3.2) sass-rails (~> 5.0) + sidekiq simple_form spring spring-commands-rspec @@ -585,6 +606,7 @@ DEPENDENCIES twitter tzinfo-data uglifier (>= 1.3.0) + unicorn vcr web-console (>= 3.3.0) webmock diff --git a/Procfile b/Procfile index 9fb04ab5..e08b23cd 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ -web: bundle exec puma -C config/puma.rb -resque: env QUEUE=* TERM_CHILD=1 bundle exec rake resque:work +web: bundle exec unicorn -C config/unicorn.rb +sidekiq: bundle exec sidekiq -C config/sidekiq.yml diff --git a/app/jobs/submission_check_job.rb b/app/jobs/submission_check_job.rb deleted file mode 100644 index acbc5422..00000000 --- a/app/jobs/submission_check_job.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true -class SubmissionCheckJob - @queue = :submission_check - - def self.perform(submission_id) - submission = Submission.find(submission_id) - submission.run_tests - end -end diff --git a/app/models/output_test_runner.rb b/app/models/output_test_runner.rb index 629011d8..0f36cdf3 100644 --- a/app/models/output_test_runner.rb +++ b/app/models/output_test_runner.rb @@ -67,6 +67,7 @@ def capture_output end def clean_string(string) + string = string.force_encoding('ISO-8859-2').encode!('UTF-8') string.split("\n").map(&:strip).reject(&:blank?).join("\n") end end diff --git a/app/models/submission.rb b/app/models/submission.rb index 8c7406c6..a2b1a4a8 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -14,7 +14,7 @@ class Submission < ApplicationRecord end after_commit on: :create do - Resque.enqueue(SubmissionCheckJob, id) + SubmissionCheckWorker.perform_async(id) end after_save do diff --git a/app/models/user.rb b/app/models/user.rb index 11bd5344..0e29ea7b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -57,7 +57,7 @@ def github_repositories_with_admin_permissions end def create_github_webhook(repository) - url = Rails.application.routes.url_helpers.github_webhooks_url + url = Rails.application.routes.url_helpers.github_webhooks_url(protocol: 'https') github_client.create_hook(repository, 'web', { url: url, content_type: :json, secret: ENV.fetch('GITHUB_WEBHOOK_SECRET') }, diff --git a/app/workers/submission_check_worker.rb b/app/workers/submission_check_worker.rb new file mode 100644 index 00000000..91078585 --- /dev/null +++ b/app/workers/submission_check_worker.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +class SubmissionCheckWorker + include Sidekiq::Worker + sidekiq_options queue: 'high' + + def perform(submission_id) + submission = Submission.find(submission_id) + submission.run_tests + end +end diff --git a/config/cable.yml b/config/cable.yml index ab1c018b..4fbdf25b 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -7,4 +7,4 @@ test: production: adapter: redis - url: <%= ENV['REDISTOGO_URL'] %> + url: redis://:<%= ENV['REDIS_PASSWORD']%>@localhost:6379 diff --git a/config/database.yml b/config/database.yml index 22a91acd..0fa4e8f6 100644 --- a/config/database.yml +++ b/config/database.yml @@ -71,4 +71,4 @@ production: <<: *default database: alphagrader_production username: alphagrader - password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %> + password: <%= ENV['DATABASE_PASSWORD'] %> diff --git a/config/deploy.rb b/config/deploy.rb new file mode 100644 index 00000000..f3c71f58 --- /dev/null +++ b/config/deploy.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'dotenv' +# config valid only for Capistrano 3 +lock '3.8.2' + +set :user, 'deploy' +set :ssh_options, user: fetch(:user) + +set :application, 'alphagrader' +set :ssl_cert_name, 'alphagrader' +set :repo_url, 'git@github.com:rhomeister/alphagrader.git' + +set :pty, true + +set :pg_extensions, %w(hstore unaccent) + +set :memcached_ip, :all # make memcached listen to all IP addresses. The firewall blocks any unauthorized connection + +# These settings are only useful when updating the SSL certificate using the "nginx:setup_ssl" task. This task will +# upload the local SSL certificate and key to the server. +set :nginx_ssl_cert_local_path, ".cert/#{fetch(:ssl_cert_name)}_bundle.crt" +set :nginx_ssl_cert_key_local_path, ".cert/#{fetch(:ssl_cert_name)}.key" + +# enabled logrotate for unicorn logs +set :unicorn_logrotate_enabled, true + +set :unicorn_env, 'RUBY_GC_MALLOC_LIMIT=100000000 LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1' + +# define which roles are going to have a release installed. These are the app role, the roles that run Sidekiq and +# the role that runs the DB migrations +code_release_roles = -> { [:app, :worker, fetch(:migration_role)] } + +set :rvm1_roles, code_release_roles # only install Ruby on release roles that are going to need the code +set :bundle_roles, code_release_roles # bundling is only necessary on nodes that have the Ruby code +set :memcached_roles, %i(app worker) +set :console_role, :worker + +set :sidekiq_role, :worker +set :sidekiq_config, 'config/sidekiq.yml' +set :sidekiq_user, 'deploy' +set :sidekiq_default_hooks, false +after 'deploy:published', 'sidekiq:monit:restart' # restart using monit, not systemctl + +set :assets_roles, %i(app worker db) + +# split whenever scripts per environment (stage) +set :whenever_identifier, -> { "#{fetch(:application)}_#{fetch(:stage)}" } +set :whenever_roles, [:db] + +production_config = Dotenv::Parser.new(File.read('.env.production')).call +set :aws_access_key_id, production_config.fetch('S3_ACCESS_KEY') +set :aws_secret_access_key, production_config.fetch('S3_SECRET_KEY') +set :aws_region, production_config.fetch('S3_REGION') + +before 'deploy', 'postgresql:database_yml_symlink' +before 'deploy:compile_assets', 'deploy:symlink:linked_dirs' # we need database.yml because asset_sync connects to DB +before 'deploy:compile_assets', 'deploy:symlink:linked_files' + +# set :log_level, :info + +before 'setup', 'rvm1:install:rvm' +before 'deploy', 'rvm1:install:ruby' + +task :setup do + invoke 'dotenv:setup' + invoke 'sidekiq:monit:config' +end + +namespace :rvm1 do # https://github.com/rvm/rvm1-capistrano3/issues/45 + desc 'Setup bundle alias' + task :create_bundle_alias do + on release_roles :all do + execute %(echo "alias bundle='#{fetch(:rvm1_auto_script_path)}/rvm-auto.sh . bundle'" > ~/.bash_aliases) + end + end + + desc 'Install Bundler' + task :install_bundler do + on release_roles :all do + execute "cd #{release_path} && #{fetch(:rvm1_auto_script_path)}/rvm-auto.sh . gem install bundler" + end + end +end +after 'rvm1:install:rvm', 'rvm1:create_bundle_alias' +after 'rvm1:install:ruby', 'rvm1:install_bundler' diff --git a/config/deploy/production.rb b/config/deploy/production.rb new file mode 100644 index 00000000..91c07a6a --- /dev/null +++ b/config/deploy/production.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +set :stage, :production +set :rails_env, fetch(:stage) # set the rails environment + +set :branch, 'capistrano' + +ansible_roles('config/ansible/production_linode') # load roles from ansible repository + +set :migration_role, :db # the primary (first) DB performs the migrations +set :pg_host, primary(:db).hostname # setting the host name of PostgreSQL in the database.yml config + +set :unicorn_workers, 2 # run two unicorn workers per app server + +set :memcached_memory_limit, 128 + +set :nginx_upload_local_cert, true # set this to true when there is a new SSL certificate +set :nginx_use_http2, true +set :nginx_use_ssl, true +set :nginx_pass_ssl_client_cert, true # pass the client SSL certificate to the ruby code +set :nginx_fail_timeout, 0 diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb new file mode 100644 index 00000000..80dc5a5e --- /dev/null +++ b/config/deploy/staging.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +set :stage, :staging +set :rails_env, fetch(:stage) # set the rails environment + +set :branch, 'cap3' + +ansible_roles('config/ansible/staging_linode') # load roles from ansible repository + +set :migration_role, :db # the primary (first) DB performs the migrations +set :pg_host, primary(:db).hostname # setting the host name of PostgreSQL in the database.yml config + +set :unicorn_workers, 4 # run two unicorn workers per app server + +set :memcached_memory_limit, 64 + +set :nginx_upload_local_cert, true # set this to true when there is a new SSL certificate +set :nginx_use_http2, true +set :nginx_use_ssl, true +set :nginx_pass_ssl_client_cert, true # pass the client SSL certificate to the ruby code +set :nginx_fail_timeout, 0 diff --git a/config/environments/production.rb b/config/environments/production.rb index 3c7de101..00fa1c7f 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -42,7 +42,7 @@ %(https:\/\/#{ENV.fetch('DOMAIN_NAME')}.*)] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true + config.force_ssl = true # Use the lowest log level to ensure availability of diagnostic information # when problems arise. diff --git a/config/initializers/resque.rb b/config/initializers/resque.rb deleted file mode 100644 index fbae198e..00000000 --- a/config/initializers/resque.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true -REDIS_URL = ENV['REDISTOGO_URL'] || 'localhost:6379' -Resque.redis = REDIS_URL diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 00000000..d08401e1 --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +redis_config = { + url: "redis://#{ENV['REDIS_HOST'] || 'localhost'}:6379", + password: ENV['REDIS_PASSWORD'], + namespace: 'alphagrader' +} + +Sidekiq.options[:poll_interval] = 2 + +Sidekiq.configure_server do |config| + sidekiq_config = YAML.load(File.new("#{Rails.root}/config/sidekiq.yml")) + config.redis = redis_config + schedule_file = 'config/sidekiq_schedule.yml' + + # set the ActiveRecord pool size equal to concurrency + + if defined?(ActiveRecord::Base) + config = ActiveRecord::Base.configurations[Rails.env] || + Rails.application.config.database_configuration[Rails.env] + + # not sure why +1 is necessary, but otherwise getting + # ActiveRecord::ConnectionTimeOut errors + config['pool'] = sidekiq_config[:concurrency] + 1 + ActiveRecord::Base.establish_connection(config) + end + + if File.exist?(schedule_file) && Sidekiq.server? + Sidekiq::Cron::Job.load_from_hash! YAML.load_file(schedule_file) + end +end + +Sidekiq.configure_client do |config| + config.redis = redis_config +end diff --git a/config/routes.rb b/config/routes.rb index 7ecc9c5a..d5f0c27b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require 'resque_web' + +require 'sidekiq/web' Rails.application.routes.draw do devise_for :users, controllers: { registrations: 'registrations', omniauth_callbacks: 'omniauth_callbacks' } @@ -43,7 +44,7 @@ ActiveAdmin.routes(self) - resque_web_constraint = lambda do |request| + sidekiq_web_constraint = lambda do |request| if Rails.env.development? true else @@ -52,8 +53,8 @@ end end - constraints resque_web_constraint do - mount ResqueWeb::Engine, at: '/resque' + constraints sidekiq_web_constraint do + mount Sidekiq::Web => '/admin/sidekiq' end # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html diff --git a/config/sidekiq.yml b/config/sidekiq.yml new file mode 100644 index 00000000..b5cae60d --- /dev/null +++ b/config/sidekiq.yml @@ -0,0 +1,11 @@ +:concurrency: 20 +:pidfile: tmp/pids/sidekiq.pid +staging: + :concurrency: 5 +production: + :concurrency: 1 +:queues: + - high + - default + - mailers + - low diff --git a/lib/capistrano/tasks/ansible.rake b/lib/capistrano/tasks/ansible.rake new file mode 100644 index 00000000..31d56bbd --- /dev/null +++ b/lib/capistrano/tasks/ansible.rake @@ -0,0 +1,41 @@ +require "English" + +# This method creates roles from an Ansible inventory + +def ansible_roles(file = nil) + file ||= "config/ansible/#{fetch(:stage)}" + + contents = IO.read(file) + + header = '=' * 30 + " Reading roles from Ansible inventory " + '=' * 30 + puts header + + current_role = nil + contents.split("\n").each do |line| + next if line.strip.start_with? '#' + + if line =~ /^\[(\w+)\]/ + current_role = $1 + elsif line =~ /^(?[\.\w]+).+ (ansible_ssh_host=(?[\.\w]+))?/ + if current_role.nil? + puts "[INFO] Host without role found. Skipping: #{line}" + next + end + + hostname = $LAST_MATCH_INFO[:hostname] + ssh_ip = $LAST_MATCH_INFO[:ssh_ip] + + fail "SSH IP is nil for #{hostname}" if ssh_ip.nil? + + no_release = !(line =~ /no_release=true/).nil? + + host_string = format('%-20s', ssh_ip || hostname) + puts "Creating role: #{format('%-10s', current_role)} host: #{host_string} no_release: #{no_release}" + + role current_role, ssh_ip || hostname, no_release: no_release + elsif !line.strip.empty? + fail "[ERROR] found unparsable line: #{line}" + end + end + puts '=' * header.length +end diff --git a/lib/capistrano/tasks/dotenv.rake b/lib/capistrano/tasks/dotenv.rake new file mode 100644 index 00000000..b5148cfd --- /dev/null +++ b/lib/capistrano/tasks/dotenv.rake @@ -0,0 +1,19 @@ + +namespace :dotenv do + desc 'Setup the .env file' + task :setup do + on release_roles :all do + name = ".env.#{fetch(:stage)}" + file = File.open(name) + destination = shared_path.join(name) + execute :mkdir, '-pv', File.dirname(destination) + upload! file, destination + end + end + + task :symlink do + name = ".env.#{fetch(:stage)}" + set :linked_files, fetch(:linked_files, []).push(name) + end + before 'deploy:symlink:linked_files', 'dotenv:symlink' +end diff --git a/lib/capistrano/tasks/rails_console.rake b/lib/capistrano/tasks/rails_console.rake new file mode 100644 index 00000000..f0708e63 --- /dev/null +++ b/lib/capistrano/tasks/rails_console.rake @@ -0,0 +1,54 @@ +namespace :rails do + task :setup_console do # setup a clear prompt + irbrc = %q( + env_string = Rails.env + + unless Rails.env.development? + env_string = "\e[31;1;5m#{env_string}\e[0m" + end + + IRB.conf[:PROMPT][:RAILS_ENV] = { + :PROMPT_I => "#{Rails.application.class.parent_name} #{env_string}> ", + :PROMPT_N => "#{Rails.application.class.parent_name} #{env_string}> ", + :PROMPT_S => nil, + :PROMPT_C => "?> ", + :RETURN => "=> %s\n" + } + + IRB.conf[:PROMPT_MODE] = :RAILS_ENV + ) + + on primary :app do + upload! StringIO.new(irbrc), '/home/deploy/.irbrc' + end + end + + desc 'Remote console' + task :console do + on primary fetch(:console_role) do |h| + run_interactively "bundle exec rails console #{fetch(:rails_env)}", h.user + end + end + before 'rails:console', 'rails:setup_console' + + desc 'Remote dbconsole' + task :dbconsole do + on primary fetch(:console_role) do |h| + run_interactively "bundle exec rails dbconsole #{fetch(:rails_env)}", h.user + end + end + + desc 'SSH console' + task :ssh_console do + # role = ENV['CAP_ROLE'] || :console + on primary fetch(:console_role) do |_h| + run_interactively '/bin/bash', 'root' + end + end + + def run_interactively(command, user) + user ||= fetch(:user) + info "Running `#{command}` as #{user}@#{host}" + exec %(ssh #{user}@#{host} -t "bash --login -c 'cd #{fetch(:deploy_to)}/current && #{command}'") + end +end diff --git a/lib/tasks/reque.rake b/lib/tasks/reque.rake deleted file mode 100644 index 8c58c40c..00000000 --- a/lib/tasks/reque.rake +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true -require 'resque/tasks' -task 'resque:setup' => :environment diff --git a/spec/features/submissions_spec.rb b/spec/features/submissions_spec.rb index f008cdcc..1e443c3e 100644 --- a/spec/features/submissions_spec.rb +++ b/spec/features/submissions_spec.rb @@ -16,7 +16,7 @@ other_membership = course.memberships.create!(role: :student, user: create(:user)) other_team = create(:team, memberships: [other_membership], assignment: assignment) - other_team.submissions << submission = create(:submission, assignment: assignment) + other_team.submissions << submission = create(:file_submission, assignment: assignment) login_as(user) visit assignment_submissions_path(assignment) diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 0a2f40ee..39c96dfa 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -103,7 +103,7 @@ assignment = create(:assignment) assignment.course.memberships.create!(user: user, role: :student) team = create(:team, assignment: assignment) - submission = create(:submission, team: team, assignment: assignment) + submission = create(:file_submission, team: team, assignment: assignment) expect(ability.can?(:read, submission)).to be false expect(Submission.accessible_by(ability)).to be_empty @@ -114,7 +114,7 @@ assignment = create(:assignment) membership = assignment.course.memberships.create!(user: user, role: :student) team = create(:team, assignment: assignment, memberships: [membership]) - submission = create(:submission, team: team, assignment: assignment) + submission = create(:file_submission, team: team, assignment: assignment) expect(ability.can?(:read, submission)).to be true expect(Submission.accessible_by(ability)).to eq [submission] @@ -130,7 +130,7 @@ membership = assignment.course.memberships.create!(user: student, role: :student) team = create(:team, assignment: assignment, memberships: [membership]) - submission = create(:submission, team: team, assignment: assignment) + submission = create(:file_submission, team: team, assignment: assignment) expect(Submission.accessible_by(ability)).to eq [submission] expect(ability.can?(:read, submission)).to be true diff --git a/spec/models/assignment_spec.rb b/spec/models/assignment_spec.rb index e1fa55fc..77e82009 100644 --- a/spec/models/assignment_spec.rb +++ b/spec/models/assignment_spec.rb @@ -4,7 +4,7 @@ describe Assignment, type: :model do it 'has submissions' do assignment = create(:assignment) - submission = create(:submission) + submission = build(:file_submission) assignment.submissions << submission expect(submission.reload.assignment).to eq assignment diff --git a/spec/models/author_contribution_test_spec.rb b/spec/models/author_contribution_test_spec.rb index 328af529..947bdb55 100644 --- a/spec/models/author_contribution_test_spec.rb +++ b/spec/models/author_contribution_test_spec.rb @@ -3,6 +3,8 @@ describe AuthorContributionTest, type: :model do context 'run' do + before { Sidekiq::Testing.fake! } + it 'returns success if all team members are authors' do user = create(:user) team = create(:team, memberships: [create(:membership, user: user)]) diff --git a/spec/models/expected_output_test_spec.rb b/spec/models/expected_output_test_spec.rb index 654de98b..4a71a663 100644 --- a/spec/models/expected_output_test_spec.rb +++ b/spec/models/expected_output_test_spec.rb @@ -3,6 +3,8 @@ describe ExpectedOutputTest, type: :model do context 'run' do + before { Sidekiq::Testing.fake! } + it 'returns success if the program runs and returns the correct output' do submission = create(:submission) FileUtils.cp('spec/fixtures/dummy_programs/adder', submission.tempdir + '/run') diff --git a/spec/models/file_submission_spec.rb b/spec/models/file_submission_spec.rb index d7efd44f..f1106e90 100644 --- a/spec/models/file_submission_spec.rb +++ b/spec/models/file_submission_spec.rb @@ -3,7 +3,6 @@ describe FileSubmission, type: :model do it 'downloads and unzips the file before running tests' do - Resque.inline = true assignment = create(:assignment) assignment.tests << create(:expected_output_test) @@ -11,12 +10,9 @@ submission.file = File.new('spec/fixtures/dummy_submissions/correct.zip') submission.save! expect(submission.reload.status).to eq 'success' - - Resque.inline = false end it 'injects the run file if its missing and a programming language has been chosen' do - Resque.inline = true assignment = create(:assignment) assignment.tests << create(:expected_output_test) @@ -24,12 +20,9 @@ submission.file = File.new('spec/fixtures/dummy_submissions/correct_without_runfile.zip') submission.save! expect(submission.reload.status).to eq 'success' - - Resque.inline = false end it 'does not overwrite the run file submitted by the user' do - Resque.inline = true assignment = create(:assignment) assignment.tests << create(:expected_output_test) @@ -37,7 +30,5 @@ submission.file = File.new('spec/fixtures/dummy_submissions/correct.zip') submission.save! expect(submission.reload.status).to eq 'success' - - Resque.inline = false end end diff --git a/spec/models/regexp_output_test_spec.rb b/spec/models/regexp_output_test_spec.rb index d6d42e35..cdb783cf 100644 --- a/spec/models/regexp_output_test_spec.rb +++ b/spec/models/regexp_output_test_spec.rb @@ -3,6 +3,8 @@ describe RegexpOutputTest, type: :model do context 'run' do + before { Sidekiq::Testing.fake! } + it 'returns success if the program runs and returns the correct output' do submission = create(:submission) FileUtils.cp('spec/fixtures/dummy_programs/adder_with_prompt', submission.tempdir + '/run') diff --git a/spec/models/required_file_test_spec.rb b/spec/models/required_file_test_spec.rb index c436fe5b..43f2264e 100644 --- a/spec/models/required_file_test_spec.rb +++ b/spec/models/required_file_test_spec.rb @@ -3,6 +3,8 @@ describe RequiredFileTest, type: :model do context 'run' do + before { Sidekiq::Testing.fake! } + it 'returns success if the file is present' do submission = create(:submission) File.open(submission.tempdir + '/report.pdf', 'w') do |file| diff --git a/spec/models/submission_spec.rb b/spec/models/submission_spec.rb index 84ca04f6..57b0e766 100644 --- a/spec/models/submission_spec.rb +++ b/spec/models/submission_spec.rb @@ -3,6 +3,7 @@ describe Submission, type: :model do it 'has been uploaded by a user' do + Sidekiq::Testing.fake! submission = create(:submission) user = create(:user) @@ -13,6 +14,7 @@ end it 'has contributions' do + Sidekiq::Testing.fake! submission = create(:submission) submission.contributions = contributions = create_list(:contribution, 3) @@ -25,6 +27,7 @@ end it 'has test results' do + Sidekiq::Testing.fake! submission = create(:submission) submission.test_results = results = create_list(:test_result, 3) diff --git a/spec/models/team_spec.rb b/spec/models/team_spec.rb index cf8cfebb..1e3c4614 100644 --- a/spec/models/team_spec.rb +++ b/spec/models/team_spec.rb @@ -15,7 +15,8 @@ it 'has submissions' do team = create(:team) - team.submissions = submissions = create_list(:submission, 3) + Sidekiq::Testing.fake! + team.submissions = submissions = create_list(:file_submission, 3) expect(team.submissions).to match_array(submissions) submissions.each do |submission| diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7e8e0579..8842835c 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -7,6 +7,7 @@ require 'spec_helper' require 'rspec/rails' require 'webmock/rspec' +require 'sidekiq/testing' # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in @@ -38,7 +39,8 @@ config.use_transactional_fixtures = true config.before(:each) do - Resque.inline = false + Sidekiq::Worker.clear_all + Sidekiq::Testing.inline! ActionMailer::Base.deliveries.clear end