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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
class Cli
|
|
5
|
+
module Unlock
|
|
6
|
+
# Command removes deployment lock from remote server
|
|
7
|
+
class Command
|
|
8
|
+
def initialize(options)
|
|
9
|
+
@options = options
|
|
10
|
+
@log = Nvoi.logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
config_path = resolve_config_path
|
|
15
|
+
@config = Utils::ConfigLoader.load(config_path)
|
|
16
|
+
|
|
17
|
+
# Apply branch override if specified
|
|
18
|
+
apply_branch_override if @options[:branch]
|
|
19
|
+
|
|
20
|
+
# Initialize cloud provider
|
|
21
|
+
@provider = External::Cloud.for(@config)
|
|
22
|
+
|
|
23
|
+
# Find main server
|
|
24
|
+
server = @provider.find_server(@config.server_name)
|
|
25
|
+
raise Errors::ServiceError, "server not found: #{@config.server_name}" unless server
|
|
26
|
+
|
|
27
|
+
ssh = External::Ssh.new(server.public_ipv4, @config.ssh_key_path)
|
|
28
|
+
lock_file = @config.namer.deployment_lock_file_path
|
|
29
|
+
|
|
30
|
+
# Check if lock exists and show info
|
|
31
|
+
output = ssh.execute("test -f #{lock_file} && cat #{lock_file} || echo ''").strip
|
|
32
|
+
|
|
33
|
+
if output.empty?
|
|
34
|
+
@log.info "No lock file found: %s", lock_file
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
timestamp = output.to_i
|
|
39
|
+
if timestamp > 0
|
|
40
|
+
lock_time = Time.at(timestamp)
|
|
41
|
+
age = Time.now - lock_time
|
|
42
|
+
@log.info "Lock file age: %ds (since %s)", age.round, lock_time.strftime("%H:%M:%S")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
ssh.execute("rm -f #{lock_file}")
|
|
46
|
+
@log.success "Removed lock file: %s", lock_file
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def resolve_config_path
|
|
52
|
+
config_path = @options[:config] || "deploy.enc"
|
|
53
|
+
working_dir = @options[:dir]
|
|
54
|
+
|
|
55
|
+
if config_path == "deploy.enc" && working_dir && working_dir != "."
|
|
56
|
+
File.join(working_dir, "deploy.enc")
|
|
57
|
+
else
|
|
58
|
+
config_path
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def apply_branch_override
|
|
63
|
+
branch = @options[:branch]
|
|
64
|
+
return if branch.nil? || branch.empty?
|
|
65
|
+
|
|
66
|
+
override = Objects::ConfigOverride.new(branch:)
|
|
67
|
+
override.apply(@config)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
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
|
|
@@ -92,99 +21,368 @@ module Nvoi
|
|
|
92
21
|
puts "nvoi #{VERSION}"
|
|
93
22
|
end
|
|
94
23
|
|
|
24
|
+
desc "onboard", "Interactive setup wizard"
|
|
25
|
+
def onboard
|
|
26
|
+
require_relative "cli/onboard/command"
|
|
27
|
+
Cli::Onboard::Command.new.run
|
|
28
|
+
end
|
|
29
|
+
|
|
95
30
|
desc "deploy", "Deploy application"
|
|
96
31
|
option :dockerfile_path, desc: "Path to Dockerfile (optional, defaults to ./Dockerfile)"
|
|
97
32
|
option :config_dir, desc: "Directory containing SSH keys (optional, defaults to ~/.ssh)"
|
|
98
33
|
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
|
|
34
|
+
require_relative "cli/deploy/command"
|
|
35
|
+
Cli::Deploy::Command.new(options).run
|
|
115
36
|
end
|
|
116
37
|
|
|
117
38
|
desc "delete", "Delete server, firewall, and network"
|
|
118
39
|
option :config_dir, desc: "Directory containing SSH keys (optional, defaults to ~/.ssh)"
|
|
119
40
|
def delete
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
41
|
+
require_relative "cli/delete/command"
|
|
42
|
+
Cli::Delete::Command.new(options).run
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "unlock", "Remove deployment lock (use when deploy hangs)"
|
|
46
|
+
def unlock
|
|
47
|
+
require_relative "cli/unlock/command"
|
|
48
|
+
Cli::Unlock::Command.new(options).run
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "logs APP_NAME", "Stream logs from an app"
|
|
52
|
+
option :follow, aliases: "-f", type: :boolean, default: false, desc: "Follow log output"
|
|
53
|
+
option :tail, aliases: "-n", type: :numeric, default: 100, desc: "Number of lines to show"
|
|
54
|
+
def logs(app_name)
|
|
55
|
+
require_relative "cli/logs/command"
|
|
56
|
+
Cli::Logs::Command.new(options).run(app_name)
|
|
133
57
|
end
|
|
134
58
|
|
|
135
59
|
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
60
|
option :server, default: "main", desc: "Server to execute on (main, worker-1, worker-2, etc.)"
|
|
141
61
|
option :all, type: :boolean, default: false, desc: "Execute on all servers"
|
|
142
62
|
option :interactive, aliases: "-i", type: :boolean, default: false,
|
|
143
63
|
desc: "Open interactive SSH shell instead of executing command"
|
|
144
64
|
def exec(*args)
|
|
145
|
-
|
|
146
|
-
|
|
65
|
+
require_relative "cli/exec/command"
|
|
66
|
+
Cli::Exec::Command.new(options).run(args)
|
|
67
|
+
end
|
|
147
68
|
|
|
148
|
-
|
|
69
|
+
desc "credentials SUBCOMMAND", "Manage encrypted deployment credentials"
|
|
70
|
+
subcommand "credentials", Class.new(Thor) {
|
|
71
|
+
def self.exit_on_failure?
|
|
72
|
+
true
|
|
73
|
+
end
|
|
149
74
|
|
|
150
|
-
|
|
151
|
-
|
|
75
|
+
class_option :credentials, desc: "Path to encrypted credentials file (default: deploy.enc)"
|
|
76
|
+
class_option :master_key, desc: "Path to master key file (default: deploy.key or $NVOI_MASTER_KEY)"
|
|
77
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
152
78
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
raise ArgumentError, "command required (use --interactive/-i for shell)" if args.empty?
|
|
79
|
+
desc "edit", "Edit encrypted credentials"
|
|
80
|
+
def edit
|
|
81
|
+
require_relative "cli/credentials/edit/command"
|
|
82
|
+
Nvoi::Cli::Credentials::Edit::Command.new(options).run
|
|
83
|
+
end
|
|
159
84
|
|
|
160
|
-
|
|
85
|
+
desc "show", "Show decrypted credentials"
|
|
86
|
+
def show
|
|
87
|
+
require_relative "cli/credentials/show/command"
|
|
88
|
+
Nvoi::Cli::Credentials::Show::Command.new(options).run
|
|
89
|
+
end
|
|
161
90
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
rescue StandardError => e
|
|
169
|
-
log.error "Exec failed: %s", e.message
|
|
170
|
-
raise
|
|
91
|
+
desc "set PATH VALUE", "Set a value at a dot-notation path"
|
|
92
|
+
def set(path, value)
|
|
93
|
+
require_relative "cli/credentials/edit/command"
|
|
94
|
+
Nvoi::Cli::Credentials::Edit::Command.new(options).set(path, value)
|
|
171
95
|
end
|
|
172
|
-
|
|
96
|
+
}
|
|
173
97
|
|
|
174
|
-
desc "
|
|
175
|
-
subcommand "
|
|
98
|
+
desc "config SUBCOMMAND", "Manage deployment configuration"
|
|
99
|
+
subcommand "config", Class.new(Thor) {
|
|
100
|
+
def self.exit_on_failure?
|
|
101
|
+
true
|
|
102
|
+
end
|
|
176
103
|
|
|
177
|
-
|
|
104
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
105
|
+
class_option :master_key, desc: "Path to master key file"
|
|
106
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
178
107
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
108
|
+
desc "init", "Initialize new config"
|
|
109
|
+
option :name, required: true, desc: "Application name"
|
|
110
|
+
option :environment, default: "production", desc: "Environment"
|
|
111
|
+
def init
|
|
112
|
+
require_relative "cli/config/command"
|
|
113
|
+
Nvoi::Cli::Config::Command.new(options).init(options[:name], options[:environment])
|
|
114
|
+
end
|
|
182
115
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
116
|
+
desc "provider SUBCOMMAND", "Manage compute provider"
|
|
117
|
+
subcommand "provider", Class.new(Thor) {
|
|
118
|
+
def self.exit_on_failure? = true
|
|
119
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
120
|
+
class_option :master_key, desc: "Path to master key file"
|
|
121
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
122
|
+
|
|
123
|
+
desc "set PROVIDER", "Set compute provider (hetzner, aws, scaleway)"
|
|
124
|
+
option :api_token, desc: "API token (hetzner)"
|
|
125
|
+
option :server_type, desc: "Server type (cx22, etc)"
|
|
126
|
+
option :server_location, desc: "Location (fsn1, etc)"
|
|
127
|
+
option :access_key_id, desc: "AWS access key ID"
|
|
128
|
+
option :secret_access_key, desc: "AWS secret access key"
|
|
129
|
+
option :region, desc: "AWS region"
|
|
130
|
+
option :instance_type, desc: "AWS instance type"
|
|
131
|
+
option :secret_key, desc: "Scaleway secret key"
|
|
132
|
+
option :project_id, desc: "Scaleway project ID"
|
|
133
|
+
option :zone, desc: "Scaleway zone"
|
|
134
|
+
def set(provider)
|
|
135
|
+
require_relative "cli/config/command"
|
|
136
|
+
Nvoi::Cli::Config::Command.new(options).provider_set(provider, **options.slice(
|
|
137
|
+
:api_token, :server_type, :server_location,
|
|
138
|
+
:access_key_id, :secret_access_key, :region, :instance_type,
|
|
139
|
+
:secret_key, :project_id, :zone
|
|
140
|
+
).transform_keys(&:to_sym).compact)
|
|
187
141
|
end
|
|
142
|
+
|
|
143
|
+
desc "rm", "Remove compute provider"
|
|
144
|
+
def rm
|
|
145
|
+
require_relative "cli/config/command"
|
|
146
|
+
Nvoi::Cli::Config::Command.new(options).provider_rm
|
|
147
|
+
end
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
desc "domain SUBCOMMAND", "Manage domain provider"
|
|
151
|
+
subcommand "domain", Class.new(Thor) {
|
|
152
|
+
def self.exit_on_failure? = true
|
|
153
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
154
|
+
class_option :master_key, desc: "Path to master key file"
|
|
155
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
156
|
+
|
|
157
|
+
desc "set PROVIDER", "Set domain provider (cloudflare)"
|
|
158
|
+
option :api_token, required: true, desc: "API token"
|
|
159
|
+
option :account_id, required: true, desc: "Account ID"
|
|
160
|
+
def set(provider)
|
|
161
|
+
require_relative "cli/config/command"
|
|
162
|
+
Nvoi::Cli::Config::Command.new(options).domain_set(provider, api_token: options[:api_token], account_id: options[:account_id])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
desc "rm", "Remove domain provider"
|
|
166
|
+
def rm
|
|
167
|
+
require_relative "cli/config/command"
|
|
168
|
+
Nvoi::Cli::Config::Command.new(options).domain_rm
|
|
169
|
+
end
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
desc "server SUBCOMMAND", "Manage servers"
|
|
173
|
+
subcommand "server", Class.new(Thor) {
|
|
174
|
+
def self.exit_on_failure? = true
|
|
175
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
176
|
+
class_option :master_key, desc: "Path to master key file"
|
|
177
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
178
|
+
|
|
179
|
+
desc "set NAME", "Add or update server"
|
|
180
|
+
option :master, type: :boolean, default: false, desc: "Set as master server"
|
|
181
|
+
option :type, desc: "Server type override"
|
|
182
|
+
option :location, desc: "Location override"
|
|
183
|
+
option :count, type: :numeric, default: 1, desc: "Number of servers"
|
|
184
|
+
def set(name)
|
|
185
|
+
require_relative "cli/config/command"
|
|
186
|
+
Nvoi::Cli::Config::Command.new(options).server_set(name, master: options[:master], type: options[:type], location: options[:location], count: options[:count])
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
desc "rm NAME", "Remove server"
|
|
190
|
+
def rm(name)
|
|
191
|
+
require_relative "cli/config/command"
|
|
192
|
+
Nvoi::Cli::Config::Command.new(options).server_rm(name)
|
|
193
|
+
end
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
desc "volume SUBCOMMAND", "Manage volumes"
|
|
197
|
+
subcommand "volume", Class.new(Thor) {
|
|
198
|
+
def self.exit_on_failure? = true
|
|
199
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
200
|
+
class_option :master_key, desc: "Path to master key file"
|
|
201
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
202
|
+
|
|
203
|
+
desc "set SERVER NAME", "Add or update volume"
|
|
204
|
+
option :size, type: :numeric, default: 10, desc: "Volume size in GB"
|
|
205
|
+
def set(server, name)
|
|
206
|
+
require_relative "cli/config/command"
|
|
207
|
+
Nvoi::Cli::Config::Command.new(options).volume_set(server, name, size: options[:size])
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
desc "rm SERVER NAME", "Remove volume"
|
|
211
|
+
def rm(server, name)
|
|
212
|
+
require_relative "cli/config/command"
|
|
213
|
+
Nvoi::Cli::Config::Command.new(options).volume_rm(server, name)
|
|
214
|
+
end
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
desc "app SUBCOMMAND", "Manage applications"
|
|
218
|
+
subcommand "app", Class.new(Thor) {
|
|
219
|
+
def self.exit_on_failure? = true
|
|
220
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
221
|
+
class_option :master_key, desc: "Path to master key file"
|
|
222
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
223
|
+
|
|
224
|
+
desc "set NAME", "Add or update app"
|
|
225
|
+
option :servers, type: :array, required: true, desc: "Server names to run on"
|
|
226
|
+
option :domain, desc: "Domain"
|
|
227
|
+
option :subdomain, desc: "Subdomain"
|
|
228
|
+
option :port, type: :numeric, desc: "Port"
|
|
229
|
+
option :command, desc: "Run command"
|
|
230
|
+
option :pre_run_command, desc: "Pre-run command (migrations, etc)"
|
|
231
|
+
def set(name)
|
|
232
|
+
require_relative "cli/config/command"
|
|
233
|
+
Nvoi::Cli::Config::Command.new(options).app_set(name, **options.slice(:servers, :domain, :subdomain, :port, :command, :pre_run_command).transform_keys(&:to_sym).compact)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
desc "rm NAME", "Remove app"
|
|
237
|
+
def rm(name)
|
|
238
|
+
require_relative "cli/config/command"
|
|
239
|
+
Nvoi::Cli::Config::Command.new(options).app_rm(name)
|
|
240
|
+
end
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
desc "database SUBCOMMAND", "Manage database"
|
|
244
|
+
subcommand "database", Class.new(Thor) {
|
|
245
|
+
def self.exit_on_failure? = true
|
|
246
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
247
|
+
class_option :master_key, desc: "Path to master key file"
|
|
248
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
249
|
+
|
|
250
|
+
desc "set", "Set database configuration"
|
|
251
|
+
option :servers, type: :array, required: true, desc: "Server names"
|
|
252
|
+
option :adapter, required: true, desc: "Database adapter (postgres, mysql, sqlite3)"
|
|
253
|
+
option :user, desc: "Database user"
|
|
254
|
+
option :password, desc: "Database password"
|
|
255
|
+
option :database, desc: "Database name"
|
|
256
|
+
option :url, desc: "Database URL (alternative to user/pass/db)"
|
|
257
|
+
option :image, desc: "Custom Docker image"
|
|
258
|
+
def set
|
|
259
|
+
require_relative "cli/config/command"
|
|
260
|
+
Nvoi::Cli::Config::Command.new(options).database_set(**options.slice(:servers, :adapter, :user, :password, :database, :url, :image).transform_keys(&:to_sym).compact)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
desc "rm", "Remove database"
|
|
264
|
+
def rm
|
|
265
|
+
require_relative "cli/config/command"
|
|
266
|
+
Nvoi::Cli::Config::Command.new(options).database_rm
|
|
267
|
+
end
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
desc "service SUBCOMMAND", "Manage services (redis, etc)"
|
|
271
|
+
subcommand "service", Class.new(Thor) {
|
|
272
|
+
def self.exit_on_failure? = true
|
|
273
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
274
|
+
class_option :master_key, desc: "Path to master key file"
|
|
275
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
276
|
+
|
|
277
|
+
desc "set NAME", "Add or update service"
|
|
278
|
+
option :servers, type: :array, required: true, desc: "Server names"
|
|
279
|
+
option :image, required: true, desc: "Docker image"
|
|
280
|
+
option :port, type: :numeric, desc: "Port"
|
|
281
|
+
option :command, desc: "Command"
|
|
282
|
+
def set(name)
|
|
283
|
+
require_relative "cli/config/command"
|
|
284
|
+
Nvoi::Cli::Config::Command.new(options).service_set(name, **options.slice(:servers, :image, :port, :command).transform_keys(&:to_sym).compact)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
desc "rm NAME", "Remove service"
|
|
288
|
+
def rm(name)
|
|
289
|
+
require_relative "cli/config/command"
|
|
290
|
+
Nvoi::Cli::Config::Command.new(options).service_rm(name)
|
|
291
|
+
end
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
desc "secret SUBCOMMAND", "Manage secrets"
|
|
295
|
+
subcommand "secret", Class.new(Thor) {
|
|
296
|
+
def self.exit_on_failure? = true
|
|
297
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
298
|
+
class_option :master_key, desc: "Path to master key file"
|
|
299
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
300
|
+
|
|
301
|
+
desc "set KEY VALUE", "Set secret"
|
|
302
|
+
def set(key, value)
|
|
303
|
+
require_relative "cli/config/command"
|
|
304
|
+
Nvoi::Cli::Config::Command.new(options).secret_set(key, value)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
desc "rm KEY", "Remove secret"
|
|
308
|
+
def rm(key)
|
|
309
|
+
require_relative "cli/config/command"
|
|
310
|
+
Nvoi::Cli::Config::Command.new(options).secret_rm(key)
|
|
311
|
+
end
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
desc "env SUBCOMMAND", "Manage environment variables"
|
|
315
|
+
subcommand "env", Class.new(Thor) {
|
|
316
|
+
def self.exit_on_failure? = true
|
|
317
|
+
class_option :credentials, desc: "Path to encrypted config file"
|
|
318
|
+
class_option :master_key, desc: "Path to master key file"
|
|
319
|
+
class_option :dir, aliases: "-d", default: ".", desc: "Working directory"
|
|
320
|
+
|
|
321
|
+
desc "set KEY VALUE", "Set environment variable"
|
|
322
|
+
def set(key, value)
|
|
323
|
+
require_relative "cli/config/command"
|
|
324
|
+
Nvoi::Cli::Config::Command.new(options).env_set(key, value)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
desc "rm KEY", "Remove environment variable"
|
|
328
|
+
def rm(key)
|
|
329
|
+
require_relative "cli/config/command"
|
|
330
|
+
Nvoi::Cli::Config::Command.new(options).env_rm(key)
|
|
331
|
+
end
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
desc "db SUBCOMMAND", "Database operations"
|
|
336
|
+
subcommand "db", Class.new(Thor) {
|
|
337
|
+
def self.exit_on_failure?
|
|
338
|
+
true
|
|
188
339
|
end
|
|
340
|
+
|
|
341
|
+
class_option :config, aliases: "-c", default: "deploy.enc",
|
|
342
|
+
desc: "Path to deployment configuration file"
|
|
343
|
+
class_option :dir, aliases: "-d", default: ".",
|
|
344
|
+
desc: "Working directory"
|
|
345
|
+
class_option :branch, aliases: "-b",
|
|
346
|
+
desc: "Branch name for isolated deployments"
|
|
347
|
+
|
|
348
|
+
desc "branch SUBCOMMAND", "Database branch operations"
|
|
349
|
+
subcommand "branch", Class.new(Thor) {
|
|
350
|
+
def self.exit_on_failure?
|
|
351
|
+
true
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
class_option :config, aliases: "-c", default: "deploy.enc",
|
|
355
|
+
desc: "Path to deployment configuration file"
|
|
356
|
+
class_option :dir, aliases: "-d", default: ".",
|
|
357
|
+
desc: "Working directory"
|
|
358
|
+
class_option :branch, aliases: "-b",
|
|
359
|
+
desc: "Branch name for isolated deployments"
|
|
360
|
+
|
|
361
|
+
desc "create [NAME]", "Create a new database branch (snapshot)"
|
|
362
|
+
def create(name = nil)
|
|
363
|
+
require_relative "cli/db/command"
|
|
364
|
+
Nvoi::Cli::Db::Command.new(options).branch_create(name)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
desc "list", "List all database branches"
|
|
368
|
+
def list
|
|
369
|
+
require_relative "cli/db/command"
|
|
370
|
+
Nvoi::Cli::Db::Command.new(options).branch_list
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
desc "restore ID [NEW_DB_NAME]", "Restore a database branch to a new database"
|
|
374
|
+
def restore(branch_id, new_db_name = nil)
|
|
375
|
+
require_relative "cli/db/command"
|
|
376
|
+
Nvoi::Cli::Db::Command.new(options).branch_restore(branch_id, new_db_name)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
desc "download ID", "Download a database branch dump"
|
|
380
|
+
option :path, aliases: "-p", desc: "Output file path (default: {branch_id}.sql)"
|
|
381
|
+
def download(branch_id)
|
|
382
|
+
require_relative "cli/db/command"
|
|
383
|
+
Nvoi::Cli::Db::Command.new(options).branch_download(branch_id)
|
|
384
|
+
end
|
|
385
|
+
}
|
|
386
|
+
}
|
|
189
387
|
end
|
|
190
388
|
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
|