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,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
@@ -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
data/sig/kitsune/kit.rbs DELETED
@@ -1,6 +0,0 @@
1
- module Kitsune
2
- module Kit
3
- VERSION: String
4
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
- end
6
- end