kitsune-kit 0.1.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 +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/Rakefile +8 -0
- data/bin/kit +8 -0
- data/kitsune-kit-logo.jpg +0 -0
- data/lib/kitsune/blueprints/.env.template +17 -0
- data/lib/kitsune/blueprints/docker/postgres.yml +20 -0
- data/lib/kitsune/blueprints/kit.env.template +1 -0
- data/lib/kitsune/kit/cli.rb +64 -0
- data/lib/kitsune/kit/commands/bootstrap.rb +118 -0
- data/lib/kitsune/kit/commands/bootstrap_docker.rb +66 -0
- data/lib/kitsune/kit/commands/init.rb +148 -0
- data/lib/kitsune/kit/commands/install_docker_engine.rb +146 -0
- data/lib/kitsune/kit/commands/postinstall_docker.rb +142 -0
- data/lib/kitsune/kit/commands/provision.rb +43 -0
- data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +150 -0
- data/lib/kitsune/kit/commands/setup_firewall.rb +132 -0
- data/lib/kitsune/kit/commands/setup_postgres_docker.rb +246 -0
- data/lib/kitsune/kit/commands/setup_unattended.rb +132 -0
- data/lib/kitsune/kit/commands/setup_user.rb +189 -0
- data/lib/kitsune/kit/commands/switch_env.rb +42 -0
- data/lib/kitsune/kit/defaults.rb +56 -0
- data/lib/kitsune/kit/env_loader.rb +41 -0
- data/lib/kitsune/kit/options_builder.rb +26 -0
- data/lib/kitsune/kit/provisioner.rb +109 -0
- data/lib/kitsune/kit/version.rb +7 -0
- data/lib/kitsune/kit.rb +10 -0
- data/sig/kitsune/kit.rbs +6 -0
- metadata +180 -0
@@ -0,0 +1,189 @@
|
|
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
|
@@ -0,0 +1,42 @@
|
|
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
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Kitsune
|
2
|
+
module Kit
|
3
|
+
module Defaults
|
4
|
+
DROPLET = {
|
5
|
+
droplet_name: "app-prod",
|
6
|
+
region: "sfo3",
|
7
|
+
size: "s-1vcpu-1gb",
|
8
|
+
image: "ubuntu-22-04-x64",
|
9
|
+
tag: "rails-prod"
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
SSH = {
|
13
|
+
ssh_port: "22",
|
14
|
+
ssh_key_path: "~/.ssh/id_rsa"
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
POSTGRES = {
|
18
|
+
db_prefix: "myapp_db",
|
19
|
+
user: "postgres",
|
20
|
+
password: "secret",
|
21
|
+
port: "5432",
|
22
|
+
image: "postgres:15"
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
def self.infra
|
26
|
+
{
|
27
|
+
droplet_name: ENV.fetch('DROPLET_NAME', DROPLET[:droplet_name]),
|
28
|
+
region: ENV.fetch('REGION', DROPLET[:region]),
|
29
|
+
size: ENV.fetch('SIZE', DROPLET[:size]),
|
30
|
+
image: ENV.fetch('IMAGE', DROPLET[:image]),
|
31
|
+
tag: ENV.fetch('TAG_NAME', DROPLET[:tag]),
|
32
|
+
ssh_key_id: ENV.fetch('SSH_KEY_ID') { abort "β Missing SSH_KEY_ID" }
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.ssh
|
37
|
+
{
|
38
|
+
ssh_port: ENV.fetch('SSH_PORT', SSH[:ssh_port]),
|
39
|
+
ssh_key_path: ENV.fetch('SSH_KEY_PATH', SSH[:ssh_key_path])
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.postgres
|
44
|
+
env = ENV.fetch('KIT_ENV', 'development')
|
45
|
+
|
46
|
+
{
|
47
|
+
postgres_db: ENV.fetch('POSTGRES_DB') { "#{POSTGRES[:db_prefix]}_#{env}" },
|
48
|
+
postgres_user: ENV.fetch('POSTGRES_USER', POSTGRES[:user]),
|
49
|
+
postgres_password: ENV.fetch('POSTGRES_PASSWORD', POSTGRES[:password]),
|
50
|
+
postgres_port: ENV.fetch('POSTGRES_PORT', POSTGRES[:port]),
|
51
|
+
postgres_image: ENV.fetch('POSTGRES_IMAGE', POSTGRES[:image])
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "dotenv"
|
2
|
+
|
3
|
+
module Kitsune
|
4
|
+
module Kit
|
5
|
+
class EnvLoader
|
6
|
+
@loaded = false
|
7
|
+
|
8
|
+
def self.load!
|
9
|
+
return if @loaded
|
10
|
+
|
11
|
+
env = ENV["KIT_ENV"] || read_kit_env || "development"
|
12
|
+
|
13
|
+
possible_paths = [
|
14
|
+
".kitsune/infra.#{env}.env",
|
15
|
+
".kitsune/infra.env"
|
16
|
+
]
|
17
|
+
|
18
|
+
found = possible_paths.find { |path| File.exist?(path) }
|
19
|
+
|
20
|
+
if found
|
21
|
+
Dotenv.load(found)
|
22
|
+
puts "π§ͺ Loaded Kitsune environment from #{found}"
|
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
|
@@ -0,0 +1,26 @@
|
|
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
|
@@ -0,0 +1,109 @@
|
|
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] do
|
13
|
+
abort "β You must export SSH_KEY_ID or use --ssh_key_id"
|
14
|
+
end
|
15
|
+
|
16
|
+
@client = DropletKit::Client.new(access_token: ENV.fetch("DO_API_TOKEN"))
|
17
|
+
end
|
18
|
+
|
19
|
+
# Find an existing droplet by name
|
20
|
+
def find_droplet
|
21
|
+
@client.droplets.all(tag_name: @tag).detect { |d| d.name == @droplet_name }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create command: shows if it exists or creates a new one
|
25
|
+
def create_or_show
|
26
|
+
if (d = find_droplet)
|
27
|
+
ip = public_ip(d)
|
28
|
+
puts "β
Droplet '#{@droplet_name}' already exists (ID: #{d.id}, IP: #{ip})"
|
29
|
+
else
|
30
|
+
puts "βπ» Creating Droplet '#{@droplet_name}'..."
|
31
|
+
spec = DropletKit::Droplet.new(
|
32
|
+
name: @droplet_name,
|
33
|
+
region: @region,
|
34
|
+
size: @size,
|
35
|
+
image: @image,
|
36
|
+
ssh_keys: [@ssh_key_id],
|
37
|
+
tags: [@tag]
|
38
|
+
)
|
39
|
+
created = @client.droplets.create(spec)
|
40
|
+
wait_for_status(created.id)
|
41
|
+
ip = wait_for_public_ip(created.id)
|
42
|
+
|
43
|
+
wait_for_ssh(ip)
|
44
|
+
|
45
|
+
puts "β
Droplet created: ID=#{created.id}, IP=#{ip}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Rollback command: deletes it if it exists
|
50
|
+
def rollback
|
51
|
+
if (d = find_droplet)
|
52
|
+
puts "π Deleting Droplet '#{@droplet_name}' (ID: #{d.id})..."
|
53
|
+
@client.droplets.delete(id: d.id)
|
54
|
+
puts "β
Droplet deleted π₯"
|
55
|
+
else
|
56
|
+
puts "β
Nothing to delete: '#{@droplet_name}' does not exist"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Extracts the public IP from a DropletKit::Droplet
|
63
|
+
def public_ip(droplet)
|
64
|
+
v4 = droplet.networks.v4.find { |n| n.type == "public" }
|
65
|
+
v4 ? v4.ip_address : "(no public IP yet)"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Waits until the droplet reaches the 'active' status
|
69
|
+
def wait_for_status(droplet_id, interval: 5, max_attempts: 24)
|
70
|
+
max_attempts.times do |i|
|
71
|
+
droplet = @client.droplets.find(id: droplet_id)
|
72
|
+
estado = droplet.status
|
73
|
+
puts "β³ Droplet status: #{estado} (#{i + 1}/#{max_attempts})"
|
74
|
+
return if estado == "active"
|
75
|
+
sleep interval
|
76
|
+
end
|
77
|
+
abort "β Timeout: the Droplet did not reach 'active' status after #{interval * max_attempts} seconds"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Waits until obtaining the public IP
|
81
|
+
def wait_for_public_ip(droplet_id, interval: 5, max_attempts: 24)
|
82
|
+
max_attempts.times do |i|
|
83
|
+
droplet = @client.droplets.find(id: droplet_id)
|
84
|
+
if (v4 = droplet.networks.v4.find { |n| n.type == "public" })
|
85
|
+
return v4.ip_address
|
86
|
+
end
|
87
|
+
puts "β³ Waiting for public IP... (#{i + 1}/#{max_attempts})"
|
88
|
+
sleep interval
|
89
|
+
end
|
90
|
+
abort "β Timeout: the Droplet did not obtain a public IP after #{interval * max_attempts} seconds"
|
91
|
+
end
|
92
|
+
|
93
|
+
# Waits for the SSH port to become accessible
|
94
|
+
def wait_for_ssh(ip, interval: 5, max_attempts: 24)
|
95
|
+
max_attempts.times do |i|
|
96
|
+
begin
|
97
|
+
puts "π Waiting for SSH connection to #{ip}... (#{i + 1}/#{max_attempts})"
|
98
|
+
TCPSocket.new(ip, 22).close
|
99
|
+
puts "π SSH connection to #{ip} established"
|
100
|
+
return
|
101
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
|
102
|
+
sleep interval
|
103
|
+
end
|
104
|
+
end
|
105
|
+
abort "β Could not connect via SSH to #{ip} after #{interval * max_attempts} seconds"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/kitsune/kit.rb
ADDED
data/sig/kitsune/kit.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kitsune-kit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Omar Herrera
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-04-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-ssh
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ed25519
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bcrypt_pbkdf
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: dotenv
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: droplet_kit
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: thor
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Kitsune Kit is a CLI toolkit that automates the provisioning, configuration,
|
112
|
+
and Docker setup of remote servers, especially tailored for Ruby developers using
|
113
|
+
Kamal. Includes rollback features and multi-environment support.
|
114
|
+
email:
|
115
|
+
- contact@omarherrera.me
|
116
|
+
executables:
|
117
|
+
- kit
|
118
|
+
extensions: []
|
119
|
+
extra_rdoc_files: []
|
120
|
+
files:
|
121
|
+
- ".rspec"
|
122
|
+
- CHANGELOG.md
|
123
|
+
- CODE_OF_CONDUCT.md
|
124
|
+
- LICENSE.txt
|
125
|
+
- README.md
|
126
|
+
- Rakefile
|
127
|
+
- bin/kit
|
128
|
+
- kitsune-kit-logo.jpg
|
129
|
+
- lib/kitsune/blueprints/.env.template
|
130
|
+
- lib/kitsune/blueprints/docker/postgres.yml
|
131
|
+
- lib/kitsune/blueprints/kit.env.template
|
132
|
+
- lib/kitsune/kit.rb
|
133
|
+
- lib/kitsune/kit/cli.rb
|
134
|
+
- lib/kitsune/kit/commands/bootstrap.rb
|
135
|
+
- lib/kitsune/kit/commands/bootstrap_docker.rb
|
136
|
+
- lib/kitsune/kit/commands/init.rb
|
137
|
+
- lib/kitsune/kit/commands/install_docker_engine.rb
|
138
|
+
- lib/kitsune/kit/commands/postinstall_docker.rb
|
139
|
+
- lib/kitsune/kit/commands/provision.rb
|
140
|
+
- lib/kitsune/kit/commands/setup_docker_prereqs.rb
|
141
|
+
- lib/kitsune/kit/commands/setup_firewall.rb
|
142
|
+
- lib/kitsune/kit/commands/setup_postgres_docker.rb
|
143
|
+
- lib/kitsune/kit/commands/setup_unattended.rb
|
144
|
+
- lib/kitsune/kit/commands/setup_user.rb
|
145
|
+
- lib/kitsune/kit/commands/switch_env.rb
|
146
|
+
- lib/kitsune/kit/defaults.rb
|
147
|
+
- lib/kitsune/kit/env_loader.rb
|
148
|
+
- lib/kitsune/kit/options_builder.rb
|
149
|
+
- lib/kitsune/kit/provisioner.rb
|
150
|
+
- lib/kitsune/kit/version.rb
|
151
|
+
- sig/kitsune/kit.rbs
|
152
|
+
homepage: https://github.com/omarhrra/kitsune-kit
|
153
|
+
licenses:
|
154
|
+
- MIT
|
155
|
+
metadata:
|
156
|
+
allowed_push_host: https://rubygems.org
|
157
|
+
homepage_uri: https://github.com/omarhrra/kitsune-kit
|
158
|
+
source_code_uri: https://github.com/omarhrra/kitsune-kit
|
159
|
+
changelog_uri: https://github.com/omarhrra/kitsune-kit/blob/main/CHANGELOG.md
|
160
|
+
post_install_message:
|
161
|
+
rdoc_options: []
|
162
|
+
require_paths:
|
163
|
+
- lib
|
164
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: 3.0.0
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
requirements: []
|
175
|
+
rubygems_version: 3.5.22
|
176
|
+
signing_key:
|
177
|
+
specification_version: 4
|
178
|
+
summary: Provision and setup DigitalOcean VPSs with Docker and PostgreSQL, ideal for
|
179
|
+
Kamal deployments.
|
180
|
+
test_files: []
|