5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-10-06 17:16:57 +00:00

Merge branch 'distributed-press' into issue-10464

This commit is contained in:
f 2023-03-18 16:37:51 -03:00
commit 2921f202b2
16 changed files with 468 additions and 16 deletions

View file

@ -15,6 +15,8 @@ RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
RUN gem install --no-document --no-user-install foreman RUN gem install --no-document --no-user-install foreman
RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc
RUN apk add npm && npm install -g pnpm && apk del npm
VOLUME "/srv" VOLUME "/srv"
EXPOSE 3000 EXPOSE 3000

View file

@ -39,6 +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 'njalla-api-client'
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'

View file

@ -115,6 +115,7 @@ GEM
xpath (>= 2.0, < 4.0) xpath (>= 2.0, < 4.0)
chartkick (4.1.2) chartkick (4.1.2)
childprocess (4.1.0) childprocess (4.1.0)
climate_control (1.2.0)
coderay (1.1.3) coderay (1.1.3)
colorator (1.1.0) colorator (1.1.0)
commonmarker (0.21.2-x86_64-linux-musl) commonmarker (0.21.2-x86_64-linux-musl)
@ -153,12 +154,45 @@ GEM
devise_invitable (2.0.5) devise_invitable (2.0.5)
actionmailer (>= 5.0) actionmailer (>= 5.0)
devise (>= 4.6) devise (>= 4.6)
distributed-press-api-client (0.2.2)
addressable (~> 2.3, >= 2.3.0)
climate_control
dry-schema
httparty (~> 0.18)
json (~> 2.1, >= 2.1.0)
jwt (~> 2.6.0)
dotenv (2.7.6) dotenv (2.7.6)
dotenv-rails (2.7.6) dotenv-rails (2.7.6)
dotenv (= 2.7.6) dotenv (= 2.7.6)
railties (>= 3.2) railties (>= 3.2)
down (5.2.4) down (5.2.4)
addressable (~> 2.8) addressable (~> 2.8)
dry-configurable (1.0.1)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-core (1.0.0)
concurrent-ruby (~> 1.0)
zeitwerk (~> 2.6)
dry-inflector (1.0.0)
dry-initializer (3.1.1)
dry-logic (1.5.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-schema (1.13.0)
concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.0, < 2)
dry-initializer (~> 3.0)
dry-logic (>= 1.5, < 2)
dry-types (>= 1.7, < 2)
zeitwerk (~> 2.6)
dry-types (1.7.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2)
dry-inflector (~> 1.0, < 2)
dry-logic (>= 1.4, < 2)
zeitwerk (~> 2.6)
ed25519 (1.2.4-x86_64-linux-musl) ed25519 (1.2.4-x86_64-linux-musl)
em-websocket (0.5.3) em-websocket (0.5.3)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
@ -216,8 +250,8 @@ GEM
thor thor
hiredis (0.6.3-x86_64-linux-musl) hiredis (0.6.3-x86_64-linux-musl)
http_parser.rb (0.8.0-x86_64-linux-musl) http_parser.rb (0.8.0-x86_64-linux-musl)
httparty (0.18.1) httparty (0.21.0)
mime-types (~> 3.0) mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
i18n (1.8.11) i18n (1.8.11)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
@ -292,6 +326,7 @@ GEM
jekyll-write-and-commit-changes (0.2.1) jekyll-write-and-commit-changes (0.2.1)
jekyll (~> 4) jekyll (~> 4)
rugged (~> 1) rugged (~> 1)
jwt (2.6.0)
kaminari (1.2.1) kaminari (1.2.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1) kaminari-actionview (= 1.2.1)
@ -352,6 +387,9 @@ 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)
dry-schema
httparty (~> 0.18)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pairing_heap (3.0.0) pairing_heap (3.0.0)
parallel (1.21.0) parallel (1.21.0)
@ -584,6 +622,7 @@ DEPENDENCIES
devise devise
devise-i18n devise-i18n
devise_invitable devise_invitable
distributed-press-api-client (~> 0.2.2)
dotenv-rails dotenv-rails
down down
ed25519 ed25519
@ -618,6 +657,7 @@ DEPENDENCIES
mini_magick mini_magick
mobility mobility
net-ssh net-ssh
njalla-api-client
nokogiri nokogiri
pg pg
pg_search pg_search

View file

@ -6,3 +6,4 @@ blazer_1d: bundle exec rake blazer:run_checks SCHEDULE="1 day"
blazer: bundle exec rake blazer:send_failing_checks blazer: bundle exec rake blazer:send_failing_checks
prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_" prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
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

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
# Renueva los tokens de Distributed Press antes que se venzan,
# activando los callbacks que hacen que se refresque el token.
class RenewDistributedPressTokensJob < ApplicationJob
# Renueva todos los tokens a punto de vencer o informa el error sin
# detener la tarea si algo pasa.
def perform
DistributedPressPublisher.with_about_to_expire_tokens.find_each do |publisher|
publisher.touch
rescue DistributedPress::V1::Error => e
data = { instance: publisher.instance, expires_at: publisher.client.token.expires_at }
ExceptionNotifier.notify_exception(e, data: data)
end
end
end

View file

@ -88,6 +88,13 @@ class Deploy < ApplicationRecord
r&.success? r&.success?
end end
# Variables de entorno
#
# @return [Hash]
def local_env
@local_env ||= {}
end
private private
# @param [String] # @param [String]
@ -95,4 +102,12 @@ class Deploy < ApplicationRecord
def readable_cmd(cmd) def readable_cmd(cmd)
cmd.split(' -', 2).first.tr(' ', '_') cmd.split(' -', 2).first.tr(' ', '_')
end end
def deploy_local
@deploy_local ||= site.deploys.find_by(type: 'DeployLocal')
end
def non_local_deploys
@non_local_deploys ||= site.deploys.where.not(type: 'DeployLocal')
end
end end

View file

@ -0,0 +1,166 @@
# frozen_string_literal: true
require 'distributed_press/v1/client/site'
require 'njalla/v1'
# Soportar Distributed Press APIv1
#
# Usa tokens de publicación efímeros para todas las acciones.
#
# Al ser creado, genera el sitio en la instancia de Distributed Press
# configurada y almacena el ID.
#
# Al ser publicado, envía los archivos en un tarball y actualiza la
# información.
class DeployDistributedPress < Deploy
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
before_create :create_remote_site!, :create_njalla_records!
# Actualiza la información y luego envía los cambios
#
# @param :output [Bool]
# @return [Bool]
def deploy
status = false
log = []
time_start
create_remote_site! if remote_site_id.blank?
create_njalla_records! if remote_info['njalla'].blank?
save
if remote_site_id.blank? || remote_info['njalla'].blank?
raise DeployJob::DeployException, ''
end
site_client.tap do |c|
stdout = Thread.new(publisher.logger_out) do |io|
until io.eof?
line = io.gets
puts line if output
log << line
end
end
update remote_info: c.show(publishing_site).to_h
status = c.publish(publishing_site, deploy_local.destination)
publisher.logger.close
stdout.join
end
time_stop
create_stat! status, log.join
status
end
def limit; end
def size
deploy_local.size
end
def destination; end
# Devuelve las URLs de todos los protocolos
def urls
remote_info[:links].values.map do |protocol|
[ protocol[:link], protocol[:gateway] ]
end.flatten.compact.select do |link|
link.include? '://'
end
end
private
# El cliente de la API
#
# TODO: cuando soportemos más, tiene que haber una relación entre
# DeployDistributedPress y DistributedPressPublisher.
#
# @return [DistributedPressPublisher]
def publisher
@publisher ||= DistributedPressPublisher.first
end
# El cliente para actualizar el sitio
#
# @return [DistributedPress::V1::Client::Site]
def site_client
DistributedPress::V1::Client::Site.new(publisher.client)
end
# Genera el esquema de datos para poder publicar el sitio
#
# @return [DistributedPress::V1::Schemas::PublishingSite]
def publishing_site
DistributedPress::V1::Schemas::PublishingSite.new.call(id: remote_site_id)
end
# Genera el esquema de datos para crear el sitio
#
# @return [DistributedPressPublisher::V1::Schemas::NewSite]
def create_site
DistributedPress::V1::Schemas::NewSite.new.call(domain: hostname, protocols: { http: true, ipfs: true, hyper: true })
end
# Crea el sitio en la instancia con el hostname especificado
#
# @return [nil]
def create_remote_site!
created_site = site_client.create(create_site)
self.remote_site_id = created_site[:id]
self.remote_info = created_site.to_h
rescue DistributedPress::V1::Error
ExceptionNotifier.notify_exception(e, data: { site: site.name })
ensure
nil
end
# Crea los registros en Njalla
#
# @return [nil]
def create_njalla_records!
# XXX: Esto depende de nuestro DNS actual, cuando lo migremos hay
# que eliminarlo.
unless site.name.end_with? '.'
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']['ns'] = njalla.add_record(name: "_dnslink.#{site.name}", type: 'NS', content: "#{publisher.hostname}.").to_h
end
rescue HTTParty::Error => e
ExceptionNotifier.notify_exception(e, data: { site: site.name })
self.remote_info['njalla'] = nil
ensure
nil
end
# Registra lo que sucedió
#
# @param status [Bool]
# @param log [String]
# @return [nil]
def create_stat!(status, log)
build_stats.create action: publisher.to_s,log: log, seconds: time_spent_in_seconds, bytes: size, status: status
nil
end
# Actualizar registros en Njalla
#
# @return [Njalla::V1::Domain]
def njalla
@njalla ||=
begin
client = Njalla::V1::Client.new(token: ENV['NJALLA_TOKEN'])
Njalla::V1::Domain.new(domain: Site.domain, client: client)
end
end
end

View file

@ -15,6 +15,7 @@ class DeployLocal < Deploy
def deploy(output: false) def deploy(output: false)
return false unless mkdir return false unless mkdir
return false unless yarn(output: output) return false unless yarn(output: output)
return false unless pnpm(output: output)
return false unless bundle(output: output) return false unless bundle(output: output)
jekyll_build(output: output) jekyll_build(output: output)
@ -56,27 +57,34 @@ class DeployLocal < Deploy
end end
# Un entorno que solo tiene lo que necesitamos # Un entorno que solo tiene lo que necesitamos
#
# @return [Hash]
def env def env
# XXX: This doesn't support Windows paths :B # XXX: This doesn't support Windows paths :B
paths = [File.dirname(`which bundle`), '/usr/bin', '/bin'] paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
{ # Las variables de entorno extra no pueden superponerse al local.
'HOME' => home_dir, extra_env.merge({
'PATH' => paths.join(':'), 'HOME' => home_dir,
'SPREE_API_KEY' => site.tienda_api_key, 'PATH' => paths.join(':'),
'SPREE_URL' => site.tienda_url, 'SPREE_API_KEY' => site.tienda_api_key,
'AIRBRAKE_PROJECT_ID' => site.id.to_s, 'SPREE_URL' => site.tienda_url,
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key, 'AIRBRAKE_PROJECT_ID' => site.id.to_s,
'JEKYLL_ENV' => Rails.env, 'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
'LANG' => ENV['LANG'], 'JEKYLL_ENV' => Rails.env,
'YARN_CACHE_FOLDER' => yarn_cache_dir 'LANG' => ENV['LANG'],
} 'YARN_CACHE_FOLDER' => yarn_cache_dir
})
end end
def yarn_cache_dir def yarn_cache_dir
Rails.root.join('_yarn_cache').to_s Rails.root.join('_yarn_cache').to_s
end end
def pnpm_cache_dir
Rails.root.join('_pnpm_cache').to_s
end
def yarn_lock def yarn_lock
File.join(site.path, 'yarn.lock') File.join(site.path, 'yarn.lock')
end end
@ -85,7 +93,15 @@ class DeployLocal < Deploy
File.exist? yarn_lock File.exist? yarn_lock
end end
def gem(output: false) def pnpm_lock
File.join(site.path, 'pnpm-lock.yaml')
end
def pnpm_lock?
File.exist? pnpm_lock
end
def gem
run %(gem install bundler --no-document), output: output run %(gem install bundler --no-document), output: output
end end
@ -96,6 +112,13 @@ class DeployLocal < Deploy
run 'yarn install --production', output: output run 'yarn install --production', output: output
end end
def pnpm(output: output)
return true unless pnpm_lock?
run %(pnpm config set store-dir "#{pnpm_cache_dir}"), output: output
run 'pnpm install --production', output: output
end
def bundle def bundle
run %(bundle install --no-cache --path="#{gems_dir}"), output: output run %(bundle install --no-cache --path="#{gems_dir}"), output: output
end end
@ -114,4 +137,17 @@ class DeployLocal < Deploy
def remove_destination! def remove_destination!
FileUtils.rm_rf destination FileUtils.rm_rf destination
end end
# Consigue todas las variables de entorno configuradas por otros
# deploys.
#
# @return [Hash]
def extra_env
@extra_env ||=
non_local_deploys.reduce({}) do |extra_env, deploy|
extra_env.tap do |e|
e.merge! deploy.local_env
end
end
end
end end

View file

@ -0,0 +1,84 @@
# frozen_string_literal: true
require 'distributed_press/v1'
# Almacena el token de autenticación y la URL, por ahora solo vamos
# a tener uno, pero queda abierta la posibilidad de agregar más.
class DistributedPressPublisher < ApplicationRecord
# Cifrar la información del token en la base de datos
has_encrypted :token
# La salida del log
#
# @return [IO]
attr_reader :logger_out
# La instancia es única
validates_uniqueness_of :instance
# El token es necesario
validates_presence_of :token
# Mantener la fecha de vencimiento actualizada
before_save :update_expires_at_from_token!, :update_token_from_client!
# Devuelve todos los tokens que vencen en una hora
scope :with_about_to_expire_tokens, lambda {
where('expires_at > ? and expires_at < ?', Time.now, Time.now + 1.hour)
}
# Instancia un cliente de Distributed Press a partir del token. Al
# cargar un token a punto de vencer se renueva automáticamente.
#
# @return [DistributedPress::V1::Client]
def client
@client ||= DistributedPress::V1::Client.new(url: instance, token: token, logger: logger)
end
# @return [String]
def to_s
"Distributed Press <#{instance}>"
end
# Devuelve el hostname de la instancia
#
# @return [String]
def hostname
@hostname ||= URI.parse(instance).hostname
end
# @return [Logger]
def logger
@logger ||=
begin
@logger_out, @logger_in = IO.pipe
::Logger.new @logger_in, formatter: formatter
end
end
private
def formatter
@formatter ||= lambda do |_, _, _, msg|
"#{msg}\n"
end
end
# Actualiza o desactiva la fecha de vencimiento a partir de la
# información del token.
#
# @return [nil]
def update_expires_at_from_token!
self.expires_at = client.token.forever? ? nil : client.token.expires_at
nil
end
# Actualiza el token a partir del cliente, que ya actualiza el token
# automáticamente.
#
# @return [nil]
def update_token_from_client!
self.token = client.token.to_s
nil
end
end

View file

@ -18,7 +18,7 @@ class Site < ApplicationRecord
# TODO: Hacer que los diferentes tipos de deploy se auto registren # TODO: Hacer que los diferentes tipos de deploy se auto registren
# @see app/services/site_service.rb # @see app/services/site_service.rb
DEPLOYS = %i[local private www zip hidden_service].freeze DEPLOYS = %i[local private www zip hidden_service distributed_press].freeze
validates :name, uniqueness: true, hostname: { validates :name, uniqueness: true, hostname: {
allow_root_label: true allow_root_label: true

View file

@ -0,0 +1,21 @@
-# Publicar a la web distribuida
.row
.col
= deploy.hidden_field :id
= deploy.hidden_field :type
.custom-control.custom-switch
-#
El checkbox invierte la lógica de destrucción porque queremos
crear el deploy si está activado y destruirlo si está
desactivado.
= deploy.check_box :_destroy,
{ checked: deploy.object.persisted?, class: 'custom-control-input' },
'0', '1'
= deploy.label :_destroy, class: 'custom-control-label' do
%h3= t('.title')
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
tags: %w[p strong em a]
%hr/

View file

@ -109,6 +109,10 @@ en:
title: Synchronize to backup server title: Synchronize to backup server
success: Success! success: Success!
error: Error 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:
@ -255,6 +259,22 @@ en:
Only accessible through [Tor Only accessible through [Tor
Browser](https://www.torproject.org/download/) Browser](https://www.torproject.org/download/)
deploy_distributed_press:
title: 'Publish to the distributed Web'
help: |
Make your site available through peer-to-peer protocols,
Inter-Planetary File System (IPFS), Hypercore, and via
BitTorrent, so your site is more resilient and can be available
offline, including in community mesh networks.
**Important:** Only use this option if you would like your data
to be permanently available. If you decide to undo this
selection, a cleared version of the site will be shared in its
place. However, it is possible that nodes on the distributed
storage network may continue retaining copies of the data
indefinitely.
[Learn more](https://ffdweb.org/building-distributed-press-a-publishing-tool-for-the-decentralized-web/)
stats: stats:
index: index:
title: Statistics title: Statistics

View file

@ -109,6 +109,10 @@ es:
title: Sincronizar al servidor alternativo title: Sincronizar al servidor alternativo
success: ¡Éxito! success: ¡Éxito!
error: Hubo un error 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:
@ -260,6 +264,22 @@ es:
Sólo será accesible a través del [Navegador Sólo será accesible a través del [Navegador
Tor](https://www.torproject.org/es/download/). Tor](https://www.torproject.org/es/download/).
deploy_distributed_press:
title: 'Publicar a la Web distribuida'
help: |
Utiliza protocolos de pares, Inter-Planetary File System (IPFS),
Hypercore y torrents, para que tu sitio sea más resiliente y
esté disponible _offline_, inclusive en redes _mesh_
comunitarias.
**Importante:** Sólo usa esta opción si te parece correcto que
tu contenido esté disponible permanentemente. Cuando elijas
des-hacer esta acción, una versión "vacía" del sitio será
compartida en su lugar. Sin embargo, es posible que algunos
nodos en la red de almacenamiento distribuida puedan retener
copias de tu contenido indefinidamente.
[Saber más (en inglés)](https://ffdweb.org/building-distributed-press-a-publishing-tool-for-the-decentralized-web/)
stats: stats:
index: index:
title: Estadísticas title: Estadísticas

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
# Crea la tabla de publishers de Distributed Press que contiene las
# instancias y tokens
class CreateDistributedPressPublisher < ActiveRecord::Migration[6.1]
def change
create_table :distributed_press_publishers do |t|
t.timestamps
t.string :instance, unique: true
t.text :token_ciphertext, null: false
t.datetime :expires_at, null: true
end
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
namespace :distributed_press do
namespace :tokens do
desc 'Renew tokens'
task renew: :environment do
RenewDistributedPressTokensJob.perform_now
end
end
end

View file

@ -34,4 +34,8 @@ check program access_logs
check program stats 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 * * *"
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 if status != 0 then alert