chagall 0.0.1.beta5 → 0.0.1.beta6
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/lib/chagall/base.rb +18 -14
- data/lib/chagall/cli.rb +14 -55
- data/lib/chagall/compose/main.rb +20 -13
- data/lib/chagall/compose.rb +54 -0
- data/lib/chagall/deploy.rb +258 -0
- data/lib/chagall/rollback.rb +11 -0
- data/lib/chagall/setup.rb +115 -0
- data/lib/chagall/ssh.rb +4 -16
- data/lib/chagall/version.rb +1 -1
- metadata +6 -4
- data/lib/chagall/deploy/main.rb +0 -261
- data/lib/chagall/setup/main.rb +0 -121
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3cbd29143d65c6c6713ea03e0e26a29d7bb520139638cec49467fa932a1915d
|
4
|
+
data.tar.gz: 3c758792a4a406ff7981de63eae1512602b2b41184ce1c68ea00c1aa369dfcd3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53637eb5f31edfd125b9a14c844e888f316e8914a5bfd817eea97e60ae9e5cd5d5b3619f7f1cee2af1421b6ccf4c2e903845bb95cd70a7eaf9d7537312ef0447
|
7
|
+
data.tar.gz: 51b246cc1a5e21171552f56bae8e1b9b4f179468369613de76d7c0e45cf37e8a67b048f63d61248f0deaeb0cc9316ace2d46ad506ff2f516dfd5206ad777b227
|
data/lib/chagall/base.rb
CHANGED
@@ -1,29 +1,33 @@
|
|
1
1
|
require "logger"
|
2
|
+
require_relative "ssh"
|
2
3
|
|
3
4
|
module Chagall
|
4
|
-
class Base
|
5
|
+
class Base < Clamp::Command
|
5
6
|
attr_reader :logger, :ssh
|
6
7
|
|
7
8
|
LOG_LEVELS = {
|
8
9
|
"info" => Logger::INFO,
|
9
10
|
"debug" => Logger::DEBUG,
|
10
11
|
"warn" => Logger::WARN,
|
11
|
-
"error" => Logger::ERROR
|
12
|
-
}
|
13
|
-
|
14
|
-
def
|
15
|
-
@logger
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
12
|
+
"error" => Logger::ERROR,
|
13
|
+
}
|
14
|
+
|
15
|
+
def logger
|
16
|
+
@logger ||= Logger.new($stdout).tap do |l|
|
17
|
+
l.formatter = proc do |severity, _, _, msg|
|
18
|
+
if severity == "DEBUG"
|
19
|
+
"[#{severity}] #{msg}\n"
|
20
|
+
else
|
21
|
+
"#{msg}\n"
|
22
|
+
end
|
21
23
|
end
|
22
|
-
end
|
23
24
|
|
24
|
-
|
25
|
+
l.level = LOG_LEVELS[ENV.fetch("LOG_LEVEL", "info")]
|
26
|
+
end
|
27
|
+
end
|
25
28
|
|
26
|
-
|
29
|
+
def ssh
|
30
|
+
@ssh ||= SSH.new(logger: logger)
|
27
31
|
end
|
28
32
|
end
|
29
33
|
end
|
data/lib/chagall/cli.rb
CHANGED
@@ -2,13 +2,21 @@
|
|
2
2
|
|
3
3
|
require "clamp"
|
4
4
|
require_relative "settings"
|
5
|
-
require_relative "deploy
|
6
|
-
require_relative "compose
|
5
|
+
require_relative "deploy"
|
6
|
+
require_relative "compose"
|
7
|
+
require_relative "rollback"
|
7
8
|
|
8
9
|
Clamp.allow_options_after_parameters = true
|
9
10
|
|
10
11
|
module Chagall
|
11
12
|
class Cli < Clamp::Command
|
13
|
+
|
14
|
+
def run(arguments)
|
15
|
+
parse(arguments)
|
16
|
+
Chagall::Settings.configure(collect_options_hash)
|
17
|
+
execute
|
18
|
+
end
|
19
|
+
|
12
20
|
def self.options_from_config_file
|
13
21
|
@options_from_config_file ||= begin
|
14
22
|
config_path = File.join(Dir.pwd, "chagall.yml") || File.join(Dir.pwd, "chagall.yaml")
|
@@ -49,59 +57,10 @@ module Chagall
|
|
49
57
|
exit(0)
|
50
58
|
end
|
51
59
|
|
52
|
-
subcommand "deploy", "Deploy the application to the server"
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
subcommand "setup", "Setup the server for deployment" do
|
60
|
-
def execute
|
61
|
-
Chagall::Settings.configure(collect_options_hash)
|
62
|
-
Chagall::Setup::Main.new
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
subcommand "compose", "Run Docker Compose commands with arguments passed through" do
|
67
|
-
# Override parse method to handle all arguments after the subcommand
|
68
|
-
def parse(arguments)
|
69
|
-
if arguments.empty?
|
70
|
-
puts "ERROR: Missing required arguments"
|
71
|
-
puts "Usage: chagall compose COMMAND [OPTIONS]"
|
72
|
-
exit(1)
|
73
|
-
end
|
74
|
-
|
75
|
-
# Extract the first argument as command
|
76
|
-
@command = arguments.shift
|
77
|
-
|
78
|
-
# Store the rest as raw args
|
79
|
-
@raw_args = arguments
|
80
|
-
|
81
|
-
# Validate required arguments
|
82
|
-
if @command.nil? || @command.empty?
|
83
|
-
puts "ERROR: Command is required"
|
84
|
-
puts "Usage: chagall compose COMMAND [OPTIONS]"
|
85
|
-
exit(1)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def execute
|
90
|
-
Chagall::Settings.configure(collect_options_hash)
|
91
|
-
Chagall::Compose::Main.new(@command, @raw_args)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
subcommand "rollback", "Rollback to previous deployment" do
|
96
|
-
option [ "--steps" ], "STEPS", "Number of steps to rollback", default: "1" do |s|
|
97
|
-
Integer(s)
|
98
|
-
end
|
99
|
-
|
100
|
-
def execute
|
101
|
-
Chagall::Settings.configure(collect_options_hash)
|
102
|
-
puts "Rollback functionality not implemented yet"
|
103
|
-
end
|
104
|
-
end
|
60
|
+
subcommand "deploy", "Deploy the application to the server", Chagall::Deploy
|
61
|
+
subcommand "setup", "Setup the server for deployment", Chagall::Setup
|
62
|
+
subcommand "compose", "Run Docker Compose commands with arguments passed through", Chagall::Compose
|
63
|
+
subcommand "rollback", "Rollback to previous deployment", Chagall::Rollback
|
105
64
|
|
106
65
|
private
|
107
66
|
|
data/lib/chagall/compose/main.rb
CHANGED
@@ -1,32 +1,39 @@
|
|
1
|
-
require_relative "../settings"
|
2
1
|
require_relative "../base"
|
3
2
|
|
4
3
|
module Chagall
|
5
4
|
module Compose
|
6
|
-
# Build and execute command usign docker compose on server
|
7
5
|
class Main < Base
|
8
|
-
attr_reader :command, :
|
6
|
+
attr_reader :command, :service, :args
|
9
7
|
|
10
|
-
def initialize(command, args)
|
11
|
-
super()
|
8
|
+
def initialize(command, *args)
|
12
9
|
@command = command
|
13
|
-
@
|
14
|
-
@
|
10
|
+
@service = args.first if args.first && !args.first.start_with?('-')
|
11
|
+
@args = @service ? args[1..-1] : args
|
15
12
|
|
16
13
|
raise Chagall::Error, "Command is required" if @command.nil? || @command.empty?
|
17
14
|
|
18
15
|
run_command
|
19
16
|
end
|
20
17
|
|
21
|
-
|
22
|
-
cmd = "cd #{Settings.instance.project_folder_path} && #{build_docker_compose_command} #{@command}"
|
23
|
-
cmd << " #{arguments}" unless arguments.empty?
|
18
|
+
private
|
24
19
|
|
20
|
+
def run_command
|
21
|
+
cmd = build_command
|
25
22
|
logger.debug "Executing: #{cmd}"
|
26
|
-
|
23
|
+
|
24
|
+
result = ssh.execute(cmd, tty: true)
|
25
|
+
raise Chagall::Error, "Command failed: #{cmd}" unless result
|
27
26
|
end
|
28
27
|
|
29
|
-
|
28
|
+
def build_command
|
29
|
+
cmd = [ "cd #{Settings.instance.project_folder_path}" ]
|
30
|
+
cmd << build_docker_compose_command
|
31
|
+
cmd << @command
|
32
|
+
cmd << @service if @service
|
33
|
+
cmd << @args.join(" ") if @args && @args.any?
|
34
|
+
|
35
|
+
cmd.join(" && ")
|
36
|
+
end
|
30
37
|
|
31
38
|
def build_docker_compose_command
|
32
39
|
compose_files = Settings[:compose_files]
|
@@ -42,4 +49,4 @@ module Chagall
|
|
42
49
|
end
|
43
50
|
end
|
44
51
|
end
|
45
|
-
end
|
52
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
|
3
|
+
module Chagall
|
4
|
+
# Build and execute command usign docker compose on server
|
5
|
+
class Compose < Base
|
6
|
+
attr_reader :command, :arguments
|
7
|
+
|
8
|
+
|
9
|
+
# Override parse method to handle all arguments after the subcommand
|
10
|
+
def parse(arguments)
|
11
|
+
if arguments.empty?
|
12
|
+
puts "ERROR: Missing required arguments"
|
13
|
+
puts "Usage: chagall compose COMMAND [OPTIONS]"
|
14
|
+
exit(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Extract the first argument as command
|
18
|
+
@command = arguments.shift
|
19
|
+
|
20
|
+
# Store the rest as raw args
|
21
|
+
@raw_args = arguments
|
22
|
+
|
23
|
+
# Validate required arguments
|
24
|
+
if @command.nil? || @command.empty?
|
25
|
+
puts "ERROR: Command is required"
|
26
|
+
puts "Usage: chagall compose COMMAND [OPTIONS]"
|
27
|
+
exit(1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def execute
|
32
|
+
cmd = "cd #{Settings.instance.project_folder_path} && #{build_docker_compose_command} #{@command}"
|
33
|
+
cmd << " #{@raw_args.join(" ")}" unless @raw_args.empty?
|
34
|
+
|
35
|
+
logger.debug "Executing: #{cmd}"
|
36
|
+
ssh.execute(cmd, tty: true)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def build_docker_compose_command
|
42
|
+
compose_files = Settings[:compose_files]
|
43
|
+
compose_cmd = [ "docker compose" ]
|
44
|
+
|
45
|
+
if compose_files && !compose_files.empty?
|
46
|
+
compose_files.each do |file|
|
47
|
+
compose_cmd << "-f #{File.basename(file)}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
compose_cmd.join(" ")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require "digest"
|
5
|
+
require "benchmark"
|
6
|
+
require "yaml"
|
7
|
+
|
8
|
+
module Chagall
|
9
|
+
class Deploy < Base
|
10
|
+
|
11
|
+
attr_reader :total_time
|
12
|
+
|
13
|
+
def execute
|
14
|
+
@interrupted = false
|
15
|
+
@total_time = 0.0
|
16
|
+
setup_signal_handlers
|
17
|
+
Time.now
|
18
|
+
|
19
|
+
t("Checking uncommitted changes") { check_uncommit_changes } unless Settings[:skip_uncommit]
|
20
|
+
t("Check image or build") { check_image_or_build }
|
21
|
+
t("tag as production") { tag_as_production }
|
22
|
+
t("update compose files") { update_compose_files }
|
23
|
+
t("deploy compose files") { deploy_compose_files }
|
24
|
+
t("rotate release") { rotate_releases }
|
25
|
+
|
26
|
+
print_total_time
|
27
|
+
rescue Interrupt
|
28
|
+
logger.info "\nDeployment interrupted by user"
|
29
|
+
print_total_time
|
30
|
+
cleanup_and_exit
|
31
|
+
rescue StandardError => e
|
32
|
+
logger.error "Deployment failed: #{e.message}"
|
33
|
+
logger.debug e.backtrace.join("\n") if ENV["DEBUG"]
|
34
|
+
print_total_time
|
35
|
+
exit 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_image_or_build
|
39
|
+
image_exists = verify_image(check_only: true)
|
40
|
+
|
41
|
+
if image_exists
|
42
|
+
logger.info "Image #{@image_tag} exists and compose files are up to date"
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
t("Building image") { build }
|
47
|
+
t("Rotating cache") { rotate_cache }
|
48
|
+
t("Verifying image") { verify_image }
|
49
|
+
end
|
50
|
+
|
51
|
+
def setup_signal_handlers
|
52
|
+
# Handle CTRL+C (SIGINT)
|
53
|
+
Signal.trap("INT") do
|
54
|
+
@interrupted = true
|
55
|
+
puts "\nReceived interrupt signal. Cleaning up..."
|
56
|
+
cleanup_and_exit
|
57
|
+
end
|
58
|
+
|
59
|
+
# Handle SIGTERM
|
60
|
+
Signal.trap("TERM") do
|
61
|
+
@interrupted = true
|
62
|
+
puts "\nReceived termination signal. Cleaning up..."
|
63
|
+
cleanup_and_exit
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def cleanup_and_exit
|
68
|
+
puts "Cleaning up..."
|
69
|
+
# Add any cleanup tasks here
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_interrupted
|
74
|
+
return unless @interrupted
|
75
|
+
|
76
|
+
puts "Operation interrupted by user"
|
77
|
+
cleanup_and_exit
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def print_total_time
|
83
|
+
logger.info "Total execution time: #{format('%.2f', @total_time)}s"
|
84
|
+
end
|
85
|
+
|
86
|
+
def t(title)
|
87
|
+
logger.info "[#{title.upcase}]..."
|
88
|
+
start_time = Time.now
|
89
|
+
result = yield
|
90
|
+
duration = Time.now - start_time
|
91
|
+
@total_time += duration
|
92
|
+
logger.info " done #{'%.2f' % duration}s"
|
93
|
+
check_interrupted
|
94
|
+
result
|
95
|
+
rescue StandardError
|
96
|
+
duration = Time.now - start_time
|
97
|
+
@total_time += duration
|
98
|
+
logger.error " failed #{format('%.2f', duration)}s"
|
99
|
+
raise
|
100
|
+
end
|
101
|
+
|
102
|
+
def check_uncommit_changes
|
103
|
+
status = `git status --porcelain`.strip
|
104
|
+
raise "Uncommitted changes found. Commit first" unless status.empty?
|
105
|
+
end
|
106
|
+
|
107
|
+
def build
|
108
|
+
logger.debug "Building #{Settings.instance.image_tag} image and load to server"
|
109
|
+
system(build_cmd)
|
110
|
+
end
|
111
|
+
|
112
|
+
def rotate_cache
|
113
|
+
system("rm -rf #{Settings[:cache_from]}")
|
114
|
+
system("mv #{Settings[:cache_to]} #{Settings[:cache_from]}")
|
115
|
+
end
|
116
|
+
|
117
|
+
def verify_image(check_only: false)
|
118
|
+
logger.debug "Verifying image on server..."
|
119
|
+
|
120
|
+
check_cmd = "docker images --filter=reference=#{Settings.instance.image_tag} --format '{{.ID}}' | grep ."
|
121
|
+
|
122
|
+
# Use backticks to capture output instead of system
|
123
|
+
output = `#{ssh.command(check_cmd)} 2>/dev/null`.strip
|
124
|
+
exists = !output.empty?
|
125
|
+
|
126
|
+
if check_only
|
127
|
+
logger.debug "Image #{exists ? 'found' : 'not found'}: #{Settings.instance.image_tag}"
|
128
|
+
return exists
|
129
|
+
end
|
130
|
+
|
131
|
+
raise "Docker image #{Settings.instance.image_tag} not found on #{Settings[:server]}" unless exists
|
132
|
+
|
133
|
+
true
|
134
|
+
end
|
135
|
+
|
136
|
+
def verify_compose_files
|
137
|
+
puts "Verifying compose files on server..."
|
138
|
+
|
139
|
+
Settings[:compose_files].all? do |file|
|
140
|
+
remote_file = "#{Settings[:projects_folder]}/#{File.basename(file)}"
|
141
|
+
local_md5 = ::Digest::MD5.file(file).hexdigest
|
142
|
+
|
143
|
+
check_cmd = "md5sum #{remote_file} 2>/dev/null | cut -d' ' -f1"
|
144
|
+
remote_md5 = `#{ssh.command(check_cmd)}`.strip
|
145
|
+
local_md5 == remote_md5
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def tag_as_production
|
150
|
+
logger.debug "Tagging Docker #{Settings.instance.image_tag} image as production..."
|
151
|
+
|
152
|
+
command = "docker tag #{Settings.instance.image_tag} #{Settings[:name]}:production"
|
153
|
+
ssh.execute(command) or raise "Failed to tag Docker image"
|
154
|
+
end
|
155
|
+
|
156
|
+
def build_cmd
|
157
|
+
args = [
|
158
|
+
"--cache-from type=local,src=#{Settings[:cache_from]}",
|
159
|
+
"--cache-to type=local,dest=#{Settings[:cache_to]},mode=max",
|
160
|
+
"--platform #{Settings[:platform]}",
|
161
|
+
"--tag #{Settings.instance.image_tag}",
|
162
|
+
"--target #{Settings[:target]}",
|
163
|
+
"--file #{Settings[:dockerfile]}"
|
164
|
+
]
|
165
|
+
|
166
|
+
if Settings[:remote]
|
167
|
+
args.push("--load")
|
168
|
+
else
|
169
|
+
args.push("--output type=docker,dest=-")
|
170
|
+
end
|
171
|
+
|
172
|
+
args.push(Settings[:docker_context])
|
173
|
+
|
174
|
+
args = args.map { |arg| " #{arg}" }
|
175
|
+
.join(" \\\n")
|
176
|
+
|
177
|
+
cmd = "docker build \\\n#{args}"
|
178
|
+
if Settings[:remote]
|
179
|
+
ssh.command(cmd)
|
180
|
+
else
|
181
|
+
"#{cmd} | #{ssh.command('docker load')}"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def update_compose_files
|
186
|
+
logger.debug "Updating compose configuration files on remote server..."
|
187
|
+
|
188
|
+
Settings[:compose_files].each do |file|
|
189
|
+
remote_destination = "#{Settings.instance.project_folder_path}/#{File.basename(file)}"
|
190
|
+
copy_file(file, remote_destination)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def deploy_compose_files
|
195
|
+
logger.debug "Updating compose services..."
|
196
|
+
deploy_command = [ "docker compose" ]
|
197
|
+
|
198
|
+
# Use the remote file paths for docker compose command
|
199
|
+
Settings[:compose_files].each do |file|
|
200
|
+
deploy_command << "-f #{File.basename(file)}"
|
201
|
+
end
|
202
|
+
deploy_command << "up -d"
|
203
|
+
|
204
|
+
ssh.execute(deploy_command.join(" "),
|
205
|
+
directory: Settings.instance.project_folder_path) or raise "Failed to update compose services"
|
206
|
+
end
|
207
|
+
|
208
|
+
def copy_file(local_file, remote_destination)
|
209
|
+
logger.debug "Copying #{local_file} to #{Settings[:server]}:#{remote_destination}..."
|
210
|
+
command = "scp #{local_file} #{Settings[:server]}:#{remote_destination}"
|
211
|
+
|
212
|
+
system(command) or raise "Failed to copy #{local_file} to server"
|
213
|
+
end
|
214
|
+
|
215
|
+
def rotate_releases
|
216
|
+
logger.debug "Rotating releases..."
|
217
|
+
release_folder = "#{Settings.instance.project_folder_path}/releases"
|
218
|
+
release_file = "#{release_folder}/#{Settings[:release]}"
|
219
|
+
|
220
|
+
# Create releases directory if it doesn't exist
|
221
|
+
ssh.execute("mkdir -p #{release_folder}")
|
222
|
+
|
223
|
+
# Save current release
|
224
|
+
ssh.execute("touch #{release_file}")
|
225
|
+
|
226
|
+
# Get list of releases sorted by modification time (newest first)
|
227
|
+
list_cmd = "ls -t #{release_folder}"
|
228
|
+
releases = `#{ssh.command(list_cmd)}`.strip.split("\n")
|
229
|
+
|
230
|
+
# Keep only the last N releases
|
231
|
+
logger.info "releases #{releases.length}"
|
232
|
+
return unless releases.length > Settings[:keep_releases]
|
233
|
+
|
234
|
+
releases_to_remove = releases[Settings[:keep_releases]..]
|
235
|
+
|
236
|
+
# Remove old release files
|
237
|
+
releases_to_remove.each do |release|
|
238
|
+
ssh.execute("rm #{release_folder}/#{release}")
|
239
|
+
|
240
|
+
# Remove corresponding Docker image
|
241
|
+
image = "#{Settings[:name]}:#{release}"
|
242
|
+
logger.info "Removing old Docker image: #{image}"
|
243
|
+
ssh.execute("docker rmi #{image} || true") # Use || true to prevent failure if image is already removed
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def system(*args)
|
248
|
+
result = super
|
249
|
+
raise "Command failed with exit code #{$CHILD_STATUS.exitstatus}: #{args.join(' ')}" unless result
|
250
|
+
|
251
|
+
result
|
252
|
+
end
|
253
|
+
|
254
|
+
def ssh
|
255
|
+
@ssh ||= SSH.new
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "io/console"
|
5
|
+
require_relative "base"
|
6
|
+
|
7
|
+
module Chagall
|
8
|
+
# Handles server provisioning and Docker environment setup for deployment
|
9
|
+
class Setup < Base
|
10
|
+
class DockerSetupError < StandardError; end
|
11
|
+
|
12
|
+
def execute
|
13
|
+
install_docker unless docker_installed?
|
14
|
+
setup_non_root_docker_deamon_access if unable_to_access_docker_deamon?
|
15
|
+
create_project_folder unless project_folder_exists?
|
16
|
+
create_env_files
|
17
|
+
|
18
|
+
logger.info "Docker environment setup complete"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def create_env_files
|
24
|
+
logger.debug "Create env files described at services..."
|
25
|
+
|
26
|
+
Settings[:compose_files].each do |file|
|
27
|
+
yaml = YAML.load_file(file, aliases: true)
|
28
|
+
yaml["services"].each do |service|
|
29
|
+
service[1]["env_file"]&.each do |env_file|
|
30
|
+
ssh.execute("touch #{env_file}", directory: Settings.instance.project_folder_path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def unable_to_access_docker_deamon?
|
37
|
+
return true
|
38
|
+
docker_result = ssh.command("docker ps")
|
39
|
+
docker_output = `#{docker_result} 2>&1`.strip
|
40
|
+
logger.debug "Docker output: #{docker_output}"
|
41
|
+
docker_output.downcase.include?("permission denied") ||
|
42
|
+
docker_output.downcase.include?("connect: permission denied")
|
43
|
+
end
|
44
|
+
|
45
|
+
def setup_non_root_docker_deamon_access
|
46
|
+
logger.info "Add user to docker group..."
|
47
|
+
|
48
|
+
username = `#{ssh.command("whoami")} 2>&1`.strip
|
49
|
+
return true if username == "root"
|
50
|
+
|
51
|
+
groups = `#{ssh.command("groups")} 2>&1`.strip
|
52
|
+
return if groups.include?("docker")
|
53
|
+
|
54
|
+
logger.debug "Adding #{username} user to docker group"
|
55
|
+
ssh.execute("sudo usermod -aG docker #{username}", tty: true)
|
56
|
+
ssh.execute("groups #{username} | grep -q docker")
|
57
|
+
|
58
|
+
logger.debug "Successfully added user to docker group"
|
59
|
+
true
|
60
|
+
rescue StandardError => e
|
61
|
+
logger.error "Error setting up Docker daemon access: #{e.message}"
|
62
|
+
logger.debug e.backtrace.join("\n")
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def docker_installed?
|
67
|
+
logger.debug "Checking Docker installation..."
|
68
|
+
|
69
|
+
docker_output = `#{ssh.command("docker --version")} 2>&1`.strip
|
70
|
+
logger.debug "Docker version output: '#{docker_output}'"
|
71
|
+
|
72
|
+
compose_output = `#{ssh.command("docker compose version")} 2>&1`.strip
|
73
|
+
logger.debug "Docker Compose output: '#{compose_output}'"
|
74
|
+
|
75
|
+
return true if docker_output.include?("Docker version") &&
|
76
|
+
compose_output.include?("Docker Compose version")
|
77
|
+
|
78
|
+
logger.warn "Docker check failed:"
|
79
|
+
logger.warn "Docker output: #{docker_output}"
|
80
|
+
logger.warn "Docker Compose output: #{compose_output}"
|
81
|
+
false
|
82
|
+
rescue StandardError => e
|
83
|
+
logger.error "Error checking Docker installation: #{e.message}"
|
84
|
+
logger.debug e.backtrace.join("\n")
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
def project_folder_exists?
|
89
|
+
ssh.execute("test -d #{Settings.instance.project_folder_path}")
|
90
|
+
rescue StandardError => e
|
91
|
+
logger.error "Error checking project folder: #{e.message}"
|
92
|
+
false
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_project_folder
|
96
|
+
logger.debug "Creating project folder #{Settings.instance.project_folder_path}"
|
97
|
+
ssh.execute("mkdir -p #{Settings.instance.project_folder_path}")
|
98
|
+
end
|
99
|
+
|
100
|
+
def install_docker
|
101
|
+
logger.debug "Installing Docker..."
|
102
|
+
|
103
|
+
command = '(curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo "exit 1") | sh'
|
104
|
+
# Clear any existing sudo session
|
105
|
+
ssh.execute("sudo -k")
|
106
|
+
|
107
|
+
logger.debug "Executing: #{command}"
|
108
|
+
result = ssh.execute(command, tty: true)
|
109
|
+
raise DockerSetupError, "Failed to execute: #{command}" unless result
|
110
|
+
rescue StandardError => e
|
111
|
+
logger.error "Docker installation failed: #{e.message}"
|
112
|
+
raise DockerSetupError, "Failed to install Docker: #{e.message}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/chagall/ssh.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
|
-
|
2
1
|
require "English"
|
2
|
+
|
3
3
|
module Chagall
|
4
4
|
class SSH
|
5
|
-
attr_reader :server, :ssh_args
|
5
|
+
attr_reader :server, :ssh_args, :logger
|
6
6
|
|
7
7
|
DEFAULT_SSH_ARGS = "-o StrictHostKeyChecking=no -o ServerAliveInterval=60".freeze
|
8
8
|
|
9
|
-
def initialize(server: Settings.instance.options[:server], ssh_args: DEFAULT_SSH_ARGS)
|
9
|
+
def initialize(server: Settings.instance.options[:server], ssh_args: DEFAULT_SSH_ARGS, logger: Logger.new($stdout))
|
10
10
|
@server = server
|
11
11
|
@ssh_args = ssh_args
|
12
|
+
@logger = logger
|
12
13
|
end
|
13
14
|
|
14
15
|
def execute(command, directory: nil, tty: false)
|
@@ -38,18 +39,5 @@ module Chagall
|
|
38
39
|
|
39
40
|
"#{ssh_cmd.join(' ')} '#{cmd}'"
|
40
41
|
end
|
41
|
-
|
42
|
-
def logger
|
43
|
-
@logger ||= Logger.new($stdout).tap do |l|
|
44
|
-
l.formatter = proc do |severity, _, _, msg|
|
45
|
-
if severity == "DEBUG"
|
46
|
-
"[#{severity}] #{msg}\n"
|
47
|
-
else
|
48
|
-
"#{msg}\n"
|
49
|
-
end
|
50
|
-
end
|
51
|
-
l.level = ENV.fetch("LOG_LEVEL") { "debug" }.downcase == "debug" ? Logger::DEBUG : Logger::INFO
|
52
|
-
end
|
53
|
-
end
|
54
42
|
end
|
55
43
|
end
|
data/lib/chagall/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chagall
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1.
|
4
|
+
version: 0.0.1.beta6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Klevtsov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: clamp
|
@@ -93,13 +93,15 @@ files:
|
|
93
93
|
- lib/chagall.rb
|
94
94
|
- lib/chagall/base.rb
|
95
95
|
- lib/chagall/cli.rb
|
96
|
+
- lib/chagall/compose.rb
|
96
97
|
- lib/chagall/compose/main.rb
|
97
|
-
- lib/chagall/deploy
|
98
|
+
- lib/chagall/deploy.rb
|
98
99
|
- lib/chagall/install/main.rb
|
99
100
|
- lib/chagall/install/templates/template.Dockerfile
|
100
101
|
- lib/chagall/install/templates/template.compose.yaml
|
102
|
+
- lib/chagall/rollback.rb
|
101
103
|
- lib/chagall/settings.rb
|
102
|
-
- lib/chagall/setup
|
104
|
+
- lib/chagall/setup.rb
|
103
105
|
- lib/chagall/ssh.rb
|
104
106
|
- lib/chagall/version.rb
|
105
107
|
homepage: https://github.com/frontandstart/chagall
|
data/lib/chagall/deploy/main.rb
DELETED
@@ -1,261 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "../settings"
|
4
|
-
require_relative "../ssh"
|
5
|
-
require "digest"
|
6
|
-
require "benchmark"
|
7
|
-
require "yaml"
|
8
|
-
|
9
|
-
module Chagall
|
10
|
-
module Deploy
|
11
|
-
class Main < Base
|
12
|
-
attr_reader :total_time
|
13
|
-
|
14
|
-
def initialize
|
15
|
-
super()
|
16
|
-
@interrupted = false
|
17
|
-
@total_time = 0.0
|
18
|
-
setup_signal_handlers
|
19
|
-
Time.now
|
20
|
-
|
21
|
-
t("Checking uncommitted changes") { check_uncommit_changes } unless Settings[:skip_uncommit]
|
22
|
-
t("Check image or build") { check_image_or_build }
|
23
|
-
t("tag as production") { tag_as_production }
|
24
|
-
t("update compose files") { update_compose_files }
|
25
|
-
t("deploy compose files") { deploy_compose_files }
|
26
|
-
t("rotate release") { rotate_releases }
|
27
|
-
|
28
|
-
print_total_time
|
29
|
-
rescue Interrupt
|
30
|
-
logger.info "\nDeployment interrupted by user"
|
31
|
-
print_total_time
|
32
|
-
cleanup_and_exit
|
33
|
-
rescue StandardError => e
|
34
|
-
logger.error "Deployment failed: #{e.message}"
|
35
|
-
logger.debug e.backtrace.join("\n") if ENV["DEBUG"]
|
36
|
-
print_total_time
|
37
|
-
exit 1
|
38
|
-
end
|
39
|
-
|
40
|
-
def check_image_or_build
|
41
|
-
image_exists = verify_image(check_only: true)
|
42
|
-
|
43
|
-
if image_exists
|
44
|
-
logger.info "Image #{Settings.instance.image_tag} exists and compose files are up to date"
|
45
|
-
return
|
46
|
-
end
|
47
|
-
|
48
|
-
t("Building image") { build }
|
49
|
-
t("Rotating cache") { rotate_cache }
|
50
|
-
t("Verifying image") { verify_image }
|
51
|
-
end
|
52
|
-
|
53
|
-
def setup_signal_handlers
|
54
|
-
# Handle CTRL+C (SIGINT)
|
55
|
-
Signal.trap("INT") do
|
56
|
-
@interrupted = true
|
57
|
-
puts "\nReceived interrupt signal. Cleaning up..."
|
58
|
-
cleanup_and_exit
|
59
|
-
end
|
60
|
-
|
61
|
-
# Handle SIGTERM
|
62
|
-
Signal.trap("TERM") do
|
63
|
-
@interrupted = true
|
64
|
-
puts "\nReceived termination signal. Cleaning up..."
|
65
|
-
cleanup_and_exit
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def cleanup_and_exit
|
70
|
-
puts "Cleaning up..."
|
71
|
-
# Add any cleanup tasks here
|
72
|
-
exit 1
|
73
|
-
end
|
74
|
-
|
75
|
-
def check_interrupted
|
76
|
-
return unless @interrupted
|
77
|
-
|
78
|
-
puts "Operation interrupted by user"
|
79
|
-
cleanup_and_exit
|
80
|
-
end
|
81
|
-
|
82
|
-
private
|
83
|
-
|
84
|
-
def print_total_time
|
85
|
-
logger.info "Total execution time: #{format('%.2f', @total_time)}s"
|
86
|
-
end
|
87
|
-
|
88
|
-
def t(title)
|
89
|
-
logger.info "[#{title.upcase}]..."
|
90
|
-
start_time = Time.now
|
91
|
-
result = yield
|
92
|
-
duration = Time.now - start_time
|
93
|
-
@total_time += duration
|
94
|
-
logger.info " done #{'%.2f' % duration}s"
|
95
|
-
check_interrupted
|
96
|
-
result
|
97
|
-
rescue StandardError
|
98
|
-
duration = Time.now - start_time
|
99
|
-
@total_time += duration
|
100
|
-
logger.error " failed #{format('%.2f', duration)}s"
|
101
|
-
raise
|
102
|
-
end
|
103
|
-
|
104
|
-
def check_uncommit_changes
|
105
|
-
status = `git status --porcelain`.strip
|
106
|
-
raise "Uncommitted changes found. Commit first" unless status.empty?
|
107
|
-
end
|
108
|
-
|
109
|
-
def setup_server
|
110
|
-
SetupServer.new(ssh, logger).setup
|
111
|
-
end
|
112
|
-
|
113
|
-
def build
|
114
|
-
logger.debug "Building #{Settings.instance.image_tag} image and load to server"
|
115
|
-
system(build_cmd)
|
116
|
-
end
|
117
|
-
|
118
|
-
def rotate_cache
|
119
|
-
system("rm -rf #{Settings[:cache_from]}")
|
120
|
-
system("mv #{Settings[:cache_to]} #{Settings[:cache_from]}")
|
121
|
-
end
|
122
|
-
|
123
|
-
def verify_image(check_only: false)
|
124
|
-
logger.debug "Verifying image on server..."
|
125
|
-
|
126
|
-
check_cmd = "docker images --filter=reference=#{Settings.instance.image_tag} --format '{{.ID}}' | grep ."
|
127
|
-
|
128
|
-
# Use backticks to capture output instead of system
|
129
|
-
output = `#{ssh.command(check_cmd)} 2>/dev/null`.strip
|
130
|
-
exists = !output.empty?
|
131
|
-
|
132
|
-
if check_only
|
133
|
-
logger.debug "Image #{exists ? 'found' : 'not found'}: #{Settings.instance.image_tag}"
|
134
|
-
return exists
|
135
|
-
end
|
136
|
-
|
137
|
-
raise "Docker image #{Settings.instance.image_tag} not found on #{Settings[:server]}" unless exists
|
138
|
-
|
139
|
-
true
|
140
|
-
end
|
141
|
-
|
142
|
-
def verify_compose_files
|
143
|
-
puts "Verifying compose files on server..."
|
144
|
-
|
145
|
-
Settings[:compose_files].all? do |file|
|
146
|
-
remote_file = "#{Settings[:projects_folder]}/#{File.basename(file)}"
|
147
|
-
local_md5 = ::Digest::MD5.file(file).hexdigest
|
148
|
-
|
149
|
-
check_cmd = "md5sum #{remote_file} 2>/dev/null | cut -d' ' -f1"
|
150
|
-
remote_md5 = `#{ssh.command(check_cmd)}`.strip
|
151
|
-
local_md5 == remote_md5
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
def tag_as_production
|
156
|
-
logger.debug "Tagging Docker #{Settings.instance.image_tag} image as production..."
|
157
|
-
|
158
|
-
command = "docker tag #{Settings.instance.image_tag} #{Settings[:name]}:production"
|
159
|
-
ssh.execute(command) or raise "Failed to tag Docker image"
|
160
|
-
end
|
161
|
-
|
162
|
-
def build_cmd
|
163
|
-
args = [
|
164
|
-
"--cache-from type=local,src=#{Settings[:cache_from]}",
|
165
|
-
"--cache-to type=local,dest=#{Settings[:cache_to]},mode=max",
|
166
|
-
"--platform #{Settings[:platform]}",
|
167
|
-
"--tag #{Settings.instance.image_tag}",
|
168
|
-
"--target #{Settings[:target]}",
|
169
|
-
"--file #{Settings[:dockerfile]}"
|
170
|
-
]
|
171
|
-
|
172
|
-
if Settings[:remote]
|
173
|
-
args.push("--load")
|
174
|
-
else
|
175
|
-
args.push("--output type=docker,dest=-")
|
176
|
-
end
|
177
|
-
|
178
|
-
args.push(Settings[:docker_context])
|
179
|
-
|
180
|
-
args = args.map { |arg| " #{arg}" }
|
181
|
-
.join(" \\\n")
|
182
|
-
|
183
|
-
cmd = "docker build \\\n#{args}"
|
184
|
-
if Settings[:remote]
|
185
|
-
ssh.command(cmd)
|
186
|
-
else
|
187
|
-
"#{cmd} | #{ssh.command('docker load')}"
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def update_compose_files
|
192
|
-
logger.debug "Updating compose configuration files on remote server..."
|
193
|
-
|
194
|
-
Settings[:compose_files].each do |file|
|
195
|
-
remote_destination = "#{Settings.instance.project_folder_path}/#{File.basename(file)}"
|
196
|
-
copy_file(file, remote_destination)
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
def deploy_compose_files
|
201
|
-
logger.debug "Updating compose services..."
|
202
|
-
deploy_command = [ "docker compose" ]
|
203
|
-
|
204
|
-
# Use the remote file paths for docker compose command
|
205
|
-
Settings[:compose_files].each do |file|
|
206
|
-
deploy_command << "-f #{File.basename(file)}"
|
207
|
-
end
|
208
|
-
deploy_command << "up -d"
|
209
|
-
|
210
|
-
ssh.execute(deploy_command.join(" "),
|
211
|
-
directory: Settings.instance.project_folder_path) or raise "Failed to update compose services"
|
212
|
-
end
|
213
|
-
|
214
|
-
def copy_file(local_file, remote_destination)
|
215
|
-
logger.debug "Copying #{local_file} to #{Settings[:server]}:#{remote_destination}..."
|
216
|
-
command = "scp #{local_file} #{Settings[:server]}:#{remote_destination}"
|
217
|
-
|
218
|
-
system(command) or raise "Failed to copy #{local_file} to server"
|
219
|
-
end
|
220
|
-
|
221
|
-
def rotate_releases
|
222
|
-
logger.debug "Rotating releases..."
|
223
|
-
release_folder = "#{Settings.instance.project_folder_path}/releases"
|
224
|
-
release_file = "#{release_folder}/#{Settings[:release]}"
|
225
|
-
|
226
|
-
# Create releases directory if it doesn't exist
|
227
|
-
ssh.execute("mkdir -p #{release_folder}")
|
228
|
-
|
229
|
-
# Save current release
|
230
|
-
ssh.execute("touch #{release_file}")
|
231
|
-
|
232
|
-
# Get list of releases sorted by modification time (newest first)
|
233
|
-
list_cmd = "ls -t #{release_folder}"
|
234
|
-
releases = `#{ssh.command(list_cmd)}`.strip.split("\n")
|
235
|
-
|
236
|
-
# Keep only the last N releases
|
237
|
-
logger.info "releases #{releases.length}"
|
238
|
-
return unless releases.length > Settings[:keep_releases]
|
239
|
-
|
240
|
-
releases_to_remove = releases[Settings[:keep_releases]..]
|
241
|
-
|
242
|
-
# Remove old release files
|
243
|
-
releases_to_remove.each do |release|
|
244
|
-
ssh.execute("rm #{release_folder}/#{release}")
|
245
|
-
|
246
|
-
# Remove corresponding Docker image
|
247
|
-
image = "#{Settings[:name]}:#{release}"
|
248
|
-
logger.info "Removing old Docker image: #{image}"
|
249
|
-
ssh.execute("docker rmi #{image} || true") # Use || true to prevent failure if image is already removed
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
def system(*args)
|
254
|
-
result = super
|
255
|
-
raise "Command failed with exit code #{$CHILD_STATUS.exitstatus}: #{args.join(' ')}" unless result
|
256
|
-
|
257
|
-
result
|
258
|
-
end
|
259
|
-
end
|
260
|
-
end
|
261
|
-
end
|
data/lib/chagall/setup/main.rb
DELETED
@@ -1,121 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "yaml"
|
4
|
-
require "io/console"
|
5
|
-
|
6
|
-
module Chagall
|
7
|
-
module Setup
|
8
|
-
# Handles server provisioning and Docker environment setup for deployment
|
9
|
-
class Main < Base
|
10
|
-
class DockerSetupError < StandardError; end
|
11
|
-
|
12
|
-
def initialize
|
13
|
-
super()
|
14
|
-
setup
|
15
|
-
end
|
16
|
-
|
17
|
-
def setup
|
18
|
-
install_docker unless docker_installed?
|
19
|
-
setup_non_root_docker_deamon_access if unable_to_access_docker_deamon?
|
20
|
-
create_project_folder unless project_folder_exists?
|
21
|
-
create_env_files
|
22
|
-
|
23
|
-
logger.info "Docker environment setup complete"
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def create_env_files
|
29
|
-
logger.debug "Create env files described at services..."
|
30
|
-
|
31
|
-
Settings[:compose_files].each do |file|
|
32
|
-
yaml = YAML.load_file(file, aliases: true)
|
33
|
-
yaml["services"].each do |service|
|
34
|
-
service[1]["env_file"]&.each do |env_file|
|
35
|
-
ssh.execute("touch #{env_file}", directory: Settings.instance.project_folder_path)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def unable_to_access_docker_deamon?
|
42
|
-
return true
|
43
|
-
docker_result = ssh.command("docker ps")
|
44
|
-
docker_output = `#{docker_result} 2>&1`.strip
|
45
|
-
logger.debug "Docker output: #{docker_output}"
|
46
|
-
docker_output.downcase.include?("permission denied") ||
|
47
|
-
docker_output.downcase.include?("connect: permission denied")
|
48
|
-
end
|
49
|
-
|
50
|
-
def setup_non_root_docker_deamon_access
|
51
|
-
logger.info "Add user to docker group..."
|
52
|
-
|
53
|
-
username = `#{ssh.command("whoami")} 2>&1`.strip
|
54
|
-
return true if username == "root"
|
55
|
-
|
56
|
-
groups = `#{ssh.command("groups")} 2>&1`.strip
|
57
|
-
return if groups.include?("docker")
|
58
|
-
|
59
|
-
logger.debug "Adding #{username} user to docker group"
|
60
|
-
ssh.execute("sudo usermod -aG docker #{username}", tty: true)
|
61
|
-
ssh.execute("groups #{username} | grep -q docker")
|
62
|
-
|
63
|
-
logger.debug "Successfully added user to docker group"
|
64
|
-
true
|
65
|
-
rescue StandardError => e
|
66
|
-
logger.error "Error setting up Docker daemon access: #{e.message}"
|
67
|
-
logger.debug e.backtrace.join("\n")
|
68
|
-
false
|
69
|
-
end
|
70
|
-
|
71
|
-
def docker_installed?
|
72
|
-
logger.debug "Checking Docker installation..."
|
73
|
-
|
74
|
-
docker_output = `#{ssh.command("docker --version")} 2>&1`.strip
|
75
|
-
logger.debug "Docker version output: '#{docker_output}'"
|
76
|
-
|
77
|
-
compose_output = `#{ssh.command("docker compose version")} 2>&1`.strip
|
78
|
-
logger.debug "Docker Compose output: '#{compose_output}'"
|
79
|
-
|
80
|
-
return true if docker_output.include?("Docker version") &&
|
81
|
-
compose_output.include?("Docker Compose version")
|
82
|
-
|
83
|
-
logger.warn "Docker check failed:"
|
84
|
-
logger.warn "Docker output: #{docker_output}"
|
85
|
-
logger.warn "Docker Compose output: #{compose_output}"
|
86
|
-
false
|
87
|
-
rescue StandardError => e
|
88
|
-
logger.error "Error checking Docker installation: #{e.message}"
|
89
|
-
logger.debug e.backtrace.join("\n")
|
90
|
-
false
|
91
|
-
end
|
92
|
-
|
93
|
-
def project_folder_exists?
|
94
|
-
ssh.execute("test -d #{Settings.instance.project_folder_path}")
|
95
|
-
rescue StandardError => e
|
96
|
-
logger.error "Error checking project folder: #{e.message}"
|
97
|
-
false
|
98
|
-
end
|
99
|
-
|
100
|
-
def create_project_folder
|
101
|
-
logger.debug "Creating project folder #{Settings.instance.project_folder_path}"
|
102
|
-
ssh.execute("mkdir -p #{Settings.instance.project_folder_path}")
|
103
|
-
end
|
104
|
-
|
105
|
-
def install_docker
|
106
|
-
logger.debug "Installing Docker..."
|
107
|
-
|
108
|
-
command = '(curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo "exit 1") | sh'
|
109
|
-
# Clear any existing sudo session
|
110
|
-
ssh.execute("sudo -k")
|
111
|
-
|
112
|
-
logger.debug "Executing: #{command}"
|
113
|
-
result = ssh.execute(command, tty: true)
|
114
|
-
raise DockerSetupError, "Failed to execute: #{command}" unless result
|
115
|
-
rescue StandardError => e
|
116
|
-
logger.error "Docker installation failed: #{e.message}"
|
117
|
-
raise DockerSetupError, "Failed to install Docker: #{e.message}"
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|