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.
- 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/_target.md +79 -0
- data/.claude/todo/scaleway.impl.md +644 -0
- data/.claude/todo/scaleway.reference.md +520 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +12 -2
- 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/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 +49 -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 +100 -0
- data/lib/nvoi/cli/deploy/steps/deploy_service.rb +396 -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 +481 -0
- data/lib/nvoi/cli/exec/command.rb +173 -0
- data/lib/nvoi/cli.rb +83 -142
- 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/env.rb +32 -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/volume.rb +40 -0
- data/lib/nvoi/config_api/base.rb +44 -0
- data/lib/nvoi/config_api/result.rb +26 -0
- data/lib/nvoi/config_api.rb +70 -0
- data/lib/nvoi/errors.rb +68 -50
- data/lib/nvoi/external/cloud/aws.rb +425 -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 +376 -0
- data/lib/nvoi/external/cloud/scaleway.rb +533 -0
- data/lib/nvoi/external/cloud.rb +15 -0
- data/lib/nvoi/external/containerd.rb +82 -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 +292 -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 +463 -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} +28 -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 +10 -54
- data/templates/error-backend.yaml.erb +134 -0
- metadata +97 -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/deployer/types.rb
DELETED
data/lib/nvoi/k8s/renderer.rb
DELETED
|
@@ -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
|
data/lib/nvoi/k8s/templates.rb
DELETED
|
@@ -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
|
data/lib/nvoi/providers/aws.rb
DELETED
|
@@ -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
|
data/lib/nvoi/providers/base.rb
DELETED
|
@@ -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
|