PlantUML doc & Human Readable commentary
This commit is contained in:
214
docs/plantuml-human-readable.md
Normal file
214
docs/plantuml-human-readable.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# PlantUML — Format de commentaire human-readable
|
||||||
|
|
||||||
|
Ce document décrit la syntaxe des commentaires attachés aux ressources et aux liens
|
||||||
|
dans les fichiers PlantUML importés par OpenCloud.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Syntaxe générale
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
TypeRessource(varName, "Nom affiché") ' clé: valeur, clé.sous_clé: valeur
|
||||||
|
```
|
||||||
|
|
||||||
|
### Règles de parsing
|
||||||
|
|
||||||
|
| Règle | Détail |
|
||||||
|
|---|---|
|
||||||
|
| Séparateur de paires | `,` |
|
||||||
|
| Séparateur clé/valeur | premier `:` de la paire (les URLs `http://...` sont gérées) |
|
||||||
|
| Sous-objets | notation pointée `access.container.image: nginx` |
|
||||||
|
| Types | auto-inférés : `bool` > `float64` > `string` |
|
||||||
|
| Fallback | JSON brut si le commentaire commence par `{` (compatibilité ascendante) |
|
||||||
|
|
||||||
|
### Comportement à l'import
|
||||||
|
|
||||||
|
Chaque ressource reçoit automatiquement une **instance par défaut**, seedée avec les
|
||||||
|
attributs de la ressource parente. Le commentaire vient ensuite **surcharger** uniquement
|
||||||
|
les champs explicitement renseignés.
|
||||||
|
|
||||||
|
> **Exception :** `WorkflowEvent` n'a pas d'instance (voir section dédiée).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ressources disponibles
|
||||||
|
|
||||||
|
### `Data(var, "nom")` — Données
|
||||||
|
|
||||||
|
Ressource de données. Les attributs qualifient le modèle de données **et** son instance
|
||||||
|
(source d'accès).
|
||||||
|
|
||||||
|
| Clé | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `type` | string | Type de données (`raster`, `vector`, `tabular`…) |
|
||||||
|
| `quality` | string | Niveau de qualité |
|
||||||
|
| `open_data` | bool | Données en accès libre |
|
||||||
|
| `static` | bool | Données statiques (pas de mise à jour) |
|
||||||
|
| `personal_data` | bool | Contient des données personnelles |
|
||||||
|
| `anonymized_personal_data` | bool | Données personnelles anonymisées |
|
||||||
|
| `size` | float64 | Taille en GB |
|
||||||
|
| `access_protocol` | string | Protocole d'accès (`http`, `s3`, `ftp`…) |
|
||||||
|
| `country` | string | Code pays ISO (`FR`, `DE`…) |
|
||||||
|
| `location.latitude` | float64 | Latitude géographique |
|
||||||
|
| `location.longitude` | float64 | Longitude géographique |
|
||||||
|
| `source` | string | URL / endpoint d'accès à la donnée |
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
Data(d1, "Satellites L2A") ' type: raster, open_data: true, size: 120.5, source: https://catalogue.example.com, country: FR
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Processing(var, "nom")` — Traitement
|
||||||
|
|
||||||
|
Ressource de traitement (algorithme, conteneur, service). Les attributs qualifient
|
||||||
|
le modèle de traitement **et** sa configuration d'exécution.
|
||||||
|
|
||||||
|
| Clé | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `infrastructure` | int | Infrastructure cible : `0`=DOCKER, `1`=KUBERNETES, `2`=SLURM, `3`=HW, `4`=CONDOR |
|
||||||
|
| `is_service` | bool | Traitement persistant (service long-running) |
|
||||||
|
| `open_source` | bool | Code source ouvert |
|
||||||
|
| `license` | string | Licence (`MIT`, `Apache-2.0`, `GPL-3.0`…) |
|
||||||
|
| `maturity` | string | Maturité (`prototype`, `beta`, `production`…) |
|
||||||
|
| `access_protocol` | string | Protocole d'accès |
|
||||||
|
| `country` | string | Code pays ISO |
|
||||||
|
| `location.latitude` | float64 | Latitude |
|
||||||
|
| `location.longitude` | float64 | Longitude |
|
||||||
|
| `access.container.image` | string | Image du conteneur |
|
||||||
|
| `access.container.command` | string | Commande de démarrage |
|
||||||
|
| `access.container.args` | string | Arguments de la commande |
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
Processing(p1, "NDVI Calc") ' infrastructure: 0, open_source: true, license: MIT, maturity: production, access.container.image: myrepo/ndvi:1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `Storage(var, "nom")` — Stockage
|
||||||
|
|
||||||
|
Ressource de stockage. Produit une instance live (`LiveStorage`) à l'import.
|
||||||
|
|
||||||
|
| Clé | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `storage_type` | int | Type de stockage (enum) |
|
||||||
|
| `source` | string | URL / endpoint du stockage |
|
||||||
|
| `path` | string | Chemin ou bucket dans le stockage |
|
||||||
|
| `local` | bool | Stockage local |
|
||||||
|
| `security_level` | string | Niveau de sécurité |
|
||||||
|
| `size` | float64 | Taille allouée en GB |
|
||||||
|
| `encryption` | bool | Chiffrement activé |
|
||||||
|
| `redundancy` | string | Politique de redondance |
|
||||||
|
| `throughput` | string | Débit cible |
|
||||||
|
| `access_protocol` | string | Protocole (`s3`, `nfs`, `smb`…) |
|
||||||
|
| `country` | string | Code pays ISO |
|
||||||
|
| `location.latitude` | float64 | Latitude |
|
||||||
|
| `location.longitude` | float64 | Longitude |
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
Storage(s1, "Minio OVH") ' source: http://minio.example.com:9000, path: /bucket/data, access_protocol: s3, encryption: true, size: 500, country: FR
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `ComputeUnit(var, "nom")` — Unité de calcul
|
||||||
|
|
||||||
|
Ressource de calcul (datacenter, cluster). Produit une instance live (`LiveDatacenter`)
|
||||||
|
à l'import.
|
||||||
|
|
||||||
|
| Clé | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `architecture` | string | Architecture CPU (`x86_64`, `arm64`…) |
|
||||||
|
| `infrastructure` | int | `0`=DOCKER, `1`=KUBERNETES, `2`=SLURM, `3`=HW, `4`=CONDOR |
|
||||||
|
| `source` | string | URL de l'API du datacenter |
|
||||||
|
| `security_level` | string | Niveau de sécurité |
|
||||||
|
| `annual_co2_emissions` | float64 | Émissions CO₂ annuelles (kg) |
|
||||||
|
| `access_protocol` | string | Protocole d'accès |
|
||||||
|
| `country` | string | Code pays ISO |
|
||||||
|
| `location.latitude` | float64 | Latitude |
|
||||||
|
| `location.longitude` | float64 | Longitude |
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
ComputeUnit(c1, "Datacenter Rennes") ' source: https://api.dc-rennes.example.com, infrastructure: 1, country: FR, location.latitude: 48.11, location.longitude: -1.68, security_level: high
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `WorkflowEvent(var, "nom")` — Événement déclencheur de workflow
|
||||||
|
|
||||||
|
Crée directement un `NativeTool` de type `WORKFLOW_EVENT` (Kind = 0).
|
||||||
|
Représente le point de départ d'un workflow.
|
||||||
|
|
||||||
|
> **Pas d'instance. Pas de commentaire.**
|
||||||
|
> Le nom du `NativeTool` est forcé à `WORKFLOW_EVENT` à l'import.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
WorkflowEvent(e1, "Start")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Liens
|
||||||
|
|
||||||
|
Les commentaires sur les liens qualifient la connexion entre deux ressources
|
||||||
|
(typiquement entre un traitement et un stockage).
|
||||||
|
|
||||||
|
### Syntaxe
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
source --> destination ' clé: valeur
|
||||||
|
source <-- destination ' clé: valeur
|
||||||
|
source -- destination ' clé: valeur (non directionnel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attributs disponibles
|
||||||
|
|
||||||
|
| Clé | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `storage_link_infos.write` | bool | `true` = écriture, `false` = lecture |
|
||||||
|
| `storage_link_infos.source` | string | Chemin source dans le lien |
|
||||||
|
| `storage_link_infos.destination` | string | Chemin destination dans le lien |
|
||||||
|
| `storage_link_infos.filename` | string | Nom du fichier échangé |
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
p1 --> s1 ' storage_link_infos.write: true, storage_link_infos.filename: output.tif
|
||||||
|
d1 --> p1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exemple complet
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
!include opencloud.puml
|
||||||
|
|
||||||
|
WorkflowEvent(e1, "Start")
|
||||||
|
|
||||||
|
Data(d1, "Satellites L2A") ' type: raster, open_data: true, size: 120.5, source: https://catalogue.example.com, country: FR
|
||||||
|
|
||||||
|
Processing(p1, "NDVI") ' infrastructure: 0, open_source: true, license: MIT, access.container.image: myrepo/ndvi:1.2
|
||||||
|
|
||||||
|
Storage(s1, "Minio résultats") ' source: http://minio.example.com:9000, path: /results, access_protocol: s3, encryption: true, size: 500, country: FR
|
||||||
|
|
||||||
|
ComputeUnit(c1, "DC Rennes") ' source: https://api.dc.example.com, infrastructure: 1, country: FR, location.latitude: 48.11, location.longitude: -1.68
|
||||||
|
|
||||||
|
e1 --> p1
|
||||||
|
d1 --> p1
|
||||||
|
p1 --> s1 ' storage_link_infos.write: true, storage_link_infos.filename: ndvi.tif
|
||||||
|
s1 --> c1
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Récapitulatif des types de ressources
|
||||||
|
|
||||||
|
| Mot-clé PlantUML | Type Go | Instance | Live | Commentaire |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `Data` | `DataResource` | `DataInstance` | non | oui |
|
||||||
|
| `Processing` | `ProcessingResource` | `ProcessingInstance` | non | oui |
|
||||||
|
| `Storage` | `StorageResource` | `StorageResourceInstance` | oui → `LiveStorage` | oui |
|
||||||
|
| `ComputeUnit` | `ComputeResource` | `ComputeResourceInstance` | oui → `LiveDatacenter` | oui |
|
||||||
|
| `WorkflowEvent` | `NativeTool` (Kind=WORKFLOW_EVENT) | aucune | non | non |
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"cloud.o-forge.io/core/oc-lib/models/live"
|
"cloud.o-forge.io/core/oc-lib/models/live"
|
||||||
"cloud.o-forge.io/core/oc-lib/models/peer"
|
"cloud.o-forge.io/core/oc-lib/models/peer"
|
||||||
"cloud.o-forge.io/core/oc-lib/models/resources"
|
"cloud.o-forge.io/core/oc-lib/models/resources"
|
||||||
|
"cloud.o-forge.io/core/oc-lib/models/resources/native_tools"
|
||||||
"cloud.o-forge.io/core/oc-lib/models/utils"
|
"cloud.o-forge.io/core/oc-lib/models/utils"
|
||||||
"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
|
"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
|
||||||
"cloud.o-forge.io/core/oc-lib/tools"
|
"cloud.o-forge.io/core/oc-lib/tools"
|
||||||
@@ -133,12 +135,18 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A
|
|||||||
},
|
},
|
||||||
"ComputeUnit": func() resources.ResourceInterface {
|
"ComputeUnit": func() resources.ResourceInterface {
|
||||||
return &resources.ComputeResource{
|
return &resources.ComputeResource{
|
||||||
|
|
||||||
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.ComputeResourceInstance]{
|
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.ComputeResourceInstance]{
|
||||||
Instances: []*resources.ComputeResourceInstance{},
|
Instances: []*resources.ComputeResourceInstance{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// WorkflowEvent creates a NativeTool of Kind=WORKFLOW_EVENT directly,
|
||||||
|
// without DB lookup. It has no user-defined instance.
|
||||||
|
"WorkflowEvent": func() resources.ResourceInterface {
|
||||||
|
return &resources.NativeTool{
|
||||||
|
Kind: int(native_tools.WORKFLOW_EVENT),
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
graphVarName := map[string]graph.GraphItem{}
|
graphVarName := map[string]graph.GraphItem{}
|
||||||
scanner := bufio.NewScanner(plantUML)
|
scanner := bufio.NewScanner(plantUML)
|
||||||
@@ -231,6 +239,60 @@ func (d *Workflow) generateResource(datas []resources.ResourceInterface, request
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setNestedKey sets a value in a nested map using dot-notation path.
|
||||||
|
// "access.container.image" → m["access"]["container"]["image"] = value
|
||||||
|
func setNestedKey(m map[string]any, path string, value any) {
|
||||||
|
parts := strings.SplitN(path, ".", 2)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
m[path] = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key, rest := parts[0], parts[1]
|
||||||
|
if _, ok := m[key]; !ok {
|
||||||
|
m[key] = map[string]any{}
|
||||||
|
}
|
||||||
|
if sub, ok := m[key].(map[string]any); ok {
|
||||||
|
setNestedKey(sub, rest, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHumanFriendlyAttrs converts a human-friendly comment into JSON bytes.
|
||||||
|
// Supports:
|
||||||
|
// - flat: "source: http://example.com, encryption: true, size: 500"
|
||||||
|
// - nested: "access.container.image: nginx, access.container.tag: latest"
|
||||||
|
// - raw JSON passthrough (backward-compat): '{"key": "value"}'
|
||||||
|
//
|
||||||
|
// Values are auto-typed: bool, float64, or string.
|
||||||
|
// Note: the first ':' in each pair is the key/value separator,
|
||||||
|
// so URLs like "http://..." are handled correctly.
|
||||||
|
func parseHumanFriendlyAttrs(comment string) []byte {
|
||||||
|
comment = strings.TrimSpace(comment)
|
||||||
|
if strings.HasPrefix(comment, "{") {
|
||||||
|
return []byte(comment)
|
||||||
|
}
|
||||||
|
m := map[string]any{}
|
||||||
|
for _, pair := range strings.Split(comment, ",") {
|
||||||
|
pair = strings.TrimSpace(pair)
|
||||||
|
parts := strings.SplitN(pair, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
val := strings.TrimSpace(parts[1])
|
||||||
|
var typed any
|
||||||
|
if b, err := strconv.ParseBool(val); err == nil {
|
||||||
|
typed = b
|
||||||
|
} else if n, err := strconv.ParseFloat(val, 64); err == nil {
|
||||||
|
typed = n
|
||||||
|
} else {
|
||||||
|
typed = val
|
||||||
|
}
|
||||||
|
setNestedKey(m, key, typed)
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(m)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Workflow) extractLink(line string, graphVarName map[string]graph.GraphItem, pattern string, reverse bool) error {
|
func (d *Workflow) extractLink(line string, graphVarName map[string]graph.GraphItem, pattern string, reverse bool) error {
|
||||||
splitted := strings.Split(line, pattern)
|
splitted := strings.Split(line, pattern)
|
||||||
if len(splitted) < 2 {
|
if len(splitted) < 2 {
|
||||||
@@ -255,8 +317,8 @@ func (d *Workflow) extractLink(line string, graphVarName map[string]graph.GraphI
|
|||||||
}
|
}
|
||||||
splittedComments := strings.Split(line, "'")
|
splittedComments := strings.Split(line, "'")
|
||||||
if len(splittedComments) > 1 {
|
if len(splittedComments) > 1 {
|
||||||
comment := strings.ReplaceAll(splittedComments[1], "'", "") // for now it's a json.
|
comment := strings.ReplaceAll(splittedComments[1], "'", "")
|
||||||
json.Unmarshal([]byte(comment), link)
|
json.Unmarshal(parseHumanFriendlyAttrs(comment), link)
|
||||||
}
|
}
|
||||||
d.Graph.Links = append(d.Graph.Links, *link)
|
d.Graph.Links = append(d.Graph.Links, *link)
|
||||||
return nil
|
return nil
|
||||||
@@ -268,7 +330,7 @@ func (d *Workflow) extractResourcePlantUML(line string, resource resources.Resou
|
|||||||
return "", nil, errors.New("Can't deserialize Object, there's no func")
|
return "", nil, errors.New("Can't deserialize Object, there's no func")
|
||||||
}
|
}
|
||||||
splittedParams := strings.Split(splittedFunc[1], ",")
|
splittedParams := strings.Split(splittedFunc[1], ",")
|
||||||
if len(splittedFunc) <= 1 {
|
if len(splittedParams) <= 1 {
|
||||||
return "", nil, errors.New("Can't deserialize Object, there's no params")
|
return "", nil, errors.New("Can't deserialize Object, there's no params")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,18 +343,22 @@ func (d *Workflow) extractResourcePlantUML(line string, resource resources.Resou
|
|||||||
}
|
}
|
||||||
resource.SetName(strings.ReplaceAll(splitted[1], "\\n", " "))
|
resource.SetName(strings.ReplaceAll(splitted[1], "\\n", " "))
|
||||||
|
|
||||||
|
// Resources with instances get a default one seeded from the parent resource,
|
||||||
|
// then overridden by any explicit comment attributes.
|
||||||
|
// Event (NativeTool) has no instance: getNewInstance returns nil and is skipped.
|
||||||
|
instance := d.getNewInstance(dataName, splitted[1], peerID)
|
||||||
|
if instance != nil {
|
||||||
|
if b, err := json.Marshal(resource); err == nil {
|
||||||
|
json.Unmarshal(b, instance)
|
||||||
|
}
|
||||||
splittedComments := strings.Split(line, "'")
|
splittedComments := strings.Split(line, "'")
|
||||||
if len(splittedComments) > 1 {
|
if len(splittedComments) > 1 {
|
||||||
comment := strings.ReplaceAll(splittedComments[1], "'", "") // for now it's a json.
|
comment := strings.ReplaceAll(splittedComments[1], "'", "")
|
||||||
instance := d.getNewInstance(dataName, splitted[1], peerID)
|
json.Unmarshal(parseHumanFriendlyAttrs(comment), instance)
|
||||||
if instance == nil {
|
|
||||||
return "", nil, errors.New("No instance found.")
|
|
||||||
}
|
}
|
||||||
resource.AddInstances(instance)
|
resource.AddInstances(instance)
|
||||||
|
|
||||||
json.Unmarshal([]byte(comment), instance)
|
|
||||||
}
|
}
|
||||||
// deserializer les instances... une instance doit par défaut avoir certaines valeurs d'accès.
|
|
||||||
item := d.getNewGraphItem(dataName, resource)
|
item := d.getNewGraphItem(dataName, resource)
|
||||||
if item != nil {
|
if item != nil {
|
||||||
d.Graph.Items[item.ID] = *item
|
d.Graph.Items[item.ID] = *item
|
||||||
@@ -318,15 +384,13 @@ func (d *Workflow) getNewGraphItem(dataName string, resource resources.ResourceI
|
|||||||
d.Processings = append(d.Processings, resource.GetID())
|
d.Processings = append(d.Processings, resource.GetID())
|
||||||
d.ProcessingResources = append(d.ProcessingResources, resource.(*resources.ProcessingResource))
|
d.ProcessingResources = append(d.ProcessingResources, resource.(*resources.ProcessingResource))
|
||||||
graphItem.Processing = resource.(*resources.ProcessingResource)
|
graphItem.Processing = resource.(*resources.ProcessingResource)
|
||||||
case "Event":
|
case "WorkflowEvent":
|
||||||
access := resources.NewAccessor[*resources.NativeTool](tools.NATIVE_TOOL, &tools.APIRequest{
|
// The resource is already a *NativeTool with Kind=WORKFLOW_EVENT set by the
|
||||||
Admin: true,
|
// catalog factory. We use it directly without any DB lookup.
|
||||||
}, func() utils.DBObject { return &resources.NativeTool{} })
|
nt := resource.(*resources.NativeTool)
|
||||||
t, _, err := access.Search(nil, "WORKFLOW_EVENT", false)
|
nt.Name = native_tools.WORKFLOW_EVENT.String()
|
||||||
if err == nil && len(t) > 0 {
|
d.NativeTool = append(d.NativeTool, nt.GetID())
|
||||||
d.NativeTool = append(d.NativeTool, t[0].GetID())
|
graphItem.NativeTool = nt
|
||||||
graphItem.NativeTool = t[0].(*resources.NativeTool)
|
|
||||||
}
|
|
||||||
case "Storage":
|
case "Storage":
|
||||||
d.Storages = append(d.Storages, resource.GetID())
|
d.Storages = append(d.Storages, resource.GetID())
|
||||||
d.StorageResources = append(d.StorageResources, resource.(*resources.StorageResource))
|
d.StorageResources = append(d.StorageResources, resource.(*resources.StorageResource))
|
||||||
|
|||||||
Reference in New Issue
Block a user