mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-16 15:31:43 +00:00
Merge branch 'issue-10464' into 'rails'
Issue #10464 Closes #10509, #10506, #10467, #12753, and #12410 See merge request sutty/sutty!121
This commit is contained in:
commit
69c5d8d7bb
27 changed files with 377 additions and 103 deletions
|
@ -22,6 +22,8 @@ RUN apk add npm && npm install -g pnpm@~7 && apk del npm
|
||||||
|
|
||||||
COPY ./monit.conf /etc/monit.d/sutty.conf
|
COPY ./monit.conf /etc/monit.d/sutty.conf
|
||||||
|
|
||||||
|
RUN apk add npm && npm install -g pnpm && apk del npm
|
||||||
|
|
||||||
VOLUME "/srv"
|
VOLUME "/srv"
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
5
Gemfile
5
Gemfile
|
@ -23,6 +23,7 @@ if ENV['RAILS_GROUPS']&.split(',')&.include? 'assets'
|
||||||
end
|
end
|
||||||
|
|
||||||
gem 'nokogiri'
|
gem 'nokogiri'
|
||||||
|
gem 'rgl'
|
||||||
|
|
||||||
# Turbolinks makes navigating your web application faster. Read more:
|
# Turbolinks makes navigating your web application faster. Read more:
|
||||||
# https://github.com/turbolinks/turbolinks
|
# https://github.com/turbolinks/turbolinks
|
||||||
|
@ -38,8 +39,8 @@ gem 'commonmarker'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-i18n'
|
gem 'devise-i18n'
|
||||||
gem 'devise_invitable'
|
gem 'devise_invitable'
|
||||||
gem 'distributed-press-api-client', '~> 0.2.2'
|
gem 'distributed-press-api-client', '~> 0.2.3'
|
||||||
gem 'njalla-api-client'
|
gem 'njalla-api-client', '~> 0.2.0'
|
||||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||||
gem 'exception_notification'
|
gem 'exception_notification'
|
||||||
gem 'fast_blank'
|
gem 'fast_blank'
|
||||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -387,10 +387,11 @@ GEM
|
||||||
nokogiri (1.12.5-x86_64-linux-musl)
|
nokogiri (1.12.5-x86_64-linux-musl)
|
||||||
mini_portile2 (~> 2.6.1)
|
mini_portile2 (~> 2.6.1)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
njalla-api-client (0.1.0)
|
njalla-api-client (0.2.0)
|
||||||
dry-schema
|
dry-schema
|
||||||
httparty (~> 0.18)
|
httparty (~> 0.18)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
pairing_heap (3.0.0)
|
||||||
parallel (1.21.0)
|
parallel (1.21.0)
|
||||||
parser (3.0.2.0)
|
parser (3.0.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
|
@ -481,6 +482,10 @@ GEM
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
|
rgl (0.6.2)
|
||||||
|
pairing_heap (>= 0.3.0)
|
||||||
|
rexml (~> 3.2, >= 3.2.4)
|
||||||
|
stream (~> 0.5.3)
|
||||||
rouge (3.26.1)
|
rouge (3.26.1)
|
||||||
rubocop (1.23.0)
|
rubocop (1.23.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
@ -548,6 +553,7 @@ GEM
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sqlite3 (1.4.2-x86_64-linux-musl)
|
sqlite3 (1.4.2-x86_64-linux-musl)
|
||||||
stackprof (0.2.17-x86_64-linux-musl)
|
stackprof (0.2.17-x86_64-linux-musl)
|
||||||
|
stream (0.5.5)
|
||||||
sucker_punch (3.0.1)
|
sucker_punch (3.0.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
sutty-archives (2.5.4)
|
sutty-archives (2.5.4)
|
||||||
|
@ -616,7 +622,7 @@ DEPENDENCIES
|
||||||
devise
|
devise
|
||||||
devise-i18n
|
devise-i18n
|
||||||
devise_invitable
|
devise_invitable
|
||||||
distributed-press-api-client (~> 0.2.2)
|
distributed-press-api-client (~> 0.2.3)
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
down
|
down
|
||||||
ed25519
|
ed25519
|
||||||
|
@ -666,6 +672,7 @@ DEPENDENCIES
|
||||||
rails_warden
|
rails_warden
|
||||||
redis
|
redis
|
||||||
redis-rails
|
redis-rails
|
||||||
|
rgl
|
||||||
rollups!
|
rollups!
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubyzip
|
rubyzip
|
||||||
|
|
1
Procfile
1
Procfile
|
@ -8,3 +8,4 @@ prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
|
||||||
distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
|
distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
|
||||||
cleanup: bundle exec rake cleanup:everything
|
cleanup: bundle exec rake cleanup:everything
|
||||||
stats: bundle exec rake stats:process_all
|
stats: bundle exec rake stats:process_all
|
||||||
|
distributed_press_renew_tokens: bundle exec rake distributed_press:tokens:renew
|
||||||
|
|
|
@ -28,8 +28,6 @@ class SitesController < ApplicationController
|
||||||
|
|
||||||
@site = Site.new
|
@site = Site.new
|
||||||
authorize @site
|
authorize @site
|
||||||
|
|
||||||
@site.deploys.build type: 'DeployLocal'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
|
@ -30,79 +30,90 @@ class DeployJob < ApplicationJob
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@deployed = {}
|
||||||
@site.update status: 'building'
|
@site.update status: 'building'
|
||||||
# Asegurarse que DeployLocal sea el primero!
|
@site.deployment_list.each do |d|
|
||||||
@deployed = {
|
begin
|
||||||
deploy_local: {
|
raise DeployException, 'Una dependencia falló' if failed_dependencies? d
|
||||||
status: deploy_locally,
|
|
||||||
seconds: deploy_local.build_stats.last.seconds,
|
|
||||||
size: deploy_local.size,
|
|
||||||
urls: [deploy_local.url]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# No es opcional
|
status = d.deploy(output: @output)
|
||||||
unless @deployed[:deploy_local][:status]
|
seconds = d.build_stats.last.try(:seconds) || 0
|
||||||
# Hacer fallar la tarea
|
size = d.size
|
||||||
raise DeployException, "#{@site.name}: Falló la compilación"
|
urls = d.respond_to?(:urls) ? d.urls : [d.url].compact
|
||||||
|
rescue StandardError => e
|
||||||
|
status = false
|
||||||
|
seconds ||= 0
|
||||||
|
size ||= 0
|
||||||
|
# XXX: Hace que se vea la tabla
|
||||||
|
urls ||= [nil]
|
||||||
|
|
||||||
|
notify_exception e, d
|
||||||
end
|
end
|
||||||
|
|
||||||
deploy_others
|
@deployed[d.type.underscore.to_sym] = {
|
||||||
|
status: status,
|
||||||
|
seconds: seconds,
|
||||||
|
size: size,
|
||||||
|
urls: urls
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless @output
|
||||||
|
|
||||||
|
puts (Terminal::Table.new do |t|
|
||||||
|
t << (%w[type] + @deployed.values.first.keys)
|
||||||
|
t.add_separator
|
||||||
|
@deployed.each do |type, row|
|
||||||
|
t << ([type.to_s] + row.values)
|
||||||
|
end
|
||||||
|
end)
|
||||||
rescue DeployTimedOutException => e
|
rescue DeployTimedOutException => e
|
||||||
notify_exception e
|
notify_exception e
|
||||||
rescue DeployException => e
|
|
||||||
notify_exception e, deploy_local
|
|
||||||
ensure
|
ensure
|
||||||
@site&.update status: 'waiting'
|
if @site.present?
|
||||||
|
@site.update status: 'waiting'
|
||||||
|
|
||||||
notify_usuaries if notify
|
notify_usuaries if notify
|
||||||
|
|
||||||
|
puts "\a" if @output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Detecta si un método de publicación tiene dependencias fallidas
|
||||||
|
#
|
||||||
|
# @param :deploy [Deploy]
|
||||||
|
# @return [Boolean]
|
||||||
|
def failed_dependencies?(deploy)
|
||||||
|
failed_dependencies(deploy).present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene las dependencias fallidas de un deploy
|
||||||
|
#
|
||||||
|
# @param :deploy [Deploy]
|
||||||
|
# @return [Array]
|
||||||
|
def failed_dependencies(deploy)
|
||||||
|
deploy.class::DEPENDENCIES & (@deployed.reject do |_, v|
|
||||||
|
v[:status]
|
||||||
|
end.keys)
|
||||||
|
end
|
||||||
|
|
||||||
# @param :exception [StandardError]
|
# @param :exception [StandardError]
|
||||||
# @param :deploy [Deploy]
|
# @param :deploy [Deploy]
|
||||||
def notify_exception(exception, deploy = nil)
|
def notify_exception(exception, deploy = nil)
|
||||||
data = {
|
data = {
|
||||||
site: @site.id,
|
site: @site.id,
|
||||||
deploy: deploy&.type,
|
deploy: deploy&.type,
|
||||||
log: deploy&.build_stats&.last&.log
|
log: deploy&.build_stats&.last&.log,
|
||||||
|
failed_dependencies: (failed_dependencies(deploy) if deploy)
|
||||||
}
|
}
|
||||||
|
|
||||||
ExceptionNotifier.notify_exception(exception, data: data)
|
ExceptionNotifier.notify_exception(exception, data: data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def deploy_local
|
|
||||||
@deploy_local ||= @site.deploys.find_by(type: 'DeployLocal')
|
|
||||||
end
|
|
||||||
|
|
||||||
def deploy_locally
|
|
||||||
deploy_local.deploy(output: @output)
|
|
||||||
end
|
|
||||||
|
|
||||||
def deploy_others
|
|
||||||
@site.deploys.where.not(type: 'DeployLocal').find_each do |d|
|
|
||||||
begin
|
|
||||||
status = d.deploy(output: @output)
|
|
||||||
seconds = d.build_stats.last.try(:seconds)
|
|
||||||
rescue StandardError => e
|
|
||||||
status = false
|
|
||||||
seconds = 0
|
|
||||||
|
|
||||||
notify_exception e, d
|
|
||||||
end
|
|
||||||
|
|
||||||
@deployed[d.type.underscore.to_sym] = {
|
|
||||||
status: status,
|
|
||||||
seconds: seconds || 0,
|
|
||||||
size: d.size,
|
|
||||||
urls: d.respond_to?(:urls) ? d.urls : [d.url].compact
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def notify_usuaries
|
def notify_usuaries
|
||||||
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
||||||
DeployMailer.with(usuarie: usuarie, site: @site.id)
|
DeployMailer.with(usuarie: usuarie, site: @site.id)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'open3'
|
require 'open3'
|
||||||
|
|
||||||
# Este modelo implementa los distintos tipos de alojamiento que provee
|
# Este modelo implementa los distintos tipos de alojamiento que provee
|
||||||
# Sutty.
|
# Sutty.
|
||||||
#
|
#
|
||||||
|
@ -11,6 +12,9 @@ class Deploy < ApplicationRecord
|
||||||
belongs_to :site
|
belongs_to :site
|
||||||
has_many :build_stats, dependent: :destroy
|
has_many :build_stats, dependent: :destroy
|
||||||
|
|
||||||
|
DEPENDENCIES = []
|
||||||
|
SOFT_DEPENDENCIES = []
|
||||||
|
|
||||||
def deploy(**)
|
def deploy(**)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
@ -96,6 +100,13 @@ class Deploy < ApplicationRecord
|
||||||
@local_env ||= {}
|
@local_env ||= {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Trae todas las dependencias
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def self.all_dependencies
|
||||||
|
self::DEPENDENCIES | self::SOFT_DEPENDENCIES
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# @param [String]
|
# @param [String]
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
class DeployAlternativeDomain < Deploy
|
class DeployAlternativeDomain < Deploy
|
||||||
store :values, accessors: %i[hostname], coder: JSON
|
store :values, accessors: %i[hostname], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# Generar un link simbólico del sitio principal al alternativo
|
# Generar un link simbólico del sitio principal al alternativo
|
||||||
def deploy(**)
|
def deploy(**)
|
||||||
File.symlink?(destination) ||
|
File.symlink?(destination) ||
|
||||||
|
@ -18,7 +20,11 @@ class DeployAlternativeDomain < Deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
def destination
|
def destination
|
||||||
@destination ||= File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, ''))
|
@destination ||= File.join(Rails.root, '_deploy', fqdn)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fqdn
|
||||||
|
hostname.gsub(/\.\z/, '')
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
|
|
|
@ -16,6 +16,9 @@ class DeployDistributedPress < Deploy
|
||||||
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
|
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
|
||||||
|
|
||||||
before_create :create_remote_site!, :create_njalla_records!
|
before_create :create_remote_site!, :create_njalla_records!
|
||||||
|
before_destroy :delete_remote_site!, :delete_njalla_records!
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# Actualiza la información y luego envía los cambios
|
# Actualiza la información y luego envía los cambios
|
||||||
#
|
#
|
||||||
|
@ -28,11 +31,15 @@ class DeployDistributedPress < Deploy
|
||||||
time_start
|
time_start
|
||||||
|
|
||||||
create_remote_site! if remote_site_id.blank?
|
create_remote_site! if remote_site_id.blank?
|
||||||
create_njalla_records! if remote_info['njalla'].blank?
|
create_njalla_records!
|
||||||
save
|
save
|
||||||
|
|
||||||
if remote_site_id.blank? || remote_info['njalla'].blank?
|
if remote_site_id.blank?
|
||||||
raise DeployJob::DeployException, ''
|
raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press'
|
||||||
|
end
|
||||||
|
|
||||||
|
if create_njalla_records? && remote_info[:njalla].blank?
|
||||||
|
raise DeployJob::DeployException, 'No se pudieron crear los registros necesarios en Njalla'
|
||||||
end
|
end
|
||||||
|
|
||||||
site_client.tap do |c|
|
site_client.tap do |c|
|
||||||
|
@ -45,10 +52,13 @@ class DeployDistributedPress < Deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
update remote_info: c.show(publishing_site).to_h
|
|
||||||
|
|
||||||
status = c.publish(publishing_site, deploy_local.destination)
|
status = c.publish(publishing_site, deploy_local.destination)
|
||||||
|
|
||||||
|
if status
|
||||||
|
self.remote_info[:distributed_press] = c.show(publishing_site).to_h
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
publisher.logger.close
|
publisher.logger.close
|
||||||
stdout.join
|
stdout.join
|
||||||
end
|
end
|
||||||
|
@ -70,14 +80,26 @@ class DeployDistributedPress < Deploy
|
||||||
|
|
||||||
# Devuelve las URLs de todos los protocolos
|
# Devuelve las URLs de todos los protocolos
|
||||||
def urls
|
def urls
|
||||||
remote_info[:links].values.map do |protocol|
|
protocol_urls + gateway_urls
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def gateway_urls
|
||||||
|
remote_info.dig(:distributed_press, :links).values.map do |protocol|
|
||||||
[ protocol[:link], protocol[:gateway] ]
|
[ protocol[:link], protocol[:gateway] ]
|
||||||
end.flatten.compact.select do |link|
|
end.flatten.compact.select do |link|
|
||||||
link.include? '://'
|
link.include? '://'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def protocol_urls
|
||||||
|
remote_info.dig(:distributed_press, :protocols).select do |_, enabled|
|
||||||
|
enabled
|
||||||
|
end.map do |protocol, _|
|
||||||
|
"#{protocol}://#{site.hostname}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# El cliente de la API
|
# El cliente de la API
|
||||||
#
|
#
|
||||||
|
@ -86,7 +108,7 @@ class DeployDistributedPress < Deploy
|
||||||
#
|
#
|
||||||
# @return [DistributedPressPublisher]
|
# @return [DistributedPressPublisher]
|
||||||
def publisher
|
def publisher
|
||||||
@publisher ||= DistributedPressPublisher.first
|
@publisher ||= DistributedPressPublisher.last
|
||||||
end
|
end
|
||||||
|
|
||||||
# El cliente para actualizar el sitio
|
# El cliente para actualizar el sitio
|
||||||
|
@ -117,27 +139,33 @@ class DeployDistributedPress < Deploy
|
||||||
created_site = site_client.create(create_site)
|
created_site = site_client.create(create_site)
|
||||||
|
|
||||||
self.remote_site_id = created_site[:id]
|
self.remote_site_id = created_site[:id]
|
||||||
self.remote_info = created_site.to_h
|
self.remote_info ||= {}
|
||||||
|
self.remote_info[:distributed_press] = created_site.to_h
|
||||||
|
nil
|
||||||
rescue DistributedPress::V1::Error => e
|
rescue DistributedPress::V1::Error => e
|
||||||
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
ensure
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# Crea los registros en Njalla
|
# Crea los registros en Njalla
|
||||||
#
|
#
|
||||||
# @return [nil]
|
|
||||||
def create_njalla_records!
|
|
||||||
# XXX: Esto depende de nuestro DNS actual, cuando lo migremos hay
|
# XXX: Esto depende de nuestro DNS actual, cuando lo migremos hay
|
||||||
# que eliminarlo.
|
# que eliminarlo.
|
||||||
unless site.name.end_with? '.'
|
#
|
||||||
self.remote_info['njalla'] = {}
|
# @return [nil]
|
||||||
self.remote_info['njalla']['a'] = njalla.add_record(name: site.name, type: 'CNAME', content: "#{Site.domain}.").to_h
|
def create_njalla_records!
|
||||||
self.remote_info['njalla']['ns'] = njalla.add_record(name: "_dnslink.#{site.name}", type: 'NS', content: "#{publisher.hostname}.").to_h
|
return unless create_njalla_records?
|
||||||
end
|
|
||||||
|
self.remote_info ||= {}
|
||||||
|
self.remote_info[:njalla] ||= {}
|
||||||
|
self.remote_info[:njalla][:a] ||= njalla.add_record(name: site.name, type: 'CNAME', content: "#{Site.domain}.").to_h
|
||||||
|
self.remote_info[:njalla][:cname] ||= njalla.add_record(name: "www.#{site.name}", type: 'CNAME', content: "#{Site.domain}.").to_h
|
||||||
|
self.remote_info[:njalla][:ns] ||= njalla.add_record(name: "_dnslink.#{site.name}", type: 'NS', content: "#{publisher.hostname}.").to_h
|
||||||
|
|
||||||
|
nil
|
||||||
rescue HTTParty::Error => e
|
rescue HTTParty::Error => e
|
||||||
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
self.remote_info['njalla'] = nil
|
self.remote_info.delete :njalla
|
||||||
ensure
|
ensure
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
@ -152,15 +180,38 @@ class DeployDistributedPress < Deploy
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_remote_site!
|
||||||
|
site_client.delete(publishing_site)
|
||||||
|
nil
|
||||||
|
rescue DistributedPress::V1::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_njalla_records!
|
||||||
|
return unless create_njalla_records?
|
||||||
|
|
||||||
|
%w[a ns cname].each do |type|
|
||||||
|
next if (id = remote_info.dig('njalla', type, 'id')).blank?
|
||||||
|
|
||||||
|
njalla.remove_record(id: id.to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Actualizar registros en Njalla
|
# Actualizar registros en Njalla
|
||||||
#
|
#
|
||||||
# @return [Njalla::V1::Domain]
|
# @return [Njalla::V1::Domain]
|
||||||
def njalla
|
def njalla
|
||||||
@njalla ||=
|
@njalla ||=
|
||||||
begin
|
begin
|
||||||
client = Njalla::V1::Client.new(token: ENV['NJALLA_TOKEN'])
|
client = Njalla::V1::Client.new(token: Rails.application.credentials.njalla)
|
||||||
|
|
||||||
Njalla::V1::Domain.new(domain: Site.domain, client: client)
|
Njalla::V1::Domain.new(domain: Site.domain, client: client)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Detecta si tenemos que crear registros en Njalla
|
||||||
|
def create_njalla_records?
|
||||||
|
!site.name.end_with?('.')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
34
app/models/deploy_full_rsync.rb
Normal file
34
app/models/deploy_full_rsync.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DeployFullRsync < DeployRsync
|
||||||
|
SOFT_DEPENDENCIES = %i[
|
||||||
|
deploy_alternative_domain
|
||||||
|
deploy_localized_domain
|
||||||
|
deploy_hidden_service
|
||||||
|
deploy_www
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sincroniza las ubicaciones alternativas también, ignorando las que
|
||||||
|
# todavía no se generaron. Solo falla si ningún sitio fue
|
||||||
|
# sincronizado o si alguna sincronización falló.
|
||||||
|
#
|
||||||
|
# @param :output [Boolean]
|
||||||
|
# @return [Boolean]
|
||||||
|
def rsync(output: false)
|
||||||
|
result =
|
||||||
|
self.class.all_dependencies.map(&:to_s).map(&:classify).map do |dependency|
|
||||||
|
site.deploys.where(type: dependency).find_each.map do |deploy|
|
||||||
|
next unless File.exist? deploy.destination
|
||||||
|
|
||||||
|
run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape deploy.destination} #{Shellwords.escape destination}), output: output
|
||||||
|
rescue StandardError
|
||||||
|
end
|
||||||
|
end.flatten.compact
|
||||||
|
|
||||||
|
result.present? && result.all?
|
||||||
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
|
"https://#{user_host.last}/"
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,14 +2,10 @@
|
||||||
|
|
||||||
# Genera una versión onion
|
# Genera una versión onion
|
||||||
class DeployHiddenService < DeployWww
|
class DeployHiddenService < DeployWww
|
||||||
def deploy(**)
|
|
||||||
return true if fqdn.blank?
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def fqdn
|
def fqdn
|
||||||
values[:onion]
|
values[:onion].tap do |onion|
|
||||||
|
raise ArgumentError, 'Aun no se generó la dirección .onion' if onion.blank?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
|
|
|
@ -84,7 +84,8 @@ class DeployLocal < Deploy
|
||||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||||
'JEKYLL_ENV' => Rails.env,
|
'JEKYLL_ENV' => Rails.env,
|
||||||
'LANG' => ENV['LANG'],
|
'LANG' => ENV['LANG'],
|
||||||
'YARN_CACHE_FOLDER' => yarn_cache_dir
|
'YARN_CACHE_FOLDER' => yarn_cache_dir,
|
||||||
|
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -116,6 +117,14 @@ class DeployLocal < Deploy
|
||||||
run %(gem install bundler --no-document), output: output
|
run %(gem install bundler --no-document), output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pnpm_lock
|
||||||
|
File.join(site.path, 'pnpm-lock.yaml')
|
||||||
|
end
|
||||||
|
|
||||||
|
def pnpm_lock?
|
||||||
|
File.exist? pnpm_lock
|
||||||
|
end
|
||||||
|
|
||||||
# Corre yarn dentro del repositorio
|
# Corre yarn dentro del repositorio
|
||||||
def yarn(output: false)
|
def yarn(output: false)
|
||||||
return true unless yarn_lock?
|
return true unless yarn_lock?
|
||||||
|
@ -152,6 +161,7 @@ class DeployLocal < Deploy
|
||||||
# Consigue todas las variables de entorno configuradas por otros
|
# Consigue todas las variables de entorno configuradas por otros
|
||||||
# deploys.
|
# deploys.
|
||||||
#
|
#
|
||||||
|
# @deprecated Solo tenía sentido para Distributed Press v0
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def extra_env
|
def extra_env
|
||||||
@extra_env ||=
|
@extra_env ||=
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
# XXX: La plantilla tiene que soportar esto con el plugin
|
# XXX: La plantilla tiene que soportar esto con el plugin
|
||||||
# jekyll-private-data
|
# jekyll-private-data
|
||||||
class DeployPrivate < DeployLocal
|
class DeployPrivate < DeployLocal
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# No es necesario volver a instalar dependencias
|
# No es necesario volver a instalar dependencias
|
||||||
def deploy(output: false)
|
def deploy(output: false)
|
||||||
jekyll_build(output: output)
|
jekyll_build(output: output)
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
# Sincroniza sitios a servidores remotos usando Rsync. El servidor
|
# Sincroniza sitios a servidores remotos usando Rsync. El servidor
|
||||||
# remoto tiene que tener rsync instalado.
|
# remoto tiene que tener rsync instalado.
|
||||||
class DeployRsync < Deploy
|
class DeployRsync < Deploy
|
||||||
store :values, accessors: %i[destination host_keys], coder: JSON
|
store :values, accessors: %i[hostname destination host_keys], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local deploy_zip]
|
||||||
|
|
||||||
def deploy(output: false)
|
def deploy(output: false)
|
||||||
ssh? && rsync(output: output)
|
ssh? && rsync(output: output)
|
||||||
|
@ -23,6 +25,11 @@ class DeployRsync < Deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def url
|
||||||
|
"https://#{hostname}/"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Verificar la conexión SSH implementando Trust On First Use
|
# Verificar la conexión SSH implementando Trust On First Use
|
||||||
|
@ -83,8 +90,8 @@ class DeployRsync < Deploy
|
||||||
# Sincroniza hacia el directorio remoto
|
# Sincroniza hacia el directorio remoto
|
||||||
#
|
#
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def rsync(output: output)
|
def rsync(output: false)
|
||||||
run %(rsync -aviH --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output
|
run %(rsync -aviH --delete-after --timeout=5 #{Shellwords.escape source}/ #{Shellwords.escape destination}/), output: output
|
||||||
end
|
end
|
||||||
|
|
||||||
# El origen es el destino de la compilación
|
# El origen es el destino de la compilación
|
||||||
|
|
|
@ -4,9 +4,13 @@
|
||||||
class DeployWww < Deploy
|
class DeployWww < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
store :values, accessors: %i[], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
before_destroy :remove_destination!
|
before_destroy :remove_destination!
|
||||||
|
|
||||||
def deploy(**)
|
def deploy(output: false)
|
||||||
|
puts "Creando symlink #{site.hostname} => #{destination}" if output
|
||||||
|
|
||||||
File.symlink?(destination) ||
|
File.symlink?(destination) ||
|
||||||
File.symlink(site.hostname, destination).zero?
|
File.symlink(site.hostname, destination).zero?
|
||||||
end
|
end
|
||||||
|
@ -28,7 +32,7 @@ class DeployWww < Deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
"https://www.#{site.hostname}/"
|
"https://#{fqdn}/"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -8,28 +8,49 @@ require 'zip'
|
||||||
class DeployZip < Deploy
|
class DeployZip < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
store :values, accessors: %i[], coder: JSON
|
||||||
|
|
||||||
|
DEPENDENCIES = %i[deploy_local]
|
||||||
|
|
||||||
# Una vez que el sitio está generado, tomar todos los archivos y
|
# Una vez que el sitio está generado, tomar todos los archivos y
|
||||||
# y generar un zip accesible públicamente.
|
# y generar un zip accesible públicamente.
|
||||||
#
|
#
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def deploy(**)
|
def deploy(output: false)
|
||||||
FileUtils.rm_f path
|
FileUtils.rm_f path
|
||||||
|
|
||||||
time_start
|
time_start
|
||||||
Dir.chdir(destination) do
|
Zip::File.open(path, Zip::File::CREATE) do |zip|
|
||||||
Zip::File.open(path, Zip::File::CREATE) do |z|
|
Dir.glob(File.join(destination, '**', '**')).each do |file|
|
||||||
Dir.glob('./**/**').each do |f|
|
entry = Pathname.new(file).relative_path_from(destination).to_s
|
||||||
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
|
|
||||||
|
if File.directory? file
|
||||||
|
log "Creando directorio #{entry}", output
|
||||||
|
|
||||||
|
zip.mkdir(entry)
|
||||||
|
else
|
||||||
|
log "Comprimiendo #{entry}", output
|
||||||
|
zip.add(entry, file)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
time_stop
|
time_stop
|
||||||
|
|
||||||
|
File.exist?(path).tap do |status|
|
||||||
build_stats.create action: 'zip',
|
build_stats.create action: 'zip',
|
||||||
seconds: time_spent_in_seconds,
|
seconds: time_spent_in_seconds,
|
||||||
bytes: size
|
bytes: size,
|
||||||
|
log: @log.join("\n"),
|
||||||
|
status: status
|
||||||
|
end
|
||||||
|
rescue Zip::Error => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
|
||||||
File.exist? path
|
build_stats.create action: 'zip',
|
||||||
|
seconds: 0,
|
||||||
|
bytes: 0,
|
||||||
|
log: @log.join("\n"),
|
||||||
|
status: false
|
||||||
|
|
||||||
|
false
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
|
@ -41,8 +62,11 @@ class DeployZip < Deploy
|
||||||
File.size path
|
File.size path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
def destination
|
def destination
|
||||||
File.join(Rails.root, '_deploy', site.hostname)
|
Rails.root.join('_deploy', site.hostname).realpath.to_s
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
Rails.root.join('_deploy', site.hostname).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def file
|
def file
|
||||||
|
@ -56,4 +80,15 @@ class DeployZip < Deploy
|
||||||
def path
|
def path
|
||||||
File.join(destination, file)
|
File.join(destination, file)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @param :line [String]
|
||||||
|
# @param :output [Boolean]
|
||||||
|
def log(line, output)
|
||||||
|
@log ||= []
|
||||||
|
@log << line
|
||||||
|
|
||||||
|
puts line if output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ class Site < ApplicationRecord
|
||||||
include Site::Forms
|
include Site::Forms
|
||||||
include Site::FindAndReplace
|
include Site::FindAndReplace
|
||||||
include Site::Api
|
include Site::Api
|
||||||
|
include Site::DeployDependencies
|
||||||
include Site::BuildStats
|
include Site::BuildStats
|
||||||
include Tienda
|
include Tienda
|
||||||
|
|
||||||
|
|
38
app/models/site/deploy_dependencies.rb
Normal file
38
app/models/site/deploy_dependencies.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rgl/adjacency'
|
||||||
|
require 'rgl/topsort'
|
||||||
|
|
||||||
|
class Site
|
||||||
|
module DeployDependencies
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Genera un grafo dirigido de todos los métodos de publicación
|
||||||
|
#
|
||||||
|
# @return [RGL::DirectedAdjacencyGraph]
|
||||||
|
def deployment_graph
|
||||||
|
@deployment_graph ||= RGL::DirectedAdjacencyGraph.new.tap do |graph|
|
||||||
|
deploys.each do |deploy|
|
||||||
|
graph.add_vertex deploy
|
||||||
|
end
|
||||||
|
|
||||||
|
deploys.each do |deploy|
|
||||||
|
deploy.class.all_dependencies.each do |dependency|
|
||||||
|
deploys.where(type: dependency.to_s.classify).each do |deploy_dependency|
|
||||||
|
graph.add_edge deploy_dependency, deploy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve una lista ordenada de todos los métodos de publicación
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def deployment_list
|
||||||
|
@deployment_list ||= deployment_graph.topsort_iterator.to_a
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,6 +14,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
self.site = Site.new params
|
self.site = Site.new params
|
||||||
|
|
||||||
add_role temporal: false, rol: 'usuarie'
|
add_role temporal: false, rol: 'usuarie'
|
||||||
|
site.deploys.build type: 'DeployLocal'
|
||||||
sync_nodes
|
sync_nodes
|
||||||
|
|
||||||
I18n.with_locale(usuarie.lang.to_sym || I18n.default_locale) do
|
I18n.with_locale(usuarie.lang.to_sym || I18n.default_locale) do
|
||||||
|
@ -217,7 +218,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
# Crea los deploys necesarios para sincronizar a otros nodos de Sutty
|
# Crea los deploys necesarios para sincronizar a otros nodos de Sutty
|
||||||
def sync_nodes
|
def sync_nodes
|
||||||
Rails.application.nodes.each do |node|
|
Rails.application.nodes.each do |node|
|
||||||
site.deploys.build(type: 'DeployRsync', destination: "sutty@#{node}:#{site.hostname}")
|
site.deploys.build(type: 'DeployFullRsync', destination: "sutty@#{node}:")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
1
app/views/deploys/_deploy_full_rsync.haml
Normal file
1
app/views/deploys/_deploy_full_rsync.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
-# nada
|
|
@ -17,7 +17,8 @@
|
||||||
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
|
|
||||||
- if deploy.object.fqdn
|
- begin
|
||||||
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
|
- rescue ArgumentError
|
||||||
%hr/
|
%hr/
|
||||||
|
|
1
app/views/deploys/_deploy_localized_domain.haml
Normal file
1
app/views/deploys/_deploy_localized_domain.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
-# nada
|
|
@ -160,9 +160,6 @@
|
||||||
= f.fields_for :deploys do |deploy|
|
= f.fields_for :deploys do |deploy|
|
||||||
= render "deploys/#{deploy.object.type.underscore}",
|
= render "deploys/#{deploy.object.type.underscore}",
|
||||||
deploy: deploy, site: site
|
deploy: deploy, site: site
|
||||||
- else
|
|
||||||
= f.fields_for :deploys do |deploy|
|
|
||||||
= deploy.hidden_field :type
|
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.submit submit, class: 'btn btn-lg btn-block'
|
= f.submit submit, class: 'btn btn-lg btn-block'
|
||||||
|
|
|
@ -133,6 +133,14 @@ en:
|
||||||
title: Synchronize to backup server
|
title: Synchronize to backup server
|
||||||
success: Success!
|
success: Success!
|
||||||
error: Error
|
error: Error
|
||||||
|
deploy_full_rsync:
|
||||||
|
title: Synchronize to another Sutty node
|
||||||
|
success: Success!
|
||||||
|
error: Error
|
||||||
|
deploy_distributed_press:
|
||||||
|
title: Distributed Web
|
||||||
|
success: Success!
|
||||||
|
error: Error
|
||||||
help: You can contact us by replying to this e-mail
|
help: You can contact us by replying to this e-mail
|
||||||
maintenance_mailer:
|
maintenance_mailer:
|
||||||
notice:
|
notice:
|
||||||
|
|
|
@ -133,6 +133,14 @@ es:
|
||||||
title: Sincronizar al servidor alternativo
|
title: Sincronizar al servidor alternativo
|
||||||
success: ¡Éxito!
|
success: ¡Éxito!
|
||||||
error: Hubo un error
|
error: Hubo un error
|
||||||
|
deploy_full_rsync:
|
||||||
|
title: Sincronizar a otro nodo de Sutty
|
||||||
|
success: ¡Éxito!
|
||||||
|
error: Hubo un error
|
||||||
|
deploy_distributed_press:
|
||||||
|
title: Web distribuida
|
||||||
|
success: ¡Éxito!
|
||||||
|
error: Hubo un error
|
||||||
help: Por cualquier duda, responde este correo para contactarte con nosotres.
|
help: Por cualquier duda, responde este correo para contactarte con nosotres.
|
||||||
maintenance_mailer:
|
maintenance_mailer:
|
||||||
notice:
|
notice:
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Cambia todos los DeployRsync propios de Sutty a DeployFullRsync que se
|
||||||
|
# encarga de sincronizar todo.
|
||||||
|
class RenameDeployRsyncToDeployFullRsync < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
DeployRsync.all.find_each do |deploy|
|
||||||
|
dest = deploy.destination.split(':', 2).first
|
||||||
|
|
||||||
|
next unless nodes.include? dest
|
||||||
|
|
||||||
|
deploy.destination = "#{dest}:"
|
||||||
|
deploy.type = 'DeployFullRsync'
|
||||||
|
|
||||||
|
deploy.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
DeployFullRsync.all.find_each do |deploy|
|
||||||
|
next unless nodes.include? deploy.destination.split(':', 2).first
|
||||||
|
|
||||||
|
deploy.destination = "#{deploy.destination}#{deploy.site.hostname}"
|
||||||
|
deploy.type = 'DeployRsync'
|
||||||
|
|
||||||
|
deploy.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def nodes
|
||||||
|
@nodes ||= Rails.application.nodes.map do |node|
|
||||||
|
"sutty@#{node}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,3 +18,8 @@ check program stats
|
||||||
with path "/usr/bin/foreman run -f /srv/Procfile -d /srv stats" as uid "rails" gid "www-data"
|
with path "/usr/bin/foreman run -f /srv/Procfile -d /srv stats" as uid "rails" gid "www-data"
|
||||||
every "0 1 * * *"
|
every "0 1 * * *"
|
||||||
if status != 0 then alert
|
if status != 0 then alert
|
||||||
|
|
||||||
|
check program distributed_press_tokens_renew
|
||||||
|
with path "/usr/bin/foreman run -f /srv/Procfile -d /srv distributed_press_tokens_renew" as uid "rails" gid "www-data"
|
||||||
|
every "0 3 * * *"
|
||||||
|
if status != 0 then alert
|
||||||
|
|
Loading…
Reference in a new issue