nvoi 0.1.5 → 0.1.6

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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/todo/refactor/00-overview.md +171 -0
  3. data/.claude/todo/refactor/01-objects.md +96 -0
  4. data/.claude/todo/refactor/02-utils.md +143 -0
  5. data/.claude/todo/refactor/03-external-cloud.md +164 -0
  6. data/.claude/todo/refactor/04-external-dns.md +104 -0
  7. data/.claude/todo/refactor/05-external.md +133 -0
  8. data/.claude/todo/refactor/06-cli.md +123 -0
  9. data/.claude/todo/refactor/07-cli-deploy-command.md +177 -0
  10. data/.claude/todo/refactor/08-cli-deploy-steps.md +201 -0
  11. data/.claude/todo/refactor/09-cli-delete-command.md +169 -0
  12. data/.claude/todo/refactor/10-cli-exec-command.md +157 -0
  13. data/.claude/todo/refactor/11-cli-credentials-command.md +190 -0
  14. data/.claude/todo/refactor/_target.md +79 -0
  15. data/.claude/todo/scaleway.impl.md +644 -0
  16. data/.claude/todo/scaleway.reference.md +520 -0
  17. data/Gemfile +1 -0
  18. data/Gemfile.lock +12 -2
  19. data/doc/config-schema.yaml +44 -11
  20. data/examples/golang/deploy.enc +0 -0
  21. data/examples/golang/main.go +18 -0
  22. data/exe/nvoi +3 -1
  23. data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
  24. data/lib/nvoi/cli/credentials/show/command.rb +35 -0
  25. data/lib/nvoi/cli/db/command.rb +308 -0
  26. data/lib/nvoi/cli/delete/command.rb +75 -0
  27. data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
  28. data/lib/nvoi/cli/delete/steps/teardown_dns.rb +49 -0
  29. data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
  30. data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
  31. data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
  32. data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
  33. data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
  34. data/lib/nvoi/cli/deploy/command.rb +184 -0
  35. data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
  36. data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
  37. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +100 -0
  38. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +396 -0
  39. data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
  40. data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
  41. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
  42. data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +481 -0
  43. data/lib/nvoi/cli/exec/command.rb +173 -0
  44. data/lib/nvoi/cli.rb +83 -142
  45. data/lib/nvoi/config_api/actions/app.rb +53 -0
  46. data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
  47. data/lib/nvoi/config_api/actions/database.rb +70 -0
  48. data/lib/nvoi/config_api/actions/env.rb +32 -0
  49. data/lib/nvoi/config_api/actions/secret.rb +32 -0
  50. data/lib/nvoi/config_api/actions/server.rb +66 -0
  51. data/lib/nvoi/config_api/actions/volume.rb +40 -0
  52. data/lib/nvoi/config_api/base.rb +44 -0
  53. data/lib/nvoi/config_api/result.rb +26 -0
  54. data/lib/nvoi/config_api.rb +70 -0
  55. data/lib/nvoi/errors.rb +68 -50
  56. data/lib/nvoi/external/cloud/aws.rb +425 -0
  57. data/lib/nvoi/external/cloud/base.rb +99 -0
  58. data/lib/nvoi/external/cloud/factory.rb +48 -0
  59. data/lib/nvoi/external/cloud/hetzner.rb +376 -0
  60. data/lib/nvoi/external/cloud/scaleway.rb +533 -0
  61. data/lib/nvoi/external/cloud.rb +15 -0
  62. data/lib/nvoi/external/containerd.rb +82 -0
  63. data/lib/nvoi/external/database/mysql.rb +84 -0
  64. data/lib/nvoi/external/database/postgres.rb +82 -0
  65. data/lib/nvoi/external/database/provider.rb +65 -0
  66. data/lib/nvoi/external/database/sqlite.rb +72 -0
  67. data/lib/nvoi/external/database.rb +22 -0
  68. data/lib/nvoi/external/dns/cloudflare.rb +292 -0
  69. data/lib/nvoi/external/kubectl.rb +65 -0
  70. data/lib/nvoi/external/ssh.rb +106 -0
  71. data/lib/nvoi/objects/config_override.rb +60 -0
  72. data/lib/nvoi/objects/configuration.rb +463 -0
  73. data/lib/nvoi/objects/database.rb +56 -0
  74. data/lib/nvoi/objects/dns.rb +14 -0
  75. data/lib/nvoi/objects/firewall.rb +11 -0
  76. data/lib/nvoi/objects/network.rb +11 -0
  77. data/lib/nvoi/objects/server.rb +14 -0
  78. data/lib/nvoi/objects/service_spec.rb +26 -0
  79. data/lib/nvoi/objects/tunnel.rb +14 -0
  80. data/lib/nvoi/objects/volume.rb +17 -0
  81. data/lib/nvoi/utils/config_loader.rb +172 -0
  82. data/lib/nvoi/utils/constants.rb +61 -0
  83. data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
  84. data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
  85. data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
  86. data/lib/nvoi/utils/logger.rb +84 -0
  87. data/lib/nvoi/{config/naming.rb → utils/namer.rb} +28 -25
  88. data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
  89. data/lib/nvoi/utils/templates.rb +62 -0
  90. data/lib/nvoi/version.rb +1 -1
  91. data/lib/nvoi.rb +10 -54
  92. data/templates/error-backend.yaml.erb +134 -0
  93. metadata +97 -44
  94. data/examples/golang/deploy.yml +0 -54
  95. data/lib/nvoi/cloudflare/client.rb +0 -287
  96. data/lib/nvoi/config/config.rb +0 -248
  97. data/lib/nvoi/config/loader.rb +0 -102
  98. data/lib/nvoi/config/ssh_keys.rb +0 -82
  99. data/lib/nvoi/config/types.rb +0 -274
  100. data/lib/nvoi/constants.rb +0 -59
  101. data/lib/nvoi/credentials/editor.rb +0 -272
  102. data/lib/nvoi/deployer/cleaner.rb +0 -36
  103. data/lib/nvoi/deployer/image_builder.rb +0 -23
  104. data/lib/nvoi/deployer/infrastructure.rb +0 -126
  105. data/lib/nvoi/deployer/orchestrator.rb +0 -146
  106. data/lib/nvoi/deployer/service_deployer.rb +0 -311
  107. data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
  108. data/lib/nvoi/deployer/types.rb +0 -8
  109. data/lib/nvoi/k8s/renderer.rb +0 -44
  110. data/lib/nvoi/k8s/templates.rb +0 -29
  111. data/lib/nvoi/logger.rb +0 -72
  112. data/lib/nvoi/providers/aws.rb +0 -403
  113. data/lib/nvoi/providers/base.rb +0 -111
  114. data/lib/nvoi/providers/hetzner.rb +0 -288
  115. data/lib/nvoi/providers/hetzner_client.rb +0 -170
  116. data/lib/nvoi/remote/docker_manager.rb +0 -203
  117. data/lib/nvoi/remote/ssh_executor.rb +0 -72
  118. data/lib/nvoi/remote/volume_manager.rb +0 -103
  119. data/lib/nvoi/service/delete.rb +0 -234
  120. data/lib/nvoi/service/deploy.rb +0 -80
  121. data/lib/nvoi/service/exec.rb +0 -144
  122. data/lib/nvoi/service/provider.rb +0 -36
  123. data/lib/nvoi/steps/application_deployer.rb +0 -26
  124. data/lib/nvoi/steps/database_provisioner.rb +0 -60
  125. data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
  126. data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
  127. data/lib/nvoi/steps/server_provisioner.rb +0 -43
  128. data/lib/nvoi/steps/services_provisioner.rb +0 -29
  129. data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
  130. data/lib/nvoi/steps/volume_provisioner.rb +0 -154
@@ -0,0 +1,425 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-ec2"
4
+ require "base64"
5
+
6
+ module Nvoi
7
+ module External
8
+ module Cloud
9
+ # AWS provider implements the compute provider interface for AWS EC2
10
+ class Aws < Base
11
+ def initialize(access_key_id, secret_access_key, region)
12
+ @region = region || "us-east-1"
13
+ @client = ::Aws::EC2::Client.new(
14
+ region: @region,
15
+ credentials: ::Aws::Credentials.new(access_key_id, secret_access_key)
16
+ )
17
+ end
18
+
19
+ # Network operations
20
+
21
+ def find_or_create_network(name)
22
+ vpc = find_vpc_by_name(name)
23
+ if vpc
24
+ return Objects::Network::Record.new(
25
+ id: vpc.vpc_id,
26
+ name:,
27
+ ip_range: vpc.cidr_block
28
+ )
29
+ end
30
+
31
+ # Create new VPC
32
+ create_resp = @client.create_vpc(
33
+ cidr_block: "10.0.0.0/16",
34
+ tag_specifications: [{
35
+ resource_type: "vpc",
36
+ tags: [{ key: "Name", value: name }]
37
+ }]
38
+ )
39
+ vpc_id = create_resp.vpc.vpc_id
40
+
41
+ # Enable DNS hostnames
42
+ @client.modify_vpc_attribute(
43
+ vpc_id:,
44
+ enable_dns_hostnames: { value: true }
45
+ )
46
+
47
+ # Create subnet
48
+ subnet_resp = @client.create_subnet(
49
+ vpc_id:,
50
+ cidr_block: "10.0.1.0/24",
51
+ tag_specifications: [{
52
+ resource_type: "subnet",
53
+ tags: [{ key: "Name", value: "#{name}-subnet" }]
54
+ }]
55
+ )
56
+
57
+ # Create internet gateway
58
+ igw_resp = @client.create_internet_gateway(
59
+ tag_specifications: [{
60
+ resource_type: "internet-gateway",
61
+ tags: [{ key: "Name", value: "#{name}-igw" }]
62
+ }]
63
+ )
64
+ igw_id = igw_resp.internet_gateway.internet_gateway_id
65
+
66
+ # Attach internet gateway to VPC
67
+ @client.attach_internet_gateway(vpc_id:, internet_gateway_id: igw_id)
68
+
69
+ # Create route table
70
+ rtb_resp = @client.create_route_table(
71
+ vpc_id:,
72
+ tag_specifications: [{
73
+ resource_type: "route-table",
74
+ tags: [{ key: "Name", value: "#{name}-rtb" }]
75
+ }]
76
+ )
77
+ rtb_id = rtb_resp.route_table.route_table_id
78
+
79
+ # Add route to internet gateway
80
+ @client.create_route(
81
+ route_table_id: rtb_id,
82
+ destination_cidr_block: "0.0.0.0/0",
83
+ gateway_id: igw_id
84
+ )
85
+
86
+ # Associate route table with subnet
87
+ @client.associate_route_table(
88
+ route_table_id: rtb_id,
89
+ subnet_id: subnet_resp.subnet.subnet_id
90
+ )
91
+
92
+ Objects::Network::Record.new(
93
+ id: vpc_id,
94
+ name:,
95
+ ip_range: create_resp.vpc.cidr_block
96
+ )
97
+ end
98
+
99
+ def get_network_by_name(name)
100
+ vpc = find_vpc_by_name(name)
101
+ raise Errors::NetworkError, "network not found: #{name}" unless vpc
102
+
103
+ Objects::Network::Record.new(
104
+ id: vpc.vpc_id,
105
+ name:,
106
+ ip_range: vpc.cidr_block
107
+ )
108
+ end
109
+
110
+ def delete_network(id)
111
+ @client.delete_vpc(vpc_id: id)
112
+ end
113
+
114
+ # Firewall operations
115
+
116
+ def find_or_create_firewall(name)
117
+ sg = find_security_group_by_name(name)
118
+ if sg
119
+ return Objects::Firewall::Record.new(id: sg.group_id, name:)
120
+ end
121
+
122
+ # Get default VPC
123
+ vpcs = @client.describe_vpcs(filters: [{ name: "isDefault", values: ["true"] }])
124
+ raise Errors::NetworkError, "no default VPC found" if vpcs.vpcs.empty?
125
+
126
+ # Create security group
127
+ create_resp = @client.create_security_group(
128
+ group_name: name,
129
+ description: "Managed by nvoi",
130
+ vpc_id: vpcs.vpcs[0].vpc_id,
131
+ tag_specifications: [{
132
+ resource_type: "security-group",
133
+ tags: [{ key: "Name", value: name }]
134
+ }]
135
+ )
136
+
137
+ # Add SSH ingress rule
138
+ @client.authorize_security_group_ingress(
139
+ group_id: create_resp.group_id,
140
+ ip_permissions: [{
141
+ ip_protocol: "tcp",
142
+ from_port: 22,
143
+ to_port: 22,
144
+ ip_ranges: [{ cidr_ip: "0.0.0.0/0" }]
145
+ }]
146
+ )
147
+
148
+ Objects::Firewall::Record.new(id: create_resp.group_id, name:)
149
+ end
150
+
151
+ def get_firewall_by_name(name)
152
+ sg = find_security_group_by_name(name)
153
+ raise Errors::FirewallError, "firewall not found: #{name}" unless sg
154
+
155
+ Objects::Firewall::Record.new(id: sg.group_id, name:)
156
+ end
157
+
158
+ def delete_firewall(id)
159
+ @client.delete_security_group(group_id: id)
160
+ end
161
+
162
+ # Server operations
163
+
164
+ def find_server(name)
165
+ instance = find_instance_by_name(name)
166
+ return nil unless instance
167
+
168
+ instance_to_server(instance)
169
+ end
170
+
171
+ def find_server_by_id(id)
172
+ result = @client.describe_instances(instance_ids: [id])
173
+ return nil if result.reservations.empty? || result.reservations[0].instances.empty?
174
+
175
+ instance_to_server(result.reservations[0].instances[0])
176
+ rescue ::Aws::EC2::Errors::InvalidInstanceIDNotFound
177
+ nil
178
+ end
179
+
180
+ def list_servers
181
+ result = @client.describe_instances(
182
+ filters: [{
183
+ name: "instance-state-name",
184
+ values: %w[pending running stopping stopped]
185
+ }]
186
+ )
187
+
188
+ servers = []
189
+ result.reservations.each do |reservation|
190
+ reservation.instances.each do |instance|
191
+ servers << instance_to_server(instance)
192
+ end
193
+ end
194
+ servers
195
+ end
196
+
197
+ def create_server(opts)
198
+ ami_id = get_ubuntu_ami
199
+
200
+ input = {
201
+ image_id: ami_id,
202
+ instance_type: opts.type,
203
+ min_count: 1,
204
+ max_count: 1,
205
+ user_data: opts.user_data ? Base64.encode64(opts.user_data) : nil,
206
+ tag_specifications: [{
207
+ resource_type: "instance",
208
+ tags: [{ key: "Name", value: opts.name }]
209
+ }]
210
+ }
211
+
212
+ # Add network configuration if provided
213
+ if opts.network_id && !opts.network_id.empty?
214
+ subnets = @client.describe_subnets(
215
+ filters: [{ name: "vpc-id", values: [opts.network_id] }]
216
+ )
217
+ input[:subnet_id] = subnets.subnets[0].subnet_id unless subnets.subnets.empty?
218
+ end
219
+
220
+ # Add security group if provided
221
+ if opts.firewall_id && !opts.firewall_id.empty?
222
+ input[:security_group_ids] = [opts.firewall_id]
223
+ end
224
+
225
+ result = @client.run_instances(input)
226
+ raise Errors::ServerCreationError, "no instance created" if result.instances.empty?
227
+
228
+ instance_to_server(result.instances[0])
229
+ end
230
+
231
+ def wait_for_server(server_id, max_attempts)
232
+ server = Utils::Retry.poll(max_attempts: max_attempts, interval: 5) do
233
+ resp = @client.describe_instances(instance_ids: [server_id])
234
+
235
+ if resp.reservations.any? && resp.reservations[0].instances.any?
236
+ instance = resp.reservations[0].instances[0]
237
+ instance_to_server(instance) if instance.state.name == "running"
238
+ end
239
+ end
240
+
241
+ raise Errors::ServerCreationError, "instance did not become running after #{max_attempts} attempts" unless server
242
+
243
+ server
244
+ end
245
+
246
+ def delete_server(id)
247
+ @client.terminate_instances(instance_ids: [id])
248
+ end
249
+
250
+ # Volume operations
251
+
252
+ def create_volume(opts)
253
+ resp = @client.describe_instances(instance_ids: [opts.server_id])
254
+ raise Errors::VolumeError, "instance not found: #{opts.server_id}" if resp.reservations.empty?
255
+
256
+ instance = resp.reservations[0].instances[0]
257
+ az = instance.placement.availability_zone
258
+
259
+ create_resp = @client.create_volume(
260
+ availability_zone: az,
261
+ size: opts.size,
262
+ volume_type: "gp3",
263
+ tag_specifications: [{
264
+ resource_type: "volume",
265
+ tags: [{ key: "Name", value: opts.name }]
266
+ }]
267
+ )
268
+
269
+ Objects::Volume::Record.new(
270
+ id: create_resp.volume_id,
271
+ name: opts.name,
272
+ size: create_resp.size,
273
+ location: create_resp.availability_zone,
274
+ status: create_resp.state
275
+ )
276
+ end
277
+
278
+ def get_volume(id)
279
+ resp = @client.describe_volumes(volume_ids: [id])
280
+ return nil if resp.volumes.empty?
281
+
282
+ volume_to_object(resp.volumes[0])
283
+ end
284
+
285
+ def get_volume_by_name(name)
286
+ resp = @client.describe_volumes(
287
+ filters: [{ name: "tag:Name", values: [name] }]
288
+ )
289
+ return nil if resp.volumes.empty?
290
+
291
+ volume_to_object(resp.volumes[0])
292
+ end
293
+
294
+ def delete_volume(id)
295
+ @client.delete_volume(volume_id: id)
296
+ end
297
+
298
+ def attach_volume(volume_id, server_id)
299
+ @client.attach_volume(
300
+ volume_id:,
301
+ instance_id: server_id,
302
+ device: "/dev/xvdf"
303
+ )
304
+ end
305
+
306
+ def detach_volume(volume_id)
307
+ @client.detach_volume(volume_id:)
308
+ end
309
+
310
+ def wait_for_device_path(volume_id, _ssh)
311
+ # AWS provides device path in attachment info
312
+ Utils::Retry.poll(max_attempts: 30, interval: 2) do
313
+ resp = @client.describe_volumes(volume_ids: [volume_id])
314
+ next nil if resp.volumes.empty?
315
+
316
+ vol = resp.volumes[0]
317
+ next nil if vol.attachments.empty?
318
+
319
+ device = vol.attachments[0].device
320
+ device if device && !device.empty?
321
+ end
322
+ end
323
+
324
+ # Validation operations
325
+
326
+ def validate_instance_type(instance_type)
327
+ resp = @client.describe_instance_types(instance_types: [instance_type])
328
+ raise Errors::ValidationError, "invalid AWS instance type: #{instance_type}" if resp.instance_types.empty?
329
+
330
+ true
331
+ end
332
+
333
+ def validate_region(region)
334
+ resp = @client.describe_regions(region_names: [region])
335
+ raise Errors::ValidationError, "invalid AWS region: #{region}" if resp.regions.empty?
336
+
337
+ true
338
+ end
339
+
340
+ def validate_credentials
341
+ @client.describe_regions
342
+ true
343
+ rescue StandardError => e
344
+ raise Errors::ValidationError, "aws credentials invalid: #{e.message}"
345
+ end
346
+
347
+ private
348
+
349
+ def find_vpc_by_name(name)
350
+ resp = @client.describe_vpcs(
351
+ filters: [{ name: "tag:Name", values: [name] }]
352
+ )
353
+ resp.vpcs.first
354
+ end
355
+
356
+ def find_security_group_by_name(name)
357
+ resp = @client.describe_security_groups(
358
+ filters: [{ name: "group-name", values: [name] }]
359
+ )
360
+ resp.security_groups.first
361
+ end
362
+
363
+ def find_instance_by_name(name)
364
+ resp = @client.describe_instances(
365
+ filters: [
366
+ { name: "tag:Name", values: [name] },
367
+ { name: "instance-state-name", values: %w[pending running stopping stopped] }
368
+ ]
369
+ )
370
+
371
+ resp.reservations.each do |reservation|
372
+ return reservation.instances.first unless reservation.instances.empty?
373
+ end
374
+ nil
375
+ end
376
+
377
+ def get_ubuntu_ami
378
+ resp = @client.describe_images(
379
+ owners: ["099720109477"], # Canonical's AWS account ID
380
+ filters: [
381
+ { name: "name", values: ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] },
382
+ { name: "state", values: ["available"] }
383
+ ]
384
+ )
385
+
386
+ raise Errors::ProviderError, "no Ubuntu 22.04 AMI found" if resp.images.empty?
387
+
388
+ latest = resp.images.max_by(&:creation_date)
389
+ latest.image_id
390
+ end
391
+
392
+ def instance_to_server(instance)
393
+ name = instance.tags&.find { |t| t.key == "Name" }&.value || ""
394
+
395
+ Objects::Server::Record.new(
396
+ id: instance.instance_id,
397
+ name:,
398
+ status: instance.state.name,
399
+ public_ipv4: instance.public_ip_address,
400
+ private_ipv4: instance.private_ip_address
401
+ )
402
+ end
403
+
404
+ def volume_to_object(vol)
405
+ name = vol.tags&.find { |t| t.key == "Name" }&.value || ""
406
+
407
+ v = Objects::Volume::Record.new(
408
+ id: vol.volume_id,
409
+ name:,
410
+ size: vol.size,
411
+ location: vol.availability_zone,
412
+ status: vol.state
413
+ )
414
+
415
+ if vol.attachments.any?
416
+ v.server_id = vol.attachments[0].instance_id
417
+ v.device_path = vol.attachments[0].device
418
+ end
419
+
420
+ v
421
+ end
422
+ end
423
+ end
424
+ end
425
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module External
5
+ module Cloud
6
+ # Base provider interface - all providers must implement these methods
7
+ class Base
8
+ # Network operations
9
+ def find_or_create_network(name)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def get_network_by_name(name)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def delete_network(id)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ # Firewall operations
22
+ def find_or_create_firewall(name)
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def get_firewall_by_name(name)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def delete_firewall(id)
31
+ raise NotImplementedError
32
+ end
33
+
34
+ # Server operations
35
+ def find_server(name)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ def find_server_by_id(id)
40
+ raise NotImplementedError
41
+ end
42
+
43
+ def list_servers
44
+ raise NotImplementedError
45
+ end
46
+
47
+ def create_server(opts)
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def wait_for_server(server_id, max_attempts)
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def delete_server(id)
56
+ raise NotImplementedError
57
+ end
58
+
59
+ # Volume operations
60
+ def create_volume(opts)
61
+ raise NotImplementedError
62
+ end
63
+
64
+ def get_volume(id)
65
+ raise NotImplementedError
66
+ end
67
+
68
+ def get_volume_by_name(name)
69
+ raise NotImplementedError
70
+ end
71
+
72
+ def delete_volume(id)
73
+ raise NotImplementedError
74
+ end
75
+
76
+ def attach_volume(volume_id, server_id)
77
+ raise NotImplementedError
78
+ end
79
+
80
+ def detach_volume(volume_id)
81
+ raise NotImplementedError
82
+ end
83
+
84
+ # Validation operations
85
+ def validate_instance_type(instance_type)
86
+ raise NotImplementedError
87
+ end
88
+
89
+ def validate_region(region)
90
+ raise NotImplementedError
91
+ end
92
+
93
+ def validate_credentials
94
+ raise NotImplementedError
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module External
5
+ module Cloud
6
+ # Factory creates cloud providers from config
7
+ module Factory
8
+ class << self
9
+ def for(config)
10
+ case config.provider_name
11
+ when "hetzner"
12
+ h = config.hetzner
13
+ Hetzner.new(h.api_token)
14
+ when "aws"
15
+ a = config.aws
16
+ Aws.new(a.access_key_id, a.secret_access_key, a.region)
17
+ when "scaleway"
18
+ s = config.scaleway
19
+ Scaleway.new(s.secret_key, s.project_id, zone: s.zone)
20
+ else
21
+ raise Errors::ProviderError, "unknown provider: #{config.provider_name}"
22
+ end
23
+ end
24
+
25
+ def validate(config, provider)
26
+ case config.provider_name
27
+ when "hetzner"
28
+ h = config.hetzner
29
+ provider.validate_credentials
30
+ provider.validate_instance_type(h.server_type)
31
+ provider.validate_region(h.server_location)
32
+ when "aws"
33
+ a = config.aws
34
+ provider.validate_credentials
35
+ provider.validate_instance_type(a.instance_type)
36
+ provider.validate_region(a.region)
37
+ when "scaleway"
38
+ s = config.scaleway
39
+ provider.validate_credentials
40
+ provider.validate_instance_type(s.server_type)
41
+ provider.validate_region(s.zone)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end