# frozen_string_literal: true require 'open3' # Este modelo implementa los distintos tipos de alojamiento que provee # Sutty. # # 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 # 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 # Siempre tienen que pertenecer a un sitio validates :site, presence: true # El hostname tiene que ser único en toda la plataforma validates :hostname, uniqueness: true # Cada deploy puede implementar su propia validación validates :hostname, hostname: true, unless: :implements_hostname_validation? # Genera el hostname # # @return [String] def default_hostname raise NotImplementedError end # Devolver la URL # # @return [String] def url "https://#{hostname}" end # Ejecutar la tarea # # @return [Boolean] def deploy raise NotImplementedError end # El espacio ocupado por este deploy. # # @return [Integer] def size raise NotImplementedError end # Empezar a contar el tiempo # # @return [Time] def time_start @start = Time.now end # Detener el contador # # @return [Time] def time_stop @stop = Time.now end # Obtener la demora de la tarea # # @return [Float] def time_spent_in_seconds (@stop - @start).round(3) end # El directorio donde se almacenan las gemas. # # TODO: En un momento podíamos tenerlas todas compartidas y ahorrar # espacio, pero bundler empezó a mezclar cosas. # # @return [String] def gems_dir @gems_dir ||= Rails.root.join('_storage', 'gems', site.name) end # Corre un comando, lo registra en la base de datos y devuelve el # estado. # # @param [String] # @return [Boolean] def run(cmd) r = nil lines = [] time_start Dir.chdir(site.path) do Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t| r = t.value # XXX: Tenemos que leer línea por línea porque en salidas largas # se cuelga la IO # TODO: Enviar a un websocket para ver el proceso en vivo? o.each do |line| lines << line end end end time_stop build_stats.create action: readable_cmd(cmd), log: lines.join, seconds: time_spent_in_seconds, bytes: size, status: r&.success? r&.success? end private # Genera el hostname pero permitir la inicialización del valor. Luego # validamos que sea el formato correcto. # # @return [Boolean] def default_hostname! self.hostname ||= default_hostname true end # Elimina los archivos generados por el deploy # # @return [Boolean] def remove_destination! raise NotImplementedError end # Convierte el comando en una versión resumida. # # @param [String] # @return [String] def readable_cmd(cmd) cmd.split(' -', 2).first.tr(' ', '_') end # Cada deploy puede decidir su propia validación # # @return [Boolean] def implements_hostname_validation? false end end