kitsune-kit 0.3.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 508f86259061cf1c767fc1a1678aeb8195aaf174214c2c7f9be5557aa1b73c2a
4
- data.tar.gz: 38f6179c02ad9a59608bb3e3d3f62bd7b23648bb0bb24a6cdbd9fba179af3255
3
+ metadata.gz: 2860b56f9307be49ac06572e05cdf79e02bf76946262e64651d36c729505c8a8
4
+ data.tar.gz: c38a154d06774e6755dfec97f35a75d4ad2208b262ac37bb3ce20a25612836a7
5
5
  SHA512:
6
- metadata.gz: 65b84489d96cc8228afe78113dcd08065d3297957b77fcece8dfb9fe1d5722695086f18cc6387c400a9888342ac82ce56feaa7d2f9d67c780bf6abedb4c7da3e
7
- data.tar.gz: 59a28feaca900d703eb9ebe9943993cb1709b66784d7160425dc50f682bf7e93296408f8cdaedac04f3b2583a7757e74e69f7538515cc83abf3bf0f7088a1411
6
+ metadata.gz: 45ca3cd5d65502bb6c4e2fbdfa965152426d19bb732812651aee0957988d05eff5fe93432758825120e9ac2f20d8041afee8f7d34ac80aab438931c6948c15ad
7
+ data.tar.gz: f53ede98dd9cbf482e42ed91530cd07ddba5ed1744df1fcc46a92e6e5797f9629180057ff1a5e53cd80987f2ef0892d41400caff2b3a51273310b1676e9f93c5
data/README.md CHANGED
@@ -18,6 +18,7 @@
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
23
  - 🐘 Deploys PostgreSQL via Docker Compose with healthcheck
23
24
  - 🗄️ Deploys Redis via Docker Compose with healthcheck
@@ -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=
@@ -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
@@ -1,9 +1,10 @@
1
1
  require "thor"
2
- require "colorize"
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"
@@ -16,6 +17,7 @@ require_relative "commands/postinstall_docker"
16
17
  require_relative "commands/bootstrap_docker"
17
18
  require_relative "commands/setup_postgres_docker"
18
19
  require_relative "commands/setup_redis_docker"
20
+ require_relative "commands/ssh"
19
21
 
20
22
  module Kitsune
21
23
  module Kit
@@ -47,6 +49,9 @@ module Kitsune
47
49
  desc "provision SUBCOMMAND", "Provisioning tasks"
48
50
  subcommand "provision", Kitsune::Kit::Commands::Provision
49
51
 
52
+ desc "dns SUBCOMMAND", "Manage DNS"
53
+ subcommand "dns", Kitsune::Kit::Commands::Dns
54
+
50
55
  desc "setup_user SUBCOMMAND", "Create or rollback deploy user on remote server"
51
56
  subcommand "setup_user", Kitsune::Kit::Commands::SetupUser
52
57
 
@@ -82,6 +87,9 @@ module Kitsune
82
87
 
83
88
  desc "setup_redis_docker SUBCOMMAND", "Setup Redis via Docker Compose on remote server"
84
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
85
93
  end
86
94
  end
87
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
- Kitsune::Kit::CLI.start([
103
- subcommand, action,
104
- "--server-ip", droplet_ip,
105
- "--ssh-port", filled_options[:ssh_port],
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
@@ -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
@@ -2,11 +2,11 @@ module Kitsune
2
2
  module Kit
3
3
  module Defaults
4
4
  DROPLET = {
5
- droplet_name: "app-prod",
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-prod"
9
+ tag: "rails-dev"
10
10
  }.freeze
11
11
 
12
12
  SSH = {
@@ -15,7 +15,6 @@ 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",
@@ -59,7 +58,7 @@ module Kitsune
59
58
  env = ENV.fetch('KIT_ENV', 'development')
60
59
 
61
60
  {
62
- postgres_db: ENV.fetch('POSTGRES_DB') { "#{POSTGRES[:db_prefix]}_#{env}" },
61
+ postgres_db: ENV.fetch('POSTGRES_DB') { "myapp_db_#{env}" },
63
62
  postgres_user: ENV.fetch('POSTGRES_USER', POSTGRES[:user]),
64
63
  postgres_password: ENV.fetch('POSTGRES_PASSWORD', POSTGRES[:password]),
65
64
  postgres_port: ENV.fetch('POSTGRES_PORT', POSTGRES[:port]),
@@ -18,8 +18,8 @@ module Kitsune
18
18
 
19
19
  if found
20
20
  Dotenv.load(found)
21
- puts "🧪 Loaded Kitsune environment from #{found.light_cyan}"
22
- puts "=======================================================================\n".light_cyan
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] do
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kitsune
4
4
  module Kit
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
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.3.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-04 00:00:00.000000000 Z
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
@@ -187,9 +173,11 @@ files:
187
173
  - lib/kitsune/blueprints/docker/redis.yml
188
174
  - lib/kitsune/blueprints/kit.env.template
189
175
  - lib/kitsune/kit.rb
176
+ - lib/kitsune/kit/ansi_color.rb
190
177
  - lib/kitsune/kit/cli.rb
191
178
  - lib/kitsune/kit/commands/bootstrap.rb
192
179
  - lib/kitsune/kit/commands/bootstrap_docker.rb
180
+ - lib/kitsune/kit/commands/dns.rb
193
181
  - lib/kitsune/kit/commands/init.rb
194
182
  - lib/kitsune/kit/commands/install_docker_engine.rb
195
183
  - lib/kitsune/kit/commands/postinstall_docker.rb
@@ -202,6 +190,7 @@ files:
202
190
  - lib/kitsune/kit/commands/setup_swap.rb
203
191
  - lib/kitsune/kit/commands/setup_unattended.rb
204
192
  - lib/kitsune/kit/commands/setup_user.rb
193
+ - lib/kitsune/kit/commands/ssh.rb
205
194
  - lib/kitsune/kit/commands/switch_env.rb
206
195
  - lib/kitsune/kit/defaults.rb
207
196
  - lib/kitsune/kit/env_loader.rb