Corregge il provisioning Hetzner: Faraday, cloud-init e default cpx12.

Sistemati path API /v1, AppArmor/auth MediaMTX sul nodo e defaults nbg1 così lo smoke Cloud è ripetibile.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-10 12:20:36 +02:00
co-authored by Cursor
parent e5fc925bae
commit 559284f0b2
10 changed files with 113 additions and 66 deletions
@@ -17,6 +17,8 @@ RSpec.describe Streams::Autoscaler do
redis.del(Streams::Autoscaler::LOCK_KEY)
redis.del(Streams::Autoscaler::KILL_SWITCH_KEY)
redis.del(Streams::DnsProviders::Lab::REDIS_KEY)
StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current)
StreamNode.where.not(slug: Streams::NodeRegistry::HOME_SLUG).delete_all
end
it "is a no-op when disabled" do
@@ -153,7 +155,10 @@ RSpec.describe Streams::Autoscaler do
"MEDIAMTX_RTMP_URL" => "rtmp://home.example:1935",
"HLS_PUBLIC_URL" => "https://home.example/hls"
) do
StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current)
StreamNode.where.not(slug: Streams::NodeRegistry::HOME_SLUG).delete_all
home = Streams::NodeRegistry.ensure_home_from_env!
home.update!(max_publishers: 1)
user = User.create!(email: "budget@example.com", name: "B", password: "Password123", role: "coach")
club = Club.create!(name: "Budget", sport: "volleyball")
team = club.teams.create!(name: "T", sport: "volleyball", slug: "budget-t")
@@ -165,7 +170,9 @@ RSpec.describe Streams::Autoscaler do
result = described_class.reconcile!(provisioner: provisioner)
expect(result.actions).to include(:blocked_capacity)
expect(described_class.metrics[:within_budget]).to eq(true)
# 0 overflow nodes → stima 0€ ≤ budget; il blocco è sul nodo successivo (+1)
expect(described_class.within_budget?(0)).to eq(true)
expect(described_class.within_budget?(1)).to eq(false)
end
end
end
@@ -5,7 +5,7 @@ require "rails_helper"
RSpec.describe Streams::CloudProviders::Hetzner do
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:conn) do
Faraday.new(url: "https://api.hetzner.cloud/v1") do |f|
Faraday.new(url: Streams::CloudProviders::Hetzner::API) do |f|
f.request :json
f.response :json
f.adapter :test, stubs
@@ -25,15 +25,15 @@ RSpec.describe Streams::CloudProviders::Hetzner do
it "creates a server and waits until running" do
with_env(
"HCLOUD_LOCATION" => "fsn1",
"HCLOUD_SERVER_TYPE" => "cpx21",
"HCLOUD_LOCATION" => "nbg1",
"HCLOUD_SERVER_TYPE" => "cpx12",
"HCLOUD_IMAGE" => "debian-12",
"HCLOUD_SSH_KEY" => "matchlivetv-stream"
"HCLOUD_SSH_KEY" => "matchlivetv-stream-hetzner"
) do
stubs.post("/servers") do |env|
stubs.post("https://api.hetzner.cloud/v1/servers") do |env|
body = JSON.parse(env.body)
expect(body["name"]).to eq("mltv-stream-ingest-01")
expect(body["location"]).to eq("fsn1")
expect(body["location"]).to eq("nbg1")
[
201,
{ "Content-Type" => "application/json" },
@@ -49,10 +49,10 @@ RSpec.describe Streams::CloudProviders::Hetzner do
}
]
end
stubs.get("/actions/7") do
stubs.get("https://api.hetzner.cloud/v1/actions/7") do
[200, { "Content-Type" => "application/json" }, { "action" => { "id" => 7, "status" => "success" } }]
end
stubs.get("/servers/42") do
stubs.get("https://api.hetzner.cloud/v1/servers/42") do
[
200,
{ "Content-Type" => "application/json" },
@@ -76,7 +76,7 @@ RSpec.describe Streams::CloudProviders::Hetzner do
end
it "destroys a server" do
stubs.delete("/servers/42") { [204, {}, ""] }
stubs.delete("https://api.hetzner.cloud/v1/servers/42") { [204, {}, ""] }
expect(provider.destroy_node("42")).to eq(true)
end
end
@@ -5,7 +5,7 @@ require "rails_helper"
RSpec.describe Streams::DnsProviders::Hetzner do
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:conn) do
Faraday.new(url: "https://api.hetzner.cloud/v1") do |f|
Faraday.new(url: Streams::DnsProviders::Hetzner::API) do |f|
f.request :json
f.response :json
f.adapter :test, stubs
@@ -16,8 +16,8 @@ RSpec.describe Streams::DnsProviders::Hetzner do
after { stubs.verify_stubbed_calls }
it "upserts an A record (delete then create)" do
stubs.delete("/zones/mltv-stream.net/rrsets/ingest-01/A") { [404, {}, ""] }
stubs.post("/zones/mltv-stream.net/rrsets") do |env|
stubs.delete("https://api.hetzner.cloud/v1/zones/mltv-stream.net/rrsets/ingest-01/A") { [404, {}, ""] }
stubs.post("https://api.hetzner.cloud/v1/zones/mltv-stream.net/rrsets") do |env|
body = JSON.parse(env.body)
expect(body["name"]).to eq("ingest-01")
expect(body["type"]).to eq("A")
@@ -29,7 +29,7 @@ RSpec.describe Streams::DnsProviders::Hetzner do
end
it "deletes an A record" do
stubs.delete("/zones/mltv-stream.net/rrsets/ingest-01/A") { [204, {}, ""] }
stubs.delete("https://api.hetzner.cloud/v1/zones/mltv-stream.net/rrsets/ingest-01/A") { [204, {}, ""] }
expect(provider.delete_a("ingest-01.mltv-stream.net")).to eq(true)
end
end
@@ -15,6 +15,8 @@ RSpec.describe Streams::NodeProvisioner do
before do
redis.del(Streams::DnsProviders::Lab::REDIS_KEY)
StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current)
StreamNode.where.not(slug: Streams::NodeRegistry::HOME_SLUG).delete_all
end
it "provisions and decommissions a simulated lab node" do
@@ -22,6 +22,12 @@ RSpec.describe Streams::NodeRegistry do
}
end
before do
StreamSession.where.not(stream_node_id: nil).update_all(stream_node_id: nil, status: "ended", ended_at: Time.current)
StreamNode.where.not(slug: Streams::NodeRegistry::HOME_SLUG).delete_all
StreamNode.find_by(slug: Streams::NodeRegistry::HOME_SLUG)&.update!(max_publishers: 2, status: "ready")
end
it "creates home node from ENV" do
with_env(home_env) do
node = described_class.ensure_home_from_env!