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,132 +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 SetupUnattended < Thor
|
|
10
|
-
namespace "setup_unattended"
|
|
11
|
-
|
|
12
|
-
class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address or hostname"
|
|
13
|
-
class_option :ssh_port, aliases: "-p", desc: "SSH port"
|
|
14
|
-
class_option :ssh_key_path, aliases: "-k", desc: "Path to your private SSH key"
|
|
15
|
-
|
|
16
|
-
desc "create", "Configure unattended-upgrades on the remote server"
|
|
17
|
-
def create
|
|
18
|
-
filled_options = Kitsune::Kit::OptionsBuilder.build(
|
|
19
|
-
options,
|
|
20
|
-
required: [:server_ip],
|
|
21
|
-
defaults: Kitsune::Kit::Defaults.ssh
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
with_ssh_connection(filled_options) do |ssh|
|
|
25
|
-
perform_setup(ssh)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
desc "rollback", "Revert unattended-upgrades configuration on the remote server"
|
|
30
|
-
def rollback
|
|
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_rollback(ssh)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
no_commands do
|
|
43
|
-
def with_ssh_connection(filled_options)
|
|
44
|
-
server = filled_options[:server_ip]
|
|
45
|
-
port = filled_options[:ssh_port]
|
|
46
|
-
key = File.expand_path(filled_options[:ssh_key_path])
|
|
47
|
-
|
|
48
|
-
say "🔑 Connecting as deploy@#{server}:#{port}", :green
|
|
49
|
-
Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
|
|
50
|
-
yield ssh
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def perform_setup(ssh)
|
|
55
|
-
output = ssh.exec! <<~EOH
|
|
56
|
-
set -e
|
|
57
|
-
|
|
58
|
-
sudo mkdir -p /usr/local/backups
|
|
59
|
-
sudo chown deploy:deploy /usr/local/backups
|
|
60
|
-
|
|
61
|
-
RESOURCE="/etc/apt/apt.conf.d/20auto-upgrades"
|
|
62
|
-
BACKUP_DIR="/usr/local/backups"
|
|
63
|
-
SCRIPT_ID="setup_unattended"
|
|
64
|
-
BACKUP_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
|
|
65
|
-
MARKER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
|
|
66
|
-
|
|
67
|
-
echo "✍🏻 Installing required packages…"
|
|
68
|
-
if ! dpkg -l | grep -q "^ii\\s*unattended-upgrades"; then
|
|
69
|
-
sudo apt-get update -y
|
|
70
|
-
sudo apt-get install -y unattended-upgrades apt-listchanges && echo " - packages installed"
|
|
71
|
-
else
|
|
72
|
-
echo " - unattended-upgrades already installed"
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
if [ ! -f "$MARKER_FILE" ]; then
|
|
76
|
-
echo "✍🏻 Backing up existing config…"
|
|
77
|
-
sudo cp "$RESOURCE" "$BACKUP_FILE" && echo " - backup saved to $BACKUP_FILE"
|
|
78
|
-
sudo touch "$MARKER_FILE" && echo " - marker created at $MARKER_FILE"
|
|
79
|
-
else
|
|
80
|
-
echo " - backup & marker already exist"
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
echo "✍🏻 Applying new auto-upgrades config…"
|
|
84
|
-
sudo tee "$RESOURCE" > /dev/null <<UPGR
|
|
85
|
-
APT::Periodic::Update-Package-Lists "1";
|
|
86
|
-
APT::Periodic::Download-Upgradeable-Packages "1";
|
|
87
|
-
APT::Periodic::AutocleanInterval "7";
|
|
88
|
-
APT::Periodic::Unattended-Upgrade "1";
|
|
89
|
-
UPGR
|
|
90
|
-
echo " - config applied"
|
|
91
|
-
|
|
92
|
-
echo "✍🏻 Enabling & restarting unattended-upgrades…"
|
|
93
|
-
sudo systemctl --quiet enable unattended-upgrades.service >/dev/null 2>&1 && echo " - service enabled"
|
|
94
|
-
sudo systemctl --quiet restart unattended-upgrades.service && echo " - service restarted"
|
|
95
|
-
EOH
|
|
96
|
-
say output
|
|
97
|
-
say "✅ Unattended-upgrades setup completed", :green
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def perform_rollback(ssh)
|
|
101
|
-
output = ssh.exec! <<~EOH
|
|
102
|
-
set -e
|
|
103
|
-
|
|
104
|
-
sudo mkdir -p /usr/local/backups
|
|
105
|
-
sudo chown deploy:deploy /usr/local/backups
|
|
106
|
-
|
|
107
|
-
RESOURCE="/etc/apt/apt.conf.d/20auto-upgrades"
|
|
108
|
-
BACKUP_DIR="/usr/local/backups"
|
|
109
|
-
SCRIPT_ID="setup_unattended"
|
|
110
|
-
BACKUP_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
|
|
111
|
-
MARKER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
|
|
112
|
-
|
|
113
|
-
if [ -f "$MARKER_FILE" ]; then
|
|
114
|
-
echo "🔁 Restoring original auto-upgrades config…"
|
|
115
|
-
sudo mv "$BACKUP_FILE" "$RESOURCE" && echo " - config restored from $BACKUP_FILE"
|
|
116
|
-
sudo rm -f "$MARKER_FILE" && echo " - marker $MARKER_FILE removed"
|
|
117
|
-
else
|
|
118
|
-
echo " - no marker for $SCRIPT_ID, skipping restore"
|
|
119
|
-
fi
|
|
120
|
-
|
|
121
|
-
echo "✍🏻 Stopping & disabling unattended-upgrades…"
|
|
122
|
-
sudo systemctl --quiet stop unattended-upgrades.service apt-daily.timer apt-daily-upgrade.timer && echo " - services stopped"
|
|
123
|
-
sudo systemctl --quiet disable unattended-upgrades.service && echo " - service disabled"
|
|
124
|
-
EOH
|
|
125
|
-
say output
|
|
126
|
-
say "✅ Unattended-upgrades rollback completed", :green
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
@@ -1,189 +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 SetupUser < Thor
|
|
10
|
-
namespace "setup_user"
|
|
11
|
-
|
|
12
|
-
class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address or hostname"
|
|
13
|
-
class_option :ssh_port, aliases: "-p", desc: "SSH port"
|
|
14
|
-
class_option :ssh_key_path, aliases: "-k", desc: "Path to your private SSH key"
|
|
15
|
-
|
|
16
|
-
desc "create", "Create and configure 'deploy' user on remote server"
|
|
17
|
-
def create
|
|
18
|
-
filled_options = Kitsune::Kit::OptionsBuilder.build(
|
|
19
|
-
options,
|
|
20
|
-
required: [:server_ip],
|
|
21
|
-
defaults: Kitsune::Kit::Defaults.ssh
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
with_ssh_connection(false, filled_options) do |ssh|
|
|
25
|
-
perform_setup(ssh)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
desc "rollback", "Revert configuration and remove 'deploy' user from remote server"
|
|
30
|
-
def rollback
|
|
31
|
-
filled_options = Kitsune::Kit::OptionsBuilder.build(
|
|
32
|
-
options,
|
|
33
|
-
required: [:server_ip],
|
|
34
|
-
defaults: Kitsune::Kit::Defaults.ssh
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
server = filled_options[:server_ip]
|
|
38
|
-
port = filled_options[:ssh_port]
|
|
39
|
-
key = File.expand_path(filled_options[:ssh_key_path])
|
|
40
|
-
|
|
41
|
-
# First, attempt SSH config restore as 'deploy'
|
|
42
|
-
begin
|
|
43
|
-
with_ssh_connection(true, filled_options) do |ssh|
|
|
44
|
-
perform_rollback_config(ssh)
|
|
45
|
-
end
|
|
46
|
-
rescue StandardError => e
|
|
47
|
-
say "⚠️ Skipping SSH config restore: #{e.message}", :yellow
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Then reconnect as 'root' to remove sudoers and delete user
|
|
51
|
-
say "🔑 Reconnecting as root@#{server}:#{port}", :green
|
|
52
|
-
Net::SSH.start(server, 'root', port: port, keys: [key], non_interactive: true) do |ssh|
|
|
53
|
-
perform_rollback_cleanup(ssh)
|
|
54
|
-
end
|
|
55
|
-
say "✅ Rollback completed", :green
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
no_commands do
|
|
59
|
-
def with_ssh_connection(rollback, 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
|
-
user = rollback ? 'deploy' : detect_remote_user(server, port, key)
|
|
65
|
-
say "🔑 Connecting as #{user}@#{server}:#{port}", :green
|
|
66
|
-
|
|
67
|
-
Net::SSH.start(server, user, port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
|
|
68
|
-
yield ssh
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def detect_remote_user(server, port, key)
|
|
73
|
-
%w[deploy root].each do |u|
|
|
74
|
-
begin
|
|
75
|
-
Net::SSH.start(server, u, port: port, keys: [key], non_interactive: true, timeout: 5) { }
|
|
76
|
-
say "✔️ Able to SSH as #{u}", :green
|
|
77
|
-
return u
|
|
78
|
-
rescue
|
|
79
|
-
next
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
abort("❌ Could not connect as deploy or root on #{server}:#{port}")
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def perform_setup(ssh)
|
|
86
|
-
output = ssh.exec! <<~'EOH'
|
|
87
|
-
set -e
|
|
88
|
-
|
|
89
|
-
echo "✍🏻 Creating deploy user…"
|
|
90
|
-
if ! id deploy &>/dev/null; then
|
|
91
|
-
if command -v adduser &>/dev/null; then
|
|
92
|
-
sudo adduser --disabled-password --gecos "" deploy && echo " - user 'deploy' created"
|
|
93
|
-
else
|
|
94
|
-
sudo useradd -m -s /bin/bash deploy && echo " - user 'deploy' created"
|
|
95
|
-
fi
|
|
96
|
-
sudo usermod -aG sudo deploy && echo " - 'deploy' added to sudo"
|
|
97
|
-
else
|
|
98
|
-
echo " - user 'deploy' already exists"
|
|
99
|
-
fi
|
|
100
|
-
|
|
101
|
-
echo "✍🏻 Configuring passwordless sudo…"
|
|
102
|
-
if [ ! -f /etc/sudoers.d/deploy ]; then
|
|
103
|
-
echo 'deploy ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/deploy \
|
|
104
|
-
&& sudo chmod 440 /etc/sudoers.d/deploy \
|
|
105
|
-
&& echo " - sudoers entry created"
|
|
106
|
-
else
|
|
107
|
-
echo " - sudoers entry exists"
|
|
108
|
-
fi
|
|
109
|
-
|
|
110
|
-
echo "✍🏻 Backing up SSH config…"
|
|
111
|
-
sudo test -f /etc/ssh/sshd_config.bak || sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak && echo " - sshd_config backed up"
|
|
112
|
-
|
|
113
|
-
echo "✍🏻 Hardening SSH…"
|
|
114
|
-
grep -q '^PermitRootLogin no' /etc/ssh/sshd_config \
|
|
115
|
-
|| sudo sed -i 's/^#*PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config && echo " - PermitRootLogin no"
|
|
116
|
-
grep -q '^PasswordAuthentication no' /etc/ssh/sshd_config \
|
|
117
|
-
|| sudo sed -i 's/^#*PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config && echo " - PasswordAuthentication no"
|
|
118
|
-
sudo systemctl restart sshd && echo " - sshd restarted"
|
|
119
|
-
|
|
120
|
-
echo "✍🏻 Installing SSH keys for deploy…"
|
|
121
|
-
if [ ! -f /home/deploy/.ssh/authorized_keys ]; then
|
|
122
|
-
sudo mkdir -p /home/deploy/.ssh
|
|
123
|
-
sudo cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
|
|
124
|
-
sudo chown -R deploy:deploy /home/deploy/.ssh
|
|
125
|
-
sudo chmod 700 /home/deploy/.ssh
|
|
126
|
-
sudo chmod 600 /home/deploy/.ssh/authorized_keys
|
|
127
|
-
echo " - authorized_keys copied"
|
|
128
|
-
else
|
|
129
|
-
echo " - authorized_keys already present"
|
|
130
|
-
fi
|
|
131
|
-
EOH
|
|
132
|
-
say output
|
|
133
|
-
say "✅ Setup completed", :green
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def perform_rollback_config(ssh)
|
|
137
|
-
output = ssh.exec! <<~'EOH'
|
|
138
|
-
set -e
|
|
139
|
-
|
|
140
|
-
echo "🔁 Backing up SSH config…"
|
|
141
|
-
sudo test -f /etc/ssh/sshd_config.bak || sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak && echo " - sshd_config backed up"
|
|
142
|
-
|
|
143
|
-
echo "✍🏻 Restoring SSH config…"
|
|
144
|
-
grep -q '^PermitRootLogin yes' /etc/ssh/sshd_config \
|
|
145
|
-
|| sudo sed -i 's/^#*PermitRootLogin .*/PermitRootLogin yes/' /etc/ssh/sshd_config && echo " - PermitRootLogin yes"
|
|
146
|
-
grep -q '^PasswordAuthentication yes' /etc/ssh/sshd_config \
|
|
147
|
-
|| sudo sed -i 's/^#*PasswordAuthentication .*/PasswordAuthentication yes/' /etc/ssh/sshd_config && echo " - PasswordAuthentication yes"
|
|
148
|
-
sudo systemctl restart sshd && echo " - sshd restarted"
|
|
149
|
-
EOH
|
|
150
|
-
say output
|
|
151
|
-
say "✅ SSH config restored, closing deploy session", :green
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def perform_rollback_cleanup(ssh)
|
|
155
|
-
output = ssh.exec! <<~'EOH'
|
|
156
|
-
set -e
|
|
157
|
-
|
|
158
|
-
echo "✍🏻 Removing sudoers file…"
|
|
159
|
-
if [ -f /etc/sudoers.d/deploy ]; then
|
|
160
|
-
sudo rm -f /etc/sudoers.d/deploy && echo " - /etc/sudoers.d/deploy removed"
|
|
161
|
-
else
|
|
162
|
-
echo " - no sudoers file to remove"
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
echo "✍🏻 Killing remaining processes for deploy…"
|
|
166
|
-
if id deploy &>/dev/null; then
|
|
167
|
-
sudo pkill -u deploy && echo " - processes killed" || echo " - no processes found"
|
|
168
|
-
else
|
|
169
|
-
echo " - user 'deploy' does not exist, skipping"
|
|
170
|
-
fi
|
|
171
|
-
|
|
172
|
-
echo "✍🏻 Deleting deploy user…"
|
|
173
|
-
if id deploy &>/dev/null; then
|
|
174
|
-
if command -v deluser &>/dev/null; then
|
|
175
|
-
sudo deluser --remove-home deploy && echo " - deploy user removed"
|
|
176
|
-
else
|
|
177
|
-
sudo userdel -r deploy && echo " - deploy user removed"
|
|
178
|
-
fi
|
|
179
|
-
else
|
|
180
|
-
echo " - deploy user does not exist"
|
|
181
|
-
fi
|
|
182
|
-
EOH
|
|
183
|
-
say output
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "thor"
|
|
4
|
-
require "droplet_kit"
|
|
5
|
-
|
|
6
|
-
module Kitsune
|
|
7
|
-
module Kit
|
|
8
|
-
module Commands
|
|
9
|
-
class Ssh < Thor
|
|
10
|
-
namespace "ssh"
|
|
11
|
-
default_task :connect
|
|
12
|
-
|
|
13
|
-
desc "connect", "Connect to a remote server via SSH"
|
|
14
|
-
option :ip, type: :string, desc: "Server IP address (optional)"
|
|
15
|
-
option :user, type: :string, default: "deploy", desc: "SSH user"
|
|
16
|
-
def connect
|
|
17
|
-
Kitsune::Kit::EnvLoader.load!
|
|
18
|
-
|
|
19
|
-
ip = options[:ip] || fetch_server_ip
|
|
20
|
-
user = options[:user]
|
|
21
|
-
key_path = Kitsune::Kit::Defaults.ssh[:ssh_key_path]
|
|
22
|
-
|
|
23
|
-
say "🔗 Connecting to #{user}@#{ip}...", :green
|
|
24
|
-
exec "ssh -i #{key_path} -o StrictHostKeyChecking=no #{user}@#{ip}"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
no_commands do
|
|
28
|
-
def fetch_server_ip
|
|
29
|
-
token = ENV.fetch("DO_API_TOKEN") { abort "❌ DO_API_TOKEN is missing" }
|
|
30
|
-
|
|
31
|
-
client = DropletKit::Client.new(access_token: token)
|
|
32
|
-
name = Kitsune::Kit::Defaults.infra[:droplet_name]
|
|
33
|
-
|
|
34
|
-
droplet = client.droplets.all.find { |d| d.name == name }
|
|
35
|
-
abort "❌ Droplet '#{name}' not found on DigitalOcean" unless droplet
|
|
36
|
-
|
|
37
|
-
ip = droplet.networks.v4.find { |n| n.type == "public" }&.ip_address
|
|
38
|
-
abort "❌ No public IP found for droplet '#{name}'" unless ip
|
|
39
|
-
|
|
40
|
-
ip
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
require "thor"
|
|
2
|
-
require "fileutils"
|
|
3
|
-
|
|
4
|
-
module Kitsune
|
|
5
|
-
module Kit
|
|
6
|
-
module Commands
|
|
7
|
-
class SwitchEnv < Thor
|
|
8
|
-
namespace "switch_env"
|
|
9
|
-
|
|
10
|
-
desc "to ENV_NAME", "Switch active Kitsune Kit environment (development, production, etc)"
|
|
11
|
-
def to(env_name)
|
|
12
|
-
kit_env_path = ".kitsune/kit.env"
|
|
13
|
-
infra_env_path = ".kitsune/infra.#{env_name}.env"
|
|
14
|
-
blueprint_path = File.expand_path("../../blueprints/.env.template", __dir__)
|
|
15
|
-
|
|
16
|
-
unless File.exist?(kit_env_path)
|
|
17
|
-
say "❌ No .kitsune/kit.env found. Did you run `kitsune kit init`?", :red
|
|
18
|
-
exit(1)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
content = File.read(kit_env_path)
|
|
22
|
-
|
|
23
|
-
if content.match?(/^KIT_ENV=/)
|
|
24
|
-
new_content = content.gsub(/^KIT_ENV=.*/, "KIT_ENV=#{env_name}")
|
|
25
|
-
else
|
|
26
|
-
new_content = "KIT_ENV=#{env_name}\n" + content
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
File.write(kit_env_path, new_content)
|
|
30
|
-
say "🎯 Environment switched to '#{env_name}' in .kitsune/kit.env", :green
|
|
31
|
-
|
|
32
|
-
unless File.exist?(infra_env_path)
|
|
33
|
-
FileUtils.cp(blueprint_path, infra_env_path)
|
|
34
|
-
say "📝 Created new infra environment file: #{infra_env_path}", :cyan
|
|
35
|
-
else
|
|
36
|
-
say "📄 Infra environment file already exists: #{infra_env_path}", :green
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
data/lib/kitsune/kit/defaults.rb
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
module Kitsune
|
|
2
|
-
module Kit
|
|
3
|
-
module Defaults
|
|
4
|
-
DROPLET = {
|
|
5
|
-
droplet_name: "app-dev",
|
|
6
|
-
region: "sfo3",
|
|
7
|
-
size: "s-1vcpu-1gb",
|
|
8
|
-
image: "ubuntu-22-04-x64",
|
|
9
|
-
tag: "rails-dev"
|
|
10
|
-
}.freeze
|
|
11
|
-
|
|
12
|
-
SSH = {
|
|
13
|
-
ssh_port: "22",
|
|
14
|
-
ssh_key_path: "~/.ssh/id_rsa"
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
POSTGRES = {
|
|
18
|
-
user: "postgres",
|
|
19
|
-
password: "secret",
|
|
20
|
-
port: "5432",
|
|
21
|
-
image: "postgres:17"
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
|
-
SYSTEM = {
|
|
25
|
-
swap_size_gb: 2,
|
|
26
|
-
swap_swappiness: 10,
|
|
27
|
-
disable_swap: false
|
|
28
|
-
}.freeze
|
|
29
|
-
|
|
30
|
-
METRICS = {
|
|
31
|
-
enable_do_metrics: true
|
|
32
|
-
}.freeze
|
|
33
|
-
|
|
34
|
-
REDIS = {
|
|
35
|
-
port: "6379",
|
|
36
|
-
password: "redis:7.2"
|
|
37
|
-
}.freeze
|
|
38
|
-
|
|
39
|
-
def self.infra
|
|
40
|
-
{
|
|
41
|
-
droplet_name: ENV.fetch('DROPLET_NAME', DROPLET[:droplet_name]),
|
|
42
|
-
region: ENV.fetch('REGION', DROPLET[:region]),
|
|
43
|
-
size: ENV.fetch('SIZE', DROPLET[:size]),
|
|
44
|
-
image: ENV.fetch('IMAGE', DROPLET[:image]),
|
|
45
|
-
tag: ENV.fetch('TAG_NAME', DROPLET[:tag]),
|
|
46
|
-
ssh_key_id: ENV.fetch('SSH_KEY_ID') { abort "❌ Missing SSH_KEY_ID" }
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def self.ssh
|
|
51
|
-
{
|
|
52
|
-
ssh_port: ENV.fetch('SSH_PORT', SSH[:ssh_port]),
|
|
53
|
-
ssh_key_path: ENV.fetch('SSH_KEY_PATH', SSH[:ssh_key_path])
|
|
54
|
-
}
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.postgres
|
|
58
|
-
env = ENV.fetch('KIT_ENV', 'development')
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
postgres_db: ENV.fetch('POSTGRES_DB') { "myapp_db_#{env}" },
|
|
62
|
-
postgres_user: ENV.fetch('POSTGRES_USER', POSTGRES[:user]),
|
|
63
|
-
postgres_password: ENV.fetch('POSTGRES_PASSWORD', POSTGRES[:password]),
|
|
64
|
-
postgres_port: ENV.fetch('POSTGRES_PORT', POSTGRES[:port]),
|
|
65
|
-
postgres_image: ENV.fetch('POSTGRES_IMAGE', POSTGRES[:image])
|
|
66
|
-
}
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def self.redis
|
|
70
|
-
{
|
|
71
|
-
redis_port: ENV.fetch("REDIS_PORT", REDIS[:port]),
|
|
72
|
-
redis_password: ENV.fetch("REDIS_PASSWORD", REDIS[:password])
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def self.system
|
|
77
|
-
{
|
|
78
|
-
swap_size_gb: ENV.fetch("SWAP_SIZE_GB", SYSTEM[:swap_size_gb]).to_i,
|
|
79
|
-
swap_swappiness: ENV.fetch("SWAP_SWAPPINESS", SYSTEM[:swap_swappiness]).to_i,
|
|
80
|
-
disable_swap: ENV.fetch("DISABLE_SWAP", SYSTEM[:disable_swap].to_s) == "true"
|
|
81
|
-
}
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def self.metrics
|
|
85
|
-
{
|
|
86
|
-
enable_do_metrics: ENV.fetch("ENABLE_DO_METRICS", METRICS[:enable_do_metrics].to_s) == "true"
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
require "dotenv"
|
|
2
|
-
module Kitsune
|
|
3
|
-
module Kit
|
|
4
|
-
class EnvLoader
|
|
5
|
-
@loaded = false
|
|
6
|
-
|
|
7
|
-
def self.load!
|
|
8
|
-
return if @loaded
|
|
9
|
-
|
|
10
|
-
env = ENV["KIT_ENV"] || read_kit_env || "development"
|
|
11
|
-
|
|
12
|
-
possible_paths = [
|
|
13
|
-
".kitsune/infra.#{env}.env",
|
|
14
|
-
".kitsune/infra.env"
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
found = possible_paths.find { |path| File.exist?(path) }
|
|
18
|
-
|
|
19
|
-
if found
|
|
20
|
-
Dotenv.load(found)
|
|
21
|
-
puts AnsiColor.colorize("🧪 Loaded Kitsune environment from #{found}", color: :light_cyan)
|
|
22
|
-
puts AnsiColor.colorize("=======================================================================\n", color: :light_cyan)
|
|
23
|
-
else
|
|
24
|
-
puts "⚠️ No Kitsune infra config found for environment '#{env}' (looked for infra.#{env}.env and infra.env)"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
@loaded = true
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.read_kit_env
|
|
31
|
-
path = ".kitsune/kit.env"
|
|
32
|
-
if File.exist?(path)
|
|
33
|
-
vars = Dotenv.parse(path)
|
|
34
|
-
vars["KIT_ENV"]
|
|
35
|
-
else
|
|
36
|
-
nil
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Kitsune
|
|
2
|
-
module Kit
|
|
3
|
-
class OptionsBuilder
|
|
4
|
-
def self.build(current_options, required: [], defaults: {})
|
|
5
|
-
current = current_options.transform_keys(&:to_sym)
|
|
6
|
-
|
|
7
|
-
filled = defaults.dup
|
|
8
|
-
|
|
9
|
-
defaults.keys.each do |key|
|
|
10
|
-
env_key = key.to_s.upcase
|
|
11
|
-
filled[key] = ENV[env_key] if ENV[env_key]
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
filled.merge!(current)
|
|
15
|
-
|
|
16
|
-
missing = required.select { |key| filled[key].nil? }
|
|
17
|
-
|
|
18
|
-
unless missing.empty?
|
|
19
|
-
abort "❌ Missing required options: #{missing.join(', ')}"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
filled
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
require "droplet_kit"
|
|
2
|
-
|
|
3
|
-
module Kitsune
|
|
4
|
-
module Kit
|
|
5
|
-
class Provisioner
|
|
6
|
-
def initialize(opts)
|
|
7
|
-
@droplet_name = opts[:droplet_name]
|
|
8
|
-
@region = opts[:region]
|
|
9
|
-
@size = opts[:size]
|
|
10
|
-
@image = opts[:image]
|
|
11
|
-
@tag = opts[:tag]
|
|
12
|
-
@ssh_key_id = opts[:ssh_key_id] { abort "❌ You must export SSH_KEY_ID or use --ssh_key_id" }
|
|
13
|
-
|
|
14
|
-
@client = DropletKit::Client.new(access_token: ENV.fetch("DO_API_TOKEN"))
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Find an existing droplet by name
|
|
18
|
-
def find_droplet
|
|
19
|
-
@client.droplets.all(tag_name: @tag).detect { |d| d.name == @droplet_name }
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Create command: shows if it exists or creates a new one
|
|
23
|
-
def create_or_show
|
|
24
|
-
if (d = find_droplet)
|
|
25
|
-
ip = public_ip(d)
|
|
26
|
-
puts "✅ Droplet '#{@droplet_name}' already exists (ID: #{d.id}, IP: #{ip})"
|
|
27
|
-
else
|
|
28
|
-
puts "✍🏻 Creating Droplet '#{@droplet_name}'..."
|
|
29
|
-
spec = DropletKit::Droplet.new(
|
|
30
|
-
name: @droplet_name,
|
|
31
|
-
region: @region,
|
|
32
|
-
size: @size,
|
|
33
|
-
image: @image,
|
|
34
|
-
ssh_keys: [@ssh_key_id],
|
|
35
|
-
tags: [@tag]
|
|
36
|
-
)
|
|
37
|
-
created = @client.droplets.create(spec)
|
|
38
|
-
wait_for_status(created.id)
|
|
39
|
-
ip = wait_for_public_ip(created.id)
|
|
40
|
-
|
|
41
|
-
wait_for_ssh(ip)
|
|
42
|
-
|
|
43
|
-
puts "✅ Droplet created: ID=#{created.id}, IP=#{ip}"
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Rollback command: deletes it if it exists
|
|
48
|
-
def rollback
|
|
49
|
-
if (d = find_droplet)
|
|
50
|
-
puts "🔁 Deleting Droplet '#{@droplet_name}' (ID: #{d.id})..."
|
|
51
|
-
@client.droplets.delete(id: d.id)
|
|
52
|
-
puts "✅ Droplet deleted 💥"
|
|
53
|
-
else
|
|
54
|
-
puts "✅ Nothing to delete: '#{@droplet_name}' does not exist"
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
# Extracts the public IP from a DropletKit::Droplet
|
|
61
|
-
def public_ip(droplet)
|
|
62
|
-
v4 = droplet.networks.v4.find { |n| n.type == "public" }
|
|
63
|
-
v4 ? v4.ip_address : "(no public IP yet)"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Waits until the droplet reaches the 'active' status
|
|
67
|
-
def wait_for_status(droplet_id, interval: 5, max_attempts: 24)
|
|
68
|
-
max_attempts.times do |i|
|
|
69
|
-
droplet = @client.droplets.find(id: droplet_id)
|
|
70
|
-
estado = droplet.status
|
|
71
|
-
puts "⏳ Droplet status: #{estado} (#{i + 1}/#{max_attempts})"
|
|
72
|
-
return if estado == "active"
|
|
73
|
-
sleep interval
|
|
74
|
-
end
|
|
75
|
-
abort "❌ Timeout: the Droplet did not reach 'active' status after #{interval * max_attempts} seconds"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Waits until obtaining the public IP
|
|
79
|
-
def wait_for_public_ip(droplet_id, interval: 5, max_attempts: 24)
|
|
80
|
-
max_attempts.times do |i|
|
|
81
|
-
droplet = @client.droplets.find(id: droplet_id)
|
|
82
|
-
if (v4 = droplet.networks.v4.find { |n| n.type == "public" })
|
|
83
|
-
return v4.ip_address
|
|
84
|
-
end
|
|
85
|
-
puts "⏳ Waiting for public IP... (#{i + 1}/#{max_attempts})"
|
|
86
|
-
sleep interval
|
|
87
|
-
end
|
|
88
|
-
abort "❌ Timeout: the Droplet did not obtain a public IP after #{interval * max_attempts} seconds"
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Waits for the SSH port to become accessible
|
|
92
|
-
def wait_for_ssh(ip, interval: 5, max_attempts: 24)
|
|
93
|
-
max_attempts.times do |i|
|
|
94
|
-
begin
|
|
95
|
-
puts "🔐 Waiting for SSH connection to #{ip}... (#{i + 1}/#{max_attempts})"
|
|
96
|
-
TCPSocket.new(ip, 22).close
|
|
97
|
-
puts "🔓 SSH connection to #{ip} established"
|
|
98
|
-
return
|
|
99
|
-
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
|
|
100
|
-
sleep interval
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
abort "❌ Could not connect via SSH to #{ip} after #{interval * max_attempts} seconds"
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|