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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
class Cli
|
|
5
|
+
module Exec
|
|
6
|
+
# Command handles remote command execution on servers
|
|
7
|
+
class Command
|
|
8
|
+
def initialize(options)
|
|
9
|
+
@options = options
|
|
10
|
+
@log = Nvoi.logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(args)
|
|
14
|
+
@log.info "Exec CLI %s", VERSION
|
|
15
|
+
|
|
16
|
+
# Load configuration
|
|
17
|
+
config_path = resolve_config_path
|
|
18
|
+
@config = Utils::ConfigLoader.load(config_path)
|
|
19
|
+
|
|
20
|
+
# Apply branch override if specified
|
|
21
|
+
apply_branch_override if @options[:branch]
|
|
22
|
+
|
|
23
|
+
# Initialize cloud provider
|
|
24
|
+
@provider = External::Cloud.for(@config)
|
|
25
|
+
|
|
26
|
+
if @options[:interactive]
|
|
27
|
+
@log.warning "Ignoring command arguments in interactive mode" unless args.empty?
|
|
28
|
+
@log.warning "Ignoring --all flag in interactive mode" if @options[:all]
|
|
29
|
+
open_shell(@options[:server])
|
|
30
|
+
else
|
|
31
|
+
raise ArgumentError, "command required (use --interactive/-i for shell)" if args.empty?
|
|
32
|
+
|
|
33
|
+
command = args.join(" ")
|
|
34
|
+
|
|
35
|
+
if @options[:all]
|
|
36
|
+
run_all(command)
|
|
37
|
+
else
|
|
38
|
+
run_on_server(command, @options[:server])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def resolve_config_path
|
|
46
|
+
config_path = @options[:config] || "deploy.enc"
|
|
47
|
+
working_dir = @options[:dir]
|
|
48
|
+
|
|
49
|
+
if config_path == "deploy.enc" && working_dir && working_dir != "."
|
|
50
|
+
File.join(working_dir, "deploy.enc")
|
|
51
|
+
else
|
|
52
|
+
config_path
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def apply_branch_override
|
|
57
|
+
branch = @options[:branch]
|
|
58
|
+
return if branch.nil? || branch.empty?
|
|
59
|
+
|
|
60
|
+
override = Objects::ConfigOverride.new(branch:)
|
|
61
|
+
override.apply(@config)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run_on_server(command, server_name)
|
|
65
|
+
actual_server_name = resolve_server_name(server_name)
|
|
66
|
+
server = find_server(actual_server_name)
|
|
67
|
+
|
|
68
|
+
@log.info "Connecting to %s (%s)", actual_server_name, server.public_ipv4
|
|
69
|
+
|
|
70
|
+
ssh = External::Ssh.new(server.public_ipv4, @config.ssh_key_path)
|
|
71
|
+
|
|
72
|
+
@log.info "Executing: %s", command
|
|
73
|
+
output = ssh.execute(command, stream: true)
|
|
74
|
+
|
|
75
|
+
puts output if !output.empty? && !output.include?("\n")
|
|
76
|
+
|
|
77
|
+
@log.success "Command completed successfully"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run_all(command)
|
|
81
|
+
server_names = get_all_server_names
|
|
82
|
+
|
|
83
|
+
raise Errors::ServiceError, "no servers found in configuration" if server_names.empty?
|
|
84
|
+
|
|
85
|
+
@log.info "Executing on %d server(s): %s", server_names.size, server_names.join(", ")
|
|
86
|
+
@log.separator
|
|
87
|
+
|
|
88
|
+
results = {}
|
|
89
|
+
mutex = Mutex.new
|
|
90
|
+
threads = server_names.map do |name|
|
|
91
|
+
Thread.new do
|
|
92
|
+
begin
|
|
93
|
+
server = find_server(name)
|
|
94
|
+
ssh = External::Ssh.new(server.public_ipv4, @config.ssh_key_path)
|
|
95
|
+
|
|
96
|
+
@log.info "[%s] Executing...", name
|
|
97
|
+
output = ssh.execute(command)
|
|
98
|
+
|
|
99
|
+
output.strip.split("\n").each do |line|
|
|
100
|
+
puts "[#{name}] #{line}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
mutex.synchronize { results[name] = nil }
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
@log.error "[%s] Failed: %s", name, e.message
|
|
106
|
+
mutex.synchronize { results[name] = e }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
threads.each(&:join)
|
|
112
|
+
|
|
113
|
+
@log.separator
|
|
114
|
+
|
|
115
|
+
failures = results.select { |_, err| err }.keys
|
|
116
|
+
if failures.any?
|
|
117
|
+
@log.warning "Command failed on %d server(s): %s", failures.size, failures.join(", ")
|
|
118
|
+
raise Errors::ServiceError, "command failed on some servers"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@log.success "Command completed successfully on all servers"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def open_shell(server_name)
|
|
125
|
+
actual_server_name = resolve_server_name(server_name)
|
|
126
|
+
server = find_server(actual_server_name)
|
|
127
|
+
|
|
128
|
+
@log.info "Opening SSH shell to %s (%s)", actual_server_name, server.public_ipv4
|
|
129
|
+
|
|
130
|
+
ssh = External::Ssh.new(server.public_ipv4, @config.ssh_key_path)
|
|
131
|
+
ssh.open_shell
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def resolve_server_name(name)
|
|
135
|
+
return @config.server_name if name.nil? || name.empty? || name == "main"
|
|
136
|
+
|
|
137
|
+
parts = name.split("-")
|
|
138
|
+
if parts.length >= 2
|
|
139
|
+
num_str = parts.last
|
|
140
|
+
if num_str.match?(/^\d+$/)
|
|
141
|
+
group_name = parts[0...-1].join("-")
|
|
142
|
+
return @config.namer.server_name(group_name, num_str.to_i)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
@config.namer.server_name(name, 1)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def get_all_server_names
|
|
150
|
+
names = []
|
|
151
|
+
|
|
152
|
+
@config.deploy.application.servers.each do |group_name, group_config|
|
|
153
|
+
next unless group_config
|
|
154
|
+
|
|
155
|
+
count = group_config.count.positive? ? group_config.count : 1
|
|
156
|
+
(1..count).each do |i|
|
|
157
|
+
names << @config.namer.server_name(group_name, i)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
names
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def find_server(server_name)
|
|
165
|
+
server = @provider.find_server(server_name)
|
|
166
|
+
raise Errors::ServiceError, "server not found: #{server_name}" unless server
|
|
167
|
+
|
|
168
|
+
server
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/nvoi/cli.rb
CHANGED
|
@@ -3,85 +3,14 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
|
|
5
5
|
module Nvoi
|
|
6
|
-
#
|
|
7
|
-
class
|
|
8
|
-
class_option :
|
|
9
|
-
class_option :master_key, desc: "Path to master key file (default: deploy.key or $NVOI_MASTER_KEY)"
|
|
10
|
-
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
11
|
-
|
|
12
|
-
def self.exit_on_failure?
|
|
13
|
-
true
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
desc "edit", "Edit encrypted credentials"
|
|
17
|
-
long_desc <<~DESC
|
|
18
|
-
Decrypt credentials, open in $EDITOR, validate, and re-encrypt.
|
|
19
|
-
|
|
20
|
-
On first run, generates a new master key and creates deploy.key (git-ignored).
|
|
21
|
-
The master key can also be provided via $NVOI_MASTER_KEY environment variable.
|
|
22
|
-
DESC
|
|
23
|
-
def edit
|
|
24
|
-
log = Nvoi.logger
|
|
25
|
-
log.info "Credentials Editor"
|
|
26
|
-
|
|
27
|
-
working_dir = resolve_working_dir
|
|
28
|
-
|
|
29
|
-
enc_path = options[:credentials]
|
|
30
|
-
enc_path = File.join(working_dir, Credentials::DEFAULT_ENCRYPTED_FILE) if enc_path.nil? || enc_path.empty?
|
|
31
|
-
|
|
32
|
-
if File.exist?(enc_path)
|
|
33
|
-
# Existing file: load manager
|
|
34
|
-
manager = Credentials::Manager.new(working_dir, options[:credentials], options[:master_key])
|
|
35
|
-
else
|
|
36
|
-
# First time: initialize
|
|
37
|
-
log.info "Creating new encrypted credentials file"
|
|
38
|
-
manager = Credentials::Manager.for_init(working_dir)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
editor = Credentials::Editor.new(manager)
|
|
42
|
-
editor.edit
|
|
43
|
-
|
|
44
|
-
# Update .gitignore on first run
|
|
45
|
-
if manager.key_path
|
|
46
|
-
begin
|
|
47
|
-
manager.update_gitignore
|
|
48
|
-
log.info "Added %s to .gitignore", Credentials::DEFAULT_KEY_FILE
|
|
49
|
-
rescue StandardError => e
|
|
50
|
-
log.warning "Failed to update .gitignore: %s", e.message
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
log.success "Master key saved to: %s", manager.key_path
|
|
54
|
-
log.warning "Keep this key safe! You cannot decrypt credentials without it."
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
desc "show", "Display decrypted credentials"
|
|
59
|
-
long_desc "Decrypt and print credentials to stdout. Useful for debugging or piping to other tools."
|
|
60
|
-
def show
|
|
61
|
-
working_dir = resolve_working_dir
|
|
62
|
-
manager = Credentials::Manager.new(working_dir, options[:credentials], options[:master_key])
|
|
63
|
-
editor = Credentials::Editor.new(manager)
|
|
64
|
-
editor.show
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
def resolve_working_dir
|
|
70
|
-
wd = options[:dir]
|
|
71
|
-
if wd.nil? || wd.empty? || wd == "."
|
|
72
|
-
Dir.pwd
|
|
73
|
-
else
|
|
74
|
-
File.expand_path(wd)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Main CLI for nvoi commands
|
|
80
|
-
class CLI < Thor
|
|
81
|
-
class_option :config, aliases: "-c", default: Constants::DEFAULT_CONFIG_FILE,
|
|
6
|
+
# Main CLI for nvoi commands - Thor routing only
|
|
7
|
+
class Cli < Thor
|
|
8
|
+
class_option :config, aliases: "-c", default: "deploy.enc",
|
|
82
9
|
desc: "Path to deployment configuration file"
|
|
83
10
|
class_option :dir, aliases: "-d", default: ".",
|
|
84
11
|
desc: "Working directory containing the application code"
|
|
12
|
+
class_option :branch, aliases: "-b",
|
|
13
|
+
desc: "Branch name for isolated deployments (prefixes app name and subdomains)"
|
|
85
14
|
|
|
86
15
|
def self.exit_on_failure?
|
|
87
16
|
true
|
|
@@ -96,95 +25,107 @@ module Nvoi
|
|
|
96
25
|
option :dockerfile_path, desc: "Path to Dockerfile (optional, defaults to ./Dockerfile)"
|
|
97
26
|
option :config_dir, desc: "Directory containing SSH keys (optional, defaults to ~/.ssh)"
|
|
98
27
|
def deploy
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
config_path = resolve_config_path
|
|
103
|
-
working_dir = options[:dir]
|
|
104
|
-
dockerfile_path = options[:dockerfile_path] || File.join(working_dir, "Dockerfile")
|
|
105
|
-
|
|
106
|
-
begin
|
|
107
|
-
svc = Service::DeployService.new(config_path, working_dir, log)
|
|
108
|
-
svc.config_dir = options[:config_dir] if options[:config_dir]
|
|
109
|
-
svc.dockerfile_path = dockerfile_path
|
|
110
|
-
svc.run
|
|
111
|
-
rescue StandardError => e
|
|
112
|
-
log.error "Deployment failed: %s", e.message
|
|
113
|
-
raise
|
|
114
|
-
end
|
|
28
|
+
require_relative "cli/deploy/command"
|
|
29
|
+
Cli::Deploy::Command.new(options).run
|
|
115
30
|
end
|
|
116
31
|
|
|
117
32
|
desc "delete", "Delete server, firewall, and network"
|
|
118
33
|
option :config_dir, desc: "Directory containing SSH keys (optional, defaults to ~/.ssh)"
|
|
119
34
|
def delete
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
config_path = resolve_config_path
|
|
124
|
-
|
|
125
|
-
begin
|
|
126
|
-
svc = Service::DeleteService.new(config_path, log)
|
|
127
|
-
svc.config_dir = options[:config_dir] if options[:config_dir]
|
|
128
|
-
svc.run
|
|
129
|
-
rescue StandardError => e
|
|
130
|
-
log.error "Delete failed: %s", e.message
|
|
131
|
-
raise
|
|
132
|
-
end
|
|
35
|
+
require_relative "cli/delete/command"
|
|
36
|
+
Cli::Delete::Command.new(options).run
|
|
133
37
|
end
|
|
134
38
|
|
|
135
39
|
desc "exec [COMMAND...]", "Execute command on remote server or open interactive shell"
|
|
136
|
-
long_desc <<~DESC
|
|
137
|
-
Execute arbitrary bash commands on remote servers using existing configuration,
|
|
138
|
-
or open an interactive SSH shell with --interactive flag.
|
|
139
|
-
DESC
|
|
140
40
|
option :server, default: "main", desc: "Server to execute on (main, worker-1, worker-2, etc.)"
|
|
141
41
|
option :all, type: :boolean, default: false, desc: "Execute on all servers"
|
|
142
42
|
option :interactive, aliases: "-i", type: :boolean, default: false,
|
|
143
43
|
desc: "Open interactive SSH shell instead of executing command"
|
|
144
44
|
def exec(*args)
|
|
145
|
-
|
|
146
|
-
|
|
45
|
+
require_relative "cli/exec/command"
|
|
46
|
+
Cli::Exec::Command.new(options).run(args)
|
|
47
|
+
end
|
|
147
48
|
|
|
148
|
-
|
|
49
|
+
desc "credentials SUBCOMMAND", "Manage encrypted deployment credentials"
|
|
50
|
+
subcommand "credentials", Class.new(Thor) {
|
|
51
|
+
def self.exit_on_failure?
|
|
52
|
+
true
|
|
53
|
+
end
|
|
149
54
|
|
|
150
|
-
|
|
151
|
-
|
|
55
|
+
class_option :credentials, desc: "Path to encrypted credentials file (default: deploy.enc)"
|
|
56
|
+
class_option :master_key, desc: "Path to master key file (default: deploy.key or $NVOI_MASTER_KEY)"
|
|
57
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
152
58
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
raise ArgumentError, "command required (use --interactive/-i for shell)" if args.empty?
|
|
59
|
+
desc "edit", "Edit encrypted credentials"
|
|
60
|
+
def edit
|
|
61
|
+
require_relative "cli/credentials/edit/command"
|
|
62
|
+
Nvoi::Cli::Credentials::Edit::Command.new(options).run
|
|
63
|
+
end
|
|
159
64
|
|
|
160
|
-
|
|
65
|
+
desc "show", "Show decrypted credentials"
|
|
66
|
+
def show
|
|
67
|
+
require_relative "cli/credentials/show/command"
|
|
68
|
+
Nvoi::Cli::Credentials::Show::Command.new(options).run
|
|
69
|
+
end
|
|
161
70
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
rescue StandardError => e
|
|
169
|
-
log.error "Exec failed: %s", e.message
|
|
170
|
-
raise
|
|
71
|
+
desc "set PATH VALUE", "Set a value at a dot-notation path"
|
|
72
|
+
def set(path, value)
|
|
73
|
+
require_relative "cli/credentials/edit/command"
|
|
74
|
+
Nvoi::Cli::Credentials::Edit::Command.new(options).set(path, value)
|
|
171
75
|
end
|
|
172
|
-
|
|
76
|
+
}
|
|
173
77
|
|
|
174
|
-
desc "
|
|
175
|
-
subcommand "
|
|
78
|
+
desc "db SUBCOMMAND", "Database operations"
|
|
79
|
+
subcommand "db", Class.new(Thor) {
|
|
80
|
+
def self.exit_on_failure?
|
|
81
|
+
true
|
|
82
|
+
end
|
|
176
83
|
|
|
177
|
-
|
|
84
|
+
class_option :config, aliases: "-c", default: "deploy.enc",
|
|
85
|
+
desc: "Path to deployment configuration file"
|
|
86
|
+
class_option :dir, aliases: "-d", default: ".",
|
|
87
|
+
desc: "Working directory"
|
|
88
|
+
class_option :branch, aliases: "-b",
|
|
89
|
+
desc: "Branch name for isolated deployments"
|
|
90
|
+
|
|
91
|
+
desc "branch SUBCOMMAND", "Database branch operations"
|
|
92
|
+
subcommand "branch", Class.new(Thor) {
|
|
93
|
+
def self.exit_on_failure?
|
|
94
|
+
true
|
|
95
|
+
end
|
|
178
96
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
97
|
+
class_option :config, aliases: "-c", default: "deploy.enc",
|
|
98
|
+
desc: "Path to deployment configuration file"
|
|
99
|
+
class_option :dir, aliases: "-d", default: ".",
|
|
100
|
+
desc: "Working directory"
|
|
101
|
+
class_option :branch, aliases: "-b",
|
|
102
|
+
desc: "Branch name for isolated deployments"
|
|
103
|
+
|
|
104
|
+
desc "create [NAME]", "Create a new database branch (snapshot)"
|
|
105
|
+
def create(name = nil)
|
|
106
|
+
require_relative "cli/db/command"
|
|
107
|
+
Nvoi::Cli::Db::Command.new(options).branch_create(name)
|
|
108
|
+
end
|
|
182
109
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
110
|
+
desc "list", "List all database branches"
|
|
111
|
+
def list
|
|
112
|
+
require_relative "cli/db/command"
|
|
113
|
+
Nvoi::Cli::Db::Command.new(options).branch_list
|
|
187
114
|
end
|
|
188
|
-
|
|
115
|
+
|
|
116
|
+
desc "restore ID [NEW_DB_NAME]", "Restore a database branch to a new database"
|
|
117
|
+
def restore(branch_id, new_db_name = nil)
|
|
118
|
+
require_relative "cli/db/command"
|
|
119
|
+
Nvoi::Cli::Db::Command.new(options).branch_restore(branch_id, new_db_name)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
desc "download ID", "Download a database branch dump"
|
|
123
|
+
option :path, aliases: "-p", desc: "Output file path (default: {branch_id}.sql)"
|
|
124
|
+
def download(branch_id)
|
|
125
|
+
require_relative "cli/db/command"
|
|
126
|
+
Nvoi::Cli::Db::Command.new(options).branch_download(branch_id)
|
|
127
|
+
end
|
|
128
|
+
}
|
|
129
|
+
}
|
|
189
130
|
end
|
|
190
131
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class SetApp < Base
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def mutate(data, name:, servers:, domain: nil, subdomain: nil, port: nil, command: nil, pre_run_command: nil, env: nil, mounts: nil)
|
|
10
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
11
|
+
raise ArgumentError, "servers is required" if servers.nil? || servers.empty?
|
|
12
|
+
raise ArgumentError, "servers must be an array" unless servers.is_a?(Array)
|
|
13
|
+
|
|
14
|
+
validate_server_refs(data, servers)
|
|
15
|
+
|
|
16
|
+
app(data)["app"] ||= {}
|
|
17
|
+
app(data)["app"][name.to_s] = {
|
|
18
|
+
"servers" => servers.map(&:to_s),
|
|
19
|
+
"domain" => domain,
|
|
20
|
+
"subdomain" => subdomain,
|
|
21
|
+
"port" => port,
|
|
22
|
+
"command" => command,
|
|
23
|
+
"pre_run_command" => pre_run_command,
|
|
24
|
+
"env" => env,
|
|
25
|
+
"mounts" => mounts
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def validate_server_refs(data, servers)
|
|
32
|
+
defined = (app(data)["servers"] || {}).keys
|
|
33
|
+
servers.each do |ref|
|
|
34
|
+
raise Errors::ConfigValidationError, "server '#{ref}' not found" unless defined.include?(ref.to_s)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class DeleteApp < Base
|
|
40
|
+
protected
|
|
41
|
+
|
|
42
|
+
def mutate(data, name:)
|
|
43
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
44
|
+
|
|
45
|
+
apps = app(data)["app"] || {}
|
|
46
|
+
raise Errors::ConfigValidationError, "app '#{name}' not found" unless apps.key?(name.to_s)
|
|
47
|
+
|
|
48
|
+
apps.delete(name.to_s)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class SetComputeProvider < Base
|
|
7
|
+
PROVIDERS = %w[hetzner aws scaleway].freeze
|
|
8
|
+
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def mutate(data, provider:, **opts)
|
|
12
|
+
raise ArgumentError, "provider is required" if provider.nil? || provider.to_s.empty?
|
|
13
|
+
raise ArgumentError, "provider must be one of: #{PROVIDERS.join(', ')}" unless PROVIDERS.include?(provider.to_s)
|
|
14
|
+
|
|
15
|
+
app(data)["compute_provider"] = { provider.to_s => build_config(provider.to_s, opts) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def build_config(provider, opts)
|
|
21
|
+
case provider
|
|
22
|
+
when "hetzner"
|
|
23
|
+
{
|
|
24
|
+
"api_token" => opts[:api_token],
|
|
25
|
+
"server_type" => opts[:server_type],
|
|
26
|
+
"server_location" => opts[:server_location]
|
|
27
|
+
}.compact
|
|
28
|
+
when "aws"
|
|
29
|
+
{
|
|
30
|
+
"access_key_id" => opts[:access_key_id],
|
|
31
|
+
"secret_access_key" => opts[:secret_access_key],
|
|
32
|
+
"region" => opts[:region],
|
|
33
|
+
"instance_type" => opts[:instance_type]
|
|
34
|
+
}.compact
|
|
35
|
+
when "scaleway"
|
|
36
|
+
{
|
|
37
|
+
"secret_key" => opts[:secret_key],
|
|
38
|
+
"project_id" => opts[:project_id],
|
|
39
|
+
"zone" => opts[:zone],
|
|
40
|
+
"server_type" => opts[:server_type]
|
|
41
|
+
}.compact
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class DeleteComputeProvider < Base
|
|
47
|
+
protected
|
|
48
|
+
|
|
49
|
+
def mutate(data, **)
|
|
50
|
+
app(data)["compute_provider"] = {}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class SetDatabase < Base
|
|
7
|
+
ADAPTERS = %w[postgres postgresql mysql sqlite sqlite3].freeze
|
|
8
|
+
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def mutate(data, servers:, adapter:, image: nil, url: nil, user: nil, password: nil, database: nil, mount: nil, path: nil)
|
|
12
|
+
raise ArgumentError, "servers is required" if servers.nil? || servers.empty?
|
|
13
|
+
raise ArgumentError, "servers must be an array" unless servers.is_a?(Array)
|
|
14
|
+
raise ArgumentError, "adapter is required" if adapter.nil? || adapter.to_s.empty?
|
|
15
|
+
raise ArgumentError, "adapter must be one of: #{ADAPTERS.join(', ')}" unless ADAPTERS.include?(adapter.to_s.downcase)
|
|
16
|
+
|
|
17
|
+
validate_server_refs(data, servers)
|
|
18
|
+
|
|
19
|
+
secrets = build_secrets(adapter, user, password, database)
|
|
20
|
+
|
|
21
|
+
app(data)["database"] = {
|
|
22
|
+
"servers" => servers.map(&:to_s),
|
|
23
|
+
"adapter" => adapter.to_s,
|
|
24
|
+
"image" => image,
|
|
25
|
+
"url" => url,
|
|
26
|
+
"secrets" => secrets.empty? ? nil : secrets,
|
|
27
|
+
"mount" => mount,
|
|
28
|
+
"path" => path
|
|
29
|
+
}.compact
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def validate_server_refs(data, servers)
|
|
35
|
+
defined = (app(data)["servers"] || {}).keys
|
|
36
|
+
servers.each do |ref|
|
|
37
|
+
raise Errors::ConfigValidationError, "server '#{ref}' not found" unless defined.include?(ref.to_s)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_secrets(adapter, user, password, database)
|
|
42
|
+
case adapter.to_s.downcase
|
|
43
|
+
when "postgres", "postgresql"
|
|
44
|
+
{
|
|
45
|
+
"POSTGRES_USER" => user,
|
|
46
|
+
"POSTGRES_PASSWORD" => password,
|
|
47
|
+
"POSTGRES_DB" => database
|
|
48
|
+
}.compact
|
|
49
|
+
when "mysql"
|
|
50
|
+
{
|
|
51
|
+
"MYSQL_USER" => user,
|
|
52
|
+
"MYSQL_PASSWORD" => password,
|
|
53
|
+
"MYSQL_DATABASE" => database
|
|
54
|
+
}.compact
|
|
55
|
+
else
|
|
56
|
+
{}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class DeleteDatabase < Base
|
|
62
|
+
protected
|
|
63
|
+
|
|
64
|
+
def mutate(data, **)
|
|
65
|
+
app(data).delete("database")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class SetEnv < Base
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def mutate(data, key:, value:)
|
|
10
|
+
raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
|
|
11
|
+
raise ArgumentError, "value is required" if value.nil?
|
|
12
|
+
|
|
13
|
+
app(data)["env"] ||= {}
|
|
14
|
+
app(data)["env"][key.to_s] = value.to_s
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class DeleteEnv < Base
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def mutate(data, key:)
|
|
22
|
+
raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
|
|
23
|
+
|
|
24
|
+
env = app(data)["env"] || {}
|
|
25
|
+
raise Errors::ConfigValidationError, "env '#{key}' not found" unless env.key?(key.to_s)
|
|
26
|
+
|
|
27
|
+
env.delete(key.to_s)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class SetSecret < Base
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def mutate(data, key:, value:)
|
|
10
|
+
raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
|
|
11
|
+
raise ArgumentError, "value is required" if value.nil?
|
|
12
|
+
|
|
13
|
+
app(data)["secrets"] ||= {}
|
|
14
|
+
app(data)["secrets"][key.to_s] = value.to_s
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class DeleteSecret < Base
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def mutate(data, key:)
|
|
22
|
+
raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
|
|
23
|
+
|
|
24
|
+
secrets = app(data)["secrets"] || {}
|
|
25
|
+
raise Errors::ConfigValidationError, "secret '#{key}' not found" unless secrets.key?(key.to_s)
|
|
26
|
+
|
|
27
|
+
secrets.delete(key.to_s)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|