mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-15 04:41:43 +00:00
deployear los sitios de distintas formas
This commit is contained in:
parent
5ace66f20a
commit
11cf2cb08b
13 changed files with 282 additions and 1 deletions
1
Gemfile
1
Gemfile
|
@ -54,6 +54,7 @@ gem 'mobility'
|
||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rails_warden'
|
gem 'rails_warden'
|
||||||
|
gem 'rubyzip'
|
||||||
gem 'rugged'
|
gem 'rugged'
|
||||||
gem 'validates_hostname'
|
gem 'validates_hostname'
|
||||||
gem 'whenever', require: false
|
gem 'whenever', require: false
|
||||||
|
|
|
@ -423,6 +423,7 @@ DEPENDENCIES
|
||||||
rails_warden
|
rails_warden
|
||||||
rbnacl (< 5.0)
|
rbnacl (< 5.0)
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
|
rubyzip
|
||||||
rugged
|
rugged
|
||||||
sass-rails (~> 5.0)
|
sass-rails (~> 5.0)
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
|
|
33
app/models/deploy.rb
Normal file
33
app/models/deploy.rb
Normal 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
|
77
app/models/deploy_local.rb
Normal file
77
app/models/deploy_local.rb
Normal 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
47
app/models/deploy_zip.rb
Normal 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
|
|
@ -13,6 +13,7 @@ class Site < ApplicationRecord
|
||||||
belongs_to :design
|
belongs_to :design
|
||||||
belongs_to :licencia
|
belongs_to :licencia
|
||||||
|
|
||||||
|
has_many :deploys
|
||||||
has_many :roles
|
has_many :roles
|
||||||
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
|
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
|
||||||
through: :roles
|
through: :roles
|
||||||
|
|
13
db/migrate/20190723220002_create_deploys.rb
Normal file
13
db/migrate/20190723220002_create_deploys.rb
Normal 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
|
12
db/schema.rb
12
db/schema.rb
|
@ -12,7 +12,17 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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|
|
create_table 'designs', force: :cascade do |t|
|
||||||
t.datetime 'created_at', null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime 'updated_at', null: false
|
t.datetime 'updated_at', null: false
|
||||||
|
|
|
@ -106,6 +106,9 @@ falta (algunas plantillas tienen requisitos extraños).
|
||||||
Las plantillas se instalan como gemas en los sitios, de forma que
|
Las plantillas se instalan como gemas en los sitios, de forma que
|
||||||
podemos cambiarla desde las opciones del sitio luego.
|
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
|
## Internamente
|
||||||
|
|
||||||
Al crear un sitio, clonar el esqueleto en el lugar correcto. Al
|
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
|
son los cambios que se van a aplicar. Este historial viene del
|
||||||
repositorio git con lo que tenemos que tomarnos la costumbre de escribir
|
repositorio git con lo que tenemos que tomarnos la costumbre de escribir
|
||||||
commits completos con explicación.
|
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.
|
||||||
|
|
7
test/factories/deploy_local.rb
Normal file
7
test/factories/deploy_local.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :deploy_local do
|
||||||
|
site
|
||||||
|
end
|
||||||
|
end
|
7
test/factories/deploy_zip.rb
Normal file
7
test/factories/deploy_zip.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :deploy_zip do
|
||||||
|
site
|
||||||
|
end
|
||||||
|
end
|
13
test/models/deploy_local_test.rb
Normal file
13
test/models/deploy_local_test.rb
Normal 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
|
21
test/models/deploy_zip_test.rb
Normal file
21
test/models/deploy_zip_test.rb
Normal 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
|
Loading…
Reference in a new issue