Files
ubicloud/spec/routes/web/kubernetes_cluster_spec.rb
Eren Başak 76d7bff0d4 Create Services LB as part of Kubernetes Cluster creation
This change moves the services LB creation from kubernetes nodepool
to kubernetes cluster nexus, alongside the apiserver lb creation.
We still don't know how to support multiple nodepools and their
relationship to service LBs, but it's simpler to assume that we will
support a single service LB for the whole cluster.

This change also starts using the FK for the service LB reference,
instead of the name based query.
2025-06-27 15:12:38 +03:00

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_with_id(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