Class: Kamal::Providers::Upcloud

Inherits:
Base
  • Object
show all
Defined in:
lib/kamal/providers/upcloud.rb

Overview

UpCloud API v1.3 provider implementation

Implements VM provisioning, status querying, and cleanup via UpCloud's REST API. Uses Faraday with retry middleware for robust HTTP communication.

Examples:

Initialize provider

provider = Kamal::Providers::Upcloud.new(
  username: ENV['UPCLOUD_USERNAME'],
  password: ENV['UPCLOUD_PASSWORD']
)

Provision a VM

vm = provider.provision_vm(
  zone: 'us-nyc1',
  plan: '1xCPU-2GB',
  title: 'my-dev-vm',
  ssh_key: File.read('~/.ssh/id_rsa.pub')
)
# => { id: 'uuid', ip: '1.2.3.4', status: :running }

Constant Summary collapse

API_BASE_URL =
"https://api.upcloud.com"
API_VERSION =
"1.3"
POLLING_INTERVAL =

seconds

5
POLLING_TIMEOUT =

seconds

120
DEFAULT_UBUNTU_TEMPLATE =

UpCloud storage template for Ubuntu 24.04 LTS (latest LTS) Using template UUID (universal across all UpCloud zones) Template type: cloud-init See: https://developers.upcloud.com/1.3/7-templates/

Available Ubuntu templates:

- Ubuntu 24.04 LTS (Noble Numbat) - UUID: 01000000-0000-4000-8000-000030240200
- Ubuntu 22.04 LTS (Jammy Jellyfish) - UUID: 01000000-0000-4000-8000-000030220200

Note: UUIDs verified from UpCloud API (2025-11-18)

"01000000-0000-4000-8000-000030240200"

Instance Method Summary collapse

Constructor Details

#initialize(username:, password:) ⇒ Upcloud

Initialize UpCloud provider with credentials

Parameters:

  • username (String)

    UpCloud API username

  • password (String)

    UpCloud API password



51
52
53
# File 'lib/kamal/providers/upcloud.rb', line 51

def initialize(username:, password:)
  @conn = build_connection(username, password)
end

Instance Method Details

#destroy_vm(vm_id) ⇒ Boolean

Destroy VM and cleanup all associated resources

Parameters:

  • vm_id (String)

    VM identifier (UUID)

Returns:

  • (Boolean)

    true if successful (idempotent)

Raises:



126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/kamal/providers/upcloud.rb', line 126

def destroy_vm(vm_id)
  @conn.delete("/#{API_VERSION}/server/#{vm_id}") do |req|
    req.params["storages"] = "1" # Delete attached storages
  end

  true
rescue Faraday::ResourceNotFound
  # Already deleted - idempotent
  true
rescue Faraday::UnauthorizedError
  raise AuthenticationError, "Invalid UpCloud credentials"
end

#estimate_cost(config) ⇒ Hash

Estimate monthly cost for VM configuration

Provides generic cost guidance and pricing page link. Real-time pricing queries not implemented in Phase 1.

Parameters:

  • config (Hash)

    VM configuration

  • return (Hash)

    a customizable set of options

Options Hash (config):

  • "zone" (String)

    Cloud zone (string key)

  • "plan" (String)

    VM plan (string key)

Returns:

  • (Hash)

    Cost estimate details



153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/kamal/providers/upcloud.rb', line 153

def estimate_cost(config)
  plan = config["plan"] || config[:plan]
  zone = config["zone"] || config[:zone]

  {
    warning: "Deploying VMs with plan #{plan} in zone #{zone}. " \
             "Check pricing for accurate costs.",
    plan: plan,
    zone: zone,
    pricing_url: "https://upcloud.com/pricing"
  }
end

#provision_vm(config) ⇒ Hash

Provision a new VM on UpCloud

Parameters:

  • config (Hash)

    VM configuration

  • return (Hash)

    a customizable set of options

Options Hash (config):

  • :zone (String)

    Cloud zone (e.g., "us-nyc1")

  • :plan (String)

    VM plan (e.g., "1xCPU-2GB")

  • :title (String)

    VM name/title

  • :ssh_key (String)

    Public SSH key for access

Returns:

  • (Hash)

    VM details

Raises:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/kamal/providers/upcloud.rb', line 72

def provision_vm(config)
  response = @conn.post("/#{API_VERSION}/server") do |req|
    req.headers["Content-Type"] = "application/json"
    req.body = build_server_spec(config).to_json
  end

  server_data = parse_response(response)
  vm_id = server_data["uuid"]
  vm_ip = extract_ip_address(server_data)

  # If already started, return immediately
  return {id: vm_id, ip: vm_ip, status: :running} if server_data["state"] == "started"

  # Otherwise poll until running
  poll_until_running(vm_id)

  {id: vm_id, ip: vm_ip, status: :running}
rescue Faraday::UnauthorizedError
  raise AuthenticationError, "Invalid UpCloud credentials"
rescue Faraday::ForbiddenError => e
  handle_forbidden_error(e)
rescue Faraday::ClientError => e
  raise ProvisioningError, "UpCloud API error: #{e.response[:body]}"
rescue Faraday::ServerError
  raise ProvisioningError, "UpCloud service unavailable"
end

#query_status(vm_id) ⇒ Symbol

Query VM status

Parameters:

  • vm_id (String)

    VM identifier (UUID)

Returns:

  • (Symbol)

    VM status

    • :pending - VM is being created or in maintenance
    • :running - VM is running
    • :failed - VM failed to start
    • :stopped - VM is stopped

Raises:



110
111
112
113
114
115
116
117
# File 'lib/kamal/providers/upcloud.rb', line 110

def query_status(vm_id)
  response = @conn.get("/#{API_VERSION}/server/#{vm_id}")
  server_data = response.body["server"]

  map_state_to_status(server_data["state"])
rescue Faraday::UnauthorizedError
  raise AuthenticationError, "Invalid UpCloud credentials"
end