Skip to content

Commit 08de316

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. This also respects states provided in the SBOM which will overwrite existing states.
1 parent 38d7087 commit 08de316

File tree

4 files changed

+51
-37
lines changed

4 files changed

+51
-37
lines changed

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 = self.client.get_id_from_href(pms_release)
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)