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

feat: models for the craft manifest #473

Merged
merged 6 commits into from
Sep 19, 2024
Merged
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
160 changes: 160 additions & 0 deletions craft_application/models/manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# This file is part of craft-application.
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Models representing manifests for projects and fetch-service assets."""
import hashlib
import pathlib
from datetime import datetime, timezone
from typing import Any, Literal

from pydantic import Field
from typing_extensions import Self, override

from craft_application import models
from craft_application.models import CraftBaseModel


class Hashes(CraftBaseModel):
"""Digests identifying an artifact/asset."""

sha1: str
sha256: str

@classmethod
def from_path(cls, path: pathlib.Path) -> Self:
"""Compute digests for a given path."""
read_bytes = path.read_bytes()

return cls(
sha1=hashlib.sha1( # noqa: S324 (insecure hash function)
read_bytes
).hexdigest(),
sha256=hashlib.sha256(read_bytes).hexdigest(),
)


class ComponentID(CraftBaseModel):
"""Unique identifications for an artifact/asset."""

hashes: Hashes


class BaseManifestModel(CraftBaseModel):
"""Common properties shared between project and fetch-service manifests."""

component_name: str
component_version: str
component_description: str
component_id: ComponentID
architecture: str


class ProjectManifest(BaseManifestModel):
"""Model for the project-specific properties of the craft manifest."""

license: str | None = None
comment: str | None = None
metadata_generator: Literal["Craft Application"] = "Craft Application"
lengau marked this conversation as resolved.
Show resolved Hide resolved
creation_timestamp: str

@override
def marshal(self) -> dict[str, str | list[str] | dict[str, Any]]:
"""Overridden to include the metadata_generator constant field."""
return self.model_dump(
mode="json",
by_alias=True,
exclude_none=True,
exclude_defaults=False, # to include 'metadata_generator'
)

@classmethod
def from_packed_artifact(
cls,
project: models.Project,
build_info: models.BuildInfo,
artifact: pathlib.Path,
) -> Self:
"""Create the project manifest for a packed artifact."""
hashes = Hashes.from_path(artifact)

now = datetime.now(timezone.utc)

return cls.unmarshal(
{
"component-name": project.name,
"component-version": project.version,
"component-description": project.summary,
"component-id": {"hashes": hashes.marshal()},
"architecture": build_info.build_for,
"license": project.license,
"creation_timestamp": now.isoformat(),
}
)


class SessionArtifactManifest(BaseManifestModel):
"""Model for an artifact downloaded during the fetch-service session."""

component_type: str = Field(alias="type")
component_author: str
component_vendor: str
size: int
url: list[str]

@classmethod
def from_session_report(cls, report: dict[str, Any]) -> list[Self]:
"""Create session manifests from a fetch-session report."""
artifacts: list[Self] = []
for artifact in report["artefacts"]:
metadata = artifact["metadata"]
data = {
"type": metadata["type"],
"component-name": metadata["name"],
"component-version": metadata["version"],
"component-description": metadata["description"],
# "architecture" is only present on the metadata if applicable.
"architecture": metadata.get("architecture", ""),
"component-id": {
"hashes": {"sha1": metadata["sha1"], "sha256": metadata["sha256"]}
},
"component-author": metadata["author"],
"component-vendor": metadata["vendor"],
"size": metadata["size"],
"url": [d["url"] for d in artifact["downloads"]],
}
artifacts.append(cls.unmarshal(data))

return artifacts


class CraftManifest(ProjectManifest):
"""Full manifest for a generated artifact.

Includes project metadata and information on assets downloaded through a
fetch-service session.
"""

dependencies: list[SessionArtifactManifest]

@classmethod
def create_craft_manifest(
cls, project_manifest_path: pathlib.Path, session_report: dict[str, Any]
) -> Self:
"""Create the full Craft manifest from a project and session report."""
project = ProjectManifest.from_yaml_file(project_manifest_path)
session_deps = SessionArtifactManifest.from_session_report(session_report)

data = {**project.marshal(), "dependencies": session_deps}
return cls.model_validate(data)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ dev = [
"pytest==8.3.2",
"pytest-check==2.4.1",
"pytest-cov==5.0.0",
"pytest-freezegun==0.4.2",
"pytest-mock==3.14.0",
"pytest-rerunfailures==14.0",
"pytest-subprocess~=1.5.2",
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/models/data/manifest/craft-manifest-expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"component-name": "full-project",
"component-version": "1.0.0.post64+git12345678",
"component-description": "A fully-defined craft-application project.",
"component-id": {
"hashes": {
"sha1": "27d3150b433071ec1e2bd5bf04bc6de92b8b12b5",
"sha256": "9d7f74856a64282de8cb743fafdba600f18eef2a6f6049746b7cb842e47a3123"
}
},
"architecture": "amd64",
"license": "LGPLv3",
"metadata-generator": "Craft Application",
"creation-timestamp": "2024-09-16T01:02:03.456789+00:00",
"dependencies": [
{
"component-name": "Translation",
"component-version": "",
"component-description": "",
"component-id": {
"hashes": {
"sha1": "af5834abfa1a537fd383d41f1be33cea47c7b6a7",
"sha256": "38cbbf5467682ada956d14168e301a383d96aaa2f1f694cbaa47cee38b47847d"
}
},
"architecture": "",
"type": "application/x.apt.translation",
"component-author": "Ubuntu",
"component-vendor": "Ubuntu",
"size": 111544,
"url": [
"http://archive.ubuntu.com/ubuntu/dists/jammy/multiverse/i18n/by-hash/SHA256/38cbbf5467682ada956d14168e301a383d96aaa2f1f694cbaa47cee38b47847d"
]
},
{
"component-name": "Packages.xz",
"component-version": "jammy",
"component-description": "jammy main Packages file",
"component-id": {
"hashes": {
"sha1": "370c66437d49460dbc16be011209c4de9977212d",
"sha256": "37cb57f1554cbfa71c5a29ee9ffee18a9a8c1782bb0568e0874b7ff4ce8f9c11"
}
},
"architecture": "amd64",
"type": "application/x.apt.packages",
"component-author": "Ubuntu",
"component-vendor": "Ubuntu",
"size": 1394768,
"url": [
"http://archive.ubuntu.com/ubuntu/dists/jammy/main/binary-amd64/by-hash/SHA256/37cb57f1554cbfa71c5a29ee9ffee18a9a8c1782bb0568e0874b7ff4ce8f9c11"
]
},
{
"component-name": "go",
"component-version": "10660",
"component-description": "The Go programming language",
"component-id": {
"hashes": {
"sha1": "376506001849698af3f9e07a236a47ee8cddded0",
"sha256": "cf7e02ebfdaa898107d2dbf84cf1231cee6c244dd5646580d09cfd6f6cf12577"
}
},
"architecture": "amd64",
"type": "application/x.canonical.snap-package",
"component-author": "",
"component-vendor": "Canonical",
"size": 64892928,
"url": [
"https://canonical-bos01.cdn.snapcraftcontent.com:443/download-origin/canonical-lgw01/Md1HBASHzP4i0bniScAjXGnOII9cEK6e_10660.snap?interactive=1&token=1720738800_68d3c27ac109407168ed776e46653c7883b8ef40"
]
}
]
}
11 changes: 11 additions & 0 deletions tests/unit/models/data/manifest/project-expected.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
component-name: full-project
component-version: 1.0.0.post64+git12345678
component-description: A fully-defined craft-application project.
component-id:
hashes:
sha1: 27d3150b433071ec1e2bd5bf04bc6de92b8b12b5
sha256: 9d7f74856a64282de8cb743fafdba600f18eef2a6f6049746b7cb842e47a3123
architecture: amd64
license: LGPLv3
metadata-generator: Craft Application
creation-timestamp: '2024-09-16T01:02:03.456789+00:00'
42 changes: 42 additions & 0 deletions tests/unit/models/data/manifest/session-manifest-expected.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
- component-name: Translation
component-version: ''
component-description: ''
component-id:
hashes:
sha1: af5834abfa1a537fd383d41f1be33cea47c7b6a7
sha256: 38cbbf5467682ada956d14168e301a383d96aaa2f1f694cbaa47cee38b47847d
architecture: ''
type: application/x.apt.translation
component-author: Ubuntu
component-vendor: Ubuntu
size: 111544
url:
- http://archive.ubuntu.com/ubuntu/dists/jammy/multiverse/i18n/by-hash/SHA256/38cbbf5467682ada956d14168e301a383d96aaa2f1f694cbaa47cee38b47847d
- component-name: Packages.xz
component-version: jammy
component-description: jammy main Packages file
component-id:
hashes:
sha1: 370c66437d49460dbc16be011209c4de9977212d
sha256: 37cb57f1554cbfa71c5a29ee9ffee18a9a8c1782bb0568e0874b7ff4ce8f9c11
architecture: amd64
type: application/x.apt.packages
component-author: Ubuntu
component-vendor: Ubuntu
size: 1394768
url:
- http://archive.ubuntu.com/ubuntu/dists/jammy/main/binary-amd64/by-hash/SHA256/37cb57f1554cbfa71c5a29ee9ffee18a9a8c1782bb0568e0874b7ff4ce8f9c11
- component-name: go
component-version: '10660'
component-description: The Go programming language
component-id:
hashes:
sha1: 376506001849698af3f9e07a236a47ee8cddded0
sha256: cf7e02ebfdaa898107d2dbf84cf1231cee6c244dd5646580d09cfd6f6cf12577
architecture: amd64
type: application/x.canonical.snap-package
component-author: ''
component-vendor: Canonical
size: 64892928
url:
- https://canonical-bos01.cdn.snapcraftcontent.com:443/download-origin/canonical-lgw01/Md1HBASHzP4i0bniScAjXGnOII9cEK6e_10660.snap?interactive=1&token=1720738800_68d3c27ac109407168ed776e46653c7883b8ef40
67 changes: 67 additions & 0 deletions tests/unit/models/data/manifest/session-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"THIS IS A STRIPPED DOWN SESSION REPORT FOR TESTING PURPOSES": 1,
"comment": "Metadata format is unstable and may change without prior notice.",
"session-id": "f17e28e952c84d7c955a1eb5277de201",
"policy": "",
"artefacts": [
{
"metadata": {
"type": "application/x.apt.translation",
"sha1": "af5834abfa1a537fd383d41f1be33cea47c7b6a7",
"sha256": "38cbbf5467682ada956d14168e301a383d96aaa2f1f694cbaa47cee38b47847d",
"size": 111544,
"name": "Translation",
"version": "",
"vendor": "Ubuntu",
"description": "",
"author": "Ubuntu",
"license": ""
},
"downloads": [
{
"url": "http://archive.ubuntu.com/ubuntu/dists/jammy/multiverse/i18n/by-hash/SHA256/38cbbf5467682ada956d14168e301a383d96aaa2f1f694cbaa47cee38b47847d"
}
]
},
{
"metadata": {
"type": "application/x.apt.packages",
"sha1": "370c66437d49460dbc16be011209c4de9977212d",
"sha256": "37cb57f1554cbfa71c5a29ee9ffee18a9a8c1782bb0568e0874b7ff4ce8f9c11",
"size": 1394768,
"name": "Packages.xz",
"version": "jammy",
"vendor": "Ubuntu",
"description": "jammy main Packages file",
"author": "Ubuntu",
"architecture": "amd64",
"license": ""
},
"downloads": [
{
"url": "http://archive.ubuntu.com/ubuntu/dists/jammy/main/binary-amd64/by-hash/SHA256/37cb57f1554cbfa71c5a29ee9ffee18a9a8c1782bb0568e0874b7ff4ce8f9c11"
}
]
},
{
"metadata": {
"type": "application/x.canonical.snap-package",
"sha1": "376506001849698af3f9e07a236a47ee8cddded0",
"sha256": "cf7e02ebfdaa898107d2dbf84cf1231cee6c244dd5646580d09cfd6f6cf12577",
"size": 64892928,
"name": "go",
"version": "10660",
"vendor": "Canonical",
"description": "The Go programming language",
"author": "",
"architecture": "amd64",
"license": ""
},
"downloads": [
{
"url": "https://canonical-bos01.cdn.snapcraftcontent.com:443/download-origin/canonical-lgw01/Md1HBASHzP4i0bniScAjXGnOII9cEK6e_10660.snap?interactive=1&token=1720738800_68d3c27ac109407168ed776e46653c7883b8ef40"
}
]
}
]
}
Loading
Loading