kitsune-kit 0.2.1 → 0.4.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/README.md +3 -1
- data/lib/kitsune/blueprints/.env.template +12 -3
- data/lib/kitsune/blueprints/docker/redis.yml +23 -0
- data/lib/kitsune/kit/ansi_color.rb +78 -0
- data/lib/kitsune/kit/cli.rb +24 -2
- data/lib/kitsune/kit/commands/bootstrap.rb +15 -7
- data/lib/kitsune/kit/commands/dns.rb +112 -0
- data/lib/kitsune/kit/commands/init.rb +2 -2
- data/lib/kitsune/kit/commands/setup_do_metrics.rb +3 -3
- data/lib/kitsune/kit/commands/setup_postgres_docker.rb +30 -32
- data/lib/kitsune/kit/commands/setup_redis_docker.rb +241 -0
- data/lib/kitsune/kit/commands/ssh.rb +46 -0
- data/lib/kitsune/kit/defaults.rb +16 -5
- data/lib/kitsune/kit/env_loader.rb +2 -2
- data/lib/kitsune/kit/provisioner.rb +1 -3
- data/lib/kitsune/kit/version.rb +1 -1
- metadata +7 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2860b56f9307be49ac06572e05cdf79e02bf76946262e64651d36c729505c8a8
|
4
|
+
data.tar.gz: c38a154d06774e6755dfec97f35a75d4ad2208b262ac37bb3ce20a25612836a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45ca3cd5d65502bb6c4e2fbdfa965152426d19bb732812651aee0957988d05eff5fe93432758825120e9ac2f20d8041afee8f7d34ac80aab438931c6948c15ad
|
7
|
+
data.tar.gz: f53ede98dd9cbf482e42ed91530cd07ddba5ed1744df1fcc46a92e6e5797f9629180057ff1a5e53cd80987f2ef0892d41400caff2b3a51273310b1676e9f93c5
|
data/README.md
CHANGED
@@ -18,8 +18,10 @@
|
|
18
18
|
- ♻️ Enables automatic security updates (unattended-upgrades)
|
19
19
|
- 💾 Configures swap space for better performance
|
20
20
|
- 📊 Installs [DigitalOcean monitoring agent](https://docs.digitalocean.com/products/monitoring/how-to/install-agent/)
|
21
|
+
- 🌐 **Automatically links** domains or subdomains (A records) to your server using DigitalOcean DNS
|
21
22
|
- 🐳 Installs and configures Docker Engine and private networking
|
22
|
-
- 🐘 Deploys PostgreSQL via Docker Compose with healthcheck
|
23
|
+
- 🐘 Deploys PostgreSQL via Docker Compose with healthcheck
|
24
|
+
- 🗄️ Deploys Redis via Docker Compose with healthcheck
|
23
25
|
- 🔄 All steps can be rolled back (`--rollback`)
|
24
26
|
- ⚡ Fast, reproducible and without relying on YAML or complex external tools
|
25
27
|
|
@@ -7,6 +7,11 @@ IMAGE=
|
|
7
7
|
SSH_KEY_ID=
|
8
8
|
TAG_NAME=
|
9
9
|
ENABLE_DO_METRICS=true
|
10
|
+
# If you want DNS records to be created automatically, set the following variables:
|
11
|
+
# DOMAIN_NAMES=example.com,test.example.org # (optional, comma-separated list of domain names to add to the droplet)
|
12
|
+
# DNS_TTL=3600 # (optional, time-to-live for DNS records in seconds, default is 3600)
|
13
|
+
# Check the DigitalOcean documentation for more information on domain names:
|
14
|
+
# https://docs.digitalocean.com/products/networking/dns/getting-started/dns-registrars/
|
10
15
|
|
11
16
|
# SSH
|
12
17
|
SSH_KEY_PATH=
|
@@ -17,6 +22,10 @@ SWAP_SIZE_GB=2
|
|
17
22
|
DISABLE_SWAP=false
|
18
23
|
|
19
24
|
# PostgreSQL Docker
|
20
|
-
POSTGRES_DB=
|
21
|
-
POSTGRES_USER=
|
22
|
-
POSTGRES_PASSWORD=
|
25
|
+
POSTGRES_DB=myapp_db
|
26
|
+
POSTGRES_USER=postgres
|
27
|
+
POSTGRES_PASSWORD=secret
|
28
|
+
|
29
|
+
# Redis Docker
|
30
|
+
REDIS_PORT=6379
|
31
|
+
REDIS_PASSWORD=secret
|
@@ -0,0 +1,23 @@
|
|
1
|
+
services:
|
2
|
+
redis:
|
3
|
+
image: redis:7.2
|
4
|
+
restart: always
|
5
|
+
ports:
|
6
|
+
- "${REDIS_PORT:-6379}:6379"
|
7
|
+
command: redis-server --requirepass ${REDIS_PASSWORD}
|
8
|
+
networks:
|
9
|
+
- private
|
10
|
+
volumes:
|
11
|
+
- redisdata:/data
|
12
|
+
healthcheck:
|
13
|
+
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
14
|
+
interval: 30s
|
15
|
+
timeout: 10s
|
16
|
+
retries: 5
|
17
|
+
|
18
|
+
volumes:
|
19
|
+
redisdata:
|
20
|
+
|
21
|
+
networks:
|
22
|
+
private:
|
23
|
+
driver: bridge
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module AnsiColor
|
2
|
+
COLORS = {
|
3
|
+
black: 30,
|
4
|
+
red: 31,
|
5
|
+
green: 32,
|
6
|
+
yellow: 33,
|
7
|
+
blue: 34,
|
8
|
+
magenta: 35,
|
9
|
+
cyan: 36,
|
10
|
+
white: 37,
|
11
|
+
|
12
|
+
light_black: 90,
|
13
|
+
light_red: 91,
|
14
|
+
light_green: 92,
|
15
|
+
light_yellow: 93,
|
16
|
+
light_blue: 94,
|
17
|
+
light_magenta: 95,
|
18
|
+
light_cyan: 96,
|
19
|
+
light_white: 97
|
20
|
+
}
|
21
|
+
|
22
|
+
BACKGROUNDS = {
|
23
|
+
black: 40,
|
24
|
+
red: 41,
|
25
|
+
green: 42,
|
26
|
+
yellow: 43,
|
27
|
+
blue: 44,
|
28
|
+
magenta: 45,
|
29
|
+
cyan: 46,
|
30
|
+
white: 47,
|
31
|
+
|
32
|
+
light_black: 100,
|
33
|
+
light_red: 101,
|
34
|
+
light_green: 102,
|
35
|
+
light_yellow: 103,
|
36
|
+
light_blue: 104,
|
37
|
+
light_magenta: 105,
|
38
|
+
light_cyan: 106,
|
39
|
+
light_white: 107
|
40
|
+
}
|
41
|
+
|
42
|
+
STYLES = {
|
43
|
+
bold: 1,
|
44
|
+
italic: 3,
|
45
|
+
underline: 4
|
46
|
+
}
|
47
|
+
|
48
|
+
def self.colorize(text, color: nil, background: nil, style: nil)
|
49
|
+
codes = []
|
50
|
+
codes << COLORS[color] if color
|
51
|
+
codes << BACKGROUNDS[background] if background
|
52
|
+
codes << STYLES[style] if style
|
53
|
+
|
54
|
+
raise ArgumentError, "No valid formatting options given" if codes.empty?
|
55
|
+
|
56
|
+
"\e[#{codes.join(';')}m#{text}\e[0m"
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.demo
|
60
|
+
puts "Text Colors:"
|
61
|
+
COLORS.each_key do |color|
|
62
|
+
puts colorize(" #{color.to_s.ljust(15)}", color: color)
|
63
|
+
end
|
64
|
+
|
65
|
+
puts "\nBackground Colors:"
|
66
|
+
BACKGROUNDS.each_key do |bg|
|
67
|
+
puts colorize(" #{bg.to_s.ljust(15)}", background: bg)
|
68
|
+
end
|
69
|
+
|
70
|
+
puts "\nStyles:"
|
71
|
+
STYLES.each_key do |style|
|
72
|
+
puts colorize(" #{style.to_s.ljust(15)}", color: :white, style: style)
|
73
|
+
end
|
74
|
+
|
75
|
+
puts "\nCombined Example:"
|
76
|
+
puts colorize(" bold green on light_black ", color: :green, background: :light_black, style: :bold)
|
77
|
+
end
|
78
|
+
end
|
data/lib/kitsune/kit/cli.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
require "thor"
|
2
|
-
|
2
|
+
require_relative "ansi_color"
|
3
3
|
require_relative "env_loader"
|
4
4
|
require_relative "commands/init"
|
5
5
|
require_relative "commands/switch_env"
|
6
6
|
require_relative "commands/provision"
|
7
|
+
require_relative "commands/dns"
|
7
8
|
require_relative "commands/setup_user"
|
8
9
|
require_relative "commands/setup_firewall"
|
9
10
|
require_relative "commands/setup_unattended"
|
@@ -15,18 +16,30 @@ require_relative "commands/install_docker_engine"
|
|
15
16
|
require_relative "commands/postinstall_docker"
|
16
17
|
require_relative "commands/bootstrap_docker"
|
17
18
|
require_relative "commands/setup_postgres_docker"
|
19
|
+
require_relative "commands/setup_redis_docker"
|
20
|
+
require_relative "commands/ssh"
|
18
21
|
|
19
22
|
module Kitsune
|
20
23
|
module Kit
|
21
24
|
class CLI < Thor
|
22
25
|
def self.dispatch(m, args, options, config)
|
23
|
-
|
26
|
+
if args.include?("-v") || args.include?("--version")
|
27
|
+
puts "Kitsune Kit v#{Kitsune::Kit::VERSION}"
|
28
|
+
exit(0)
|
29
|
+
end
|
30
|
+
|
31
|
+
unless ["version", "init", "switch_env", "help", nil].include?(args.first)
|
24
32
|
Kitsune::Kit::EnvLoader.load!
|
25
33
|
end
|
26
34
|
|
27
35
|
super
|
28
36
|
end
|
29
37
|
|
38
|
+
desc "version", "Show Kitsune Kit version"
|
39
|
+
def version
|
40
|
+
say "Kitsune Kit v#{Kitsune::Kit::VERSION}", :green
|
41
|
+
end
|
42
|
+
|
30
43
|
desc "init", "Initialize Kitsune Kit project structure"
|
31
44
|
subcommand "init", Kitsune::Kit::Commands::Init
|
32
45
|
|
@@ -36,6 +49,9 @@ module Kitsune
|
|
36
49
|
desc "provision SUBCOMMAND", "Provisioning tasks"
|
37
50
|
subcommand "provision", Kitsune::Kit::Commands::Provision
|
38
51
|
|
52
|
+
desc "dns SUBCOMMAND", "Manage DNS"
|
53
|
+
subcommand "dns", Kitsune::Kit::Commands::Dns
|
54
|
+
|
39
55
|
desc "setup_user SUBCOMMAND", "Create or rollback deploy user on remote server"
|
40
56
|
subcommand "setup_user", Kitsune::Kit::Commands::SetupUser
|
41
57
|
|
@@ -68,6 +84,12 @@ module Kitsune
|
|
68
84
|
|
69
85
|
desc "setup_postgres_docker SUBCOMMAND", "Setup PostgreSQL via Docker Compose on remote server"
|
70
86
|
subcommand "setup_postgres_docker", Kitsune::Kit::Commands::SetupPostgresDocker
|
87
|
+
|
88
|
+
desc "setup_redis_docker SUBCOMMAND", "Setup Redis via Docker Compose on remote server"
|
89
|
+
subcommand "setup_redis_docker", Kitsune::Kit::Commands::SetupRedisDocker
|
90
|
+
|
91
|
+
desc "ssh connect", "SSH into the server"
|
92
|
+
subcommand "ssh", Kitsune::Kit::Commands::Ssh
|
71
93
|
end
|
72
94
|
end
|
73
95
|
end
|
@@ -44,6 +44,7 @@ module Kitsune
|
|
44
44
|
run_cli("setup_unattended create", droplet_ip, filled_options)
|
45
45
|
run_cli("setup_swap create", droplet_ip, filled_options)
|
46
46
|
run_cli("setup_do_metrics create", droplet_ip, filled_options)
|
47
|
+
run_cli("dns link", droplet_ip, filled_options.merge(domains: ENV["DOMAIN_NAMES"]))
|
47
48
|
end
|
48
49
|
|
49
50
|
def rollback_sequence(filled_options)
|
@@ -75,10 +76,11 @@ module Kitsune
|
|
75
76
|
run_cli("setup_user rollback", droplet_ip, filled_options)
|
76
77
|
|
77
78
|
unless filled_options[:keep_server]
|
79
|
+
run_cli("dns rollback", droplet_ip, filled_options.merge(domains: ENV["DOMAIN_NAMES"]))
|
78
80
|
say "▶️ Running: kitsune kit provision rollback", :blue
|
79
81
|
Kitsune::Kit::CLI.start(%w[provision rollback])
|
80
82
|
else
|
81
|
-
say "⏭️ Skipping droplet deletion (--keep-server enabled)", :yellow
|
83
|
+
say "⏭️ Skipping droplet deletion (--keep-server enabled), DNS rollback won't be executed", :yellow
|
82
84
|
end
|
83
85
|
end
|
84
86
|
|
@@ -99,12 +101,18 @@ module Kitsune
|
|
99
101
|
def run_cli(command, droplet_ip, filled_options)
|
100
102
|
say "\n▶️ Running: kitsune kit #{command} --server-ip #{droplet_ip}", :blue
|
101
103
|
subcommand, action = command.split(" ", 2)
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
"--ssh-key-path", filled_options[:ssh_key_path]
|
107
|
-
|
104
|
+
|
105
|
+
args = [subcommand, action, "--server-ip", droplet_ip]
|
106
|
+
|
107
|
+
if subcommand != "dns"
|
108
|
+
args += ["--ssh-port", filled_options[:ssh_port], "--ssh-key-path", filled_options[:ssh_key_path]]
|
109
|
+
end
|
110
|
+
|
111
|
+
if subcommand == "dns" && ENV["DOMAIN_NAMES"]
|
112
|
+
args += ["--domains", ENV["DOMAIN_NAMES"]]
|
113
|
+
end
|
114
|
+
|
115
|
+
Kitsune::Kit::CLI.start(args)
|
108
116
|
rescue SystemExit => e
|
109
117
|
abort "❌ Command failed: #{command} (exit #{e.status})"
|
110
118
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require "droplet_kit"
|
2
|
+
require "thor"
|
3
|
+
|
4
|
+
module Kitsune
|
5
|
+
module Kit
|
6
|
+
module Commands
|
7
|
+
class Dns < Thor
|
8
|
+
namespace "dns"
|
9
|
+
|
10
|
+
class_option :domains, type: :string, desc: "Comma-separated domain list (or from ENV['DOMAIN_NAMES'])"
|
11
|
+
class_option :server_ip, type: :string, required: true, desc: "IPv4 to assign to domain(s)"
|
12
|
+
class_option :ttl, type: :numeric, default: 3600, desc: "TTL in seconds for DNS records"
|
13
|
+
|
14
|
+
desc "link", "Link domains to a given IP using A records"
|
15
|
+
def link
|
16
|
+
validate_ip!
|
17
|
+
|
18
|
+
domains = resolve_domains
|
19
|
+
return if domains.empty?
|
20
|
+
|
21
|
+
ip = options[:server_ip]
|
22
|
+
ttl = ENV.fetch("DNS_TTL", options[:ttl]).to_i
|
23
|
+
|
24
|
+
client = DropletKit::Client.new(access_token: ENV.fetch("DO_API_TOKEN"))
|
25
|
+
|
26
|
+
domains.each do |fqdn|
|
27
|
+
parts = fqdn.split('.')
|
28
|
+
next if parts.size < 2
|
29
|
+
|
30
|
+
root_domain = parts[-2..].join('.')
|
31
|
+
subdomain = parts[0..-3].join('.')
|
32
|
+
name_for_a = subdomain.empty? ? "@" : subdomain
|
33
|
+
|
34
|
+
puts "\n🌐 Linking '#{fqdn}' to IP #{ip} (domain: #{root_domain}, record: #{name_for_a})"
|
35
|
+
|
36
|
+
records = client.domain_records.all(for_domain: root_domain)
|
37
|
+
existing = records.find { |r| r.type == "A" && r.name == name_for_a }
|
38
|
+
|
39
|
+
domain_record = DropletKit::DomainRecord.new(
|
40
|
+
type: "A",
|
41
|
+
name: name_for_a,
|
42
|
+
data: ip,
|
43
|
+
ttl: ttl
|
44
|
+
)
|
45
|
+
|
46
|
+
msg = "'#{AnsiColor.colorize(name_for_a, color: :green)}.#{AnsiColor.colorize(root_domain, color: :green)}' → #{AnsiColor.colorize(ip, color: :light_cyan)}"
|
47
|
+
if existing
|
48
|
+
client.domain_records.update(
|
49
|
+
domain_record,
|
50
|
+
for_domain: root_domain,
|
51
|
+
id: existing.id
|
52
|
+
)
|
53
|
+
puts "✅ Updated A record #{msg}"
|
54
|
+
else
|
55
|
+
client.domain_records.create(
|
56
|
+
domain_record,
|
57
|
+
for_domain: root_domain
|
58
|
+
)
|
59
|
+
puts "✅ Created A record #{msg}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
desc "rollback", "Remove A records for the specified domains"
|
65
|
+
def rollback
|
66
|
+
domains = resolve_domains
|
67
|
+
return if domains.empty?
|
68
|
+
|
69
|
+
client = DropletKit::Client.new(access_token: ENV.fetch("DO_API_TOKEN"))
|
70
|
+
|
71
|
+
domains.each do |fqdn|
|
72
|
+
parts = fqdn.split('.')
|
73
|
+
next if parts.size < 2
|
74
|
+
|
75
|
+
root_domain = parts[-2..].join('.')
|
76
|
+
subdomain = parts[0..-3].join('.')
|
77
|
+
name_for_a = subdomain.empty? ? "@" : subdomain
|
78
|
+
|
79
|
+
puts "\n🗑️ Attempting to delete A record for '#{fqdn}' (domain: #{root_domain}, record: #{name_for_a})"
|
80
|
+
|
81
|
+
records = client.domain_records.all(for_domain: root_domain)
|
82
|
+
existing = records.find { |r| r.type == "A" && r.name == name_for_a }
|
83
|
+
|
84
|
+
if existing
|
85
|
+
client.domain_records.delete(for_domain: root_domain, id: existing.id)
|
86
|
+
puts "✅ Deleted A record '#{name_for_a}.#{root_domain}'"
|
87
|
+
else
|
88
|
+
puts "💡 No A record found for '#{name_for_a}.#{root_domain}', nothing to delete"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
no_commands do
|
94
|
+
def validate_ip!
|
95
|
+
if options[:server_ip].nil? || options[:server_ip].empty?
|
96
|
+
abort "❌ Missing required option: --server-ip"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def resolve_domains
|
101
|
+
raw = options[:domains] || ENV["DOMAIN_NAMES"]
|
102
|
+
if raw.nil? || raw.strip.empty?
|
103
|
+
puts "⏭️ No domains provided. Skipping DNS operation."
|
104
|
+
return []
|
105
|
+
end
|
106
|
+
raw.split(',').map(&:strip).reject(&:empty?)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -124,8 +124,8 @@ module Kitsune
|
|
124
124
|
end
|
125
125
|
|
126
126
|
def copy_docker_templates
|
127
|
-
|
128
|
-
copy_with_prompt(blueprint_path("docker/
|
127
|
+
copy_with_prompt(blueprint_path("docker/postgres.yml"), ".kitsune/docker/postgres.yml")
|
128
|
+
copy_with_prompt(blueprint_path("docker/redis.yml"), ".kitsune/docker/redis.yml")
|
129
129
|
end
|
130
130
|
|
131
131
|
def copy_with_prompt(source, destination)
|
@@ -26,7 +26,7 @@ module Kitsune
|
|
26
26
|
defaults: Kitsune::Kit::Defaults.ssh
|
27
27
|
)
|
28
28
|
|
29
|
-
|
29
|
+
with_ssh_connection(filled_options) do |ssh|
|
30
30
|
install_agent(ssh)
|
31
31
|
end
|
32
32
|
end
|
@@ -39,13 +39,13 @@ module Kitsune
|
|
39
39
|
defaults: Kitsune::Kit::Defaults.ssh
|
40
40
|
)
|
41
41
|
|
42
|
-
|
42
|
+
with_ssh_connection(filled_options) do |ssh|
|
43
43
|
uninstall_agent(ssh)
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
47
|
no_commands do
|
48
|
-
def
|
48
|
+
def with_ssh_connection(filled_options)
|
49
49
|
Net::SSH.start(
|
50
50
|
filled_options[:server_ip],
|
51
51
|
"deploy",
|
@@ -36,11 +36,7 @@ module Kitsune
|
|
36
36
|
)
|
37
37
|
|
38
38
|
with_ssh_connection(filled_options) do |ssh|
|
39
|
-
perform_setup(ssh, postgres_defaults)
|
40
|
-
|
41
|
-
database_url = build_database_url(filled_options, postgres_defaults)
|
42
|
-
say "🔗 Your DATABASE_URL is:\t", :cyan
|
43
|
-
say database_url, :green
|
39
|
+
perform_setup(ssh, postgres_defaults, filled_options)
|
44
40
|
end
|
45
41
|
end
|
46
42
|
|
@@ -71,26 +67,21 @@ module Kitsune
|
|
71
67
|
end
|
72
68
|
end
|
73
69
|
|
74
|
-
def perform_setup(ssh, postgres_defaults)
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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"
|
80
76
|
|
81
|
-
|
82
|
-
docker_compose_remote = "#{docker_dir_remote}/docker-compose.yml"
|
83
|
-
docker_env_remote = "#{docker_dir_remote}/.env"
|
84
|
-
backup_marker = "/usr/local/backups/setup_postgres_docker.after"
|
77
|
+
abort "❌ Missing #{local_compose}" unless File.exist?(local_compose)
|
85
78
|
|
86
79
|
# 1. Create base directory securely
|
87
|
-
ssh.exec!("mkdir -p #{
|
88
|
-
ssh.exec!("chmod 700 #{docker_dir_remote}")
|
80
|
+
ssh.exec!("mkdir -p #{remote_dir} && chmod 700 #{remote_dir}")
|
89
81
|
|
90
82
|
# 2. Upload docker-compose.yml
|
91
|
-
say "📦 Uploading docker-compose.yml to
|
92
|
-
|
93
|
-
upload_file(ssh, content_compose, docker_compose_remote)
|
83
|
+
say "📦 Uploading docker-compose.yml to #{remote_dir}", :cyan
|
84
|
+
upload_file(ssh, File.read(local_compose), compose_remote)
|
94
85
|
|
95
86
|
# 3. Create .env file for docker-compose based on postgres_defaults
|
96
87
|
say "📦 Creating .env file for Docker Compose...", :cyan
|
@@ -101,17 +92,17 @@ module Kitsune
|
|
101
92
|
POSTGRES_PORT=#{postgres_defaults[:postgres_port]}
|
102
93
|
POSTGRES_IMAGE=#{postgres_defaults[:postgres_image]}
|
103
94
|
ENVFILE
|
104
|
-
upload_file(ssh, env_content,
|
95
|
+
upload_file(ssh, env_content, env_remote)
|
105
96
|
|
106
97
|
# 4. Secure file permissions
|
107
|
-
ssh.exec!("chmod 600 #{
|
98
|
+
ssh.exec!("chmod 600 #{compose_remote} #{env_remote}")
|
108
99
|
|
109
100
|
# 5. Create backup marker
|
110
|
-
ssh.exec!("sudo mkdir -p /usr/local/backups && sudo touch #{
|
101
|
+
ssh.exec!("sudo mkdir -p /usr/local/backups && sudo touch #{marker}")
|
111
102
|
|
112
103
|
# 6. Validate docker-compose.yml
|
113
104
|
say "🔍 Validating docker-compose.yml...", :cyan
|
114
|
-
validation_output = ssh.exec!("cd #{
|
105
|
+
validation_output = ssh.exec!("cd #{remote_dir} && docker compose config")
|
115
106
|
say validation_output, :cyan
|
116
107
|
|
117
108
|
# 7. Check if container is running
|
@@ -119,19 +110,20 @@ module Kitsune
|
|
119
110
|
|
120
111
|
if container_status.empty?
|
121
112
|
say "▶️ No running container. Running docker compose up...", :cyan
|
122
|
-
ssh.exec!("cd #{
|
113
|
+
ssh.exec!("cd #{remote_dir} && docker compose up -d")
|
123
114
|
else
|
124
115
|
say "⚠️ PostgreSQL container is already running.", :yellow
|
125
116
|
if yes?("🔁 Recreate the container with updated configuration? [y/N]", :yellow)
|
126
117
|
say "🔄 Recreating container...", :cyan
|
127
|
-
ssh.exec!("cd #{
|
118
|
+
ssh.exec!("cd #{remote_dir} && docker compose down -v && docker compose up -d")
|
128
119
|
else
|
129
120
|
say "⏩ Keeping existing container.", :cyan
|
130
121
|
end
|
131
122
|
end
|
132
123
|
|
124
|
+
# 8. Check container status
|
133
125
|
say "📋 Final container status (docker compose ps):", :cyan
|
134
|
-
docker_ps_output = ssh.exec!("cd #{
|
126
|
+
docker_ps_output = ssh.exec!("cd #{remote_dir} && docker compose ps --format json")
|
135
127
|
|
136
128
|
if docker_ps_output.nil? || docker_ps_output.strip.empty? || docker_ps_output.include?("no configuration file")
|
137
129
|
say "⚠️ docker compose ps returned no valid output.", :yellow
|
@@ -168,9 +160,13 @@ module Kitsune
|
|
168
160
|
if healthcheck.include?("accepting connections")
|
169
161
|
say "✅ PostgreSQL is up and accepting connections! (attempt #{attempt})", :green
|
170
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
|
171
167
|
break
|
172
168
|
else
|
173
|
-
say "⏳ PostgreSQL not ready yet, retrying in 5 seconds... (#{attempt
|
169
|
+
say "⏳ PostgreSQL not ready yet, retrying in 5 seconds... (#{attempt}/#{max_attempts})", :yellow
|
174
170
|
sleep 5
|
175
171
|
end
|
176
172
|
end
|
@@ -181,7 +177,7 @@ module Kitsune
|
|
181
177
|
|
182
178
|
# 10. Allow PostgreSQL port through firewall (ufw)
|
183
179
|
say "🛡️ Configuring firewall to allow PostgreSQL (port #{postgres_defaults[:postgres_port]})...", :cyan
|
184
|
-
|
180
|
+
output = ssh.exec! <<~EOH
|
185
181
|
if command -v ufw >/dev/null; then
|
186
182
|
if ! sudo ufw status | grep -q "#{postgres_defaults[:postgres_port]}"; then
|
187
183
|
sudo ufw allow #{postgres_defaults[:postgres_port]}
|
@@ -192,7 +188,9 @@ module Kitsune
|
|
192
188
|
echo "⚠️ ufw not found. Skipping firewall configuration."
|
193
189
|
fi
|
194
190
|
EOH
|
195
|
-
|
191
|
+
say output
|
192
|
+
|
193
|
+
say "✅ PostgreSQL setup completed successfully!", :green
|
196
194
|
end
|
197
195
|
|
198
196
|
def perform_rollback(ssh, postgres_defaults)
|
@@ -227,9 +225,9 @@ module Kitsune
|
|
227
225
|
end
|
228
226
|
|
229
227
|
def upload_file(ssh, content, remote_path)
|
230
|
-
|
228
|
+
escaped = Shellwords.escape(content)
|
231
229
|
ssh.exec!("mkdir -p #{File.dirname(remote_path)}")
|
232
|
-
ssh.exec!("echo #{
|
230
|
+
ssh.exec!("echo #{escaped} > #{remote_path}")
|
233
231
|
end
|
234
232
|
|
235
233
|
def build_database_url(filled_options, postgres_defaults)
|
@@ -0,0 +1,241 @@
|
|
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
|
@@ -0,0 +1,46 @@
|
|
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
|
data/lib/kitsune/kit/defaults.rb
CHANGED
@@ -2,11 +2,11 @@ module Kitsune
|
|
2
2
|
module Kit
|
3
3
|
module Defaults
|
4
4
|
DROPLET = {
|
5
|
-
droplet_name: "app-
|
5
|
+
droplet_name: "app-dev",
|
6
6
|
region: "sfo3",
|
7
7
|
size: "s-1vcpu-1gb",
|
8
8
|
image: "ubuntu-22-04-x64",
|
9
|
-
tag: "rails-
|
9
|
+
tag: "rails-dev"
|
10
10
|
}.freeze
|
11
11
|
|
12
12
|
SSH = {
|
@@ -15,11 +15,10 @@ module Kitsune
|
|
15
15
|
}.freeze
|
16
16
|
|
17
17
|
POSTGRES = {
|
18
|
-
db_prefix: "myapp_db",
|
19
18
|
user: "postgres",
|
20
19
|
password: "secret",
|
21
20
|
port: "5432",
|
22
|
-
image: "postgres:
|
21
|
+
image: "postgres:17"
|
23
22
|
}.freeze
|
24
23
|
|
25
24
|
SYSTEM = {
|
@@ -32,6 +31,11 @@ module Kitsune
|
|
32
31
|
enable_do_metrics: true
|
33
32
|
}.freeze
|
34
33
|
|
34
|
+
REDIS = {
|
35
|
+
port: "6379",
|
36
|
+
password: "redis:7.2"
|
37
|
+
}.freeze
|
38
|
+
|
35
39
|
def self.infra
|
36
40
|
{
|
37
41
|
droplet_name: ENV.fetch('DROPLET_NAME', DROPLET[:droplet_name]),
|
@@ -54,7 +58,7 @@ module Kitsune
|
|
54
58
|
env = ENV.fetch('KIT_ENV', 'development')
|
55
59
|
|
56
60
|
{
|
57
|
-
postgres_db: ENV.fetch('POSTGRES_DB') { "#{
|
61
|
+
postgres_db: ENV.fetch('POSTGRES_DB') { "myapp_db_#{env}" },
|
58
62
|
postgres_user: ENV.fetch('POSTGRES_USER', POSTGRES[:user]),
|
59
63
|
postgres_password: ENV.fetch('POSTGRES_PASSWORD', POSTGRES[:password]),
|
60
64
|
postgres_port: ENV.fetch('POSTGRES_PORT', POSTGRES[:port]),
|
@@ -62,6 +66,13 @@ module Kitsune
|
|
62
66
|
}
|
63
67
|
end
|
64
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
|
+
|
65
76
|
def self.system
|
66
77
|
{
|
67
78
|
swap_size_gb: ENV.fetch("SWAP_SIZE_GB", SYSTEM[:swap_size_gb]).to_i,
|
@@ -18,8 +18,8 @@ module Kitsune
|
|
18
18
|
|
19
19
|
if found
|
20
20
|
Dotenv.load(found)
|
21
|
-
puts "🧪 Loaded Kitsune environment from #{found
|
22
|
-
puts "=======================================================================\n"
|
21
|
+
puts AnsiColor.colorize("🧪 Loaded Kitsune environment from #{found}", color: :light_cyan)
|
22
|
+
puts AnsiColor.colorize("=======================================================================\n", color: :light_cyan)
|
23
23
|
else
|
24
24
|
puts "⚠️ No Kitsune infra config found for environment '#{env}' (looked for infra.#{env}.env and infra.env)"
|
25
25
|
end
|
@@ -9,9 +9,7 @@ module Kitsune
|
|
9
9
|
@size = opts[:size]
|
10
10
|
@image = opts[:image]
|
11
11
|
@tag = opts[:tag]
|
12
|
-
@ssh_key_id = opts[:ssh_key_id]
|
13
|
-
abort "❌ You must export SSH_KEY_ID or use --ssh_key_id"
|
14
|
-
end
|
12
|
+
@ssh_key_id = opts[:ssh_key_id] { abort "❌ You must export SSH_KEY_ID or use --ssh_key_id" }
|
15
13
|
|
16
14
|
@client = DropletKit::Client.new(access_token: ENV.fetch("DO_API_TOKEN"))
|
17
15
|
end
|
data/lib/kitsune/kit/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kitsune-kit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Omar Herrera
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-05-
|
11
|
+
date: 2025-05-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: net-ssh
|
@@ -94,20 +94,6 @@ dependencies:
|
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '1.3'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: colorize
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - "~>"
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: 1.0.4
|
104
|
-
type: :runtime
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - "~>"
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: 1.0.4
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
98
|
name: pry
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -184,11 +170,14 @@ files:
|
|
184
170
|
- kitsune-kit-logo.jpg
|
185
171
|
- lib/kitsune/blueprints/.env.template
|
186
172
|
- lib/kitsune/blueprints/docker/postgres.yml
|
173
|
+
- lib/kitsune/blueprints/docker/redis.yml
|
187
174
|
- lib/kitsune/blueprints/kit.env.template
|
188
175
|
- lib/kitsune/kit.rb
|
176
|
+
- lib/kitsune/kit/ansi_color.rb
|
189
177
|
- lib/kitsune/kit/cli.rb
|
190
178
|
- lib/kitsune/kit/commands/bootstrap.rb
|
191
179
|
- lib/kitsune/kit/commands/bootstrap_docker.rb
|
180
|
+
- lib/kitsune/kit/commands/dns.rb
|
192
181
|
- lib/kitsune/kit/commands/init.rb
|
193
182
|
- lib/kitsune/kit/commands/install_docker_engine.rb
|
194
183
|
- lib/kitsune/kit/commands/postinstall_docker.rb
|
@@ -197,9 +186,11 @@ files:
|
|
197
186
|
- lib/kitsune/kit/commands/setup_docker_prereqs.rb
|
198
187
|
- lib/kitsune/kit/commands/setup_firewall.rb
|
199
188
|
- lib/kitsune/kit/commands/setup_postgres_docker.rb
|
189
|
+
- lib/kitsune/kit/commands/setup_redis_docker.rb
|
200
190
|
- lib/kitsune/kit/commands/setup_swap.rb
|
201
191
|
- lib/kitsune/kit/commands/setup_unattended.rb
|
202
192
|
- lib/kitsune/kit/commands/setup_user.rb
|
193
|
+
- lib/kitsune/kit/commands/ssh.rb
|
203
194
|
- lib/kitsune/kit/commands/switch_env.rb
|
204
195
|
- lib/kitsune/kit/defaults.rb
|
205
196
|
- lib/kitsune/kit/env_loader.rb
|