Skip to content

add BinaryApp app type #26

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

Merged
merged 22 commits into from
Apr 26, 2025
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
42 changes: 42 additions & 0 deletions src/deploy_tools/app_builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import hashlib
import uuid
from itertools import chain
from pathlib import Path
from urllib.request import urlretrieve

from deploy_tools.models.binary_app import BinaryApp, HashType

from .apptainer import create_sif_file
from .layout import ModuleBuildLayout
Expand All @@ -9,6 +13,8 @@
from .models.shell_app import ShellApp
from .templater import Templater, TemplateType

ALL_READ_EXECUTE_PERMISSIONS = 0o555


class AppBuilderError(Exception):
pass
Expand All @@ -27,6 +33,8 @@
self._create_apptainer_files(app, module)
case ShellApp():
self._create_shell_file(app, module)
case BinaryApp():
self._create_binary_file(app, module)

def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None:
"""Create apptainer entrypoints using a specified image and commands."""
Expand Down Expand Up @@ -100,3 +108,37 @@
executable=True,
create_parents=True,
)

def _create_binary_file(self, app: BinaryApp, module: Module) -> None:
"""
Download a URL, validate it against its hash, make it executable
and add it to PATH
"""
binary_folder = self._build_layout.get_entrypoints_folder(
module.name, module.version
)
binary_path = binary_folder / app.name
binary_path.parent.mkdir(parents=True, exist_ok=True)
urlretrieve(app.url, binary_path)

match app.hash_type:
case HashType.SHA256:
h = hashlib.sha256()
case HashType.SHA512:
h = hashlib.sha512()
case HashType.MD5:
h = hashlib.md5()
case HashType.NONE:
h = None

Check warning on line 132 in src/deploy_tools/app_builder.py

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/app_builder.py#L127-L132

Added lines #L127 - L132 were not covered by tests

if h is not None:
with open(binary_path, "rb") as fh:
while True:
data = fh.read(4096)
if len(data) == 0:
break
h.update(data)
if h.hexdigest() != app.hash:
raise AppBuilderError(f"Downloaded Binary {app.url} hash check failed")

Check warning on line 142 in src/deploy_tools/app_builder.py

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/app_builder.py#L142

Added line #L142 was not covered by tests

binary_path.chmod(ALL_READ_EXECUTE_PERMISSIONS)
13 changes: 13 additions & 0 deletions src/deploy_tools/demo_configuration/argocd/0.1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json

module:
name: argocd
version: "0.1"
description: Demonstration of binary download

applications:
- app_type: binary
name: argocd
url: https://github.com/argoproj/argo-cd/releases/download/v2.14.10/argocd-linux-amd64
hash_type: sha256
hash: d1750274a336f0a090abf196a832cee14cb9f1c2fc3d20d80b0dbfeff83550fa
42 changes: 42 additions & 0 deletions src/deploy_tools/models/binary_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from enum import StrEnum
from typing import Literal

from pydantic import Field

from .parent import ParentModel


class HashType(StrEnum):
"""Type of hash to use for the binary."""

SHA256 = "sha256"
SHA512 = "sha512"
MD5 = "md5"
NONE = "none"


class BinaryApp(ParentModel):
"""
Represents a standalone Binary application.

This will fetch a standalone binary, validate its hash and add its
location to that path.
"""

app_type: Literal["binary"]
name: str = Field(
...,
description="Binary filename to use locally",
)
url: str = Field(
...,
description="URL to download the binary from.",
)
hash: str = Field(
"",
description="Hash to verify binary integrity",
)
hash_type: HashType = Field(
...,
description="Type of hash used to check the binary.",
)
6 changes: 5 additions & 1 deletion src/deploy_tools/models/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@

from pydantic import Field

from deploy_tools.models.binary_app import BinaryApp

from .apptainer_app import ApptainerApp
from .parent import ParentModel
from .shell_app import ShellApp

Application = Annotated[ApptainerApp | ShellApp, Field(..., discriminator="app_type")]
Application = Annotated[
ApptainerApp | ShellApp | BinaryApp, Field(..., discriminator="app_type")
]

DEVELOPMENT_VERSION = "dev"

Expand Down
13 changes: 11 additions & 2 deletions src/deploy_tools/models/parent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@


class ParentModel(BaseModel):
"""Will forbid any extra parameters being provided in any subclass."""
"""
Provides Model Config for all Pydantic models in this project:

model_config = ConfigDict(extra="forbid")
forbid: forbid any extra parameters being provided in any subclass.
use_enum_values: use the enum value only when serializing the model,
this means the yaml serializer can work with enums
"""

model_config = ConfigDict(
extra="forbid",
use_enum_values=True,
)
54 changes: 54 additions & 0 deletions src/deploy_tools/models/schemas/deployment.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,45 @@
"title": "ApptainerApp",
"type": "object"
},
"BinaryApp": {
"additionalProperties": false,
"description": "Represents a standalone Binary application.\n\nThis will fetch a standalone binary, validate its hash and add its\nlocation to that path.",
"properties": {
"app_type": {
"const": "binary",
"title": "App Type",
"type": "string"
},
"name": {
"description": "Binary filename to use locally",
"title": "Name",
"type": "string"
},
"url": {
"description": "URL to download the binary from.",
"title": "Url",
"type": "string"
},
"hash": {
"default": "",
"description": "Hash to verify binary integrity",
"title": "Hash",
"type": "string"
},
"hash_type": {
"$ref": "#/$defs/HashType",
"description": "Type of hash used to check the binary."
}
},
"required": [
"app_type",
"name",
"url",
"hash_type"
],
"title": "BinaryApp",
"type": "object"
},
"ContainerImage": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -167,6 +206,17 @@
"title": "EnvVar",
"type": "object"
},
"HashType": {
"description": "Type of hash to use for the binary.",
"enum": [
"sha256",
"sha512",
"md5",
"none"
],
"title": "HashType",
"type": "string"
},
"Module": {
"additionalProperties": false,
"description": "Represents a Module to be deployed.\n\nModules can optionally include a set of applications, environment variables to load,\nand a list of module dependencies.",
Expand Down Expand Up @@ -212,6 +262,7 @@
"discriminator": {
"mapping": {
"apptainer": "#/$defs/ApptainerApp",
"binary": "#/$defs/BinaryApp",
"shell": "#/$defs/ShellApp"
},
"propertyName": "app_type"
Expand All @@ -222,6 +273,9 @@
},
{
"$ref": "#/$defs/ShellApp"
},
{
"$ref": "#/$defs/BinaryApp"
}
]
},
Expand Down
54 changes: 54 additions & 0 deletions src/deploy_tools/models/schemas/module.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,45 @@
"title": "ApptainerApp",
"type": "object"
},
"BinaryApp": {
"additionalProperties": false,
"description": "Represents a standalone Binary application.\n\nThis will fetch a standalone binary, validate its hash and add its\nlocation to that path.",
"properties": {
"app_type": {
"const": "binary",
"title": "App Type",
"type": "string"
},
"name": {
"description": "Binary filename to use locally",
"title": "Name",
"type": "string"
},
"url": {
"description": "URL to download the binary from.",
"title": "Url",
"type": "string"
},
"hash": {
"default": "",
"description": "Hash to verify binary integrity",
"title": "Hash",
"type": "string"
},
"hash_type": {
"$ref": "#/$defs/HashType",
"description": "Type of hash used to check the binary."
}
},
"required": [
"app_type",
"name",
"url",
"hash_type"
],
"title": "BinaryApp",
"type": "object"
},
"ContainerImage": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -149,6 +188,17 @@
"title": "EnvVar",
"type": "object"
},
"HashType": {
"description": "Type of hash to use for the binary.",
"enum": [
"sha256",
"sha512",
"md5",
"none"
],
"title": "HashType",
"type": "string"
},
"Module": {
"additionalProperties": false,
"description": "Represents a Module to be deployed.\n\nModules can optionally include a set of applications, environment variables to load,\nand a list of module dependencies.",
Expand Down Expand Up @@ -194,6 +244,7 @@
"discriminator": {
"mapping": {
"apptainer": "#/$defs/ApptainerApp",
"binary": "#/$defs/BinaryApp",
"shell": "#/$defs/ShellApp"
},
"propertyName": "app_type"
Expand All @@ -204,6 +255,9 @@
},
{
"$ref": "#/$defs/ShellApp"
},
{
"$ref": "#/$defs/BinaryApp"
}
]
},
Expand Down
Loading
Loading