Skip to content

Commit a128897

Browse files
committed
fix(project create): optimize release state handling on update
Instead of restoring all release states after project update in a big loop, SW360 REST API also allows to pass full release information during release update. For large projects, this is much faster and avoids timeouts or even crashes due to REST API rate limiting. This also respects states provided in the SBOM which will overwrite existing states. Fixes #121
1 parent 38d7087 commit a128897

4 files changed

Lines changed: 51 additions & 37 deletions

File tree

ChangeLog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* `bom map` fix: In few cases with --nocache, it added mixed matches to output
1616
BOM, now we assure that only the best mapping results are added.
1717
* `project createbom` stores release relations (`CONTAINED`, `SIDE_BY_SIDE` etc.) as capycli:projectRelation
18+
* `project update`: optimized handling of release mainline state and release relation. Now states
19+
provided in the SBOM are used and slowdowns/crashes introduced in 2.7.0 (#121) fixed again.
1820

1921
## 2.7.0
2022

capycli/project/create_project.py

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ def __init__(self, onlyUpdateProject: bool = False) -> None:
3333
self.onlyUpdateProject = onlyUpdateProject
3434
self.project_mainline_state: str = ""
3535

36-
def bom_to_release_list(self, sbom: Bom) -> List[str]:
37-
"""Creates a list with linked releases"""
38-
linkedReleases = []
36+
def bom_to_release_list(self, sbom: Bom) -> Dict[str, Any]:
37+
"""Creates a list with linked releases from the SBOM."""
38+
linkedReleases: Dict[str, Any] = {}
3939

4040
for cx_comp in sbom.components:
4141
rid = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_SW360ID)
@@ -45,31 +45,39 @@ def bom_to_release_list(self, sbom: Bom) -> List[str]:
4545
+ ", " + str(cx_comp.version))
4646
continue
4747

48-
linkedReleases.append(rid)
48+
linkedReleases[rid] = {}
49+
50+
mainlineState = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_PROJ_STATE)
51+
if mainlineState:
52+
linkedReleases[rid]["mainlineState"] = mainlineState
53+
relation = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_PROJ_RELATION)
54+
if relation:
55+
# No typo. In project structure, it's "relation", while release update API uses "releaseRelation".
56+
linkedReleases[rid]["releaseRelation"] = relation
4957

5058
return linkedReleases
5159

52-
def get_release_project_mainline_states(self, project: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
53-
pms: List[Dict[str, Any]] = []
60+
def merge_project_mainline_states(self, data: Dict[str, Any], project: Optional[Dict[str, Any]]) -> None:
5461
if not project:
55-
return pms
62+
return
5663

5764
if "linkedReleases" not in project:
58-
return pms
65+
return
5966

6067
for release in project["linkedReleases"]: # NOT ["sw360:releases"]
6168
pms_release = release.get("release", "")
6269
if not pms_release:
6370
continue
71+
pms_release = pms_release.split("/")[-1]
72+
if pms_release not in data:
73+
continue
6474
pms_state = release.get("mainlineState", "OPEN")
6575
pms_relation = release.get("relation", "UNKNOWN")
66-
pms_entry: Dict[str, Any] = {}
67-
pms_entry["release"] = pms_release
68-
pms_entry["mainlineState"] = pms_state
69-
pms_entry["new_relation"] = pms_relation
70-
pms.append(pms_entry)
7176

72-
return pms
77+
if "mainlineState" not in data[pms_release]:
78+
data[pms_release]["mainlineState"] = pms_state
79+
if "releaseRelation" not in data[pms_release]:
80+
data[pms_release]["releaseRelation"] = pms_relation
7381

7482
def update_project(self, project_id: str, project: Optional[Dict[str, Any]],
7583
sbom: Bom, project_info: Dict[str, Any]) -> None:
@@ -79,7 +87,7 @@ def update_project(self, project_id: str, project: Optional[Dict[str, Any]],
7987
sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360)
8088

8189
data = self.bom_to_release_list(sbom)
82-
pms = self.get_release_project_mainline_states(project)
90+
self.merge_project_mainline_states(data, project)
8391

8492
ignore_update_elements = ["name", "version"]
8593
# remove elements from list because they are handled separately
@@ -90,13 +98,17 @@ def update_project(self, project_id: str, project: Optional[Dict[str, Any]],
9098
try:
9199
print_text(" " + str(len(data)) + " releases in SBOM")
92100

101+
update_mode = self.onlyUpdateProject
93102
if project and "_embedded" in project and "sw360:releases" in project["_embedded"]:
94103
print_text(
95104
" " + str(len(project["_embedded"]["sw360:releases"])) +
96105
" releases in project before update")
106+
else:
107+
# Workaround for SW360 API bug: add releases will hang forever for empty projects
108+
update_mode = False
97109

98110
# note: type in sw360python, 1.4.0 is wrong - we are using the correct one!
99-
result = self.client.update_project_releases(data, project_id, add=self.onlyUpdateProject) # type: ignore
111+
result = self.client.update_project_releases(data, project_id, add=update_mode) # type: ignore
100112
if not result:
101113
print_red(" Error updating project releases!")
102114
project = self.client.get_project(project_id)
@@ -114,20 +126,6 @@ def update_project(self, project_id: str, project: Optional[Dict[str, Any]],
114126
if not result2:
115127
print_red(" Error updating project!")
116128

117-
if pms and project:
118-
print_text(" Restoring original project mainline states...")
119-
for pms_entry in pms:
120-
update_release = False
121-
for r in project.get("linkedReleases", []):
122-
if r["release"] == pms_entry["release"]:
123-
update_release = True
124-
break
125-
126-
if update_release:
127-
rid = self.client.get_id_from_href(pms_entry["release"])
128-
self.client.update_project_release_relationship(
129-
project_id, rid, pms_entry["mainlineState"], pms_entry["new_relation"], "")
130-
131129
except SW360Error as swex:
132130
if swex.response is None:
133131
print_red(" Unknown error: " + swex.message)

tests/fixtures/sbom_for_create_project.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
"name": "siemens:primaryLanguage",
6767
"value": "Python"
6868
},
69+
{
70+
"name": "capycli:projectRelation",
71+
"value": "DYNAMICALLY_LINKED"
72+
},
6973
{
7074
"name": "siemens:sw360Id",
7175
"value": "a5cae39f39db4e2587a7d760f59ce3d0"
@@ -79,4 +83,4 @@
7983
"dependsOn": []
8084
}
8185
]
82-
}
86+
}

tests/test_create_project.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import json
1010
import os
11-
from typing import Any, Dict, List, Tuple
11+
from typing import Any, Dict, Tuple
1212

1313
import responses
1414
import responses.matchers
@@ -44,7 +44,7 @@ def match(request: Any) -> Tuple[bool, str]:
4444
return match
4545

4646

47-
def update_release_matcher(releases: List[str]) -> Any:
47+
def update_release_matcher(releases: Dict[str, Any]) -> Any:
4848
"""
4949
Matches the updated releases.
5050
@@ -66,10 +66,15 @@ def match(request: Any) -> Tuple[bool, str]:
6666
reason = ("Number of releases does not match, got " + str(len(json_body)) +
6767
" expected: " + str(len(releases)))
6868
else:
69-
for rel in releases:
69+
for rel, rel_data in releases.items():
7070
if rel not in request_body:
7171
result = False
7272
reason = ("Release " + rel + " not found in: " + request_body)
73+
if rel_data != json_body[rel]:
74+
result = False
75+
reason = ("Release[" + rel + "] = '" + str(json_body[rel]) + "' does not match expected " +
76+
str(rel_data))
77+
break
7378

7479
return result, reason
7580
return match
@@ -484,7 +489,8 @@ def test_project_update(self) -> None:
484489
}
485490
},
486491
match=[
487-
update_release_matcher(["a5cae39f39db4e2587a7d760f59ce3d0"])
492+
update_release_matcher({"a5cae39f39db4e2587a7d760f59ce3d0": {
493+
"releaseRelation": "DYNAMICALLY_LINKED"}})
488494
],
489495
status=201,
490496
content_type="application/json",
@@ -645,7 +651,10 @@ def test_project_copy_from(self) -> None:
645651
}
646652
},
647653
match=[
648-
update_release_matcher(["a5cae39f39db4e2587a7d760f59ce3d0"])
654+
update_release_matcher({"a5cae39f39db4e2587a7d760f59ce3d0": {
655+
"mainlineState": "SPECIFIC", # from project 007
656+
"releaseRelation": "DYNAMICALLY_LINKED" # from SBOM
657+
}})
649658
],
650659
status=201,
651660
content_type="application/json",
@@ -791,7 +800,8 @@ def xtest_project_update_old_version(self) -> None:
791800
}
792801
},
793802
match=[
794-
update_release_matcher(["a5cae39f39db4e2587a7d760f59ce3d0"])
803+
update_release_matcher({"a5cae39f39db4e2587a7d760f59ce3d0": {
804+
"releaseRelation": "DYNAMICALLY_LINKED"}})
795805
],
796806
status=201,
797807
content_type="application/json",

0 commit comments

Comments
 (0)