Skip to content

Commit 5e42620

Browse files
committed
add support for registry creds
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
1 parent 81a3638 commit 5e42620

3 files changed

Lines changed: 138 additions & 21 deletions

File tree

python/packages/jumpstarter-driver-ridesx/jumpstarter_driver_ridesx/client.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from dataclasses import dataclass
23
from pathlib import Path
34
from typing import Dict, Optional
@@ -215,12 +216,33 @@ def _flash_operation():
215216

216217
return self._execute_flash_operation(_flash_operation)
217218

219+
def _read_oci_credentials(self):
220+
"""Read OCI registry credentials from environment variables.
221+
222+
Returns:
223+
Tuple of (username, password), both None if not set.
224+
225+
Raises:
226+
click.ClickException: If only one of username/password is set.
227+
"""
228+
username = os.environ.get("OCI_USERNAME")
229+
password = os.environ.get("OCI_PASSWORD")
230+
231+
if bool(username) != bool(password):
232+
raise click.ClickException(
233+
"OCI authentication requires both OCI_USERNAME and OCI_PASSWORD environment variables"
234+
)
235+
236+
return username, password
237+
218238
def _flash_oci_auto_impl(
219239
self,
220240
oci_url: str,
221241
partitions: Dict[str, str] | None = None,
222242
):
223243
"""Core implementation of OCI flash without wrapper logic."""
244+
oci_username, oci_password = self._read_oci_credentials()
245+
224246
self.logger.info("Checking for fastboot devices on Exporter...")
225247
detection_result = self.call("detect_fastboot_device", 5, 2.0)
226248

@@ -230,7 +252,10 @@ def _flash_oci_auto_impl(
230252
device_id = detection_result["device_id"]
231253
self.logger.info(f"Found fastboot device: {device_id}")
232254

233-
flash_result = self.call("flash_oci_image", oci_url, partitions)
255+
flash_result = self.call(
256+
"flash_oci_image", oci_url, partitions,
257+
oci_username, oci_password,
258+
)
234259

235260
# Display FLS output to user
236261
if flash_result.get("status") == "success" and flash_result.get("output"):
@@ -360,6 +385,9 @@ def flash(path, target_specs):
360385
# OCI with explicit partition->filename mapping
361386
j storage flash -t boot_a:boot.img oci://registry.com/image:tag
362387
388+
# OCI with registry credentials (via env vars)
389+
OCI_USERNAME=user OCI_PASSWORD=pass j storage flash oci://registry.com/image:tag
390+
363391
# Single file to partition
364392
j storage flash /local/boot.img --target boot_a
365393
@@ -368,6 +396,11 @@ def flash(path, target_specs):
368396
369397
# HTTP URLs
370398
j storage flash -t boot_a:https://example.com/boot.img
399+
400+
\b
401+
Environment variables:
402+
OCI_USERNAME Registry username for private OCI images
403+
OCI_PASSWORD Registry password for private OCI images
371404
"""
372405
self._execute_flash_command(path, target_specs)
373406

python/packages/jumpstarter-driver-ridesx/jumpstarter_driver_ridesx/driver.py

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import os
23
import subprocess
34
import time
45
from dataclasses import dataclass, field
@@ -220,21 +221,8 @@ def flash_with_fastboot(self, device_id: str, partitions: Dict[str, str]):
220221
self.logger.warning(f"stdout: {e.stdout}")
221222
self.logger.warning(f"stderr: {e.stderr}")
222223

223-
@export
224-
def flash_oci_image(
225-
self,
226-
oci_url: str,
227-
partitions: Dict[str, str] | None = None,
228-
):
229-
"""Flash OCI image using FLS fastboot CLI
230-
231-
Args:
232-
oci_url: OCI image reference (e.g., "quay.io/bzlotnik/ridesx-image:latest")
233-
partitions: Optional mapping of partition -> filename inside OCI image
234-
"""
235-
if not oci_url.startswith("oci://"):
236-
raise ValueError(f"OCI URL must start with oci://, got: {oci_url}")
237-
224+
def _build_fls_command(self, oci_url, partitions):
225+
"""Build FLS fastboot command and environment."""
238226
fls_binary = get_fls_binary(
239227
fls_version=self.fls_version,
240228
fls_binary_url=self.fls_custom_binary_url,
@@ -251,13 +239,47 @@ def flash_oci_image(
251239
)
252240
fls_cmd.extend(["-t", f"{partition_name}:{filename}"])
253241

254-
# Align fastboot timeout with driver timeout
255242
fls_cmd.extend(["--timeout", str(self.flash_timeout)])
243+
return fls_cmd
244+
245+
@export
246+
def flash_oci_image(
247+
self,
248+
oci_url: str,
249+
partitions: Dict[str, str] | None = None,
250+
oci_username: str | None = None,
251+
oci_password: str | None = None,
252+
):
253+
"""Flash OCI image using FLS fastboot CLI
254+
255+
Args:
256+
oci_url: OCI image reference (e.g., "quay.io/bzlotnik/ridesx-image:latest")
257+
partitions: Optional mapping of partition -> filename inside OCI image
258+
oci_username: Registry username for OCI authentication
259+
oci_password: Registry password for OCI authentication
260+
"""
261+
if not oci_url.startswith("oci://"):
262+
raise ValueError(f"OCI URL must start with oci://, got: {oci_url}")
263+
264+
if bool(oci_username) != bool(oci_password):
265+
raise ValueError("OCI authentication requires both --username and --password")
266+
267+
fls_cmd = self._build_fls_command(oci_url, partitions)
268+
269+
fls_env = os.environ.copy()
270+
if oci_username and oci_password:
271+
fls_env["FLS_REGISTRY_USERNAME"] = oci_username
272+
fls_env["FLS_REGISTRY_PASSWORD"] = oci_password
256273

257274
self.logger.info(f"Running FLS fastboot: {' '.join(fls_cmd)}")
275+
if oci_username:
276+
self.logger.info("Using OCI registry credentials from environment")
258277

259278
try:
260-
result = subprocess.run(fls_cmd, capture_output=True, text=True, check=True, timeout=self.flash_timeout)
279+
result = subprocess.run(
280+
fls_cmd, capture_output=True, text=True,
281+
check=True, timeout=self.flash_timeout, env=fls_env,
282+
)
261283

262284
self.logger.info("FLS fastboot auto-detection completed successfully")
263285
self.logger.debug(f"FLS stdout: {result.stdout}")
@@ -267,10 +289,11 @@ def flash_oci_image(
267289
return {"status": "success", "output": result.stdout}
268290

269291
except subprocess.CalledProcessError as e:
270-
self.logger.error(f"FLS fastboot auto-detection failed - return code: {e.returncode}")
292+
self.logger.error(f"FLS fastboot failed - return code: {e.returncode}")
271293
self.logger.error(f"stdout: {e.stdout}")
272294
self.logger.error(f"stderr: {e.stderr}")
273-
raise RuntimeError(f"FLS fastboot auto-detection failed: {e}") from e
295+
output = (e.stderr or e.stdout or "").strip()
296+
raise RuntimeError(f"FLS fastboot failed: {output}") from e
274297

275298
except subprocess.TimeoutExpired:
276299
self.logger.error("FLS fastboot auto-detection timed out")

python/packages/jumpstarter-driver-ridesx/jumpstarter_driver_ridesx/driver_test.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ def test_flash_oci_image_error_cases(temp_storage_dir, ridesx_driver):
515515
error.stderr = "Flash failed"
516516
mock_subprocess.side_effect = error
517517

518-
with pytest.raises(DriverError, match="FLS fastboot auto-detection failed"):
518+
with pytest.raises(DriverError, match="FLS fastboot failed: Flash failed"):
519519
client.call("flash_oci_image", "oci://image:tag", None)
520520

521521
# TimeoutExpired
@@ -531,6 +531,67 @@ def test_flash_oci_image_error_cases(temp_storage_dir, ridesx_driver):
531531
client.call("flash_oci_image", "oci://image:tag", None)
532532

533533

534+
def test_flash_oci_image_with_credentials(temp_storage_dir, ridesx_driver):
535+
"""Test that OCI credentials are passed via env vars to FLS"""
536+
with serve(ridesx_driver) as client:
537+
with patch("jumpstarter_driver_ridesx.driver.get_fls_binary", return_value="fls"):
538+
with patch("subprocess.run") as mock_subprocess:
539+
mock_result = MagicMock()
540+
mock_result.stdout = "Flashing complete"
541+
mock_result.stderr = ""
542+
mock_result.returncode = 0
543+
mock_subprocess.return_value = mock_result
544+
545+
result = client.call(
546+
"flash_oci_image", "oci://quay.io/private/image:tag", None, "myuser", "mypass"
547+
)
548+
549+
assert result["status"] == "success"
550+
# Credentials should NOT appear in the command args
551+
call_args = mock_subprocess.call_args[0][0]
552+
assert "-u" not in call_args
553+
assert "-p" not in call_args
554+
assert "myuser" not in call_args
555+
assert "mypass" not in call_args
556+
# Credentials should be passed via env vars
557+
call_kwargs = mock_subprocess.call_args[1]
558+
env = call_kwargs["env"]
559+
assert env["FLS_REGISTRY_USERNAME"] == "myuser"
560+
assert env["FLS_REGISTRY_PASSWORD"] == "mypass"
561+
562+
563+
def test_flash_oci_image_partial_credentials_rejected(temp_storage_dir, ridesx_driver):
564+
"""Test that providing only username or only password is rejected"""
565+
from jumpstarter.client.core import DriverError
566+
567+
with serve(ridesx_driver) as client:
568+
with pytest.raises(DriverError, match="OCI authentication requires both"):
569+
client.call("flash_oci_image", "oci://image:tag", None, "myuser", None)
570+
571+
with pytest.raises(DriverError, match="OCI authentication requires both"):
572+
client.call("flash_oci_image", "oci://image:tag", None, None, "mypass")
573+
574+
575+
def test_flash_oci_image_no_credentials(temp_storage_dir, ridesx_driver):
576+
"""Test that omitting credentials works (anonymous access)"""
577+
with serve(ridesx_driver) as client:
578+
with patch("jumpstarter_driver_ridesx.driver.get_fls_binary", return_value="fls"):
579+
with patch("subprocess.run") as mock_subprocess:
580+
mock_result = MagicMock()
581+
mock_result.stdout = "Flashing complete"
582+
mock_result.stderr = ""
583+
mock_result.returncode = 0
584+
mock_subprocess.return_value = mock_result
585+
586+
result = client.call("flash_oci_image", "oci://image:tag", None, None, None)
587+
588+
assert result["status"] == "success"
589+
call_kwargs = mock_subprocess.call_args[1]
590+
env = call_kwargs["env"]
591+
assert "FLS_REGISTRY_USERNAME" not in env
592+
assert "FLS_REGISTRY_PASSWORD" not in env
593+
594+
534595
def test_flash_oci_image_requires_oci_scheme(temp_storage_dir, ridesx_driver):
535596
"""Test that only oci:// URLs are accepted"""
536597
from jumpstarter.client.core import DriverError

0 commit comments

Comments
 (0)