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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +117 -2
- data/CONTRIBUTING.md +83 -0
- data/README.md +108 -136
- data/SECURITY.md +25 -0
- data/bin/kit +4 -1
- data/docs/architecture/decisions/0001-product-boundary.md +23 -0
- data/docs/architecture/decisions/0002-core-and-interfaces.md +23 -0
- data/docs/architecture/decisions/0003-configuration-state-and-secrets.md +23 -0
- data/docs/architecture/decisions/0004-supported-platforms.md +19 -0
- data/docs/architecture/decisions/0005-operation-semantics.md +19 -0
- data/docs/architecture/decisions/0006-tui-backend.md +21 -0
- data/docs/architecture/provider-adapters.md +49 -0
- data/docs/architecture.md +89 -0
- data/docs/commands.md +210 -0
- data/docs/configuration.md +210 -0
- data/docs/getting-started.md +133 -0
- data/docs/providers/digitalocean.md +52 -0
- data/docs/releasing.md +43 -0
- data/docs/roadmap.md +46 -0
- data/docs/security-audit.md +130 -0
- data/docs/security.md +116 -0
- data/docs/services/postgres.md +78 -0
- data/docs/services/redis.md +53 -0
- data/docs/testing.md +200 -0
- data/docs/troubleshooting.md +123 -0
- data/docs/tui.md +57 -0
- data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
- data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
- data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
- data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
- data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
- data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
- data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
- data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
- data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
- data/lib/kitsune/kit/adapters/provider.rb +34 -0
- data/lib/kitsune/kit/adapters/transport.rb +22 -0
- data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
- data/lib/kitsune/kit/application.rb +170 -0
- data/lib/kitsune/kit/cancellation.rb +21 -0
- data/lib/kitsune/kit/cli.rb +802 -64
- data/lib/kitsune/kit/clock.rb +11 -0
- data/lib/kitsune/kit/configuration.rb +493 -0
- data/lib/kitsune/kit/errors.rb +98 -0
- data/lib/kitsune/kit/events.rb +55 -0
- data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
- data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
- data/lib/kitsune/kit/operations/ensure_service.rb +285 -0
- data/lib/kitsune/kit/operations/remote_script.rb +214 -0
- data/lib/kitsune/kit/operations/service_backup.rb +71 -0
- data/lib/kitsune/kit/operations/service_files.rb +127 -0
- data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
- data/lib/kitsune/kit/operations/service_state.rb +59 -0
- data/lib/kitsune/kit/plan.rb +72 -0
- data/lib/kitsune/kit/reporters/human.rb +89 -0
- data/lib/kitsune/kit/reporters/json.rb +65 -0
- data/lib/kitsune/kit/reporters/reporter.rb +11 -0
- data/lib/kitsune/kit/result.rb +28 -0
- data/lib/kitsune/kit/run_journal.rb +102 -0
- data/lib/kitsune/kit/run_logger.rb +37 -0
- data/lib/kitsune/kit/scripts/docker.sh +137 -0
- data/lib/kitsune/kit/scripts/firewall.sh +205 -0
- data/lib/kitsune/kit/scripts/metrics.sh +54 -0
- data/lib/kitsune/kit/scripts/ssh.sh +95 -0
- data/lib/kitsune/kit/scripts/swap.sh +86 -0
- data/lib/kitsune/kit/scripts/unattended.sh +81 -0
- data/lib/kitsune/kit/scripts/user.sh +114 -0
- data/lib/kitsune/kit/secret_filter.rb +54 -0
- data/lib/kitsune/kit/secret_store.rb +32 -0
- data/lib/kitsune/kit/secret_stores/store.rb +12 -0
- data/lib/kitsune/kit/service_compose.rb +72 -0
- data/lib/kitsune/kit/state_store.rb +158 -0
- data/lib/kitsune/kit/state_stores/store.rb +15 -0
- data/lib/kitsune/kit/tui/actions.rb +72 -0
- data/lib/kitsune/kit/tui/application.rb +35 -0
- data/lib/kitsune/kit/tui/controller.rb +162 -0
- data/lib/kitsune/kit/tui/renderer.rb +134 -0
- data/lib/kitsune/kit/tui/state.rb +18 -0
- data/lib/kitsune/kit/tui/store.rb +88 -0
- data/lib/kitsune/kit/tui/terminal.rb +95 -0
- data/lib/kitsune/kit/version.rb +1 -1
- data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
- data/lib/kitsune/kit/workflows/base.rb +31 -0
- data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
- data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
- data/lib/kitsune/kit/workflows/doctor.rb +225 -0
- data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
- data/lib/kitsune/kit/workflows/import_server.rb +100 -0
- data/lib/kitsune/kit/workflows/initialize_project.rb +121 -0
- data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
- data/lib/kitsune/kit/workflows/rollback.rb +54 -0
- data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
- data/lib/kitsune/kit.rb +41 -2
- metadata +122 -79
- data/.rspec +0 -3
- data/Rakefile +0 -8
- data/kitsune-kit-logo.jpg +0 -0
- data/lib/kitsune/blueprints/.env.template +0 -31
- data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
- data/lib/kitsune/blueprints/docker/redis.yml +0 -23
- data/lib/kitsune/blueprints/kit.env.template +0 -1
- data/lib/kitsune/kit/ansi_color.rb +0 -78
- data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
- data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
- data/lib/kitsune/kit/commands/dns.rb +0 -112
- data/lib/kitsune/kit/commands/init.rb +0 -148
- data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
- data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
- data/lib/kitsune/kit/commands/provision.rb +0 -43
- data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
- data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
- data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
- data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
- data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
- data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
- data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
- data/lib/kitsune/kit/commands/setup_user.rb +0 -189
- data/lib/kitsune/kit/commands/ssh.rb +0 -46
- data/lib/kitsune/kit/commands/switch_env.rb +0 -42
- data/lib/kitsune/kit/defaults.rb +0 -91
- data/lib/kitsune/kit/env_loader.rb +0 -41
- data/lib/kitsune/kit/options_builder.rb +0 -26
- data/lib/kitsune/kit/provisioner.rb +0 -107
- 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
|