chagall 0.0.1.beta1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/Readme.md +54 -0
- data/bin/chagall +12 -0
- data/lib/chagall/base.rb +29 -0
- data/lib/chagall/cli.rb +130 -0
- data/lib/chagall/compose/main.rb +45 -0
- data/lib/chagall/deploy/main.rb +261 -0
- data/lib/chagall/install/main.rb +321 -0
- data/lib/chagall/install/templates/template.Dockerfile +37 -0
- data/lib/chagall/install/templates/template.compose.yaml +121 -0
- data/lib/chagall/settings.rb +232 -0
- data/lib/chagall/setup/main.rb +121 -0
- data/lib/chagall/ssh.rb +55 -0
- data/lib/chagall/version.rb +3 -0
- data/lib/chagall.rb +21 -0
- metadata +129 -0
@@ -0,0 +1,321 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "erb"
|
5
|
+
require "yaml"
|
6
|
+
require "fileutils"
|
7
|
+
require "optparse"
|
8
|
+
require "logger"
|
9
|
+
require "tmpdir"
|
10
|
+
require "securerandom"
|
11
|
+
require "net/http"
|
12
|
+
require "json"
|
13
|
+
require "uri"
|
14
|
+
|
15
|
+
# The Installer class is responsible for setting up the environment for a Ruby on Rails application.
|
16
|
+
# It detects the required services and versions, generates necessary Docker and Compose files, and logs the process.
|
17
|
+
#
|
18
|
+
# Constants:
|
19
|
+
# - TEMPLATES_DIR: Directory where template files are stored.
|
20
|
+
# - DEFAULT_NODE_VERSION: Default Node.js version to use if not specified.
|
21
|
+
# - DEFAULT_RUBY_VERSION: Default Ruby version to use if not specified.
|
22
|
+
#
|
23
|
+
# Attributes:
|
24
|
+
# - app_name: Name of the application.
|
25
|
+
# - services: List of services to be included in the setup.
|
26
|
+
# - versions: Hash containing versions of Ruby, Node.js, PostgreSQL, and Redis.
|
27
|
+
# - logger: Logger instance for logging messages.
|
28
|
+
# - database_type: Type of database to use (e.g., 'postgres', 'mysql', 'sqlite').
|
29
|
+
#
|
30
|
+
# Methods:
|
31
|
+
# - initialize(options = {}): Initializes the installer with given options.
|
32
|
+
# - install: Main method to perform the installation process.
|
33
|
+
#
|
34
|
+
# Private Methods:
|
35
|
+
# - backup_file(file): Creates a backup of the specified file if it exists.
|
36
|
+
# - detect_ruby_version: Detects the Ruby version from various files or defaults to a predefined version.
|
37
|
+
# - detect_node_version: Detects the Node.js version from various files or defaults to a predefined version.
|
38
|
+
# - node_version_from_package_json: Extracts the Node.js version from package.json if available.
|
39
|
+
# - node_version_from_file: Extracts the Node.js version from .node-version, .tool-versions, or .nvmrc files if available.
|
40
|
+
# - detect_services: Detects required services based on the gems listed in the Gemfile.
|
41
|
+
# - generate_compose: Generates the Docker Compose file from a template.
|
42
|
+
# - generate_dockerfile: Generates the Dockerfile from a template.
|
43
|
+
class Installer # rubocop:disable Metrics/ClassLength
|
44
|
+
TEMPLATES_DIR = File.expand_path("./templates", __dir__)
|
45
|
+
TEMP_DIR = File.join(Dir.tmpdir, "chagall-#{SecureRandom.hex(4)}").freeze
|
46
|
+
DEFAULT_RUBY_VERSION = "3.3.0"
|
47
|
+
DEFAULT_NODE_VERSION = "20.11.0"
|
48
|
+
|
49
|
+
GITHUB_REPO = "frontandstart/chagall"
|
50
|
+
TEMPLATE_FILES = %w[template.compose.yaml
|
51
|
+
template.Dockerfile].freeze
|
52
|
+
|
53
|
+
DEPENDENCIES = [
|
54
|
+
{
|
55
|
+
adapter: :postgresql,
|
56
|
+
gem_name: "pg",
|
57
|
+
service: :postgres,
|
58
|
+
image: "postgres:16.4-bullseye",
|
59
|
+
docker_env: "DATABASE_URL: postgres://postgres:postgres@postgres:5432"
|
60
|
+
},
|
61
|
+
{
|
62
|
+
adapter: :mysql2,
|
63
|
+
gem_name: "mysql2",
|
64
|
+
service: :mariadb,
|
65
|
+
image: "mariadb:8.0-bullseye",
|
66
|
+
docker_env: "DATABASE_URL: mysql://mysql:mysql@mariadb:3306"
|
67
|
+
},
|
68
|
+
{
|
69
|
+
adapter: :mongoid,
|
70
|
+
gem_name: "mongoid",
|
71
|
+
service: :mongodb,
|
72
|
+
image: "mongo:8.0-noble",
|
73
|
+
docker_env: "DATABASE_URL: mongodb://mongodb:27017"
|
74
|
+
},
|
75
|
+
{
|
76
|
+
gem_name: "redis",
|
77
|
+
service: :redis,
|
78
|
+
image: "redis:7.4-bookworm",
|
79
|
+
docker_env: "REDIS_URL: redis://redis:6379"
|
80
|
+
},
|
81
|
+
{
|
82
|
+
gem_name: "sidekiq",
|
83
|
+
service: :sidekiq,
|
84
|
+
image: -> { app_name }
|
85
|
+
},
|
86
|
+
{
|
87
|
+
gem_name: "elasticsearch",
|
88
|
+
service: :elasticsearch,
|
89
|
+
image: "elasticsearch:8.15.3",
|
90
|
+
docker_env: "ELASTICSEARCH_URL: elasticsearch://elasticsearch:9200"
|
91
|
+
},
|
92
|
+
{
|
93
|
+
gem_name: "solid_queue",
|
94
|
+
service: :solid_queue,
|
95
|
+
image: -> { app_name }
|
96
|
+
}
|
97
|
+
].freeze
|
98
|
+
|
99
|
+
attr_reader :app_name,
|
100
|
+
:versions,
|
101
|
+
:logger,
|
102
|
+
:database_type,
|
103
|
+
:database_config,
|
104
|
+
:gemfile,
|
105
|
+
:gemfile_lock
|
106
|
+
|
107
|
+
attr_accessor :project_services,
|
108
|
+
:environments
|
109
|
+
|
110
|
+
def initialize(options = {})
|
111
|
+
raise "Gemfile not found" unless File.exist?("Gemfile")
|
112
|
+
|
113
|
+
Chagall::Settings.configure(argv)
|
114
|
+
|
115
|
+
@app_name = options[:app_name] || File.basename(Dir.pwd)
|
116
|
+
@services = []
|
117
|
+
@logger = Logger.new($stdout)
|
118
|
+
@logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
119
|
+
@environments = {}
|
120
|
+
@gemfile = File.read("Gemfile")
|
121
|
+
@gemfile_lock = File.read("Gemfile.lock")
|
122
|
+
@database_adapters = YAML.load_file("config/database.yml")
|
123
|
+
.map { |_, config| config["adapter"] }.uniq
|
124
|
+
@database_type = @database_adapters
|
125
|
+
end
|
126
|
+
|
127
|
+
def install
|
128
|
+
setup_temp_directory
|
129
|
+
detect_services
|
130
|
+
generate_environment_variables
|
131
|
+
generate_compose_file
|
132
|
+
generate_dockerfile
|
133
|
+
logger.info "Installation completed successfully!"
|
134
|
+
ensure
|
135
|
+
cleanup_temp_directory
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def setup_temp_directory
|
141
|
+
FileUtils.mkdir_p(TEMP_DIR)
|
142
|
+
download_template_files
|
143
|
+
rescue StandardError => e
|
144
|
+
logger.error "Failed to set up temporary directory: #{e.message}"
|
145
|
+
cleanup_temp_directory
|
146
|
+
raise
|
147
|
+
end
|
148
|
+
|
149
|
+
def detect_services
|
150
|
+
DEPENDENCIES.each do |dependency|
|
151
|
+
@services << dependency if gemfile_has_dependency?(dependency[:gem_name])
|
152
|
+
end
|
153
|
+
|
154
|
+
logger.info "Detected services: #{services.map { |s| s[:service] }.join(', ')}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def gemfile_has_dependency?(gem_name)
|
158
|
+
gemfile_match = gemfile.match?(/^\s*[^#].*gem ['"]#{gem_name}['"]/)
|
159
|
+
gemfile_lock_match = gemfile_lock.match?(/^\s+#{gem_name}\s+\(/) || false
|
160
|
+
|
161
|
+
gemfile_match && gemfile_lock_match
|
162
|
+
end
|
163
|
+
|
164
|
+
def generate_environment_variables
|
165
|
+
services.each do |service|
|
166
|
+
url = generate_service_url_for(service[:adapter])
|
167
|
+
environments[service[:url_name]] = url if url
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def cleanup_temp_directory
|
172
|
+
FileUtils.remove_entry_secure(TEMP_DIR) if Dir.exist?(TEMP_DIR)
|
173
|
+
end
|
174
|
+
|
175
|
+
def download_template_files
|
176
|
+
release_info = fetch_latest_release
|
177
|
+
TEMPLATE_FILES.each do |filename|
|
178
|
+
download_template(filename, release_info)
|
179
|
+
end
|
180
|
+
rescue StandardError => e
|
181
|
+
logger.error "Failed to download template files: #{e.message}"
|
182
|
+
raise
|
183
|
+
end
|
184
|
+
|
185
|
+
def fetch_latest_release
|
186
|
+
uri = URI("https://api.github.com/repos/#{GITHUB_REPO}/releases/latest")
|
187
|
+
response = Net::HTTP.get_response(uri)
|
188
|
+
|
189
|
+
raise "Failed to fetch latest release info: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
190
|
+
|
191
|
+
JSON.parse(response.body)
|
192
|
+
end
|
193
|
+
|
194
|
+
def download_template(filename, release_info)
|
195
|
+
asset = release_info["assets"].find { |a| a["name"] == filename }
|
196
|
+
raise "Template file #{filename} not found in release" unless asset
|
197
|
+
|
198
|
+
download_url = asset["browser_download_url"]
|
199
|
+
target_path = File.join(TEMP_DIR, filename)
|
200
|
+
|
201
|
+
uri = URI(download_url)
|
202
|
+
response = Net::HTTP.get_response(uri)
|
203
|
+
|
204
|
+
raise "Failed to download #{filename}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
205
|
+
|
206
|
+
File.write(target_path, response.body)
|
207
|
+
logger.info "Downloaded #{filename}"
|
208
|
+
end
|
209
|
+
|
210
|
+
def backup_file(file)
|
211
|
+
return unless File.exist?(file)
|
212
|
+
|
213
|
+
backup = "#{file}.chagall.bak"
|
214
|
+
FileUtils.cp(file, backup)
|
215
|
+
logger.info "Backed up existing #{file} to #{backup}"
|
216
|
+
end
|
217
|
+
|
218
|
+
def generate_database_url(adapter)
|
219
|
+
case adapter
|
220
|
+
when "postgresql"
|
221
|
+
"postgres://postgres:postgres@postgres:5432/db"
|
222
|
+
when "mysql2"
|
223
|
+
"mysql2://mysql:mysql@mysql:3306/db"
|
224
|
+
when "sqlite3"
|
225
|
+
"sqlite3:///data/db.sqlite3"
|
226
|
+
when "redis"
|
227
|
+
"redis://redis:5432/0"
|
228
|
+
else
|
229
|
+
raise "Unsupported adapter: #{adapter}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def generate_compose_file
|
234
|
+
backup_file("compose.yaml")
|
235
|
+
|
236
|
+
template_path = File.join(TEMP_DIR, "template.compose.yaml")
|
237
|
+
raise "Compose template not found at #{template_path}" unless File.exist?(template_path)
|
238
|
+
|
239
|
+
template = File.read(template_path)
|
240
|
+
result = ERB.new(
|
241
|
+
template,
|
242
|
+
trim_mode: "-",
|
243
|
+
services: services,
|
244
|
+
environments: environments
|
245
|
+
).result(binding)
|
246
|
+
|
247
|
+
File.write("compose.yaml", result)
|
248
|
+
logger.info "Generated compose.yaml"
|
249
|
+
end
|
250
|
+
|
251
|
+
def generate_dockerfile
|
252
|
+
backup_file("Dockerfile")
|
253
|
+
|
254
|
+
template_path = File.join(TEMP_DIR, "template.Dockerfile")
|
255
|
+
raise "Dockerfile template not found at #{template_path}" unless File.exist?(template_path)
|
256
|
+
|
257
|
+
template = File.read(template_path)
|
258
|
+
result = ERB.new(template, trim_mode: "-").result(binding)
|
259
|
+
|
260
|
+
File.write("Dockerfile", result)
|
261
|
+
logger.info "Generated Dockerfile"
|
262
|
+
end
|
263
|
+
|
264
|
+
def detect_ruby_version
|
265
|
+
from_gemfile = gemfile.match(/ruby ['"](.+?)['"]/)[1]
|
266
|
+
return from_gemfile if from_gemfile
|
267
|
+
|
268
|
+
if File.exist?(".ruby-version")
|
269
|
+
File.read(".ruby-version").strip
|
270
|
+
elsif File.exist?(".tool-versions")
|
271
|
+
File.read(".tool-versions").match(/ruby (.+?)\n/)[1]
|
272
|
+
else
|
273
|
+
DEFAULT_RUBY_VERSION
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def detect_node_version
|
278
|
+
node_version_from_package_json || node_version_from_file || DEFAULT_NODE_VERSION
|
279
|
+
end
|
280
|
+
|
281
|
+
def node_version_from_package_json
|
282
|
+
return unless File.exist?("package.json")
|
283
|
+
|
284
|
+
begin
|
285
|
+
JSON.parse(File.read("package.json")).dig("engines", "node")&.delete("^")
|
286
|
+
rescue JSON::ParserError
|
287
|
+
nil
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def node_version_from_file
|
292
|
+
if File.exist?(".node-version")
|
293
|
+
File.read(".node-version").strip
|
294
|
+
elsif File.exist?(".tool-versions")
|
295
|
+
File.read(".tool-versions").match(/node (.+?)\n/)[1]
|
296
|
+
elsif File.exist?(".nvmrc")
|
297
|
+
File.read(".nvmrc").strip
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def find_dependecy_by(name, value)
|
302
|
+
DEPENDENCIES.find { |d| d[name.to_sym] == value.to_sym }
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
if __FILE__ == $PROGRAM_NAME
|
307
|
+
options = {}
|
308
|
+
OptionParser.new do |opts|
|
309
|
+
opts.banner = "Usage: install.rb [options]"
|
310
|
+
|
311
|
+
opts.on("-n", "--non-interactive", "Run in non-interactive mode") do
|
312
|
+
options[:non_interactive] = true
|
313
|
+
end
|
314
|
+
|
315
|
+
opts.on("-a", "--app-name NAME", "Set application name") do |name|
|
316
|
+
options[:app_name] = name
|
317
|
+
end
|
318
|
+
end.parse!
|
319
|
+
|
320
|
+
Installer.new(options).install
|
321
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
ARG RUBY_VERSION=<%= @versions['ruby'] %>
|
2
|
+
ARG NODE_VERSION=<%= @versions['node'] %>
|
3
|
+
|
4
|
+
FROM ruby:${RUBY_VERSION}-slim AS development
|
5
|
+
|
6
|
+
LABEL app.name="<%= @app_name %>"
|
7
|
+
|
8
|
+
ARG NODE_VERSION
|
9
|
+
ENV NODE_VERSION=${NODE_VERSION}
|
10
|
+
WORKDIR /app
|
11
|
+
|
12
|
+
RUN apt-get update -qq && apt-get install -y \
|
13
|
+
build-essential \
|
14
|
+
libvips \
|
15
|
+
libffi-dev \
|
16
|
+
libssl-dev \
|
17
|
+
gnupg2 \
|
18
|
+
curl \
|
19
|
+
git
|
20
|
+
|
21
|
+
# Node section
|
22
|
+
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
|
23
|
+
export NVM_DIR="/root/.nvm" && \
|
24
|
+
. "$NVM_DIR/nvm.sh" && \
|
25
|
+
nvm install "$NODE_VERSION" && \
|
26
|
+
nvm use "$NODE_VERSION" && \
|
27
|
+
nvm alias default "$NODE_VERSION"
|
28
|
+
|
29
|
+
RUN gem install bundler
|
30
|
+
|
31
|
+
FROM development AS production
|
32
|
+
|
33
|
+
COPY . .
|
34
|
+
|
35
|
+
RUN bundle config set production true
|
36
|
+
RUN BUNDLE_JOBS=$(nproc) bundle install
|
37
|
+
RUN bundle exec rails assets:precompile
|
@@ -0,0 +1,121 @@
|
|
1
|
+
x-docker-environment: &docker-environment
|
2
|
+
<% if services.each do |service| %>
|
3
|
+
<%= service[:docker_env] if service[:docker_env] %>
|
4
|
+
<% end %>
|
5
|
+
HISTFILE: tmp/.docker_shell_history
|
6
|
+
PRY_HISTFILE: tmp/.docker_pry_history
|
7
|
+
RAILS_LOG_TO_STDOUT: true
|
8
|
+
|
9
|
+
x-app: &app
|
10
|
+
image: <%= @app_name %>:development
|
11
|
+
environment:
|
12
|
+
<<: *docker-environment
|
13
|
+
build:
|
14
|
+
context: .
|
15
|
+
target: development
|
16
|
+
working_dir: /app
|
17
|
+
env_file:
|
18
|
+
- .env
|
19
|
+
stdin_open: true
|
20
|
+
ports:
|
21
|
+
- ${PORT:-3000}:${PORT:-3000}
|
22
|
+
depends_on:
|
23
|
+
postgres:
|
24
|
+
condition: service_healthy
|
25
|
+
redis:
|
26
|
+
condition: service_healthy
|
27
|
+
tmpfs:
|
28
|
+
- /app/tmp/pids
|
29
|
+
volumes:
|
30
|
+
- .:/app:c
|
31
|
+
- cache:/app/tmp/cache:d
|
32
|
+
- bundle:/usr/local/bundle:d
|
33
|
+
- node_modules:/app/node_modules:d
|
34
|
+
|
35
|
+
services:
|
36
|
+
app: &app
|
37
|
+
command: bin/dev
|
38
|
+
|
39
|
+
<%- if @services.include?('postgres') -%>
|
40
|
+
postgres:
|
41
|
+
image: postgres:<%= @versions['postgres'] %>
|
42
|
+
environment:
|
43
|
+
POSTGRES_USER: postgres
|
44
|
+
POSTGRES_PASSWORD: postgres
|
45
|
+
POSTGRES_HOST: 0.0.0.0
|
46
|
+
volumes:
|
47
|
+
- postgres:/var/lib/postgresql/data:c
|
48
|
+
healthcheck:
|
49
|
+
test: ["CMD-SHELL", "pg_isready -h postgres -U postgres"]
|
50
|
+
interval: 5s
|
51
|
+
timeout: 5s
|
52
|
+
retries: 10
|
53
|
+
<%- end -%>
|
54
|
+
|
55
|
+
<%- if services.include?('redis') -%>
|
56
|
+
redis:
|
57
|
+
image: redis:<%= @versions['redis'] %>
|
58
|
+
volumes:
|
59
|
+
- redis:/data:delegated
|
60
|
+
healthcheck:
|
61
|
+
test: ["CMD", "redis-cli", "ping"]
|
62
|
+
interval: 5s
|
63
|
+
timeout: 3s
|
64
|
+
retries: 10
|
65
|
+
entrypoint: redis-server --appendonly yes
|
66
|
+
<%- end -%>
|
67
|
+
|
68
|
+
prod:
|
69
|
+
<<: *app
|
70
|
+
image: <%= @app_name %>:production
|
71
|
+
environment:
|
72
|
+
<<: *docker-environment
|
73
|
+
RAILS_ENV: production
|
74
|
+
RACK_ENV: production
|
75
|
+
profiles:
|
76
|
+
- prod
|
77
|
+
healthcheck:
|
78
|
+
test: ["CMD", "curl", "http://prod:3000/health"]
|
79
|
+
interval: 20s
|
80
|
+
timeout: 5s
|
81
|
+
retries: 3
|
82
|
+
start_period: 20s
|
83
|
+
deploy:
|
84
|
+
mode: replicated
|
85
|
+
replicas: 2
|
86
|
+
endpoint_mode: vip
|
87
|
+
update_config:
|
88
|
+
parallelism: 1
|
89
|
+
order: start-first
|
90
|
+
delay: 5s
|
91
|
+
failure_action: rollback
|
92
|
+
restart_policy:
|
93
|
+
condition: on-failure
|
94
|
+
max_attempts: 3
|
95
|
+
window: 120s
|
96
|
+
volumes:
|
97
|
+
- cache:/app/tmp/cache:d
|
98
|
+
|
99
|
+
reproxy:
|
100
|
+
image: umputun/reproxy
|
101
|
+
profiles:
|
102
|
+
- prod
|
103
|
+
ports:
|
104
|
+
- 80:8080
|
105
|
+
- 443:8443
|
106
|
+
environment:
|
107
|
+
SSL_TYPE: auto
|
108
|
+
SSL_ACME_FQDN: domain.com
|
109
|
+
SSL_ACME_LOCATION: /srv/var/acme
|
110
|
+
SSL_ACME_EMAIL: mail@example.com
|
111
|
+
FILE_ENABLED: true
|
112
|
+
volumes:
|
113
|
+
- ./config/reproxy.conf:/srv/reproxy.yml
|
114
|
+
- certs:/srv/var/acme
|
115
|
+
|
116
|
+
volumes:
|
117
|
+
postgres:
|
118
|
+
redis:
|
119
|
+
bundle:
|
120
|
+
node_modules:
|
121
|
+
cache:
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "singleton"
|
5
|
+
|
6
|
+
module Chagall
|
7
|
+
class Settings
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
attr_accessor :options, :missing_options, :missing_compose_files
|
11
|
+
CHAGALL_PROJECTS_FOLDER = "~/projects"
|
12
|
+
TMP_CACHE_FOLDER = "tmp"
|
13
|
+
|
14
|
+
OPTIONS = [
|
15
|
+
{
|
16
|
+
key: :debug,
|
17
|
+
flags: [ "--debug" ],
|
18
|
+
description: "Debug mode with pry attaching",
|
19
|
+
type: :boolean,
|
20
|
+
default: false,
|
21
|
+
environment_variable: "CHAGALL_DEBUG"
|
22
|
+
},
|
23
|
+
{
|
24
|
+
key: :skip_uncommit,
|
25
|
+
flags: [ "--skip-uncommit" ],
|
26
|
+
description: "Skip uncommitted changes check",
|
27
|
+
type: :boolean,
|
28
|
+
default: false,
|
29
|
+
environment_variable: "CHAGALL_SKIP_UNCOMMIT"
|
30
|
+
},
|
31
|
+
{
|
32
|
+
key: :server,
|
33
|
+
flags: [ "-s", "--server" ],
|
34
|
+
description: "Server to deploy to",
|
35
|
+
type: :string,
|
36
|
+
required: true,
|
37
|
+
environment_variable: "CHAGALL_SERVER"
|
38
|
+
},
|
39
|
+
{
|
40
|
+
key: :name,
|
41
|
+
flags: [ "-n", "--name" ],
|
42
|
+
description: "Project name",
|
43
|
+
type: :string,
|
44
|
+
default: Pathname.new(Dir.pwd).basename.to_s,
|
45
|
+
environment_variable: "CHAGALL_NAME"
|
46
|
+
},
|
47
|
+
{
|
48
|
+
key: :release,
|
49
|
+
flags: [ "--release" ],
|
50
|
+
description: "Release tag",
|
51
|
+
required: true,
|
52
|
+
default: `git rev-parse --short HEAD`.strip,
|
53
|
+
type: :string,
|
54
|
+
environment_variable: "CHAGALL_RELEASE"
|
55
|
+
},
|
56
|
+
{
|
57
|
+
key: :dry_run,
|
58
|
+
type: :boolean,
|
59
|
+
default: false,
|
60
|
+
flags: [ "-d", "--dry-run" ],
|
61
|
+
environment_variable: "CHAGALL_DRY_RUN",
|
62
|
+
description: "Dry run"
|
63
|
+
},
|
64
|
+
{
|
65
|
+
key: :remote,
|
66
|
+
flags: [ "-r", "--remote" ],
|
67
|
+
description: "Deploy remotely (build on remote server)",
|
68
|
+
type: :boolean,
|
69
|
+
default: false,
|
70
|
+
environment_variable: "CHAGALL_REMOTE"
|
71
|
+
},
|
72
|
+
{
|
73
|
+
key: :compose_files,
|
74
|
+
flags: [ "-c", "--compose-files" ],
|
75
|
+
description: "Comma separated list of compose files",
|
76
|
+
type: :array,
|
77
|
+
required: true,
|
78
|
+
environment_variable: "CHAGALL_COMPOSE_FILES",
|
79
|
+
proc: ->(value) { value.split(",") }
|
80
|
+
},
|
81
|
+
{
|
82
|
+
key: :target,
|
83
|
+
type: :string,
|
84
|
+
default: "production",
|
85
|
+
flags: [ "--target" ],
|
86
|
+
environment_variable: "CHAGALL_TARGET",
|
87
|
+
description: "Target"
|
88
|
+
},
|
89
|
+
{
|
90
|
+
key: :dockerfile,
|
91
|
+
type: :string,
|
92
|
+
flags: [ "-f", "--file" ],
|
93
|
+
default: "Dockerfile",
|
94
|
+
environment_variable: "CHAGALL_DOCKERFILE",
|
95
|
+
description: "Dockerfile"
|
96
|
+
},
|
97
|
+
{
|
98
|
+
key: :projects_folder,
|
99
|
+
type: :string,
|
100
|
+
default: CHAGALL_PROJECTS_FOLDER,
|
101
|
+
flags: [ "-p", "--projects-folder" ],
|
102
|
+
environment_variable: "CHAGALL_PROJECTS_FOLDER",
|
103
|
+
description: "Projects folder"
|
104
|
+
},
|
105
|
+
{
|
106
|
+
key: :cache_from,
|
107
|
+
type: :string,
|
108
|
+
default: "#{TMP_CACHE_FOLDER}/.buildx-cache",
|
109
|
+
flags: [ "--cache-from" ],
|
110
|
+
environment_variable: "CHAGALL_CACHE_FROM",
|
111
|
+
description: "Cache from"
|
112
|
+
},
|
113
|
+
{
|
114
|
+
key: :cache_to,
|
115
|
+
type: :string,
|
116
|
+
default: "#{TMP_CACHE_FOLDER}/.buildx-cache-new",
|
117
|
+
flags: [ "--cache-to" ],
|
118
|
+
environment_variable: "CHAGALL_CACHE_TO",
|
119
|
+
description: "Cache to"
|
120
|
+
},
|
121
|
+
{
|
122
|
+
key: :keep_releases,
|
123
|
+
type: :integer,
|
124
|
+
default: 3,
|
125
|
+
flags: [ "-k", "--keep-releases" ],
|
126
|
+
environment_variable: "CHAGALL_KEEP_RELEASES",
|
127
|
+
description: "Keep releases",
|
128
|
+
proc: ->(value) { Integer(value) }
|
129
|
+
},
|
130
|
+
{
|
131
|
+
key: :ssh_args,
|
132
|
+
type: :string,
|
133
|
+
default: "-o StrictHostKeyChecking=no",
|
134
|
+
environment_variable: "CHAGALL_SSH_ARGS",
|
135
|
+
flags: [ "--ssh-args" ],
|
136
|
+
description: "SSH arguments"
|
137
|
+
},
|
138
|
+
{
|
139
|
+
key: :docker_context,
|
140
|
+
type: :string,
|
141
|
+
flags: [ "--docker-context" ],
|
142
|
+
environment_variable: "CHAGALL_DOCKER_CONTEXT",
|
143
|
+
default: ".",
|
144
|
+
description: "Docker context"
|
145
|
+
},
|
146
|
+
{
|
147
|
+
key: :platform,
|
148
|
+
type: :string,
|
149
|
+
flags: [ "--platform" ],
|
150
|
+
environment_variable: "CHAGALL_PLATFORM",
|
151
|
+
default: "linux/x86_64",
|
152
|
+
description: "Platform"
|
153
|
+
}
|
154
|
+
].freeze
|
155
|
+
class << self
|
156
|
+
def configure(argv)
|
157
|
+
instance.configure(argv)
|
158
|
+
end
|
159
|
+
|
160
|
+
def [](key)
|
161
|
+
instance.options[key]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def configure(parsed_options)
|
166
|
+
@options = parsed_options
|
167
|
+
@missing_options = []
|
168
|
+
@missing_compose_files = []
|
169
|
+
|
170
|
+
# @options.merge!(options_from_config_file)
|
171
|
+
# @options.merge!(parsed_options)
|
172
|
+
|
173
|
+
validate_options
|
174
|
+
end
|
175
|
+
|
176
|
+
def validate_options
|
177
|
+
error_message_string = "\n"
|
178
|
+
|
179
|
+
OPTIONS.each do |option|
|
180
|
+
@missing_options << option if option[:required] && @options[option[:key]].to_s.empty?
|
181
|
+
end
|
182
|
+
|
183
|
+
if @missing_options.any?
|
184
|
+
error_message_string += " Missing required options: #{@missing_options.map { |o| o[:key] }.join(', ')}\n"
|
185
|
+
error_message_string += " These can be set via:\n"
|
186
|
+
error_message_string += " - CLI arguments (#{@missing_options.map { |o| o[:flags] }.join(', ')})\n"
|
187
|
+
error_message_string += " - Environment variables (#{@missing_options.map do |o|
|
188
|
+
o[:environment_variable] || o[:env_name]
|
189
|
+
end.join(', ')})\n"
|
190
|
+
error_message_string += " - chagall.yml file\n"
|
191
|
+
end
|
192
|
+
|
193
|
+
if @options[:compose_files]
|
194
|
+
@options[:compose_files].each do |file|
|
195
|
+
unless File.exist?(file)
|
196
|
+
@missing_compose_files << file
|
197
|
+
error_message_string += " Missing compose file: #{file}\n"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
return unless @missing_options.any? || @missing_compose_files.any?
|
203
|
+
|
204
|
+
raise Chagall::SettingsError, error_message_string unless @options[:dry_run]
|
205
|
+
end
|
206
|
+
|
207
|
+
def options_from_config_file
|
208
|
+
@options_from_config_file ||= begin
|
209
|
+
config_path = File.join(Dir.pwd, "chagall.yml") || File.join(Dir.pwd, "chagall.yaml")
|
210
|
+
return {} unless File.exist?(config_path)
|
211
|
+
|
212
|
+
config = YAML.load_file(config_path)
|
213
|
+
config.transform_keys(&:to_sym)
|
214
|
+
rescue StandardError => e
|
215
|
+
puts "Warning: Error loading chagall.yml: #{e.message}"
|
216
|
+
{}
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def true?(value)
|
221
|
+
value.to_s.strip.downcase == "true"
|
222
|
+
end
|
223
|
+
|
224
|
+
def image_tag
|
225
|
+
@image_tag ||= "#{options[:name]}:#{options[:release]}"
|
226
|
+
end
|
227
|
+
|
228
|
+
def project_folder_path
|
229
|
+
@project_folder_path ||= "#{options[:projects_folder]}/#{options[:name]}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|