deployear los sitios de distintas formas

This commit is contained in:
f 2019-07-24 20:51:29 -03:00
parent 5ace66f20a
commit 11cf2cb08b
No known key found for this signature in database
GPG key ID: 2AE5A13E321F953D
13 changed files with 282 additions and 1 deletions

View file

@ -54,6 +54,7 @@ gem 'mobility'
gem 'pundit'
gem 'rails-i18n'
gem 'rails_warden'
gem 'rubyzip'
gem 'rugged'
gem 'validates_hostname'
gem 'whenever', require: false

View file

@ -423,6 +423,7 @@ DEPENDENCIES
rails_warden
rbnacl (< 5.0)
rubocop-rails
rubyzip
rugged
sass-rails (~> 5.0)
selenium-webdriver

33
app/models/deploy.rb Normal file
View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'open3'
# Este modelo implementa los distintos tipos de alojamiento que provee
# Sutty.
#
# Los datos se guardan en la tabla `deploys`. Para guardar los
# atributos, cada modelo tiene que definir su propio `store
# :attributes`.
class Deploy < ApplicationRecord
belongs_to :site
def deploy
raise NotImplementedError
end
def limit
raise NotImplementedError
end
# Corre un comando y devuelve true si terminó correctamente
def run(cmd)
r = 1
Dir.chdir(site.path) do
Open3.popen2(env, cmd, unsetenv_others: true) do |_, _o, t|
r = t.value
end
end
r.exited?
end
end

View file

@ -0,0 +1,77 @@
# frozen_string_literal: true
# Alojamiento local, solo genera el sitio, con lo que no necesita hacer
# nada más
class DeployLocal < Deploy
store :values, accessors: %i[fqdn destination], coder: YAML
before_create :fqdn!, :destination!
before_destroy :remove_destination!
# Realizamos la construcción del sitio usando Jekyll y un entorno
# limpio para no pasarle secretos
#
# Pasamos variables de entorno mínimas para no filtrar secretos de
# Sutty
#
# TODO: Recolectar estadísticas y enviarlas a la base de datos
def deploy
yarn && bundle && jekyll_build
end
# Sólo permitimos un deploy local
def limit
1
end
private
# Un entorno que solo tiene lo que necesitamos
def env
paths = [File.dirname(`which bundle`), '/usr/bin']
{ 'PATH' => paths.join(':'), 'JEKYLL_ENV' => 'production' }
end
def yarn_lock
File.join(site.path, 'yarn.lock')
end
def yarn_lock?
File.exist? yarn_lock
end
# Corre yarn dentro del repositorio
def yarn
return unless yarn_lock?
run 'yarn'
end
def bundle
run 'bundle'
end
def jekyll_build
run "bundle exec jekyll build --destination \"#{escaped_destination}\""
end
def fqdn!
self.fqdn ||= "#{site.name}.#{ENV.fetch('SUTTY', 'sutty.nl')}"
end
def destination!
self.destination ||= File.join(Rails.root, '_deploy', fqdn)
end
# no debería haber espacios ni caracteres especiales, pero por si
# acaso...
def escaped_destination
Shellwords.escape destination
end
# Eliminar el destino si se elimina el deploy
def remove_destination!
FileUtils.rm_rf destination
end
end

47
app/models/deploy_zip.rb Normal file
View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
# Genera un ZIP a partir del sitio ya construido
#
# TODO: Firmar con minisign
class DeployZip < Deploy
store :values, accessors: %i[fqdn destination file path], coder: YAML
before_create :fqdn!, :destination!
before_create :file!, :path!
# Una vez que el sitio está generado, tomar todos los archivos y
# y generar un zip accesible públicamente.
#
# TODO: Recolectar estadísticas y enviarlas a la base de datos
def deploy
Dir.chdir(destination) do
Zip::File.open(path, Zip::File::CREATE) do |z|
Dir.glob('./**/**').each do |f|
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
end
end
end
File.exist? path
end
private
# Copiamos de DeployLocal para no cargar todos los métodos de
# compilación...
def fqdn!
self.fqdn ||= "#{site.name}.#{ENV.fetch('SUTTY', 'sutty.nl')}"
end
def destination!
self.destination ||= File.join(Rails.root, '_deploy', fqdn)
end
def file!
self.file ||= "#{fqdn}.zip"
end
def path!
self.path = File.join(destination, file)
end
end

View file

@ -13,6 +13,7 @@ class Site < ApplicationRecord
belongs_to :design
belongs_to :licencia
has_many :deploys
has_many :roles
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
through: :roles

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
# Crea la tabla de deploys posibles para un sitio
class CreateDeploys < ActiveRecord::Migration[5.2]
def change
create_table :deploys do |t|
t.timestamps
t.belongs_to :site, index: true
t.string :type, index: true
t.text :values
end
end
end

View file

@ -12,7 +12,17 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20_190_719_221_653) do
ActiveRecord::Schema.define(version: 20_190_723_220_002) do
create_table 'deploys', force: :cascade do |t|
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.integer 'site_id'
t.string 'type'
t.text 'values'
t.index ['site_id'], name: 'index_deploys_on_site_id'
t.index ['type'], name: 'index_deploys_on_type'
end
create_table 'designs', force: :cascade do |t|
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false

View file

@ -106,6 +106,9 @@ falta (algunas plantillas tienen requisitos extraños).
Las plantillas se instalan como gemas en los sitios, de forma que
podemos cambiarla desde las opciones del sitio luego.
Para poder cambiar el diseño, necesitamos poder editar la configuración
del sitio en _config.yml y colocar el nombre de la gema en `theme:`
## Internamente
Al crear un sitio, clonar el esqueleto en el lugar correcto. Al
@ -162,3 +165,50 @@ actualizaciones muestra el historial para que les usuaries vean cuales
son los cambios que se van a aplicar. Este historial viene del
repositorio git con lo que tenemos que tomarnos la costumbre de escribir
commits completos con explicación.
## Alojamiento
Para elegir las opciones de alojamiento, agregamos clases que
implementan el nombre del alojamiento y sus opciones específicas:
* Local (sitio.sutty.nl): Deploy local. Genera el sitio en
_deploy/name.sutty.nl y lo pone a disposición del servidor web (es
como veniamos trabajando hasta ahora). También admite dominios
propios.
Solo se puede tener uno y no es opcional porque lo necesitamos para
poder hacer el resto de deploys
* Neocities: Deploy a neocities.org, necesita API key. Se conecta con
neocities usando la API y envía los archivos.
Solo se puede tener uno (pendiente hablar con neocities si podemos
hacer esto).
* Zip: genera un zip con el contenido del sitio y provee una url de
descarga estilo https://sutty.nl/sites/site.zip
Solo se puede tener uno
* SSH: Deploy al servidor SSH que especifiquemos. Necesita que le
pasemos user@host, puerto y ruta (quizás la ruta no es necesaria y
vaya junto con user@host). Informar a les usuaries la llave pública
de Sutty para que la agreguen.
No hay límite a los servidores SSH que se pueden agregar.
La clase Deploy::* tiene que implementar estas interfaces:
* `#limit` devuelve un entero que es la cantidad de deploys de este
tipo que se pueden agregar, `nil` significa sin límite
* `#deploy` el método que realiza el deploy. En realidad este método
hay que correrlo por fuera de Sutty...
* `#attributes` los valores serializados que acepta el Deploy
Son modelos que pueden guardar atributos en la base y a través de los
que se pueden tomar los datos a través de la API (cuando tengamos un
método de deployment).
El plan es migrar todo esto a <drone.io> de forma que la compilación se
haga por separado de sutty. Este es un plan intermedio hasta que
tengamos tiempo de hacerlo realmente.

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :deploy_local do
site
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :deploy_zip do
site
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class DeployZipTest < ActiveSupport::TestCase
test 'se puede deployear' do
deploy_local = create :deploy_local
assert deploy_local.deploy
assert File.directory?(deploy_local.destination)
assert deploy_local.destroy
assert_not File.directory?(deploy_local.destination)
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class DeployLocalTest < ActiveSupport::TestCase
test 'se puede deployear' do
site = create :site
local = create :deploy_local, site: site
deploy = create :deploy_zip, site: site
# Primero tenemos que generar el sitio
local.deploy
escaped_path = Shellwords.escape(deploy.path)
assert deploy.deploy
assert File.file?(deploy.path)
assert_equal 'application/zip',
`file --mime-type "#{escaped_path}"`.split(' ').last
local.destroy
end
end