It hasn't been necessary to use create_with_id since
ebc79622df
, in December 2024.
I have plans to introduce:
```ruby
def create_with_id(id, values)
obj = new(values)
obj.id = id
obj.save_changes
end
```
This will make it easier to use the same id when creating
multiple objects. The first step is removing the existing
uses of create_with_id.
384 lines
14 KiB
Ruby
384 lines
14 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "spec_helper"
|
|
|
|
RSpec.describe Clover, "Kubernetes" do
|
|
let(:user) { create_account }
|
|
|
|
let(:project) { user.create_project_with_default_policy("project-1") }
|
|
|
|
let(:project_wo_permissions) { user.create_project_with_default_policy("project-2", default_policy: nil) }
|
|
|
|
let(:kc) do
|
|
cluster = Prog::Kubernetes::KubernetesClusterNexus.assemble(
|
|
name: "myk8s",
|
|
version: "v1.32",
|
|
project_id: project.id,
|
|
private_subnet_id: PrivateSubnet.create(net6: "0::0", net4: "127.0.0.1", name: "mysubnet", location_id: Location::HETZNER_FSN1_ID, project_id: Config.kubernetes_service_project_id).id,
|
|
location_id: Location::HETZNER_FSN1_ID
|
|
).subject
|
|
|
|
services_lb = Prog::Vnet::LoadBalancerNexus.assemble(
|
|
cluster.private_subnet_id,
|
|
name: cluster.services_load_balancer_name,
|
|
algorithm: "hash_based",
|
|
# TODO: change the api to support LBs without ports
|
|
# The next two fields will be later modified by the sync_kubernetes_services label
|
|
# These are just set for passing the creation validations
|
|
src_port: 443,
|
|
dst_port: 6443,
|
|
health_check_endpoint: "/",
|
|
health_check_protocol: "tcp",
|
|
stack: LoadBalancer::Stack::IPV4
|
|
).subject
|
|
|
|
cluster.update(services_lb_id: services_lb.id)
|
|
cluster
|
|
end
|
|
|
|
let(:kc_no_perm) do
|
|
Prog::Kubernetes::KubernetesClusterNexus.assemble(
|
|
name: "not-my-k8s",
|
|
version: "v1.32",
|
|
project_id: project_wo_permissions.id,
|
|
private_subnet_id: PrivateSubnet.create(net6: "0::0", net4: "127.0.0.1", name: "othersubnet", location_id: Location::HETZNER_FSN1_ID, project_id: Config.kubernetes_service_project_id).id,
|
|
location_id: Location::HETZNER_FSN1_ID
|
|
).subject
|
|
end
|
|
|
|
before do
|
|
allow(Config).to receive(:kubernetes_service_project_id).and_return(Project.create(name: "UbicloudKubernetesService").id)
|
|
end
|
|
|
|
describe "unauthenticated" do
|
|
it "can not list without login" do
|
|
visit "#{project.path}/kubernetes-cluster"
|
|
expect(page.title).to eq("Ubicloud - Login")
|
|
end
|
|
|
|
it "can not create without login" do
|
|
visit "#{project.path}/kubernetes-cluster/create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Login")
|
|
end
|
|
end
|
|
|
|
describe "authenticated" do
|
|
before do
|
|
login(user.email)
|
|
end
|
|
|
|
describe "list" do
|
|
it "works with 0 kubernetes clusters" do
|
|
visit "#{project.path}/kubernetes-cluster"
|
|
expect(page.title).to eq("Ubicloud - Kubernetes Clusters")
|
|
expect(page).to have_content "No Kubernetes Clusters"
|
|
expect(page).to have_content "Create Kubernetes Cluster"
|
|
|
|
click_link "Create Kubernetes Cluster"
|
|
expect(page.title).to eq("Ubicloud - Create Kubernetes Cluster")
|
|
end
|
|
|
|
it "lists existing permissible clusters" do
|
|
kc
|
|
kc_no_perm
|
|
|
|
visit "#{project.path}/kubernetes-cluster"
|
|
|
|
expect(page).to have_content "myk8s"
|
|
expect(page).to have_no_content "not-my-k8s"
|
|
expect(page).to have_content "Create Kubernetes Cluster"
|
|
end
|
|
|
|
it "doesn't show the create button without permission" do
|
|
project
|
|
AccessControlEntry.dataset.destroy
|
|
AccessControlEntry.create(project_id: project.id, subject_id: user.id, action_id: ActionType::NAME_MAP["KubernetesCluster:view"])
|
|
|
|
expect(KubernetesCluster.count).to eq(0)
|
|
|
|
visit "#{project.path}/kubernetes-cluster"
|
|
|
|
expect(page).to have_content("No Kubernetes Clusters")
|
|
expect(page).to have_no_content "Create Kubernetes Cluster"
|
|
|
|
kc
|
|
AccessControlEntry.dataset.destroy
|
|
AccessControlEntry.create(project_id: project.id, subject_id: user.id, action_id: ActionType::NAME_MAP["KubernetesCluster:view"])
|
|
|
|
expect(KubernetesCluster.count).to eq(1)
|
|
visit "#{project.path}/kubernetes-cluster"
|
|
|
|
expect(page).to have_content(kc.name)
|
|
expect(page).to have_no_content "No Kubernetes Clusters"
|
|
expect(page).to have_no_content "Create Kubernetes Cluster"
|
|
end
|
|
end
|
|
|
|
describe "create" do
|
|
before do
|
|
kc
|
|
visit "#{project.path}/kubernetes-cluster/create"
|
|
expect(page.title).to eq("Ubicloud - Create Kubernetes Cluster")
|
|
end
|
|
|
|
it "cannot create kubernetes cluster when location does not exist" do
|
|
fill_in "Cluster Name", with: "cannotcreate"
|
|
choose option: 3
|
|
find('select#worker_nodes option[value="4"]:not([disabled])').select_option
|
|
choose option: Location::LEASEWEB_WDC02_UBID
|
|
Location[Location::LEASEWEB_WDC02_ID].destroy
|
|
|
|
click_button "Create"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content("ResourceNotFound")
|
|
end
|
|
|
|
it "can not create cluster if project has no valid payment method" do
|
|
expect(described_class).to receive(:authorized_project).with(user, project.id).and_return(project).at_least(:once)
|
|
expect(Config).to receive(:stripe_secret_key).and_return("secret_key").at_least(:once)
|
|
|
|
page.refresh
|
|
|
|
expect(page).to have_content "Project doesn't have valid billing information"
|
|
|
|
fill_in "Cluster Name", with: "dummyk8s"
|
|
choose option: Location::HETZNER_FSN1_UBID
|
|
choose option: 3
|
|
find('select#worker_nodes option[value="4"]:not([disabled])').select_option
|
|
|
|
click_button "Create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create Kubernetes Cluster")
|
|
expect(page).to have_content "Project doesn't have valid billing information"
|
|
end
|
|
|
|
it "can create new kubernetes cluster" do
|
|
fill_in "Cluster Name", with: "k8stest"
|
|
choose option: Location::HETZNER_FSN1_UBID
|
|
choose option: 3
|
|
find('select#worker_nodes option[value="4"]:not([disabled])').select_option
|
|
|
|
[1, 2, 4, 8].each do
|
|
expect(page).to have_content "#{it * 2} vCPUs / #{it * 8} GB RAM / #{it * 40} GB NVMe Storage"
|
|
end
|
|
|
|
click_button "Create"
|
|
expect(page.title).to eq("Ubicloud - k8stest")
|
|
expect(page).to have_flash_notice("'k8stest' will be ready in a few minutes")
|
|
expect(KubernetesCluster.count).to eq(2)
|
|
|
|
new_kc = KubernetesCluster[name: "k8stest"]
|
|
|
|
expect(new_kc.project_id).to eq(project.id)
|
|
expect(new_kc.cp_node_count).to eq(3)
|
|
expect(new_kc.nodepools.first.node_count).to eq(4)
|
|
expect(new_kc.private_subnet.name).to eq("#{new_kc.ubid}-subnet")
|
|
end
|
|
|
|
it "can not create kubernetes cluster with invalid name" do
|
|
fill_in "Cluster Name", with: "invalid name"
|
|
choose option: Location::HETZNER_FSN1_UBID
|
|
choose option: 3
|
|
find('select#worker_nodes option[value="4"]:not([disabled])').select_option
|
|
|
|
click_button "Create"
|
|
expect(page.title).to eq("Ubicloud - Create Kubernetes Cluster")
|
|
expect(page).to have_content "Kubernetes cluster name must only contain lowercase"
|
|
expect((find "input[name=name]")["value"]).to eq("invalid name")
|
|
end
|
|
|
|
it "can not create kubernetes cluster with same name in same project & location" do
|
|
fill_in "Cluster Name", with: "myk8s"
|
|
choose option: Location::HETZNER_FSN1_UBID
|
|
choose option: 3
|
|
find('select#worker_nodes option[value="4"]:not([disabled])').select_option
|
|
|
|
click_button "Create"
|
|
expect(page.title).to eq("Ubicloud - Create Kubernetes Cluster")
|
|
expect(page).to have_flash_error("project_id and location_id and name is already taken")
|
|
end
|
|
|
|
it "can not select invisible location" do
|
|
expect { choose option: Location::GITHUB_RUNNERS_UBID }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
|
|
it "can not create kubernetes cluster in a project when does not have permissions" do
|
|
visit "#{project_wo_permissions.path}/kubernetes-cluster/create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Forbidden")
|
|
expect(page.status_code).to eq(403)
|
|
expect(page).to have_content "Forbidden"
|
|
end
|
|
end
|
|
|
|
describe "show" do
|
|
it "can show kubernetes cluster details" do
|
|
kc
|
|
visit "#{project.path}/kubernetes-cluster"
|
|
expect(page.title).to eq("Ubicloud - Kubernetes Clusters")
|
|
|
|
expect(page).to have_content kc.name
|
|
|
|
click_link kc.name, href: "#{project.path}#{kc.path}"
|
|
|
|
expect(page.title).to eq("Ubicloud - #{kc.name}")
|
|
expect(page).to have_content kc.name
|
|
expect(page).to have_content kc.ubid
|
|
expect(page).to have_content kc.display_location
|
|
expect(page).to have_content kc.version
|
|
|
|
kc.add_cp_vm(create_vm(name: "cp1"))
|
|
kc.add_cp_vm(create_vm(name: "cp2"))
|
|
|
|
kn = Prog::Kubernetes::KubernetesNodepoolNexus.assemble(
|
|
name: "kn",
|
|
node_count: 2,
|
|
kubernetes_cluster_id: kc.id
|
|
).subject
|
|
|
|
kn.add_vm(create_vm(name: "node1"))
|
|
|
|
kc.reload
|
|
page.refresh
|
|
|
|
expect(page).to have_content "cp1"
|
|
expect(page).to have_content "cp2"
|
|
expect(page).to have_content "node1"
|
|
|
|
expect(kc.display_state).to eq("creating")
|
|
expect(page.body).to include "auto-refresh hidden"
|
|
expect(page.body).to include "creating"
|
|
expect(page).to have_content "Waiting for cluster to be ready..."
|
|
expect(page).to have_no_content "Download"
|
|
|
|
kc.strand.update(label: "wait")
|
|
kn.strand.update(label: "wait")
|
|
page.refresh
|
|
expect(page.body).not_to include "auto-refresh hidden"
|
|
expect(page.body).to include "running"
|
|
expect(page).to have_no_content "Waiting for cluster to be ready..."
|
|
expect(page).to have_content "Download"
|
|
|
|
kc.incr_destroy
|
|
kc.reload
|
|
|
|
expect(kc.display_state).to eq("deleting")
|
|
page.refresh
|
|
expect(page.body).to include "deleting"
|
|
expect(page.body).to include "auto-refresh hidden"
|
|
end
|
|
|
|
it "works with ubid" do
|
|
visit "#{project.path}/location/#{kc.display_location}/kubernetes-cluster/#{kc.ubid}"
|
|
|
|
expect(page.title).to eq("Ubicloud - #{kc.name}")
|
|
expect(page).to have_content kc.name
|
|
end
|
|
|
|
it "does not show delete option without permissions" do
|
|
kc
|
|
AccessControlEntry.dataset.destroy
|
|
AccessControlEntry.create(project_id: project.id, subject_id: user.id, action_id: ActionType::NAME_MAP["KubernetesCluster:view"])
|
|
visit "#{project.path}#{kc.path}"
|
|
|
|
expect(page.title).to eq("Ubicloud - #{kc.name}")
|
|
expect(page).to have_content kc.name
|
|
expect(page).to have_content kc.ubid
|
|
expect(page).to have_content kc.display_location
|
|
|
|
expect(page).to have_no_content "Danger Zone"
|
|
end
|
|
|
|
it "raises forbidden when does not have permissions" do
|
|
visit "#{project_wo_permissions.path}#{kc_no_perm.path}"
|
|
expect(page.title).to eq("Ubicloud - Forbidden")
|
|
expect(page.status_code).to eq(403)
|
|
expect(page).to have_content "Forbidden"
|
|
end
|
|
|
|
it "raises not found when kubernetes cluster does not exist" do
|
|
visit "#{project.path}/location/eu-central-h1/kubernetes-cluster/blabla"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content "ResourceNotFound"
|
|
end
|
|
end
|
|
|
|
describe "kubeconfig" do
|
|
before do
|
|
kc
|
|
end
|
|
|
|
it "returns kubeconfig content for authorized users" do
|
|
expect(KubernetesCluster).to receive(:kubeconfig).and_return "kubeconfig content"
|
|
|
|
visit "#{project.path}#{kc.path}/kubeconfig"
|
|
|
|
expect(page.response_headers["Content-Type"]).to eq("text/plain")
|
|
expect(page.response_headers["Content-Disposition"]).to include("attachment; filename=\"#{kc.name}-kubeconfig.yaml\"")
|
|
expect(page.body).to eq("kubeconfig content")
|
|
end
|
|
|
|
it "raises forbidden error when user does not have permission" do
|
|
AccessControlEntry.dataset.destroy
|
|
AccessControlEntry.create(project_id: project.id, subject_id: user.id, action_id: ActionType::NAME_MAP["KubernetesCluster:view"])
|
|
|
|
visit "#{project.path}#{kc.path}/kubeconfig"
|
|
expect(page.status_code).to eq(403)
|
|
expect(page).to have_content("Forbidden")
|
|
end
|
|
|
|
it "raises not found when Kubernetes cluster does not exist" do
|
|
visit "#{project.path}/kubernetes-cluster/_nonexistent/kubeconfig"
|
|
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content("ResourceNotFound")
|
|
end
|
|
|
|
it "returns proper content headers and content" do
|
|
expect(KubernetesCluster).to receive(:kubeconfig).and_return "mocked kubeconfig content"
|
|
|
|
visit "#{project.path}#{kc.path}/kubeconfig"
|
|
expect(page.response_headers["Content-Type"]).to eq("text/plain")
|
|
expect(page.response_headers["Content-Disposition"]).to include("attachment; filename=\"#{kc.name}-kubeconfig.yaml\"")
|
|
expect(page.body).to eq("mocked kubeconfig content")
|
|
end
|
|
|
|
it "does not allow unauthorized access" do
|
|
AccessControlEntry.dataset.destroy
|
|
visit "#{project.path}#{kc.path}/kubeconfig"
|
|
|
|
expect(page.status_code).to eq(403)
|
|
expect(page).to have_content("Forbidden")
|
|
end
|
|
end
|
|
|
|
describe "delete" do
|
|
it "can delete kubernetes cluster" do
|
|
visit "#{project.path}#{kc.path}"
|
|
|
|
# We send delete request manually instead of just clicking to button because delete action triggered by JavaScript.
|
|
# UI tests run without a JavaScript enginer.
|
|
btn = find "#kc-delete-#{kc.ubid} .delete-btn"
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
expect(SemSnap.new(kc.id).set?("destroy")).to be true
|
|
end
|
|
|
|
it "can not delete kubernetes cluster when does not have permissions" do
|
|
# Give permission to view, so we can see the detail page
|
|
AccessControlEntry.create(project_id: project_wo_permissions.id, subject_id: user.id, action_id: ActionType::NAME_MAP["KubernetesCluster:view"])
|
|
|
|
visit "#{project_wo_permissions.path}#{kc_no_perm.path}"
|
|
expect(page.title).to eq "Ubicloud - not-my-k8s"
|
|
|
|
expect { find ".delete-btn" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
end
|
|
end
|
|
end
|