mirror of
https://github.com/ubicloud/ubicloud.git
synced 2025-11-28 00:20:26 +08:00
No longer create PostgresFirewallRule instances when creating PostgresResources. Create FirewallRule instances instead. Remove PostgresResource firewall_rules association and set_firewall_rules method. Add a before_destroy so the postgres_firewall_rule entries will be removed before the resource is destroyed. This does not drop the table. That can be done in later, and requires multiple steps (delete all records, drop before_destroy, drop postgres_firewall_rule table). This keeps the update_firewall_rules semaphore for PostgresResource, but makes it a no-op. That can also be cleaned up later. Update regen-screenshots to handle networking changes. It's showing: Missing screenshots: managed-postgresql/metrics-1-screenshot.png managed-postgresql/metrics-2-screenshot.png But I think that is unrelated to this change.
539 lines
17 KiB
Ruby
Executable file
539 lines
17 KiB
Ruby
Executable file
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
DOCUMENTATION_DIR = "../documentation/"
|
|
|
|
unless File.directory?(DOCUMENTATION_DIR)
|
|
warn "Documentation site must be checked out in ../documentation"
|
|
exit(1)
|
|
end
|
|
|
|
require "find"
|
|
require "capybara"
|
|
require "capybara/dsl"
|
|
require "capybara/cuprite"
|
|
require "puma/cli"
|
|
require "nio"
|
|
require "securerandom"
|
|
|
|
ENV["RACK_ENV"] = "test"
|
|
|
|
# Enable shared connections for Sequel DB, to allow server and screenshot code to share the same transaction
|
|
ENV["SHARED_CONNECTION"] = "1"
|
|
|
|
# Set fake stripe secret key in order to take screenshots of billing pages
|
|
ENV["STRIPE_SECRET_KEY"] = "1"
|
|
|
|
# Set fake GitHub app name in order to take screenshots of GitHub Runner pages
|
|
ENV["GITHUB_APP_NAME"] = "1"
|
|
|
|
require_relative "../loader"
|
|
|
|
PORT = 8383
|
|
db_name = DB.get { current_database.function }
|
|
raise "Doesn't look like a test database (#{db_name}), not generating screenshots" unless db_name.end_with?("test")
|
|
|
|
Capybara.exact = true
|
|
Capybara.default_selector = :css
|
|
Capybara.default_driver = :cuprite
|
|
Capybara.server_port = PORT
|
|
Capybara.register_driver(:cuprite) do |app|
|
|
Capybara::Cuprite::Driver.new(app, window_size: [1200, 800], browser_options: {timeout: 15}, base_url: "http://localhost:#{PORT}")
|
|
end
|
|
|
|
require "tilt/erubi"
|
|
require "tilt/string"
|
|
|
|
queue = Queue.new
|
|
server = Puma::CLI.new(["-s", "-e", "test", "-b", "tcp://localhost:#{PORT}", "-t", "1:1", "config.ru"])
|
|
server.launcher.events.after_booted { queue.push(nil) }
|
|
Thread.new do
|
|
server.launcher.run
|
|
end
|
|
queue.pop
|
|
|
|
::Mail.defaults do
|
|
delivery_method :test
|
|
end
|
|
|
|
class RegenScreenshots
|
|
include Capybara::DSL
|
|
|
|
SCREENSHOTS = {}
|
|
Find.find(DOCUMENTATION_DIR) do |file|
|
|
if File.file?(file) && file.end_with?("screenshot.png")
|
|
SCREENSHOTS[file.delete_prefix(DOCUMENTATION_DIR)] = file
|
|
end
|
|
end
|
|
|
|
def screenshot(filename)
|
|
unless (path = SCREENSHOTS.delete(filename))
|
|
raise "No existing screenshot for #{filename} in documentation site"
|
|
end
|
|
|
|
# rubocop:disable Lint/Debugger
|
|
save_screenshot(path:)
|
|
# rubocop:enable Lint/Debugger
|
|
|
|
puts "Saved screenshot: #{filename}"
|
|
end
|
|
|
|
def resize(height, width: 1200)
|
|
Capybara.current_session.driver.browser.resize(width:, height:)
|
|
end
|
|
|
|
def call
|
|
visit "/"
|
|
click_link "Create a new account"
|
|
|
|
password = SecureRandom.base64(48)
|
|
fill_in "Full Name", with: "Demo"
|
|
fill_in "Email Address", with: "demo@example.com"
|
|
fill_in "Password", with: password
|
|
fill_in "Password Confirmation", with: password
|
|
click_button "Create Account"
|
|
|
|
mail = Mail::TestMailer.deliveries.shift
|
|
body = mail.parts[1].decoded
|
|
unless (match = %r{(/verify-account\?key=[^"]+)"}.match(body))
|
|
raise "no verify link in email"
|
|
end
|
|
|
|
visit match[1]
|
|
|
|
click_button "Verify Account"
|
|
|
|
project = Project.first
|
|
|
|
resize(900, width: 1600)
|
|
visit "/"
|
|
screenshot "quick-start/managed-services-1-screenshot.png"
|
|
|
|
find("#billing-icon").hover
|
|
screenshot "github-actions-integration/quickstart-1-screenshot.png"
|
|
|
|
Project.define_method(:has_valid_payment_method?) { true }
|
|
click_link "GitHub Runners"
|
|
screenshot "github-actions-integration/quickstart-2-screenshot.png"
|
|
|
|
GithubInstallation.create(installation_id: 123, name: "ubicloud-demo", type: "Organization", project_id: project.id, allocator_preferences: {"family_filter" => ["premium", "standard"]})
|
|
page.refresh
|
|
click_link "Settings", class: "text-gray-500"
|
|
screenshot "github-actions-integration/use-premium-runners-1-screenshot.png"
|
|
|
|
resize(650)
|
|
click_link "Tokens"
|
|
screenshot "quick-start/cli-1-screenshot.png"
|
|
|
|
click_button "Create Token"
|
|
ApiKey.dataset.update(id: "bf444ee6-2532-8153-975e-af787dbc796e")
|
|
page.refresh
|
|
screenshot "quick-start/cli-2-screenshot.png"
|
|
|
|
click_link "PostgreSQL"
|
|
screenshot "managed-postgresql/overview-1-screenshot.png"
|
|
|
|
resize(1400)
|
|
click_link "Create PostgreSQL Database"
|
|
screenshot "managed-postgresql/create-screenshot.png"
|
|
screenshot "quick-start/using-kamal-with-ubicloud-3-screenshot.png"
|
|
|
|
pg_private_subnet = PrivateSubnet.create_with_id(UBID.parse("psdkk2ssb6cy4j31yx1cjn2jqm").to_uuid,
|
|
project_id: project.id,
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
name: "subnet",
|
|
net4: "5.4.3.0/24",
|
|
net6: "5432::/16")
|
|
pg_resource = PostgresResource.create(
|
|
name: "postgresql-demo",
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
target_vm_size: "standard-2",
|
|
target_storage_size_gib: 10,
|
|
superuser_password: "1",
|
|
project_id: project.id,
|
|
private_subnet_id: pg_private_subnet.id,
|
|
target_version: "16"
|
|
) do |pg|
|
|
pg.id = UBID.parse("pgmjy3v4ef1y7gdpzv6b3fchef").to_uuid
|
|
end
|
|
pg_private_subnet.update(name: "#{pg_resource.ubid}-subnet")
|
|
Project.define_method(:postgres_resources_dataset) do |*a|
|
|
super(*a).with_extend do
|
|
define_method(:all) { [pg_resource] }
|
|
define_method(:first) { |*| pg_resource }
|
|
end
|
|
end
|
|
|
|
pg_vm = Vm.create(
|
|
name: "postgresql-demo-vm",
|
|
memory_gib: 8,
|
|
vcpus: 2,
|
|
cores: 2,
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
project_id: project.id,
|
|
unix_user: "postgres",
|
|
public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7b8ZxEjGHV54sF4z/7H5z9hGJtYI5RvV2kz8KjQhYJvQG8W1vZK8dG9vLWJyZWFrZXItdGVzdEBleGFtcGxlLmNvbYIxP634e6Y0p2FQI+CvLVgNxYqaQZxjx84+SN/XY4FOR4",
|
|
boot_image: "ubuntu-jammy",
|
|
family: "standard"
|
|
)
|
|
pg_timeline = PostgresTimeline.create(
|
|
location_id: Location::HETZNER_FSN1_ID
|
|
) do |timeline|
|
|
timeline.id = UBID.parse("ptmjy3v4ef1y7gdpzv6b3fchef").to_uuid
|
|
end
|
|
pg_server = PostgresServer.create(
|
|
resource_id: pg_resource.id,
|
|
vm_id: pg_vm.id,
|
|
timeline_id: pg_timeline.id,
|
|
version: "16"
|
|
) do |server|
|
|
server.id = UBID.parse("pvmjy3v4ef1y7gdpzv6b3fchef").to_uuid
|
|
end
|
|
PostgresResource.define_method(:display_state) { "running" }
|
|
pg_firewall = Firewall.create_with_id(UBID.parse("fw63akem7s0j5kz48083j3k5fq").to_uuid,
|
|
project_id: project.id,
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
name: "#{pg_resource.ubid}-firewall")
|
|
FirewallRule.create_with_id(UBID.parse("fre795gq2vmt7gatmc53mt6rgj").to_uuid,
|
|
firewall_id: pg_firewall.id,
|
|
cidr: "0.0.0.0/0",
|
|
port_range: 5432..5432)
|
|
|
|
PostgresResource.define_method(:connection_string) { "sample-connection-string" }
|
|
PostgresResource.define_method(:ca_certificates) { "certs" }
|
|
PostgresResource.define_method(:representative_server) { pg_server }
|
|
PostgresResource.define_method(:user_config) { {"max_connections" => 1000, "work_mem" => "64MB"} }
|
|
PostgresResource.define_method(:pgbouncer_user_config) { {"default_pool_size" => 50} }
|
|
PostgresResource.define_method(:upgrade_stage) { "upgrade_standby" }
|
|
PostgresResource.define_method(:version) { "16" }
|
|
Authorization.define_singleton_method(:authorize) { |*| }
|
|
PostgresServer.define_method(:storage_size_gib) { 128 }
|
|
|
|
# Create VictoriaMetrics resources for metrics display
|
|
vmr = VictoriaMetricsResource.create(
|
|
name: "victoria-metrics-demo",
|
|
admin_user: "admin",
|
|
admin_password: "password",
|
|
target_vm_size: "standard-2",
|
|
target_storage_size_gib: 100,
|
|
project_id: project.id,
|
|
location_id: Location::HETZNER_FSN1_ID
|
|
)
|
|
|
|
vm_metrics = Vm.create(
|
|
name: "victoria-metrics-vm",
|
|
memory_gib: 8,
|
|
vcpus: 2,
|
|
cores: 2,
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
project_id: project.id,
|
|
unix_user: "ubi",
|
|
public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7b8ZxEjGHV54sF4z/7H5z9hGJtYI5RvV2kz8KjQhYJvQG8W1vZK8dG9vLWJyZWFrZXItdGVzdEBleGFtcGxlLmNvbYIxP634e6Y0p2FQI+CvLVgNxYqaQZxjx84+SN/XY4FOR4",
|
|
boot_image: "ubuntu-jammy",
|
|
family: "standard"
|
|
)
|
|
|
|
VictoriaMetricsServer.create(
|
|
victoria_metrics_resource_id: vmr.id,
|
|
vm_id: vm_metrics.id,
|
|
cert: "cert-data",
|
|
cert_key: "cert-key-data"
|
|
)
|
|
|
|
# Mock the VictoriaMetrics client to return sample data
|
|
mock_client = Object.new
|
|
def mock_client.query_range(query:, start_ts:, end_ts:)
|
|
# Use a separate RNG for VictoriaMetrics metrics to ensure consistent results
|
|
metrics_rng = Random.new(1234)
|
|
|
|
step = 60 # 1 minute intervals
|
|
timestamps = (start_ts..end_ts).step(step).to_a
|
|
|
|
case query
|
|
when /node_filesystem_avail_bytes/
|
|
# Return disk usage around 45%
|
|
values = timestamps.map { |ts| [ts, metrics_rng.rand(40..49).to_s] }
|
|
[{
|
|
"labels" => {},
|
|
"values" => values
|
|
}]
|
|
when /node_cpu_seconds_total/
|
|
[{
|
|
"labels" => {"mode" => "user"},
|
|
"values" => timestamps.map { |ts| [ts, metrics_rng.rand(15..64).to_s] }
|
|
},
|
|
{
|
|
"labels" => {"mode" => "system"},
|
|
"values" => timestamps.map { |ts| [ts, metrics_rng.rand(8..12).to_s] }
|
|
},
|
|
{
|
|
"labels" => {"mode" => "iowait"},
|
|
"values" => timestamps.map { |ts| [ts, metrics_rng.rand(2..4).to_s] }
|
|
}]
|
|
else
|
|
# Default sample data
|
|
values = timestamps.map { |ts| [ts, metrics_rng.rand(20..39).to_s] }
|
|
[{
|
|
"labels" => {},
|
|
"values" => values
|
|
}]
|
|
end
|
|
end
|
|
|
|
VictoriaMetricsServer.define_method(:client) { |*| mock_client }
|
|
|
|
# Mock the metrics_config method on PostgresServer to return the correct project_id
|
|
PostgresServer.define_method(:metrics_config) { {project_id: project.id} }
|
|
Config.define_singleton_method(:postgres_service_project_id) { project.id }
|
|
|
|
resize(900, width: 1600)
|
|
click_link "PostgreSQL"
|
|
click_link "postgresql-demo"
|
|
sleep 1 # Wait for metrics charts to load.
|
|
screenshot "managed-postgresql/overview-2-screenshot.png"
|
|
|
|
resize(800, width: 1600)
|
|
click_link "Connection"
|
|
screenshot "managed-postgresql/connection-screenshot.png"
|
|
|
|
resize(800, width: 1600)
|
|
within("aside nav") do
|
|
click_link "Networking"
|
|
end
|
|
screenshot "managed-postgresql/networking-screenshot.png"
|
|
|
|
resize(1200, width: 1600)
|
|
click_link "Resize"
|
|
screenshot "managed-postgresql/resizing-screenshot.png"
|
|
|
|
resize(900, width: 1200)
|
|
click_link "Upgrade"
|
|
screenshot "managed-postgresql/upgrade-screenshot.png"
|
|
|
|
pg_resource.update(target_version: "17")
|
|
resize(900, width: 1200)
|
|
click_link "Upgrade"
|
|
screenshot "managed-postgresql/upgrade-progress-screenshot.png"
|
|
|
|
resize(850, width: 1600)
|
|
click_link "Configuration"
|
|
screenshot "managed-postgresql/configuration-screenshot.png"
|
|
|
|
resize(1000, width: 1600)
|
|
within("aside nav") do
|
|
click_link "Settings"
|
|
end
|
|
screenshot "managed-postgresql/settings-screenshot.png"
|
|
|
|
click_link "Kubernetes"
|
|
click_link "Create Kubernetes Cluster"
|
|
|
|
fill_in "Cluster Name", with: "kubernetes-demo"
|
|
find('input[name="cp_nodes"][value="3"]:not([disabled])').trigger("click")
|
|
find('select#worker_nodes option[value="5"]:not([disabled])').select_option
|
|
resize(900)
|
|
screenshot "managed-kubernetes/create-screenshot.png"
|
|
|
|
kc_ubid = "kcj4veqfy46a4pcxpmj3dzwzf5"
|
|
kn_ubid = "kna6c1cyxqba5pbmfthkb0xawa"
|
|
|
|
k8s_cluster = KubernetesCluster.create(
|
|
name: "kubernetes-demo",
|
|
version: "v1.34",
|
|
cp_node_count: 3,
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
target_node_size: "standard-2",
|
|
private_subnet_id: Prog::Vnet::SubnetNexus.assemble(
|
|
project.id,
|
|
name: "#{kc_ubid}-subnet",
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
ipv4_range: Prog::Vnet::SubnetNexus.random_private_ipv4(Location[Location::HETZNER_FSN1_ID], project, 18).to_s
|
|
).subject.id,
|
|
project_id: project.id
|
|
) do |kc|
|
|
kc.id = UBID.parse(kc_ubid).to_uuid
|
|
end
|
|
|
|
k8s_nodepool = KubernetesNodepool.create(
|
|
name: "kubernetes-demo-np",
|
|
node_count: 5,
|
|
kubernetes_cluster_id: k8s_cluster.id,
|
|
target_node_size: "standard-8"
|
|
) do |kn|
|
|
kn.id = UBID.parse(kn_ubid).to_uuid
|
|
end
|
|
|
|
services_lb = Prog::Vnet::LoadBalancerNexus.assemble(
|
|
k8s_cluster.private_subnet_id,
|
|
name: k8s_cluster.services_load_balancer_name,
|
|
algorithm: "hash_based",
|
|
src_port: 443,
|
|
dst_port: 6443,
|
|
health_check_endpoint: "/",
|
|
health_check_protocol: "tcp",
|
|
stack: LoadBalancer::Stack::IPV4
|
|
).subject
|
|
|
|
# Associate the load balancer with the Kubernetes cluster
|
|
k8s_cluster.update(services_lb_id: services_lb.id)
|
|
|
|
Config.define_singleton_method(:kubernetes_service_project_id) { project.id }
|
|
Firewall.create_with_id(UBID.parse("fwfzd1vb5r1t6p1xye9w346v9s").to_uuid,
|
|
project_id: project.id,
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
name: "#{k8s_cluster.ubid}-cp-vm-firewall")
|
|
Firewall.create_with_id(UBID.parse("fwp092jypnmt2h29wgxst8qwp0").to_uuid,
|
|
project_id: project.id,
|
|
location_id: Location::HETZNER_FSN1_ID,
|
|
name: "#{k8s_cluster.ubid}-worker-vm-firewall")
|
|
|
|
["le9ec", "e12en", "zd3ka"].each do |suffix|
|
|
Prog::Kubernetes::KubernetesNodeNexus.assemble(
|
|
project.id,
|
|
sshable_unix_user: "ubi",
|
|
name: "#{kc_ubid}-#{suffix}",
|
|
location_id: k8s_cluster.location.id,
|
|
size: k8s_cluster.target_node_size,
|
|
storage_volumes: nil,
|
|
boot_image: "kubernetes-v1_34",
|
|
private_subnet_id: k8s_cluster.private_subnet_id,
|
|
enable_ip4: true,
|
|
kubernetes_cluster_id: k8s_cluster.id
|
|
)
|
|
end
|
|
|
|
["ubicl", "krn71", "fysgd", "mant4", "m0h11"].each do |suffix|
|
|
Prog::Kubernetes::KubernetesNodeNexus.assemble(
|
|
project.id,
|
|
sshable_unix_user: "ubi",
|
|
name: "#{kn_ubid}-#{suffix}",
|
|
location_id: k8s_cluster.location.id,
|
|
size: k8s_nodepool.target_node_size,
|
|
storage_volumes: nil,
|
|
boot_image: "kubernetes-v1_34",
|
|
private_subnet_id: k8s_cluster.private_subnet_id,
|
|
enable_ip4: true,
|
|
kubernetes_cluster_id: k8s_cluster.id,
|
|
kubernetes_nodepool_id: k8s_nodepool.id
|
|
)
|
|
end
|
|
|
|
Project.define_method(:kubernetes_clusters_dataset) do |*a|
|
|
super(*a).with_extend do
|
|
define_method(:all) { [k8s_cluster] }
|
|
define_method(:first) { |*| k8s_cluster }
|
|
end
|
|
end
|
|
KubernetesCluster.define_method(:display_state) { "running" }
|
|
Vm.define_method(:display_state) { "running" }
|
|
LoadBalancer.define_method(:hostname) { "f9f2x4td37-services.k8s.ubicloud.com" }
|
|
|
|
click_link "Kubernetes"
|
|
click_link "kubernetes-demo"
|
|
resize(800, width: 1600)
|
|
screenshot "managed-kubernetes/overview-screenshot.png"
|
|
|
|
resize(1000, width: 1600)
|
|
within("aside nav") do
|
|
click_link "Settings"
|
|
end
|
|
screenshot "managed-kubernetes/resize-screenshot.png"
|
|
|
|
within("aside nav") do
|
|
click_link "Nodes"
|
|
end
|
|
screenshot "managed-kubernetes/nodes-screenshot.png"
|
|
|
|
resize(800)
|
|
click_link "Users"
|
|
fill_in "email", with: "other@example.com"
|
|
find("input[name=email]").click
|
|
screenshot "security/users-1-screenshot.png"
|
|
|
|
account2 = Account.create(email: "other@example.com", name: "Other")
|
|
click_button "Invite"
|
|
page.refresh
|
|
page.evaluate_script("$('#user_policy_#{account2.ubid}').focus()")
|
|
screenshot "security/users-2-screenshot.png"
|
|
|
|
click_link "Access Control"
|
|
access_control_path = page.current_path
|
|
screenshot "security/access-control-1-screenshot.png"
|
|
|
|
click_link "subject-tags-link"
|
|
screenshot "security/subject-tag-1-screenshot.png"
|
|
|
|
fill_in "name", with: "System-Admins"
|
|
click_button "Create"
|
|
fill_in "name", with: "Network-Admins"
|
|
click_button "Create"
|
|
page.refresh
|
|
fill_in "name", with: "Database-Admins"
|
|
page.evaluate_script("$('#name').focus()")
|
|
screenshot "security/subject-tag-2-screenshot.png"
|
|
click_button "Create"
|
|
|
|
resize(1020)
|
|
click_link "#{SubjectTag[name: "System-Admins"].ubid}-edit"
|
|
screenshot "security/subject-tag-3-screenshot.png"
|
|
|
|
resize(1140)
|
|
check "add[]-#{account2.ubid}-0"
|
|
click_button "Add Members"
|
|
page.refresh
|
|
screenshot "security/subject-tag-4-screenshot.png"
|
|
|
|
resize(800)
|
|
visit access_control_path
|
|
click_link "action-tags-link"
|
|
fill_in "name", with: "Networking"
|
|
click_button "Create"
|
|
page.refresh
|
|
screenshot "security/action-tag-1-screenshot.png"
|
|
|
|
resize(1140)
|
|
click_link "Manage"
|
|
check "add[]-tazzzzzzzz021gzzzz0fw0a110-0"
|
|
check "add[]-tazzzzzzzz021gzzzz01b0a111-0"
|
|
check "add[]-tazzzzzzzz021gzzzz0ps0a111-0"
|
|
screenshot "security/action-tag-2-screenshot.png"
|
|
|
|
resize(800)
|
|
click_button "Add Members"
|
|
page.refresh
|
|
screenshot "security/action-tag-3-screenshot.png"
|
|
|
|
resize(850)
|
|
visit access_control_path
|
|
3.times do
|
|
click_button "New Access Control Entry"
|
|
end
|
|
screenshot "security/access-control-2-screenshot.png"
|
|
|
|
select "System-Admins", from: "ace-select-1-0"
|
|
select "Vm:all", from: "ace-select-1-1"
|
|
|
|
select "Network-Admins", from: "ace-select-2-0"
|
|
select "Networking", from: "ace-select-2-1"
|
|
|
|
select "Database-Admins", from: "ace-select-3-0"
|
|
select "Postgres:all", from: "ace-select-3-1"
|
|
screenshot "security/access-control-3-screenshot.png"
|
|
|
|
resize(1800)
|
|
click_link "Compute"
|
|
click_link "Create Virtual Machine"
|
|
screenshot "quick-start/managed-services-2-screenshot.png"
|
|
end
|
|
end
|
|
|
|
DB.transaction(rollback: :always, auto_savepoint: true) do |conn|
|
|
DB.temporarily_release_connection(conn) do
|
|
RegenScreenshots.new.call
|
|
end
|
|
end
|
|
|
|
unless RegenScreenshots::SCREENSHOTS.empty?
|
|
warn "Missing screenshots:", RegenScreenshots::SCREENSHOTS.keys.sort
|
|
exit(1)
|
|
end
|