kamal-dev 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +508 -0
- data/LICENSE.txt +21 -0
- data/README.md +899 -0
- data/Rakefile +10 -0
- data/exe/kamal-dev +10 -0
- data/exe/plugin-kamal-dev +283 -0
- data/lib/kamal/cli/dev.rb +1192 -0
- data/lib/kamal/dev/builder.rb +332 -0
- data/lib/kamal/dev/compose_parser.rb +255 -0
- data/lib/kamal/dev/config.rb +359 -0
- data/lib/kamal/dev/devcontainer.rb +122 -0
- data/lib/kamal/dev/devcontainer_parser.rb +204 -0
- data/lib/kamal/dev/registry.rb +149 -0
- data/lib/kamal/dev/secrets_loader.rb +93 -0
- data/lib/kamal/dev/state_manager.rb +271 -0
- data/lib/kamal/dev/templates/dev-entrypoint.sh +44 -0
- data/lib/kamal/dev/templates/dev.yml +93 -0
- data/lib/kamal/dev/version.rb +7 -0
- data/lib/kamal/dev.rb +33 -0
- data/lib/kamal/providers/base.rb +121 -0
- data/lib/kamal/providers/upcloud.rb +299 -0
- data/lib/kamal-dev.rb +5 -0
- data/sig/kamal/dev.rbs +6 -0
- data/test_installer.sh +73 -0
- metadata +141 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kamal
|
|
4
|
+
module Providers
|
|
5
|
+
# Custom exception hierarchy for provider errors
|
|
6
|
+
class ProvisioningError < StandardError; end
|
|
7
|
+
class TimeoutError < ProvisioningError; end
|
|
8
|
+
class QuotaExceededError < ProvisioningError; end
|
|
9
|
+
class AuthenticationError < StandardError; end
|
|
10
|
+
class RateLimitError < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Factory method to instantiate the appropriate provider
|
|
13
|
+
#
|
|
14
|
+
# @param config [Hash] Provider configuration including type and credentials
|
|
15
|
+
# @option config [String] "type" Provider type (e.g., "upcloud")
|
|
16
|
+
# @option config [String] "username" Provider API username (provider-specific)
|
|
17
|
+
# @option config [String] "password" Provider API password (provider-specific)
|
|
18
|
+
#
|
|
19
|
+
# @return [Kamal::Providers::Base] Provider instance
|
|
20
|
+
#
|
|
21
|
+
# @raise [Kamal::Dev::ConfigurationError] if provider type is unknown
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# provider = Kamal::Providers.for({
|
|
25
|
+
# "type" => "upcloud",
|
|
26
|
+
# "username" => ENV["UPCLOUD_USERNAME"],
|
|
27
|
+
# "password" => ENV["UPCLOUD_PASSWORD"]
|
|
28
|
+
# })
|
|
29
|
+
def self.for(config)
|
|
30
|
+
type = config["type"] || config[:type]
|
|
31
|
+
|
|
32
|
+
case type&.to_s&.downcase
|
|
33
|
+
when "upcloud"
|
|
34
|
+
require_relative "upcloud" unless defined?(Upcloud)
|
|
35
|
+
Upcloud.new(
|
|
36
|
+
username: config["username"] || config[:username],
|
|
37
|
+
password: config["password"] || config[:password]
|
|
38
|
+
)
|
|
39
|
+
else
|
|
40
|
+
raise Kamal::Dev::ConfigurationError, "Unknown provider type: #{type.inspect}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Abstract base class defining the provider adapter interface
|
|
45
|
+
# All cloud provider implementations must inherit from this class
|
|
46
|
+
# and implement the required methods.
|
|
47
|
+
#
|
|
48
|
+
# Example implementation:
|
|
49
|
+
# class MyProvider < Kamal::Providers::Base
|
|
50
|
+
# def provision_vm(config)
|
|
51
|
+
# # Implementation here
|
|
52
|
+
# end
|
|
53
|
+
# end
|
|
54
|
+
class Base
|
|
55
|
+
# Provision a new VM
|
|
56
|
+
#
|
|
57
|
+
# @param config [Hash] VM configuration
|
|
58
|
+
# @option config [String] :zone Cloud zone (e.g., "us-nyc1")
|
|
59
|
+
# @option config [String] :plan VM plan/size (e.g., "1xCPU-2GB")
|
|
60
|
+
# @option config [String] :ssh_key Public SSH key for access
|
|
61
|
+
# @option config [String] :title VM name/title
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash] VM details
|
|
64
|
+
# @option return [String] :id VM identifier
|
|
65
|
+
# @option return [String] :ip Public IP address
|
|
66
|
+
# @option return [Symbol] :status VM status (:pending, :running, :failed)
|
|
67
|
+
#
|
|
68
|
+
# @raise [AuthenticationError] if credentials are invalid
|
|
69
|
+
# @raise [QuotaExceededError] if provider quota is exceeded
|
|
70
|
+
# @raise [TimeoutError] if VM doesn't start within timeout period
|
|
71
|
+
# @raise [ProvisioningError] for other provisioning failures
|
|
72
|
+
def provision_vm(config)
|
|
73
|
+
raise NotImplementedError, "#{self.class}#provision_vm must be implemented"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Query VM status
|
|
77
|
+
#
|
|
78
|
+
# @param vm_id [String] VM identifier
|
|
79
|
+
#
|
|
80
|
+
# @return [Symbol] VM status
|
|
81
|
+
# - :pending - VM is being created
|
|
82
|
+
# - :running - VM is running
|
|
83
|
+
# - :failed - VM failed to start
|
|
84
|
+
# - :stopped - VM is stopped
|
|
85
|
+
#
|
|
86
|
+
# @raise [AuthenticationError] if credentials are invalid
|
|
87
|
+
def query_status(vm_id)
|
|
88
|
+
raise NotImplementedError, "#{self.class}#query_status must be implemented"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Destroy VM and cleanup all associated resources
|
|
92
|
+
#
|
|
93
|
+
# @param vm_id [String] VM identifier
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] true if successful, false otherwise
|
|
96
|
+
#
|
|
97
|
+
# @raise [AuthenticationError] if credentials are invalid
|
|
98
|
+
def destroy_vm(vm_id)
|
|
99
|
+
raise NotImplementedError, "#{self.class}#destroy_vm must be implemented"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Estimate monthly cost for VM configuration
|
|
103
|
+
#
|
|
104
|
+
# This provides generic cost guidance and pricing page link.
|
|
105
|
+
# Real-time pricing queries are not implemented in Phase 1.
|
|
106
|
+
#
|
|
107
|
+
# @param config [Hash] VM configuration
|
|
108
|
+
# @option config [String] :zone Cloud zone
|
|
109
|
+
# @option config [String] :plan VM plan/size
|
|
110
|
+
#
|
|
111
|
+
# @return [Hash] Cost estimate details
|
|
112
|
+
# @option return [String] :warning User-friendly cost warning message
|
|
113
|
+
# @option return [String] :plan VM plan being estimated
|
|
114
|
+
# @option return [String] :zone Cloud zone
|
|
115
|
+
# @option return [String] :pricing_url URL to provider's pricing page
|
|
116
|
+
def estimate_cost(config)
|
|
117
|
+
raise NotImplementedError, "#{self.class}#estimate_cost must be implemented"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "base"
|
|
7
|
+
|
|
8
|
+
module Kamal
|
|
9
|
+
module Providers
|
|
10
|
+
# UpCloud API v1.3 provider implementation
|
|
11
|
+
#
|
|
12
|
+
# Implements VM provisioning, status querying, and cleanup via UpCloud's REST API.
|
|
13
|
+
# Uses Faraday with retry middleware for robust HTTP communication.
|
|
14
|
+
#
|
|
15
|
+
# @example Initialize provider
|
|
16
|
+
# provider = Kamal::Providers::Upcloud.new(
|
|
17
|
+
# username: ENV['UPCLOUD_USERNAME'],
|
|
18
|
+
# password: ENV['UPCLOUD_PASSWORD']
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example Provision a VM
|
|
22
|
+
# vm = provider.provision_vm(
|
|
23
|
+
# zone: 'us-nyc1',
|
|
24
|
+
# plan: '1xCPU-2GB',
|
|
25
|
+
# title: 'my-dev-vm',
|
|
26
|
+
# ssh_key: File.read('~/.ssh/id_rsa.pub')
|
|
27
|
+
# )
|
|
28
|
+
# # => { id: 'uuid', ip: '1.2.3.4', status: :running }
|
|
29
|
+
class Upcloud < Base
|
|
30
|
+
API_BASE_URL = "https://api.upcloud.com"
|
|
31
|
+
API_VERSION = "1.3"
|
|
32
|
+
POLLING_INTERVAL = 5 # seconds
|
|
33
|
+
POLLING_TIMEOUT = 120 # seconds
|
|
34
|
+
|
|
35
|
+
# UpCloud storage template for Ubuntu 24.04 LTS (latest LTS)
|
|
36
|
+
# Using template UUID (universal across all UpCloud zones)
|
|
37
|
+
# Template type: cloud-init
|
|
38
|
+
# See: https://developers.upcloud.com/1.3/7-templates/
|
|
39
|
+
#
|
|
40
|
+
# Available Ubuntu templates:
|
|
41
|
+
# - Ubuntu 24.04 LTS (Noble Numbat) - UUID: 01000000-0000-4000-8000-000030240200
|
|
42
|
+
# - Ubuntu 22.04 LTS (Jammy Jellyfish) - UUID: 01000000-0000-4000-8000-000030220200
|
|
43
|
+
#
|
|
44
|
+
# Note: UUIDs verified from UpCloud API (2025-11-18)
|
|
45
|
+
DEFAULT_UBUNTU_TEMPLATE = "01000000-0000-4000-8000-000030240200"
|
|
46
|
+
|
|
47
|
+
# Initialize UpCloud provider with credentials
|
|
48
|
+
#
|
|
49
|
+
# @param username [String] UpCloud API username
|
|
50
|
+
# @param password [String] UpCloud API password
|
|
51
|
+
def initialize(username:, password:)
|
|
52
|
+
@conn = build_connection(username, password)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Provision a new VM on UpCloud
|
|
56
|
+
#
|
|
57
|
+
# @param config [Hash] VM configuration
|
|
58
|
+
# @option config [String] :zone Cloud zone (e.g., "us-nyc1")
|
|
59
|
+
# @option config [String] :plan VM plan (e.g., "1xCPU-2GB")
|
|
60
|
+
# @option config [String] :title VM name/title
|
|
61
|
+
# @option config [String] :ssh_key Public SSH key for access
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash] VM details
|
|
64
|
+
# @option return [String] :id VM identifier (UUID)
|
|
65
|
+
# @option return [String] :ip Public IP address
|
|
66
|
+
# @option return [Symbol] :status VM status (:running)
|
|
67
|
+
#
|
|
68
|
+
# @raise [AuthenticationError] if credentials are invalid
|
|
69
|
+
# @raise [QuotaExceededError] if provider quota is exceeded
|
|
70
|
+
# @raise [TimeoutError] if VM doesn't start within timeout
|
|
71
|
+
# @raise [ProvisioningError] for other failures
|
|
72
|
+
def provision_vm(config)
|
|
73
|
+
response = @conn.post("/#{API_VERSION}/server") do |req|
|
|
74
|
+
req.headers["Content-Type"] = "application/json"
|
|
75
|
+
req.body = build_server_spec(config).to_json
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
server_data = parse_response(response)
|
|
79
|
+
vm_id = server_data["uuid"]
|
|
80
|
+
vm_ip = extract_ip_address(server_data)
|
|
81
|
+
|
|
82
|
+
# If already started, return immediately
|
|
83
|
+
return {id: vm_id, ip: vm_ip, status: :running} if server_data["state"] == "started"
|
|
84
|
+
|
|
85
|
+
# Otherwise poll until running
|
|
86
|
+
poll_until_running(vm_id)
|
|
87
|
+
|
|
88
|
+
{id: vm_id, ip: vm_ip, status: :running}
|
|
89
|
+
rescue Faraday::UnauthorizedError
|
|
90
|
+
raise AuthenticationError, "Invalid UpCloud credentials"
|
|
91
|
+
rescue Faraday::ForbiddenError => e
|
|
92
|
+
handle_forbidden_error(e)
|
|
93
|
+
rescue Faraday::ClientError => e
|
|
94
|
+
raise ProvisioningError, "UpCloud API error: #{e.response[:body]}"
|
|
95
|
+
rescue Faraday::ServerError
|
|
96
|
+
raise ProvisioningError, "UpCloud service unavailable"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Query VM status
|
|
100
|
+
#
|
|
101
|
+
# @param vm_id [String] VM identifier (UUID)
|
|
102
|
+
#
|
|
103
|
+
# @return [Symbol] VM status
|
|
104
|
+
# - :pending - VM is being created or in maintenance
|
|
105
|
+
# - :running - VM is running
|
|
106
|
+
# - :failed - VM failed to start
|
|
107
|
+
# - :stopped - VM is stopped
|
|
108
|
+
#
|
|
109
|
+
# @raise [AuthenticationError] if credentials are invalid
|
|
110
|
+
def query_status(vm_id)
|
|
111
|
+
response = @conn.get("/#{API_VERSION}/server/#{vm_id}")
|
|
112
|
+
server_data = response.body["server"]
|
|
113
|
+
|
|
114
|
+
map_state_to_status(server_data["state"])
|
|
115
|
+
rescue Faraday::UnauthorizedError
|
|
116
|
+
raise AuthenticationError, "Invalid UpCloud credentials"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Destroy VM and cleanup all associated resources
|
|
120
|
+
#
|
|
121
|
+
# @param vm_id [String] VM identifier (UUID)
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean] true if successful (idempotent)
|
|
124
|
+
#
|
|
125
|
+
# @raise [AuthenticationError] if credentials are invalid
|
|
126
|
+
def destroy_vm(vm_id)
|
|
127
|
+
@conn.delete("/#{API_VERSION}/server/#{vm_id}") do |req|
|
|
128
|
+
req.params["storages"] = "1" # Delete attached storages
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
true
|
|
132
|
+
rescue Faraday::ResourceNotFound
|
|
133
|
+
# Already deleted - idempotent
|
|
134
|
+
true
|
|
135
|
+
rescue Faraday::UnauthorizedError
|
|
136
|
+
raise AuthenticationError, "Invalid UpCloud credentials"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Estimate monthly cost for VM configuration
|
|
140
|
+
#
|
|
141
|
+
# Provides generic cost guidance and pricing page link.
|
|
142
|
+
# Real-time pricing queries not implemented in Phase 1.
|
|
143
|
+
#
|
|
144
|
+
# @param config [Hash] VM configuration
|
|
145
|
+
# @option config [String] "zone" Cloud zone (string key)
|
|
146
|
+
# @option config [String] "plan" VM plan (string key)
|
|
147
|
+
#
|
|
148
|
+
# @return [Hash] Cost estimate details
|
|
149
|
+
# @option return [String] :warning User-friendly cost warning
|
|
150
|
+
# @option return [String] :plan VM plan
|
|
151
|
+
# @option return [String] :zone Cloud zone
|
|
152
|
+
# @option return [String] :pricing_url UpCloud pricing page
|
|
153
|
+
def estimate_cost(config)
|
|
154
|
+
plan = config["plan"] || config[:plan]
|
|
155
|
+
zone = config["zone"] || config[:zone]
|
|
156
|
+
|
|
157
|
+
{
|
|
158
|
+
warning: "Deploying VMs with plan #{plan} in zone #{zone}. " \
|
|
159
|
+
"Check pricing for accurate costs.",
|
|
160
|
+
plan: plan,
|
|
161
|
+
zone: zone,
|
|
162
|
+
pricing_url: "https://upcloud.com/pricing"
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# Build Faraday connection with middleware
|
|
169
|
+
def build_connection(username, password)
|
|
170
|
+
Faraday.new(url: API_BASE_URL) do |f|
|
|
171
|
+
# Request middleware (order matters!)
|
|
172
|
+
f.request :authorization, :basic, username, password
|
|
173
|
+
f.request :json # Auto-encode request bodies as JSON
|
|
174
|
+
|
|
175
|
+
# Retry middleware with exponential backoff
|
|
176
|
+
f.request :retry,
|
|
177
|
+
max: 3,
|
|
178
|
+
interval: 0.5,
|
|
179
|
+
interval_randomness: 0.5,
|
|
180
|
+
backoff_factor: 2,
|
|
181
|
+
retry_statuses: [429, 500, 502, 503, 504],
|
|
182
|
+
methods: [:get, :post, :delete]
|
|
183
|
+
|
|
184
|
+
# Response middleware
|
|
185
|
+
f.response :json, content_type: /\bjson$/ # Auto-parse JSON responses
|
|
186
|
+
f.response :raise_error # Raise on 4xx/5xx responses
|
|
187
|
+
|
|
188
|
+
# Adapter (must be last)
|
|
189
|
+
f.adapter Faraday.default_adapter
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Build UpCloud server specification from config
|
|
194
|
+
def build_server_spec(config)
|
|
195
|
+
# Determine storage template UUID
|
|
196
|
+
# Priority: 1) config override, 2) default Ubuntu 24.04 template
|
|
197
|
+
# Template UUIDs are universal (same across all UpCloud zones)
|
|
198
|
+
storage_template = config[:storage_template] || DEFAULT_UBUNTU_TEMPLATE
|
|
199
|
+
|
|
200
|
+
{
|
|
201
|
+
server: {
|
|
202
|
+
zone: config[:zone],
|
|
203
|
+
title: config[:title] || "kamal-dev-vm",
|
|
204
|
+
hostname: "#{config[:title] || "kamal-dev-vm"}.local",
|
|
205
|
+
plan: config[:plan],
|
|
206
|
+
metadata: "yes", # Required for cloud-init templates
|
|
207
|
+
storage_devices: {
|
|
208
|
+
storage_device: [
|
|
209
|
+
{
|
|
210
|
+
action: "clone",
|
|
211
|
+
storage: storage_template,
|
|
212
|
+
title: "#{config[:title]}-disk",
|
|
213
|
+
size: config[:disk_size] || 25 # GB
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
login_user: {
|
|
218
|
+
username: "root",
|
|
219
|
+
ssh_keys: {
|
|
220
|
+
ssh_key: [config[:ssh_key]]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Parse JSON response
|
|
228
|
+
def parse_response(response)
|
|
229
|
+
response.body["server"] || response.body
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Extract public IPv4 address from server data
|
|
233
|
+
# Prefers IPv4 over IPv6 for broader network compatibility
|
|
234
|
+
def extract_ip_address(server_data)
|
|
235
|
+
ip_addresses = server_data.dig("ip_addresses", "ip_address") || []
|
|
236
|
+
|
|
237
|
+
# Priority 1: Public IPv4 address
|
|
238
|
+
ipv4_public = ip_addresses.find { |ip| ip["access"] == "public" && ip["family"] == "IPv4" }
|
|
239
|
+
return ipv4_public["address"] if ipv4_public
|
|
240
|
+
|
|
241
|
+
# Priority 2: Any IPv4 address
|
|
242
|
+
ipv4_any = ip_addresses.find { |ip| ip["family"] == "IPv4" }
|
|
243
|
+
return ipv4_any["address"] if ipv4_any
|
|
244
|
+
|
|
245
|
+
# Priority 3: Public IPv6 address (fallback)
|
|
246
|
+
ipv6_public = ip_addresses.find { |ip| ip["access"] == "public" && ip["family"] == "IPv6" }
|
|
247
|
+
return ipv6_public["address"] if ipv6_public
|
|
248
|
+
|
|
249
|
+
# Priority 4: Any IP address (last resort)
|
|
250
|
+
ip_addresses.first&.fetch("address")
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Poll VM status until running or timeout
|
|
254
|
+
def poll_until_running(vm_id)
|
|
255
|
+
start_time = Time.now
|
|
256
|
+
|
|
257
|
+
loop do
|
|
258
|
+
status = query_status(vm_id)
|
|
259
|
+
|
|
260
|
+
return if status == :running
|
|
261
|
+
|
|
262
|
+
raise ProvisioningError, "VM failed to start" if status == :failed
|
|
263
|
+
raise TimeoutError, "VM provision timeout after #{POLLING_TIMEOUT}s" if Time.now - start_time > POLLING_TIMEOUT
|
|
264
|
+
|
|
265
|
+
sleep POLLING_INTERVAL
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Map UpCloud state to standard status symbol
|
|
270
|
+
def map_state_to_status(state)
|
|
271
|
+
case state
|
|
272
|
+
when "started"
|
|
273
|
+
:running
|
|
274
|
+
when "stopped"
|
|
275
|
+
:stopped
|
|
276
|
+
when "error"
|
|
277
|
+
:failed
|
|
278
|
+
when "maintenance", "pending"
|
|
279
|
+
:pending
|
|
280
|
+
else
|
|
281
|
+
:pending
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Handle 403 Forbidden errors (quota, credits, etc.)
|
|
286
|
+
def handle_forbidden_error(error)
|
|
287
|
+
body = error.response[:body].to_s
|
|
288
|
+
|
|
289
|
+
if body.include?("quota")
|
|
290
|
+
raise QuotaExceededError, "UpCloud quota exceeded"
|
|
291
|
+
elsif body.include?("credit")
|
|
292
|
+
raise ProvisioningError, "Insufficient UpCloud credits"
|
|
293
|
+
else
|
|
294
|
+
raise ProvisioningError, "Access forbidden: #{body}"
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
data/lib/kamal-dev.rb
ADDED
data/sig/kamal/dev.rbs
ADDED
data/test_installer.sh
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Test script for kamal-dev-install executable
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
echo "๐งช Testing kamal-dev-install..."
|
|
7
|
+
echo
|
|
8
|
+
|
|
9
|
+
# Store current directory (the gem root)
|
|
10
|
+
GEM_ROOT=$(pwd)
|
|
11
|
+
|
|
12
|
+
# Create a temporary test directory
|
|
13
|
+
TEST_DIR=$(mktemp -d)
|
|
14
|
+
echo "๐ Created test directory: $TEST_DIR"
|
|
15
|
+
|
|
16
|
+
cd "$TEST_DIR"
|
|
17
|
+
|
|
18
|
+
# Create a minimal Gemfile
|
|
19
|
+
cat > Gemfile <<EOF
|
|
20
|
+
source 'https://rubygems.org'
|
|
21
|
+
|
|
22
|
+
gem 'kamal', '~> 2.0'
|
|
23
|
+
gem 'kamal-dev', path: '$GEM_ROOT'
|
|
24
|
+
EOF
|
|
25
|
+
|
|
26
|
+
echo "โ Created test Gemfile"
|
|
27
|
+
|
|
28
|
+
# Run bundle install
|
|
29
|
+
echo "๐ฆ Running bundle install..."
|
|
30
|
+
bundle install --quiet
|
|
31
|
+
|
|
32
|
+
# Run the installer
|
|
33
|
+
echo "๐ง Running plugin-kamal-dev..."
|
|
34
|
+
bundle exec plugin-kamal-dev
|
|
35
|
+
|
|
36
|
+
# Verify bin/kamal exists
|
|
37
|
+
if [ ! -f "bin/kamal" ]; then
|
|
38
|
+
echo "โ FAILED: bin/kamal not created"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "โ bin/kamal exists"
|
|
43
|
+
|
|
44
|
+
# Verify it contains the require
|
|
45
|
+
if ! grep -q 'require "kamal-dev"' bin/kamal; then
|
|
46
|
+
echo "โ FAILED: bin/kamal does not contain kamal-dev require"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
echo "โ bin/kamal contains kamal-dev require"
|
|
51
|
+
|
|
52
|
+
# Run installer again to test idempotency
|
|
53
|
+
echo "๐ง Running installer again (testing idempotency)..."
|
|
54
|
+
bundle exec plugin-kamal-dev
|
|
55
|
+
|
|
56
|
+
# Verify still only one require line
|
|
57
|
+
REQUIRE_COUNT=$(grep -c 'require "kamal-dev"' bin/kamal || true)
|
|
58
|
+
if [ "$REQUIRE_COUNT" -ne 1 ]; then
|
|
59
|
+
echo "โ FAILED: Expected 1 require line, found $REQUIRE_COUNT"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
echo "โ Installer is idempotent"
|
|
64
|
+
|
|
65
|
+
# Cleanup
|
|
66
|
+
cd -
|
|
67
|
+
rm -rf "$TEST_DIR"
|
|
68
|
+
echo "๐งน Cleaned up test directory"
|
|
69
|
+
|
|
70
|
+
echo
|
|
71
|
+
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
|
72
|
+
echo "โ
All installer tests passed!"
|
|
73
|
+
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
|
metadata
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kamal-dev
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Lauri Jutila
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: kamal
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: faraday
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: faraday-retry
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: webmock
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.18'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.18'
|
|
82
|
+
description: A Ruby gem that helps to deploy development containers using Kamal.
|
|
83
|
+
email:
|
|
84
|
+
- ljuti@nmux.dev
|
|
85
|
+
executables:
|
|
86
|
+
- kamal-dev
|
|
87
|
+
- plugin-kamal-dev
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- ".rspec"
|
|
92
|
+
- ".standard.yml"
|
|
93
|
+
- CHANGELOG.md
|
|
94
|
+
- LICENSE.txt
|
|
95
|
+
- README.md
|
|
96
|
+
- Rakefile
|
|
97
|
+
- exe/kamal-dev
|
|
98
|
+
- exe/plugin-kamal-dev
|
|
99
|
+
- lib/kamal-dev.rb
|
|
100
|
+
- lib/kamal/cli/dev.rb
|
|
101
|
+
- lib/kamal/dev.rb
|
|
102
|
+
- lib/kamal/dev/builder.rb
|
|
103
|
+
- lib/kamal/dev/compose_parser.rb
|
|
104
|
+
- lib/kamal/dev/config.rb
|
|
105
|
+
- lib/kamal/dev/devcontainer.rb
|
|
106
|
+
- lib/kamal/dev/devcontainer_parser.rb
|
|
107
|
+
- lib/kamal/dev/registry.rb
|
|
108
|
+
- lib/kamal/dev/secrets_loader.rb
|
|
109
|
+
- lib/kamal/dev/state_manager.rb
|
|
110
|
+
- lib/kamal/dev/templates/dev-entrypoint.sh
|
|
111
|
+
- lib/kamal/dev/templates/dev.yml
|
|
112
|
+
- lib/kamal/dev/version.rb
|
|
113
|
+
- lib/kamal/providers/base.rb
|
|
114
|
+
- lib/kamal/providers/upcloud.rb
|
|
115
|
+
- sig/kamal/dev.rbs
|
|
116
|
+
- test_installer.sh
|
|
117
|
+
homepage: https://github.com/hyperengineering/kamal-dev
|
|
118
|
+
licenses:
|
|
119
|
+
- MIT
|
|
120
|
+
metadata:
|
|
121
|
+
homepage_uri: https://github.com/hyperengineering/kamal-dev
|
|
122
|
+
source_code_uri: https://github.com/hyperengineering/kamal-dev
|
|
123
|
+
changelog_uri: https://github.com/hyperengineering/kamal-dev/blob/main/CHANGELOG.md
|
|
124
|
+
rdoc_options: []
|
|
125
|
+
require_paths:
|
|
126
|
+
- lib
|
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: 3.0.0
|
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
requirements: []
|
|
138
|
+
rubygems_version: 3.6.9
|
|
139
|
+
specification_version: 4
|
|
140
|
+
summary: Deploy devcontainers with Kamal
|
|
141
|
+
test_files: []
|