Il modulo backend è stato realizzato come un package `Python` denominato ``sophon``, e poi `containerizzato <Containerizzazione del modulo backend>`, creando un'immagine `Docker` standalone.
Il package è stato creato utilizzando l'utility ``startproject`` di Django, la quale crea una cartella di script `Python` con i quali partire per lo sviluppo di una nuovo software web.
La cartella generata è stata modificata significativamente: ne si è modificata la struttura in modo tale da trasformarla da un insieme di script a un vero e proprio modulo Python eseguibile e distribuibile, e si sono aggiunte nuove funzionalità di utilità generale all'applicazione, quali una `pagina di amministrazione personalizzata <Pagina di amministrazione personalizzata>`, il `caricamento dinamico delle impostazioni <Caricamento dinamico delle impostazioni>` e vari `miglioramenti all'autenticazione <Miglioramenti all'autenticazione>`
La pagina di amministrazione viene personalizzata con la classe `SophonAdminSite`, che modifica alcuni parametri della classe base.
Inoltre, il template predefinito viene sovrascritto da quello all'interno del file ``templates/admin/base.html``, che sostituisce il foglio di stile con uno personalizzato per Sophon.
Il file di impostazioni viene modificato per **permettere la configurazione attraverso variabili di ambiente** invece che attraverso la modifica del file ``settings.py``, rendendo la `containerizzazione <Containerizzazione del modulo backend>` molto più semplice.
Si configura `rest_framework` per accettare header di autenticazione nella forma ``Bearer <token>``, invece che il default di `rest_framework```Token <token>``.
Per permettere l'integrazione la creazione automatica del primo :ref:`superutente` quando Sophon viene eseguito da Docker, viene introdotto dall'app il comando di gestione ``initsuperuser``.
..class:: Command
Questo comando crea automaticamente un :ref:`superutente` con le credenziali specificate in :ref:`\`\`DJANGO_SU_USERNAME\`\``, :ref:`\`\`DJANGO_SU_EMAIL\`\`` e :ref:`\`\`DJANGO_SU_PASSWORD\`\``.
Viene estesa la classe astratta `django.db.models.Model` con funzioni per stabilire il `livello di accesso` di un `utente <Utenti in Sophon>` all'oggetto e per generare automaticamente i `rest_framework.serializers.ModelSerializer` in base ad esso.
Viene definito un nuovo modello astratto, basato su `SophonModel`, che permette di determinare i permessi dell'`utente <Utenti in Sophon>` in base alla sua appartenenza al gruppo a cui è collegato l'oggetto implementatore.
Enumerazione che stabilisce il livello di autorità che un `utente <Utenti in Sophon>` può avere all'interno di un `gruppo di ricerca <Gruppi di ricerca in Sophon>`.
Impostando ``1`` come unica scelta per il campo della chiave primaria ``id``, si crea un modello "singleton", ovvero un modello di cui può esistere un'istanza sola in tutto il database.
L'istanza unica viene creata dalla migrazione ``0004_sophoninstancedetails.py``.
I permessi di `rest_framework` vengono estesi con due nuove classi che utilizzano il `modello di autorizzazione astratto <Modello di autorizzazione astratto>` precedentemente definito.
Classe **astratta** che estende la classe base `rest_framework.viewsets.ReadOnlyModelViewSet` con metodi di utilità mancanti nell'implementazione originale, allacciandola inoltre a `.models.SophonGroupModel`.
..method:: get_queryset(self) -> QuerySet
:abstractmethod:
Imposta come astratto (e quindi obbligatorio) il metodo `rest_framework.viewsets.ReadOnlyModelViewSet.get_queryset`.
..method:: permission_classes(self)
:property:
Sovrascrive il campo di classe `rest_framework.viewsets.ReadOnlyModelViewSet.permission_classes` con una funzione, permettendone la selezione dei permessi richiesti al momento di ricezione di una richiesta HTTP (invece che al momento di definizione della classe).
Delega la selezione delle classi a `.get_permission_classes`.
Funzione che permette la selezione del `rest_framework.serializers.Serializer` da utilizzare per una determinata richiesta al momento di ricezione di quest'ultima.
Utilizza:
- il serializzatore **in sola lettura** per elencare gli oggetti (azione ``list``);
- il serializzatore **di creazione** per creare nuovi oggetti (azione ``create``) e per generare i metadati del viewset (azione ``metadata``);
- il serializzatore ottenuto da `.models.SophonGroupModel.get_access_serializer` per la visualizzazione dettagliata (azione ``retrieve``), la modifica (azioni ``update`` e ``partial_update``) e l'eliminazione (azione ``destroy``) di un singolo oggetto;
- il serializzatore ottenuto da `.get_custom_serializer_classes` per le azioni personalizzate.
Classe **astratta** che estende la classe base `ReadSophonViewSet` aggiungendoci i metodi di `rest_framework.viewsets.ModelViewSet` che effettuano modifiche sugli oggetti.
Depreca i metodi ``perform_*`` di `rest_framework`, introducendone versioni migliorate con una signature diversa dal nome di ``hook_*``.
Funzione chiamata durante l'esecuzione dell'azione di creazione oggetto ``create``.
:param serializer:Il `~rest_framework.serializers.Serializer` già "riempito" contenente i dati dell'oggetto che sta per essere creato.
:raises .HTTPException:È possibile interrompere la creazione dell'oggetto con uno specifico codice errore sollevando una `.HTTPException` all'interno della funzione.
:returns:Un `dict` da unire a quello del `~rest_framework.serializers.Serializer` per formare l'oggetto da creare.
Funzione chiamata durante l'esecuzione delle azioni di modifica oggetto ``update`` e ``partial_update``.
:param serializer:Il `~rest_framework.serializers.Serializer` già "riempito" contenente i dati dell'oggetto che sta per essere modificato.
:raises .HTTPException:È possibile interrompere la creazione dell'oggetto con uno specifico codice errore sollevando una `.HTTPException` all'interno della funzione.
:returns:Un `dict` da unire a quello del `~rest_framework.serializers.Serializer` per formare l'oggetto da modificare.
Funzione chiamata durante l'esecuzione dell'azione di eliminazione oggetto ``destroy``.
:raises .HTTPException:È possibile interrompere la creazione dell'oggetto con uno specifico codice errore sollevando una `.HTTPException` all'interno della funzione.
..exception:: sophon.core.errors.HTTPException
Tipo di eccezione che è possibile sollevare nei metodi ``hook_*`` di `.WriteSophonViewSet` per interrompere l'azione in corso senza applicare le modifiche.
..attribute:: status: int
Permette di specificare il codice errore con cui rispondere alla richiesta interrotta.
Classe **astratta** che estende la classe base `.WriteSophonViewSet` estendendo gli ``hook_*`` con verifiche dei permessi dell'utente che tenta di effettuare l'azione.
View che restituisce il valore attuale dell'unico oggetto `.models.SophonInstanceDetails`.
Accessibile tramite richieste ``GET`` all'URL :samp:`/api/core/instance/`.
Pagina di amministrazione
^^^^^^^^^^^^^^^^^^^^^^^^^
..module:: sophon.core.admin
Vengono infine registrati nella pagina di amministrazione i modelli concreti definiti in questa app, effettuando alcune personalizzazioni elencate in seguito.
..class:: ResearchGroupAdmin(SophonAdmin)
Per i gruppi di ricerca, viene specificato un ordinamento, permesso il filtraggio e selezionati i campi più importanti da visualizzare nella lista.
..class:: SophonInstanceDetails(SophonAdmin)
Per i dettagli dell'istanza, vengono disattivate tutte le azioni, impedendo la creazione o eliminazione del singleton.
Per verificare che i `modelli <Modello base astratto>` e `viewset <Viewset astratti>` funzionassero correttamente e non avessero problemi di `sicurezza <Sicurezza>`, sono stati realizzati degli unit test in grado di rilevare la presenza di errori all'interno dell'app.
Test case generici
^^^^^^^^^^^^^^^^^^
Vengono definiti alcuni test case generici per facilitare le interazioni tra ``APITestCase`` e viewset.
..note::
I nomi delle funzioni usano nomi con capitalizzazione inconsistente, in quanto lo stesso modulo `unittest` non rispetta lo stile suggerito in :pep:`8`.
Context manager che permette di effettuare richieste all'API come uno specifico utente, effettuando il logout quando sono state effettuate le richieste necessarie.
Asserzione che permette di verificare che l'oggetto restituito da una richiesta all'API contenga almeno le chiavi e i valori contenuti nel dizionario ``expected``.
I seguenti metodi asseriscono che una determinata azione con determinati parametri risponderà con il codice di stato ``code``, e restituiscono i dati contenuti nella risposta se l'azione è riuscita (``200 <= code < 300``)
..method:: assertActionList(self, code: int = 200) -> typing.Optional[ReturnDict]
..method:: assertActionRetrieve(self, pk, code: int = 200) -> typing.Optional[ReturnDict]
L'app `sophon.projects` è un app secondaria che dipende da `sophon.core` che introduce in Sophon il concetto di `progetto di ricerca <Progetti di ricerca in Sophon>`.
L'app `sophon.projects` teoricamente è opzionale, in quanto il modulo backend può funzionare senza di essa, e può essere rimossa dal modulo `sophon.settings`.
Non è però possibile rimuoverla nella versione finale distribuita, in quanto il modulo `sophon.settings` non è modificabile dall'esterno, e in quanto il `modulo frontend <Modulo frontend>` non prevede questa funzionalità e si aspetta che i percorsi API relativi all'app siano disponibili.
Viewset in lettura e scrittura che permette di interagire con i progetti di ricerca a cui l'utente loggato ha accesso, filtrati per il gruppo a cui appartengono.
Il filtraggio viene effettuato limitando il queryset.
Classe per la pagina di amministrazione che specifica un ordinamento, permette il filtraggio per gruppo di appartenenza e visibilità, e specifica i campi da visualizzare nell'elenco dei progetti.
L'app `sophon.notebooks` teoricamente è opzionale, in quanto il modulo backend può funzionare senza di essa, e può essere rimossa dal modulo `sophon.settings`.
Non è però possibile rimuoverla nella versione finale distribuita, in quanto il modulo `sophon.settings` non è modificabile dall'esterno, e in quanto il `modulo frontend <Modulo frontend>` non prevede questa funzionalità e si aspetta che i percorsi API relativi all'app siano disponibili.
Internamente, un notebook non è altro che un container `Docker` accessibile ad un determinato indirizzo il cui stato è sincronizzato con un oggetto del database del `modulo backend <Modulo backend>`.
Per facilitare lo sviluppo di Sophon, sono state realizzate due modalità di operazione di quest'ultimo.
* Nella prima, la **modalità sviluppo**, il `modulo proxy <Modulo proxy>` non è in esecuzione, ed è possibile collegarsi direttamente ai container all'indirizzo IP locale ``127.0.0.1``.
* Nella seconda, la **modalità produzione**, il `modulo proxy <Modulo proxy>` è in esecuzione all'interno di un container Docker, e si collega ai `moduli Jupyter <Modulo Jupyter>` attraverso i relativi network Docker tramite indirizzi presenti all'interno .
La rubrica mappa gli URL pubblici dei notebook a URL privati relativi al `modulo proxy <Modulo proxy>`, in modo da effettuare reverse proxying **dinamico**.
Classe che permette il recupero, la creazione, la modifica e l'eliminazioni di chiavi di un database `dbm.gnu` come se quest'ultimo fosse un `dict` con supporto a chiavi e valori `str` e `bytes`.
Tutte le `str` passate a questa classe vengono convertite in `bytes` attraverso questa funzione, che effettua un encoding in ASCII e solleva un errore se quest'ultimo fallisce.
Assegnazione porta effimera
^^^^^^^^^^^^^^^^^^^^^^^^^^^
In *modalità sviluppo*, è necessario trovare una porta libera a cui rendere accessibile i container Docker dei notebook.
..function:: get_ephemeral_port() -> int
Questa funzione apre e chiude immediatamente un `socket.socket` all'indirizzo ``localhost:0`` in modo da ricevere dal sistema operativo un numero di porta sicuramente libero.
L'implementazione di questa funzione potrebbe causare rallentamenti nella risposta alle pagine web per via di una chiamata al metodo `time.sleep` al suo interno.
Ciò è dovuto al mancato supporto alle funzioni asincrone nella versione attuale di `rest_framework`.
Si è deciso di mantenere comunque la funzionalità a scopi dimostrativi e per compatibilità futura.
Per rendere l'interfaccia grafica più `intuibile <intuibile>`, si è scelto di rendere trasparente all'utente il meccanismo di autenticazione a JupyterLab.
Al momento ne è supportata una sola per semplificare l'esperienza utente, ma altre possono essere aggiunte al file che definisce il modello per permettere agli utenti di scegliere tra più immagini.
Al momento, Sophon si aspetta che tutte le immagini specificate espongano un server web sulla porta ``8888``, e supportino il protocollo di autenticazione di Jupyter, ovvero che sia possibile raggiungere il container ai seguenti indirizzi: :samp:`{PROTOCOLLO}://immagine:8888/lab?token={TOKEN}` e :samp:`{PROTOCOLLO}://immagine:8888/tree?token={TOKEN}`.
Il token segreto che verrà passato attraverso le variabili di ambiente al container Docker dell'oggetto per permettere solo agli utenti autorizzati di accedere a quest'ultimo.
..attribute:: container_id: CharField
L'id assegnato dal daemon Docker al container di questo oggetto.
Se il notebook non è avviato, questo attributo varrà `None`.
Tenta di creare e avviare un container `Docker` per l'oggetto, bloccando fino a quando esso non sarà avviato con `~.docker.sleep_until_container_has_started`.
Come per il modulo `sophon.projects`, vengono creati due viewset per interagire con i progetti di ricerca, basati entrambi su un viewset astratto che ne definisce le proprietà comuni.
Classe **astratta** che effettua l'override di `~sophon.core.views.SophonGroupView.get_group_from_serializer` e definisce cinque azioni personalizzate per l'interazione con il notebook.
Il modulo backend è incapsulato in un'immagine `Docker` basata sull'immagine ufficiale `python:3.9.7-bullseye <https://hub.docker.com/_/python>`_.
L'immagine utilizza `Poetry` per installare le dipendenze, poi esegue il file ``docker_start.sh`` riportato sotto che effettua le migrazioni, prepara i file statici di Django e `prova a creare un superutente <Aggiunta di un nuovo comando di gestione>`, per poi avviare il progetto Django attraverso :mod:`gunicorn` sulla porta 8000.
Il modulo frontend è stato realizzato come un package `Node.js` denominato ``@steffo/sophon-frontend``, e poi `containerizzato <Containerizzazione del modulo frontend>`, creando un'immagine `Docker` standalone, esattamente come per il `modulo backend <Containerizzazione del modulo backend>`.
Le directory di :mod:`@steffo45/sophon-frontend` sono strutturate nella seguente maniera:
src/components
Contiene i componenti React sia con le classi sia funzionali.
src/contexts
Contiene i contesti React creati con :func:`React.createContext`.
src/hooks
Contiene gli hook React personalizzati utilizzati nei componenti funzionali.
src/types
Contiene estensioni ai tipi base TypeScript, come ad esempio i tipi restituiti dalla web API del :ref:`modulo backend`.
src/utils
Contiene varie funzioni di utility.
public
Contiene i file statici da servire assieme all'app.
Comunicazione con il server
---------------------------
Axios
^^^^^
Per effettuare richieste all'API web, si è deciso di utilizzare la libreria :mod:`axios`, in quanto permette di creare dei "client" personalizzabili con varie proprietà.
In particolare, si è scelto di forkarla, integrando anticipatamente una proposta di funzionalità che permette alle richieste di essere interrotte attraverso degli :class:`AbortController`.
Client personalizzati
^^^^^^^^^^^^^^^^^^^^^
Per permettere all'utente di selezionare l'istanza da utilizzare e di comunicare con l'API con le proprie credenziali, si è scelto di creare client personalizzati partendo da due contesti.
All'interno di un contesto in cui è stata selezionata un'istanza (:data:`InstanceContext`), viene creato un client dal seguente hook:
..function:: useInstanceAxios(config = {})
Questo hook specifica il ``baseURL`` del client Axios, impostandolo all'URL dell'istanza selezionata.
All'interno di un contesto in cui è stato effettuato l'accesso come utente (:data:`AuthorizationContext`), viene creato invece un client dal seguente hook:
..function:: useAuthorizedAxios(config = {})
Questo hook specifica il valore dell'header ``Authorization`` da inviare in tutte le richieste effettuate a :samp:`Bearer {TOKEN}`, utilizzando il token ottenuto al momento dell'accesso.
Utilizzo di viewset
^^^^^^^^^^^^^^^^^^^
Viene implementato un hook che si integra con i viewset di Django, fornendo un API semplificato per effettuare azioni su di essi.
..function:: useViewSet(baseRoute)
Questo hook implementa tutte le azioni :py:mod:`rest_framework` di un viewset in lettura e scrittura.
Richiede di essere chiamato all'interno di un :data:`AuthorizationContext`.
..function:: async list(config = {})
..function:: async retrieve(pk, config = {})
..function:: async create(config)
..function:: async update(pk, config)
..function:: async destroy(pk, config)
Viene inoltre fornito supporto per le azioni personalizzate.
..function:: async command(config)
Permette azioni personalizzate su tutto il viewset.
..function:: async action(config)
Permette azioni personalizzate su uno specifico oggetto del viewset.
Emulazione di viewset
^^^^^^^^^^^^^^^^^^^^^
Viene creato un hook che tiene traccia degli oggetti restituiti da un determinato viewset, ed emula i risultati delle azioni effettuate, minimizzando i rerender e ottenendo una ottima user experience.
Il viewset restituito da :func:`useViewSet`, utilizzato come interfaccia di basso livello per effettuare azioni.
..attribute:: state
Lo stato del viewset, che tiene traccia degli oggetti e delle azioni in corso su di essi.
Gli oggetti all'interno di esso sono istanze di :class:`ManagedResource`, create usando wrapper di :func:`.update`, :func:`.destroy` e :func:`.action`, che permettono di modificare direttamente l'oggetto senza preoccuparsi dell'indice a cui si trova nell'array.
..attribute:: dispatch
Riduttore che permette di alterare lo :attr:`.state`.
..function:: async refresh()
Ricarica gli oggetti del viewset.
Viene chiamata automaticamente al primo render se ``refreshOnMount`` è :data:`True`.
..function:: async create(data)
Crea un nuovo oggetto nel viewset con i dati specificati come argomento, e lo aggiunge allo stato se la richiesta va a buon fine.
..function:: async command(method, cmd, data)
Esegue l'azione personalizzata ``cmd`` su tutto il viewset, utilizzando il metodo ``method`` e con i dati specificati in ``data``.
Se la richiesta va a buon fine, il valore restituito dal backend sostituisce nello stato le risorse dell'intero viewset.
..function:: async update(index, data)
Modifica l'oggetto alla posizione ``index`` dell'array :attr:`.state` con i dati specificati in ``data``.
Se la richiesta va a buon fine, la modifica viene anche applicata all'interno di :attr:`.state`
..function:: async destroy(index)
Elimina l'oggetto alla posizione ``index`` dell'array :attr:`.state`.
Se la richiesta va a buon fine, l'oggetto viene eliminato anche da :attr:`.state`.
Esegue l'azione personalizzata ``act`` sull'oggetto alla posizione ``index`` dell'array :attr:`.state`, utilizzando il metodo ``method`` e con i dati specificati in ``data``.
Se la richiesta va a buon fine, il valore restituito dal backend sostituisce l'oggetto utilizzato in :attr:`.state`.
Viene definito un contesto per ogni tipo di risorsa selezionabile nell'interfaccia.
Essi sono, in ordine dal più esterno al più interno:
#.:data:`InstanceContext` (:ref:`Istanza`)
#.:data:`AuthorizationContext` (:ref:`Utente`)
#.:data:`GroupContext` (:ref:`Gruppo di ricerca`)
#.:data:`ProjectContext` (:ref:`Progetto di ricerca`)
#.:data:`NotebookContext` (:ref:`Notebook`)
Contenuto dei contesti
""""""""""""""""""""""
Questi contesti possono avere tre tipi di valori: :data:`undefined` se ci si trova al di fuori del contesto, :data:`null` se non è stato selezionato alcun oggetto oppure **l'oggetto selezionato** se esso esiste.
URL contestuale
^^^^^^^^^^^^^^^
Si è definita la seguente struttura per gli URL del frontend di Sophon, in modo che essi identificassero universalmente una risorsa e che essi fossero human-readable.
..code-block:: text
/i/{ISTANZA}
/l/logged-in
/g/{GROUP_SLUG}
/p/{PROJECT_SLUG}
/n/{NOTEBOOK_SLUG}/
Ad esempio, l'URL per il notebook ``my-first-notebook`` dell'istanza demo di Sophon sarebbe:
..code-block:: text
/i/https:api.prod.sophon.steffo.eu:
/l/logged-in
/g/my-first-group
/p/my-first-project
/n/my-first-notebook/
Parsing degli URL contestuali
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Viene definita una funzione in grado di comprendere gli URL contestuali:
..function:: parsePath(path)
:param path:Il "path" da leggere.
:returns:
Un oggetto con le seguenti chiavi, dette "segmenti di percorso", le quali possono essere :data:`undefined` per indicare che non è stato selezionato un oggetto di quel tipo:
-``instance``: l'URL dell'istanza da utilizzare, con caratteri speciali sostituiti da ``:``
-``loggedIn``: :class:`Boolean`, se :data:`True` l'utente ha effettuato il login (come :ref:`Ospite` o :ref:`Utente`)
-``researchGroup``: lo slug del :ref:`gruppo di ricerca` selezionato
-``researchProject``: lo slug del :ref:`progetto di ricerca` selezionato
-``notebook``: lo slug del :ref:`notebook` selezionato
Ad esempio, l'URL precedente restituirebbe il seguente oggetto se processato:
I valori dei contesti vengono utilizzati per selezionare i componenti da mostrare all'utente nell'interfaccia grafica attraverso i seguenti componenti:
Componente basato su :func:`ResourceRouter` che seleziona automaticamente l'elemento del viewset avente il valore del segmento di percorso ``pathSegment`` alla chiave ``pkKey``.
L'insieme di tutti i contesti è definito come componente :func:`App` nel modulo "principale" ``App.tsx``.
Se ne riassume la struttura in pseudocodice:
..code-block:: html
<InstanceContext>
<InstanceRouter>
unselected:
<InstanceSelect>
selected:
<AuthorizationContext>
<AuthorizationRouter>
unselected:
<UserLogin>
selected:
<GroupContext>
<GroupRouter>
unselected:
<GroupSelect>
selected:
<ProjectContext>
<ProjectRouter>
unselected:
<ProjectSelect>
selected:
<NotebookContext>
<NotebookRouter>
unselected:
<NotebookSelect>
selected:
<NotebookDetails>
Altri contesti
^^^^^^^^^^^^^^
Tema
""""
Il tema dell'istanza è implementato come uno speciale contesto globale :data:`ThemeContext` che riceve i dettagli dell'istanza a cui si è collegati dall':data:`InstanceContext`.
Cache
"""""
Viene salvato l'elenco di tutti i membri dell':ref:`istanza` in uno speciale contesto :data:`CacheContext` in modo da poter risolvere gli id degli utenti al loro username senza dover effettuare ulteriori richieste.
Il modulo frontend è incapsulato in un'immagine `Docker` basata sull'immagine ufficiale `node:16.11.1-bullseye <https://hub.docker.com/_/node>`_.
L'immagine installa le dipendenze del modulo con `Yarn`, per poi eseguire il comando ``yarn run serve``, che avvia la procedura di preparazione della pagina e la rende disponibile su un webserver locale alla porta 3000.
Il file di configurazione abilita i moduli httpd `rewrite`_, `proxy`_, `proxy_wstunnel`_ e `proxy_http`_, impostando quest'ultimo per inoltrare l'header `Host`_ alle pagine verso cui viene effettuato reverse proxying.
Inoltre, nel file di configurazione viene abilitato il ``RewriteEngine``, che viene utilizzato per effettuare reverse proxying secondo le seguenti regole:
#. Tutte le richieste verso ``static.`` prefisso ad :ref:`\`\`APACHE_PROXY_BASE_DOMAIN\`\`` vengono processate direttamente dal webserver, utilizzando i file disponibili nella cartella ``/var/www/html/django-static`` che gli vengono forniti dal volume ``django-static`` del :ref:`modulo backend`.
..code-block:: apacheconf
# If ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST
#. Tutte le richieste verso :ref:`\`\`APACHE_PROXY_BASE_DOMAIN\`\`` senza nessun sottodominio vengono inoltrate al container Docker del :ref:`modulo frontend` utilizzando la risoluzione dei nomi di dominio di Docker Compose.
..code-block:: apacheconf
# If ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST
#. Tutte le richieste verso ``api.`` prefisso ad :ref:`\`\`APACHE_PROXY_BASE_DOMAIN\`\`` vengono inoltrate al container Docker del :ref:`modulo backend` utilizzando la risoluzione dei nomi di dominio di Docker Compose.
..code-block:: apacheconf
# If api. prefixed to ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST
#. Carica in memoria la rubrica dei notebook generata dal :ref:`modulo backend` e disponibile in ``/run/sophon/proxy/proxy.dbm`` attraverso il volume ``proxy-data``, assegnandogli il nome di ``sophonproxy``.
..code-block:: apacheconf
# Create a map between the proxy file generated by Sophon and Apache
Tutte le regole usano il flag ``L`` di ``RewriteRule``, che porta il motore di rewriting a ignorare tutte le regole successive, come il ``return`` di una funzione di un linguaggio di programmazione imperativo.
Il modulo proxy è incapsulato in un'immagine `Docker` basata sull'immagine ufficiale `httpd:2.4 <https://hub.docker.com/_/httpd>`_, che si limita ad applicare la configurazione personalizzata.
Il *modulo Jupyter* consiste in un ambiente `Jupyter <https://jupyter.org/>`_ e `JupyterLab <https://jupyterlab.readthedocs.io/en/stable/>`_ modificato per una migliore integrazione con Sophon, in particolare con il :ref:`modulo frontend` e il :ref:`modulo backend`.
È collocato all'interno del repository in ``/jupyter``.
Sviluppo del tema per Jupyter
=============================
..todo:: Tema personalizzato di Jupyter
Funzionamento del modulo
------------------------
Il modulo è composto da un singolo ``Dockerfile`` che crea un immagine Docker in quattro fasi:
#.**Base**: Parte dall'immagine base ``jupyter/scipy-notebook`` ed altera i label dell'immagine;
..code-block:: docker
FROM jupyter/scipy-notebook AS base
# Set the maintainer label
LABEL maintainer="Stefano Pigozzi <me@steffo.eu>"
#.**Env**: Configura le variabili di ambiente dell'immagine, attivando JupyterLab, configurando il riavvio automatico di Jupyter e permettendo all'utente non-privilegiato di acquisire i privilegi di root attraverso il comando ``sudo``;
..code-block:: docker
FROM base AS env
# Set useful envvars for Sophon notebooks
ENV JUPYTER_ENABLE_LAB=yes
ENV RESTARTABLE=yes
ENV GRANT_SUDO=yes
#.**Extensions**: Installa, abilita e configura le estensioni necessarie all'integrazione con Sophon (attualmente, soltanto il tema JupyterLab Sophon);
..code-block:: docker
FROM env AS extensions
# As the default user...
USER ${NB_UID}
WORKDIR "${HOME}"
# Install the JupyterLab Sophon theme
RUN jupyter labextension install "jupyterlab_theme_sophon"
# Enable the JupyterLab Sophon theme
RUN jupyter labextension enable "jupyterlab_theme_sophon"
# Set the JupyterLab Sophon theme as default
RUN mkdir -p '.jupyter/lab/user-settings/@jupyterlab/apputils-extension/'
RUN echo '{"theme": "JupyterLab Sophon"}' > ".jupyter/lab/user-settings/@jupyterlab/apputils-extension/themes.jupyterlab-settings"
#.**Healthcheck**: Installa ``curl``, e aggiunge all'immagine un controllo per verificarne lo stato di avvio, permettendo al :ref:`modulo backend` di comunicare una richiesta di avvio riuscita solo quando l'intera immagine è avviata
I blocchi di codice all'interno di questa sezione sono stati inseriti manualmente e potrebbero non essere interamente aggiornati alla versione più recente del file.
Si consiglia di consultare il ``Dockerfile`` in caso si necessiti di informazioni aggiornate.