mirror of
https://github.com/ubicloud/ubicloud.git
synced 2025-10-05 22:31:57 +08:00
Table-based model browsing can be friendlier than the unordered list display currently used on the admin site. However, you need to be careful to not introduce N+1 queries when using table-based browsing. AutoForme is a library that builds on top of Forme (already used on the admin site) and provides the ability to browse and search the models in a way that avoids N+1 queries. It's quite flexible, requiring only a few lines of configuration code per model to have it display and allow searching of the columns desired. AutoForme also supports CRUD actions for models, but those are currently disabled, and it is only used for the tabular display and searching. It also supports downloading of data in CSV format (both in browse and search mode), which can be useful with external analysis tools. One potential regression with the AutoForme based browsing and searching is the use of offsets for pagination, instead of using a filter. If this becomes problematic, it's possible to add filter-based pagination to AutoForme. While it is possible to implement table-based browsing without using AutoForme, it would require reimplementing parts of AutoForme, and I think using AutoForme will result in smaller and simpler code in the long run. Currently, this only implements the table-based browsing for Firewall as a proof of concept. We can expand it to other models in the future.
407 lines
15 KiB
Ruby
407 lines
15 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "spec_helper"
|
|
|
|
RSpec.describe CloverAdmin do
|
|
def object_data
|
|
page.all(".object-table tbody tr").map { it.all("td").map(&:text) }.to_h.transform_keys(&:to_sym).except(:created_at)
|
|
end
|
|
|
|
def page_data
|
|
page.all(".page-table tbody tr").map do
|
|
tds = it.all("td").map(&:text)
|
|
tds.delete_at(-3)
|
|
tds
|
|
end
|
|
end
|
|
|
|
before do
|
|
admin_account_setup_and_login
|
|
end
|
|
|
|
let(:vm_pool) do
|
|
vp = VmPool.create(
|
|
size: 3,
|
|
vm_size: "standard-2",
|
|
boot_image: "img",
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
storage_size_gib: 86
|
|
)
|
|
Strand.create(prog: "Vm::VmPool", label: "create_new_vm") { it.id = vp.id }
|
|
vp
|
|
end
|
|
|
|
it "allows searching by ubid and navigating to related objects" do
|
|
expect(page.title).to eq "Ubicloud Admin"
|
|
|
|
account = create_account
|
|
fill_in "UBID", with: account.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - Account #{account.ubid}"
|
|
expect(object_data).to eq(email: "user@example.com", name: "", status_id: "2", suspended_at: "")
|
|
|
|
project = account.projects.first
|
|
click_link project.name
|
|
expect(page.title).to eq "Ubicloud Admin - Project #{project.ubid}"
|
|
expect(object_data).to eq(billable: "true", billing_info_id: "", credit: "0.0", discount: "0", feature_flags: "{}", name: "Default", reputation: "new", visible: "true")
|
|
|
|
subject_tag = project.subject_tags.first
|
|
click_link subject_tag.name
|
|
expect(page.title).to eq "Ubicloud Admin - SubjectTag #{subject_tag.ubid}"
|
|
expect(object_data).to eq(name: "Admin", project_id: "Default")
|
|
|
|
# Column Link
|
|
click_link project.name
|
|
expect(page.title).to eq "Ubicloud Admin - Project #{project.ubid}"
|
|
end
|
|
|
|
it "allows browsing by class" do
|
|
account = create_account
|
|
page.refresh
|
|
click_link "Account"
|
|
click_link account.email
|
|
expect(page.title).to eq "Ubicloud Admin - Account #{account.ubid}"
|
|
|
|
project = account.projects.first
|
|
click_link project.name
|
|
expect(page.title).to eq "Ubicloud Admin - Project #{project.ubid}"
|
|
end
|
|
|
|
it "allows browsing by class when using Autoforme" do
|
|
project = Project.create(name: "Default")
|
|
firewall = Firewall.create(name: "fw", project_id: project.id, location_id: Location::HETZNER_FSN1_ID)
|
|
click_link "Firewall"
|
|
expect(page.title).to eq "Ubicloud Admin - Firewall - Browse"
|
|
expect(page.all("#autoforme_content td").map(&:text)).to eq ["fw", "Default", "hetzner-fsn1", "Default firewall"]
|
|
path = page.current_path
|
|
|
|
click_link firewall.name
|
|
expect(page.title).to eq "Ubicloud Admin - Firewall #{firewall.ubid}"
|
|
|
|
visit path
|
|
click_link project.name
|
|
expect(page.title).to eq "Ubicloud Admin - Project #{project.ubid}"
|
|
|
|
visit path
|
|
click_link "hetzner-fsn1"
|
|
expect(page.title).to eq "Ubicloud Admin - Location #{Location::HETZNER_FSN1_UBID}"
|
|
end
|
|
|
|
it "allows searching by class when using Autoforme" do
|
|
project = Project.create(name: "Default")
|
|
firewall = Firewall.create(name: "fw", project_id: project.id, location_id: Location::HETZNER_FSN1_ID)
|
|
click_link "Firewall"
|
|
click_link "Search"
|
|
expect(page.title).to eq "Ubicloud Admin - Firewall - Search"
|
|
|
|
click_button "Search"
|
|
expect(page.all("#autoforme_content td").map(&:text)).to eq ["fw", "Default", "hetzner-fsn1", "Default firewall"]
|
|
|
|
click_link "Search"
|
|
fill_in "Name", with: "fw2"
|
|
click_button "Search"
|
|
expect(page.all("#autoforme_content td").map(&:text)).to eq []
|
|
|
|
click_link "Search"
|
|
fill_in "Name", with: "fw"
|
|
click_button "Search"
|
|
expect(page.all("#autoforme_content td").map(&:text)).to eq ["fw", "Default", "hetzner-fsn1", "Default firewall"]
|
|
|
|
path = page.current_url
|
|
click_link firewall.name
|
|
expect(page.title).to eq "Ubicloud Admin - Firewall #{firewall.ubid}"
|
|
|
|
visit path
|
|
click_link project.name
|
|
expect(page.title).to eq "Ubicloud Admin - Project #{project.ubid}"
|
|
|
|
visit path
|
|
click_link "hetzner-fsn1"
|
|
expect(page.title).to eq "Ubicloud Admin - Location #{Location::HETZNER_FSN1_UBID}"
|
|
end
|
|
|
|
it "handles basic pagination when browsing by class" do
|
|
accounts = Array.new(101) { |i| create_account("a#{i}@a.com", with_project: false) }
|
|
page.refresh
|
|
click_link "Account"
|
|
found_accounts = page.all("#object-list a").map(&:text)
|
|
|
|
click_link "More"
|
|
found_accounts.concat(page.all("#object-list a").map(&:text))
|
|
|
|
expect(accounts.map(&:email) - found_accounts).to eq []
|
|
account = Account.last
|
|
click_link account.email
|
|
expect(page.title).to eq "Ubicloud Admin - Account #{account.ubid}"
|
|
end
|
|
|
|
it "ignores bogus ubids when paginating" do
|
|
account = create_account
|
|
page.refresh
|
|
click_link "Account"
|
|
page.visit "#{page.current_path}?after=foo"
|
|
click_link account.email
|
|
expect(page.title).to eq "Ubicloud Admin - Account #{account.ubid}"
|
|
end
|
|
|
|
it "shows semaphores set on the object, if any" do
|
|
fill_in "UBID", with: vm_pool.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - VmPool #{vm_pool.ubid}"
|
|
expect(page).to have_no_content "Semaphores Set:"
|
|
|
|
vm_pool.incr_destroy
|
|
page.refresh
|
|
expect(page).to have_content "Semaphores Set: destroy"
|
|
end
|
|
|
|
it "shows object's strand, if any" do
|
|
fill_in "UBID", with: vm_pool.ubid
|
|
click_button "Show Object"
|
|
path = page.current_path
|
|
expect(page.title).to eq "Ubicloud Admin - VmPool #{vm_pool.ubid}"
|
|
expect(page).to have_content "Strand: Vm::VmPool#create_new_vm | schedule: 2"
|
|
expect(page).to have_no_content "| try"
|
|
|
|
vm_pool.strand.update(try: 3)
|
|
visit path
|
|
expect(page).to have_content "| try: 3"
|
|
|
|
click_link "Strand"
|
|
expect(page.title).to eq "Ubicloud Admin - Strand #{vm_pool.ubid}"
|
|
|
|
vm_pool.strand.destroy
|
|
visit path
|
|
expect(page).to have_no_content "Strand"
|
|
end
|
|
|
|
it "shows sshable information for object, if any" do
|
|
vm_host = Prog::Vm::HostNexus.assemble("1.2.3.4").subject
|
|
fill_in "UBID", with: vm_host.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vm_host.ubid}"
|
|
expect(page).to have_content "SSH Command: ssh root@1.2.3.4"
|
|
|
|
visit "/"
|
|
fill_in "UBID", with: vm_pool.ubid
|
|
click_button "Show Object"
|
|
expect(page).to have_no_content "SSH Command"
|
|
end
|
|
|
|
it "shows active pages on index page, grouped by related host" do
|
|
expect(page).to have_no_content "Active Pages"
|
|
|
|
page1 = Page.create(summary: "some problem", tag: "a")
|
|
page.refresh
|
|
expect(page).to have_content "Active Pages"
|
|
expect(page_data).to eq [
|
|
["", page1.ubid, "some problem", "{}"]
|
|
]
|
|
click_link page1.ubid
|
|
expect(page.title).to eq "Ubicloud Admin - Page #{page1.ubid}"
|
|
|
|
page2 = Page.create(summary: "another problem", tag: "b", details: {"related_resources" => [vm_pool.ubid]})
|
|
visit "/"
|
|
expect(page_data).to eq [
|
|
["", page1.ubid, "some problem", "{}"],
|
|
[page2.ubid, "another problem", "{\"related_resources\" => [\"#{vm_pool.ubid}\"]}"]
|
|
]
|
|
click_link vm_pool.ubid
|
|
expect(page.title).to eq "Ubicloud Admin - VmPool #{vm_pool.ubid}"
|
|
|
|
vmh = Prog::Vm::HostNexus.assemble("1.2.3.4").subject
|
|
pj = Project.create(name: "test")
|
|
vm = Prog::Vm::Nexus.assemble("a a", pj.id).subject
|
|
vm.update(vm_host_id: vmh.id)
|
|
page3 = Page.create(summary: "third problem", tag: "c", details: {"related_resources" => [vm.ubid]})
|
|
visit "/"
|
|
expect(page_data).to eq [
|
|
[vmh.ubid, page3.ubid, "third problem", "{\"related_resources\" => [\"#{vm.ubid}\"]}"],
|
|
["", page1.ubid, "some problem", "{}"],
|
|
[page2.ubid, "another problem", "{\"related_resources\" => [\"#{vm_pool.ubid}\"]}"]
|
|
]
|
|
|
|
click_link vmh.ubid
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vmh.ubid}"
|
|
|
|
visit "/"
|
|
click_link vm.ubid
|
|
expect(page.title).to eq "Ubicloud Admin - Vm #{vm.ubid}"
|
|
end
|
|
|
|
it "handles request for invalid ubid" do
|
|
fill_in "UBID", with: "foo"
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin"
|
|
expect(page).to have_flash_error("Invalid ubid provided")
|
|
|
|
fill_in "UBID", with: "ts1cyaqvp5ha6j5jt8ypbyagw9"
|
|
expect { click_button "Show Object" }.to raise_error(RuntimeError, "admin route not handled: /model/SubjectTag/ts1cyaqvp5ha6j5jt8ypbyagw9")
|
|
end
|
|
|
|
it "handles request for invalid model or missing object" do
|
|
%w[/model/Foo/ts1cyaqvp5ha6j5jt8ypbyagw9
|
|
/model/ArchivedRecord/ts1cyaqvp5ha6j5jt8ypbyagw9
|
|
/model/SubjectTag/ts1cyaqvp5ha6j5jt8ypbyagw9].each do |path|
|
|
expect { visit path }.to raise_error(RuntimeError, "admin route not handled: #{path}")
|
|
end
|
|
end
|
|
|
|
it "raises for 404 by default in tests" do
|
|
expect { visit "/invalid-page" }.to raise_error(RuntimeError)
|
|
end
|
|
|
|
it "shows 404 page if DONT_RAISE_ADMIN_ERRORS environment variable is set" do
|
|
ENV["DONT_RAISE_ADMIN_ERRORS"] = "1"
|
|
visit "/invalid-page"
|
|
expect(page.title).to eq "Ubicloud Admin - File Not Found"
|
|
ensure
|
|
ENV.delete("DONT_RAISE_ADMIN_ERRORS")
|
|
end
|
|
|
|
it "raises errors by default in tests" do
|
|
expect { visit "/error" }.to raise_error(RuntimeError)
|
|
end
|
|
|
|
it "shows error page for errors if DONT_RAISE_ADMIN_ERRORS environment variable is set" do
|
|
ENV["DONT_RAISE_ADMIN_ERRORS"] = "1"
|
|
expect(Clog).to receive(:emit).with("admin route exception").and_call_original
|
|
visit "/error"
|
|
expect(page.title).to eq "Ubicloud Admin - Internal Server Error"
|
|
ensure
|
|
ENV.delete("DONT_RAISE_ADMIN_ERRORS")
|
|
end
|
|
|
|
it "handles incorrect/missing CSRF tokens" do
|
|
schedule = Time.now + 10
|
|
st = Strand.create(prog: "Test", label: "hop_entry", schedule:)
|
|
fill_in "UBID", with: st.ubid
|
|
click_button "Show Object"
|
|
|
|
find("#strand-info input[name=_csrf]", visible: false).set("")
|
|
click_button "Schedule Strand to Run Now"
|
|
expect(page.title).to eq "Ubicloud Admin - Invalid Security Token"
|
|
expect(page).to have_flash_error("An invalid security token submitted with this request, please try again")
|
|
expect(st.reload.schedule).not_to be_within(5).of(Time.now)
|
|
end
|
|
|
|
it "raises for 404 by default for missing action" do
|
|
account = create_account(with_project: false)
|
|
path = "/model/Account/#{account.ubid}/invalid"
|
|
expect { visit path }.to raise_error(RuntimeError, "admin route not handled: #{path}")
|
|
end
|
|
|
|
it "supports scheduling strands to run immediately" do
|
|
schedule = Time.now + 10
|
|
st = Strand.create(prog: "Test", label: "hop_entry", schedule:)
|
|
fill_in "UBID", with: st.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - Strand #{st.ubid}"
|
|
|
|
click_button "Schedule Strand to Run Now"
|
|
expect(page).to have_flash_notice("Scheduled strand to run immediately")
|
|
expect(page.title).to eq "Ubicloud Admin - Strand #{st.ubid}"
|
|
expect(st.reload.schedule).to be_within(5).of(Time.now)
|
|
end
|
|
|
|
it "supports restarting Vms" do
|
|
vm = Prog::Vm::Nexus.assemble("dummy-public key", Project.create(name: "Default").id, name: "dummy-vm-1").subject
|
|
fill_in "UBID", with: vm.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - Vm #{vm.ubid}"
|
|
|
|
expect(vm.semaphores_dataset.select_map(:name)).to eq []
|
|
click_link "Restart"
|
|
click_button "Restart"
|
|
expect(page).to have_flash_notice("Restart scheduled for Vm")
|
|
expect(page.title).to eq "Ubicloud Admin - Vm #{vm.ubid}"
|
|
expect(vm.semaphores_dataset.select_map(:name)).to eq ["restart"]
|
|
end
|
|
|
|
it "supports restarting PostgresResource" do
|
|
project_id = Project.create(name: "Default").id
|
|
expect(Config).to receive(:postgres_service_project_id).and_return(project_id).at_least(:once)
|
|
pg = Prog::Postgres::PostgresResourceNexus.assemble(
|
|
project_id:,
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
name: "a",
|
|
target_vm_size: "standard-2",
|
|
target_storage_size_gib: 64
|
|
).subject
|
|
fill_in "UBID", with: pg.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - PostgresResource #{pg.ubid}"
|
|
|
|
expect(Semaphore.where(strand_id: pg.servers_dataset.select_map(:id)).select_map(:name)).to eq []
|
|
click_link "Restart"
|
|
click_button "Restart"
|
|
expect(page).to have_flash_notice("Restart scheduled for PostgresResource")
|
|
expect(page.title).to eq "Ubicloud Admin - PostgresResource #{pg.ubid}"
|
|
expect(Semaphore.where(strand_id: pg.servers_dataset.select_map(:id)).select_map(:name)).to eq ["restart"]
|
|
end
|
|
|
|
it "supports moving VmHost to draining/accepting state" do
|
|
vmh = Prog::Vm::HostNexus.assemble("127.0.0.2").subject
|
|
fill_in "UBID", with: vmh.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vmh.ubid}"
|
|
expect(vmh.allocation_state).to eq "unprepared"
|
|
|
|
click_link "Move to Draining"
|
|
click_button "Move to Draining"
|
|
expect(page).to have_flash_notice("Host allocation state changed to draining")
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vmh.ubid}"
|
|
expect(vmh.reload.allocation_state).to eq "draining"
|
|
|
|
click_link "Move to Accepting"
|
|
click_button "Move to Accepting"
|
|
expect(page).to have_flash_notice("Host allocation state changed to accepting")
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vmh.ubid}"
|
|
expect(vmh.reload.allocation_state).to eq "accepting"
|
|
end
|
|
|
|
it "supports rebooting VmHosts" do
|
|
vmh = Prog::Vm::HostNexus.assemble("127.0.0.2").subject
|
|
fill_in "UBID", with: vmh.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vmh.ubid}"
|
|
|
|
expect(vmh.semaphores_dataset.select_map(:name)).to eq []
|
|
click_link "Reboot"
|
|
click_button "Reboot"
|
|
expect(page).to have_flash_notice("Reboot scheduled for VmHost")
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vmh.ubid}"
|
|
expect(vmh.semaphores_dataset.select_map(:name)).to eq ["reboot"]
|
|
end
|
|
|
|
it "supports hardware reseting VmHosts" do
|
|
vmh = Prog::Vm::HostNexus.assemble("127.0.0.2").subject
|
|
fill_in "UBID", with: vmh.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vmh.ubid}"
|
|
|
|
expect(vmh.semaphores_dataset.select_map(:name)).to eq []
|
|
click_link "Hardware Reset"
|
|
click_button "Hardware Reset"
|
|
expect(page).to have_flash_notice("Hardware reset scheduled for VmHost")
|
|
expect(page.title).to eq "Ubicloud Admin - VmHost #{vmh.ubid}"
|
|
expect(vmh.semaphores_dataset.select_map(:name)).to eq ["hardware_reset"]
|
|
end
|
|
|
|
it "supports provisioning spare GitHubRunner" do
|
|
ins = GithubInstallation.create(installation_id: 123, name: "test-installation", type: "User")
|
|
ghr = GithubRunner.create(repository_name: "test-repo", label: "ubicloud", installation_id: ins.id)
|
|
|
|
fill_in "UBID", with: ghr.ubid
|
|
click_button "Show Object"
|
|
expect(page.title).to eq "Ubicloud Admin - GithubRunner #{ghr.ubid}"
|
|
|
|
expect(GithubRunner.count).to eq 1
|
|
click_link "Provision Spare Runner"
|
|
click_button "Provision Spare Runner"
|
|
expect(page).to have_flash_notice("Spare runner provisioned")
|
|
expect(page.title).to eq "Ubicloud Admin - GithubRunner #{ghr.ubid}"
|
|
expect(GithubRunner.count).to eq 2
|
|
expect(GithubRunner.select_map([:repository_name, :label, :installation_id])).to eq([["test-repo", "ubicloud", ins.id]] * 2)
|
|
end
|
|
end
|