kitsune-kit 0.4.0 → 0.5.0

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +117 -2
  3. data/CONTRIBUTING.md +83 -0
  4. data/README.md +108 -136
  5. data/SECURITY.md +25 -0
  6. data/bin/kit +4 -1
  7. data/docs/architecture/decisions/0001-product-boundary.md +23 -0
  8. data/docs/architecture/decisions/0002-core-and-interfaces.md +23 -0
  9. data/docs/architecture/decisions/0003-configuration-state-and-secrets.md +23 -0
  10. data/docs/architecture/decisions/0004-supported-platforms.md +19 -0
  11. data/docs/architecture/decisions/0005-operation-semantics.md +19 -0
  12. data/docs/architecture/decisions/0006-tui-backend.md +21 -0
  13. data/docs/architecture/provider-adapters.md +49 -0
  14. data/docs/architecture.md +89 -0
  15. data/docs/commands.md +210 -0
  16. data/docs/configuration.md +210 -0
  17. data/docs/getting-started.md +133 -0
  18. data/docs/providers/digitalocean.md +52 -0
  19. data/docs/releasing.md +43 -0
  20. data/docs/roadmap.md +46 -0
  21. data/docs/security-audit.md +130 -0
  22. data/docs/security.md +116 -0
  23. data/docs/services/postgres.md +78 -0
  24. data/docs/services/redis.md +53 -0
  25. data/docs/testing.md +200 -0
  26. data/docs/troubleshooting.md +123 -0
  27. data/docs/tui.md +57 -0
  28. data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
  29. data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
  30. data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
  31. data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
  32. data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
  33. data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
  34. data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
  35. data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
  36. data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
  37. data/lib/kitsune/kit/adapters/provider.rb +34 -0
  38. data/lib/kitsune/kit/adapters/transport.rb +22 -0
  39. data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
  40. data/lib/kitsune/kit/application.rb +170 -0
  41. data/lib/kitsune/kit/cancellation.rb +21 -0
  42. data/lib/kitsune/kit/cli.rb +802 -64
  43. data/lib/kitsune/kit/clock.rb +11 -0
  44. data/lib/kitsune/kit/configuration.rb +493 -0
  45. data/lib/kitsune/kit/errors.rb +98 -0
  46. data/lib/kitsune/kit/events.rb +55 -0
  47. data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
  48. data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
  49. data/lib/kitsune/kit/operations/ensure_service.rb +285 -0
  50. data/lib/kitsune/kit/operations/remote_script.rb +214 -0
  51. data/lib/kitsune/kit/operations/service_backup.rb +71 -0
  52. data/lib/kitsune/kit/operations/service_files.rb +127 -0
  53. data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
  54. data/lib/kitsune/kit/operations/service_state.rb +59 -0
  55. data/lib/kitsune/kit/plan.rb +72 -0
  56. data/lib/kitsune/kit/reporters/human.rb +89 -0
  57. data/lib/kitsune/kit/reporters/json.rb +65 -0
  58. data/lib/kitsune/kit/reporters/reporter.rb +11 -0
  59. data/lib/kitsune/kit/result.rb +28 -0
  60. data/lib/kitsune/kit/run_journal.rb +102 -0
  61. data/lib/kitsune/kit/run_logger.rb +37 -0
  62. data/lib/kitsune/kit/scripts/docker.sh +137 -0
  63. data/lib/kitsune/kit/scripts/firewall.sh +205 -0
  64. data/lib/kitsune/kit/scripts/metrics.sh +54 -0
  65. data/lib/kitsune/kit/scripts/ssh.sh +95 -0
  66. data/lib/kitsune/kit/scripts/swap.sh +86 -0
  67. data/lib/kitsune/kit/scripts/unattended.sh +81 -0
  68. data/lib/kitsune/kit/scripts/user.sh +114 -0
  69. data/lib/kitsune/kit/secret_filter.rb +54 -0
  70. data/lib/kitsune/kit/secret_store.rb +32 -0
  71. data/lib/kitsune/kit/secret_stores/store.rb +12 -0
  72. data/lib/kitsune/kit/service_compose.rb +72 -0
  73. data/lib/kitsune/kit/state_store.rb +158 -0
  74. data/lib/kitsune/kit/state_stores/store.rb +15 -0
  75. data/lib/kitsune/kit/tui/actions.rb +72 -0
  76. data/lib/kitsune/kit/tui/application.rb +35 -0
  77. data/lib/kitsune/kit/tui/controller.rb +162 -0
  78. data/lib/kitsune/kit/tui/renderer.rb +134 -0
  79. data/lib/kitsune/kit/tui/state.rb +18 -0
  80. data/lib/kitsune/kit/tui/store.rb +88 -0
  81. data/lib/kitsune/kit/tui/terminal.rb +95 -0
  82. data/lib/kitsune/kit/version.rb +1 -1
  83. data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
  84. data/lib/kitsune/kit/workflows/base.rb +31 -0
  85. data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
  86. data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
  87. data/lib/kitsune/kit/workflows/doctor.rb +225 -0
  88. data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
  89. data/lib/kitsune/kit/workflows/import_server.rb +100 -0
  90. data/lib/kitsune/kit/workflows/initialize_project.rb +121 -0
  91. data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
  92. data/lib/kitsune/kit/workflows/rollback.rb +54 -0
  93. data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
  94. data/lib/kitsune/kit.rb +41 -2
  95. metadata +122 -79
  96. data/.rspec +0 -3
  97. data/Rakefile +0 -8
  98. data/kitsune-kit-logo.jpg +0 -0
  99. data/lib/kitsune/blueprints/.env.template +0 -31
  100. data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
  101. data/lib/kitsune/blueprints/docker/redis.yml +0 -23
  102. data/lib/kitsune/blueprints/kit.env.template +0 -1
  103. data/lib/kitsune/kit/ansi_color.rb +0 -78
  104. data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
  105. data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
  106. data/lib/kitsune/kit/commands/dns.rb +0 -112
  107. data/lib/kitsune/kit/commands/init.rb +0 -148
  108. data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
  109. data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
  110. data/lib/kitsune/kit/commands/provision.rb +0 -43
  111. data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
  112. data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
  113. data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
  114. data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
  115. data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
  116. data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
  117. data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
  118. data/lib/kitsune/kit/commands/setup_user.rb +0 -189
  119. data/lib/kitsune/kit/commands/ssh.rb +0 -46
  120. data/lib/kitsune/kit/commands/switch_env.rb +0 -42
  121. data/lib/kitsune/kit/defaults.rb +0 -91
  122. data/lib/kitsune/kit/env_loader.rb +0 -41
  123. data/lib/kitsune/kit/options_builder.rb +0 -26
  124. data/lib/kitsune/kit/provisioner.rb +0 -107
  125. data/sig/kitsune/kit.rbs +0 -6
@@ -1,246 +0,0 @@
1
- require "thor"
2
- require "net/ssh"
3
- require "tempfile"
4
- require "fileutils"
5
- require "shellwords"
6
- require_relative "../defaults"
7
- require_relative "../options_builder"
8
-
9
- module Kitsune
10
- module Kit
11
- module Commands
12
- class SetupPostgresDocker < Thor
13
- namespace "setup_postgres_docker"
14
-
15
- class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address or hostname"
16
- class_option :ssh_port, aliases: "-p", desc: "SSH port"
17
- class_option :ssh_key_path, aliases: "-k", desc: "Path to SSH private key"
18
-
19
- desc "create", "Setup PostgreSQL using Docker Compose on remote server"
20
- def create
21
- postgres_defaults = Kitsune::Kit::Defaults.postgres
22
-
23
- if postgres_defaults[:postgres_password] == "secret"
24
- say "⚠️ Warning: You are using the default PostgreSQL password ('secret').", :yellow
25
- if ENV.fetch("KIT_ENV", "development") == "production"
26
- abort "❌ Production environment requires a secure PostgreSQL password!"
27
- else
28
- say "🔒 Please change POSTGRES_PASSWORD in your .env if needed.", :yellow
29
- end
30
- end
31
-
32
- filled_options = Kitsune::Kit::OptionsBuilder.build(
33
- options,
34
- required: [:server_ip],
35
- defaults: Kitsune::Kit::Defaults.ssh
36
- )
37
-
38
- with_ssh_connection(filled_options) do |ssh|
39
- perform_setup(ssh, postgres_defaults, filled_options)
40
- end
41
- end
42
-
43
- desc "rollback", "Remove PostgreSQL Docker setup from remote server"
44
- def rollback
45
- postgres_defaults = Kitsune::Kit::Defaults.postgres
46
-
47
- filled_options = Kitsune::Kit::OptionsBuilder.build(
48
- options,
49
- required: [:server_ip],
50
- defaults: Kitsune::Kit::Defaults.ssh
51
- )
52
-
53
- with_ssh_connection(filled_options) do |ssh|
54
- perform_rollback(ssh, postgres_defaults)
55
- end
56
- end
57
-
58
- no_commands do
59
- def with_ssh_connection(filled_options)
60
- server = filled_options[:server_ip]
61
- port = filled_options[:ssh_port]
62
- key = File.expand_path(filled_options[:ssh_key_path])
63
-
64
- say "🔑 Connecting as deploy@#{server}:#{port}", :green
65
- Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
66
- yield ssh
67
- end
68
- end
69
-
70
- def perform_setup(ssh, postgres_defaults, filled_options)
71
- local_compose = ".kitsune/docker/postgres.yml"
72
- remote_dir = "$HOME/docker/postgres"
73
- compose_remote = "#{remote_dir}/docker-compose.yml"
74
- env_remote = "#{remote_dir}/.env"
75
- marker = "/usr/local/backups/setup_postgres_docker.after"
76
-
77
- abort "❌ Missing #{local_compose}" unless File.exist?(local_compose)
78
-
79
- # 1. Create base directory securely
80
- ssh.exec!("mkdir -p #{remote_dir} && chmod 700 #{remote_dir}")
81
-
82
- # 2. Upload docker-compose.yml
83
- say "📦 Uploading docker-compose.yml to #{remote_dir}", :cyan
84
- upload_file(ssh, File.read(local_compose), compose_remote)
85
-
86
- # 3. Create .env file for docker-compose based on postgres_defaults
87
- say "📦 Creating .env file for Docker Compose...", :cyan
88
- env_content = <<~ENVFILE
89
- POSTGRES_DB=#{postgres_defaults[:postgres_db]}
90
- POSTGRES_USER=#{postgres_defaults[:postgres_user]}
91
- POSTGRES_PASSWORD=#{postgres_defaults[:postgres_password]}
92
- POSTGRES_PORT=#{postgres_defaults[:postgres_port]}
93
- POSTGRES_IMAGE=#{postgres_defaults[:postgres_image]}
94
- ENVFILE
95
- upload_file(ssh, env_content, env_remote)
96
-
97
- # 4. Secure file permissions
98
- ssh.exec!("chmod 600 #{compose_remote} #{env_remote}")
99
-
100
- # 5. Create backup marker
101
- ssh.exec!("sudo mkdir -p /usr/local/backups && sudo touch #{marker}")
102
-
103
- # 6. Validate docker-compose.yml
104
- say "🔍 Validating docker-compose.yml...", :cyan
105
- validation_output = ssh.exec!("cd #{remote_dir} && docker compose config")
106
- say validation_output, :cyan
107
-
108
- # 7. Check if container is running
109
- container_status = ssh.exec!("docker ps --filter 'name=postgres' --format '{{.Status}}'").strip
110
-
111
- if container_status.empty?
112
- say "▶️ No running container. Running docker compose up...", :cyan
113
- ssh.exec!("cd #{remote_dir} && docker compose up -d")
114
- else
115
- say "⚠️ PostgreSQL container is already running.", :yellow
116
- if yes?("🔁 Recreate the container with updated configuration? [y/N]", :yellow)
117
- say "🔄 Recreating container...", :cyan
118
- ssh.exec!("cd #{remote_dir} && docker compose down -v && docker compose up -d")
119
- else
120
- say "⏩ Keeping existing container.", :cyan
121
- end
122
- end
123
-
124
- # 8. Check container status
125
- say "📋 Final container status (docker compose ps):", :cyan
126
- docker_ps_output = ssh.exec!("cd #{remote_dir} && docker compose ps --format json")
127
-
128
- if docker_ps_output.nil? || docker_ps_output.strip.empty? || docker_ps_output.include?("no configuration file")
129
- say "⚠️ docker compose ps returned no valid output.", :yellow
130
- else
131
- begin
132
- services = JSON.parse(docker_ps_output)
133
- services = [services] if services.is_a?(Hash)
134
-
135
- postgres = services.find { |svc| svc["Service"] == "postgres" }
136
- status = postgres && postgres["State"]
137
- health = postgres && postgres["Health"]
138
-
139
- if (status == "running" && health == "healthy") || (health == "healthy")
140
- say "✅ PostgreSQL container is running and healthy.", :green
141
- else
142
- say "⚠️ PostgreSQL container is not healthy yet.", :yellow
143
- end
144
- rescue JSON::ParserError => e
145
- say "🚨 Failed to parse docker compose ps output as JSON: #{e.message}", :red
146
- end
147
- end
148
-
149
- # 9. Check PostgreSQL readiness with retries
150
- say "🔍 Checking PostgreSQL health with retries...", :cyan
151
-
152
- max_attempts = 10
153
- attempt = 0
154
- success = false
155
-
156
- while attempt < max_attempts
157
- attempt += 1
158
- healthcheck = ssh.exec!("docker exec $(docker ps -qf name=postgres) pg_isready -U #{postgres_defaults[:postgres_user]} -d #{postgres_defaults[:postgres_db]} -h localhost")
159
-
160
- if healthcheck.include?("accepting connections")
161
- say "✅ PostgreSQL is up and accepting connections! (attempt #{attempt})", :green
162
- success = true
163
-
164
- database_url = build_database_url(filled_options, postgres_defaults)
165
- say "🔗 Your DATABASE_URL is:\t", :cyan
166
- say database_url, :green
167
- break
168
- else
169
- say "⏳ PostgreSQL not ready yet, retrying in 5 seconds... (#{attempt}/#{max_attempts})", :yellow
170
- sleep 5
171
- end
172
- end
173
-
174
- unless success
175
- say "❌ PostgreSQL did not become ready after #{max_attempts} attempts.", :red
176
- end
177
-
178
- # 10. Allow PostgreSQL port through firewall (ufw)
179
- say "🛡️ Configuring firewall to allow PostgreSQL (port #{postgres_defaults[:postgres_port]})...", :cyan
180
- output = ssh.exec! <<~EOH
181
- if command -v ufw >/dev/null; then
182
- if ! sudo ufw status | grep -q "#{postgres_defaults[:postgres_port]}"; then
183
- sudo ufw allow #{postgres_defaults[:postgres_port]}
184
- else
185
- echo "💡 Port #{postgres_defaults[:postgres_port]} is already allowed in ufw."
186
- fi
187
- else
188
- echo "⚠️ ufw not found. Skipping firewall configuration."
189
- fi
190
- EOH
191
- say output
192
-
193
- say "✅ PostgreSQL setup completed successfully!", :green
194
- end
195
-
196
- def perform_rollback(ssh, postgres_defaults)
197
- output = ssh.exec! <<~EOH
198
- set -e
199
-
200
- BASE_DIR="$HOME/docker/postgres"
201
- BACKUP_DIR="/usr/local/backups"
202
- SCRIPT_ID="setup_postgres_docker"
203
- AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
204
-
205
- if [ -f "$AFTER_FILE" ]; then
206
- echo "🔁 Stopping and removing docker containers..."
207
- cd "$BASE_DIR"
208
- docker compose down -v || true
209
-
210
- echo "🧹 Cleaning up files..."
211
- rm -rf "$BASE_DIR"
212
- sudo rm -f "$AFTER_FILE"
213
-
214
- if command -v ufw >/dev/null; then
215
- echo "🛡️ Removing PostgreSQL port from firewall..."
216
- sudo ufw delete allow #{postgres_defaults[:postgres_port]} || true
217
- fi
218
- else
219
- echo "💡 Nothing to rollback"
220
- fi
221
-
222
- echo "✅ Rollback completed"
223
- EOH
224
- say output
225
- end
226
-
227
- def upload_file(ssh, content, remote_path)
228
- escaped = Shellwords.escape(content)
229
- ssh.exec!("mkdir -p #{File.dirname(remote_path)}")
230
- ssh.exec!("echo #{escaped} > #{remote_path}")
231
- end
232
-
233
- def build_database_url(filled_options, postgres_defaults)
234
- user = postgres_defaults[:postgres_user]
235
- password = postgres_defaults[:postgres_password]
236
- host = filled_options[:server_ip]
237
- port = postgres_defaults[:postgres_port]
238
- db = postgres_defaults[:postgres_db]
239
-
240
- "postgres://#{user}:#{password}@#{host}:#{port}/#{db}"
241
- end
242
- end
243
- end
244
- end
245
- end
246
- end
@@ -1,241 +0,0 @@
1
- require "thor"
2
- require "net/ssh"
3
- require "fileutils"
4
- require "shellwords"
5
- require_relative "../defaults"
6
- require_relative "../options_builder"
7
- require "pry"
8
- module Kitsune
9
- module Kit
10
- module Commands
11
- class SetupRedisDocker < Thor
12
- namespace "setup_redis_docker"
13
-
14
- class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address or hostname"
15
- class_option :ssh_port, aliases: "-p", desc: "SSH port"
16
- class_option :ssh_key_path, aliases: "-k", desc: "Path to SSH private key"
17
-
18
- desc "create", "Setup Redis using Docker Compose on remote server"
19
- def create
20
- redis_defaults = Kitsune::Kit::Defaults.redis
21
-
22
- if redis_defaults[:redis_password] == "secret"
23
- say "⚠️ Warning: You are using the default Redis password ('secret').", :yellow
24
- if ENV.fetch("KIT_ENV", "development") == "production"
25
- abort "❌ Production environment requires a secure Redis password!"
26
- else
27
- say "🔒 Please change REDIS_PASSWORD in your .env if needed.", :yellow
28
- end
29
- end
30
-
31
- filled_options = Kitsune::Kit::OptionsBuilder.build(
32
- options,
33
- required: [:server_ip],
34
- defaults: Kitsune::Kit::Defaults.ssh
35
- )
36
-
37
- with_ssh_connection(filled_options) do |ssh|
38
- perform_setup(ssh, redis_defaults, filled_options)
39
- end
40
- end
41
-
42
- desc "rollback", "Remove Redis Docker setup from remote server"
43
- def rollback
44
- redis_defaults = Kitsune::Kit::Defaults.redis
45
-
46
- filled_options = Kitsune::Kit::OptionsBuilder.build(
47
- options,
48
- required: [:server_ip],
49
- defaults: Kitsune::Kit::Defaults.ssh
50
- )
51
-
52
- with_ssh_connection(filled_options) do |ssh|
53
- perform_rollback(ssh, redis_defaults)
54
- end
55
- end
56
-
57
- no_commands do
58
- def with_ssh_connection(filled_options)
59
- server = filled_options[:server_ip]
60
- port = filled_options[:ssh_port]
61
- key = File.expand_path(filled_options[:ssh_key_path])
62
-
63
- say "🔑 Connecting as deploy@#{server}:#{port}", :green
64
- Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
65
- yield ssh
66
- end
67
- end
68
-
69
- def perform_setup(ssh, redis_defaults, filled_options)
70
- local_compose = ".kitsune/docker/redis.yml"
71
- remote_dir = "$HOME/docker/redis"
72
- compose_remote = "#{remote_dir}/docker-compose.yml"
73
- env_remote = "#{remote_dir}/.env"
74
- marker = "/usr/local/backups/setup_redis_docker.after"
75
-
76
- abort "❌ Missing #{local_compose}" unless File.exist?(local_compose)
77
-
78
- # 1. Create base directory securely
79
- ssh.exec!("mkdir -p #{remote_dir} && chmod 700 #{remote_dir}")
80
-
81
- # 2. Upload docker-compose.yml
82
- say "📦 Uploading docker-compose.yml to #{remote_dir}", :cyan
83
- upload_file(ssh, File.read(local_compose), compose_remote)
84
-
85
- # 3. Create .env file for docker-compose based on redis_defaults
86
- env_content = <<~ENVFILE
87
- REDIS_PORT=#{redis_defaults[:redis_port]}
88
- REDIS_PASSWORD=#{redis_defaults[:redis_password]}
89
- ENVFILE
90
- upload_file(ssh, env_content, env_remote)
91
-
92
- # 4. Secure file permissions
93
- ssh.exec!("chmod 600 #{compose_remote} #{env_remote}")
94
-
95
- # 5. Create a backup marker
96
- ssh.exec!("sudo mkdir -p /usr/local/backups && sudo touch #{marker}")
97
-
98
- # 6. Validate docker-compose.yml
99
- say "🔍 Validating docker-compose.yml...", :cyan
100
- validation_output = ssh.exec!("cd #{remote_dir} && docker compose config")
101
- say validation_output, :cyan
102
-
103
- # 7. Check if container is running
104
- container_status = ssh.exec!("docker ps --filter 'name=redis' --format '{{.Status}}'").strip
105
-
106
- if container_status.empty?
107
- say "▶️ No running container. Running docker compose up...", :cyan
108
- ssh.exec!("cd #{remote_dir} && docker compose up -d")
109
- else
110
- say "⚠️ Redis container is already running.", :yellow
111
- if yes?("🔁 Recreate the container with updated configuration? [y/N]", :yellow)
112
- say "🔄 Recreating container...", :cyan
113
- ssh.exec!("cd #{remote_dir} && docker compose down -v && docker compose up -d")
114
- else
115
- say "⏩ Keeping existing container.", :cyan
116
- end
117
- end
118
-
119
- # 8. Check container status
120
- say "📋 Final container status (docker compose ps):", :cyan
121
- docker_ps_output = ssh.exec!("cd #{remote_dir} && docker compose ps --format json")
122
-
123
- if docker_ps_output.nil? || docker_ps_output.strip.empty? || docker_ps_output.include?("no configuration file")
124
- say "⚠️ docker compose ps returned no valid output.", :yellow
125
- else
126
- begin
127
- services = JSON.parse(docker_ps_output)
128
- services = [services] if services.is_a?(Hash)
129
-
130
- redis = services.find { |svc| svc["Service"] == "redis" }
131
- status = redis && redis["State"]
132
- health = redis && redis["Health"]
133
-
134
- if (status == "running" && health == "healthy") || (health == "healthy")
135
- say "✅ Redis container is running and healthy.", :green
136
- else
137
- say "⚠️ Redis container is not healthy yet.", :yellow
138
- end
139
- rescue JSON::ParserError => e
140
- say "🚨 Failed to parse docker compose ps output as JSON: #{e.message}", :red
141
- end
142
- end
143
-
144
- # 9. Check Redis readiness with retries
145
- say "🔍 Checking Redis health with retries...", :cyan
146
-
147
- max_attempts = 10
148
- attempt = 0
149
- success = false
150
-
151
- while attempt < max_attempts
152
- attempt += 1
153
- healthcheck = ssh.exec!("docker exec $(docker ps -qf name=redis) redis-cli --no-auth-warning -a #{redis_defaults[:redis_password]} PING")
154
-
155
- if healthcheck.strip == "PONG"
156
- say "✅ Redis is up and responding to PING! (attempt #{attempt})", :green
157
- success = true
158
-
159
- redis_url = build_redis_url(filled_options, redis_defaults)
160
- say "🔗 Your REDIS_URL is:\t", :cyan
161
- say redis_url, :green
162
- break
163
- else
164
- say "⏳ Redis not ready yet, retrying in 5 seconds... (#{attempt}/#{max_attempts})", :yellow
165
- sleep 5
166
- end
167
- end
168
-
169
- unless success
170
- say "❌ Redis did not become ready after #{max_attempts} attempts.", :red
171
- end
172
-
173
- # 10. Allow Redis port through firewall (ufw)
174
- say "🛡️ Configuring firewall to allow Redis (port #{redis_defaults[:redis_port]})...", :cyan
175
- output = ssh.exec! <<~EOH
176
- if command -v ufw >/dev/null; then
177
- if ! sudo ufw status | grep -q "#{redis_defaults[:redis_port]}"; then
178
- sudo ufw allow #{redis_defaults[:redis_port]}
179
- else
180
- echo "💡 Port #{redis_defaults[:redis_port]} is already allowed in ufw."
181
- fi
182
- else
183
- echo "⚠️ ufw not found. Skipping firewall configuration."
184
- fi
185
- EOH
186
- say output
187
-
188
- say "✅ Redis setup completed successfully!", :green
189
- end
190
-
191
- def perform_rollback(ssh, defaults)
192
- output = ssh.exec! <<~EOH
193
- set -e
194
-
195
- BASE_DIR="$HOME/docker/redis"
196
- BACKUP_DIR="/usr/local/backups"
197
- SCRIPT_ID="setup_redis_docker"
198
- AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
199
-
200
- if [ -f "$AFTER_FILE" ]; then
201
- echo "🔁 Stopping and removing docker containers..."
202
- cd "$BASE_DIR"
203
- docker compose down -v || true
204
-
205
- echo "🧹 Cleaning up files..."
206
- rm -rf "$BASE_DIR"
207
- sudo rm -f "$AFTER_FILE"
208
-
209
- if command -v ufw >/dev/null; then
210
- echo "🛡️ Removing Redis port from firewall..."
211
- sudo ufw delete allow #{defaults[:redis_port]} || true
212
- fi
213
- else
214
- echo "💡 Nothing to rollback"
215
- fi
216
-
217
- echo "✅ Rollback completed"
218
- EOH
219
-
220
- say output
221
- end
222
-
223
-
224
- def upload_file(ssh, content, remote_path)
225
- escaped = Shellwords.escape(content)
226
- ssh.exec!("mkdir -p #{File.dirname(remote_path)}")
227
- ssh.exec!("echo #{escaped} > #{remote_path}")
228
- end
229
-
230
- def build_redis_url(filled_options, redis_defaults)
231
- password = redis_defaults[:redis_password]
232
- host = filled_options[:server_ip]
233
- port = redis_defaults[:redis_port]
234
-
235
- "redis://:#{password}@#{host}:#{port}/0"
236
- end
237
- end
238
- end
239
- end
240
- end
241
- end
@@ -1,151 +0,0 @@
1
- require "thor"
2
- require "net/ssh"
3
- require_relative "../defaults"
4
- require_relative "../options_builder"
5
-
6
- module Kitsune
7
- module Kit
8
- module Commands
9
- class SetupSwap < Thor
10
- namespace "setup_swap"
11
-
12
- class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address"
13
- class_option :ssh_port, aliases: "-p", desc: "SSH port"
14
- class_option :ssh_key_path, aliases: "-k", desc: "SSH private key path"
15
- class_option :size_gb, type: :numeric, desc: "Swap size in GB"
16
- class_option :swappiness, type: :numeric, desc: "vm.swappiness value (0-100)"
17
-
18
- desc "create", "Create and activate a swap file on the remote server"
19
- def create
20
- if Kitsune::Kit::Defaults.system[:disable_swap]
21
- say "⚠️ Swap setup is disabled via DISABLE_SWAP=true", :yellow
22
- return
23
- end
24
-
25
- filled_options = Kitsune::Kit::OptionsBuilder.build(
26
- options,
27
- required: [:server_ip],
28
- defaults: Kitsune::Kit::Defaults.system.merge(Kitsune::Kit::Defaults.ssh)
29
- )
30
-
31
- with_ssh_connection(filled_options) do |ssh|
32
- perform_setup(ssh, filled_options)
33
- end
34
- end
35
-
36
- desc "rollback", "Disable and remove swap file from remote server"
37
- def rollback
38
- filled_options = Kitsune::Kit::OptionsBuilder.build(
39
- options,
40
- required: [:server_ip],
41
- defaults: Kitsune::Kit::Defaults.system.merge(Kitsune::Kit::Defaults.ssh)
42
- )
43
-
44
- with_ssh_connection(filled_options) do |ssh|
45
- perform_rollback(ssh)
46
- end
47
- end
48
-
49
- no_commands do
50
- def with_ssh_connection(filled)
51
- server = filled[:server_ip]
52
- port = filled[:ssh_port]
53
- key = File.expand_path(filled[:ssh_key_path])
54
-
55
- say "🔑 Connecting as deploy@#{server}:#{port}", :green
56
- Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
57
- yield ssh
58
- end
59
- end
60
-
61
- def perform_setup(ssh, filled_options)
62
- size_gb = filled_options[:swap_size_gb].to_i
63
- swappiness = filled_options[:swap_swappiness].to_i
64
-
65
- abort "❌ Invalid swap size" if size_gb <= 0
66
-
67
- script = <<~EOH
68
- set -e
69
-
70
- BACKUP_DIR="/usr/local/backups"
71
- MARKER_FILE="${BACKUP_DIR}/setup_swap.after"
72
- SWAPPINESS_BEFORE_FILE="${BACKUP_DIR}/setup_swap.swappiness.before"
73
-
74
- if [ -f "$MARKER_FILE" ]; then
75
- echo "🔁 Swap already set up, skipping."
76
- exit 0
77
- fi
78
-
79
- SWAPFILE="/swapfile"
80
- SIZE_GB=#{size_gb}
81
- SIZE_BYTES=$((SIZE_GB * 1024 * 1024 * 1024))
82
-
83
- echo "📝 Creating ${SIZE_GB}GB swap file..."
84
- sudo fallocate -l ${SIZE_BYTES} ${SWAPFILE}
85
- sudo chmod 600 ${SWAPFILE}
86
- sudo mkswap ${SWAPFILE}
87
- sudo swapon ${SWAPFILE}
88
- echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
89
-
90
- # Backup swappiness if not already backed up
91
- if [ ! -f "$SWAPPINESS_BEFORE_FILE" ]; then
92
- CURRENT=$(cat /proc/sys/vm/swappiness)
93
- echo "$CURRENT" | sudo tee "$SWAPPINESS_BEFORE_FILE"
94
- echo "✍🏻 Backed up vm.swappiness: $CURRENT"
95
- fi
96
-
97
- echo "🛠️ Setting vm.swappiness=#{swappiness}"
98
- sudo sysctl vm.swappiness=#{swappiness}
99
- echo "vm.swappiness=#{swappiness}" | sudo tee -a /etc/sysctl.conf
100
-
101
- sudo mkdir -p "$BACKUP_DIR"
102
- sudo touch "$MARKER_FILE"
103
-
104
- echo "✅ Swap file created and swappiness set"
105
- free -h
106
- EOH
107
-
108
- say ssh.exec!(script)
109
- end
110
-
111
- def perform_rollback(ssh)
112
- script = <<~EOH
113
- set -e
114
-
115
- BACKUP_DIR="/usr/local/backups"
116
- MARKER_FILE="${BACKUP_DIR}/setup_swap.after"
117
- SWAPPINESS_BEFORE_FILE="${BACKUP_DIR}/setup_swap.swappiness.before"
118
-
119
- if [ ! -f "$MARKER_FILE" ]; then
120
- echo "💡 No swap marker found, skipping rollback."
121
- exit 0
122
- fi
123
-
124
- echo "🧹 Removing swap..."
125
- sudo swapoff /swapfile || true
126
- sudo rm -f /swapfile
127
- sudo sed -i '/\\/swapfile none swap sw 0 0/d' /etc/fstab
128
- sudo rm -f "$MARKER_FILE"
129
-
130
- if [ -f "$SWAPPINESS_BEFORE_FILE" ]; then
131
- ORIGINAL=$(cat "$SWAPPINESS_BEFORE_FILE")
132
- echo "🔁 Restoring vm.swappiness: $ORIGINAL"
133
- sudo sysctl vm.swappiness=$ORIGINAL
134
- sudo sed -i '/vm.swappiness=/d' /etc/sysctl.conf
135
- echo "vm.swappiness=$ORIGINAL" | sudo tee -a /etc/sysctl.conf
136
- sudo rm -f "$SWAPPINESS_BEFORE_FILE"
137
- else
138
- echo "💡 No swappiness backup found, skipping restore."
139
- fi
140
-
141
- echo "✅ Swap rollback completed"
142
- free -h
143
- EOH
144
-
145
- say ssh.exec!(script)
146
- end
147
- end
148
- end
149
- end
150
- end
151
- end