Skip to content

Draft: Add BinaryApp application type and p47 example deployment #23

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

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
8 changes: 6 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"features": {
// add in eternal history and other bash features
"ghcr.io/diamondlightsource/devcontainer-features/bash-config:1.0.0": {}
"ghcr.io/diamondlightsource/devcontainer-features/bash-config:1": {}
},
// Create the config folder for the bash-config feature
"initializeCommand": "mkdir -p ${localEnv:HOME}/.config/bash-config",
Expand All @@ -42,7 +42,11 @@
"--device=/dev/fuse"
],
"mounts": [
"source=/dls_sw/,target=/dls_sw,type=bind,consistency=cached"
"source=/dls_sw/,target=/dls_sw,type=bind,consistency=cached",
// these two mounts allow us to pick up cache and config folders that have
// been symlinked out of the home directory
"source=/dls/science/users/,target=/dls/science/users,type=bind,consistency=cached",
"source=/scratch/,target=/scratch,type=bind,consistency=cached"
],
// Mount the parent as /workspaces so we can pip install peers as editable
"workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind",
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,6 @@ lockfiles/

# Demo output path for vscode tasks
demo-output

# vscode workspaces are local only
*.code-workspace
34 changes: 34 additions & 0 deletions src/deploy_tools/app_builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import uuid
from hashlib import sha256
from itertools import chain
from pathlib import Path
from urllib.request import urlretrieve

from deploy_tools.models.binary_app import BinaryApp

from .apptainer import create_sif_file
from .layout import ModuleBuildLayout
Expand All @@ -27,6 +31,8 @@
self._create_apptainer_files(app, module)
case ShellApp():
self._create_shell_file(app, module)
case BinaryApp():
self._create_binary_app(app, module)

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

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/app_builder.py#L34-L35

Added lines #L34 - L35 were not covered by tests

def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None:
"""Create apptainer entrypoints using a specified image and commands."""
Expand All @@ -44,6 +50,9 @@
entrypoint_file = entrypoints_folder / entrypoint.name

mounts = ",".join(chain(global_options.mounts, options.mounts)).strip()
host_binaries = " ".join(

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

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/app_builder.py#L53

Added line #L53 was not covered by tests
chain(global_options.host_binaries, options.host_binaries)
).strip()

apptainer_args = f"{global_options.apptainer_args} {options.apptainer_args}"
apptainer_args = apptainer_args.strip()
Expand All @@ -55,6 +64,7 @@

params = {
"mounts": mounts,
"host_binaries": host_binaries,
"apptainer_args": apptainer_args,
"relative_sif_file": relative_sif_file,
"command": command,
Expand Down Expand Up @@ -96,3 +106,27 @@
executable=True,
create_parents=True,
)

def _create_binary_app(self, app: BinaryApp, module: Module) -> None:
"""
Download a URL, validate it against its sha256, make it executable
and add it to PATH
"""
binary_folder = self._build_layout.get_binary_files_folder(

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

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/app_builder.py#L115

Added line #L115 was not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the same folder for all entrypoints. Maybe even rename it to bin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool - lets do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still todo - right now I'm using entrypoints only.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend calling it 'bin_folder' if you do decide to rename things, as 'binary_folder' isn't commonly used. I'm happy to make this change myself, later, if you want.

module.name, module.version
)
binary_path = binary_folder / app.name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything checking that app names are unique?

binary_path.parent.mkdir(parents=True, exist_ok=True)
urlretrieve(app.url, binary_path)

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

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/app_builder.py#L118-L120

Added lines #L118 - L120 were not covered by tests

h = sha256()
with open(binary_path, "rb") as fh:
while True:
data = fh.read(4096)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Buffer size should be module constant

if len(data) == 0:
break
h.update(data)
if h.hexdigest() != app.sha256:
raise RuntimeError(f"Downloaded Binary {app.url} digest failure")

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

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/app_builder.py#L122-L130

Added lines #L122 - L130 were not covered by tests

binary_path.chmod(0o555)

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#L132

Added line #L132 was not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Permissions should at least be a module constant. At the moment, it is all tidied away in templater.py, but this obviously breaks that and it may be sensible to move these permissions to another location.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely clear how you want to handle this. So I've left it as is for now in #26 - see #26 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an extra comment there - I recommend at least a module-level constant.

59 changes: 59 additions & 0 deletions src/deploy_tools/demo_configuration/edge-containers-cli/0.1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json

module:
name: edge-containers-cli
version: "0.1"
description: ec command line tool for kubernetes IOCs

env_vars:
- name: EC_CLI_BACKEND
value: ARGOCD
- name: EC_LOG_URL
value: "https://graylog.diamond.ac.uk/search?rangetype=relative&fields=message%2Csource&width=1489&highlightMessage=&relative=172800&q=pod_name%3A{service_name}*"
# default configuration is for p47 for testing
- name: EC_TARGET
value: p47-beamline/p47
- name: EC_SERVICES_REPO
value: https://github.com/epics-containers/p47-services

applications:
- app_type: apptainer

container:
path: docker://ghcr.io/epics-containers/edge-containers-cli
version: "4.4.1"

entrypoints:
- name: ec
command: ec
# for debugging enter the container with bash shell
- name: ec-bash
command: bash

global_options:
mounts:
# places to get argocd and kubectl config from
- /dls/science/users/:/dls/science/users/
- /scratch:/scratch
- /dls_sw/apps:/dls_sw/apps
host_binaries:
- argocd
# for future expansion and to demonstrate it can be done
# load in the remaining kubernetes tools
- kubectl
- helm
- kubelogin
- klogout
- kustomize
- kubeseal
- kubectl-oidc_login

- app_type: binary
name: argocd
url: https://github.com/argoproj/argo-cd/releases/download/v2.14.10/argocd-linux-amd64
sha256: d1750274a336f0a090abf196a832cee14cb9f1c2fc3d20d80b0dbfeff83550fa

- app_type: shell
name: ec-login
script:
- argocd login argocd.diamond.ac.uk --grpc-web --sso
20 changes: 20 additions & 0 deletions src/deploy_tools/demo_configuration/p47/0.1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# yaml-language-server: $schema=/workspaces/deploy-tools/src/deploy_tools/models/schemas/release.json

module:
name: p47
version: "0.1"
description: ec command line tool for kubernetes IOCs

env_vars:
- name: EC_TARGET
value: p47-beamline/p47
- name: EC_SERVICES_REPO
value: https://github.com/epics-containers/p47-services

dependencies:
- name: pollux
version: "local"
- name: edge-containers-cli
version: "0.1"

applications: []
11 changes: 11 additions & 0 deletions src/deploy_tools/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

ENTRYPOINTS_FOLDER = "entrypoints"
BINARIES_FOLDER = "binaries"

MODULE_SNAPSHOT_FILENAME = "module.yaml"
MODULEFILE_FILENAME = "modulefile"
Expand All @@ -21,6 +22,9 @@
def get_entrypoints_folder(self, name: str, version: str) -> Path:
return self.get_module_folder(name, version) / self.ENTRYPOINTS_FOLDER

def get_binaries_folder(self, name: str, version: str) -> Path:
return self.get_module_folder(name, version) / self.BINARIES_FOLDER

Check warning on line 26 in src/deploy_tools/layout.py

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/layout.py#L26

Added line #L26 was not covered by tests

def get_modulefile(self, name: str, version: str) -> Path:
return self.get_module_folder(name, version) / self.MODULEFILE_FILENAME

Expand All @@ -37,10 +41,14 @@
"""

SIF_FILES_FOLDER = "sif_files"
BINARY_FILES_FOLDER = "binaries"

def get_sif_files_folder(self, name: str, version: str) -> Path:
return self.get_module_folder(name, version) / self.SIF_FILES_FOLDER

def get_binary_files_folder(self, name: str, version: str) -> Path:
return self.get_module_folder(name, version) / self.BINARY_FILES_FOLDER

Check warning on line 50 in src/deploy_tools/layout.py

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/layout.py#L50

Added line #L50 was not covered by tests

@property
def build_root(self):
return self._root
Expand Down Expand Up @@ -73,6 +81,9 @@
def get_entrypoints_folder(self, name: str, version: str) -> Path:
return self._modules_layout.get_entrypoints_folder(name, version)

def get_binaries_folder(self, name: str, version: str) -> Path:
return self._modules_layout.get_binaries_folder(name, version)

Check warning on line 85 in src/deploy_tools/layout.py

View check run for this annotation

Codecov / codecov/patch

src/deploy_tools/layout.py#L85

Added line #L85 was not covered by tests

def get_modulefiles_root(self, from_deprecated: bool = False) -> Path:
return (
self.deprecated_modulefiles_root
Expand Down
1 change: 1 addition & 0 deletions src/deploy_tools/models/apptainer_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class EntrypointOptions(ParentModel):
apptainer_args: str = ""
command_args: str = ""
mounts: Sequence[str] = []
host_binaries: Sequence[str] = []


class Entrypoint(ParentModel):
Expand Down
17 changes: 17 additions & 0 deletions src/deploy_tools/models/binary_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Literal

from .parent import ParentModel


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

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

app_type: Literal["binary"]
name: str
url: str
sha256: str
5 changes: 4 additions & 1 deletion src/deploy_tools/models/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
from pydantic import Field

from .apptainer_app import ApptainerApp
from .binary_app import BinaryApp
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
65 changes: 55 additions & 10 deletions src/deploy_tools/models/schemas/deployment.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$defs": {
"Apptainer": {
"ApptainerApp": {
"additionalProperties": false,
"description": "Represents an Apptainer application or set of applications for a single image.\n\nThis uses Apptainer to deploy a portable image of the desired container. Several\nentrypoints can then be specified to allow for multiple commands run on the same\ncontainer image.",
"properties": {
Expand All @@ -24,7 +24,8 @@
"default": {
"apptainer_args": "",
"command_args": "",
"mounts": []
"mounts": [],
"host_binaries": []
}
}
},
Expand All @@ -33,7 +34,38 @@
"container",
"entrypoints"
],
"title": "Apptainer",
"title": "ApptainerApp",
"type": "object"
},
"BinaryApp": {
"additionalProperties": false,
"description": "Represents a standalone Binary application.\n\nThis will fetch a standalone binary, validate its sha256 and add its\nlocation to that path.",
"properties": {
"app_type": {
"const": "binary",
"title": "App Type",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"url": {
"title": "Url",
"type": "string"
},
"sha256": {
"title": "Sha256",
"type": "string"
}
},
"required": [
"app_type",
"name",
"url",
"sha256"
],
"title": "BinaryApp",
"type": "object"
},
"ContainerImage": {
Expand Down Expand Up @@ -98,7 +130,8 @@
"default": {
"apptainer_args": "",
"command_args": "",
"mounts": []
"mounts": [],
"host_binaries": []
}
}
},
Expand Down Expand Up @@ -128,6 +161,14 @@
},
"title": "Mounts",
"type": "array"
},
"host_binaries": {
"default": [],
"items": {
"type": "string"
},
"title": "Host Binaries",
"type": "array"
}
},
"title": "EntrypointOptions",
Expand Down Expand Up @@ -197,17 +238,21 @@
"items": {
"discriminator": {
"mapping": {
"apptainer": "#/$defs/Apptainer",
"shell": "#/$defs/Shell"
"apptainer": "#/$defs/ApptainerApp",
"binary": "#/$defs/BinaryApp",
"shell": "#/$defs/ShellApp"
},
"propertyName": "app_type"
},
"oneOf": [
{
"$ref": "#/$defs/Apptainer"
"$ref": "#/$defs/ApptainerApp"
},
{
"$ref": "#/$defs/ShellApp"
},
{
"$ref": "#/$defs/Shell"
"$ref": "#/$defs/BinaryApp"
}
]
},
Expand Down Expand Up @@ -281,7 +326,7 @@
},
"type": "object"
},
"Shell": {
"ShellApp": {
"additionalProperties": false,
"description": "Represents a Shell application.\n\nThis will run the code specified as a shell script. This currently uses Bash for\nimproved functionality while retaining high compatibility with various Linux\ndistributions.",
"properties": {
Expand All @@ -307,7 +352,7 @@
"name",
"script"
],
"title": "Shell",
"title": "ShellApp",
"type": "object"
}
},
Expand Down
Loading
Loading