Skip to content

Commit 00dd148

Browse files
committed
Enable test-app with nv-gha-runner
- Updated `.github/copy-pr-bot.yaml` to initialize empty lists for additional trustees and vetters. - Added support for `--add-host` argument in packaging commands and adjusted related classes to handle new parameters. - Added support for `--rm` to remove the container after "run" exits - Improved error handling and logging in container operations. - Updated unit tests to cover new functionality related to host mappings. Signed-off-by: Victor Chang <[email protected]>
1 parent 2aea3d2 commit 00dd148

15 files changed

+317
-59
lines changed

.github/copy-pr-bot.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515

1616
enabled: true
17-
additional_trustees:
18-
additional_vetters:
17+
additional_trustees: []
18+
additional_vetters: []
1919
auto_sync_draft: false
2020
auto_sync_ready: true

.github/workflows/main.yaml

+60-22
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ name: Code Check
2424

2525
on:
2626
push:
27-
branches: ["main", "release/*"]
28-
pull_request:
29-
branches: ["main", "release/*"]
3027

3128
jobs:
3229
pre-commit:
@@ -116,8 +113,12 @@ jobs:
116113
file: tests/reports/.coverage.lcov
117114

118115
test-app:
119-
if: false # disable until GPU supported nodes become available
120-
runs-on: ${{ matrix.os }}
116+
# runs-on: ${{ matrix.os }}
117+
runs-on: linux-amd64-gpu-l4-latest-1
118+
container:
119+
image: nvidia/cuda:12.8.1-runtime-ubuntu22.04
120+
env:
121+
NVIDIA_VISIBLE_DEVICES: ${{ env.NVIDIA_VISIBLE_DEVICES }}
121122
strategy:
122123
matrix:
123124
os: [ubuntu-latest]
@@ -126,6 +127,34 @@ jobs:
126127
PYTHON_VERSION: ${{ matrix.python-version }}
127128

128129
steps:
130+
- name: Install Git & JQ
131+
run: |
132+
apt-get update
133+
apt-get install -y git jq
134+
git --version
135+
jq --version
136+
echo $(pwd)
137+
# Configure Git to handle the repository ownership
138+
git config --global --add safe.directory $(pwd)
139+
140+
- name: Install Docker
141+
run: |
142+
# Add Docker's official GPG key:
143+
apt-get update
144+
apt-get install -y ca-certificates curl
145+
install -m 0755 -d /etc/apt/keyrings
146+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
147+
chmod a+r /etc/apt/keyrings/docker.asc
148+
149+
# Add the repository to Apt sources:
150+
echo \
151+
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
152+
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
153+
tee /etc/apt/sources.list.d/docker.list > /dev/null
154+
apt-get update
155+
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
156+
docker --version
157+
129158
- name: Checkout repository
130159
uses: actions/checkout@v4
131160

@@ -142,39 +171,48 @@ jobs:
142171
poetry run pip install --upgrade pip setuptools
143172
poetry install --with test
144173
174+
- name: Locate Latest Artifacts
175+
id: latest_artifact_path
176+
run: |
177+
echo "ARTIFACT_PATH=$(find releases -type f | sort | tail -n 1)" >> $GITHUB_OUTPUT
178+
echo "CLI_VERSION=$(jq -r 'keys | sort_by(split(".") | map(tonumber)) | reverse | first' releases/3.0.0/artifacts.json)" >> $GITHUB_OUTPUT
179+
145180
- name: Build and Install CLI
146181
run: |
147182
poetry build
148183
wheel=$(find dist/ -name holoscan_cli-*.whl)
149184
echo "Installing from ${wheel}"
150185
pip install ${wheel}
151186
152-
- name: Version Check
153-
run: |
154-
output=$(holoscan version | tail -1 | tr -s ' ' | cut -d' ' -f3)
155-
expected=$(grep -m 1 version pyproject.toml | tr -s ' ' | tr -d '"' | tr -d "'" | cut -d' ' -f3)
156-
echo "Expected version: ${expected}"
157-
echo " Actual version: "$output""
158-
test "$output" = "$expected"
159-
160187
- name: Package Test App
188+
env:
189+
ARTIFACT_PATH: ${{ steps.latest_artifact_path.outputs.ARTIFACT_PATH }}
190+
CLI_VERSION: ${{ steps.latest_artifact_path.outputs.CLI_VERSION }}
161191
run: |
162-
holoscan package --source tests/app/artifacts.json \
192+
holoscan package tests/app/python/ \
193+
-l DEBUG \
163194
-c tests/app/python/app.yaml \
164195
-t test-app-python \
165-
--platform x64-workstation \
166-
--sdk-version 0.0.0 \
167-
tests/app/python/
196+
--platform x86_64 \
197+
--uid 1000 \
198+
--gid 1000 \
199+
--source ${{ env.ARTIFACT_PATH }} \
200+
--sdk-version ${{ env.CLI_VERSION }} \
201+
--add-host developer.download.nvidia.com:23.46.17.44 \
202+
--add-host security.ubuntu.com:91.189.91.81 \
203+
--add-host archive.ubuntu.com:91.189.91.82 \
204+
--add-host pypi.org:151.101.128.223 \
205+
--add-host edge.urm.nvidia.com:23.46.228.176 \
206+
--add-host www.mellanox.com:23.46.228.176 \
207+
--add-host files.pythonhosted.org:151.101.0.223
168208
169209
- name: Run Test App
170-
env:
171-
HOLOSCAN_SKIP_NVIDIA_CTK_CHECK: true
172210
run: |
173211
mkdir input
174212
mkdir output
175-
touch input/file
176-
touch output/file
177-
holoscan run -r $(docker images | grep "test-app-python" | awk '{print $1":"$2}') -i input -o output
213+
touch input/file1
214+
touch output/file2
215+
holoscan run --rm --uid 1000 --gid 1000 -r $(docker images | grep "test-app-python" | awk '{print $1":"$2}') -i input -o output
178216
179217
testpypi-deploy:
180218
name: publish-test-pypi

src/holoscan_cli/common/argparse_types.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import argparse
1616
import os
1717
from pathlib import Path
18-
1918
from .constants import SDK
2019
from .enum_types import Platform, PlatformConfiguration, SdkType
2120

@@ -155,3 +154,18 @@ def valid_sdk_type(sdk_str: str) -> SdkType:
155154
raise argparse.ArgumentTypeError(f"{sdk_str} is not a valid option for --sdk.")
156155

157156
return SdkType(sdk_str)
157+
158+
159+
def validate_host_ip(host_ip: str) -> str:
160+
"""Helper type checking and type converting method for ArgumentParser.add_argument
161+
to convert check valid host:ip format.
162+
163+
Args:
164+
host_ip: host ip string
165+
"""
166+
167+
host, ip = host_ip.split(":")
168+
if host == "" or ip == "":
169+
raise argparse.ArgumentTypeError(f"Invalid valid for --add-host: '{host_ip}'")
170+
171+
return host_ip

src/holoscan_cli/common/dockerutils.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def docker_run(
172172
platform_config: str,
173173
shared_memory_size: str = "1GB",
174174
is_root: bool = False,
175+
remove: bool = False,
175176
):
176177
"""Creates and runs a Docker container
177178
@@ -351,6 +352,7 @@ def docker_run(
351352
ulimits,
352353
devices,
353354
group_adds,
355+
remove,
354356
)
355357
else:
356358
_start_container(
@@ -368,6 +370,7 @@ def docker_run(
368370
ulimits,
369371
devices,
370372
group_adds,
373+
remove,
371374
)
372375

373376

@@ -386,6 +389,7 @@ def _start_container(
386389
ulimits,
387390
devices,
388391
group_adds,
392+
remove,
389393
):
390394
container = docker.container.create(
391395
image_name,
@@ -394,7 +398,7 @@ def _start_container(
394398
hostname=name,
395399
name=name,
396400
networks=[network],
397-
remove=True,
401+
remove=False,
398402
shm_size=shared_memory_size,
399403
user=user,
400404
volumes=volumes,
@@ -441,7 +445,18 @@ def _start_container(
441445
except Exception:
442446
print(str(log[1]))
443447

444-
logger.info(f"Container '{container_name}'({container_id}) exited.")
448+
exit_code = container.state.exit_code
449+
logger.info(
450+
f"Container '{container_name}'({container_id}) exited with code {exit_code}."
451+
)
452+
453+
if remove:
454+
container.remove()
455+
456+
if exit_code != 0:
457+
raise RuntimeError(
458+
f"Container '{container_name}'({container_id}) exited with code {exit_code}."
459+
)
445460

446461

447462
def _enter_terminal(
@@ -457,6 +472,7 @@ def _enter_terminal(
457472
ulimits,
458473
devices,
459474
group_adds,
475+
remove,
460476
):
461477
print("\n\nEntering terminal...")
462478
print(
@@ -475,7 +491,7 @@ def _enter_terminal(
475491
interactive=True,
476492
name=name,
477493
networks=[network],
478-
remove=True,
494+
remove=remove,
479495
shm_size=shared_memory_size,
480496
tty=True,
481497
user=user,

src/holoscan_cli/packager/arguments.py

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __init__(self, args: Namespace, temp_dir: str) -> None:
7676
self.build_parameters.cmake_args = args.cmake_args
7777
self.build_parameters.includes = args.includes
7878
self.build_parameters.additional_libs = args.additional_libs
79+
self.build_parameters.add_hosts = args.add_hosts
7980

8081
models = Models()
8182
platform = Platform(self._artifact_sources)

src/holoscan_cli/packager/container_builder.py

+34-17
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def _build_internal(
8888
Returns:
8989
PlatformBuildResults: build results
9090
"""
91-
self.print_build_info(platform_parameters)
9291
builder = create_and_get_builder(Constants.LOCAL_BUILDX_BUILDER_NAME)
9392

9493
build_result = PlatformBuildResults(platform_parameters)
@@ -117,9 +116,15 @@ def _build_internal(
117116
"tags": [platform_parameters.tag],
118117
}
119118

119+
if self._build_parameters.add_hosts:
120+
builds["add_hosts"] = {}
121+
for host in self._build_parameters.add_hosts:
122+
host_name, host_ip = host.split(":")
123+
builds["add_hosts"][host_name] = host_ip
124+
120125
export_to_tar_ball = False
121126
if self._build_parameters.tarball_output is not None:
122-
build_result.tarball_filenaem = str(
127+
build_result.tarball_filename = str(
123128
self._build_parameters.tarball_output
124129
/ f"{platform_parameters.tag}{Constants.TARBALL_FILE_EXTENSION}"
125130
).replace(":", "-")
@@ -134,7 +139,7 @@ def _build_internal(
134139
builds["output"] = {
135140
# type=oci cannot be loaded by docker: https://github.com/docker/buildx/issues/59
136141
"type": "docker",
137-
"dest": build_result.tarball_filenaem,
142+
"dest": build_result.tarball_filename,
138143
}
139144
else:
140145
build_result.succeeded = False
@@ -155,21 +160,24 @@ def _build_internal(
155160
f"Building Holoscan Application Package: tag={platform_parameters.tag}"
156161
)
157162

163+
self.print_build_info(platform_parameters)
164+
158165
try:
159166
build_docker_image(**builds)
160167
build_result.succeeded = True
161168
if export_to_tar_ball:
162169
try:
163170
self._logger.info(
164-
f"Saving {platform_parameters.tag} to {build_result.tarball_filenaem}..."
171+
f"Saving {platform_parameters.tag} to {build_result.tarball_filename}..."
165172
)
166173
docker_export_tarball(
167-
build_result.tarball_filenaem, platform_parameters.tag
174+
build_result.tarball_filename, platform_parameters.tag
168175
)
169176
except Exception as ex:
170177
build_result.error = f"Error saving tarball: {ex}"
171178
build_result.succeeded = False
172-
except Exception:
179+
except Exception as e:
180+
print(e)
173181
build_result.succeeded = False
174182
build_result.error = (
175183
"Error building image: see Docker output for additional details."
@@ -378,18 +386,27 @@ def _copy_pip_requirements(self):
378386
os.makedirs(pip_folder, exist_ok=True)
379387
pip_requirements_path = os.path.join(pip_folder, "requirements.txt")
380388

389+
# Build requirements content first
390+
requirements_content = []
391+
if self._build_parameters.requirements_file_path is not None:
392+
with open(self._build_parameters.requirements_file_path) as lr:
393+
requirements_content.extend(lr)
394+
requirements_content.append("")
395+
396+
if self._build_parameters.pip_packages:
397+
requirements_content.extend(self._build_parameters.pip_packages)
398+
399+
# Write all content at once
381400
with open(pip_requirements_path, "w") as requirements_file:
382-
# Use local requirements.txt packages if provided, otherwise use sdk provided packages
383-
if self._build_parameters.requirements_file_path is not None:
384-
with open(self._build_parameters.requirements_file_path) as lr:
385-
for line in lr:
386-
requirements_file.write(line)
387-
requirements_file.writelines("\n")
388-
389-
if self._build_parameters.pip_packages:
390-
requirements_file.writelines(
391-
"\n".join(self._build_parameters.pip_packages)
392-
)
401+
requirements_file.writelines(requirements_content)
402+
self._logger.debug(
403+
"================ Begin requirements.txt ================"
404+
)
405+
for req in requirements_content:
406+
self._logger.debug(f" {req.strip()}")
407+
self._logger.debug(
408+
"================ End requirements.txt =================="
409+
)
393410

394411
def _copy_sdk_file(self, sdk_file: Optional[Path]):
395412
if sdk_file is not None and os.path.isfile(sdk_file):

src/holoscan_cli/packager/package_command.py

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from packaging.version import Version
2020

2121
from ..common.argparse_types import (
22+
validate_host_ip,
2223
valid_dir_path,
2324
valid_existing_dir_path,
2425
valid_existing_path,
@@ -84,6 +85,13 @@ def create_package_parser(
8485
)
8586

8687
advanced_group = parser.add_argument_group(title="advanced build options")
88+
advanced_group.add_argument(
89+
"--add-host",
90+
action="append",
91+
dest="add_hosts",
92+
type=validate_host_ip,
93+
help="Add a custom host-to-IP mapping (format: host:ip).",
94+
)
8795
advanced_group.add_argument(
8896
"--base-image",
8997
type=str,

src/holoscan_cli/packager/packager.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def _package_application(args: Namespace):
103103
f"""\nPlatform: {result.parameters.platform.value}/{result.parameters.platform_config.value}
104104
Status: Succeeded
105105
Docker Tag: {result.docker_tag if result.docker_tag is not None else "N/A"}
106-
Tarball: {result.tarball_filenaem}""" # noqa: E501
106+
Tarball: {result.tarball_filename}""" # noqa: E501
107107
)
108108
else:
109109
print(

0 commit comments

Comments
 (0)