nvoi 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/todo/refactor/00-overview.md +171 -0
  3. data/.claude/todo/refactor/01-objects.md +96 -0
  4. data/.claude/todo/refactor/02-utils.md +143 -0
  5. data/.claude/todo/refactor/03-external-cloud.md +164 -0
  6. data/.claude/todo/refactor/04-external-dns.md +104 -0
  7. data/.claude/todo/refactor/05-external.md +133 -0
  8. data/.claude/todo/refactor/06-cli.md +123 -0
  9. data/.claude/todo/refactor/07-cli-deploy-command.md +177 -0
  10. data/.claude/todo/refactor/08-cli-deploy-steps.md +201 -0
  11. data/.claude/todo/refactor/09-cli-delete-command.md +169 -0
  12. data/.claude/todo/refactor/10-cli-exec-command.md +157 -0
  13. data/.claude/todo/refactor/11-cli-credentials-command.md +190 -0
  14. data/.claude/todo/refactor/12-cli-db-command.md +128 -0
  15. data/.claude/todo/refactor/_target.md +79 -0
  16. data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
  17. data/.claude/todo/refactor-execution/01-objects.md +42 -0
  18. data/.claude/todo/refactor-execution/02-utils.md +41 -0
  19. data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
  20. data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
  21. data/.claude/todo/refactor-execution/05-external-other.md +46 -0
  22. data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
  23. data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
  24. data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
  25. data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
  26. data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
  27. data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
  28. data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
  29. data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
  30. data/.claude/todo/scaleway.impl.md +644 -0
  31. data/.claude/todo/scaleway.reference.md +520 -0
  32. data/.claude/todos.md +550 -0
  33. data/Gemfile +6 -0
  34. data/Gemfile.lock +46 -5
  35. data/Rakefile +1 -1
  36. data/doc/config-schema.yaml +44 -11
  37. data/examples/golang/deploy.enc +0 -0
  38. data/examples/golang/main.go +18 -0
  39. data/exe/nvoi +3 -1
  40. data/ingest +0 -0
  41. data/lib/nvoi/cli/config/command.rb +219 -0
  42. data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
  43. data/lib/nvoi/cli/credentials/show/command.rb +35 -0
  44. data/lib/nvoi/cli/db/command.rb +308 -0
  45. data/lib/nvoi/cli/delete/command.rb +75 -0
  46. data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
  47. data/lib/nvoi/cli/delete/steps/teardown_dns.rb +50 -0
  48. data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
  49. data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
  50. data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
  51. data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
  52. data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
  53. data/lib/nvoi/cli/deploy/command.rb +184 -0
  54. data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
  55. data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
  56. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +102 -0
  57. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +399 -0
  58. data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
  59. data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
  60. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
  61. data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +490 -0
  62. data/lib/nvoi/cli/exec/command.rb +173 -0
  63. data/lib/nvoi/cli/logs/command.rb +66 -0
  64. data/lib/nvoi/cli/onboard/command.rb +761 -0
  65. data/lib/nvoi/cli/unlock/command.rb +72 -0
  66. data/lib/nvoi/cli.rb +339 -141
  67. data/lib/nvoi/config_api/actions/app.rb +53 -0
  68. data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
  69. data/lib/nvoi/config_api/actions/database.rb +70 -0
  70. data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
  71. data/lib/nvoi/config_api/actions/env.rb +32 -0
  72. data/lib/nvoi/config_api/actions/init.rb +67 -0
  73. data/lib/nvoi/config_api/actions/secret.rb +32 -0
  74. data/lib/nvoi/config_api/actions/server.rb +66 -0
  75. data/lib/nvoi/config_api/actions/service.rb +52 -0
  76. data/lib/nvoi/config_api/actions/volume.rb +40 -0
  77. data/lib/nvoi/config_api/base.rb +38 -0
  78. data/lib/nvoi/config_api/result.rb +26 -0
  79. data/lib/nvoi/config_api.rb +93 -0
  80. data/lib/nvoi/errors.rb +68 -50
  81. data/lib/nvoi/external/cloud/aws.rb +450 -0
  82. data/lib/nvoi/external/cloud/base.rb +99 -0
  83. data/lib/nvoi/external/cloud/factory.rb +48 -0
  84. data/lib/nvoi/external/cloud/hetzner.rb +402 -0
  85. data/lib/nvoi/external/cloud/scaleway.rb +559 -0
  86. data/lib/nvoi/external/cloud.rb +15 -0
  87. data/lib/nvoi/external/containerd.rb +86 -0
  88. data/lib/nvoi/external/database/mysql.rb +84 -0
  89. data/lib/nvoi/external/database/postgres.rb +82 -0
  90. data/lib/nvoi/external/database/provider.rb +65 -0
  91. data/lib/nvoi/external/database/sqlite.rb +72 -0
  92. data/lib/nvoi/external/database.rb +22 -0
  93. data/lib/nvoi/external/dns/cloudflare.rb +310 -0
  94. data/lib/nvoi/external/kubectl.rb +65 -0
  95. data/lib/nvoi/external/ssh.rb +106 -0
  96. data/lib/nvoi/objects/config_override.rb +60 -0
  97. data/lib/nvoi/objects/configuration.rb +483 -0
  98. data/lib/nvoi/objects/database.rb +56 -0
  99. data/lib/nvoi/objects/dns.rb +14 -0
  100. data/lib/nvoi/objects/firewall.rb +11 -0
  101. data/lib/nvoi/objects/network.rb +11 -0
  102. data/lib/nvoi/objects/server.rb +14 -0
  103. data/lib/nvoi/objects/service_spec.rb +26 -0
  104. data/lib/nvoi/objects/tunnel.rb +14 -0
  105. data/lib/nvoi/objects/volume.rb +17 -0
  106. data/lib/nvoi/utils/config_loader.rb +172 -0
  107. data/lib/nvoi/utils/constants.rb +61 -0
  108. data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
  109. data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
  110. data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
  111. data/lib/nvoi/utils/logger.rb +84 -0
  112. data/lib/nvoi/{config/naming.rb → utils/namer.rb} +37 -25
  113. data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
  114. data/lib/nvoi/utils/templates.rb +62 -0
  115. data/lib/nvoi/version.rb +1 -1
  116. data/lib/nvoi.rb +27 -55
  117. data/templates/app-ingress.yaml.erb +3 -1
  118. data/templates/error-backend.yaml.erb +134 -0
  119. metadata +121 -44
  120. data/examples/golang/deploy.yml +0 -54
  121. data/lib/nvoi/cloudflare/client.rb +0 -287
  122. data/lib/nvoi/config/config.rb +0 -248
  123. data/lib/nvoi/config/loader.rb +0 -102
  124. data/lib/nvoi/config/ssh_keys.rb +0 -82
  125. data/lib/nvoi/config/types.rb +0 -274
  126. data/lib/nvoi/constants.rb +0 -59
  127. data/lib/nvoi/credentials/editor.rb +0 -272
  128. data/lib/nvoi/deployer/cleaner.rb +0 -36
  129. data/lib/nvoi/deployer/image_builder.rb +0 -23
  130. data/lib/nvoi/deployer/infrastructure.rb +0 -126
  131. data/lib/nvoi/deployer/orchestrator.rb +0 -146
  132. data/lib/nvoi/deployer/service_deployer.rb +0 -311
  133. data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
  134. data/lib/nvoi/deployer/types.rb +0 -8
  135. data/lib/nvoi/k8s/renderer.rb +0 -44
  136. data/lib/nvoi/k8s/templates.rb +0 -29
  137. data/lib/nvoi/logger.rb +0 -72
  138. data/lib/nvoi/providers/aws.rb +0 -403
  139. data/lib/nvoi/providers/base.rb +0 -111
  140. data/lib/nvoi/providers/hetzner.rb +0 -288
  141. data/lib/nvoi/providers/hetzner_client.rb +0 -170
  142. data/lib/nvoi/remote/docker_manager.rb +0 -203
  143. data/lib/nvoi/remote/ssh_executor.rb +0 -72
  144. data/lib/nvoi/remote/volume_manager.rb +0 -103
  145. data/lib/nvoi/service/delete.rb +0 -234
  146. data/lib/nvoi/service/deploy.rb +0 -80
  147. data/lib/nvoi/service/exec.rb +0 -144
  148. data/lib/nvoi/service/provider.rb +0 -36
  149. data/lib/nvoi/steps/application_deployer.rb +0 -26
  150. data/lib/nvoi/steps/database_provisioner.rb +0 -60
  151. data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
  152. data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
  153. data/lib/nvoi/steps/server_provisioner.rb +0 -43
  154. data/lib/nvoi/steps/services_provisioner.rb +0 -29
  155. data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
  156. data/lib/nvoi/steps/volume_provisioner.rb +0 -154
@@ -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
- # CredentialsCLI handles encrypted credential management
7
- class CredentialsCLI < Thor
8
- class_option :credentials, desc: "Path to encrypted credentials file (default: deploy.enc)"
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
- log = Nvoi.logger
100
- log.info "Deploy CLI %s", VERSION
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
- log = Nvoi.logger
121
- log.info "Delete CLI %s", VERSION
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
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
- log = Nvoi.logger
146
- log.info "Exec CLI %s", VERSION
65
+ require_relative "cli/exec/command"
66
+ Cli::Exec::Command.new(options).run(args)
67
+ end
147
68
 
148
- config_path = resolve_config_path
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
- begin
151
- svc = Service::ExecService.new(config_path, log)
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
- if options[:interactive]
154
- log.warning "Ignoring command arguments in interactive mode" unless args.empty?
155
- log.warning "Ignoring --all flag in interactive mode" if options[:all]
156
- svc.open_shell(options[:server])
157
- else
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
- command = args.join(" ")
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
- if options[:all]
163
- svc.run_all(command)
164
- else
165
- svc.run(command, options[:server])
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
- end
96
+ }
173
97
 
174
- desc "credentials SUBCOMMAND", "Manage encrypted deployment credentials"
175
- subcommand "credentials", CredentialsCLI
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
- private
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
- def resolve_config_path
180
- config_path = options[:config]
181
- working_dir = options[:dir]
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
- if config_path == Constants::DEFAULT_CONFIG_FILE && working_dir && working_dir != "."
184
- File.join(working_dir, Constants::DEFAULT_CONFIG_FILE)
185
- else
186
- config_path
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