NotifyType,
MonCommandFailed,
)
-from mgr_util import build_url, NvmeofMetadataPoolHelper
+from mgr_util import build_url, is_valid_container_image_ref, NvmeofMetadataPoolHelper
import orchestrator
from orchestrator.module import to_format, Format
raise OrchestratorError(
f'Cannot redeploy {daemon_type}.{daemon_id} with a new image: Supported '
f'types are: {", ".join(CEPH_IMAGE_TYPES)}')
+ if not is_valid_container_image_ref(image):
+ raise OrchestratorError(
+ f'Invalid container image {image!r} (not a valid container image reference)')
self.check_mon_command({
'prefix': 'config set',
import socket
import time
import logging
+import re
import sys
from ipaddress import ip_address
from threading import Lock, Condition
logger = logging.getLogger(__name__)
+# NAME and TAG are taken verbatim from the OCI distribution spec:
+# https://github.com/opencontainers/distribution-spec/blob/main/spec.md
+# DIGEST is based on the OCI image-spec descriptor grammar:
+# https://github.com/opencontainers/image-spec/blob/main/descriptor.md
+# REGISTRY is a practical heuristic, not defined by either spec.
+#
+# Catches malformed input.
+# Not a full OCI image reference parser.
+
+# distribution-spec, verbatim:
+NAME = r"[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*"
+
+# distribution-spec, verbatim:
+TAG = r"[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}"
+
+# image-spec descriptor grammar translated literally:
+GENERIC_DIGEST_RE = re.compile(
+ r"^[a-z0-9]+(?:[+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$",
+ re.ASCII,
+)
+
+# Practical heuristic for hostname[:port].
+REGISTRY = r"(?:[a-zA-Z0-9.-]+(?::[0-9]+)?)"
+
+STRICT_KNOWN_DIGESTS = {
+ "sha256": re.compile(r"^[a-f0-9]{64}$", re.ASCII),
+ "sha512": re.compile(r"^[a-f0-9]{128}$", re.ASCII),
+ "blake3": re.compile(r"^[a-f0-9]{64}$", re.ASCII),
+}
+
+IMAGE_RE = re.compile(
+ rf"""
+ ^
+ (?:{REGISTRY}/)?
+ {NAME}
+ (?::{TAG})?
+ (?:@(?P<digest>[a-z0-9]+(?:[+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+))?
+ $
+ """,
+ re.VERBOSE | re.ASCII,
+)
+
+
+def is_valid_digest(digest: str) -> bool:
+ if not GENERIC_DIGEST_RE.fullmatch(digest):
+ return False
+ algorithm, encoded = digest.split(":", 1)
+ checker = STRICT_KNOWN_DIGESTS.get(algorithm)
+ if checker is None:
+ return True
+ return checker.fullmatch(encoded) is not None
+
+
+def is_valid_container_image_ref(ref: str) -> bool:
+ """Basic sanity check for OCI/Docker-style container image references."""
+ m = IMAGE_RE.fullmatch(ref)
+ if m is None:
+ return False
+ digest = m.group("digest")
+ return digest is None or is_valid_digest(digest)
+
class PortAlreadyInUse(Exception):
pass
from ceph.deployment.utils import unwrap_ipv6
from ceph.utils import datetime_now
from ceph.cephadm.images import NonCephImageServiceTypes
-from mgr_util import to_pretty_timedelta, format_bytes, parse_combined_pem_file, NvmeofMetadataPoolHelper
+from mgr_util import (
+ is_valid_container_image_ref,
+ to_pretty_timedelta,
+ format_bytes,
+ parse_combined_pem_file,
+ NvmeofMetadataPoolHelper,
+)
from mgr_module import MgrModule, HandleCommandResult, Option
from object_format import Format
@OrchestratorCLICommand.Write('orch daemon redeploy')
def _daemon_action_redeploy(self,
name: str,
- image: Optional[str] = None) -> HandleCommandResult:
+ image: Optional[str] = None,
+ force: bool = False) -> HandleCommandResult:
"""Redeploy a daemon (with a specific image)"""
if '.' not in name:
raise OrchestratorError('%s is not a valid daemon name' % name)
- completion = self.daemon_action("redeploy", name, image=image)
+ if image is not None and not is_valid_container_image_ref(image):
+ raise OrchestratorError(
+ f'Invalid container image {image!r} (not a valid container image reference)'
+ )
+ completion = self.daemon_action("redeploy", name, image=image, force=force)
raise_if_exception(completion)
return HandleCommandResult(stdout=completion.result_str())
mock_parse_earmark.side_effect = mgr_util.EarmarkParseError
result = resolver.check_earmark("error.test", mgr_util.EarmarkTopScope.SMB)
assert result is False
+
+
+_SHA256_OK = "a" * 64
+_SHA256_BAD_HEX = "g" * 64
+_SHA256_SHORT = "a" * 63
+_SHA512_OK = "b" * 128
+_SHA512_SHORT = "b" * 127
+_BLAKE3_OK = "c" * 64
+
+
+@pytest.mark.parametrize(
+ "digest, expected",
+ [
+ (f"sha256:{_SHA256_OK}", True),
+ (f"sha512:{_SHA512_OK}", True),
+ (f"blake3:{_BLAKE3_OK}", True),
+ (f"sha256:{_SHA256_BAD_HEX}", False),
+ (f"sha256:{_SHA256_SHORT}", False),
+ (f"sha512:{_SHA512_SHORT}", False),
+ ("notadigest", False),
+ ("", False),
+ ("nocolon", False),
+ # Unknown algorithm: generic descriptor grammar only (no length check).
+ ("foo:bar", True),
+ ("acme:encoded-value_1", True),
+ ],
+)
+def test_is_valid_digest(digest: str, expected: bool):
+ assert mgr_util.is_valid_digest(digest) is expected
+
+
+@pytest.mark.parametrize(
+ "ref, expected",
+ [
+ # Typical registry + path + tag
+ ("quay.io/ceph/ceph", True),
+ ("quay.io/ceph/ceph:v18.2.0", True),
+ ("registry.example.com:5000/foo/bar:latest", True),
+ ("docker.io/library/nginx:1.25", True),
+ ("gcr.io/google-containers/pause:3.1", True),
+ ("mcr.microsoft.com/devcontainers/base:debian", True),
+ (f"quay.io/foo/bar@sha256:{_SHA256_OK}", True),
+ (f"localhost:5000/myapp/service@sha512:{_SHA512_OK}", True),
+ (f"127.0.0.1:5000/img/name@blake3:{_BLAKE3_OK}", True),
+ # Shorthand names (no registry)
+ ("ceph/ceph", True),
+ ("library/nginx:1", True),
+ ("alpine", True),
+ ("postgres:16-alpine", True),
+ # Tag charset (OCI distribution-spec tag)
+ ("quay.io/x/img:v1.2.3-rc.1", True),
+ ("registry.io/a/b/c/d/e:1", True),
+ # Reject common CLI/flag confusions
+ ("--force", False),
+ ("-f", False),
+ ("--image", False),
+ ("", False),
+ # Path traversal–looking strings can still match the permissive REGISTRY/NAME
+ # heuristic (e.g. ".."/"escape"). Use characters NAME/TAG reject instead:
+ ("quay.io/ceph/ceph:bad tag", False),
+ ("!not/valid", False),
+ # Malformed or wrong-length digest
+ (f"quay.io/foo@sha256:{_SHA256_BAD_HEX}", False),
+ (f"docker.io/lib/img@sha256:{_SHA256_SHORT}", False),
+ ],
+)
+def test_is_valid_container_image_ref(ref: str, expected: bool):
+ assert mgr_util.is_valid_container_image_ref(ref) is expected