ubicloud/bin/regen-screenshots
Jeremy Evans f130629fa4 Remove PostgresFirewallRule model
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.
2025-10-30 02:30:25 +09:00

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