kitsune-kit 0.4.1 → 0.6.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -2
  3. data/CONTRIBUTING.md +83 -0
  4. data/README.md +112 -135
  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/decisions/0007-compose-customization.md +30 -0
  14. data/docs/architecture/provider-adapters.md +49 -0
  15. data/docs/architecture.md +93 -0
  16. data/docs/commands.md +224 -0
  17. data/docs/configuration.md +226 -0
  18. data/docs/getting-started.md +133 -0
  19. data/docs/providers/digitalocean.md +52 -0
  20. data/docs/releasing.md +43 -0
  21. data/docs/roadmap.md +46 -0
  22. data/docs/security-audit.md +130 -0
  23. data/docs/security.md +116 -0
  24. data/docs/services/compose.md +164 -0
  25. data/docs/services/postgres.md +78 -0
  26. data/docs/services/redis.md +53 -0
  27. data/docs/testing.md +200 -0
  28. data/docs/troubleshooting.md +123 -0
  29. data/docs/tui.md +57 -0
  30. data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
  31. data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
  32. data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
  33. data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
  34. data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
  35. data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
  36. data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
  37. data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
  38. data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
  39. data/lib/kitsune/kit/adapters/provider.rb +34 -0
  40. data/lib/kitsune/kit/adapters/transport.rb +22 -0
  41. data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
  42. data/lib/kitsune/kit/application.rb +170 -0
  43. data/lib/kitsune/kit/cancellation.rb +21 -0
  44. data/lib/kitsune/kit/cli.rb +879 -64
  45. data/lib/kitsune/kit/clock.rb +11 -0
  46. data/lib/kitsune/kit/configuration.rb +577 -0
  47. data/lib/kitsune/kit/errors.rb +98 -0
  48. data/lib/kitsune/kit/events.rb +55 -0
  49. data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
  50. data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
  51. data/lib/kitsune/kit/operations/ensure_service.rb +302 -0
  52. data/lib/kitsune/kit/operations/remote_script.rb +214 -0
  53. data/lib/kitsune/kit/operations/service_backup.rb +71 -0
  54. data/lib/kitsune/kit/operations/service_files.rb +131 -0
  55. data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
  56. data/lib/kitsune/kit/operations/service_state.rb +59 -0
  57. data/lib/kitsune/kit/plan.rb +72 -0
  58. data/lib/kitsune/kit/reporters/human.rb +89 -0
  59. data/lib/kitsune/kit/reporters/json.rb +65 -0
  60. data/lib/kitsune/kit/reporters/reporter.rb +11 -0
  61. data/lib/kitsune/kit/result.rb +28 -0
  62. data/lib/kitsune/kit/run_journal.rb +102 -0
  63. data/lib/kitsune/kit/run_logger.rb +37 -0
  64. data/lib/kitsune/kit/scripts/docker.sh +142 -0
  65. data/lib/kitsune/kit/scripts/firewall.sh +210 -0
  66. data/lib/kitsune/kit/scripts/metrics.sh +59 -0
  67. data/lib/kitsune/kit/scripts/ssh.sh +95 -0
  68. data/lib/kitsune/kit/scripts/swap.sh +86 -0
  69. data/lib/kitsune/kit/scripts/unattended.sh +86 -0
  70. data/lib/kitsune/kit/scripts/user.sh +114 -0
  71. data/lib/kitsune/kit/secret_filter.rb +54 -0
  72. data/lib/kitsune/kit/secret_store.rb +32 -0
  73. data/lib/kitsune/kit/secret_stores/store.rb +12 -0
  74. data/lib/kitsune/kit/service_compose.rb +272 -0
  75. data/lib/kitsune/kit/state_store.rb +158 -0
  76. data/lib/kitsune/kit/state_stores/store.rb +15 -0
  77. data/lib/kitsune/kit/tui/actions.rb +72 -0
  78. data/lib/kitsune/kit/tui/application.rb +35 -0
  79. data/lib/kitsune/kit/tui/controller.rb +162 -0
  80. data/lib/kitsune/kit/tui/renderer.rb +134 -0
  81. data/lib/kitsune/kit/tui/state.rb +18 -0
  82. data/lib/kitsune/kit/tui/store.rb +88 -0
  83. data/lib/kitsune/kit/tui/terminal.rb +95 -0
  84. data/lib/kitsune/kit/version.rb +1 -1
  85. data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
  86. data/lib/kitsune/kit/workflows/base.rb +31 -0
  87. data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
  88. data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
  89. data/lib/kitsune/kit/workflows/doctor.rb +225 -0
  90. data/lib/kitsune/kit/workflows/eject_compose.rb +82 -0
  91. data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
  92. data/lib/kitsune/kit/workflows/import_server.rb +100 -0
  93. data/lib/kitsune/kit/workflows/initialize_project.rb +129 -0
  94. data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
  95. data/lib/kitsune/kit/workflows/rollback.rb +54 -0
  96. data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
  97. data/lib/kitsune/kit.rb +42 -2
  98. metadata +125 -79
  99. data/.rspec +0 -3
  100. data/Rakefile +0 -8
  101. data/kitsune-kit-logo.jpg +0 -0
  102. data/lib/kitsune/blueprints/.env.template +0 -31
  103. data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
  104. data/lib/kitsune/blueprints/docker/redis.yml +0 -23
  105. data/lib/kitsune/blueprints/kit.env.template +0 -1
  106. data/lib/kitsune/kit/ansi_color.rb +0 -78
  107. data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
  108. data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
  109. data/lib/kitsune/kit/commands/dns.rb +0 -112
  110. data/lib/kitsune/kit/commands/init.rb +0 -148
  111. data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
  112. data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
  113. data/lib/kitsune/kit/commands/provision.rb +0 -43
  114. data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
  115. data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
  116. data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
  117. data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
  118. data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
  119. data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
  120. data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
  121. data/lib/kitsune/kit/commands/setup_user.rb +0 -189
  122. data/lib/kitsune/kit/commands/ssh.rb +0 -46
  123. data/lib/kitsune/kit/commands/switch_env.rb +0 -42
  124. data/lib/kitsune/kit/defaults.rb +0 -91
  125. data/lib/kitsune/kit/env_loader.rb +0 -41
  126. data/lib/kitsune/kit/options_builder.rb +0 -26
  127. data/lib/kitsune/kit/provisioner.rb +0 -107
  128. data/sig/kitsune/kit.rbs +0 -6
@@ -1,66 +0,0 @@
1
- require "thor"
2
- require "open3"
3
- require_relative "../defaults"
4
- require_relative "../options_builder"
5
-
6
- module Kitsune
7
- module Kit
8
- module Commands
9
- class BootstrapDocker < Thor
10
- namespace "bootstrap_docker"
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 for server"
14
- class_option :ssh_key_path, aliases: "-k", desc: "SSH private key path"
15
- class_option :rollback, type: :boolean, default: false, desc: "Rollback Docker setup steps"
16
-
17
- desc "execute", "Run full Docker setup or rollback sequence"
18
- def execute
19
- filled_options = Kitsune::Kit::OptionsBuilder.build(
20
- options,
21
- required: [:server_ip],
22
- defaults: Kitsune::Kit::Defaults.ssh
23
- )
24
-
25
- if filled_options[:rollback]
26
- say "🔄 Rolling back full Docker setup...", :yellow
27
- rollback_sequence(filled_options)
28
- else
29
- say "🐳 Running full Docker setup...", :green
30
- setup_sequence(filled_options)
31
- end
32
-
33
- say "🎉 Done!", :green
34
- end
35
-
36
- no_commands do
37
- def setup_sequence(filled_options)
38
- run_cli("setup_docker_prereqs create", filled_options)
39
- run_cli("install_docker_engine create", filled_options)
40
- run_cli("postinstall_docker create", filled_options)
41
- end
42
-
43
- def rollback_sequence(filled_options)
44
- run_cli("postinstall_docker rollback", filled_options)
45
- run_cli("install_docker_engine rollback", filled_options)
46
- run_cli("setup_docker_prereqs rollback", filled_options)
47
- end
48
-
49
- def run_cli(command, filled_options)
50
- say "\n▶️ Running: kitsune kit #{command} --server-ip #{filled_options[:server_ip]}", :blue
51
-
52
- subcommand, action = command.split(" ", 2)
53
- Kitsune::Kit::CLI.start([
54
- subcommand, action,
55
- "--server-ip", filled_options[:server_ip],
56
- "--ssh-port", filled_options[:ssh_port],
57
- "--ssh-key-path", filled_options[:ssh_key_path]
58
- ])
59
- rescue SystemExit => e
60
- abort "❌ Command failed: #{command} (exit #{e.status})"
61
- end
62
- end
63
- end
64
- end
65
- end
66
- end
@@ -1,112 +0,0 @@
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
@@ -1,148 +0,0 @@
1
- require "thor"
2
- require "fileutils"
3
-
4
- module Kitsune
5
- module Kit
6
- module Commands
7
- class Init < Thor
8
- namespace "init"
9
-
10
- default_task :init
11
-
12
- desc "init", "Initialize Kitsune Kit project structure"
13
- def init
14
- say "✨ Initializing Kitsune project...", :green
15
- create_base_structure
16
- selected_envs = select_environments
17
- copy_env_templates(selected_envs)
18
- selected_default_env = select_default_environment(selected_envs)
19
- create_kit_env(selected_default_env)
20
- copy_docker_templates
21
- say "🎉 Done! '.kitsune/' structure is ready.", :green
22
- end
23
-
24
- no_commands do
25
- def blueprint_path(relative_path)
26
- File.expand_path("../../blueprints/#{relative_path}", __dir__)
27
- end
28
-
29
- def create_base_structure
30
- dirs = [
31
- ".kitsune",
32
- ".kitsune/docker"
33
- ]
34
- dirs.each do |dir|
35
- unless Dir.exist?(dir)
36
- FileUtils.mkdir_p(dir)
37
- say "📂 Created directory: #{dir}", :cyan
38
- else
39
- say "📂 Directory already exists: #{dir}", :yellow
40
- end
41
- end
42
- end
43
-
44
- def environments_options
45
- {
46
- "1" => "development",
47
- "2" => "production",
48
- "3" => "staging",
49
- "4" => "test"
50
- }
51
- end
52
-
53
- def select_environments
54
- say "🌎 Which environments do you want to create?", :cyan
55
- environments_options.each do |number, env|
56
- say " #{number}) #{env}"
57
- end
58
- input = ask("➡️ Enter numbers separated by commas (or type 'all') [default: all]:", :yellow)
59
- input = input.strip.downcase
60
-
61
- if input.empty? || input == "all"
62
- environments_options.values
63
- else
64
- selected = input.split(",").map(&:strip)
65
- environments = selected.map { |num| environments_options[num] }.compact
66
- if environments.empty?
67
- say "⚠️ Invalid selection. Creating all environments.", :yellow
68
- environments_options.values
69
- else
70
- environments
71
- end
72
- end
73
- end
74
-
75
- def select_default_environment(selected_envs)
76
- say "🎯 Which environment should be set as default in '.kitsune/kit.env'?", :cyan
77
- selected_envs.each_with_index do |env, index|
78
- say " #{index + 1}) #{env}"
79
- end
80
- input = ask("➡️ Enter number [default: 1]:", :yellow)
81
- input = input.strip
82
-
83
- if input.empty?
84
- selected_envs[0] # default to first selected
85
- else
86
- index = input.to_i - 1
87
- if index >= 0 && index < selected_envs.size
88
- selected_envs[index]
89
- else
90
- say "⚠️ Invalid selection. Defaulting to '#{selected_envs[0]}'", :yellow
91
- selected_envs[0]
92
- end
93
- end
94
- end
95
-
96
- def create_kit_env(default_env)
97
- path = ".kitsune/kit.env"
98
- template = File.read(blueprint_path("kit.env.template"))
99
- content = template.gsub("KIT_ENV=development", "KIT_ENV=#{default_env}")
100
-
101
- if File.exist?(path)
102
- if yes?("⚠️ File #{path} already exists. Overwrite? [y/N]", :yellow)
103
- File.write(path, content)
104
- say "✅ Overwritten: #{path}", :cyan
105
- else
106
- say "⏩ Skipped: #{path}", :yellow
107
- end
108
- else
109
- File.write(path, content)
110
- say "📝 Created: #{path}", :cyan
111
- end
112
-
113
- say ""
114
- say "🎯 Kitsune Kit environment set to: #{default_env}", :green
115
- say "📄 Environment file used: .kitsune/infra.#{default_env}.env", :green
116
- say ""
117
- end
118
-
119
- def copy_env_templates(selected_envs)
120
- selected_envs.each do |env|
121
- dest_path = ".kitsune/infra.#{env}.env"
122
- copy_with_prompt(blueprint_path(".env.template"), dest_path)
123
- end
124
- end
125
-
126
- def copy_docker_templates
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
- end
130
-
131
- def copy_with_prompt(source, destination)
132
- if File.exist?(destination)
133
- if yes?("⚠️ File #{destination} already exists. Overwrite? [y/N]", :yellow)
134
- FileUtils.cp(source, destination)
135
- say "✅ Overwritten: #{destination}", :cyan
136
- else
137
- say "⏩ Skipped: #{destination}", :yellow
138
- end
139
- else
140
- FileUtils.cp(source, destination)
141
- say "📝 Created: #{destination}", :cyan
142
- end
143
- end
144
- end
145
- end
146
- end
147
- end
148
- end
@@ -1,146 +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 InstallDockerEngine < Thor
10
- namespace "install_docker_engine"
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", "Install Docker Engine 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", "Uninstall Docker Engine from 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
- BACKUP_DIR="/usr/local/backups"
62
- SCRIPT_ID="install_docker_engine"
63
- BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
64
- AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
65
-
66
- TARGET_PKGS=(docker-ce docker-ce-cli containerd.io)
67
-
68
- echo "✍🏻 TARGET_PKGS=(\${TARGET_PKGS[*]})"
69
-
70
- if [ ! -f "$AFTER_FILE" ]; then
71
- for pkg in "\${TARGET_PKGS[@]}"; do
72
- if dpkg -l "\$pkg" &>/dev/null; then
73
- echo "\$pkg" >> "$BEFORE_FILE"
74
- fi
75
- done
76
-
77
- if [ ! -f /usr/share/keyrings/docker-archive-keyring.gpg ]; then
78
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
79
- fi
80
-
81
- if [ ! -f /etc/apt/sources.list.d/docker.list ]; then
82
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
83
- fi
84
-
85
- echo "✍🏻 Installing Docker Engine..."
86
- sudo apt-get update -y
87
- sudo apt-get install -y "\${TARGET_PKGS[@]}"
88
- sudo touch "$AFTER_FILE" && echo " - marker at $AFTER_FILE"
89
- echo "✅ Docker installed"
90
- else
91
- echo "🔄 Docker already set up, ensuring latest..."
92
- sudo apt-get update -y
93
- sudo apt-get install -y "\${TARGET_PKGS[@]}"
94
- echo "✅ Docker packages are current"
95
- fi
96
- EOH
97
- say output
98
- say "✅ Docker Engine setup completed", :green
99
- end
100
-
101
- def perform_rollback(ssh)
102
- output = ssh.exec! <<~EOH
103
- set -e
104
-
105
- sudo mkdir -p /usr/local/backups
106
- sudo chown deploy:deploy /usr/local/backups
107
-
108
- BACKUP_DIR="/usr/local/backups"
109
- SCRIPT_ID="install_docker_engine"
110
- BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
111
- AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
112
-
113
- TARGET_PKGS=(docker-ce docker-ce-cli containerd.io)
114
-
115
- echo "✍🏻 TARGET_PKGS=(\${TARGET_PKGS[*]})"
116
-
117
- if [ -f "$AFTER_FILE" ]; then
118
- to_remove=()
119
- for pkg in "\${TARGET_PKGS[@]}"; do
120
- if dpkg -l "\$pkg" &>/dev/null && ! grep -Fxq "\$pkg" "$BEFORE_FILE"; then
121
- to_remove+=("\$pkg")
122
- fi
123
- done
124
-
125
- if [ \${#to_remove[@]} -gt 0 ]; then
126
- echo "🔁 Removing Docker packages..."
127
- sudo apt-get remove -y "\${to_remove[@]}" && echo " - removed: \${to_remove[*]}"
128
- else
129
- echo " - no Docker packages to remove"
130
- fi
131
-
132
- sudo rm -f "$BEFORE_FILE" "$AFTER_FILE" && echo " - cleanup markers"
133
- else
134
- echo " - no marker for $SCRIPT_ID, skipping"
135
- fi
136
-
137
- echo "✅ Rollback done"
138
- EOH
139
- say output
140
- say "✅ Docker Engine rollback completed", :green
141
- end
142
- end
143
- end
144
- end
145
- end
146
- end
@@ -1,142 +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 PostinstallDocker < Thor
10
- namespace "postinstall_docker"
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", "Apply Docker post-install configuration (start service, add groups, create network)"
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", "Undo Docker post-install configuration"
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
- BACKUP_DIR="/usr/local/backups"
62
- SCRIPT_ID="postinstall_docker"
63
- BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
64
- AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
65
-
66
- echo "✍🏻 Performing post-install Docker tasks"
67
-
68
- if [ ! -f "$AFTER_FILE" ]; then
69
- # Record state
70
- systemctl is-enabled docker &>/dev/null && echo "docker.service enabled" >> "$BEFORE_FILE" || echo "docker.service disabled" >> "$BEFORE_FILE"
71
- groups deploy | grep -q docker && echo "deploy in docker group" >> "$BEFORE_FILE" || echo "deploy not in docker group" >> "$BEFORE_FILE"
72
- sudo docker network inspect private &>/dev/null && echo "network private exists" >> "$BEFORE_FILE" || echo "network private absent" >> "$BEFORE_FILE"
73
-
74
- # Start and enable Docker
75
- sudo systemctl start docker
76
- sudo systemctl enable docker
77
- echo "🚀 Docker service started and enabled"
78
- echo "docker.service enabled" >> "$AFTER_FILE"
79
-
80
- # Add deploy to docker group
81
- sudo usermod -aG docker deploy
82
- echo "👥 Added 'deploy' to docker group"
83
- echo "added docker group" >> "$AFTER_FILE"
84
-
85
- # Create private network if missing
86
- if ! sudo docker network inspect private &>/dev/null; then
87
- sudo docker network create -d bridge private
88
- echo "🌐 Created Docker network 'private'"
89
- echo "created network private" >> "$AFTER_FILE"
90
- fi
91
-
92
- echo "✅ Post-install Docker tasks complete"
93
- else
94
- echo "🔄 Post-install tasks already applied, skipping setup"
95
- fi
96
- EOH
97
- say output
98
- say "✅ Post-install Docker setup completed", :green
99
- end
100
-
101
- def perform_rollback(ssh)
102
- output = ssh.exec! <<~EOH
103
- set -e
104
-
105
- sudo mkdir -p /usr/local/backups
106
- sudo chown deploy:deploy /usr/local/backups
107
-
108
- BACKUP_DIR="/usr/local/backups"
109
- SCRIPT_ID="postinstall_docker"
110
- BEFORE_FILE="${BACKUP_DIR}/${SCRIPT_ID}.before"
111
- AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"
112
-
113
- echo "🔄 Rolling back post-install Docker tasks..."
114
-
115
- if [ -f "$AFTER_FILE" ]; then
116
- if grep -Fxq "docker.service enabled" "$AFTER_FILE"; then
117
- sudo systemctl disable docker
118
- echo " - Docker service disabled"
119
- fi
120
- if grep -Fxq "added docker group" "$AFTER_FILE"; then
121
- sudo gpasswd -d deploy docker || true
122
- echo " - Removed 'deploy' from docker group"
123
- fi
124
- if grep -Fxq "created network private" "$AFTER_FILE"; then
125
- sudo docker network rm private || true
126
- echo " - Removed Docker network 'private'"
127
- fi
128
- sudo rm -f "$BEFORE_FILE" "$AFTER_FILE"
129
- else
130
- echo " - no marker for $SCRIPT_ID, skipping rollback"
131
- fi
132
-
133
- echo "✅ Rollback complete"
134
- EOH
135
- say output
136
- say "✅ Post-install Docker rollback completed", :green
137
- end
138
- end
139
- end
140
- end
141
- end
142
- end
@@ -1,43 +0,0 @@
1
- require "thor"
2
- require_relative "../defaults"
3
- require_relative "../provisioner"
4
- require_relative "../options_builder"
5
-
6
- module Kitsune
7
- module Kit
8
- module Commands
9
- class Provision < Thor
10
- namespace "provision"
11
-
12
- class_option :droplet_name, type: :string, aliases: "-n", desc: "Droplet name"
13
- class_option :region, type: :string, aliases: "-r", desc: "Region"
14
- class_option :size, type: :string, aliases: "-s", desc: "Size"
15
- class_option :image, type: :string, aliases: "-i", desc: "Image"
16
- class_option :tag, type: :string, aliases: "-t", desc: "Tag to filter/create"
17
- class_option :ssh_key_id, type: :string, aliases: "-k", desc: "SSH key ID"
18
-
19
- desc "create", "Create the Droplet if it doesn't exist"
20
- def create
21
- filled_options = Kitsune::Kit::OptionsBuilder.build(
22
- options,
23
- required: [:ssh_key_id],
24
- defaults: Kitsune::Kit::Defaults.infra
25
- )
26
-
27
- Provisioner.new(filled_options).create_or_show
28
- end
29
-
30
- desc "rollback", "Remove the Droplet if it exists"
31
- def rollback
32
- filled_options = Kitsune::Kit::OptionsBuilder.build(
33
- options,
34
- required: [:ssh_key_id],
35
- defaults: Kitsune::Kit::Defaults.infra
36
- )
37
-
38
- Provisioner.new(filled_options).rollback
39
- end
40
- end
41
- end
42
- end
43
- end