diff options
36 files changed, 368 insertions, 30 deletions
@@ -12,4 +12,4 @@ export GUIX_PACKAGE_PATH="$GUILE_LOAD_PATH" use guix --fallback -l guix.scm -source .secrets.envrc +source_env .local.envrc @@ -20,4 +20,4 @@ .terraform -.secrets.envrc +.local.envrc diff --git a/app/assets/javascripts/mini_environments.js b/app/assets/javascripts/mini_environments.js deleted file mode 100644 index dee720f..0000000 --- a/app/assets/javascripts/mini_environments.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. diff --git a/app/controllers/govuk_guix/revisions_controller.rb b/app/controllers/govuk_guix/revisions_controller.rb new file mode 100644 index 0000000..a890fd0 --- /dev/null +++ b/app/controllers/govuk_guix/revisions_controller.rb @@ -0,0 +1,4 @@ +class GovukGuix::RevisionsController < ApplicationController + def index + end +end diff --git a/app/controllers/mini_environments_controller.rb b/app/controllers/mini_environments_controller.rb index c45ed4a..8009777 100644 --- a/app/controllers/mini_environments_controller.rb +++ b/app/controllers/mini_environments_controller.rb @@ -8,14 +8,38 @@ class MiniEnvironmentsController < ApplicationController end def create - ActiveRecord::Base.transaction do - @mini_environment = MiniEnvironment.create( - params.require(:mini_environment).permit(:name) - ) + @mini_environment = MiniEnvironment.create( + params + .require(:mini_environment) + .permit( + :name, + :govuk_guix_revision_id + ) + ) - SetupJob.enqueue(@mini_environment.id) - end + GovukGuix::BuildJob.enqueue(@mini_environment.id) redirect_to @mini_environment end + + def perform_action + @mini_environment = MiniEnvironment.find(params[:mini_environment_id]) + + action = params.require(:commit) + + case action + when 'Destroy' + DestroyJob.enqueue(@mini_environment.id) + + flash[:notice] = "Destroying mini environment" + when 'Start' + StartJob.enqueue(@mini_environment.id) + + flash[:notice] = "Starting mini environment" + else + flash[:error] = "Unknown action #{action}" + end + + render 'show' + end end diff --git a/app/controllers/que_jobs_controller.rb b/app/controllers/que_jobs_controller.rb new file mode 100644 index 0000000..bab2c47 --- /dev/null +++ b/app/controllers/que_jobs_controller.rb @@ -0,0 +1,19 @@ +class QueJobsController < ApplicationController + def cancel + ActiveRecord::Base.connection.execute( + "DELETE FROM que_jobs WHERE job_id = #{job_id}::bigint" + ) + end + + def retry_now + ActiveRecord::Base.connection.execute( + "UPDATE que_jobs + SET run_at = now() + WHERE job_id = #{job_id}::bigint" + ) + end + + def job_id + params[:id].to_i + end +end diff --git a/app/helpers/govuk_guix/revisions_helper.rb b/app/helpers/govuk_guix/revisions_helper.rb new file mode 100644 index 0000000..eb84706 --- /dev/null +++ b/app/helpers/govuk_guix/revisions_helper.rb @@ -0,0 +1,2 @@ +module GovukGuix::RevisionsHelper +end diff --git a/app/jobs/destroy_job.rb b/app/jobs/destroy_job.rb index 7f13fdd..2bc72c1 100644 --- a/app/jobs/destroy_job.rb +++ b/app/jobs/destroy_job.rb @@ -12,6 +12,7 @@ class DestroyJob < TerraformJob aws_region: 'eu-west-1', slug: @mini_environment.name.parameterize, ssh_public_key: ssh_public_key, + start_command: @mini_environment.start_command }, force: true ) diff --git a/app/jobs/govuk_guix/build_job.rb b/app/jobs/govuk_guix/build_job.rb new file mode 100644 index 0000000..381a717 --- /dev/null +++ b/app/jobs/govuk_guix/build_job.rb @@ -0,0 +1,33 @@ +require 'ruby_terraform' +require 'open3' + +class GovukGuix::BuildJob < GovukGuix::Job + @retry_interval = 30 + + def run(mini_environment_id) + logger.info "GovukGuix::BuildJob: Building mini environment #{mini_environment_id}" + + mini_environment = MiniEnvironment.find(mini_environment_id) + + slug = mini_environment.name.parameterize + + command = [ + "#{mini_environment.govuk_guix_revision.store_path}/bin/govuk", + "system", + "build", + "--rails-environment=production", + "--app-domain=#{slug}.aws.cbaines.net", + "--web-domain=www.#{slug}.aws.cbaines.net", + "--use-high-ports=false", + "--use-https=certbot", + "--fallback", + ] + + run_command(command) do |output| + store_path = output.last.strip + logger.debug("GovukGuix::BuildJob: start_command: #{store_path}") + + mini_environment.update(start_command: store_path) + end + end +end diff --git a/app/jobs/govuk_guix/fetch_revision_job.rb b/app/jobs/govuk_guix/fetch_revision_job.rb new file mode 100644 index 0000000..b1cb171 --- /dev/null +++ b/app/jobs/govuk_guix/fetch_revision_job.rb @@ -0,0 +1,49 @@ +require 'ruby_terraform' +require 'git' +require 'open3' + +class GovukGuix::FetchRevisionJob < GovukGuix::Job + @retry_interval = 30 + + def run(commit_hash) + repository.fetch + + repository.checkout(commit_hash) + + command = [ + "#{repository_directory}/guix-pre-inst-env", + "guix", + "build", + "-e", + "(begin (use-modules (gds packages govuk)) (current-govuk-guix))" + ] + + run_command(command) do |output| + store_path = output.last.strip + logger.debug("FetchRevisionJob: store_path: #{store_path}") + + GovukGuix::Revision.create( + commit_hash: commit_hash, + store_path: store_path + ) + end + end + + def repository + @_repository ||= begin + if File.exists? repository_directory + Git.open(repository_directory, :log => Rails.logger) + else + Git.clone(repository_remote_location, repository_directory) + end + end + end + + def repository_directory + 'tmp/cache/govuk-guix' + end + + def repository_remote_location + 'git@git.cbaines.net:gds/govuk-guix' + end +end diff --git a/app/jobs/govuk_guix/job.rb b/app/jobs/govuk_guix/job.rb new file mode 100644 index 0000000..0161cb1 --- /dev/null +++ b/app/jobs/govuk_guix/job.rb @@ -0,0 +1,24 @@ +class GovukGuix::Job < Que::Job + def run_command(command) + logger.debug("#{self.class}: Running command #{command}") + + Open3.popen2e(*command) do |stdin, stdout_and_stderr, wait_thr| + logger.info("#{self.class}: commmand running, pid #{wait_thr.pid}") + + output = [] + stdout_and_stderr.each_line do |line| + logger.debug("#{self.class}: #{line}") + output << line + end + + exit_status = wait_thr.value + unless exit_status == 0 + logger.error("#{self.class}: failed, exit status #{exit_status}") + + raise "#{output.join}\n" + end + + yield(output) + end + end +end diff --git a/app/jobs/setup_job.rb b/app/jobs/start_job.rb index 6791dd6..ec17732 100644 --- a/app/jobs/setup_job.rb +++ b/app/jobs/start_job.rb @@ -1,10 +1,10 @@ require 'ruby_terraform' -class SetupJob < TerraformJob +class StartJob < TerraformJob @retry_interval = 30 def run_terraform - puts "Setting up #{@mini_environment.name}" + logger.info "Setting up #{@mini_environment.name}" Dir.chdir('terraform/aws') do RubyTerraform.init( @@ -15,11 +15,11 @@ class SetupJob < TerraformJob ) RubyTerraform.apply( - directory: 'mini_environment', vars: { aws_region: 'eu-west-1', slug: @mini_environment.name.parameterize, ssh_public_key: ssh_public_key, + start_command: @mini_environment.start_command }, auto_approve: true ) diff --git a/app/models/govuk_guix.rb b/app/models/govuk_guix.rb new file mode 100644 index 0000000..e98da8e --- /dev/null +++ b/app/models/govuk_guix.rb @@ -0,0 +1,5 @@ +module GovukGuix + def self.table_name_prefix + 'govuk_guix_' + end +end diff --git a/app/models/govuk_guix/revision.rb b/app/models/govuk_guix/revision.rb new file mode 100644 index 0000000..147b6d6 --- /dev/null +++ b/app/models/govuk_guix/revision.rb @@ -0,0 +1,3 @@ +class GovukGuix::Revision < ApplicationRecord + self.primary_key = "commit_hash" +end diff --git a/app/models/mini_environment.rb b/app/models/mini_environment.rb index 2d6d8ae..87ecff8 100644 --- a/app/models/mini_environment.rb +++ b/app/models/mini_environment.rb @@ -1,5 +1,6 @@ class MiniEnvironment < ApplicationRecord has_many :finished_terraform_jobs, dependent: :destroy + belongs_to :govuk_guix_revision, class_name: 'GovukGuix::Revision' def enqueued_terraform_jobs Que.execute("SELECT * FROM que_jobs WHERE args->>0 = '#{id}'") diff --git a/app/views/govuk_guix/revisions/index.html.erb b/app/views/govuk_guix/revisions/index.html.erb new file mode 100644 index 0000000..54f9e4a --- /dev/null +++ b/app/views/govuk_guix/revisions/index.html.erb @@ -0,0 +1,10 @@ + +<% GovukGuix::Revision.all.each do |revision| %> + + <div> + + <%= revision.inspect %> + + </div> + +<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 5b90ace..35ea5ff 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,6 +1,5 @@ <% content_for :head do %> <%= stylesheet_link_tag "application", :media => "all" %> - <%= javascript_include_tag "application" %> <%= csrf_meta_tag %> <%= yield :extra_headers %> <% end %> diff --git a/app/views/mini_environments/new.html.erb b/app/views/mini_environments/new.html.erb index 84d2316..7d3f2f2 100644 --- a/app/views/mini_environments/new.html.erb +++ b/app/views/mini_environments/new.html.erb @@ -1,8 +1,8 @@ <div class="row"> <div class="col-md-8"> - <%= form_for @mini_environment, - url: { action: "create" }, - html: { class: 'form-horizontal' } do |f| %> + <%= form_with(model: @mini_environment, + url: { action: "create" }, + html: { class: 'form-horizontal' }) do |f| %> <div class="form-group"> <%= f.label :name, class: 'col-sm-2 control-label' %> @@ -10,6 +10,13 @@ <%= f.text_field :name, class: 'form-control' %> </div> </div> + + <div class="form-group"> + <%= f.label :govuk_guix_revision_id, class: 'col-sm-2 control-label' %> + <div class="col-sm-10"> + <%= f.collection_select :govuk_guix_revision_id, GovukGuix::Revision.all, :commit_hash, :commit_hash %> + </div> + </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> diff --git a/app/views/mini_environments/show.html.erb b/app/views/mini_environments/show.html.erb index 0cc6bc6..a852e6c 100644 --- a/app/views/mini_environments/show.html.erb +++ b/app/views/mini_environments/show.html.erb @@ -1,5 +1,21 @@ <h1>Name: <%= @mini_environment.name %></h1> +<%= form_with(url: mini_environment_perform_action_path(@mini_environment), + local: true, + method: "post") do %> + <%= submit_tag('Start', + disabled: !@mini_environment.start_command.present?, + role: 'button', + class: 'btn btn-lg btn-success') + %> + + <%= submit_tag("Destroy", + #disabled: !@mini_environment.start_command.present?, + role: 'button', + class: 'btn btn-lg btn-danger') + %> +<% end %> + <a href="https://signon.<%= @mini_environment.name.parameterize %>.aws.cbaines.net">View</a> <h2>Finished jobs</h2> @@ -11,10 +27,48 @@ <h2>Enqueued jobs</h2> -<% @mini_environment.enqueued_terraform_jobs.each do |job| %> - <%= job['job_class'] %> +<div class="panel-group" + id="accordion" + role="tablist" + aria-multiselectable="true"> + + <% @mini_environment.enqueued_terraform_jobs.each do |job| %> - <pre> - <%= job['last_error'] %> - </pre> + <div class="panel panel-default"> + <div class="panel-heading" role="tab" id="headingOne"> + <h4 class="panel-title"> + <a role="button" + data-toggle="collapse" + data-parent="#accordion" + href="#collapseOne" + aria-expanded="true" + aria-controls="collapseOne"> + <%= job['job_class'] %> + </a> + <span class="pull-right"> + Retrying next in <%= distance_of_time_in_words_to_now(job['run_at'], include_seconds: true) %> + </span> + </h4> + </div> + <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne"> + <div class="panel-body"> + This job has failed <%= job['error_count'] %> times. + + <div class="pull-right"> + <%= form_with(url: retry_now_que_job_path(job['job_id'])) do %> + <%= submit_tag('Retry now', class: 'btn btn-default') + %> + <% end %> + <%= form_with(url: cancel_que_job_path(job['job_id'])) do %> + <%= submit_tag('Cancel', class: 'btn btn-default') + %> + <% end %> + </div> + + <pre><%= job['last_error'] %></pre> + </div> + </div> + </div> <% end %> + +</div> diff --git a/app/views/que_jobs/cancel.html.erb b/app/views/que_jobs/cancel.html.erb new file mode 100644 index 0000000..5886237 --- /dev/null +++ b/app/views/que_jobs/cancel.html.erb @@ -0,0 +1,2 @@ +<h1>QueJobs#cancel</h1> +<p>Find me in app/views/que_jobs/cancel.html.erb</p> diff --git a/app/views/que_jobs/retry_now.html.erb b/app/views/que_jobs/retry_now.html.erb new file mode 100644 index 0000000..1c8ecee --- /dev/null +++ b/app/views/que_jobs/retry_now.html.erb @@ -0,0 +1,2 @@ +<h1>QueJobs#retry_now</h1> +<p>Find me in app/views/que_jobs/retry_now.html.erb</p> diff --git a/config/initializers/action_view.rb b/config/initializers/action_view.rb new file mode 100644 index 0000000..142d382 --- /dev/null +++ b/config/initializers/action_view.rb @@ -0,0 +1 @@ +Rails.application.config.action_view.form_with_generates_remote_forms = false diff --git a/config/routes.rb b/config/routes.rb index 0dcd800..06470d3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,5 +5,18 @@ Rails.application.routes.draw do controller: :terraform_http_backend, except: %i[new edit update] - resources :mini_environments, path: '/' + resources :mini_environments, path: '/' do + post '/', to: 'mini_environments#perform_action', as: 'perform_action' + end + + namespace :govuk_guix do + get 'revisions', to: 'revisions#index' + end + + resources :que_jobs, only: [] do + member do + post 'cancel' + post 'retry_now' + end + end end diff --git a/db/migrate/20180216231420_create_govuk_guix_revisions.rb b/db/migrate/20180216231420_create_govuk_guix_revisions.rb new file mode 100644 index 0000000..0572119 --- /dev/null +++ b/db/migrate/20180216231420_create_govuk_guix_revisions.rb @@ -0,0 +1,11 @@ +class CreateGovukGuixRevisions < ActiveRecord::Migration[5.1] + def change + create_table :govuk_guix_revisions do |t| + t.string :treeish + t.string :store_path + + t.timestamps + end + add_index :govuk_guix_revisions, :treeish, unique: true + end +end diff --git a/db/migrate/20180217104954_change_govuk_guix_revisions_column_name.rb b/db/migrate/20180217104954_change_govuk_guix_revisions_column_name.rb new file mode 100644 index 0000000..cf57216 --- /dev/null +++ b/db/migrate/20180217104954_change_govuk_guix_revisions_column_name.rb @@ -0,0 +1,5 @@ +class ChangeGovukGuixRevisionsColumnName < ActiveRecord::Migration[5.1] + def change + rename_column :govuk_guix_revisions, :treeish, :commit_hash + end +end diff --git a/db/migrate/20180217105604_change_govuk_guix_revisions_primary_key.rb b/db/migrate/20180217105604_change_govuk_guix_revisions_primary_key.rb new file mode 100644 index 0000000..5b582ad --- /dev/null +++ b/db/migrate/20180217105604_change_govuk_guix_revisions_primary_key.rb @@ -0,0 +1,7 @@ +class ChangeGovukGuixRevisionsPrimaryKey < ActiveRecord::Migration[5.1] + def change + execute 'ALTER TABLE govuk_guix_revisions DROP CONSTRAINT govuk_guix_revisions_pkey;' + execute 'ALTER TABLE govuk_guix_revisions ADD PRIMARY KEY (commit_hash);' + remove_column :govuk_guix_revisions, :id + end +end diff --git a/db/migrate/20180217110041_add_govuk_guix_revision_to_mini_environment.rb b/db/migrate/20180217110041_add_govuk_guix_revision_to_mini_environment.rb new file mode 100644 index 0000000..56d5883 --- /dev/null +++ b/db/migrate/20180217110041_add_govuk_guix_revision_to_mini_environment.rb @@ -0,0 +1,5 @@ +class AddGovukGuixRevisionToMiniEnvironment < ActiveRecord::Migration[5.1] + def change + add_reference :mini_environments, :govuk_guix_revision, type: :string, foreign_key: { primary_key: :commit_hash } + end +end diff --git a/db/migrate/20180217131053_add_start_command_to_mini_environment.rb b/db/migrate/20180217131053_add_start_command_to_mini_environment.rb new file mode 100644 index 0000000..05d8704 --- /dev/null +++ b/db/migrate/20180217131053_add_start_command_to_mini_environment.rb @@ -0,0 +1,5 @@ +class AddStartCommandToMiniEnvironment < ActiveRecord::Migration[5.1] + def change + add_column :mini_environments, :start_command, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 39cc82c..bf9d773 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180206203924) do +ActiveRecord::Schema.define(version: 20180217131053) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -22,11 +22,21 @@ ActiveRecord::Schema.define(version: 20180206203924) do t.datetime "updated_at", null: false end + create_table "govuk_guix_revisions", primary_key: "commit_hash", id: :string, force: :cascade do |t| + t.string "store_path" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["commit_hash"], name: "index_govuk_guix_revisions_on_commit_hash", unique: true + end + create_table "mini_environments", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "info" + t.string "govuk_guix_revision_id" + t.string "start_command" + t.index ["govuk_guix_revision_id"], name: "index_mini_environments_on_govuk_guix_revision_id" end create_table "minienvironments", force: :cascade do |t| @@ -63,4 +73,5 @@ ActiveRecord::Schema.define(version: 20180206203924) do t.boolean "disabled", default: false end + add_foreign_key "mini_environments", "govuk_guix_revisions", primary_key: "commit_hash" end @@ -18,7 +18,8 @@ ("ruby-govuk-admin-template" ,ruby-govuk-admin-template) ("ruby-plek" ,ruby-plek) ("ruby-terraform" ,ruby-terraform) - ("ruby-que" ,ruby-que))) + ("ruby-que" ,ruby-que) + ("ruby-git" ,ruby-git))) (synopsis "") (description "") (home-page "") @@ -30,6 +31,7 @@ (inputs `(,@(package-inputs govuk-mini-environment-admin) ("ruby" ,ruby) + ("ruby-rerun" ,ruby-rerun) ("postgresql" ,postgresql))))) govuk-mini-environment-admin-development-environment diff --git a/terraform/aws/mini_environment.tf b/terraform/aws/mini_environment.tf index ddaefd0..bf3a356 100644 --- a/terraform/aws/mini_environment.tf +++ b/terraform/aws/mini_environment.tf @@ -14,6 +14,9 @@ variable "ssh_public_key" { type = "string" } +variable "start_command" { + type = "string" +} provider "aws" { region = "${var.aws_region}" @@ -43,9 +46,7 @@ data "template_file" "govuk_service" { template = "${file("${path.module}/mini_environment/govuk.service.tpl")}" vars { - guix_daemon_socket = "guix://${data.aws_instance.guix-daemon.private_dns}", - app_domain = "${var.slug}.aws.cbaines.net", - web_domain = "www.${var.slug}.aws.cbaines.net" + start_command = "${var.start_command}" } } @@ -85,6 +86,8 @@ resource "aws_spot_instance_request" "example" { "sudo mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 ${data.aws_efs_file_system.main.dns_name}:var/guix /var/guix", "echo \"export GUIX_DAEMON_SOCKET=guix://${data.aws_instance.guix-daemon.private_dns}\" | sudo tee /etc/profile.d/guix-daemon-socket.sh", #"sudo systemctl restart cachefilesd", + "sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080", + "sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8443", "sudo mv /home/ubuntu/govuk.service /etc/systemd/system/govuk.service", "sudo systemctl daemon-reload", "sudo systemctl enable govuk.service", diff --git a/terraform/aws/mini_environment/govuk.service.tpl b/terraform/aws/mini_environment/govuk.service.tpl index 750ddaf..5c65267 100644 --- a/terraform/aws/mini_environment/govuk.service.tpl +++ b/terraform/aws/mini_environment/govuk.service.tpl @@ -6,8 +6,7 @@ After=network.target Type=simple User=root WorkingDirectory=/home/ubuntu -Environment="GUIX_DAEMON_SOCKET=${guix_daemon_socket}" -ExecStart=/var/guix/profiles/per-user/ubuntu/guix-profile/bin/govuk system start --rails-environment=production --app-domain=${app_domain} --web-domain=${web_domain} --use-high-ports=false --use-https=certbot --fallback +ExecStart=${start_command} [Install] WantedBy=multi-user.target diff --git a/test/controllers/govuk_guix/revisions_controller_test.rb b/test/controllers/govuk_guix/revisions_controller_test.rb new file mode 100644 index 0000000..ae93202 --- /dev/null +++ b/test/controllers/govuk_guix/revisions_controller_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class GovukGuix::RevisionsControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get govuk_guix_revisions_index_url + assert_response :success + end + +end diff --git a/test/controllers/que_jobs_controller_test.rb b/test/controllers/que_jobs_controller_test.rb new file mode 100644 index 0000000..691d0f3 --- /dev/null +++ b/test/controllers/que_jobs_controller_test.rb @@ -0,0 +1,14 @@ +require 'test_helper' + +class QueJobsControllerTest < ActionDispatch::IntegrationTest + test "should get cancel" do + get que_jobs_cancel_url + assert_response :success + end + + test "should get retry_now" do + get que_jobs_retry_now_url + assert_response :success + end + +end diff --git a/test/fixtures/govuk_guix/revisions.yml b/test/fixtures/govuk_guix/revisions.yml new file mode 100644 index 0000000..ad7058f --- /dev/null +++ b/test/fixtures/govuk_guix/revisions.yml @@ -0,0 +1,9 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + treeish: MyString + store_path: MyString + +two: + treeish: MyString + store_path: MyString diff --git a/test/models/govuk_guix/revision_test.rb b/test/models/govuk_guix/revision_test.rb new file mode 100644 index 0000000..b46181a --- /dev/null +++ b/test/models/govuk_guix/revision_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class GovukGuix::RevisionTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end |