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.
Files changed (130) 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/_target.md +79 -0
  15. data/.claude/todo/scaleway.impl.md +644 -0
  16. data/.claude/todo/scaleway.reference.md +520 -0
  17. data/Gemfile +1 -0
  18. data/Gemfile.lock +12 -2
  19. data/doc/config-schema.yaml +44 -11
  20. data/examples/golang/deploy.enc +0 -0
  21. data/examples/golang/main.go +18 -0
  22. data/exe/nvoi +3 -1
  23. data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
  24. data/lib/nvoi/cli/credentials/show/command.rb +35 -0
  25. data/lib/nvoi/cli/db/command.rb +308 -0
  26. data/lib/nvoi/cli/delete/command.rb +75 -0
  27. data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
  28. data/lib/nvoi/cli/delete/steps/teardown_dns.rb +49 -0
  29. data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
  30. data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
  31. data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
  32. data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
  33. data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
  34. data/lib/nvoi/cli/deploy/command.rb +184 -0
  35. data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
  36. data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
  37. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +100 -0
  38. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +396 -0
  39. data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
  40. data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
  41. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
  42. data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +481 -0
  43. data/lib/nvoi/cli/exec/command.rb +173 -0
  44. data/lib/nvoi/cli.rb +83 -142
  45. data/lib/nvoi/config_api/actions/app.rb +53 -0
  46. data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
  47. data/lib/nvoi/config_api/actions/database.rb +70 -0
  48. data/lib/nvoi/config_api/actions/env.rb +32 -0
  49. data/lib/nvoi/config_api/actions/secret.rb +32 -0
  50. data/lib/nvoi/config_api/actions/server.rb +66 -0
  51. data/lib/nvoi/config_api/actions/volume.rb +40 -0
  52. data/lib/nvoi/config_api/base.rb +44 -0
  53. data/lib/nvoi/config_api/result.rb +26 -0
  54. data/lib/nvoi/config_api.rb +70 -0
  55. data/lib/nvoi/errors.rb +68 -50
  56. data/lib/nvoi/external/cloud/aws.rb +425 -0
  57. data/lib/nvoi/external/cloud/base.rb +99 -0
  58. data/lib/nvoi/external/cloud/factory.rb +48 -0
  59. data/lib/nvoi/external/cloud/hetzner.rb +376 -0
  60. data/lib/nvoi/external/cloud/scaleway.rb +533 -0
  61. data/lib/nvoi/external/cloud.rb +15 -0
  62. data/lib/nvoi/external/containerd.rb +82 -0
  63. data/lib/nvoi/external/database/mysql.rb +84 -0
  64. data/lib/nvoi/external/database/postgres.rb +82 -0
  65. data/lib/nvoi/external/database/provider.rb +65 -0
  66. data/lib/nvoi/external/database/sqlite.rb +72 -0
  67. data/lib/nvoi/external/database.rb +22 -0
  68. data/lib/nvoi/external/dns/cloudflare.rb +292 -0
  69. data/lib/nvoi/external/kubectl.rb +65 -0
  70. data/lib/nvoi/external/ssh.rb +106 -0
  71. data/lib/nvoi/objects/config_override.rb +60 -0
  72. data/lib/nvoi/objects/configuration.rb +463 -0
  73. data/lib/nvoi/objects/database.rb +56 -0
  74. data/lib/nvoi/objects/dns.rb +14 -0
  75. data/lib/nvoi/objects/firewall.rb +11 -0
  76. data/lib/nvoi/objects/network.rb +11 -0
  77. data/lib/nvoi/objects/server.rb +14 -0
  78. data/lib/nvoi/objects/service_spec.rb +26 -0
  79. data/lib/nvoi/objects/tunnel.rb +14 -0
  80. data/lib/nvoi/objects/volume.rb +17 -0
  81. data/lib/nvoi/utils/config_loader.rb +172 -0
  82. data/lib/nvoi/utils/constants.rb +61 -0
  83. data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
  84. data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
  85. data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
  86. data/lib/nvoi/utils/logger.rb +84 -0
  87. data/lib/nvoi/{config/naming.rb → utils/namer.rb} +28 -25
  88. data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
  89. data/lib/nvoi/utils/templates.rb +62 -0
  90. data/lib/nvoi/version.rb +1 -1
  91. data/lib/nvoi.rb +10 -54
  92. data/templates/error-backend.yaml.erb +134 -0
  93. metadata +97 -44
  94. data/examples/golang/deploy.yml +0 -54
  95. data/lib/nvoi/cloudflare/client.rb +0 -287
  96. data/lib/nvoi/config/config.rb +0 -248
  97. data/lib/nvoi/config/loader.rb +0 -102
  98. data/lib/nvoi/config/ssh_keys.rb +0 -82
  99. data/lib/nvoi/config/types.rb +0 -274
  100. data/lib/nvoi/constants.rb +0 -59
  101. data/lib/nvoi/credentials/editor.rb +0 -272
  102. data/lib/nvoi/deployer/cleaner.rb +0 -36
  103. data/lib/nvoi/deployer/image_builder.rb +0 -23
  104. data/lib/nvoi/deployer/infrastructure.rb +0 -126
  105. data/lib/nvoi/deployer/orchestrator.rb +0 -146
  106. data/lib/nvoi/deployer/service_deployer.rb +0 -311
  107. data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
  108. data/lib/nvoi/deployer/types.rb +0 -8
  109. data/lib/nvoi/k8s/renderer.rb +0 -44
  110. data/lib/nvoi/k8s/templates.rb +0 -29
  111. data/lib/nvoi/logger.rb +0 -72
  112. data/lib/nvoi/providers/aws.rb +0 -403
  113. data/lib/nvoi/providers/base.rb +0 -111
  114. data/lib/nvoi/providers/hetzner.rb +0 -288
  115. data/lib/nvoi/providers/hetzner_client.rb +0 -170
  116. data/lib/nvoi/remote/docker_manager.rb +0 -203
  117. data/lib/nvoi/remote/ssh_executor.rb +0 -72
  118. data/lib/nvoi/remote/volume_manager.rb +0 -103
  119. data/lib/nvoi/service/delete.rb +0 -234
  120. data/lib/nvoi/service/deploy.rb +0 -80
  121. data/lib/nvoi/service/exec.rb +0 -144
  122. data/lib/nvoi/service/provider.rb +0 -36
  123. data/lib/nvoi/steps/application_deployer.rb +0 -26
  124. data/lib/nvoi/steps/database_provisioner.rb +0 -60
  125. data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
  126. data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
  127. data/lib/nvoi/steps/server_provisioner.rb +0 -43
  128. data/lib/nvoi/steps/services_provisioner.rb +0 -29
  129. data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
  130. data/lib/nvoi/steps/volume_provisioner.rb +0 -154
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module ConfigApi
5
+ module Actions
6
+ class SetServer < Base
7
+ protected
8
+
9
+ def mutate(data, name:, master: false, type: nil, location: nil, count: 1)
10
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
11
+ raise ArgumentError, "count must be positive" if count && count < 1
12
+
13
+ app(data)["servers"] ||= {}
14
+ app(data)["servers"][name.to_s] = {
15
+ "master" => master,
16
+ "type" => type,
17
+ "location" => location,
18
+ "count" => count
19
+ }.compact
20
+ end
21
+ end
22
+
23
+ class DeleteServer < Base
24
+ protected
25
+
26
+ def mutate(data, name:)
27
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
28
+
29
+ servers = app(data)["servers"] || {}
30
+ raise Errors::ConfigValidationError, "server '#{name}' not found" unless servers.key?(name.to_s)
31
+
32
+ servers.delete(name.to_s)
33
+ end
34
+
35
+ def validate(data)
36
+ check_orphaned_references(data)
37
+ end
38
+
39
+ private
40
+
41
+ def check_orphaned_references(data)
42
+ servers = (app(data)["servers"] || {}).keys
43
+
44
+ (app(data)["app"] || {}).each do |svc_name, svc|
45
+ (svc["servers"] || []).each do |ref|
46
+ raise Errors::ConfigValidationError, "app.#{svc_name} references non-existent server '#{ref}'" unless servers.include?(ref)
47
+ end
48
+ end
49
+
50
+ db = app(data)["database"]
51
+ if db
52
+ (db["servers"] || []).each do |ref|
53
+ raise Errors::ConfigValidationError, "database references non-existent server '#{ref}'" unless servers.include?(ref)
54
+ end
55
+ end
56
+
57
+ (app(data)["services"] || {}).each do |svc_name, svc|
58
+ (svc["servers"] || []).each do |ref|
59
+ raise Errors::ConfigValidationError, "services.#{svc_name} references non-existent server '#{ref}'" unless servers.include?(ref)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module ConfigApi
5
+ module Actions
6
+ class SetVolume < Base
7
+ protected
8
+
9
+ def mutate(data, server:, name:, size: 10)
10
+ raise ArgumentError, "server is required" if server.nil? || server.to_s.empty?
11
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
12
+ raise ArgumentError, "size must be positive" if size && size < 1
13
+
14
+ servers = app(data)["servers"] ||= {}
15
+ raise Errors::ConfigValidationError, "server '#{server}' not found" unless servers.key?(server.to_s)
16
+
17
+ servers[server.to_s]["volumes"] ||= {}
18
+ servers[server.to_s]["volumes"][name.to_s] = { "size" => size }
19
+ end
20
+ end
21
+
22
+ class DeleteVolume < Base
23
+ protected
24
+
25
+ def mutate(data, server:, name:)
26
+ raise ArgumentError, "server is required" if server.nil? || server.to_s.empty?
27
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
28
+
29
+ servers = app(data)["servers"] || {}
30
+ raise Errors::ConfigValidationError, "server '#{server}' not found" unless servers.key?(server.to_s)
31
+
32
+ volumes = servers[server.to_s]["volumes"] || {}
33
+ raise Errors::ConfigValidationError, "volume '#{name}' not found on server '#{server}'" unless volumes.key?(name.to_s)
34
+
35
+ volumes.delete(name.to_s)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module ConfigApi
5
+ class Base
6
+ def initialize(encrypted_config, master_key)
7
+ @encrypted_config = encrypted_config
8
+ @master_key = master_key
9
+ end
10
+
11
+ def call(**args)
12
+ yaml = Utils::Crypto.decrypt(@encrypted_config, @master_key)
13
+ @data = YAML.safe_load(yaml, permitted_classes: [Symbol])
14
+
15
+ mutate(@data, **args)
16
+ validate(@data)
17
+
18
+ new_yaml = YAML.dump(@data)
19
+ Result.success(Utils::Crypto.encrypt(new_yaml, @master_key))
20
+ rescue Errors::DecryptionError, Errors::InvalidKeyError => e
21
+ Result.failure(:decryption_error, e.message)
22
+ rescue Errors::ConfigValidationError => e
23
+ Result.failure(:validation_error, e.message)
24
+ rescue ArgumentError => e
25
+ Result.failure(:invalid_args, e.message)
26
+ end
27
+
28
+ protected
29
+
30
+ def mutate(_data, **_args)
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def validate(_data)
35
+ # Subclasses can override to add validation
36
+ # Default: no validation (lightweight actions like set_env don't need full config validation)
37
+ end
38
+
39
+ def app(data)
40
+ data["application"] ||= {}
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module ConfigApi
5
+ class Result
6
+ attr_reader :config, :error_type, :error_message
7
+
8
+ def self.success(config)
9
+ new(config: config)
10
+ end
11
+
12
+ def self.failure(type, message)
13
+ new(error_type: type, error_message: message)
14
+ end
15
+
16
+ def initialize(config: nil, error_type: nil, error_message: nil)
17
+ @config = config
18
+ @error_type = error_type
19
+ @error_message = error_message
20
+ end
21
+
22
+ def success? = @error_type.nil?
23
+ def failure? = !success?
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module ConfigApi
5
+ class << self
6
+ # Compute Provider
7
+ def set_compute_provider(config, key, **args)
8
+ Actions::SetComputeProvider.new(config, key).call(**args)
9
+ end
10
+
11
+ def delete_compute_provider(config, key)
12
+ Actions::DeleteComputeProvider.new(config, key).call
13
+ end
14
+
15
+ # Server
16
+ def set_server(config, key, **args)
17
+ Actions::SetServer.new(config, key).call(**args)
18
+ end
19
+
20
+ def delete_server(config, key, **args)
21
+ Actions::DeleteServer.new(config, key).call(**args)
22
+ end
23
+
24
+ # Volume
25
+ def set_volume(config, key, **args)
26
+ Actions::SetVolume.new(config, key).call(**args)
27
+ end
28
+
29
+ def delete_volume(config, key, **args)
30
+ Actions::DeleteVolume.new(config, key).call(**args)
31
+ end
32
+
33
+ # App
34
+ def set_app(config, key, **args)
35
+ Actions::SetApp.new(config, key).call(**args)
36
+ end
37
+
38
+ def delete_app(config, key, **args)
39
+ Actions::DeleteApp.new(config, key).call(**args)
40
+ end
41
+
42
+ # Database
43
+ def set_database(config, key, **args)
44
+ Actions::SetDatabase.new(config, key).call(**args)
45
+ end
46
+
47
+ def delete_database(config, key)
48
+ Actions::DeleteDatabase.new(config, key).call
49
+ end
50
+
51
+ # Secret
52
+ def set_secret(config, key, **args)
53
+ Actions::SetSecret.new(config, key).call(**args)
54
+ end
55
+
56
+ def delete_secret(config, key, **args)
57
+ Actions::DeleteSecret.new(config, key).call(**args)
58
+ end
59
+
60
+ # Env
61
+ def set_env(config, key, **args)
62
+ Actions::SetEnv.new(config, key).call(**args)
63
+ end
64
+
65
+ def delete_env(config, key, **args)
66
+ Actions::DeleteEnv.new(config, key).call(**args)
67
+ end
68
+ end
69
+ end
70
+ end
data/lib/nvoi/errors.rb CHANGED
@@ -1,67 +1,85 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nvoi
4
- # Base error class for all Nvoi errors
5
- class Error < StandardError
6
- attr_reader :details
4
+ # Errors module
5
+ module Errors
6
+ # Base error class for all Nvoi errors
7
+ class Error < StandardError
8
+ attr_reader :details
7
9
 
8
- def initialize(message, details: nil)
9
- @details = details
10
- super(message)
10
+ def initialize(message, details: nil)
11
+ @details = details
12
+ super(message)
13
+ end
11
14
  end
12
- end
13
15
 
14
- # Configuration errors
15
- class ConfigError < Error; end
16
- class ConfigNotFoundError < ConfigError; end
17
- class ConfigValidationError < ConfigError; end
16
+ # Configuration errors
17
+ class ConfigError < Error; end
18
+ class ConfigNotFoundError < ConfigError; end
19
+ class ConfigValidationError < ConfigError; end
18
20
 
19
- # Credential errors
20
- class CredentialError < Error; end
21
- class DecryptionError < CredentialError; end
22
- class EncryptionError < CredentialError; end
23
- class InvalidKeyError < CredentialError; end
21
+ # Credential errors
22
+ class CredentialError < Error; end
23
+ class DecryptionError < CredentialError; end
24
+ class EncryptionError < CredentialError; end
25
+ class InvalidKeyError < CredentialError; end
24
26
 
25
- # Provider errors
26
- class ProviderError < Error; end
27
- class ServerCreationError < ProviderError; end
28
- class NetworkError < ProviderError; end
29
- class FirewallError < ProviderError; end
30
- class VolumeError < ProviderError; end
31
- class ValidationError < ProviderError; end
32
- class APIError < ProviderError; end
33
- class AuthenticationError < ProviderError; end
34
- class NotFoundError < ProviderError; end
27
+ # Provider errors
28
+ class ProviderError < Error; end
29
+ class ServerCreationError < ProviderError; end
30
+ class NetworkError < ProviderError; end
31
+ class FirewallError < ProviderError; end
32
+ class VolumeError < ProviderError; end
33
+ class ValidationError < ProviderError; end
34
+ class ApiError < ProviderError; end
35
+ class AuthenticationError < ProviderError; end
36
+ class NotFoundError < ProviderError; end
37
+ class ConflictError < ProviderError; end
38
+ class RateLimitError < ProviderError; end
35
39
 
36
- # Cloudflare errors
37
- class CloudflareError < Error; end
38
- class TunnelError < CloudflareError; end
39
- class DNSError < CloudflareError; end
40
+ # Cloudflare errors
41
+ class CloudflareError < Error; end
42
+ class TunnelError < CloudflareError; end
43
+ class DnsError < CloudflareError; end
40
44
 
41
- # SSH errors
42
- class SSHError < Error; end
43
- class SSHConnectionError < SSHError; end
44
- class SSHCommandError < SSHError; end
45
+ # Ssh errors
46
+ class SshError < Error; end
47
+ class SshConnectionError < SshError; end
48
+ class SshCommandError < SshError; end
45
49
 
46
- # Deployment errors
47
- class DeploymentError < Error
48
- attr_reader :step, :retryable
50
+ # Timeout errors
51
+ class TimeoutError < Error; end
49
52
 
50
- def initialize(step, message, retryable: false, details: nil)
51
- @step = step
52
- @retryable = retryable
53
- super("#{step}: #{message}", details:)
54
- end
53
+ # Deployment errors
54
+ class DeploymentError < Error
55
+ attr_reader :step, :retryable
55
56
 
56
- def retryable?
57
- @retryable
57
+ def initialize(step, message, retryable: false, details: nil)
58
+ @step = step
59
+ @retryable = retryable
60
+ super("#{step}: #{message}", details:)
61
+ end
62
+
63
+ def retryable?
64
+ @retryable
65
+ end
58
66
  end
59
- end
60
67
 
61
- # K8s errors
62
- class K8sError < Error; end
63
- class TemplateError < K8sError; end
68
+ # K8s errors
69
+ class K8sError < Error; end
70
+ class TemplateError < K8sError; end
64
71
 
65
- # Service errors
66
- class ServiceError < Error; end
72
+ # Service errors
73
+ class ServiceError < Error; end
74
+
75
+ # Database errors
76
+ class DatabaseError < Error
77
+ attr_reader :operation
78
+
79
+ def initialize(operation, message, details: nil)
80
+ @operation = operation
81
+ super("database #{operation}: #{message}", details:)
82
+ end
83
+ end
84
+ end
67
85
  end