diff --git a/models/workflow/workflow.go b/models/workflow/workflow.go index 61e78d3..5c07707 100644 --- a/models/workflow/workflow.go +++ b/models/workflow/workflow.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/models/booking" "cloud.o-forge.io/core/oc-lib/models/booking/planner" "cloud.o-forge.io/core/oc-lib/models/collaborative_area/shallow_collaborative_area" @@ -108,9 +109,9 @@ func (d *Workflow) GetResources(dt tools.DataType) []resources.ResourceInterface return itf } -func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.APIRequest) (*Workflow, error) { +func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.APIRequest) (*Workflow, []string, error) { if plantUML == nil { - return d, errors.New("no file available to export") + return d, nil, errors.New("no file available to export") } defer plantUML.Close() @@ -189,9 +190,11 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A lines = append(lines, scanner.Text()) } if err := scanner.Err(); err != nil { - return d, err + return d, nil, err } + var warnings []string + for i, line := range lines { trimmed := strings.TrimSpace(line) @@ -237,14 +240,15 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A continue } - for n, new := range resourceCatalog { + for n, newFn := range resourceCatalog { if strings.Contains(line, n+"(") && !strings.Contains(line, "!procedure") && !strings.Contains(line, "!define") { - newRes := new() + newRes := newFn() newRes.SetID(uuid.New().String()) - varName, graphItem, err := d.extractResourcePlantUML(parseLine, newRes, n, request.PeerID) + varName, graphItem, warns, err := d.extractResourcePlantUML(parseLine, newRes, n, request.PeerID, request) if err != nil { - return d, err + return d, warnings, err } + warnings = append(warnings, warns...) if graphItem != nil { graphVarName[varName] = *graphItem } @@ -258,10 +262,9 @@ func (d *Workflow) ExtractFromPlantUML(plantUML multipart.File, request *tools.A d.generateResource(d.GetResources(tools.STORAGE_RESOURCE), request) d.generateResource(d.GetResources(tools.COMPUTE_RESOURCE), request) d.generateResource(d.GetResources(tools.WORKFLOW_RESOURCE), request) - d.generateResource(d.GetResources(tools.SERVICE_RESOURCE), request) d.generateResource(d.GetResources(tools.DYNAMIC_RESOURCE), request) d.Graph.Items = graphVarName - return d, nil + return d, warnings, nil } func (d *Workflow) generateResource(datas []resources.ResourceInterface, request *tools.APIRequest) error { @@ -402,35 +405,46 @@ func (d *Workflow) extractLink(line string, graphVarName map[string]graph.GraphI return nil } -func (d *Workflow) extractResourcePlantUML(line string, resource resources.ResourceInterface, dataName string, peerID string) (string, *graph.GraphItem, error) { +func (d *Workflow) extractResourcePlantUML(line string, resource resources.ResourceInterface, dataName string, peerID string, request *tools.APIRequest) (string, *graph.GraphItem, []string, error) { splittedFunc := strings.Split(line, "(") if len(splittedFunc) <= 1 { - return "", nil, errors.New("Can't deserialize Object, there's no func") + return "", nil, nil, errors.New("Can't deserialize Object, there's no func") } splittedParams := strings.Split(splittedFunc[1], ",") if len(splittedParams) <= 1 { - return "", nil, errors.New("Can't deserialize Object, there's no params") + return "", nil, nil, errors.New("Can't deserialize Object, there's no params") } varName := splittedParams[0] splitted := strings.Split(splittedParams[1], "\"") - if len(splitted) <= 1 { - return "", nil, errors.New("Can't deserialize Object, there's no name") + return "", nil, nil, errors.New("Can't deserialize Object, there's no name") } - resource.SetName(strings.ReplaceAll(splitted[1], "\\n", " ")) + name := 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) + // Extract comment text (if present) for metadata parsing. + comment := "" + if parts := strings.Split(line, "'"); len(parts) > 1 { + comment = strings.ReplaceAll(parts[1], "'", "") + } + + var warns []string + + // Try to resolve an existing catalog resource (by id, then by name). + if existing, warn := d.resolveExistingResource(resource, dataName, name, comment, request); existing != nil { + warns = append(warns, warn) + item := d.addExistingGraphItem(dataName, existing) + return varName, item, warns, nil + } + + // No existing resource — create new. + resource.SetName(name) + instance := d.getNewInstance(dataName, name, peerID) if instance != nil { if b, err := json.Marshal(resource); err == nil { json.Unmarshal(b, instance) } - splittedComments := strings.Split(line, "'") - if len(splittedComments) > 1 { - comment := strings.ReplaceAll(splittedComments[1], "'", "") + if comment != "" { json.Unmarshal(parseHumanFriendlyAttrs(comment), instance) } resource.AddInstances(instance) @@ -441,7 +455,91 @@ func (d *Workflow) extractResourcePlantUML(line string, resource resources.Resou d.Graph.Items[item.ID] = *item } - return varName, item, nil + return varName, item, warns, nil +} + +// resolveExistingResource tries to find an existing catalog resource matching +// the given name or the id embedded in the PlantUML comment. +// Returns (resource, warning message) or (nil, "") if none found. +func (d *Workflow) resolveExistingResource( + proto resources.ResourceInterface, + dataName, name, comment string, + request *tools.APIRequest, +) (resources.ResourceInterface, string) { + accessor := proto.GetAccessor(request) + + // 1. Try lookup by id from comment ("id: "). + if comment != "" { + attrs := map[string]any{} + json.Unmarshal(parseHumanFriendlyAttrs(comment), &attrs) + if id, ok := attrs["id"].(string); ok && id != "" { + if dbObj, _, err := accessor.LoadOne(id); err == nil && dbObj != nil { + if ri, ok := dbObj.(resources.ResourceInterface); ok { + return ri, fmt.Sprintf(`[import warning] %s "%s": existing resource retrieved by id %s`, dataName, name, id) + } + } + } + } + + // 2. Try search by exact name. + filter := &dbs.Filters{ + Or: map[string][]dbs.Filter{ + "abstractobject.name": {{Operator: dbs.EQUAL.String(), Value: name}}, + }, + } + if results, _, err := accessor.Search(filter, "", false, 0, 10); err == nil { + for _, r := range results { + if r.GetName() == name { + if dbObj, _, err2 := accessor.LoadOne(r.GetID()); err2 == nil && dbObj != nil { + if ri, ok := dbObj.(resources.ResourceInterface); ok { + return ri, fmt.Sprintf(`[import warning] %s "%s": existing resource found by name and retrieved instead of creating a new one`, dataName, name) + } + } + } + } + } + + return nil, "" +} + +// addExistingGraphItem registers an already-existing resource in the workflow's +// ID lists and graph, without adding it to the resource lists (so generateResource +// won't try to store it again). +func (d *Workflow) addExistingGraphItem(dataName string, resource resources.ResourceInterface) *graph.GraphItem { + graphItem := &graph.GraphItem{ + ID: uuid.New().String(), + ItemResource: &resources.ItemResource{}, + } + switch dataName { + case "Data": + d.Datas = append(d.Datas, resource.GetID()) + if r, ok := resource.(*resources.DataResource); ok { + graphItem.Data = r + } + case "Processing": + d.Processings = append(d.Processings, resource.GetID()) + if r, ok := resource.(*resources.ProcessingResource); ok { + graphItem.Processing = r + } + case "Service": + d.Services = append(d.Services, resource.GetID()) + if r, ok := resource.(*resources.ServiceResource); ok { + graphItem.Service = r + } + case "Storage": + d.Storages = append(d.Storages, resource.GetID()) + if r, ok := resource.(*resources.StorageResource); ok { + graphItem.Storage = r + } + case "ComputeUnit": + d.Computes = append(d.Computes, resource.GetID()) + if r, ok := resource.(*resources.ComputeResource); ok { + graphItem.Compute = r + } + default: + return nil + } + return graphItem } func (d *Workflow) getNewGraphItem(dataName string, resource resources.ResourceInterface) *graph.GraphItem {