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
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module Steps
5
- # DatabaseProvisioner handles database deployment
6
- class DatabaseProvisioner
7
- def initialize(config, ssh, log)
8
- @config = config
9
- @ssh = ssh
10
- @log = log
11
- @service_deployer = Deployer::ServiceDeployer.new(config, ssh, log)
12
- end
13
-
14
- def run
15
- db_config = @config.deploy.application.database
16
- return unless db_config
17
-
18
- # SQLite is handled by app deployment with PVC volumes
19
- if db_config.adapter == "sqlite3"
20
- @log.info "SQLite database will be provisioned with app deployment"
21
- return
22
- end
23
-
24
- @log.info "Provisioning %s database via K8s", db_config.adapter
25
-
26
- db_spec = db_config.to_service_spec(@config.namer)
27
- @service_deployer.deploy_database(db_spec)
28
-
29
- # Wait for database to be ready
30
- wait_for_database(db_spec.name)
31
-
32
- @log.success "Database provisioned"
33
- end
34
-
35
- private
36
-
37
- def wait_for_database(name, timeout: 120)
38
- @log.info "Waiting for database to be ready..."
39
-
40
- start_time = Time.now
41
- loop do
42
- begin
43
- output = @ssh.execute("kubectl get pods -l app=#{name} -o jsonpath='{.items[0].status.phase}'")
44
- if output.strip == "Running"
45
- @log.success "Database is running"
46
- return
47
- end
48
- rescue SSHCommandError
49
- # Not ready yet
50
- end
51
-
52
- elapsed = Time.now - start_time
53
- raise K8sError, "database failed to start within #{timeout}s" if elapsed > timeout
54
-
55
- sleep(5)
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module Steps
5
- # K3sClusterSetup coordinates K3s installation across master and worker nodes
6
- class K3sClusterSetup
7
- def initialize(config, provider, log, main_server_ip)
8
- @config = config
9
- @provider = provider
10
- @log = log
11
- @main_server_ip = main_server_ip
12
- end
13
-
14
- def run
15
- @log.info "Setting up K3s cluster"
16
-
17
- # Find master server group
18
- master_group, master_config = find_master_group
19
- raise K8sError, "no master server group found" unless master_group
20
-
21
- # Setup K3s on master
22
- master_name = @config.namer.server_name(master_group, 1)
23
- master = @provider.find_server(master_name)
24
- raise K8sError, "master server not found: #{master_name}" unless master
25
-
26
- master_ssh = Remote::SSHExecutor.new(master.public_ipv4, @config.ssh_key_path)
27
- master_provisioner = K3sProvisioner.new(master_ssh, @log, server_role: master_group, server_name: master_name)
28
- master_provisioner.provision
29
-
30
- # Get cluster token and private IP from master
31
- cluster_token = master_provisioner.get_cluster_token
32
- master_private_ip = master_provisioner.get_private_ip
33
-
34
- # Setup K3s on worker nodes
35
- @config.deploy.application.servers.each do |group_name, group_config|
36
- next if group_name == master_group
37
- next unless group_config
38
-
39
- count = group_config.count.positive? ? group_config.count : 1
40
-
41
- (1..count).each do |i|
42
- worker_name = @config.namer.server_name(group_name, i)
43
- setup_worker(worker_name, group_name, cluster_token, master_private_ip, master_ssh)
44
- end
45
- end
46
-
47
- @log.success "K3s cluster setup complete"
48
- end
49
-
50
- private
51
-
52
- def find_master_group
53
- @config.deploy.application.servers.each do |name, cfg|
54
- return [name, cfg] if cfg&.master
55
- end
56
-
57
- # If only one group, use it as master
58
- if @config.deploy.application.servers.size == 1
59
- return @config.deploy.application.servers.first
60
- end
61
-
62
- nil
63
- end
64
-
65
- def setup_worker(worker_name, group_name, cluster_token, master_private_ip, master_ssh)
66
- @log.info "Setting up K3s worker: %s", worker_name
67
-
68
- worker = @provider.find_server(worker_name)
69
- unless worker
70
- @log.warning "Worker server not found: %s", worker_name
71
- return
72
- end
73
-
74
- worker_ssh = Remote::SSHExecutor.new(worker.public_ipv4, @config.ssh_key_path)
75
- worker_provisioner = K3sProvisioner.new(worker_ssh, @log, server_role: group_name, server_name: worker_name)
76
- worker_provisioner.cluster_token = cluster_token
77
- worker_provisioner.main_server_private_ip = master_private_ip
78
- worker_provisioner.provision
79
-
80
- # Label worker node from master
81
- @log.info "Labeling worker node: %s", worker_name
82
- label_worker_from_master(master_ssh, worker_name, group_name)
83
- end
84
-
85
- def label_worker_from_master(master_ssh, worker_name, group_name)
86
- # Wait for node to join cluster
87
- 30.times do
88
- begin
89
- output = master_ssh.execute("kubectl get nodes -o name")
90
- if output.include?(worker_name)
91
- master_ssh.execute("kubectl label node #{worker_name} nvoi.io/server-name=#{group_name} --overwrite")
92
- @log.success "Worker labeled: %s", worker_name
93
- return
94
- end
95
- rescue SSHCommandError
96
- # Not ready
97
- end
98
- sleep(5)
99
- end
100
-
101
- @log.warning "Worker node did not join cluster in time: %s", worker_name
102
- end
103
- end
104
- end
105
- end
@@ -1,351 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module Steps
5
- # K3sProvisioner handles K3s installation and server setup
6
- class K3sProvisioner
7
- attr_accessor :main_server_ip, :main_server_private_ip, :cluster_token
8
-
9
- def initialize(ssh, log, k3s_version: nil, enable_k3s: true, server_role: nil, server_name: nil)
10
- @ssh = ssh
11
- @log = log
12
- @k3s_version = k3s_version || Constants::DEFAULT_K3S_VERSION
13
- @enable_k3s = enable_k3s
14
- @server_role = server_role
15
- @server_name = server_name
16
- end
17
-
18
- def provision
19
- @log.info "Starting K3s provisioning"
20
-
21
- wait_for_cloud_init
22
-
23
- if @enable_k3s
24
- is_master = @cluster_token.nil? || @cluster_token.empty?
25
-
26
- if is_master
27
- install_k3s_server
28
- label_node(@server_name, { "nvoi.io/server-name" => @server_role })
29
- setup_registry
30
- setup_ingress_controller
31
- else
32
- install_k3s_agent
33
- end
34
- end
35
-
36
- @log.success "K3s provisioning complete"
37
- end
38
-
39
- def get_cluster_token
40
- @log.info "Retrieving K3s cluster token"
41
- output = @ssh.execute("sudo cat /var/lib/rancher/k3s/server/node-token")
42
- token = output.strip
43
- raise K8sError, "cluster token is empty" if token.empty?
44
-
45
- @log.success "Cluster token retrieved"
46
- token
47
- end
48
-
49
- def get_private_ip
50
- output = @ssh.execute("ip addr show | grep 'inet 10\\.' | awk '{print $2}' | cut -d/ -f1 | head -1")
51
- private_ip = output.strip
52
- raise SSHError, "private IP not found" if private_ip.empty?
53
-
54
- private_ip
55
- end
56
-
57
- private
58
-
59
- def wait_for_cloud_init
60
- @log.info "Waiting for cloud-init to complete"
61
-
62
- 60.times do
63
- begin
64
- output = @ssh.execute("test -f /var/lib/cloud/instance/boot-finished && echo 'ready'")
65
- if output.include?("ready")
66
- @log.success "Cloud-init complete"
67
- return
68
- end
69
- rescue SSHCommandError
70
- # Not ready yet
71
- end
72
- sleep(5)
73
- end
74
-
75
- raise K8sError, "cloud-init timeout"
76
- end
77
-
78
- def install_k3s_server
79
- # Check if K3s is already running
80
- begin
81
- @ssh.execute("systemctl is-active k3s")
82
- @log.info "K3s already running, skipping installation"
83
- setup_kubeconfig
84
- return
85
- rescue SSHCommandError
86
- # Not running, continue
87
- end
88
-
89
- @log.info "Installing K3s server"
90
-
91
- # Detect private IP and interface
92
- private_ip = get_private_ip
93
- private_iface = @ssh.execute("ip addr show | grep 'inet 10\\.' | awk '{print $NF}' | head -1").strip
94
-
95
- @log.info "Installing k3s on private IP: %s, interface: %s", private_ip, private_iface
96
-
97
- # Install Docker for image building
98
- install_docker(private_ip)
99
-
100
- # Configure k3s registries
101
- configure_registries
102
-
103
- # Install K3s with full configuration
104
- install_cmd = <<~CMD
105
- curl -sfL https://get.k3s.io | sudo sh -s - server \
106
- --bind-address=#{private_ip} \
107
- --advertise-address=#{private_ip} \
108
- --node-ip=#{private_ip} \
109
- --tls-san=#{private_ip} \
110
- --flannel-iface=#{private_iface} \
111
- --flannel-backend=wireguard-native \
112
- --disable=traefik \
113
- --write-kubeconfig-mode=644 \
114
- --cluster-cidr=10.42.0.0/16 \
115
- --service-cidr=10.43.0.0/16
116
- CMD
117
-
118
- @ssh.execute(install_cmd, stream: true)
119
- @log.success "K3s server installed"
120
-
121
- setup_kubeconfig(private_ip)
122
- wait_for_k3s_ready
123
- end
124
-
125
- def install_k3s_agent
126
- # Check if K3s agent is already running
127
- begin
128
- @ssh.execute("systemctl is-active k3s-agent")
129
- @log.info "K3s agent already running, skipping installation"
130
- return
131
- rescue SSHCommandError
132
- # Not running, continue
133
- end
134
-
135
- @log.info "Installing K3s agent"
136
-
137
- private_ip = get_private_ip
138
- private_iface = @ssh.execute("ip addr show | grep 'inet 10\\.' | awk '{print $NF}' | head -1").strip
139
-
140
- @log.info "Worker private IP: %s, interface: %s", private_ip, private_iface
141
-
142
- cmd = <<~CMD
143
- curl -sfL https://get.k3s.io | K3S_URL="https://#{@main_server_private_ip}:6443" K3S_TOKEN="#{@cluster_token}" sh -s - agent \
144
- --node-ip=#{private_ip} \
145
- --flannel-iface=#{private_iface} \
146
- --node-name=#{@server_name}
147
- CMD
148
-
149
- @ssh.execute(cmd, stream: true)
150
- @log.success "K3s agent installed"
151
- end
152
-
153
- def install_docker(private_ip)
154
- # Check if Docker is already installed and running
155
- begin
156
- @ssh.execute("systemctl is-active docker")
157
- @log.info "Docker already running, skipping installation"
158
- rescue SSHCommandError
159
- # Not running, install it
160
- docker_install = <<~CMD
161
- sudo apt-get update && sudo apt-get install -y docker.io
162
- sudo systemctl start docker
163
- sudo systemctl enable docker
164
- sudo usermod -aG docker deploy
165
- CMD
166
-
167
- @ssh.execute(docker_install, stream: true)
168
- end
169
-
170
- # Configure Docker for insecure registry
171
- docker_config = <<~CMD
172
- sudo mkdir -p /etc/docker
173
- sudo tee /etc/docker/daemon.json > /dev/null <<EOF
174
- {"insecure-registries": ["#{private_ip}:5001", "localhost:30500"]}
175
- EOF
176
- sudo systemctl restart docker
177
- CMD
178
-
179
- @ssh.execute(docker_config)
180
-
181
- # Add registry domain to /etc/hosts
182
- @ssh.execute('grep -q "nvoi-registry.default.svc.cluster.local" /etc/hosts || echo "127.0.0.1 nvoi-registry.default.svc.cluster.local" | sudo tee -a /etc/hosts')
183
- end
184
-
185
- def configure_registries
186
- config = <<~CMD
187
- sudo mkdir -p /etc/rancher/k3s
188
- sudo tee /etc/rancher/k3s/registries.yaml > /dev/null <<'REGEOF'
189
- mirrors:
190
- "nvoi-registry.default.svc.cluster.local:5000":
191
- endpoint:
192
- - "http://localhost:30500"
193
- "localhost:30500":
194
- endpoint:
195
- - "http://localhost:30500"
196
- configs:
197
- "nvoi-registry.default.svc.cluster.local:5000":
198
- tls:
199
- insecure_skip_verify: true
200
- "localhost:30500":
201
- tls:
202
- insecure_skip_verify: true
203
- REGEOF
204
- CMD
205
-
206
- @ssh.execute(config)
207
- end
208
-
209
- def setup_kubeconfig(private_ip = nil)
210
- private_ip ||= get_private_ip
211
-
212
- cmd = <<~CMD
213
- sudo mkdir -p /home/deploy/.kube
214
- sudo cp /etc/rancher/k3s/k3s.yaml /home/deploy/.kube/config
215
- sudo sed -i "s/127.0.0.1/#{private_ip}/g" /home/deploy/.kube/config
216
- sudo chown -R deploy:deploy /home/deploy/.kube
217
- CMD
218
-
219
- @ssh.execute(cmd)
220
- end
221
-
222
- def wait_for_k3s_ready
223
- @log.info "Waiting for K3s to be ready"
224
-
225
- 60.times do
226
- begin
227
- output = @ssh.execute("kubectl get nodes")
228
- if output.include?("Ready")
229
- @log.success "K3s is ready"
230
- return
231
- end
232
- rescue SSHCommandError
233
- # Not ready yet
234
- end
235
- sleep(5)
236
- end
237
-
238
- raise K8sError, "K3s failed to become ready"
239
- end
240
-
241
- def label_node(node_name, labels)
242
- # Get actual node name from K3s
243
- actual_node = @ssh.execute("kubectl get nodes -o jsonpath='{.items[0].metadata.name}'").strip
244
-
245
- labels.each do |key, value|
246
- @ssh.execute("kubectl label node #{actual_node} #{key}=#{value} --overwrite")
247
- end
248
- end
249
-
250
- def setup_registry
251
- @log.info "Setting up in-cluster registry"
252
-
253
- manifest = <<~YAML
254
- apiVersion: v1
255
- kind: Namespace
256
- metadata:
257
- name: nvoi-system
258
- ---
259
- apiVersion: apps/v1
260
- kind: Deployment
261
- metadata:
262
- name: nvoi-registry
263
- namespace: default
264
- spec:
265
- replicas: 1
266
- selector:
267
- matchLabels:
268
- app: nvoi-registry
269
- template:
270
- metadata:
271
- labels:
272
- app: nvoi-registry
273
- spec:
274
- containers:
275
- - name: registry
276
- image: registry:2
277
- ports:
278
- - containerPort: 5000
279
- protocol: TCP
280
- env:
281
- - name: REGISTRY_HTTP_ADDR
282
- value: "0.0.0.0:5000"
283
- volumeMounts:
284
- - name: registry-storage
285
- mountPath: /var/lib/registry
286
- volumes:
287
- - name: registry-storage
288
- emptyDir: {}
289
- ---
290
- apiVersion: v1
291
- kind: Service
292
- metadata:
293
- name: nvoi-registry
294
- namespace: default
295
- spec:
296
- type: NodePort
297
- ports:
298
- - port: 5000
299
- targetPort: 5000
300
- nodePort: 30500
301
- selector:
302
- app: nvoi-registry
303
- YAML
304
-
305
- @ssh.execute("cat <<'EOF' | kubectl apply -f -\n#{manifest}\nEOF")
306
-
307
- # Wait for registry to be ready
308
- @log.info "Waiting for registry to be ready"
309
- 24.times do
310
- begin
311
- output = @ssh.execute("kubectl get deployment nvoi-registry -n default -o jsonpath='{.status.readyReplicas}'")
312
- if output.strip == "1"
313
- @log.success "In-cluster registry running on :30500"
314
- return
315
- end
316
- rescue SSHCommandError
317
- # Not ready
318
- end
319
- sleep(5)
320
- end
321
-
322
- raise K8sError, "registry failed to become ready"
323
- end
324
-
325
- def setup_ingress_controller
326
- @log.info "Setting up NGINX Ingress Controller"
327
-
328
- @ssh.execute("kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/baremetal/deploy.yaml", stream: true)
329
-
330
- # Wait for ingress controller
331
- @log.info "Waiting for NGINX Ingress Controller to be ready"
332
- 60.times do
333
- begin
334
- ready = @ssh.execute("kubectl get deployment ingress-nginx-controller -n ingress-nginx -o jsonpath='{.status.readyReplicas}'").strip
335
- desired = @ssh.execute("kubectl get deployment ingress-nginx-controller -n ingress-nginx -o jsonpath='{.spec.replicas}'").strip
336
-
337
- if !ready.empty? && !desired.empty? && ready == desired
338
- @log.success "NGINX Ingress Controller is ready"
339
- return
340
- end
341
- rescue SSHCommandError
342
- # Not ready
343
- end
344
- sleep(10)
345
- end
346
-
347
- raise K8sError, "NGINX Ingress Controller failed to become ready"
348
- end
349
- end
350
- end
351
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module Steps
5
- # ServerProvisioner handles provisioning of compute servers
6
- class ServerProvisioner
7
- def initialize(config, provider, log)
8
- @config = config
9
- @provider = provider
10
- @log = log
11
- @infrastructure = Deployer::Infrastructure.new(config, provider, log)
12
- end
13
-
14
- # Run provisions all servers and returns the main server IP
15
- def run
16
- @log.info "Provisioning servers"
17
-
18
- # Provision network and firewall
19
- network = @infrastructure.provision_network
20
- firewall = @infrastructure.provision_firewall
21
-
22
- servers = @config.deploy.application.servers
23
- main_server_ip = nil
24
-
25
- # Provision each server group
26
- servers.each do |group_name, group_config|
27
- count = group_config&.count&.positive? ? group_config.count : 1
28
-
29
- (1..count).each do |i|
30
- server_name = @config.namer.server_name(group_name, i)
31
- server = @infrastructure.provision_server(server_name, network.id, firewall.id, group_config)
32
-
33
- # Track main server IP (first master, or just first server)
34
- main_server_ip ||= server.public_ipv4 if group_config&.master || i == 1
35
- end
36
- end
37
-
38
- @log.success "All servers provisioned"
39
- main_server_ip
40
- end
41
- end
42
- end
43
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module Steps
5
- # ServicesProvisioner handles deployment of additional services (redis, etc.)
6
- class ServicesProvisioner
7
- def initialize(config, ssh, log)
8
- @config = config
9
- @ssh = ssh
10
- @log = log
11
- @service_deployer = Deployer::ServiceDeployer.new(config, ssh, log)
12
- end
13
-
14
- def run
15
- services = @config.deploy.application.services
16
- return if services.empty?
17
-
18
- @log.info "Provisioning %d additional service(s)", services.size
19
-
20
- services.each do |service_name, service_config|
21
- service_spec = service_config.to_service_spec(@config.deploy.application.name, service_name)
22
- @service_deployer.deploy_service(service_name, service_spec)
23
- end
24
-
25
- @log.success "Additional services provisioned"
26
- end
27
- end
28
- end
29
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module Steps
5
- # TunnelConfigurator handles Cloudflare tunnel setup for services
6
- class TunnelConfigurator
7
- def initialize(config, log)
8
- @config = config
9
- @log = log
10
-
11
- cf = config.cloudflare
12
- @cf_client = Cloudflare::Client.new(cf.api_token, cf.account_id)
13
- @tunnel_manager = Deployer::TunnelManager.new(@cf_client, log)
14
- end
15
-
16
- def run
17
- @log.info "Configuring Cloudflare tunnels"
18
-
19
- tunnels = []
20
-
21
- @config.deploy.application.app.each do |service_name, service_config|
22
- next unless service_config.domain && !service_config.domain.empty?
23
- next unless service_config.port && service_config.port.positive?
24
- # Allow empty subdomain or "@" for apex domain
25
- next if service_config.subdomain.nil?
26
-
27
- tunnel_info = configure_service_tunnel(service_name, service_config)
28
- tunnels << tunnel_info
29
- end
30
-
31
- @log.success "All tunnels configured (%d)", tunnels.size
32
- tunnels
33
- end
34
-
35
- private
36
-
37
- def configure_service_tunnel(service_name, service_config)
38
- tunnel_name = @config.namer.tunnel_name(service_name)
39
- hostname = build_hostname(service_config.subdomain, service_config.domain)
40
-
41
- # Service URL points to the K8s service
42
- k8s_service_name = @config.namer.app_service_name(service_name)
43
- service_url = "http://#{k8s_service_name}:#{service_config.port}"
44
-
45
- tunnel = @tunnel_manager.setup_tunnel(tunnel_name, hostname, service_url, service_config.domain)
46
-
47
- Deployer::TunnelInfo.new(
48
- service_name:,
49
- hostname:,
50
- tunnel_id: tunnel.tunnel_id,
51
- tunnel_token: tunnel.tunnel_token
52
- )
53
- end
54
-
55
- # Build hostname from subdomain and domain
56
- # Supports: "app" -> "app.example.com", "" or "@" -> "example.com", "*" -> "*.example.com"
57
- def build_hostname(subdomain, domain)
58
- if subdomain.nil? || subdomain.empty? || subdomain == "@"
59
- domain
60
- else
61
- "#{subdomain}.#{domain}"
62
- end
63
- end
64
- end
65
- end
66
- end