Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 1bec504

Browse files
committed
support oci:// urls
Accept the oci:// scheme for flashing images from the registry Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
1 parent 664cd20 commit 1bec504

File tree

1 file changed

+43
-25
lines changed
  • packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers

1 file changed

+43
-25
lines changed

packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class FlashRetryableError(FlashError):
3838
class FlashNonRetryableError(FlashError):
3939
"""Exception for non-retryable flash errors (configuration, file system, etc.)."""
4040

41+
4142
debug_console_option = click.option("--console-debug", is_flag=True, help="Enable console debug mode")
4243

4344
EXPECT_TIMEOUT_DEFAULT = 60
@@ -113,8 +114,11 @@ def flash( # noqa: C901
113114
image_url = ""
114115
original_http_url = None
115116
operator_scheme = None
116-
# initrmafs cannot handle https yet, fallback to using the exporter's http server
117-
if path.startswith(("http://", "https://")) and not force_exporter_http:
117+
if path.startswith("oci://"):
118+
# OCI URLs are always passed directly to fls
119+
image_url = path
120+
should_download_to_httpd = False
121+
elif path.startswith(("http://", "https://")) and not force_exporter_http:
118122
# the flasher image can handle the http(s) from a remote directly, unless target is isolated
119123
image_url = path
120124
should_download_to_httpd = False
@@ -172,9 +176,19 @@ def flash( # noqa: C901
172176
for attempt in range(retries + 1): # +1 for initial attempt
173177
try:
174178
self._perform_flash_operation(
175-
partition, path, image_url, should_download_to_httpd,
176-
storage_thread, error_queue, cacert_file, insecure_tls,
177-
headers, bearer_token, method, fls_version, fls_binary_url
179+
partition,
180+
path,
181+
image_url,
182+
should_download_to_httpd,
183+
storage_thread,
184+
error_queue,
185+
cacert_file,
186+
insecure_tls,
187+
headers,
188+
bearer_token,
189+
method,
190+
fls_version,
191+
fls_binary_url,
178192
)
179193
self.logger.info(f"Flash operation succeeded on attempt {attempt + 1}")
180194
break
@@ -194,15 +208,14 @@ def flash( # noqa: C901
194208
)
195209
self.logger.info(f"Retrying flash operation (attempt {attempt + 2}/{retries + 1})")
196210
# Wait a bit before retrying
197-
time.sleep(2 ** attempt) # Exponential backoff
211+
time.sleep(2**attempt) # Exponential backoff
198212
continue
199213
else:
200214
self.logger.error(f"Flash operation failed after {retries + 1} attempts")
201215
raise FlashError(
202216
f"Flash operation failed after {retries + 1} attempts. Last error: {categorized_error}"
203217
) from e
204218

205-
206219
total_time = time.time() - start_time
207220
# total time in minutes:seconds
208221
minutes, seconds = divmod(total_time, 60)
@@ -262,7 +275,7 @@ def _find_exception_in_chain(self, exception: Exception, target_type: type) -> E
262275
The found exception instance if found, None otherwise
263276
"""
264277
# Check if this is an ExceptionGroup and look through its exceptions
265-
if hasattr(exception, 'exceptions'):
278+
if hasattr(exception, "exceptions"):
266279
for sub_exc in exception.exceptions:
267280
result = self._find_exception_in_chain(sub_exc, target_type)
268281
if result is not None:
@@ -273,17 +286,17 @@ def _find_exception_in_chain(self, exception: Exception, target_type: type) -> E
273286
return exception
274287

275288
# Check the cause chain
276-
current = getattr(exception, '__cause__', None)
289+
current = getattr(exception, "__cause__", None)
277290
while current is not None:
278291
if isinstance(current, target_type):
279292
return current
280293
# Also check if the cause is an ExceptionGroup
281-
if hasattr(current, 'exceptions'):
294+
if hasattr(current, "exceptions"):
282295
for sub_exc in current.exceptions:
283296
result = self._find_exception_in_chain(sub_exc, target_type)
284297
if result is not None:
285298
return result
286-
current = getattr(current, '__cause__', None)
299+
current = getattr(current, "__cause__", None)
287300
return None
288301

289302
def _perform_flash_operation(
@@ -353,7 +366,6 @@ def _perform_flash_operation(
353366

354367
header_args = self._prepare_headers(headers, bearer_token)
355368

356-
357369
if method == "fls":
358370
self._flash_with_fls(
359371
console,
@@ -504,10 +516,7 @@ def _flash_with_fls(
504516
elif fls_version != "":
505517
self.logger.info(f"Downloading FLS version {fls_version} from GitHub releases")
506518
# Download fls binary to the target device (until it is available on the target device)
507-
fls_url = (
508-
f"https://github.com/jumpstarter-dev/fls/releases/download/{fls_version}/"
509-
f"fls-aarch64-linux"
510-
)
519+
fls_url = f"https://github.com/jumpstarter-dev/fls/releases/download/{fls_version}/fls-aarch64-linux"
511520
console.sendline(f"curl -L {fls_url} -o /sbin/fls")
512521
console.expect(prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
513522
console.sendline("echo $?")
@@ -547,7 +556,7 @@ def _monitor_fls_progress(self, console, prompt):
547556
if len(current_output) > last_printed_length:
548557
new_output = current_output[last_printed_length:]
549558
if new_output:
550-
print(new_output, end='', flush=True)
559+
print(new_output, end="", flush=True)
551560
last_printed_length = len(current_output)
552561

553562
# Check if we matched the prompt (index 0 means prompt matched)
@@ -556,7 +565,7 @@ def _monitor_fls_progress(self, console, prompt):
556565
break
557566
# If match_index is 1, it means TIMEOUT was matched, so we continue the loop
558567

559-
if 'panicked at' in current_output:
568+
if "panicked at" in current_output:
560569
raise FlashRetryableError(f"FLS panicked: {current_output}")
561570

562571
except pexpect.EOF as err:
@@ -610,8 +619,8 @@ def _flash_with_progress(
610619
flash_cmd = (
611620
f'( set -o pipefail; curl -fsSL {tls_args} {header_args} "{image_url}" | '
612621
f"{decompress_cmd} "
613-
f'dd of={target_path} bs=64k iflag=fullblock oflag=direct ' +
614-
'&& echo "F""LASH_COMPLETE" || echo "F""LASH_FAILED" ) &'
622+
f"dd of={target_path} bs=64k iflag=fullblock oflag=direct "
623+
+ '&& echo "F""LASH_COMPLETE" || echo "F""LASH_FAILED" ) &'
615624
)
616625
console.sendline(flash_cmd)
617626
console.expect(prompt, timeout=EXPECT_TIMEOUT_DEFAULT * 2)
@@ -629,7 +638,7 @@ def _monitor_flash_progress(self, console, prompt):
629638
console.sendline("pidof dd")
630639
console.expect(prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
631640
pidof_output = console.before.decode(errors="ignore")
632-
accumulated_output = pidof_output # just in case we get the FLASH_COMPLETE or FLASH_FAILED markers soon
641+
accumulated_output = pidof_output # just in case we get the FLASH_COMPLETE or FLASH_FAILED markers soon
633642

634643
# Extract the actual process ID from the output, handling potential error messages
635644
lines = pidof_output.splitlines()
@@ -709,8 +718,8 @@ def _update_accumulated_output(self, accumulated_output, data):
709718
"""Update accumulated output with new data, keeping only last 64KB."""
710719
accumulated_output += data
711720
# Keep only the last 64KB to prevent memory growth
712-
if len(accumulated_output) > 64*1024:
713-
accumulated_output = accumulated_output[-64*1024:]
721+
if len(accumulated_output) > 64 * 1024:
722+
accumulated_output = accumulated_output[-64 * 1024 :]
714723
return accumulated_output
715724

716725
def _update_progress_stats(self, data, last_pos, last_time):
@@ -943,7 +952,16 @@ def dump(
943952

944953
def _filename(self, path: PathBuf) -> str:
945954
"""Extract filename from url or path"""
946-
if path.startswith(("http://", "https://")):
955+
if path.startswith("oci://"):
956+
oci_path = path[6:] # Remove "oci://" prefix
957+
if ":" in oci_path:
958+
repository, tag = oci_path.rsplit(":", 1)
959+
repo_name = repository.split("/")[-1] if "/" in repository else repository
960+
return f"{repo_name}-{tag}"
961+
else:
962+
repo_name = oci_path.split("/")[-1] if "/" in oci_path else oci_path
963+
return repo_name
964+
elif path.startswith(("http://", "https://")):
947965
return urlparse(path).path.split("/")[-1]
948966
else:
949967
return Path(path).name
@@ -1181,7 +1199,7 @@ def base():
11811199
@click.option(
11821200
"--fls-version",
11831201
type=str,
1184-
default="0.1.9", # TODO(majopela): set default to "" once fls is included in our images
1202+
default="0.1.9", # TODO(majopela): set default to "" once fls is included in our images
11851203
help="Download an specific fls version from the github releases",
11861204
)
11871205
@click.option(

0 commit comments

Comments
 (0)