cloner 0.13.0 → 0.14.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.
data/lib/cloner/mysql.rb CHANGED
@@ -1,17 +1,19 @@
1
1
  module Cloner::MySQL
2
+ extend ActiveSupport::Concern
3
+
2
4
  def my_local_auth
3
- if ar_conf['password'].blank?
5
+ if local_db_config['password'].blank?
4
6
  ""
5
7
  else
6
- "--password='#{ar_conf['password']}'"
8
+ "--password='#{local_db_config['password']}'"
7
9
  end
8
10
  end
9
11
 
10
12
  def my_remote_auth
11
- if ar_r_conf['password'].blank?
13
+ if remote_db_config['password'].blank?
12
14
  ""
13
15
  else
14
- "--password='#{ar_r_conf['password']}'"
16
+ "--password='#{remote_db_config['password']}'"
15
17
  end
16
18
  end
17
19
 
@@ -26,6 +28,35 @@ module Cloner::MySQL
26
28
  def my_bin_path(util)
27
29
  util
28
30
  end
31
+
32
+ def my_local_bin_path(util)
33
+ if local_docker_compose? && local_docker_compose_service
34
+ # For mysql restore, we pipe data in, so we don't wrap it
35
+ return util if util == "mysql"
36
+
37
+ # Build docker compose exec command
38
+ compose_cmd = local_docker_compose_exec(
39
+ local_docker_compose_service,
40
+ util,
41
+ no_tty: true
42
+ )
43
+ return compose_cmd
44
+ end
45
+ my_bin_path(util)
46
+ end
47
+
48
+ def my_remote_bin_path(util)
49
+ if remote_docker_compose? && remote_docker_compose_service
50
+ # Build docker compose exec command for remote
51
+ compose_cmd = remote_docker_compose_exec(
52
+ remote_docker_compose_service,
53
+ util,
54
+ no_tty: true
55
+ )
56
+ return compose_cmd
57
+ end
58
+ my_bin_path(util)
59
+ end
29
60
 
30
61
  def my_dump_remote
31
62
  puts "backup remote DB via ssh"
@@ -33,9 +64,9 @@ module Cloner::MySQL
33
64
  ssh.exec!("rm -R #{e remote_dump_path}")
34
65
  ret = ssh_exec!(ssh, "mkdir -p #{e remote_dump_path}")
35
66
  check_ssh_err(ret)
36
- host = ar_r_conf['host'].present? ? " --host #{e ar_r_conf['host']}" : ""
37
- port = ar_r_conf['port'].present? ? " --port #{e ar_r_conf['port']}" : ""
38
- dump = "#{my_bin_path 'mysqldump'} #{my_dump_param} --user #{e ar_r_conf['username']} #{my_remote_auth}#{host}#{port} #{e ar_r_conf['database']} > #{e(remote_dump_path + '/'+db_file_name+'.sql')}"
67
+ host = remote_db_config['host'].present? ? " --host #{e remote_db_config['host']}" : ""
68
+ port = remote_db_config['port'].present? ? " --port #{e remote_db_config['port']}" : ""
69
+ dump = "#{my_remote_bin_path 'mysqldump'} #{my_dump_param} --user #{e remote_db_config['username']} #{my_remote_auth}#{host}#{port} #{e remote_db_config['database']} > #{e(remote_dump_path + '/'+db_file_name+'.sql')}"
39
70
  puts dump if verbose?
40
71
  ret = ssh_exec!(ssh, dump)
41
72
  check_ssh_err(ret)
@@ -44,15 +75,34 @@ module Cloner::MySQL
44
75
 
45
76
  def my_dump_restore
46
77
  puts "restoring DB"
47
- host = ar_conf['host'].present? ? " --host #{e ar_conf['host']}" : ""
48
- port = ar_conf['port'].present? ? " --port #{e ar_conf['port']}" : ""
49
- restore = "#{my_bin_path 'mysql'} #{my_restore_param} --user #{e ar_conf['username']} #{my_local_auth}#{host}#{port} #{e ar_to} < #{e(my_path + '/'+db_file_name+'.sql')}"
50
- puts restore if verbose?
51
- pipe = IO.popen(restore)
52
- while (line = pipe.gets)
53
- print line if verbose?
78
+
79
+ if local_docker_compose? && local_docker_compose_service
80
+ # Docker compose restore - pipe the SQL file to docker compose exec
81
+ host = local_db_config['host'].present? ? " --host #{e local_db_config['host']}" : ""
82
+ port = local_db_config['port'].present? ? " --port #{e local_db_config['port']}" : ""
83
+
84
+ compose_path = local_docker_compose_path
85
+ compose_file = local_docker_compose_file
86
+ service = local_docker_compose_service
87
+
88
+ # MySQL requires password to be passed differently in Docker
89
+ restore = "cat #{e(my_path + '/'+db_file_name+'.sql')} | (cd #{e compose_path} && docker compose -f #{e compose_file} exec -T #{e service} mysql #{my_restore_param} --user #{e local_db_config['username']} #{my_local_auth}#{host}#{port} #{e ar_to})"
90
+ puts restore if verbose?
91
+ system(restore)
92
+ ret = $?.to_i
93
+ else
94
+ # Standard restore
95
+ host = local_db_config['host'].present? ? " --host #{e local_db_config['host']}" : ""
96
+ port = local_db_config['port'].present? ? " --port #{e local_db_config['port']}" : ""
97
+ restore = "#{my_local_bin_path 'mysql'} #{my_restore_param} --user #{e local_db_config['username']} #{my_local_auth}#{host}#{port} #{e ar_to} < #{e(my_path + '/'+db_file_name+'.sql')}"
98
+ puts restore if verbose?
99
+ pipe = IO.popen(restore)
100
+ while (line = pipe.gets)
101
+ print line if verbose?
102
+ end
103
+ ret = $?.to_i
54
104
  end
55
- ret = $?.to_i
105
+
56
106
  if ret != 0
57
107
  puts "Error: local command exited with #{ret}"
58
108
  end
@@ -2,18 +2,18 @@ module Cloner::Postgres
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def pg_local_auth
5
- if ar_conf['password'].blank?
5
+ if local_db_config['password'].blank?
6
6
  ""
7
7
  else
8
- "PGPASSWORD='#{ar_conf['password']}' "
8
+ "PGPASSWORD='#{local_db_config['password']}' "
9
9
  end
10
10
  end
11
11
 
12
12
  def pg_remote_auth
13
- if ar_r_conf['password'].blank?
13
+ if remote_db_config['password'].blank?
14
14
  ""
15
15
  else
16
- "PGPASSWORD='#{ar_r_conf['password']}' "
16
+ "PGPASSWORD='#{remote_db_config['password']}' "
17
17
  end
18
18
  end
19
19
 
@@ -37,10 +37,39 @@ module Cloner::Postgres
37
37
  end
38
38
 
39
39
  def pg_local_bin_path(util)
40
+ if local_docker_compose? && local_docker_compose_service
41
+ # For pg_restore, we pipe data in, so we don't wrap it in docker compose
42
+ return util if util == "pg_restore"
43
+
44
+ # Build docker compose exec command
45
+ env_vars = {}
46
+ env_vars['PGPASSWORD'] = local_db_config['password'] if local_db_config['password'].present?
47
+
48
+ compose_cmd = local_docker_compose_exec(
49
+ local_docker_compose_service,
50
+ "env PGPASSWORD='#{local_db_config['password']}' #{util}",
51
+ env: env_vars,
52
+ no_tty: true
53
+ )
54
+ return compose_cmd
55
+ end
40
56
  pg_bin_path(util)
41
57
  end
42
58
 
43
59
  def pg_remote_bin_path(util)
60
+ if remote_docker_compose? && remote_docker_compose_service
61
+ # Build docker compose exec command for remote
62
+ env_vars = {}
63
+ env_vars['PGPASSWORD'] = remote_db_config['password'] if remote_db_config['password'].present?
64
+
65
+ compose_cmd = remote_docker_compose_exec(
66
+ remote_docker_compose_service,
67
+ "env PGPASSWORD='#{remote_db_config['password']}' #{util}",
68
+ env: env_vars,
69
+ no_tty: true
70
+ )
71
+ return compose_cmd
72
+ end
44
73
  pg_bin_path(util)
45
74
  end
46
75
 
@@ -50,9 +79,9 @@ module Cloner::Postgres
50
79
  ssh.exec!("rm -R #{e remote_dump_path}")
51
80
  ret = ssh_exec!(ssh, "mkdir -p #{e remote_dump_path}")
52
81
  check_ssh_err(ret)
53
- host = ar_r_conf['host'].present? ? " -h #{e ar_r_conf['host']}" : ""
54
- port = ar_r_conf['port'].present? ? " -p #{e ar_r_conf['port']}" : ""
55
- dump = pg_remote_auth + "#{pg_remote_bin_path 'pg_dump'} #{pg_dump_param} -U #{e ar_r_conf['username']}#{host}#{port} #{e ar_r_conf['database']} > #{e(remote_dump_path + '/'+db_file_name+'.bak')}"
82
+ host = remote_db_config['host'].present? ? " -h #{e remote_db_config['host']}" : ""
83
+ port = remote_db_config['port'].present? ? " -p #{e remote_db_config['port']}" : ""
84
+ dump = pg_remote_auth + "#{pg_remote_bin_path 'pg_dump'} #{pg_dump_param} -U #{e remote_db_config['username']}#{host}#{port} #{e remote_db_config['database']} > #{e(remote_dump_path + '/'+db_file_name+'.bak')}"
56
85
  puts dump if verbose?
57
86
  ret = ssh_exec!(ssh, dump)
58
87
  check_ssh_err(ret)
@@ -61,15 +90,34 @@ module Cloner::Postgres
61
90
 
62
91
  def pg_dump_restore
63
92
  puts "restoring DB"
64
- host = ar_conf['host'].present? ? " -h #{e ar_conf['host']}" : ""
65
- port = ar_conf['port'].present? ? " -p #{e ar_conf['port']}" : ""
66
- restore = pg_local_auth + "#{pg_local_bin_path 'pg_restore'} #{pg_restore_param} -U #{e ar_conf['username']}#{host}#{port} -d #{e ar_to} #{e(pg_path + '/'+db_file_name+'.bak')}"
67
- puts restore if verbose?
68
- pipe = IO.popen(restore)
69
- while (line = pipe.gets)
70
- print line if verbose?
93
+
94
+ if local_docker_compose? && local_docker_compose_service
95
+ # Docker compose restore - pipe the backup file to docker compose exec
96
+ host = local_db_config['host'].present? ? " -h #{e local_db_config['host']}" : ""
97
+ port = local_db_config['port'].present? ? " -p #{e local_db_config['port']}" : ""
98
+
99
+ env_str = local_db_config['password'].present? ? "--env PGPASSWORD=#{e local_db_config['password']}" : ""
100
+ compose_path = local_docker_compose_path
101
+ compose_file = local_docker_compose_file
102
+ service = local_docker_compose_service
103
+
104
+ restore = "cat #{e(pg_path + '/'+db_file_name+'.bak')} | (cd #{e compose_path} && docker compose -f #{e compose_file} exec -T #{env_str} #{e service} pg_restore #{pg_restore_param} -U #{e local_db_config['username']}#{host}#{port} -d #{e ar_to})"
105
+ puts restore if verbose?
106
+ system(restore)
107
+ ret = $?.to_i
108
+ else
109
+ # Standard restore
110
+ host = local_db_config['host'].present? ? " -h #{e local_db_config['host']}" : ""
111
+ port = local_db_config['port'].present? ? " -p #{e local_db_config['port']}" : ""
112
+ restore = pg_local_auth + "#{pg_local_bin_path 'pg_restore'} #{pg_restore_param} -U #{e local_db_config['username']}#{host}#{port} -d #{e ar_to} #{e(pg_path + '/'+db_file_name+'.bak')}"
113
+ puts restore if verbose?
114
+ pipe = IO.popen(restore)
115
+ while (line = pipe.gets)
116
+ print line if verbose?
117
+ end
118
+ ret = $?.to_i
71
119
  end
72
- ret = $?.to_i
120
+
73
121
  if ret != 0
74
122
  puts "Error: local command exited with #{ret}"
75
123
  end
@@ -1,4 +1,4 @@
1
1
  module Cloner
2
- VERSION = "0.13.0"
2
+ VERSION = "0.14.0"
3
3
  end
4
4
 
data/lib/cloner.rb CHANGED
@@ -6,6 +6,7 @@ require 'fileutils'
6
6
  module Cloner
7
7
  autoload :Base, "cloner/base"
8
8
  autoload :Internal, "cloner/internal"
9
+ autoload :DockerCompose, "cloner/docker_compose"
9
10
  autoload :Ar, "cloner/ar"
10
11
  autoload :MongoDB, "cloner/mongodb"
11
12
  autoload :Postgres, "cloner/postgres"
@@ -2,13 +2,16 @@ class ClonerGenerator < Rails::Generators::Base
2
2
  source_root File.expand_path('templates', __dir__)
3
3
 
4
4
  class_option :extend, default: false, type: :boolean, aliases: '-e'
5
+ class_option :docker_compose, default: false, type: :boolean, aliases: '-d', desc: 'Include Docker Compose configuration examples'
5
6
 
6
7
  desc "This generator create lib/tasks/dl.thor"
7
8
  def create_task_file
8
- unless options[:extend]
9
- create_default_task_file
10
- else
9
+ if options[:docker_compose]
10
+ create_docker_compose_task_file
11
+ elsif options[:extend]
11
12
  create_extended_task_file
13
+ else
14
+ create_default_task_file
12
15
  end
13
16
  end
14
17
 
@@ -19,7 +22,18 @@ class ClonerGenerator < Rails::Generators::Base
19
22
 
20
23
  def create_extended_task_file
21
24
  say 'Create extend file'
22
- @username = Rails.application.class.parent_name.downcase
25
+ module_parent_name = if Rails::VERSION::MAJOR >= 6
26
+ Rails.application.class.module_parent_name
27
+ else
28
+ Rails.application.class.parent_name
29
+ end
30
+ @username = module_parent_name.downcase
23
31
  template 'cloner_extend.thor.erb', 'lib/tasks/dl.thor'
24
32
  end
33
+
34
+ def create_docker_compose_task_file
35
+ say 'Create Docker Compose file with examples'
36
+ @username = Rails.application.class.parent_name.downcase
37
+ template 'cloner_docker_compose.thor.erb', 'lib/tasks/dl.thor'
38
+ end
25
39
  end
@@ -0,0 +1,154 @@
1
+ require 'cloner'
2
+
3
+ class Dl < Cloner::Base
4
+ no_commands do
5
+ def rails_path
6
+ File.expand_path("../../../config/environment", __FILE__)
7
+ end
8
+
9
+ def stages
10
+ @_stages ||= {
11
+ # TODO: Add new stages here if you needed
12
+ production: {
13
+ # TODO: Fix production settings
14
+ ssh_host: 'production.example.com',
15
+ ssh_user: '<%= @username %>',
16
+ # Docker Compose settings for remote
17
+ docker_compose: true,
18
+ docker_compose_service: 'db', # service name in compose.yml
19
+ docker_compose_path: '/data/<%= @username %>/app/current'
20
+ },
21
+ staging: {
22
+ # TODO: Fix staging settings
23
+ ssh_host: 'staging.example.com',
24
+ ssh_user: '<%= @username %>',
25
+ # Docker Compose settings for remote
26
+ docker_compose: true,
27
+ docker_compose_service: 'db',
28
+ docker_compose_path: '/data/<%= @username %>/app/current'
29
+ }
30
+ }
31
+ end
32
+
33
+ def ssh_host
34
+ stages.dig(options[:from].to_sym, :ssh_host)
35
+ end
36
+
37
+ def ssh_user
38
+ stages.dig(options[:from].to_sym, :ssh_user)
39
+ end
40
+
41
+ def remote_dump_path
42
+ # TODO: Fix remote dump path
43
+ '/data/<%= @username %>/dump'
44
+ end
45
+
46
+ def remote_app_path
47
+ # TODO: Fix remote app path
48
+ '/data/<%= @username %>/app/current'
49
+ end
50
+
51
+ # Docker Compose configuration for remote
52
+ def remote_docker_compose?
53
+ stages.dig(options[:from].to_sym, :docker_compose) || false
54
+ end
55
+
56
+ def remote_docker_compose_service
57
+ stages.dig(options[:from].to_sym, :docker_compose_service)
58
+ end
59
+
60
+ def remote_docker_compose_path
61
+ stages.dig(options[:from].to_sym, :docker_compose_path) || remote_app_path
62
+ end
63
+
64
+ # Docker Compose configuration for local (if needed)
65
+ def local_docker_compose?
66
+ # TODO: Set to true if you use Docker Compose locally
67
+ false
68
+ end
69
+
70
+ def local_docker_compose_service
71
+ # TODO: Set your local Docker Compose service name
72
+ 'db'
73
+ end
74
+
75
+ def local_docker_compose_path
76
+ # TODO: Set your local Docker Compose path
77
+ Rails.root.to_s
78
+ end
79
+
80
+ # Override for PostgreSQL with Docker Compose (example)
81
+ # def pg_remote_bin_path(util)
82
+ # if remote_docker_compose? && remote_docker_compose_service
83
+ # # Example: Read .env file for credentials
84
+ # env_vars = read_remote_env_file
85
+ # "cd #{e remote_docker_compose_path} && docker compose exec --no-TTY --env PGPASSWORD='#{env_vars['DB_PASSWORD']}' #{e remote_docker_compose_service} #{util}"
86
+ # else
87
+ # super
88
+ # end
89
+ # end
90
+
91
+ # Helper to read remote .env file (example)
92
+ # def read_remote_env_file
93
+ # env_content = ""
94
+ # do_ssh do |ssh|
95
+ # env_content = ssh.exec!("cat #{e remote_app_path}/.env")
96
+ # end
97
+ #
98
+ # # Parse .env content
99
+ # env_vars = {}
100
+ # env_content.each_line do |line|
101
+ # next if line.strip.empty? || line.strip.start_with?('#')
102
+ # key, value = line.strip.split('=', 2)
103
+ # next unless key && value
104
+ # # Remove quotes if present
105
+ # value = value.gsub(/^["']|["']$/, '')
106
+ # env_vars[key] = value
107
+ # end
108
+ # env_vars
109
+ # end
110
+ end
111
+
112
+ class_option :from,
113
+ default: 'production',
114
+ type: :string,
115
+ desc: 'stage name where cloner get data'
116
+ class_option :skip_database,
117
+ default: false,
118
+ type: :boolean,
119
+ aliases: '-D',
120
+ desc: 'skip clone database'
121
+ class_option :skip_files,
122
+ default: false,
123
+ type: :boolean,
124
+ aliases: '-F',
125
+ desc: 'skip clone files'
126
+
127
+ desc "download", "clone files and DB from production"
128
+ def download
129
+ load_env
130
+ say "Clone from: #{options[:from]}", :green
131
+
132
+ if remote_docker_compose?
133
+ say "Using Docker Compose on remote (service: #{remote_docker_compose_service})", :blue
134
+ end
135
+
136
+ if local_docker_compose?
137
+ say "Using Docker Compose locally (service: #{local_docker_compose_service})", :blue
138
+ end
139
+
140
+ if options[:skip_database]
141
+ say "Skip clone database!", :yellow
142
+ else
143
+ clone_db
144
+ end
145
+
146
+ if options[:skip_files]
147
+ say "Skip clone files!", :yellow
148
+ else
149
+ # TODO: Fix folders for synchronization here
150
+ rsync_public("ckeditor_assets")
151
+ rsync_public("uploads")
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,158 @@
1
+ require_relative '../lib/cloner'
2
+ require 'pathname'
3
+ require 'net/ssh'
4
+ require 'shellwords'
5
+ require 'active_support/core_ext/object'
6
+
7
+ class ::Rails
8
+ def self.root
9
+ Pathname.new(File.expand_path("../", __FILE__))
10
+ end
11
+ def self.env
12
+ "development"
13
+ end
14
+ end
15
+
16
+ class DlComposeTest < Cloner::Base
17
+ no_commands do
18
+ def rails_path
19
+ File.expand_path("../", __FILE__)
20
+ end
21
+
22
+ def load_env
23
+ require 'net/ssh'
24
+ require 'shellwords'
25
+ require 'active_support/core_ext/object'
26
+ end
27
+
28
+ # SSH configuration
29
+ def ssh_host
30
+ 'rscx.ru'
31
+ end
32
+
33
+ def ssh_user
34
+ 'root'
35
+ end
36
+
37
+ def env_from
38
+ "production"
39
+ end
40
+
41
+ # Remote paths
42
+ def remote_dump_path
43
+ '/root/compose/aichaos/tmp_dump'
44
+ end
45
+
46
+ def remote_app_path
47
+ '/root/compose/aichaos'
48
+ end
49
+
50
+ # Docker Compose configuration for remote
51
+ def remote_docker_compose?
52
+ true
53
+ end
54
+
55
+ def remote_docker_compose_service
56
+ 'db' # Assuming the PostgreSQL service is named 'db'
57
+ end
58
+
59
+ def remote_docker_compose_path
60
+ remote_app_path
61
+ end
62
+
63
+ # Docker Compose configuration for local
64
+ def local_docker_compose?
65
+ true
66
+ end
67
+
68
+ def local_docker_compose_service
69
+ 'testdb'
70
+ end
71
+
72
+ def local_docker_compose_path
73
+ Rails.root.parent.to_s
74
+ end
75
+
76
+ # Override to read credentials from remote .env file
77
+ def read_ar_r_conf
78
+ # Read from remote .env file
79
+ env_content = ""
80
+ do_ssh do |ssh|
81
+ env_content = ssh.exec!("cat #{e remote_app_path}/.env")
82
+ end
83
+
84
+ # Parse .env content
85
+ env_vars = {}
86
+ env_content.each_line do |line|
87
+ next if line.strip.empty? || line.strip.start_with?('#')
88
+ key, value = line.strip.split('=', 2)
89
+ next unless key && value
90
+ # Remove quotes if present
91
+ value = value.gsub(/^["']|["']$/, '')
92
+ env_vars[key] = value
93
+ end
94
+
95
+ {
96
+ adapter: "postgresql",
97
+ host: env_vars['DB_HOST'] || 'db',
98
+ port: env_vars['DB_PORT'] || '5432',
99
+ database: env_vars['DB_NAME'] || 'aichaos',
100
+ username: env_vars['DB_USER'] || 'aichaos',
101
+ password: env_vars['DB_PASSWORD'] || ''
102
+ }.stringify_keys
103
+ end
104
+
105
+ # Local database configuration
106
+ def ar_conf
107
+ {
108
+ adapter: "postgresql",
109
+ host: "localhost",
110
+ port: "5432",
111
+ database: "test_development",
112
+ username: "testuser",
113
+ password: "testpass"
114
+ }.stringify_keys
115
+ end
116
+
117
+ def ar_to
118
+ ar_conf['database']
119
+ end
120
+
121
+ def db_file_name
122
+ "cloner_test"
123
+ end
124
+
125
+ # Override pg_bin_path to handle Docker Compose
126
+ def pg_remote_bin_path(util)
127
+ if remote_docker_compose? && remote_docker_compose_service
128
+ env_vars = read_ar_r_conf
129
+ "cd #{e remote_app_path} && docker compose exec --no-TTY --env PGPASSWORD='#{env_vars['password']}' #{e remote_docker_compose_service} #{util}"
130
+ else
131
+ super
132
+ end
133
+ end
134
+
135
+ def verbose?
136
+ true
137
+ end
138
+ end
139
+
140
+ desc "test", "Test cloning from remote Docker Compose PostgreSQL"
141
+ def test
142
+ load_env
143
+
144
+ puts "Testing Docker Compose PostgreSQL cloning..."
145
+ puts "Remote: #{ssh_user}@#{ssh_host}:#{remote_app_path}"
146
+ puts "Local: #{local_docker_compose_path}"
147
+
148
+ # Start local Docker Compose
149
+ puts "\nStarting local Docker Compose..."
150
+ system("cd #{e local_docker_compose_path} && docker compose up -d")
151
+ sleep 5 # Wait for database to be ready
152
+
153
+ # Clone the database
154
+ clone_db
155
+
156
+ puts "\nTest completed!"
157
+ end
158
+ end