5. Realizzazione di Sophon
Terminato il progetto, si è passati a realizzarne una versione funzionante su calcolatore.
5.1. Realizzazione del modulo backend
Il modulo backend è stato realizzato come un package Python denominato sophon
, e poi containerizzato, creando un'immagine Docker standalone.
Il project Django
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 app di amministrazione personalizzata, il caricamento dinamico delle impostazioni e vari miglioramenti all'autenticazione
App di amministrazione personalizzata
L'app di amministrazione di Django django.contrib.admin
viene personalizzata con la classe SophonAdminSite
, che ne modifica alcuni parametri.
Inoltre, il template predefinito viene sovrascritto dal file templates/admin/base.html
, che sostituisce il foglio di stile con uno personalizzato per Sophon.
- class SophonAdminSite(django.contrib.admin.AdminSite)
- site_header = "Sophon Server Administration"
Il nome della pagina nell'header viene modificato a Sophon Server Administration.
- site_title = "Sophon Server Administration"
Il titolo della pagina nell'header viene anch'esso modificato a Sophon Server Administration.
- site_url = None
Il collegamento View Site viene rimosso, in quanto è possibile accedere all'interfaccia web di Sophon da più domini contemporaneamente.
- index_title = "Resources Administration"
Il titolo dell'indice viene modificato a Resources Administration.
- class SophonAdminConfig(django.contrib.admin.apps.AdminConfig)
- default_site = "sophon.admin.SophonAdminSite"
SophonAdminSite
è selezionata come classe predefinita per il sito di amministrazione.
Caricamento dinamico delle impostazioni
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 molto più semplice.
try:
DATABASE_ENGINE = os.environ["DJANGO_DATABASE_ENGINE"]
except KeyError:
log.warning("DJANGO_DATABASE_ENGINE was not set, defaulting to PostgreSQL")
DATABASE_ENGINE = "django.db.backends.postgresql"
log.debug(f"{DATABASE_ENGINE = }")
Inoltre, viene configurato il modulo logging
per emettere testo colorato di più facile comprensione usando il package coloredlogs
.
"detail": {
"()": coloredlogs.ColoredFormatter,
"format": "{asctime:>19} | {name:<24} | {levelname:>8} | {message}",
"style": "{",
}
Una lista di tutte le variabili di ambiente di configurazione è riportata nel capitolo Installazione di Sophon.
Miglioramenti all'autenticazione
La classe rest_framework.authentication.TokenAuthentication
viene modificata per ottenere un comportamento conforme allo standard della Bearer authentication.
- class BearerTokenAuthentication(rest_framework.authentication.TokenAuthentication)
- keyword = "Bearer"
Si configura
rest_framework
per accettare header di autenticazione nella formaBearer <token>
, invece che il default direst_framework
Token <token>
.
La view rest_framework.authtoken.views.ObtainAuthToken
viene estesa per aggiungere dati alla risposta di autenticazione riuscita.
L'app Sophon Core
L'app sophon.core
è l'app principale del progetto, e non può essere disattivata, in quanto dipendenza obbligatoria di tutte le altre app.
Aggiunta di un nuovo comando di gestione
Per permettere l'integrazione la creazione automatica del primo superutente quando Sophon viene eseguito da Docker, viene introdotto dall'app il comando di gestione initsuperuser
.
- class Command
Questo comando crea automaticamente un superutente con le credenziali specificate in
DJANGO_SU_USERNAME
,DJANGO_SU_EMAIL
eDJANGO_SU_PASSWORD
.
Modello base astratto
Viene estesa la classe astratta django.db.models.Model
con funzioni per stabilire il livello di accesso di un utente all'oggetto e per generare automaticamente i rest_framework.serializers.ModelSerializer
in base ad esso.
- class SophonModel(django.db.models.Model)
- abstract can_edit(self, user: django.contrib.auth.models.User) bool
Controlla se un utente può modificare l'oggetto attuale.
- abstract can_admin(self, user: django.contrib.auth.models.User) bool
Controlla se un utente può amministrare l'oggetto attuale.
- classmethod get_fields(cls) set[str]
- Ritorna
il
set
di nomi di campi che devono essere mostrati quando viene richiesto l'oggetto attraverso l'API.
- classmethod get_editable_fields(cls) set[str]
- Ritorna
il
set
di nomi di campi di cui deve essere permessa la modifica se l'utente può modificare (can_edit()
) l'oggetto.
- classmethod get_administrable_fields(cls) set[str]
- Ritorna
il
set
di nomi di campi di cui deve essere permessa la modifica se l'utente può amministrare (can_admin()
) l'oggetto.
Modello di autorizzazione astratto
Viene definito un nuovo modello astratto, basato su SophonModel
, che permette di determinare i permessi dell'utente in base alla sua appartenenza al gruppo a cui è collegato l'oggetto implementatore.
- class SophonGroupModel(SophonModel)
- abstract get_group(self) ResearchGroup
- Ritorna
Il gruppo a cui appartiene l'oggetto.
- classmethod get_access_to_edit(cls) sophon.core.enums.SophonGroupAccess
- Ritorna
Il livello di autorità all'interno del gruppo necessario per modificare l'oggetto.
- classmethod get_access_to_admin(cls) sophon.core.enums.SophonGroupAccess
- Ritorna
Il livello di autorità all'interno del gruppo necessario per amministrare l'oggetto.
- get_access_serializer(self, user: User) typing.Type[rest_framework.serializers.ModelSerializer]
- Ritorna
Restituisce il
rest_framework.serializers.ModelSerializer
adeguato al livello di autorità dell'utente.
- class sophon.core.enums.SophonGroupAccess(enum.IntEnum)
Enumerazione che stabilisce il livello di autorità che un utente può avere all'interno di un gruppo di ricerca.
- NONE = 0
Ospite.
- REGISTERED = 10
Utente registrato.
- MEMBER = 50
Membro del gruppo al quale appartiene l'oggetto.
- OWNER = 100
Creatore del gruppo al quale appartiene l'oggetto.
- SUPERUSER = 200
Superutente con privilegi universali.
Modello dei dettagli dell'istanza
Viene creato il modello che rappresenta i dettagli dell'istanza di Sophon.
- class SophonInstanceDetails(SophonModel)
- id: IntegerField [1]
Impostando
1
come unica scelta per il campo della chiave primariaid
, 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
.
- name: CharField
Il titolo dell'istanza Sophon.
- description: TextField
La descrizione dell'istanza Sophon, da visualizzare in un riquadro "A proposito dell'istanza".
- theme: CharField ["sophon", "paper", "royalblue", "hacker", "amber"]
Il tema Bluelib dell'istanza.
- version: str
- Ritorna
La versione installata del pacchetto
sophon
.
Modello del gruppo di ricerca
Viene creato il modello che rappresenta un gruppo di ricerca.
- class ResearchGroup(SophonGroupModel)
- slug: SlugField
L'identificatore del gruppo di ricerca, usato nei percorsi dell'API.
- name: CharField
Il nome del gruppo di ricerca.
- description: TextField
La descrizione del gruppo di ricerca, da visualizzare in un riquadro "A proposito del gruppo".
- members: ManyToManyField → django.contrib.auth.models.User
Elenco dei membri del gruppo. L'utente
owner
è ignorato, in quanto è considerato sempre parte del gruppo.
- owner: ForeignKey → django.contrib.auth.models.User
Il creatore e proprietario del gruppo, con privilegi amministrativi.
- access: CharField ["MANUAL", "OPEN"]
La modalità di accesso del gruppo.
Estensione ai permessi di Django
I permessi di rest_framework
vengono estesi con due nuove classi che utilizzano il modello di autorizzazione astratto precedentemente definito.
- class Edit(rest_framework.permissions.BasePermission)
Consente l'interazione solo agli utenti che possono modificare l'oggetto.
- class Admin(rest_framework.permissions.BasePermission)
Consente l'interazione solo agli utenti che possono amministrare l'oggetto.
Viewset astratti
Vengono definiti tre viewset in grado di utilizzare i metodi aggiunti dalle classi astratte models.SophonModel
e models.SophonGroupModel
.
- class ReadSophonViewSet(rest_framework.viewsets.ReadOnlyModelViewSet, metaclass=abc.ABCMeta)
Classe astratta che estende la classe base
rest_framework.viewsets.ReadOnlyModelViewSet
con metodi di utilità mancanti nell'implementazione originale, allacciandola inoltre amodels.SophonGroupModel
.- abstract get_queryset(self) QuerySet
Imposta come astratto (e quindi obbligatorio) il metodo
rest_framework.viewsets.ReadOnlyModelViewSet.get_queryset()
.
- property permission_classes(self)
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()
.
- get_permission_classes(self) typing.Collection[typing.Type[permissions.BasePermission]]
Funzione che permette la selezione dei permessi necessari per effetuare una determinata richiesta al momento di ricezione di quest'ultima.
Utile per le classi che erediteranno da questa.
- get_serializer_class(self) typing.Type[Serializer]
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 (azionemetadata
);il serializzatore ottenuto da
models.SophonGroupModel.get_access_serializer()
per la visualizzazione dettagliata (azioneretrieve
), la modifica (azioniupdate
epartial_update
) e l'eliminazione (azionedestroy
) di un singolo oggetto;il serializzatore ottenuto da
get_custom_serializer_classes()
per le azioni personalizzate.
Vedi anche
- get_custom_serializer_classes(self) t.Type[Serializer]
Permette alle classi che ereditano da questa di selezionare quale
rest_framework.serializers.Serializer
utilizzare per le azioni personalizzate.
- class WriteSophonViewSet(rest_framework.viewsets.ModelViewSet, ReadSophonViewSet, metaclass=abc.ABCMeta)
Classe astratta che estende la classe base
ReadSophonViewSet
aggiungendoci i metodi direst_framework.viewsets.ModelViewSet
che effettuano modifiche sugli oggetti.Depreca i metodi
perform_*
direst_framework
, introducendone versioni estese con una signature diversa dal nome dihook_*
.- perform_create(self, serializer)
Deprecato dalla versione 0.1.
Metodo di
rest_framework
rimosso da Sophon.
- perform_update(self, serializer)
Deprecato dalla versione 0.1.
Metodo di
rest_framework
rimosso da Sophon.
- perform_destroy(self, serializer)
Deprecato dalla versione 0.1.
Metodo di
rest_framework
rimosso da Sophon.
- hook_create(self, serializer) dict[str, typing.Any]
Funzione chiamata durante l'esecuzione dell'azione di creazione oggetto
create
.- Parametri
serializer -- Il
Serializer
già "riempito" contenente i dati dell'oggetto che sta per essere creato.- Solleva
HTTPException -- È possibile interrompere la creazione dell'oggetto con uno specifico codice errore sollevando una
HTTPException
all'interno della funzione.- Ritorna
Un
dict
da unire a quello delSerializer
per formare l'oggetto da creare.
- hook_update(self, serializer) dict[str, t.Any]
Funzione chiamata durante l'esecuzione delle azioni di modifica oggetto
update
epartial_update
.- Parametri
serializer -- Il
Serializer
già "riempito" contenente i dati dell'oggetto che sta per essere modificato.- Solleva
HTTPException -- È possibile interrompere la creazione dell'oggetto con uno specifico codice errore sollevando una
HTTPException
all'interno della funzione.- Ritorna
Un
dict
da unire a quello delSerializer
per formare l'oggetto da modificare.
- hook_destroy(self, serializer) dict[str, typing.Any]
Funzione chiamata durante l'esecuzione dell'azione di eliminazione oggetto
destroy
.- Solleva
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_*
diWriteSophonViewSet
per interrompere l'azione in corso senza applicare le modifiche.- status: int
Permette di specificare il codice errore con cui rispondere alla richiesta interrotta.
- class SophonGroupViewSet(WriteSophonViewSet, metaclass=abc.ABCMeta)
Classe astratta che estende la classe base
WriteSophonViewSet
estendendo glihook_*
con verifiche dei permessi dell'utente che tenta di effettuare l'azione.- abstract get_group_from_serializer(self, serializer) models.ResearchGroup
Metodo necessario a trovare il gruppo a cui apparterrà un oggetto prima che il suo serializzatore venga elaborato.
- Parametri
serializer -- Il
Serializer
già "riempito" contenente i dati dell'oggetto.
Viewset concreti
Vengono poi definiti tre viewset e una view che permettono interazioni tra l'utente e i modelli definiti nell'app.
- class UsersByIdViewSet(ReadSophonViewSet)
Viewset in sola lettura che permette di recuperare gli utenti dell'istanza partendo dal loro
id
.Accessibile all'URL
/api/core/users/by-id/ID/
.
- class UsersByUsernameViewSet(ReadSophonViewSet)
Viewset in sola lettura che permette di recuperare gli utenti dell'istanza partendo dal loro
username
.Accessibile all'URL
/api/core/users/by-username/USERNAME/
.
- class ResearchGroupViewSet(WriteSophonViewSet)
Viewset in lettura e scrittura che permette di interagire con i gruppi di ricerca.
Accessibile all'URL
/api/core/groups/GROUP_SLUG/
.
- class SophonInstanceDetailsView(APIView)
View che restituisce il valore attuale dell'unico oggetto
models.SophonInstanceDetails
.Accessibile tramite richieste
GET
all'URL/api/core/instance/
.
Pagina di amministrazione
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.
Testing in Sophon Core
Per verificare che i modelli e viewset funzionassero correttamente e non avessero problemi di 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.
Nota
I nomi delle funzioni usano nomi con capitalizzazione inconsistente, in quanto lo stesso modulo unittest
non rispetta lo stile suggerito in PEP 8.
- class BetterAPITestCase(APITestCase)
- as_user(self, username: str, password: str = None) typing.ContextManager[None]
Context manager che permette di effettuare richieste all'API come uno specifico utente, effettuando il logout quando sono state effettuate le richieste necessarie.
- class ReadSophonTestCase(BetterAPITestCase, metaclass=abc.ABCMeta)
Classe astratta che implementa metodi per testare rapidamente le azioni di un
views.ReadSophonViewSet
.- classmethod get_basename(cls) str
Metodo astratto che deve restituire il basename del viewset da testare.
- classmethod get_url(cls, kind: str, *args, **kwargs) str
Metodo utilizzato dal test case per trovare gli URL ai quali possono essere effettuate le varie azioni.
I seguenti metodi permettono di effettuare azioni sul viewset:
- list(self) rest_framework.response.Response
- retrieve(self, pk) rest_framework.response.Response
- custom_detail(self, method: str, action: str, pk, data: dict = None) rest_framework.response.Response
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
)
- class WriteSophonTestCase(ReadSophonTestCase, metaclass=abc.ABCMeta)
Classe astratta che estende
ReadSophonTestCase
con le azioni di unviews.WriteSophonViewSet
.- create(self, data) rest_framework.response.Response
- update(self, pk, data) rest_framework.response.Response
- destroy(self, pk) rest_framework.response.Response
Test case concreti
Vengono testate tutte le view dell'app tramite BetterAPITestCase
e tutti i viewset dell'app tramite ReadSophonTestCase
e WriteSophonTestCase
.
- class UsersByIdTestCase(ReadSophonTestCase)
- class UsersByUsernameTestCase(ReadSophonTestCase)
- class ResearchGroupTestCase(WriteSophonTestCase)
- class SophonInstanceDetailsTestCase(BetterAPITestCase)
L'app Sophon Projects
L'app sophon.projects
è un app secondaria che dipende da sophon.core
che introduce in Sophon il concetto di progetto di ricerca.
Nota
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 non prevede questa funzionalità e si aspetta che i percorsi API relativi all'app siano disponibili.
Inoltre, rimuovendo l'app sophon.projects
non sarà più possibile usare l'app sophon.notebooks
, in quanto dipende da essa.
Modello del progetto di ricerca
Viene introdotto un modello concreto che rappresenta un progetto di ricerca.
- class ResearchProject(SophonGroupModel)
- slug: SlugField
L'identificatore del progetto di ricerca, usato nei percorsi dell'API.
- group: ForeignKey → sophon.core.models.ResearchGroup
Lo
slug
del gruppo di ricerca al quale appartiene il progetto.
- name: CharField
Il nome completo del progetto di ricerca.
- description: TextField
La descrizione del progetto di ricerca, da visualizzare in un riquadro "A proposito del progetto".
- visibility: CharField ["PUBLIC", "INTERNAL", "PRIVATE"]
Viewset del gruppo di ricerca
Da una base comune, vengono creati due viewset per interagire con i progetti di ricerca.
- class ResearchProjectViewSet(SophonGroupViewSet, metaclass=abc.ABCMeta)
Classe astratta che effettua l'override di
get_group_from_serializer()
per entrambi i viewset che seguono.
- class ResearchProjectsBySlugViewSet(ResearchProjectViewSet)
Viewset in lettura e scrittura che permette di interagire con tutti i progetti di ricerca a cui l'utente loggato ha accesso.
Accessibile all'URL
/api/projects/by-slug/PROJECT_SLUG/
.
- class ResearchProjectsByGroupViewSet(ResearchProjectViewSet)
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.
Accessibile all'URL
/api/projects/by-group/GROUP_SLUG/PROJECT_SLUG/
.
Amministrazione del gruppo di ricerca
Il modello models.ResearchProject
viene registrato nella pagina di amministrazione attraverso la seguente classe:
- class ResearchProjectAdmin(sophon.core.admin.SophonAdmin)
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
L'app sophon.notebooks
è un app secondaria che dipende da sophon.projects
che introduce in Sophon il concetto di notebook
.
Nota
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
non prevede questa funzionalità e si aspetta che i percorsi API relativi all'app siano disponibili.
Funzionamento di un notebook
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
.
Modalità sviluppo
Per facilitare lo sviluppo di Sophon, sono state realizzate due modalità di operazione di quest'ultimo.
Nella prima, la modalità sviluppo, il
modulo proxy
non è in esecuzione, ed è possibile collegarsi direttamente ai container all'indirizzo IP locale127.0.0.1
.Il
modulo frontend
non supporta questa modalità, in quanto intesa solamente per lo sviluppo del modulo backend.Nella seconda, la modalità produzione, il
modulo proxy
è in esecuzione all'interno di un container Docker, e si collega aimoduli Jupyter
attraverso i relativi network Docker tramite unarubrica
.
Gestione della rubrica del proxy
Viene creata una classe per la gestione della rubrica del proxy, utilizzando il modulo dbm.gnu
, supportato da HTTPd.
La rubrica mappa gli URL pubblici dei notebook a URL privati relativi al modulo proxy
, in modo da effettuare reverse proxying dinamico.
Assegnazione porta effimera
In modalità sviluppo, è necessario trovare una porta libera a cui rendere accessibile i container Docker dei notebook.
- get_ephemeral_port() int
Questa funzione apre e chiude immediatamente un
socket.socket
all'indirizzolocalhost:0
in modo da ricevere dal sistema operativo un numero di porta sicuramente libero.
Connessione al daemon Docker
Per facilitare l'utilizzo del daemon Docker per la gestione dei container dei notebook, viene utilizzato il modulo docker
.
- get_docker_client() docker.DockerClient
Funzione che crea un client Docker con le variabili di ambiente del modulo.
- client: docker.DockerClient = lazy_object_proxy.Proxy(get_docker_client)
Viene creato un client Docker globale con inizializzazione lazy al fine di non tentare connessioni (lente!) al daemon quando non sono necessarie.
Controllo dello stato di salute
Il modulo docker
viene esteso implementando supporto per l'istruzione HEALTHCHECK
dei Dockerfile
.
- class HealthState(enum.IntEnum)
Enumerazione che elenca gli stati possibili in cui può essere la salute di un container.
- UNDEFINED = -2
Il
Dockerfile
non ha unHEALTHCHECK
definito.
- STARTING = -1
Il container Docker non mai completato con successo un
HEALTHCHECK
.
- HEALTHY = 0
Il container Docker ha completato con successo l'ultimo
HEALTHCHECK
e quindi sta funzionando correttamente.
- UNHEALTHY = 1
Il container Docker ha fallito l'ultimo
HEALTHCHECK
.
- get_health(container: docker.models.containers.Container) HealthState
Funzione che utilizza l'API a basso livello del client Docker per recuperare l'
HealthState
dei container.
- sleep_until_container_has_started(container: docker.models.containers.Container) HealthState
Funzione bloccante che restituisce solo quando lo stato del container specificato non è
HealthState.STARTING
.Pericolo
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.
Generazione di token sicuri
Per rendere l'interfaccia grafica più intuitiva
, si è scelto di rendere trasparente all'utente il meccanismo di autenticazione a JupyterLab.
Pertanto, si è verificata la necessità di generare token crittograficamente sicuri da richiedere per l'accesso a JupyterLab.
- generate_secure_token() str
Funzione che utilizza
secrets.token_urlsafe
per generare un token valido e crittograficamente sicuro.
Modello dei notebook
Viene definito il modello rappresentante un notebook
.
- class Notebook(SophonGroupModel)
- slug: SlugField
Lo slug dei notebook prevede ulteriori restrizioni oltre a quelle previste dallo
django.db.models.SlugField
:non può essere uno dei seguenti valori:
api
,static
,proxy
,backend
,frontend
,src
;non può iniziare o finire con un trattino
-
.
- project: ForeignKey → sophon.projects.models.ResearchProject
Il
progetto
che include questo notebook.
- name: CharField
Il nome del notebook.
- locked_by: ForeignKey → django.contrib.auth.models.User
L'
utente
che ha richiesto il blocco del notebook, oNone
in caso il notebook non sia bloccato.
- container_image: CharField ["ghcr.io/steffo99/sophon-jupyter"]
Campo che specifica l'immagine che il client Docker dovrà avviare per questo notebook.
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.
Nota
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:PROTOCOLLO://immagine:8888/lab?token=TOKEN
ePROTOCOLLO://immagine:8888/tree?token=TOKEN
.
- jupyter_token: CharField
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.
- container_id: CharField
L'id assegnato dal daemon Docker al container di questo oggetto.
Se il notebook non è avviato, questo attributo varrà
None
.
- port: IntegerField
La porta TCP locale assegnata al container Docker dell'oggetto nel caso in cui Sophon sia avviato in
modalità sviluppo
.
- internal_url: CharField
L'URL a cui è accessibile il container Docker dell'oggetto nel caso in cui Sophon non sia avviato in
modalità sviluppo
.
- property log(self) logging.Logger
Viene creato un
logging.Logger
per ogni oggetto della classe, in modo da facilitare il debug relativo ad uno specifico notebook.Il nome del logger ha la forma
sophon.notebooks.models.Notebook.NOTEBOOK_SLUG
.
- sync_container(self) t.Optional[docker.models.containers.Container]
Sincronizza lo stato dell'oggetto nel database con lo stato del container Docker nel sistema.
- create_container(self) docker.models.containers.Container
Crea e configura un container Docker per l'oggetto, con l'immagine specificata in
container_image
.
- start(self) None
Tenta di creare e avviare un container Docker per l'oggetto, bloccando fino a quando esso non sarà avviato con
sleep_until_container_has_started
.
Viewset dei notebook
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.
- class NotebooksViewSet(SophonGroupViewSet, metaclass=abc.ABCMeta)
Classe astratta che effettua l'override di
get_group_from_serializer
e definisce cinque azioni personalizzate per l'interazione con il notebook.- sync(self, request: Request, **kwargs) Response
Azione personalizzata che sincronizza lo stato dell'oggetto dell'API con quello del daemon Docker.
- start(self, request: Request, **kwargs) Response
Azione personalizzata che avvia il notebook con
models.Notebook.start
.
- stop(self, request: Request, **kwargs) Response
Azione personalizzata che arresta il notebook con
models.Notebook.stop
.
- lock(self, request: Request, **kwargs) Response
Azione personalizzata che blocca il notebook impostando il campo
models.Notebook.locked_by
all'utente che ha effettuato la richiesta.
- class NotebooksBySlugViewSet(NotebooksViewSet)
Viewset in lettura e scrittura che permette di interagire con tutti i notebook a cui l'utente loggato ha accesso.
Accessibile all'URL
/api/notebooks/by-slug/NOTEBOOK_SLUG/
.
- class NotebooksByProjectViewSet(NotebooksViewSet)
Viewset in lettura e scrittura che permette di interagire con i notebook a cui l'utente loggato ha accesso, filtrati per il progetto di appartenenza.
Accessibile all'URL
/api/notebooks/by-project/PROJECT_SLUG/NOTEBOOK_SLUG/
.
Containerizzazione del modulo backend
Il modulo backend è incapsulato in un'immagine Docker basata sull'immagine ufficiale python:3.9.7-bullseye.
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
, per poi avviare il progetto Django attraverso gunicorn
sulla porta 8000.
poetry run python -O ./manage.py migrate --no-input
poetry run python -O ./manage.py collectstatic --no-input
poetry run python -O ./manage.py initsuperuser
poetry run python -O -m gunicorn sophon.wsgi:application --workers=4 --bind=0.0.0.0:8000 --timeout 180
5.2. Realizzazione del modulo frontend
Il modulo frontend è stato realizzato come un package Node.js
denominato @steffo/sophon-frontend
, e poi containerizzato
, creando un'immagine Docker standalone, esattamente come per il modulo backend
.
Struttura delle directory
Le directory di @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
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 Modulo backend.
- src/utils
Contiene varie funzioni di utility.
- public
Contiene i file statici da servire assieme all'app.
Comunicazione con il backend
Sono state sviluppate alcune funzioni di utilità per facilitare la comunicazione con il modulo backend
.
Axios
Per effettuare richieste all'API web, si è deciso di utilizzare la libreria axios
, in quanto permette di creare dei "client" personalizzabili con varie proprietà.
In particolare, si è scelto di effettuare un fork della stessa, integrando anticipatamente una proposta di funzionalità che permette alle richieste di essere interrotte attraverso degli 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 (InstanceContext
), viene creato un client dal seguente hook:
- 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 (AuthorizationContext
), viene creato invece un client dal seguente hook:
- useAuthorizedAxios(config = {})
Questo hook specifica il valore dell'header
Authorization
da inviare in tutte le richieste effettuate aBearer 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.
- useViewSet(baseRoute) → viewset()
Questo hook implementa tutte le azioni
rest_framework
di un viewset in lettura e scrittura.Richiede di essere chiamato all'interno di un
AuthorizationContext
.- viewset.list(config = {})
Funzione asincrona, che restituisce una
Promise()
.Richiede la lista di tutte le risorse del viewset.
- viewset.retrieve(pk, config = {})
Funzione asincrona, che restituisce una
Promise()
.Richiede i dettagli di una specifica risorsa del viewset.
- viewset.create(config)
Funzione asincrona, che restituisce una
Promise()
.Crea una nuova risorsa nel viewset.
- viewset.update(pk, config)
Funzione asincrona, che restituisce una
Promise()
.Aggiorna una specifica risorsa nel viewset.
- viewset.destroy(pk, config)
Funzione asincrona, che restituisce una
Promise()
.Elimina una specifica risorsa dal viewset.
Viene inoltre fornito supporto per le azioni personalizzate.
- viewset.command(config)
Funzione asincrona, che restituisce una
Promise()
.Permette azioni personalizzate su tutto il viewset.
- viewset.action(config)
Funzione asincrona, che restituisce una
Promise()
.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.
- useManagedViewSet(baseRoute, pkKey, refreshOnMount) → managed()
- managed.viewset
Il viewset restituito da
useViewSet()
, utilizzato come interfaccia di basso livello per effettuare azioni.
- managed.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
ManagedResource()
, create usando wrapper diupdate()
,destroy()
eaction()
, che permettono di modificare direttamente l'oggetto senza preoccuparsi dell'indice a cui si trova nell'array.
- managed.refresh()
Funzione asincrona, che restituisce una
Promise()
.Ricarica gli oggetti del viewset.
Viene chiamata automaticamente al primo render se
refreshOnMount
èTrue
.
- managed.create(data)
Funzione asincrona, che restituisce una
Promise()
.Crea un nuovo oggetto nel viewset con i dati specificati come argomento, e lo aggiunge allo stato se la richiesta va a buon fine.
- managed.command(method, cmd, data)
Funzione asincrona, che restituisce una
Promise()
.Esegue l'azione personalizzata
cmd
su tutto il viewset, utilizzando il metodomethod
e con i dati specificati indata
.Se la richiesta va a buon fine, il valore restituito dal backend sostituisce nello stato le risorse dell'intero viewset.
- managed.update(index, data)
Funzione asincrona, che restituisce una
Promise()
.Modifica l'oggetto alla posizione
index
dell'arraystate
con i dati specificati indata
.Se la richiesta va a buon fine, la modifica viene anche applicata all'interno di
state
- managed.destroy(index)
Funzione asincrona, che restituisce una
Promise()
.Elimina l'oggetto alla posizione
index
dell'arraystate
.Se la richiesta va a buon fine, l'oggetto viene eliminato anche da
state
.
- managed.action(index, method, act, data)
Funzione asincrona, che restituisce una
Promise()
.Esegue l'azione personalizzata
act
sull'oggetto alla posizioneindex
dell'arraystate
, utilizzando il metodomethod
e con i dati specificati indata
.Se la richiesta va a buon fine, il valore restituito dal backend sostituisce l'oggetto utilizzato in
state
.
Contesti innestati
Per minimizzare i re-render, l'applicazione è organizzata a "contesti innestati".
I contesti
Viene definito un contesto per ogni tipo di risorsa selezionabile nell'interfaccia.
Essi sono, in ordine dal più esterno al più interno:
InstanceContext
(Istanza
)AuthorizationContext
(Utente
)GroupContext
(Gruppo di ricerca
)ProjectContext
(Progetto di ricerca
)NotebookContext
(Notebook
)
Contenuto dei contesti
Questi contesti possono avere tre tipi di valori: undefined
se ci si trova al di fuori del contesto, null
se non è stato selezionato alcun oggetto oppure l'oggetto selezionato se esso esiste.
Segmenti di URL
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.
/i/{ISTANZA}
/l/logged-in
/g/{GROUP_SLUG}
/p/{PROJECT_SLUG}
/n/{NOTEBOOK_SLUG}
/
Ciascuna riga nel listato sopra rappresenta un segmento di URL.
Parsing dei segmenti di URL
Il parsing di questi segmenti viene effettuato dalla seguente funzione:
- parsePathSegment({path, parsed, regex, key, next}) → ParsedPath()
Funzione ricorsiva per la cattura di un segmento, che riempie ad ogni chiamata una chiave dell'oggetto
ParsedPath
.- Parametri
path -- La stringa del percorso ancora da parsare.
parsed -- L'oggetto
ParsedPath
riempito con i segmenti trovati fino ad ora.regex -- Una regular expression usata per catturare un segmento. Il primo gruppo di cattura sarà il valore che verrà mantenuto e inserito nel
ParsedPath
.key -- La chiave a cui inserire il valore catturato all'interno del
ParsedPath
.next -- Callback della prossima funzione da chiamare.
Un esempio di URL per il notebook my-first-notebook
all'interno della istanza demo di Sophon potrebbe essere:
/i/https:api.prod.sophon.steffo.eu:
/l/logged-in
/g/my-first-group
/p/my-first-project
/n/my-first-notebook
/
Componenti contestuali
Per ciascun contesto sono stati realizzati vari componenti.
I più significativi comuni a tutti i contesti sono i ResourcePanel
e le ListBox
.
- ResourcePanel({...})
Panello che rappresenta un'
entità di Sophon
, diviso in quattro parti:icona (a sinistra)
nome della risorsa (a destra dell'icona)
bottoni (a destra)
testo (a sinistra dei bottoni)
- ListBox({...})
Riquadro che mostra le risorse di un
useManagedViewSet
raffigurandole come tantiResourcePanel
.
Routing basato sui contesti
I valori dei contesti vengono utilizzati per selezionare i componenti da mostrare all'utente nell'interfaccia grafica attraverso i seguenti componenti:
- ResourceRouter({selection, unselectedRoute, selectedRoute})
Componente che sceglie se renderizzare
unselectedRoute
oselectedRoute
in base alla nullità o non-nullità diselection
.
- ViewSetRouter({viewSet, unselectedRoute, selectedRoute, pathSegment, pkKey})
Componente basato su
ResourceRouter()
che seleziona automaticamente l'elemento del viewset avente il valore del segmento di percorsopathSegment
alla chiavepkKey
.
Ad esempio, ViewSetRouter()
viene esteso specificatamente per il contesto del gruppo, creando il seguente componente.
- GroupRouter({...props})
Implementato come:
<ViewSetRouter {...props} viewSet={useManagedViewSet<SophonResearchGroup>("/api/core/groups/", "slug")} pathSegment={"researchGroup"} pkKey={"slug"} />
Albero completo dei contesti
L'insieme di tutti i contesti è definito come componente App()
nel modulo "principale" App.tsx
.
Se ne riassume la struttura in pseudocodice:
<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
All'interno di Sophon sono presenti anche altri due contesti, utilizzati a scopo di semplificare e ottimizzare il codice.
Tema
Il tema dell'istanza è implementato come uno speciale contesto globale ThemeContext
che riceve i dettagli dell'istanza a cui si è collegati dall'InstanceContext
.
Ciò permette di sincronizzare il tema della webapp con quello dell'istanza di Sophon selezionata.
Cache
Viene salvato l'elenco di tutti i membri dell'istanza
in uno speciale contesto CacheContext
in modo da poter risolvere gli id degli utenti al loro username senza dover effettuare ulteriori richieste.
Questa funzionalità al momento viene usata per risolvere i nomi dei membri in un gruppo e il nome dell'utente che ha bloccato un notebook: in entrambi i casi, vengono restituiti dal modulo backend
solamente gli ID numerici dei relativi utenti, pertanto è necessario risolverli attraverso il contesto cache.
Containerizzazione del modulo frontend
Il modulo frontend è incapsulato in un'immagine Docker basata sull'immagine ufficiale node:16.11.1-bullseye.
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.
5.3. Realizzazione del modulo proxy
Il modulo proxy consiste in un file di configurazione di Apache HTTP Server
.
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 adAPACHE_PROXY_BASE_DOMAIN
vengono processate direttamente dal webserver, utilizzando i file disponibili nella cartella/var/www/html/django-static
che gli vengono forniti dal volumedjango-static
del Modulo backend.# If ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST RewriteCond "static.%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) \1$" [NC] # Process the request yourself RewriteRule ".?" - [L]
Tutte le richieste verso
APACHE_PROXY_BASE_DOMAIN
senza nessun sottodominio vengono inoltrate al container Docker del Modulo frontend utilizzando la risoluzione dei nomi di dominio di Docker Compose.# If ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST RewriteCond "%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) \1$" [NC] # Capture ENV:SOPHON_FRONTEND_NAME for substitution in the rewriterule RewriteCond "%{ENV:SOPHON_FRONTEND_NAME}" "^(.+)$" [NC] # Forward to the frontend RewriteRule "/(.*)" "http://%1/$1" [P,L]
Tutte le richieste verso
api.
prefisso adAPACHE_PROXY_BASE_DOMAIN
vengono inoltrate al container Docker del Modulo backend utilizzando la risoluzione dei nomi di dominio di Docker Compose.# If api. prefixed to ENV:APACHE_PROXY_BASE_DOMAIN equals HTTP_HOST RewriteCond "api.%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) \1$" [NC] # Capture ENV:SOPHON_BACKEND_NAME for substitution in the rewriterule RewriteCond "%{ENV:SOPHON_BACKEND_NAME}" "^(.+)$" [NC] # Forward to the backend RewriteRule "/(.*)" "http://%1/$1" [P,L]
Carica in memoria la rubrica dei notebook generata dal Modulo backend e disponibile in
/run/sophon/proxy/proxy.dbm
attraverso il volumeproxy-data
, assegnandogli il nome disophonproxy
.# Create a map between the proxy file generated by Sophon and Apache RewriteMap "sophonproxy" "dbm=gdbm:/run/sophon/proxy/proxy.dbm"
Effettua il proxying dei websocket verso i notebook mappati dalla rubrica
sophonproxy
.# If this is any other subdomain of ENV:APACHE_PROXY_BASE_DOMAIN RewriteCond ".%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) [^ ]+\1$" [NC] # If this is a websocket connection RewriteCond "%{HTTP:Connection}" "Upgrade" [NC] RewriteCond "%{HTTP:Upgrade}" "websocket" [NC] # Forward to the notebook RewriteRule "/(.*)" "ws://${sophonproxy:%{HTTP_HOST}}/$1" [P,L]
Effettua il proxying delle richieste "normali" verso i notebook mappati dalla rubrica
sophonproxy
.# If this is any other subdomain of ENV:APACHE_PROXY_BASE_DOMAIN RewriteCond ".%{ENV:APACHE_PROXY_BASE_DOMAIN} %{HTTP_HOST}" "^([^ ]+) [^ ]+\1$" [NC] # Forward to the notebook RewriteRule "/(.*)" "http://${sophonproxy:%{HTTP_HOST}}/$1" [P,L]
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.
Containerizzazione del modulo proxy
Il modulo proxy è incapsulato in un'immagine Docker basata sull'immagine ufficiale httpd:2.4, che si limita ad applicare la configurazione personalizzata.
5.4. Realizzazione del modulo Jupyter
Il modulo Jupyter consiste in un ambiente Jupyter e JupyterLab modificato per una migliore integrazione con Sophon, in particolare con il Modulo frontend e il Modulo backend.
È collocato all'interno del repository in /jupyter
.
Sviluppo del tema per Jupyter
Per rendere l'interfaccia grafica più consistente ed user-friendly, è stato sviluppato un tema colori personalizzato per JupyterLab
.
È stato creato partendo dal template jupyterlab/theme-cookiecutter, e in esso sono state modificati le variabili di stile (contenute nel file style/variables.css
) usando i colori del tema "The Sophonity" di Bluelib
.
È stato poi pubblicato sull'PyPI e su npm
, permettendone l'uso a tutti gli utenti di JupyterLab.
Nota
Per facilitarne la distribuzione e il riutilizzo anche esternamente a Sophon, il tema è stato creato in un repository Git
esterno a quello del progetto.
Estensione del container Docker di Jupyter
Il Dockerfile
del modulo ne crea un immagine Docker
in quattro fasi:
Base: Parte dall'immagine base
jupyter/scipy-notebook
e ne altera i label.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, la collaborazione real time e permettendo all'utente non-privilegiato di acquisire i privilegi di root attraverso il comando
sudo
.FROM base AS env # Set useful envvars for Sophon notebooks ENV JUPYTER_ENABLE_LAB=yes ENV RESTARTABLE=yes ENV GRANT_SUDO=yes # Enable real time collaboration CMD ["start-notebook.sh", "--collaborative"]
Extensions: Installa, abilita e configura le estensioni necessarie all'integrazione con Sophon (attualmente, soltanto il tema JupyterLab Sophon).
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, uno strumento in grado di effettuare richieste HTTP (HyperText Transfer Protocol da linea di comando, e configura la verifica dello
stato di salute
dell'immagine, al fine di comunicare almodulo backend
il risultato di una richiesta di avvio.FROM extensions AS healthcheck # As root... USER root # Install curl RUN apt-get update RUN apt-get install -y curl # Use curl to check the health status HEALTHCHECK --start-period=5s --timeout=5s --interval=10s CMD ["curl", "--output", "/dev/null", "http://localhost:8888"] # We probably should go back to the default user USER ${NB_UID}
5.5. Automazione di sviluppo
Al fine di snellire lo sviluppo del software, è stato configurato lo strumento di automazione GitHub Actions per effettuare automaticamente alcuni compiti.
Scansione automatica delle dipendenze
È stato abilitato su GitHub il supporto a Dependabot, un software che scansiona le dipendenze dei vari moduli e notifica gli sviluppatori qualora una o più di esse siano vulnerabili ad exploit.
Controllo automatico del codice
Sono state configurate due azioni, analyze-codeql-backend
e analyze-codeql-frontend
, che usano CodeQL per scansionare staticamente il codice e identificare problemi o vulnerabilità.
La prima, analyze-codeql-backend
, viene eseguita solo quando viene inviato a GitHub nuovo codice relativo al modulo backend
, ed effettua analisi specifiche a Python
, mentre la seconda, analyze-codeql-frontend
, viene eseguita solo quando viene inviato nuovo codice del modulo frontend
, ed effettua analisi specifiche a JavaScript.
Si riportano due estratti relativi all'azione analyze-codeql-backend
.
on:
push:
branches: [ main ]
paths:
- "backend/**"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: "python"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
Costruzione automatica delle immagini Docker
Sono state configurate quattro azioni, build-docker-frontend
, build-docker-backend
, build-docker-jupyter
e build-docker-proxy
, che costruiscono automaticamente l'immagine Docker di ciascun modulo qualora il relativo codice venga modificato.
L'immagine creata viene poi caricata sul GitHub Container Registry, da cui può poi essere scaricata attraverso Docker.
Si riporta un estratto relativo all'azione build-docker-proxy
.
steps:
- name: "Checkout repository"
uses: actions/checkout@v2
- name: "Login to GitHub Containers"
run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u Steffo99 --password-stdin
- name: "Build the docker container `ghcr.io/steffo99/sophon-proxy:latest`"
run: docker build ./proxy --tag ghcr.io/steffo99/sophon-proxy:latest
- name: "Upload the container to GitHub Containers"
run: docker push ghcr.io/steffo99/sophon-proxy:latest
Costruzione automatica della documentazione
Sono state configurate due azioni, build-sphinx-report
e build-sphinx-thesis
, che compilano rispettivamente la documentazione richiesta per l'esame di Tecnologie Web e questa stessa tesi usando lo strumento Sphinx.
La documentazione per l'esame viene compilata solo da reStructuredText ad HTML; la tesi, invece, viene compilata sia in HTML sia in PDF.
Si riporta un estratto relativo all'azione build-sphinx-thesis
.
latexpdf:
name: "Build PDF document"
runs-on: ubuntu-latest
steps:
- name: "Update apt repositories"
run: sudo apt-get update -y
- name: "Checkout repository"
uses: actions/checkout@v2
with:
lfs: true
- name: "Checkout LFS objects"
run: git lfs checkout
- name: "Setup Python"
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: "Setup Poetry"
uses: abatilo/actions-poetry@v2.0.0
with:
poetry-version: 1.1.11
- name: "Install LaTeX packages"
run: sudo apt-get install -y latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended texlive-luatex fonts-ebgaramond fonts-ebgaramond-extra fonts-firacode xindy
- name: "Install backend dependencies"
working-directory: backend/
run: poetry install --no-interaction
- name: "Find Poetry Python environment"
working-directory: backend/
run: echo "pythonLocation=$(poetry env list --full-path | cut -f1 -d' ')/bin" >> $GITHUB_ENV
- name: "Build LaTeX document with Sphinx"
working-directory: thesis/
run: |
source $pythonLocation/activate
make latexpdf
- name: "Upload build artifact"
uses: actions/upload-artifact@v2
with:
name: "thesis.pdf"
path: "thesis/build/latex/progettazioneesviluppodisophonapplicativocloudasupportodellaricerca.pdf"