Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate models from OpenAPI v3 schema #57

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
venv/
._dev/
.vscode/
codegen/
codegen/temp

# Single files
swagger.yaml
Expand Down
46 changes: 46 additions & 0 deletions codegen/ast/fragments/artifact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Any, Dict

from pydantic import BaseModel, root_validator


class Artifact(BaseModel):
@root_validator(pre=True)
def _get_native_report_summary(cls, values: Dict[str, Any]) -> Dict[str, Any]:
"""Constructs a scan overview from a dict of `mime_type:scan_overview`
and populates the `native_report_summary` field with it.

The API spec does not specify the contents of the scan overview, but from
investigating the behavior of the API, it seems to return a dict that looks like this:

```py
{
"application/vnd.security.vulnerability.report; version=1.1": {
# dict that conforms to NativeReportSummary spec
...
}
}
```
"""
mime_types = (
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0",
"application/vnd.security.vulnerability.report; version=1.1",
)
overview = values.get("scan_overview")
if not overview:
return values

if isinstance(overview, PydanticBaseModel):
overview = overview.dict()

# At this point we require that scan_overview is a dict
if not isinstance(overview, dict):
raise TypeError(
f"scan_overview must be a dict, not {type(overview).__name__}"
)

# Extract overview for the first mime type that we recognize
for k, v in overview.items():
if k in mime_types:
values["scan_overview"] = v
break
return values
10 changes: 10 additions & 0 deletions codegen/ast/fragments/generalinfo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import Optional

from pydantic import BaseModel, Field


class GeneralInfo(BaseModel):
with_chartmuseum: Optional[bool] = Field(
None,
description="DEPRECATED: Harbor instance is deployed with nested chartmuseum.",
)
10 changes: 10 additions & 0 deletions codegen/ast/fragments/immutablerule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import Any, Dict, Optional

from pydantic import BaseModel

# Changed: change params field type
# Reason: params is a dict of Any, not a dict of dicts


class ImmutableRule(BaseModel):
params: Optional[Dict[str, Any]] = None
13 changes: 13 additions & 0 deletions codegen/ast/fragments/nativereportsummary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class NativeReportSummary(BaseModel):
@property
def severity_enum(self) -> Optional[Severity]:
"""The severity of the vulnerability

Returns
-------
Optional[Severity]
The severity of the vulnerability
"""
if self.severity:
return Severity(self.severity)
return None
50 changes: 50 additions & 0 deletions codegen/ast/fragments/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Fragment that adds new properties and methods to the Repository model"""
from typing import Optional

from pydantic import BaseModel


class Repository(BaseModel):
@property
def base_name(self) -> str:
"""The repository name without the project name

Returns
-------
Optional[str]
The basename of the repository name
"""
s = self.split_name()
return s[1] if s else ""

@property
def project_name(self) -> str:
"""The name of the project that the repository belongs to

Returns
-------
Optional[str]
The name of the project that the repository belongs to
"""
s = self.split_name()
return s[0] if s else ""

# TODO: cache?
def split_name(self) -> Optional[Tuple[str, str]]:
"""Split name into tuple of project and repository name

Returns
-------
Optional[Tuple[str, str]]
Tuple of project name and repo name
"""
if not self.name:
return None
components = self.name.split("/", 1)
if len(components) != 2: # no slash in name
# Shouldn't happen, but we account for it anyway
logger.warning(
"Repository name '%s' is not in the format <project>/<repo>", self.name
)
return None
return components[0], components[1]
11 changes: 11 additions & 0 deletions codegen/ast/fragments/retentionrule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing import Any, Dict, Optional

from pydantic import BaseModel

# Changed: change params field type
# Reason: params is a dict of Any, not a dict of dicts
# TODO: add descriptions


class RetentionRule(BaseModel):
params: Optional[Dict[str, Any]] = None
35 changes: 35 additions & 0 deletions codegen/ast/fragments/vulnerabilitysummary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Any, Dict

from pydantic import BaseModel, Field, root_validator


class VulnerabilitySummary(BaseModel):
# Summary dict keys added as fields
critical: int = Field(
0,
alias="Critical",
description="The number of critical vulnerabilities detected.",
)
high: int = Field(
0, alias="High", description="The number of critical vulnerabilities detected."
)
medium: int = Field(
0,
alias="Medium",
description="The number of critical vulnerabilities detected.",
)
low: int = Field(
0, alias="Low", description="The number of critical vulnerabilities detected."
)
unknown: int = Field(
0,
alias="Unknown",
description="The number of critical vulnerabilities detected.",
)

@root_validator(pre=True)
def _assign_severity_breakdown(cls, values: Dict[str, Any]) -> Dict[str, Any]:
summary = values.get("summary") or {} # account for None
if not isinstance(summary, dict):
raise ValueError("'summary' must be a dict")
return {**values, **summary}
Loading