Skip to content

Commit 82725af

Browse files
committed
push: add --allow-insecure, handle HTTP/TLS errors like install
push always forced verified HTTPS for custom registries (it discarded the base resolved by get_auth_token), so it could neither reach an HTTP-only registry nor an HTTPS registry with an untrusted certificate, and it lacked the --allow-insecure escape hatch install has. Resolve the scheme/base via get_auth_token(insecure=...) and thread that base + the insecure flag through every request (_blob_exists, _upload_blob_bytes, _upload_blob_file, _put_manifest now take base + insecure and use opener(insecure) instead of registry_base_url + auth_opener). get_auth_token is the gatekeeper, so push inherits the same handling as install: by default a bad certificate or an HTTP-only registry raises a meaningful error pointing at --allow-insecure; with the flag, a bad cert is reached over unverified HTTPS and an HTTP-only registry falls back to an http base. Docker Hub stays verified-HTTPS. Add --allow-insecure to the parser, push command, help page, and bash/ zsh/fish completions. Update and extend the push tests for the new base argument and the insecure threading.
1 parent 2cf08b3 commit 82725af

8 files changed

Lines changed: 137 additions & 34 deletions

File tree

proot_distro/commands/help/pages.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,19 @@
175175
"riscv64, x86_64) or Docker platform strings "
176176
"(linux/arm64, linux/amd64, ...). Default: host "
177177
"architecture."),
178+
("--allow-insecure",
179+
"Permit insecure transport to the target registry: a custom "
180+
"registry served over plain HTTP, or an HTTPS registry whose "
181+
"TLS certificate is untrusted, expired, self-signed, or for a "
182+
"different hostname. Use only for trusted registries on a "
183+
"network path you control."),
178184
("-q, --quiet", "Suppress non-error output."),
179185
],
180186
"examples": [
181187
f"{PROGRAM_NAME} push myuser/myapp:1.0",
182188
f"{PROGRAM_NAME} push ghcr.io/myorg/myapp:1.0",
183189
f"{PROGRAM_NAME} push --architecture aarch64 myuser/myapp:1.0",
190+
f"{PROGRAM_NAME} push --allow-insecure 192.168.1.10:5000/app:1.0",
184191
],
185192
"footer": [
186193
{

proot_distro/commands/push.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def command_push(args):
4646
image_ref = getattr(args, "image_ref", None) or ""
4747
override_arch = getattr(args, "override_arch", None) or ""
4848
quiet = bool(getattr(args, "quiet", False))
49+
allow_insecure = bool(getattr(args, "allow_insecure", False))
4950

5051
if not image_ref:
5152
crit_error("image reference is not specified (e.g. 'myrepo/myapp:1.0').")
@@ -86,7 +87,7 @@ def command_push(args):
8687

8788
try:
8889
with BuildLock(image_ref, target_arch, command="push"):
89-
result = push_image(image_ref, target_arch)
90+
result = push_image(image_ref, target_arch, insecure=allow_insecure)
9091
except KeyboardInterrupt:
9192
if sys.stderr.isatty():
9293
sys.stderr.write("\r\033[K")

proot_distro/completions/_proot-distro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ _proot_distro() {
263263
'(-h --help)'{-h,--help}'[show help]' \
264264
'(-q --quiet)'{-q,--quiet}'[suppress non-error output]' \
265265
'(-a --architecture)'{-a,--architecture}'[target CPU architecture]:arch:(aarch64 arm i686 riscv64 x86_64)' \
266+
'--allow-insecure[allow pushing to an HTTP-only or untrusted-TLS registry]' \
266267
'1:image reference'
267268
;;
268269

proot_distro/completions/proot-distro.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ _proot_distro() {
271271
return ;;
272272
esac
273273
if [[ "${cur}" == -* ]]; then
274-
COMPREPLY=($(compgen -W "-a --architecture -q --quiet -h --help" -- "${cur}"))
274+
COMPREPLY=($(compgen -W "-a --architecture --allow-insecure -q --quiet -h --help" -- "${cur}"))
275275
fi
276276
;;
277277

proot_distro/completions/proot-distro.fish

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ complete -c proot-distro -f -n '__fish_seen_subcommand_from build' \
331331
complete -c proot-distro -f -n '__fish_seen_subcommand_from push' \
332332
-s a -l architecture -r -d 'Target CPU architecture' \
333333
-a 'aarch64\tAArch64 arm\tARM(32-bit) i686\tx86(32-bit) riscv64\tRISC-V x86_64\tx86_64'
334+
complete -c proot-distro -f -n '__fish_seen_subcommand_from push' \
335+
-l allow-insecure -d 'Allow pushing to an HTTP-only or untrusted-TLS registry'
334336
complete -c proot-distro -f -n '__fish_seen_subcommand_from push' \
335337
-s q -l quiet -d 'Suppress non-error output'
336338
complete -c proot-distro -f -n '__fish_seen_subcommand_from push' \
@@ -467,6 +469,7 @@ complete -c pd -f -n '__fish_seen_subcommand_from build' -s q -l quiet
467469
complete -c pd -f -n '__fish_seen_subcommand_from build' -s h -l help -d 'Show help'
468470

469471
complete -c pd -f -n '__fish_seen_subcommand_from push' -s a -l architecture -r -d 'Target CPU architecture' -a 'aarch64 arm i686 riscv64 x86_64'
472+
complete -c pd -f -n '__fish_seen_subcommand_from push' -l allow-insecure -d 'Allow pushing to an HTTP-only or untrusted-TLS registry'
470473
complete -c pd -f -n '__fish_seen_subcommand_from push' -s q -l quiet -d 'Suppress non-error output'
471474
complete -c pd -f -n '__fish_seen_subcommand_from push' -s h -l help -d 'Show help'
472475

proot_distro/helpers/docker/push.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@
5454
from proot_distro.helpers.docker.refs import parse_image_ref
5555
from proot_distro.helpers.docker.transport import (
5656
auth_note,
57-
auth_opener,
5857
get_auth_token,
58+
opener,
5959
push_denied_msg,
60-
registry_base_url,
6160
_ua,
6261
)
6362

@@ -76,18 +75,17 @@ def _resolve_upload_url(base: str, location: str) -> str:
7675

7776

7877
def _blob_exists(
79-
repo: str, digest: str, token: str, registry: str = "",
78+
repo: str, digest: str, token: str, base: str, insecure: bool = False,
8079
) -> bool:
8180
"""Return True iff blob *digest* already exists on the registry."""
82-
base = registry_base_url(registry)
8381
url = f"{base}/v2/{repo}/blobs/{digest}"
8482
headers = {**_ua()}
8583
if token:
8684
headers["Authorization"] = f"Bearer {token}"
8785
req = urllib.request.Request(url, method="HEAD", headers=headers)
8886

8987
def _attempt():
90-
with auth_opener().open(req) as resp:
88+
with opener(insecure).open(req) as resp:
9189
return 200 <= resp.status < 300
9290

9391
try:
@@ -136,7 +134,7 @@ def read(self, size=-1):
136134

137135
def _upload_blob_bytes(
138136
repo: str, digest: str, data: bytes, token: str,
139-
registry: str = "",
137+
base: str, insecure: bool = False,
140138
) -> None:
141139
"""Upload a small in-memory blob (POST + monolithic PUT).
142140
@@ -145,7 +143,6 @@ def _upload_blob_bytes(
145143
the *content* of the blob and is set separately when the manifest
146144
itself is uploaded.
147145
"""
148-
base = registry_base_url(registry)
149146
headers = {**_ua()}
150147
if token:
151148
headers["Authorization"] = f"Bearer {token}"
@@ -160,7 +157,7 @@ def _attempt():
160157
method="POST",
161158
headers={**headers, "Content-Length": "0"},
162159
)
163-
with auth_opener().open(post_req) as resp:
160+
with opener(insecure).open(post_req) as resp:
164161
location = resp.headers.get("Location", "")
165162
put_url = _resolve_upload_url(base, location)
166163
sep = "&" if "?" in put_url else "?"
@@ -175,7 +172,7 @@ def _attempt():
175172
"Content-Length": str(len(data)),
176173
},
177174
)
178-
with auth_opener().open(put_req) as resp:
175+
with opener(insecure).open(put_req) as resp:
179176
if not 200 <= resp.status < 300:
180177
raise RuntimeError(
181178
f"Blob upload failed for {digest}: HTTP {resp.status}"
@@ -186,10 +183,9 @@ def _attempt():
186183

187184
def _upload_blob_file(
188185
repo: str, digest: str, file_path: str, token: str,
189-
registry: str = "", label: str = "",
186+
base: str, insecure: bool = False, label: str = "",
190187
) -> None:
191188
"""Upload a blob from *file_path* (streamed, POST + monolithic PUT)."""
192-
base = registry_base_url(registry)
193189
headers = {**_ua()}
194190
if token:
195191
headers["Authorization"] = f"Bearer {token}"
@@ -204,7 +200,7 @@ def _attempt():
204200
method="POST",
205201
headers={**headers, "Content-Length": "0"},
206202
)
207-
with auth_opener().open(post_req) as resp:
203+
with opener(insecure).open(post_req) as resp:
208204
location = resp.headers.get("Location", "")
209205
put_url = _resolve_upload_url(base, location)
210206
sep = "&" if "?" in put_url else "?"
@@ -222,7 +218,7 @@ def _attempt():
222218
"Content-Length": str(size),
223219
},
224220
)
225-
with auth_opener().open(put_req) as resp:
221+
with opener(insecure).open(put_req) as resp:
226222
if not 200 <= resp.status < 300:
227223
raise RuntimeError(
228224
f"Blob upload failed for {digest}: HTTP {resp.status}"
@@ -235,11 +231,10 @@ def _attempt():
235231

236232
def _put_manifest(
237233
repo: str, reference: str, body: bytes, media_type: str,
238-
token: str, registry: str = "",
234+
token: str, base: str, insecure: bool = False,
239235
) -> str:
240236
"""PUT a manifest at <reference> (tag or digest). Returns the registry
241237
digest from the Docker-Content-Digest header, if provided."""
242-
base = registry_base_url(registry)
243238
url = f"{base}/v2/{repo}/manifests/{reference}"
244239
headers = {
245240
**_ua(),
@@ -251,7 +246,7 @@ def _put_manifest(
251246
req = urllib.request.Request(url, data=body, method="PUT", headers=headers)
252247

253248
def _attempt():
254-
with auth_opener().open(req) as resp:
249+
with opener(insecure).open(req) as resp:
255250
if not 200 <= resp.status < 300:
256251
raise RuntimeError(
257252
f"Manifest upload failed: HTTP {resp.status}"
@@ -270,13 +265,21 @@ def _strip_private_keys(d: dict) -> dict:
270265
return {k: v for k, v in d.items() if not k.startswith("_")}
271266

272267

273-
def push_image(image_ref: str, arch: str) -> dict:
268+
def push_image(image_ref: str, arch: str, insecure: bool = False) -> dict:
274269
"""Push a built image (resolved from the manifest cache) to its registry.
275270
276271
The image must have been produced by `proot-distro build` under
277272
exactly this *image_ref* and *arch* — `build` stores the manifest
278273
in MANIFEST_CACHE_DIR and the layer + config blobs in
279274
LAYER_CACHE_DIR using the same digests we transmit here.
275+
276+
Registry traffic uses verified HTTPS unless *insecure* is set. With
277+
*insecure* a custom registry is reached over HTTPS with certificate
278+
verification disabled, falling back to plain HTTP when the registry only
279+
speaks HTTP (Docker Hub stays verified-HTTPS regardless). When enforcing
280+
HTTPS, an untrusted certificate or an HTTP-only registry surfaces a
281+
RuntimeError pointing the user at ``--allow-insecure`` — the same handling
282+
as the install command.
280283
"""
281284
manifest, repo, image_config = load_manifest_cache(image_ref, arch)
282285
if manifest is None:
@@ -321,8 +324,13 @@ def push_image(image_ref: str, arch: str) -> dict:
321324

322325
log_info(f"Authenticating with registry{auth_note()}...")
323326
try:
324-
# push always uses verified HTTPS; the resolved base is ignored.
325-
token, _base = get_auth_token(repo, registry, actions="pull,push")
327+
# Resolve the scheme/base for this registry. Under --allow-insecure a
328+
# bad certificate is tolerated and an HTTP-only registry falls back to
329+
# http://; otherwise a cert/plaintext failure raises a RuntimeError
330+
# pointing at --allow-insecure (handled inside get_auth_token).
331+
token, base = get_auth_token(
332+
repo, registry, actions="pull,push", insecure=insecure,
333+
)
326334
except urllib.error.HTTPError as exc:
327335
if exc.code in (401, 403):
328336
raise RuntimeError(push_denied_msg(image_ref, exc.code)) from exc
@@ -338,15 +346,15 @@ def push_image(image_ref: str, arch: str) -> dict:
338346
size = os.path.getsize(path)
339347

340348
try:
341-
if _blob_exists(repo, digest, token, registry):
349+
if _blob_exists(repo, digest, token, base, insecure):
342350
log_info(f"{short_id}: Layer {i + 1}/{n_layers} already "
343351
f"exists on registry, skipping upload.")
344352
continue
345353

346354
log_info(f"{short_id}: Uploading layer {i + 1}/{n_layers} "
347355
f"({fmt_size(size)})...")
348356
_upload_blob_file(
349-
repo, digest, path, token, registry, label=short_id,
357+
repo, digest, path, token, base, insecure, label=short_id,
350358
)
351359
bytes_uploaded += size
352360
except urllib.error.HTTPError as exc:
@@ -358,14 +366,14 @@ def push_image(image_ref: str, arch: str) -> dict:
358366

359367
cfg_short = expected_cfg_digest.split(":")[-1][:12]
360368
try:
361-
if _blob_exists(repo, expected_cfg_digest, token, registry):
369+
if _blob_exists(repo, expected_cfg_digest, token, base, insecure):
362370
log_info(f"{cfg_short}: Image config already exists on "
363371
f"registry, skipping upload.")
364372
else:
365373
log_info(f"{cfg_short}: Uploading image config "
366374
f"({fmt_size(len(config_bytes))})...")
367375
_upload_blob_bytes(
368-
repo, expected_cfg_digest, config_bytes, token, registry,
376+
repo, expected_cfg_digest, config_bytes, token, base, insecure,
369377
)
370378
bytes_uploaded += len(config_bytes)
371379
except urllib.error.HTTPError as exc:
@@ -379,7 +387,7 @@ def push_image(image_ref: str, arch: str) -> dict:
379387
f"({fmt_size(len(manifest_bytes))})...")
380388
try:
381389
registry_digest = _put_manifest(
382-
repo, tag, manifest_bytes, manifest_media, token, registry,
390+
repo, tag, manifest_bytes, manifest_media, token, base, insecure,
383391
)
384392
except urllib.error.HTTPError as exc:
385393
if exc.code in (401, 403):

proot_distro/parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ def _push(sub):
333333
p.add_argument(
334334
"-a", "--architecture", dest="override_arch", metavar="ARCH",
335335
)
336+
p.add_argument(
337+
"--allow-insecure", dest="allow_insecure", action="store_true",
338+
)
336339
p.add_argument("-q", "--quiet", action="store_true")
337340
p.add_argument("-h", "--help", action="store_true")
338341

0 commit comments

Comments
 (0)