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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler auto-requires "kamal-dev" based on the gem name
4
+ # This file redirects to the actual implementation at kamal/dev.rb
5
+ require_relative "kamal/dev"
data/sig/kamal/dev.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Kamal
2
+ module Dev
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
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: []