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,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,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class SetDomainProvider < Base
|
|
7
|
+
PROVIDERS = %w[cloudflare].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)["domain_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 "cloudflare"
|
|
23
|
+
{
|
|
24
|
+
"api_token" => opts[:api_token],
|
|
25
|
+
"account_id" => opts[:account_id]
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class DeleteDomainProvider < Base
|
|
32
|
+
protected
|
|
33
|
+
|
|
34
|
+
def mutate(data, **)
|
|
35
|
+
app(data)["domain_provider"] = {}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
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,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class Init
|
|
7
|
+
def call(name:, environment: "production")
|
|
8
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
9
|
+
|
|
10
|
+
# Generate master key
|
|
11
|
+
master_key = Utils::Crypto.generate_key
|
|
12
|
+
|
|
13
|
+
# Generate SSH keypair (reuse existing utility)
|
|
14
|
+
private_key, public_key = Utils::ConfigLoader.generate_keypair
|
|
15
|
+
|
|
16
|
+
# Build initial config
|
|
17
|
+
config_data = {
|
|
18
|
+
"application" => {
|
|
19
|
+
"name" => name.to_s,
|
|
20
|
+
"environment" => environment.to_s,
|
|
21
|
+
"domain_provider" => {},
|
|
22
|
+
"compute_provider" => {},
|
|
23
|
+
"servers" => {},
|
|
24
|
+
"app" => {},
|
|
25
|
+
"services" => {},
|
|
26
|
+
"env" => {},
|
|
27
|
+
"secrets" => {},
|
|
28
|
+
"ssh_keys" => {
|
|
29
|
+
"private_key" => private_key,
|
|
30
|
+
"public_key" => public_key
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Encrypt config
|
|
36
|
+
yaml = YAML.dump(config_data)
|
|
37
|
+
encrypted_config = Utils::Crypto.encrypt(yaml, master_key)
|
|
38
|
+
|
|
39
|
+
InitResult.new(
|
|
40
|
+
config: encrypted_config,
|
|
41
|
+
master_key:,
|
|
42
|
+
ssh_public_key: public_key
|
|
43
|
+
)
|
|
44
|
+
rescue ArgumentError => e
|
|
45
|
+
InitResult.new(error_type: :invalid_args, error_message: e.message)
|
|
46
|
+
rescue Errors::ConfigError => e
|
|
47
|
+
InitResult.new(error_type: :config_error, error_message: e.message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class InitResult
|
|
52
|
+
attr_reader :config, :master_key, :ssh_public_key, :error_type, :error_message
|
|
53
|
+
|
|
54
|
+
def initialize(config: nil, master_key: nil, ssh_public_key: nil, error_type: nil, error_message: nil)
|
|
55
|
+
@config = config
|
|
56
|
+
@master_key = master_key
|
|
57
|
+
@ssh_public_key = ssh_public_key
|
|
58
|
+
@error_type = error_type
|
|
59
|
+
@error_message = error_message
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def success? = @error_type.nil?
|
|
63
|
+
def failure? = !success?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
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
|
|
@@ -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,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class SetService < Base
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def mutate(data, name:, servers:, image:, port: nil, command: nil, env: nil, mount: 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
|
+
raise ArgumentError, "image is required" if image.nil? || image.to_s.empty?
|
|
14
|
+
|
|
15
|
+
validate_server_refs(data, servers)
|
|
16
|
+
|
|
17
|
+
app(data)["services"] ||= {}
|
|
18
|
+
app(data)["services"][name.to_s] = {
|
|
19
|
+
"servers" => servers.map(&:to_s),
|
|
20
|
+
"image" => image.to_s,
|
|
21
|
+
"port" => port,
|
|
22
|
+
"command" => command,
|
|
23
|
+
"env" => env,
|
|
24
|
+
"mount" => mount
|
|
25
|
+
}.compact
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def validate_server_refs(data, servers)
|
|
31
|
+
defined = (app(data)["servers"] || {}).keys
|
|
32
|
+
servers.each do |ref|
|
|
33
|
+
raise Errors::ConfigValidationError, "server '#{ref}' not found" unless defined.include?(ref.to_s)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class DeleteService < Base
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
def mutate(data, name:)
|
|
42
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
43
|
+
|
|
44
|
+
services = app(data)["services"] || {}
|
|
45
|
+
raise Errors::ConfigValidationError, "service '#{name}' not found" unless services.key?(name.to_s)
|
|
46
|
+
|
|
47
|
+
services.delete(name.to_s)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
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,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
# Base class for config transformations.
|
|
6
|
+
# Accepts a Hash, returns a Hash. No crypto - caller handles that.
|
|
7
|
+
class Base
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@data = data
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(**args)
|
|
13
|
+
mutate(@data, **args)
|
|
14
|
+
validate(@data)
|
|
15
|
+
Result.success(@data)
|
|
16
|
+
rescue Errors::ConfigValidationError => e
|
|
17
|
+
Result.failure(:validation_error, e.message)
|
|
18
|
+
rescue ArgumentError => e
|
|
19
|
+
Result.failure(:invalid_args, e.message)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def mutate(_data, **_args)
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate(_data)
|
|
29
|
+
# Subclasses can override to add validation
|
|
30
|
+
# Default: no validation (lightweight actions like set_env don't need full config validation)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def app(data)
|
|
34
|
+
data["application"] ||= {}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
class Result
|
|
6
|
+
attr_reader :data, :error_type, :error_message
|
|
7
|
+
|
|
8
|
+
def self.success(data)
|
|
9
|
+
new(data:)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.failure(type, message)
|
|
13
|
+
new(error_type: type, error_message: message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(data: nil, error_type: nil, error_message: nil)
|
|
17
|
+
@data = data
|
|
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,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
class << self
|
|
6
|
+
# Init (creates new config - special case, handles crypto)
|
|
7
|
+
def init(**args)
|
|
8
|
+
Actions::Init.new.call(**args)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Domain Provider
|
|
12
|
+
def set_domain_provider(data, **args)
|
|
13
|
+
Actions::SetDomainProvider.new(data).call(**args)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete_domain_provider(data)
|
|
17
|
+
Actions::DeleteDomainProvider.new(data).call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Compute Provider
|
|
21
|
+
def set_compute_provider(data, **args)
|
|
22
|
+
Actions::SetComputeProvider.new(data).call(**args)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete_compute_provider(data)
|
|
26
|
+
Actions::DeleteComputeProvider.new(data).call
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Server
|
|
30
|
+
def set_server(data, **args)
|
|
31
|
+
Actions::SetServer.new(data).call(**args)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def delete_server(data, **args)
|
|
35
|
+
Actions::DeleteServer.new(data).call(**args)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Volume
|
|
39
|
+
def set_volume(data, **args)
|
|
40
|
+
Actions::SetVolume.new(data).call(**args)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete_volume(data, **args)
|
|
44
|
+
Actions::DeleteVolume.new(data).call(**args)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# App
|
|
48
|
+
def set_app(data, **args)
|
|
49
|
+
Actions::SetApp.new(data).call(**args)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def delete_app(data, **args)
|
|
53
|
+
Actions::DeleteApp.new(data).call(**args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Database
|
|
57
|
+
def set_database(data, **args)
|
|
58
|
+
Actions::SetDatabase.new(data).call(**args)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def delete_database(data)
|
|
62
|
+
Actions::DeleteDatabase.new(data).call
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Secret
|
|
66
|
+
def set_secret(data, **args)
|
|
67
|
+
Actions::SetSecret.new(data).call(**args)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def delete_secret(data, **args)
|
|
71
|
+
Actions::DeleteSecret.new(data).call(**args)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Env
|
|
75
|
+
def set_env(data, **args)
|
|
76
|
+
Actions::SetEnv.new(data).call(**args)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def delete_env(data, **args)
|
|
80
|
+
Actions::DeleteEnv.new(data).call(**args)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Service
|
|
84
|
+
def set_service(data, **args)
|
|
85
|
+
Actions::SetService.new(data).call(**args)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def delete_service(data, **args)
|
|
89
|
+
Actions::DeleteService.new(data).call(**args)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|