Add path_prefix support for registry separation
Enables organizational separation within registry hosts by adding optional path_prefix configuration. This supports common use cases like Harbor projects, private registry namespaces, and organizational hierarchies. Key changes: - Add path_prefix field to Registry class with automatic slash normalization - Update DockerSyncImagesKeywords to construct URLs with path prefixes - Enhance configuration documentation with path_prefix examples - Add unit tests for path_prefix handling and URL construction Examples: - Harbor projects: "project-x/test" -> harbor.com/project-x/test/busybox - Team namespaces: "team-a" -> registry.com/team-a/my-image The path_prefix field is optional and trailing slashes are normalized automatically, making configuration flexible while ensuring correct URLs. Change-Id: I4784bc10e61840901baeaef2b69f323956bc23c3 Signed-off-by: Andrew Vaillancourt <andrew.vaillancourt@windriver.com>
This commit is contained in:
@@ -35,7 +35,14 @@
|
|||||||
// "manifest_registry" is set to null—for clarity and visibility.
|
// "manifest_registry" is set to null—for clarity and visibility.
|
||||||
//
|
//
|
||||||
// - "registries":
|
// - "registries":
|
||||||
// A dictionary of registry definitions including URLs and credentials.
|
// A dictionary of registry definitions including URLs, credentials, and optional path prefixes.
|
||||||
|
// Each registry can define:
|
||||||
|
// - "path_prefix": Optional path prefix prepended to image names during sync.
|
||||||
|
// Enables organizational separation within the same registry host.
|
||||||
|
// Trailing slash is optional and will be normalized automatically.
|
||||||
|
// Examples:
|
||||||
|
// - Harbor projects: "project-x/test" -> harbor.com/project-x/test/busybox
|
||||||
|
// - Private registry namespaces: "team-a" -> registry.com/team-a/my-image
|
||||||
//
|
//
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Registry Resolution Behavior:
|
// Registry Resolution Behavior:
|
||||||
@@ -76,6 +83,9 @@
|
|||||||
// Use empty strings for "user_name" and "password" in these cases.
|
// Use empty strings for "user_name" and "password" in these cases.
|
||||||
// - Private registries or internal mirrors (including "local_registry") must be configured
|
// - Private registries or internal mirrors (including "local_registry") must be configured
|
||||||
// with valid credentials if authentication is required.
|
// with valid credentials if authentication is required.
|
||||||
|
// - "path_prefix" is optional and only used when the registry organizes images using
|
||||||
|
// path-based hierarchies (Harbor projects, private registry namespaces, etc.).
|
||||||
|
// Public registries like DockerHub, k8s.io typically don't need path prefixes.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -100,8 +110,7 @@
|
|||||||
"override": false,
|
"override": false,
|
||||||
},
|
},
|
||||||
"resources/image_manifests/stx-third-party-images.yaml": {
|
"resources/image_manifests/stx-third-party-images.yaml": {
|
||||||
"manifest_registry": "null", // No manifest fallback; each image uses its "source_registry" or "default_source_registry"
|
"manifest_registry": null, // No manifest fallback; each image uses its "source_registry" or "default_source_registry"
|
||||||
"override": false,
|
|
||||||
},
|
},
|
||||||
// // Use Harbor as the default for images in this manifest that do not specify "source_registry"
|
// // Use Harbor as the default for images in this manifest that do not specify "source_registry"
|
||||||
// "resources/image_manifests/stx-sanity-images.yaml": {
|
// "resources/image_manifests/stx-sanity-images.yaml": {
|
||||||
@@ -111,7 +120,7 @@
|
|||||||
// // No manifest fallback is defined; each image will use its "source_registry" if set, or "default_source_registry".
|
// // No manifest fallback is defined; each image will use its "source_registry" if set, or "default_source_registry".
|
||||||
// "resources/stx-networking-images.yaml": {
|
// "resources/stx-networking-images.yaml": {
|
||||||
// "manifest_registry": null,
|
// "manifest_registry": null,
|
||||||
// "override": false,
|
// // "override": false, // Not needed when manifest_registry is null (nothing to override with)
|
||||||
// },
|
// },
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -137,14 +146,35 @@
|
|||||||
"password": "",
|
"password": "",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Example entry for a private registry such as Harbor:
|
// Examples of registries with path_prefix for organizational separation:
|
||||||
// "harbor": {
|
|
||||||
// "registry_name": "harbor",
|
// Harbor with project-based organization
|
||||||
|
// "harbor_project_x": {
|
||||||
|
// "registry_name": "harbor_project_x",
|
||||||
// "registry_url": "harbor.example.org:5000",
|
// "registry_url": "harbor.example.org:5000",
|
||||||
|
// "path_prefix": "project-x/test",
|
||||||
// "user_name": "robot_user",
|
// "user_name": "robot_user",
|
||||||
// "password": "robot_token",
|
// "password": "robot_token",
|
||||||
// },
|
// },
|
||||||
|
|
||||||
|
// Same Harbor host, different project
|
||||||
|
// "harbor_user": {
|
||||||
|
// "registry_name": "harbor_user",
|
||||||
|
// "registry_url": "harbor.example.org:5000",
|
||||||
|
// "path_prefix": "user-project", # Trailing slash is optional and will be normalized automatically
|
||||||
|
// "user_name": "harbor_username",
|
||||||
|
// "password": "harbor_password",
|
||||||
|
// },
|
||||||
|
|
||||||
|
// Private registry with namespace organization
|
||||||
|
// "private_team_a": {
|
||||||
|
// "registry_name": "private_team_a",
|
||||||
|
// "registry_url": "registry.company.com:5000",
|
||||||
|
// "path_prefix": "team-a/projects/", # Trailing slash is optional and will be normalized automatically
|
||||||
|
// "user_name": "team_user",
|
||||||
|
// "password": "team_token",
|
||||||
|
// },
|
||||||
|
|
||||||
"local_registry": {
|
"local_registry": {
|
||||||
"registry_name": "local_registry",
|
"registry_name": "local_registry",
|
||||||
"registry_url": "registry.local:9001",
|
"registry_url": "registry.local:9001",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class DockerConfig:
|
|||||||
registry_url=registry_dict["registry_url"],
|
registry_url=registry_dict["registry_url"],
|
||||||
user_name=registry_dict["user_name"],
|
user_name=registry_dict["user_name"],
|
||||||
password=registry_dict["password"],
|
password=registry_dict["password"],
|
||||||
|
path_prefix=registry_dict.get("path_prefix"),
|
||||||
)
|
)
|
||||||
self.registry_list.append(reg)
|
self.registry_list.append(reg)
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
class Registry:
|
class Registry:
|
||||||
"""Represents a Docker registry configuration."""
|
"""Represents a Docker registry configuration."""
|
||||||
|
|
||||||
def __init__(self, registry_name: str, registry_url: str, user_name: str, password: str):
|
def __init__(self, registry_name: str, registry_url: str, user_name: str, password: str, path_prefix: str = None):
|
||||||
"""
|
"""
|
||||||
Initializes a Registry object.
|
Initializes a Registry object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
registry_name (str): Logical name of the registry (e.g., "source_registry").
|
registry_name (str): Logical name of the registry (e.g., "source_registry").
|
||||||
registry_url (str): Registry endpoint URL (e.g., "docker.io/starlingx").
|
registry_url (str): Registry endpoint URL (e.g., "docker.io").
|
||||||
user_name (str): Username for authenticating with the registry.
|
user_name (str): Username for authenticating with the registry.
|
||||||
password (str): Password for authenticating with the registry.
|
password (str): Password for authenticating with the registry.
|
||||||
|
path_prefix (str): Optional path prefix for registry projects (e.g., "project/namespace/").
|
||||||
"""
|
"""
|
||||||
self.registry_name = registry_name
|
self.registry_name = registry_name
|
||||||
self.registry_url = registry_url
|
self.registry_url = registry_url
|
||||||
self.user_name = user_name
|
self.user_name = user_name
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.path_prefix = path_prefix or ""
|
||||||
|
|
||||||
def get_registry_name(self) -> str:
|
def get_registry_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -52,20 +54,35 @@ class Registry:
|
|||||||
"""
|
"""
|
||||||
return self.password
|
return self.password
|
||||||
|
|
||||||
|
def get_path_prefix(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns the path prefix prepended to image names during sync operations.
|
||||||
|
|
||||||
|
The path prefix enables organizational separation within the same registry host,
|
||||||
|
such as Harbor projects or private registry namespaces.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Path prefix (e.g., "project/namespace") or empty string for registries
|
||||||
|
like DockerHub that don't require path-based organization.
|
||||||
|
"""
|
||||||
|
return self.path_prefix
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a human-readable string representation of the registry.
|
Returns a human-readable string representation of the registry.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Formatted string showing registry name and URL.
|
str: Formatted string showing registry name, URL, and path prefix if present.
|
||||||
"""
|
"""
|
||||||
|
if self.path_prefix:
|
||||||
|
return f"{self.registry_name} ({self.registry_url}/{self.path_prefix})"
|
||||||
return f"{self.registry_name} ({self.registry_url})"
|
return f"{self.registry_name} ({self.registry_url})"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the representation string of the registry.
|
Returns the representation string of the registry for debugging.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Registry representation.
|
str: Registry representation showing constructor parameters (excluding credentials).
|
||||||
"""
|
"""
|
||||||
return self.__str__()
|
return f"Registry(registry_name='{self.registry_name}', registry_url='{self.registry_url}', path_prefix='{self.path_prefix}')"
|
||||||
|
|||||||
@@ -108,7 +108,14 @@ class DockerSyncImagesKeywords(BaseKeyword):
|
|||||||
|
|
||||||
source_registry = docker_config.get_registry(source_registry_name)
|
source_registry = docker_config.get_registry(source_registry_name)
|
||||||
|
|
||||||
source_image = f"{source_registry.get_registry_url()}/{name}:{tag}"
|
registry_url = source_registry.get_registry_url()
|
||||||
|
path_prefix = source_registry.get_path_prefix()
|
||||||
|
if path_prefix:
|
||||||
|
# Normalize path_prefix to ensure proper slash formatting
|
||||||
|
normalized_prefix = path_prefix.strip("/") + "/"
|
||||||
|
source_image = f"{registry_url}/{normalized_prefix}{name}:{tag}"
|
||||||
|
else:
|
||||||
|
source_image = f"{registry_url}/{name}:{tag}"
|
||||||
target_image = f"{local_registry.get_registry_url()}/{name}:{tag}"
|
target_image = f"{local_registry.get_registry_url()}/{name}:{tag}"
|
||||||
|
|
||||||
get_logger().log_info(f"Pulling {source_image}")
|
get_logger().log_info(f"Pulling {source_image}")
|
||||||
@@ -150,6 +157,7 @@ class DockerSyncImagesKeywords(BaseKeyword):
|
|||||||
|
|
||||||
source_registry = docker_config.get_registry(source_registry_name)
|
source_registry = docker_config.get_registry(source_registry_name)
|
||||||
source_url = source_registry.get_registry_url()
|
source_url = source_registry.get_registry_url()
|
||||||
|
path_prefix = source_registry.get_path_prefix()
|
||||||
|
|
||||||
# Always try to remove these two references
|
# Always try to remove these two references
|
||||||
refs = [
|
refs = [
|
||||||
@@ -159,9 +167,14 @@ class DockerSyncImagesKeywords(BaseKeyword):
|
|||||||
|
|
||||||
# Optionally add full source registry tag if not DockerHub
|
# Optionally add full source registry tag if not DockerHub
|
||||||
if "docker.io" not in source_url:
|
if "docker.io" not in source_url:
|
||||||
refs.insert(0, f"{source_url}/{image_name}:{image_tag}")
|
if path_prefix:
|
||||||
|
# Normalize path_prefix to ensure proper slash formatting
|
||||||
|
normalized_prefix = path_prefix.strip("/") + "/"
|
||||||
|
refs.insert(0, f"{source_url}/{normalized_prefix}{image_name}:{image_tag}")
|
||||||
|
else:
|
||||||
|
refs.insert(0, f"{source_url}/{image_name}:{image_tag}")
|
||||||
else:
|
else:
|
||||||
get_logger().log_debug(f"Skipping full docker.io-prefixed tag for {source_url}/{image_name}:{image_tag}")
|
get_logger().log_debug(f"Skipping full docker.io-prefixed tag for {source_url}/{path_prefix}{image_name}:{image_tag}")
|
||||||
|
|
||||||
return refs
|
return refs
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,14 @@
|
|||||||
# - Authentication details (username, password, etc.) are configured in
|
# - Authentication details (username, password, etc.) are configured in
|
||||||
# `config/docker/files/default.json5` under the corresponding `registries` entry.
|
# `config/docker/files/default.json5` under the corresponding `registries` entry.
|
||||||
# - This file defaults to `config/docker/files/default.json5`, but can be overridden using `--docker_config_file`.
|
# - This file defaults to `config/docker/files/default.json5`, but can be overridden using `--docker_config_file`.
|
||||||
# - Registry resolution is handled dynamically via `ConfigurationManager`.
|
# - `source_registry` values must match a registry name defined in the Docker config file
|
||||||
# - Resolution priority (from most to least specific):
|
# (e.g., "dockerhub", "k8s", "gcr" - not URLs like "docker.io")
|
||||||
# 1. `source_registry` field on the image entry (optional)
|
#
|
||||||
# 2. `manifest_registry_map` in `config/docker/files/default.json5`
|
# Registry resolution priority:
|
||||||
# 3. `default_source_registry` in `config/docker/files/default.json5`
|
# 1. `manifest_registry_map` with `override: true` (uses registry specified in manifest mapping in config, ignores source_registry in this file)
|
||||||
|
# 2. `source_registry` field on individual image entry (if override=false or no manifest mapping exists, must match registry name in config)
|
||||||
|
# 3. `manifest_registry_map` with `override: false` (fallback for images without source_registry)
|
||||||
|
# 4. `default_source_registry` (final fallback)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
images:
|
images:
|
||||||
|
|||||||
@@ -9,11 +9,14 @@
|
|||||||
# - Image names must include their full namespace (e.g., `starlingx/stx-platformclients`).
|
# - Image names must include their full namespace (e.g., `starlingx/stx-platformclients`).
|
||||||
# - Registry URLs and credentials are not listed here. They are defined in:
|
# - Registry URLs and credentials are not listed here. They are defined in:
|
||||||
# `config/docker/files/default.json5`
|
# `config/docker/files/default.json5`
|
||||||
# - Registry resolution is handled dynamically via `ConfigurationManager`.
|
# - `source_registry` values must match a registry name defined in the Docker config file
|
||||||
# - Resolution priority (from most to least specific):
|
# (e.g., "dockerhub", "k8s", "gcr" - not URLs like "docker.io")
|
||||||
# 1. `source_registry` field on the individual image entry (optional)
|
#
|
||||||
# 2. `manifest_registry_map` entry in `config/docker/files/default.json5`
|
# Registry resolution priority:
|
||||||
# 3. `default_source_registry` in `config/docker/files/default.json5`
|
# 1. `manifest_registry_map` with `override: true` (uses registry specified in manifest mapping in config, ignores source_registry in this file)
|
||||||
|
# 2. `source_registry` field on individual image entry (if override=false or no manifest mapping exists, must match registry name in config)
|
||||||
|
# 3. `manifest_registry_map` with `override: false` (fallback for images without source_registry)
|
||||||
|
# 4. `default_source_registry` (final fallback)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
images:
|
images:
|
||||||
- name: "starlingx/stx-platformclients"
|
- name: "starlingx/stx-platformclients"
|
||||||
|
|||||||
@@ -13,11 +13,14 @@
|
|||||||
# - Image names must include their full namespace (e.g., `google-samples/node-hello`).
|
# - Image names must include their full namespace (e.g., `google-samples/node-hello`).
|
||||||
# - Registry URLs and credentials are defined in:
|
# - Registry URLs and credentials are defined in:
|
||||||
# `config/docker/files/default.json5`
|
# `config/docker/files/default.json5`
|
||||||
|
# - `source_registry` values must match a registry name defined in the Docker config file
|
||||||
|
# (e.g., "dockerhub", "k8s", "gcr" - not URLs like "docker.io")
|
||||||
#
|
#
|
||||||
# Registry resolution priority (from most to least specific):
|
# Registry resolution priority:
|
||||||
# 1. `source_registry` field on the individual image entry (recommended)
|
# 1. `manifest_registry_map` with `override: true` (uses registry specified in manifest mapping in config, ignores source_registry in this file)
|
||||||
# 2. `manifest_registry_map` in the Docker config
|
# 2. `source_registry` field on individual image entry (if override=false or no manifest mapping exists, must match registry name in config)
|
||||||
# 3. `default_source_registry` fallback
|
# 3. `manifest_registry_map` with `override: false` (fallback for images without source_registry)
|
||||||
|
# 4. `default_source_registry` (final fallback)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
images:
|
images:
|
||||||
|
|||||||
54
unit_tests/config/docker/registry_path_prefix_test.py
Normal file
54
unit_tests/config/docker/registry_path_prefix_test.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from config.docker.objects.registry import Registry
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegistryPathPrefix:
|
||||||
|
"""Tests for Registry path_prefix handling and URL construction."""
|
||||||
|
|
||||||
|
def test_path_prefix_with_trailing_slash(self):
|
||||||
|
"""Test that path_prefix with trailing slash is returned as-is."""
|
||||||
|
registry = Registry(registry_name="test_registry", registry_url="harbor.example.com", user_name="user", password="pass", path_prefix="project-x/test/")
|
||||||
|
assert registry.get_path_prefix() == "project-x/test/"
|
||||||
|
|
||||||
|
def test_path_prefix_without_trailing_slash(self):
|
||||||
|
"""Test that path_prefix without trailing slash is returned as-is."""
|
||||||
|
registry = Registry(registry_name="test_registry", registry_url="harbor.example.com", user_name="user", password="pass", path_prefix="project-x/test")
|
||||||
|
assert registry.get_path_prefix() == "project-x/test"
|
||||||
|
|
||||||
|
def test_path_prefix_with_leading_slash(self):
|
||||||
|
"""Test that path_prefix with leading slash is returned as-is."""
|
||||||
|
registry = Registry(registry_name="test_registry", registry_url="harbor.example.com", user_name="user", password="pass", path_prefix="/project-x/test/")
|
||||||
|
assert registry.get_path_prefix() == "/project-x/test/"
|
||||||
|
|
||||||
|
def test_empty_path_prefix(self):
|
||||||
|
"""Test that empty path_prefix returns empty string."""
|
||||||
|
registry = Registry(registry_name="test_registry", registry_url="docker.io", user_name="user", password="pass", path_prefix="")
|
||||||
|
assert registry.get_path_prefix() == ""
|
||||||
|
|
||||||
|
def test_none_path_prefix(self):
|
||||||
|
"""Test that None path_prefix defaults to empty string."""
|
||||||
|
registry = Registry(registry_name="test_registry", registry_url="docker.io", user_name="user", password="pass")
|
||||||
|
assert registry.get_path_prefix() == ""
|
||||||
|
|
||||||
|
def test_url_construction_normalization(self):
|
||||||
|
"""Test URL construction with various path_prefix formats."""
|
||||||
|
test_cases = [
|
||||||
|
("project-x/test/", "harbor.com/project-x/test/busybox:1.0"),
|
||||||
|
("project-x/test", "harbor.com/project-x/test/busybox:1.0"),
|
||||||
|
("/project-x/test/", "harbor.com/project-x/test/busybox:1.0"),
|
||||||
|
("/project-x/test", "harbor.com/project-x/test/busybox:1.0"),
|
||||||
|
("", "harbor.com/busybox:1.0"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for path_prefix, expected_url in test_cases:
|
||||||
|
registry = Registry(registry_name="test_registry", registry_url="harbor.com", user_name="user", password="pass", path_prefix=path_prefix)
|
||||||
|
|
||||||
|
# Simulate the URL construction logic from the sync method
|
||||||
|
registry_url = registry.get_registry_url()
|
||||||
|
prefix = registry.get_path_prefix()
|
||||||
|
if prefix:
|
||||||
|
normalized_prefix = prefix.strip("/") + "/"
|
||||||
|
actual_url = f"{registry_url}/{normalized_prefix}busybox:1.0"
|
||||||
|
else:
|
||||||
|
actual_url = f"{registry_url}/busybox:1.0"
|
||||||
|
|
||||||
|
assert actual_url == expected_url, f"Failed for path_prefix='{path_prefix}'"
|
||||||
Reference in New Issue
Block a user