diff --git a/app/models/deploy.rb b/app/models/deploy.rb index e7e0844c..7032f969 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -5,20 +5,27 @@ require 'open3' # Este modelo implementa los distintos tipos de alojamiento que provee # Sutty. # +# Cuando cambia el hostname de un Deploy, generamos un +# DeployAlternativeDomain en su lugar. Esto permite que no se rompan +# links preexistentes y que el nombre no pueda ser tomado por alguien +# más. +# # TODO: Cambiar el nombre a algo que no sea industrial/militar. class Deploy < ApplicationRecord # Un sitio puede tener muchas formas de publicarse. belongs_to :site # Puede tener muchos access logs a través del hostname has_many :access_logs, primary_key: 'hostname', foreign_key: 'host' + # Registro de las tareas ejecutadas + has_many :build_stats, dependent: :destroy # Siempre generar el hostname after_initialize :default_hostname! # Eliminar los archivos generados por el deploy. before_destroy :remove_destination! - - # Registro de las tareas ejecutadas - has_many :build_stats, dependent: :destroy + # Los hostnames alternativos se crean después de actualizar, cuando ya + # se modificó el hostname. + around_update :create_alternative_domain!, if: :destination_changed? # Siempre tienen que pertenecer a un sitio validates :site, presence: true @@ -34,6 +41,37 @@ class Deploy < ApplicationRecord where(hostname: hostname).includes(:site).pluck(:name).first end + # Detecta si el destino existe y si no es un symlink roto. + def exist? + File.exist? destination + end + + # Detecta si el link está roto + def broken? + File.symlink?(destination) && !File.exist?(File.readlink(destination)) + end + + # Ubicación del deploy + # + # @return [String] Una ruta en el sistema de archivos + def destination + File.join(Rails.root, '_deploy', hostname) + end + + # Ubicación anterior del deploy + # + # @return [String] Una ruta en el sistema de archivos + def destination_was + return destination unless will_save_change_to_hostname? + + File.join(Rails.root, '_deploy', hostname_was) + end + + # Determina si la ubicación cambió + def destination_changed? + persisted? && will_save_change_to_hostname? + end + # Genera el hostname # # @return [String] @@ -143,6 +181,20 @@ class Deploy < ApplicationRecord raise NotImplementedError end + # Cuando el deploy cambia de hostname, generamos un dominio + # alternativo para no romper links hacia este sitio. + def create_alternative_domain! + hw = hostname_was + + # Aplicar la actualización + yield + + # Crear el deploy alternativo con el nombre anterior una vez que + # lo cambiamos en la base de datos. + ad = site.deploys.create(type: 'DeployAlternativeDomain', hostname: hw) + ad.deploy if ad.persisted? + end + # Convierte el comando en una versión resumida. # # @param [String] diff --git a/app/models/deploy_hidden_service.rb b/app/models/deploy_hidden_service.rb index 184ca2c2..3f687bd1 100644 --- a/app/models/deploy_hidden_service.rb +++ b/app/models/deploy_hidden_service.rb @@ -37,6 +37,11 @@ class DeployHiddenService < DeployWww private + # No soportamos cambiar de onion + def destination_changed? + false + end + def implements_hostname_validation? true end diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb index be8de366..655e11e9 100644 --- a/app/models/deploy_local.rb +++ b/app/models/deploy_local.rb @@ -3,7 +3,7 @@ # Alojamiento local, genera el sitio como si corriéramos `jekyll build`. class DeployLocal < Deploy # Asegurarse que el hostname es el permitido. - before_save :reset_hostname!, :default_hostname! + before_validation :reset_hostname!, :default_hostname! # Realizamos la construcción del sitio usando Jekyll y un entorno # limpio para no pasarle secretos @@ -30,13 +30,6 @@ class DeployLocal < Deploy end.inject(:+) end - # La ubicación del sitio luego de generarlo. - # - # @return [String] - def destination - File.join(Rails.root, '_deploy', hostname) - end - # El hostname es el nombre del sitio más el dominio principal. # # @return [String] diff --git a/app/models/deploy_www.rb b/app/models/deploy_www.rb index 9ef94df7..3d99a6c2 100644 --- a/app/models/deploy_www.rb +++ b/app/models/deploy_www.rb @@ -20,11 +20,6 @@ class DeployWww < Deploy File.size destination end - # @return [String] - def destination - File.join(Rails.root, '_deploy', hostname) - end - # El hostname por defecto incluye WWW # # @return [String] diff --git a/config/locales/en.yml b/config/locales/en.yml index 27ffd43c..24bd3d3d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -143,6 +143,10 @@ en: tienda_api_key: Store access key errors: models: + deploy: + attributes: + hostname: + destination_exist: 'There already is a file in the destination' site: attributes: name: diff --git a/config/locales/es.yml b/config/locales/es.yml index f98fd7db..851e33e5 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -143,6 +143,10 @@ es: tienda_api_key: Clave de acceso errors: models: + deploy: + attributes: + hostname: + destination_exist: 'Ya hay un archivo en esta ubicación' site: attributes: name: diff --git a/test/models/site/deployment_test.rb b/test/models/site/deployment_test.rb index 8ec61e8a..b8e8464b 100644 --- a/test/models/site/deployment_test.rb +++ b/test/models/site/deployment_test.rb @@ -22,4 +22,46 @@ class Site::DeploymentTest < ActiveSupport::TestCase assert site_pre.deploys.create(type: 'DeployAlternativeDomain', hostname: "#{dup_name}.#{Site.domain}") assert_not site.update(name: dup_name) end + + test 'al cambiar el nombre se crea un deploy alternativo' do + site_name = site.name + new_name = SecureRandom.hex + original_destination = site.deploy_local.destination + urls = [site.url] + + assert site.deploy_local.deploy + assert_not site.deploy_local.destination_changed? + assert site.update(name: new_name) + + urls << site.url + + assert_equal urls.sort, site.urls.sort + assert File.symlink?(original_destination) + assert File.exist?(site.deploy_local.destination) + assert_equal 2, site.deploys.count + end + + test 'al cambiar el nombre se renombra el directorio' do + site_name = site.name + new_name = "test-#{SecureRandom.hex}" + original_destination = site.deploy_local.destination + + assert site.deploy_local.deploy + assert_not site.deploy_local.destination_changed? + assert site.update(name: new_name) + assert site.deploy_local.hostname.start_with?(new_name) + assert File.symlink?(original_destination) + assert File.exist?(site.deploy_local.destination) + end + + test 'al cambiar el nombre varias veces se crean varios links' do + assert site.deploy_local.deploy + + q = rand(3..10) + q.times do + assert site.update(name: "test-#{SecureRandom.hex}") + end + + assert_equal q, site.deploys.count + end end