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.
- checksums.yaml +4 -4
- data/.claude/todo/refactor/00-overview.md +171 -0
- data/.claude/todo/refactor/01-objects.md +96 -0
- data/.claude/todo/refactor/02-utils.md +143 -0
- data/.claude/todo/refactor/03-external-cloud.md +164 -0
- data/.claude/todo/refactor/04-external-dns.md +104 -0
- data/.claude/todo/refactor/05-external.md +133 -0
- data/.claude/todo/refactor/06-cli.md +123 -0
- data/.claude/todo/refactor/07-cli-deploy-command.md +177 -0
- data/.claude/todo/refactor/08-cli-deploy-steps.md +201 -0
- data/.claude/todo/refactor/09-cli-delete-command.md +169 -0
- data/.claude/todo/refactor/10-cli-exec-command.md +157 -0
- data/.claude/todo/refactor/11-cli-credentials-command.md +190 -0
- data/.claude/todo/refactor/12-cli-db-command.md +128 -0
- data/.claude/todo/refactor/_target.md +79 -0
- data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
- data/.claude/todo/refactor-execution/01-objects.md +42 -0
- data/.claude/todo/refactor-execution/02-utils.md +41 -0
- data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
- data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
- data/.claude/todo/refactor-execution/05-external-other.md +46 -0
- data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
- data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
- data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
- data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
- data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
- data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
- data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
- data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
- data/.claude/todo/scaleway.impl.md +644 -0
- data/.claude/todo/scaleway.reference.md +520 -0
- data/.claude/todos.md +550 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +46 -5
- data/Rakefile +1 -1
- data/doc/config-schema.yaml +44 -11
- data/examples/golang/deploy.enc +0 -0
- data/examples/golang/main.go +18 -0
- data/exe/nvoi +3 -1
- data/ingest +0 -0
- data/lib/nvoi/cli/config/command.rb +219 -0
- data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
- data/lib/nvoi/cli/credentials/show/command.rb +35 -0
- data/lib/nvoi/cli/db/command.rb +308 -0
- data/lib/nvoi/cli/delete/command.rb +75 -0
- data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
- data/lib/nvoi/cli/delete/steps/teardown_dns.rb +50 -0
- data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
- data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
- data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
- data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
- data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
- data/lib/nvoi/cli/deploy/command.rb +184 -0
- data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
- data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
- data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +102 -0
- data/lib/nvoi/cli/deploy/steps/deploy_service.rb +399 -0
- data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
- data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
- data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
- data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +490 -0
- data/lib/nvoi/cli/exec/command.rb +173 -0
- data/lib/nvoi/cli/logs/command.rb +66 -0
- data/lib/nvoi/cli/onboard/command.rb +761 -0
- data/lib/nvoi/cli/unlock/command.rb +72 -0
- data/lib/nvoi/cli.rb +339 -141
- data/lib/nvoi/config_api/actions/app.rb +53 -0
- data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
- data/lib/nvoi/config_api/actions/database.rb +70 -0
- data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
- data/lib/nvoi/config_api/actions/env.rb +32 -0
- data/lib/nvoi/config_api/actions/init.rb +67 -0
- data/lib/nvoi/config_api/actions/secret.rb +32 -0
- data/lib/nvoi/config_api/actions/server.rb +66 -0
- data/lib/nvoi/config_api/actions/service.rb +52 -0
- data/lib/nvoi/config_api/actions/volume.rb +40 -0
- data/lib/nvoi/config_api/base.rb +38 -0
- data/lib/nvoi/config_api/result.rb +26 -0
- data/lib/nvoi/config_api.rb +93 -0
- data/lib/nvoi/errors.rb +68 -50
- data/lib/nvoi/external/cloud/aws.rb +450 -0
- data/lib/nvoi/external/cloud/base.rb +99 -0
- data/lib/nvoi/external/cloud/factory.rb +48 -0
- data/lib/nvoi/external/cloud/hetzner.rb +402 -0
- data/lib/nvoi/external/cloud/scaleway.rb +559 -0
- data/lib/nvoi/external/cloud.rb +15 -0
- data/lib/nvoi/external/containerd.rb +86 -0
- data/lib/nvoi/external/database/mysql.rb +84 -0
- data/lib/nvoi/external/database/postgres.rb +82 -0
- data/lib/nvoi/external/database/provider.rb +65 -0
- data/lib/nvoi/external/database/sqlite.rb +72 -0
- data/lib/nvoi/external/database.rb +22 -0
- data/lib/nvoi/external/dns/cloudflare.rb +310 -0
- data/lib/nvoi/external/kubectl.rb +65 -0
- data/lib/nvoi/external/ssh.rb +106 -0
- data/lib/nvoi/objects/config_override.rb +60 -0
- data/lib/nvoi/objects/configuration.rb +483 -0
- data/lib/nvoi/objects/database.rb +56 -0
- data/lib/nvoi/objects/dns.rb +14 -0
- data/lib/nvoi/objects/firewall.rb +11 -0
- data/lib/nvoi/objects/network.rb +11 -0
- data/lib/nvoi/objects/server.rb +14 -0
- data/lib/nvoi/objects/service_spec.rb +26 -0
- data/lib/nvoi/objects/tunnel.rb +14 -0
- data/lib/nvoi/objects/volume.rb +17 -0
- data/lib/nvoi/utils/config_loader.rb +172 -0
- data/lib/nvoi/utils/constants.rb +61 -0
- data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
- data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
- data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
- data/lib/nvoi/utils/logger.rb +84 -0
- data/lib/nvoi/{config/naming.rb → utils/namer.rb} +37 -25
- data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
- data/lib/nvoi/utils/templates.rb +62 -0
- data/lib/nvoi/version.rb +1 -1
- data/lib/nvoi.rb +27 -55
- data/templates/app-ingress.yaml.erb +3 -1
- data/templates/error-backend.yaml.erb +134 -0
- metadata +121 -44
- data/examples/golang/deploy.yml +0 -54
- data/lib/nvoi/cloudflare/client.rb +0 -287
- data/lib/nvoi/config/config.rb +0 -248
- data/lib/nvoi/config/loader.rb +0 -102
- data/lib/nvoi/config/ssh_keys.rb +0 -82
- data/lib/nvoi/config/types.rb +0 -274
- data/lib/nvoi/constants.rb +0 -59
- data/lib/nvoi/credentials/editor.rb +0 -272
- data/lib/nvoi/deployer/cleaner.rb +0 -36
- data/lib/nvoi/deployer/image_builder.rb +0 -23
- data/lib/nvoi/deployer/infrastructure.rb +0 -126
- data/lib/nvoi/deployer/orchestrator.rb +0 -146
- data/lib/nvoi/deployer/service_deployer.rb +0 -311
- data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
- data/lib/nvoi/deployer/types.rb +0 -8
- data/lib/nvoi/k8s/renderer.rb +0 -44
- data/lib/nvoi/k8s/templates.rb +0 -29
- data/lib/nvoi/logger.rb +0 -72
- data/lib/nvoi/providers/aws.rb +0 -403
- data/lib/nvoi/providers/base.rb +0 -111
- data/lib/nvoi/providers/hetzner.rb +0 -288
- data/lib/nvoi/providers/hetzner_client.rb +0 -170
- data/lib/nvoi/remote/docker_manager.rb +0 -203
- data/lib/nvoi/remote/ssh_executor.rb +0 -72
- data/lib/nvoi/remote/volume_manager.rb +0 -103
- data/lib/nvoi/service/delete.rb +0 -234
- data/lib/nvoi/service/deploy.rb +0 -80
- data/lib/nvoi/service/exec.rb +0 -144
- data/lib/nvoi/service/provider.rb +0 -36
- data/lib/nvoi/steps/application_deployer.rb +0 -26
- data/lib/nvoi/steps/database_provisioner.rb +0 -60
- data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
- data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
- data/lib/nvoi/steps/server_provisioner.rb +0 -43
- data/lib/nvoi/steps/services_provisioner.rb +0 -29
- data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
- data/lib/nvoi/steps/volume_provisioner.rb +0 -154
data/lib/nvoi/errors.rb
CHANGED
|
@@ -1,67 +1,85 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nvoi
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
# Errors module
|
|
5
|
+
module Errors
|
|
6
|
+
# Base error class for all Nvoi errors
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
attr_reader :details
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
def initialize(message, details: nil)
|
|
11
|
+
@details = details
|
|
12
|
+
super(message)
|
|
13
|
+
end
|
|
11
14
|
end
|
|
12
|
-
end
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# Configuration errors
|
|
17
|
+
class ConfigError < Error; end
|
|
18
|
+
class ConfigNotFoundError < ConfigError; end
|
|
19
|
+
class ConfigValidationError < ConfigError; end
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
# Credential errors
|
|
22
|
+
class CredentialError < Error; end
|
|
23
|
+
class DecryptionError < CredentialError; end
|
|
24
|
+
class EncryptionError < CredentialError; end
|
|
25
|
+
class InvalidKeyError < CredentialError; end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
# Provider errors
|
|
28
|
+
class ProviderError < Error; end
|
|
29
|
+
class ServerCreationError < ProviderError; end
|
|
30
|
+
class NetworkError < ProviderError; end
|
|
31
|
+
class FirewallError < ProviderError; end
|
|
32
|
+
class VolumeError < ProviderError; end
|
|
33
|
+
class ValidationError < ProviderError; end
|
|
34
|
+
class ApiError < ProviderError; end
|
|
35
|
+
class AuthenticationError < ProviderError; end
|
|
36
|
+
class NotFoundError < ProviderError; end
|
|
37
|
+
class ConflictError < ProviderError; end
|
|
38
|
+
class RateLimitError < ProviderError; end
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
# Cloudflare errors
|
|
41
|
+
class CloudflareError < Error; end
|
|
42
|
+
class TunnelError < CloudflareError; end
|
|
43
|
+
class DnsError < CloudflareError; end
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
# Ssh errors
|
|
46
|
+
class SshError < Error; end
|
|
47
|
+
class SshConnectionError < SshError; end
|
|
48
|
+
class SshCommandError < SshError; end
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
attr_reader :step, :retryable
|
|
50
|
+
# Timeout errors
|
|
51
|
+
class TimeoutError < Error; end
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
super("#{step}: #{message}", details:)
|
|
54
|
-
end
|
|
53
|
+
# Deployment errors
|
|
54
|
+
class DeploymentError < Error
|
|
55
|
+
attr_reader :step, :retryable
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
def initialize(step, message, retryable: false, details: nil)
|
|
58
|
+
@step = step
|
|
59
|
+
@retryable = retryable
|
|
60
|
+
super("#{step}: #{message}", details:)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def retryable?
|
|
64
|
+
@retryable
|
|
65
|
+
end
|
|
58
66
|
end
|
|
59
|
-
end
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
# K8s errors
|
|
69
|
+
class K8sError < Error; end
|
|
70
|
+
class TemplateError < K8sError; end
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
# Service errors
|
|
73
|
+
class ServiceError < Error; end
|
|
74
|
+
|
|
75
|
+
# Database errors
|
|
76
|
+
class DatabaseError < Error
|
|
77
|
+
attr_reader :operation
|
|
78
|
+
|
|
79
|
+
def initialize(operation, message, details: nil)
|
|
80
|
+
@operation = operation
|
|
81
|
+
super("database #{operation}: #{message}", details:)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
67
85
|
end
|
|
@@ -0,0 +1,450 @@
|
|
|
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:, 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
|
+
# List available instance types for onboarding
|
|
348
|
+
def list_instance_types
|
|
349
|
+
# Common instance types (full list is huge)
|
|
350
|
+
common_types = %w[t3.micro t3.small t3.medium t3.large t3.xlarge m5.large m5.xlarge c5.large c5.xlarge]
|
|
351
|
+
resp = @client.describe_instance_types(instance_types: common_types)
|
|
352
|
+
resp.instance_types.map do |t|
|
|
353
|
+
{
|
|
354
|
+
name: t.instance_type,
|
|
355
|
+
vcpus: t.v_cpu_info.default_v_cpus,
|
|
356
|
+
memory: t.memory_info.size_in_mi_b
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
rescue StandardError
|
|
360
|
+
# Fallback to static list if API fails
|
|
361
|
+
common_types.map { |t| { name: t, vcpus: nil, memory: nil } }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# List available regions for onboarding
|
|
365
|
+
def list_regions
|
|
366
|
+
resp = @client.describe_regions
|
|
367
|
+
resp.regions.map do |r|
|
|
368
|
+
{ name: r.region_name, endpoint: r.endpoint }
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
private
|
|
373
|
+
|
|
374
|
+
def find_vpc_by_name(name)
|
|
375
|
+
resp = @client.describe_vpcs(
|
|
376
|
+
filters: [{ name: "tag:Name", values: [name] }]
|
|
377
|
+
)
|
|
378
|
+
resp.vpcs.first
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def find_security_group_by_name(name)
|
|
382
|
+
resp = @client.describe_security_groups(
|
|
383
|
+
filters: [{ name: "group-name", values: [name] }]
|
|
384
|
+
)
|
|
385
|
+
resp.security_groups.first
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def find_instance_by_name(name)
|
|
389
|
+
resp = @client.describe_instances(
|
|
390
|
+
filters: [
|
|
391
|
+
{ name: "tag:Name", values: [name] },
|
|
392
|
+
{ name: "instance-state-name", values: %w[pending running stopping stopped] }
|
|
393
|
+
]
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
resp.reservations.each do |reservation|
|
|
397
|
+
return reservation.instances.first unless reservation.instances.empty?
|
|
398
|
+
end
|
|
399
|
+
nil
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def get_ubuntu_ami
|
|
403
|
+
resp = @client.describe_images(
|
|
404
|
+
owners: ["099720109477"], # Canonical's AWS account ID
|
|
405
|
+
filters: [
|
|
406
|
+
{ name: "name", values: ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] },
|
|
407
|
+
{ name: "state", values: ["available"] }
|
|
408
|
+
]
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
raise Errors::ProviderError, "no Ubuntu 22.04 AMI found" if resp.images.empty?
|
|
412
|
+
|
|
413
|
+
latest = resp.images.max_by(&:creation_date)
|
|
414
|
+
latest.image_id
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def instance_to_server(instance)
|
|
418
|
+
name = instance.tags&.find { |t| t.key == "Name" }&.value || ""
|
|
419
|
+
|
|
420
|
+
Objects::Server::Record.new(
|
|
421
|
+
id: instance.instance_id,
|
|
422
|
+
name:,
|
|
423
|
+
status: instance.state.name,
|
|
424
|
+
public_ipv4: instance.public_ip_address,
|
|
425
|
+
private_ipv4: instance.private_ip_address
|
|
426
|
+
)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def volume_to_object(vol)
|
|
430
|
+
name = vol.tags&.find { |t| t.key == "Name" }&.value || ""
|
|
431
|
+
|
|
432
|
+
v = Objects::Volume::Record.new(
|
|
433
|
+
id: vol.volume_id,
|
|
434
|
+
name:,
|
|
435
|
+
size: vol.size,
|
|
436
|
+
location: vol.availability_zone,
|
|
437
|
+
status: vol.state
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if vol.attachments.any?
|
|
441
|
+
v.server_id = vol.attachments[0].instance_id
|
|
442
|
+
v.device_path = vol.attachments[0].device
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
v
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
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
|