nvoi 0.1.5 → 0.1.7

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 (156) 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/12-cli-db-command.md +128 -0
  15. data/.claude/todo/refactor/_target.md +79 -0
  16. data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
  17. data/.claude/todo/refactor-execution/01-objects.md +42 -0
  18. data/.claude/todo/refactor-execution/02-utils.md +41 -0
  19. data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
  20. data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
  21. data/.claude/todo/refactor-execution/05-external-other.md +46 -0
  22. data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
  23. data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
  24. data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
  25. data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
  26. data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
  27. data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
  28. data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
  29. data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
  30. data/.claude/todo/scaleway.impl.md +644 -0
  31. data/.claude/todo/scaleway.reference.md +520 -0
  32. data/.claude/todos.md +550 -0
  33. data/Gemfile +6 -0
  34. data/Gemfile.lock +46 -5
  35. data/Rakefile +1 -1
  36. data/doc/config-schema.yaml +44 -11
  37. data/examples/golang/deploy.enc +0 -0
  38. data/examples/golang/main.go +18 -0
  39. data/exe/nvoi +3 -1
  40. data/ingest +0 -0
  41. data/lib/nvoi/cli/config/command.rb +219 -0
  42. data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
  43. data/lib/nvoi/cli/credentials/show/command.rb +35 -0
  44. data/lib/nvoi/cli/db/command.rb +308 -0
  45. data/lib/nvoi/cli/delete/command.rb +75 -0
  46. data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
  47. data/lib/nvoi/cli/delete/steps/teardown_dns.rb +50 -0
  48. data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
  49. data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
  50. data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
  51. data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
  52. data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
  53. data/lib/nvoi/cli/deploy/command.rb +184 -0
  54. data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
  55. data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
  56. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +102 -0
  57. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +399 -0
  58. data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
  59. data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
  60. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
  61. data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +490 -0
  62. data/lib/nvoi/cli/exec/command.rb +173 -0
  63. data/lib/nvoi/cli/logs/command.rb +66 -0
  64. data/lib/nvoi/cli/onboard/command.rb +761 -0
  65. data/lib/nvoi/cli/unlock/command.rb +72 -0
  66. data/lib/nvoi/cli.rb +339 -141
  67. data/lib/nvoi/config_api/actions/app.rb +53 -0
  68. data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
  69. data/lib/nvoi/config_api/actions/database.rb +70 -0
  70. data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
  71. data/lib/nvoi/config_api/actions/env.rb +32 -0
  72. data/lib/nvoi/config_api/actions/init.rb +67 -0
  73. data/lib/nvoi/config_api/actions/secret.rb +32 -0
  74. data/lib/nvoi/config_api/actions/server.rb +66 -0
  75. data/lib/nvoi/config_api/actions/service.rb +52 -0
  76. data/lib/nvoi/config_api/actions/volume.rb +40 -0
  77. data/lib/nvoi/config_api/base.rb +38 -0
  78. data/lib/nvoi/config_api/result.rb +26 -0
  79. data/lib/nvoi/config_api.rb +93 -0
  80. data/lib/nvoi/errors.rb +68 -50
  81. data/lib/nvoi/external/cloud/aws.rb +450 -0
  82. data/lib/nvoi/external/cloud/base.rb +99 -0
  83. data/lib/nvoi/external/cloud/factory.rb +48 -0
  84. data/lib/nvoi/external/cloud/hetzner.rb +402 -0
  85. data/lib/nvoi/external/cloud/scaleway.rb +559 -0
  86. data/lib/nvoi/external/cloud.rb +15 -0
  87. data/lib/nvoi/external/containerd.rb +86 -0
  88. data/lib/nvoi/external/database/mysql.rb +84 -0
  89. data/lib/nvoi/external/database/postgres.rb +82 -0
  90. data/lib/nvoi/external/database/provider.rb +65 -0
  91. data/lib/nvoi/external/database/sqlite.rb +72 -0
  92. data/lib/nvoi/external/database.rb +22 -0
  93. data/lib/nvoi/external/dns/cloudflare.rb +310 -0
  94. data/lib/nvoi/external/kubectl.rb +65 -0
  95. data/lib/nvoi/external/ssh.rb +106 -0
  96. data/lib/nvoi/objects/config_override.rb +60 -0
  97. data/lib/nvoi/objects/configuration.rb +483 -0
  98. data/lib/nvoi/objects/database.rb +56 -0
  99. data/lib/nvoi/objects/dns.rb +14 -0
  100. data/lib/nvoi/objects/firewall.rb +11 -0
  101. data/lib/nvoi/objects/network.rb +11 -0
  102. data/lib/nvoi/objects/server.rb +14 -0
  103. data/lib/nvoi/objects/service_spec.rb +26 -0
  104. data/lib/nvoi/objects/tunnel.rb +14 -0
  105. data/lib/nvoi/objects/volume.rb +17 -0
  106. data/lib/nvoi/utils/config_loader.rb +172 -0
  107. data/lib/nvoi/utils/constants.rb +61 -0
  108. data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
  109. data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
  110. data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
  111. data/lib/nvoi/utils/logger.rb +84 -0
  112. data/lib/nvoi/{config/naming.rb → utils/namer.rb} +37 -25
  113. data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
  114. data/lib/nvoi/utils/templates.rb +62 -0
  115. data/lib/nvoi/version.rb +1 -1
  116. data/lib/nvoi.rb +27 -55
  117. data/templates/app-ingress.yaml.erb +3 -1
  118. data/templates/error-backend.yaml.erb +134 -0
  119. metadata +121 -44
  120. data/examples/golang/deploy.yml +0 -54
  121. data/lib/nvoi/cloudflare/client.rb +0 -287
  122. data/lib/nvoi/config/config.rb +0 -248
  123. data/lib/nvoi/config/loader.rb +0 -102
  124. data/lib/nvoi/config/ssh_keys.rb +0 -82
  125. data/lib/nvoi/config/types.rb +0 -274
  126. data/lib/nvoi/constants.rb +0 -59
  127. data/lib/nvoi/credentials/editor.rb +0 -272
  128. data/lib/nvoi/deployer/cleaner.rb +0 -36
  129. data/lib/nvoi/deployer/image_builder.rb +0 -23
  130. data/lib/nvoi/deployer/infrastructure.rb +0 -126
  131. data/lib/nvoi/deployer/orchestrator.rb +0 -146
  132. data/lib/nvoi/deployer/service_deployer.rb +0 -311
  133. data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
  134. data/lib/nvoi/deployer/types.rb +0 -8
  135. data/lib/nvoi/k8s/renderer.rb +0 -44
  136. data/lib/nvoi/k8s/templates.rb +0 -29
  137. data/lib/nvoi/logger.rb +0 -72
  138. data/lib/nvoi/providers/aws.rb +0 -403
  139. data/lib/nvoi/providers/base.rb +0 -111
  140. data/lib/nvoi/providers/hetzner.rb +0 -288
  141. data/lib/nvoi/providers/hetzner_client.rb +0 -170
  142. data/lib/nvoi/remote/docker_manager.rb +0 -203
  143. data/lib/nvoi/remote/ssh_executor.rb +0 -72
  144. data/lib/nvoi/remote/volume_manager.rb +0 -103
  145. data/lib/nvoi/service/delete.rb +0 -234
  146. data/lib/nvoi/service/deploy.rb +0 -80
  147. data/lib/nvoi/service/exec.rb +0 -144
  148. data/lib/nvoi/service/provider.rb +0 -36
  149. data/lib/nvoi/steps/application_deployer.rb +0 -26
  150. data/lib/nvoi/steps/database_provisioner.rb +0 -60
  151. data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
  152. data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
  153. data/lib/nvoi/steps/server_provisioner.rb +0 -43
  154. data/lib/nvoi/steps/services_provisioner.rb +0 -29
  155. data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
  156. data/lib/nvoi/steps/volume_provisioner.rb +0 -154
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module Deployer
5
- # TunnelInfo holds information about a configured tunnel
6
- TunnelInfo = Struct.new(:service_name, :hostname, :tunnel_id, :tunnel_token, :port, keyword_init: true)
7
- end
8
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module K8s
5
- # TemplateBinding provides a clean binding for ERB templates
6
- class TemplateBinding
7
- def initialize(data)
8
- data.each do |key, value|
9
- instance_variable_set("@#{key}", value)
10
- define_singleton_method(key) { instance_variable_get("@#{key}") }
11
- end
12
- end
13
-
14
- def get_binding
15
- binding
16
- end
17
- end
18
-
19
- # Renderer handles K8s manifest rendering and application
20
- module Renderer
21
- class << self
22
- # Render a template with the provided data
23
- def render_template(name, data)
24
- template = Templates.load_template(name)
25
- binding_obj = TemplateBinding.new(data)
26
- template.result(binding_obj.get_binding)
27
- end
28
-
29
- # Render a template and apply it via kubectl
30
- def apply_manifest(ssh, template_name, data)
31
- manifest = render_template(template_name, data)
32
-
33
- cmd = "cat <<'EOF' | kubectl apply -f -\n#{manifest}\nEOF"
34
- ssh.execute(cmd)
35
- end
36
-
37
- # Wait for a deployment to be ready
38
- def wait_for_deployment(ssh, name, namespace: "default", timeout: 300)
39
- ssh.execute("kubectl rollout status deployment/#{name} -n #{namespace} --timeout=#{timeout}s")
40
- end
41
- end
42
- end
43
- end
44
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "erb"
4
-
5
- module Nvoi
6
- module K8s
7
- # Templates handles K8s manifest template loading
8
- module Templates
9
- class << self
10
- def template_path(name)
11
- File.join(Nvoi.templates_path, "#{name}.erb")
12
- end
13
-
14
- def load_template(name)
15
- path = template_path(name)
16
- raise TemplateError, "template #{name} not found at #{path}" unless File.exist?(path)
17
-
18
- ERB.new(File.read(path), trim_mode: "-")
19
- end
20
-
21
- def template_names
22
- Dir.glob(File.join(Nvoi.templates_path, "*.erb")).map do |path|
23
- File.basename(path, ".erb")
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
data/lib/nvoi/logger.rb DELETED
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- class Logger
5
- COLORS = {
6
- reset: "\e[0m",
7
- red: "\e[31m",
8
- green: "\e[32m",
9
- yellow: "\e[33m",
10
- blue: "\e[34m",
11
- magenta: "\e[35m",
12
- cyan: "\e[36m",
13
- white: "\e[37m",
14
- bold: "\e[1m"
15
- }.freeze
16
-
17
- def initialize(output: $stdout, color: true)
18
- @output = output
19
- @color = color && output.tty?
20
- end
21
-
22
- def info(message, *args)
23
- log(:blue, "INFO", format_message(message, args))
24
- end
25
-
26
- def success(message, *args)
27
- log(:green, "SUCCESS", format_message(message, args))
28
- end
29
-
30
- def warning(message, *args)
31
- log(:yellow, "WARNING", format_message(message, args))
32
- end
33
-
34
- def error(message, *args)
35
- log(:red, "ERROR", format_message(message, args))
36
- end
37
-
38
- def debug(message, *args)
39
- return unless ENV["NVOI_DEBUG"]
40
-
41
- log(:magenta, "DEBUG", format_message(message, args))
42
- end
43
-
44
- def separator
45
- @output.puts colorize(:cyan, "-" * 60)
46
- end
47
-
48
- def blank
49
- @output.puts
50
- end
51
-
52
- private
53
-
54
- def format_message(message, args)
55
- return message if args.empty?
56
-
57
- format(message, *args)
58
- end
59
-
60
- def log(color, level, message)
61
- timestamp = Time.now.strftime("%H:%M:%S")
62
- prefix = colorize(color, "[#{timestamp}] [#{level}]")
63
- @output.puts "#{prefix} #{message}"
64
- end
65
-
66
- def colorize(color, text)
67
- return text unless @color
68
-
69
- "#{COLORS[color]}#{text}#{COLORS[:reset]}"
70
- end
71
- end
72
- end
@@ -1,403 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "aws-sdk-ec2"
4
-
5
- module Nvoi
6
- module Providers
7
- # AWS provider implements the compute provider interface for AWS EC2
8
- class AWS < Base
9
- def initialize(access_key_id, secret_access_key, region)
10
- @region = region || "us-east-1"
11
- @client = Aws::EC2::Client.new(
12
- region: @region,
13
- credentials: Aws::Credentials.new(access_key_id, secret_access_key)
14
- )
15
- end
16
-
17
- # Network operations
18
-
19
- def find_or_create_network(name)
20
- # Find existing VPC by tag
21
- vpc = find_vpc_by_name(name)
22
- if vpc
23
- return Network.new(
24
- id: vpc.vpc_id,
25
- name:,
26
- ip_range: vpc.cidr_block
27
- )
28
- end
29
-
30
- # Create new VPC
31
- create_resp = @client.create_vpc(
32
- cidr_block: "10.0.0.0/16",
33
- tag_specifications: [{
34
- resource_type: "vpc",
35
- tags: [{ key: "Name", value: name }]
36
- }]
37
- )
38
- vpc_id = create_resp.vpc.vpc_id
39
-
40
- # Enable DNS hostnames
41
- @client.modify_vpc_attribute(
42
- vpc_id:,
43
- enable_dns_hostnames: { value: true }
44
- )
45
-
46
- # Create subnet
47
- subnet_resp = @client.create_subnet(
48
- vpc_id:,
49
- cidr_block: "10.0.1.0/24",
50
- tag_specifications: [{
51
- resource_type: "subnet",
52
- tags: [{ key: "Name", value: "#{name}-subnet" }]
53
- }]
54
- )
55
-
56
- # Create internet gateway
57
- igw_resp = @client.create_internet_gateway(
58
- tag_specifications: [{
59
- resource_type: "internet-gateway",
60
- tags: [{ key: "Name", value: "#{name}-igw" }]
61
- }]
62
- )
63
- igw_id = igw_resp.internet_gateway.internet_gateway_id
64
-
65
- # Attach internet gateway to VPC
66
- @client.attach_internet_gateway(vpc_id:, internet_gateway_id: igw_id)
67
-
68
- # Create route table
69
- rtb_resp = @client.create_route_table(
70
- vpc_id:,
71
- tag_specifications: [{
72
- resource_type: "route-table",
73
- tags: [{ key: "Name", value: "#{name}-rtb" }]
74
- }]
75
- )
76
- rtb_id = rtb_resp.route_table.route_table_id
77
-
78
- # Add route to internet gateway
79
- @client.create_route(
80
- route_table_id: rtb_id,
81
- destination_cidr_block: "0.0.0.0/0",
82
- gateway_id: igw_id
83
- )
84
-
85
- # Associate route table with subnet
86
- @client.associate_route_table(
87
- route_table_id: rtb_id,
88
- subnet_id: subnet_resp.subnet.subnet_id
89
- )
90
-
91
- Network.new(
92
- id: vpc_id,
93
- name:,
94
- ip_range: create_resp.vpc.cidr_block
95
- )
96
- end
97
-
98
- def get_network_by_name(name)
99
- vpc = find_vpc_by_name(name)
100
- raise NetworkError, "network not found: #{name}" unless vpc
101
-
102
- Network.new(
103
- id: vpc.vpc_id,
104
- name:,
105
- ip_range: vpc.cidr_block
106
- )
107
- end
108
-
109
- def delete_network(id)
110
- @client.delete_vpc(vpc_id: id)
111
- end
112
-
113
- # Firewall operations
114
-
115
- def find_or_create_firewall(name)
116
- # Find existing security group
117
- sg = find_security_group_by_name(name)
118
- if sg
119
- return Firewall.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 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
- Firewall.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 FirewallError, "firewall not found: #{name}" unless sg
154
-
155
- Firewall.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 list_servers
172
- result = @client.describe_instances(
173
- filters: [{
174
- name: "instance-state-name",
175
- values: %w[pending running stopping stopped]
176
- }]
177
- )
178
-
179
- servers = []
180
- result.reservations.each do |reservation|
181
- reservation.instances.each do |instance|
182
- servers << instance_to_server(instance)
183
- end
184
- end
185
- servers
186
- end
187
-
188
- def create_server(opts)
189
- # Get AMI ID for Ubuntu 22.04
190
- ami_id = get_ubuntu_ami
191
-
192
- input = {
193
- image_id: ami_id,
194
- instance_type: opts.type,
195
- min_count: 1,
196
- max_count: 1,
197
- user_data: opts.user_data ? Base64.encode64(opts.user_data) : nil,
198
- tag_specifications: [{
199
- resource_type: "instance",
200
- tags: [{ key: "Name", value: opts.name }]
201
- }]
202
- }
203
-
204
- # Add network configuration if provided
205
- if opts.network_id && !opts.network_id.empty?
206
- subnets = @client.describe_subnets(
207
- filters: [{ name: "vpc-id", values: [opts.network_id] }]
208
- )
209
- input[:subnet_id] = subnets.subnets[0].subnet_id unless subnets.subnets.empty?
210
- end
211
-
212
- # Add security group if provided
213
- if opts.firewall_id && !opts.firewall_id.empty?
214
- input[:security_group_ids] = [opts.firewall_id]
215
- end
216
-
217
- result = @client.run_instances(input)
218
- raise ServerCreationError, "no instance created" if result.instances.empty?
219
-
220
- instance_to_server(result.instances[0])
221
- end
222
-
223
- def wait_for_server(server_id, max_attempts)
224
- max_attempts.times do
225
- resp = @client.describe_instances(instance_ids: [server_id])
226
-
227
- if resp.reservations.any? && resp.reservations[0].instances.any?
228
- instance = resp.reservations[0].instances[0]
229
- return instance_to_server(instance) if instance.state.name == "running"
230
- end
231
-
232
- sleep(5)
233
- end
234
-
235
- raise ServerCreationError, "instance did not become running after #{max_attempts} attempts"
236
- end
237
-
238
- def delete_server(id)
239
- @client.terminate_instances(instance_ids: [id])
240
- end
241
-
242
- # Volume operations
243
-
244
- def create_volume(opts)
245
- # Get instance to derive availability zone
246
- resp = @client.describe_instances(instance_ids: [opts.server_id])
247
- raise VolumeError, "instance not found: #{opts.server_id}" if resp.reservations.empty?
248
-
249
- instance = resp.reservations[0].instances[0]
250
- az = instance.placement.availability_zone
251
-
252
- create_resp = @client.create_volume(
253
- availability_zone: az,
254
- size: opts.size,
255
- volume_type: "gp3",
256
- tag_specifications: [{
257
- resource_type: "volume",
258
- tags: [{ key: "Name", value: opts.name }]
259
- }]
260
- )
261
-
262
- Volume.new(
263
- id: create_resp.volume_id,
264
- name: opts.name,
265
- size: create_resp.size,
266
- location: create_resp.availability_zone,
267
- status: create_resp.state
268
- )
269
- end
270
-
271
- def get_volume(id)
272
- resp = @client.describe_volumes(volume_ids: [id])
273
- return nil if resp.volumes.empty?
274
-
275
- volume_to_compute(resp.volumes[0])
276
- end
277
-
278
- def get_volume_by_name(name)
279
- resp = @client.describe_volumes(
280
- filters: [{ name: "tag:Name", values: [name] }]
281
- )
282
- return nil if resp.volumes.empty?
283
-
284
- volume_to_compute(resp.volumes[0])
285
- end
286
-
287
- def delete_volume(id)
288
- @client.delete_volume(volume_id: id)
289
- end
290
-
291
- def attach_volume(volume_id, server_id)
292
- @client.attach_volume(
293
- volume_id:,
294
- instance_id: server_id,
295
- device: "/dev/xvdf"
296
- )
297
- end
298
-
299
- def detach_volume(volume_id)
300
- @client.detach_volume(volume_id:)
301
- end
302
-
303
- # Validation operations
304
-
305
- def validate_instance_type(instance_type)
306
- resp = @client.describe_instance_types(instance_types: [instance_type])
307
- raise ValidationError, "invalid AWS instance type: #{instance_type}" if resp.instance_types.empty?
308
-
309
- true
310
- end
311
-
312
- def validate_region(region)
313
- resp = @client.describe_regions(region_names: [region])
314
- raise ValidationError, "invalid AWS region: #{region}" if resp.regions.empty?
315
-
316
- true
317
- end
318
-
319
- def validate_credentials
320
- @client.describe_regions
321
- true
322
- rescue StandardError => e
323
- raise ValidationError, "aws credentials invalid: #{e.message}"
324
- end
325
-
326
- private
327
-
328
- def find_vpc_by_name(name)
329
- resp = @client.describe_vpcs(
330
- filters: [{ name: "tag:Name", values: [name] }]
331
- )
332
- resp.vpcs.first
333
- end
334
-
335
- def find_security_group_by_name(name)
336
- resp = @client.describe_security_groups(
337
- filters: [{ name: "group-name", values: [name] }]
338
- )
339
- resp.security_groups.first
340
- end
341
-
342
- def find_instance_by_name(name)
343
- resp = @client.describe_instances(
344
- filters: [
345
- { name: "tag:Name", values: [name] },
346
- { name: "instance-state-name", values: %w[pending running stopping stopped] }
347
- ]
348
- )
349
-
350
- resp.reservations.each do |reservation|
351
- return reservation.instances.first unless reservation.instances.empty?
352
- end
353
- nil
354
- end
355
-
356
- def get_ubuntu_ami
357
- resp = @client.describe_images(
358
- owners: ["099720109477"], # Canonical's AWS account ID
359
- filters: [
360
- { name: "name", values: ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] },
361
- { name: "state", values: ["available"] }
362
- ]
363
- )
364
-
365
- raise ProviderError, "no Ubuntu 22.04 AMI found" if resp.images.empty?
366
-
367
- # Return the most recent AMI
368
- latest = resp.images.max_by(&:creation_date)
369
- latest.image_id
370
- end
371
-
372
- def instance_to_server(instance)
373
- name = instance.tags&.find { |t| t.key == "Name" }&.value || ""
374
-
375
- Server.new(
376
- id: instance.instance_id,
377
- name:,
378
- status: instance.state.name,
379
- public_ipv4: instance.public_ip_address
380
- )
381
- end
382
-
383
- def volume_to_compute(vol)
384
- name = vol.tags&.find { |t| t.key == "Name" }&.value || ""
385
-
386
- v = Volume.new(
387
- id: vol.volume_id,
388
- name:,
389
- size: vol.size,
390
- location: vol.availability_zone,
391
- status: vol.state
392
- )
393
-
394
- if vol.attachments.any?
395
- v.server_id = vol.attachments[0].instance_id
396
- v.device_path = vol.attachments[0].device
397
- end
398
-
399
- v
400
- end
401
- end
402
- end
403
- end
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module Providers
5
- # Network represents a virtual network
6
- Network = Struct.new(:id, :name, :ip_range, keyword_init: true)
7
-
8
- # Firewall represents a firewall configuration
9
- Firewall = Struct.new(:id, :name, keyword_init: true)
10
-
11
- # Server represents a compute server/instance
12
- Server = Struct.new(:id, :name, :status, :public_ipv4, keyword_init: true)
13
-
14
- # Volume represents a block storage volume
15
- Volume = Struct.new(:id, :name, :size, :location, :status, :server_id, :device_path, keyword_init: true)
16
-
17
- # ServerCreateOptions contains options for creating a server
18
- ServerCreateOptions = Struct.new(:name, :type, :image, :location, :user_data, :network_id, :firewall_id, keyword_init: true)
19
-
20
- # VolumeCreateOptions contains options for creating a volume
21
- VolumeCreateOptions = Struct.new(:name, :size, :server_id, keyword_init: true)
22
-
23
- # Base provider interface - all providers must implement these methods
24
- class Base
25
- # Network operations
26
- def find_or_create_network(name)
27
- raise NotImplementedError
28
- end
29
-
30
- def get_network_by_name(name)
31
- raise NotImplementedError
32
- end
33
-
34
- def delete_network(id)
35
- raise NotImplementedError
36
- end
37
-
38
- # Firewall operations
39
- def find_or_create_firewall(name)
40
- raise NotImplementedError
41
- end
42
-
43
- def get_firewall_by_name(name)
44
- raise NotImplementedError
45
- end
46
-
47
- def delete_firewall(id)
48
- raise NotImplementedError
49
- end
50
-
51
- # Server operations
52
- def find_server(name)
53
- raise NotImplementedError
54
- end
55
-
56
- def list_servers
57
- raise NotImplementedError
58
- end
59
-
60
- def create_server(opts)
61
- raise NotImplementedError
62
- end
63
-
64
- def wait_for_server(server_id, max_attempts)
65
- raise NotImplementedError
66
- end
67
-
68
- def delete_server(id)
69
- raise NotImplementedError
70
- end
71
-
72
- # Volume operations
73
- def create_volume(opts)
74
- raise NotImplementedError
75
- end
76
-
77
- def get_volume(id)
78
- raise NotImplementedError
79
- end
80
-
81
- def get_volume_by_name(name)
82
- raise NotImplementedError
83
- end
84
-
85
- def delete_volume(id)
86
- raise NotImplementedError
87
- end
88
-
89
- def attach_volume(volume_id, server_id)
90
- raise NotImplementedError
91
- end
92
-
93
- def detach_volume(volume_id)
94
- raise NotImplementedError
95
- end
96
-
97
- # Validation operations
98
- def validate_instance_type(instance_type)
99
- raise NotImplementedError
100
- end
101
-
102
- def validate_region(region)
103
- raise NotImplementedError
104
- end
105
-
106
- def validate_credentials
107
- raise NotImplementedError
108
- end
109
- end
110
- end
111
- end