cloudsmith-cli 1.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloudsmith_cli/__init__.py +10 -0
- cloudsmith_cli/__main__.py +8 -0
- cloudsmith_cli/cli/__init__.py +1 -0
- cloudsmith_cli/cli/command.py +160 -0
- cloudsmith_cli/cli/commands/__init__.py +33 -0
- cloudsmith_cli/cli/commands/auth.py +173 -0
- cloudsmith_cli/cli/commands/check.py +129 -0
- cloudsmith_cli/cli/commands/copy.py +98 -0
- cloudsmith_cli/cli/commands/credential_helper/__init__.py +39 -0
- cloudsmith_cli/cli/commands/credential_helper/docker.py +66 -0
- cloudsmith_cli/cli/commands/credential_helper/manage.py +299 -0
- cloudsmith_cli/cli/commands/delete.py +67 -0
- cloudsmith_cli/cli/commands/dependencies.py +108 -0
- cloudsmith_cli/cli/commands/docs.py +16 -0
- cloudsmith_cli/cli/commands/download.py +620 -0
- cloudsmith_cli/cli/commands/entitlements.py +819 -0
- cloudsmith_cli/cli/commands/help_.py +12 -0
- cloudsmith_cli/cli/commands/list_.py +317 -0
- cloudsmith_cli/cli/commands/login.py +99 -0
- cloudsmith_cli/cli/commands/logout.py +151 -0
- cloudsmith_cli/cli/commands/main.py +73 -0
- cloudsmith_cli/cli/commands/mcp.py +523 -0
- cloudsmith_cli/cli/commands/metadata.py +503 -0
- cloudsmith_cli/cli/commands/metrics/__init__.py +2 -0
- cloudsmith_cli/cli/commands/metrics/command.py +20 -0
- cloudsmith_cli/cli/commands/metrics/entitlements.py +148 -0
- cloudsmith_cli/cli/commands/metrics/packages.py +134 -0
- cloudsmith_cli/cli/commands/move.py +111 -0
- cloudsmith_cli/cli/commands/policy/__init__.py +3 -0
- cloudsmith_cli/cli/commands/policy/command.py +20 -0
- cloudsmith_cli/cli/commands/policy/deny.py +248 -0
- cloudsmith_cli/cli/commands/policy/license.py +335 -0
- cloudsmith_cli/cli/commands/policy/vulnerability.py +322 -0
- cloudsmith_cli/cli/commands/push.py +1323 -0
- cloudsmith_cli/cli/commands/quarantine.py +148 -0
- cloudsmith_cli/cli/commands/quota/__init__.py +2 -0
- cloudsmith_cli/cli/commands/quota/command.py +20 -0
- cloudsmith_cli/cli/commands/quota/history.py +122 -0
- cloudsmith_cli/cli/commands/quota/quota.py +107 -0
- cloudsmith_cli/cli/commands/repos.py +330 -0
- cloudsmith_cli/cli/commands/resync.py +90 -0
- cloudsmith_cli/cli/commands/status.py +92 -0
- cloudsmith_cli/cli/commands/tags.py +375 -0
- cloudsmith_cli/cli/commands/tokens.py +318 -0
- cloudsmith_cli/cli/commands/upstream.py +479 -0
- cloudsmith_cli/cli/commands/vulnerabilities.py +141 -0
- cloudsmith_cli/cli/commands/whoami.py +188 -0
- cloudsmith_cli/cli/config.py +635 -0
- cloudsmith_cli/cli/decorators.py +624 -0
- cloudsmith_cli/cli/exceptions.py +215 -0
- cloudsmith_cli/cli/metadata_common.py +146 -0
- cloudsmith_cli/cli/saml.py +109 -0
- cloudsmith_cli/cli/table.py +59 -0
- cloudsmith_cli/cli/types.py +14 -0
- cloudsmith_cli/cli/utils.py +267 -0
- cloudsmith_cli/cli/validators.py +378 -0
- cloudsmith_cli/cli/webserver.py +263 -0
- cloudsmith_cli/core/__init__.py +1 -0
- cloudsmith_cli/core/api/__init__.py +1 -0
- cloudsmith_cli/core/api/distros.py +31 -0
- cloudsmith_cli/core/api/entitlements.py +130 -0
- cloudsmith_cli/core/api/exceptions.py +57 -0
- cloudsmith_cli/core/api/files.py +131 -0
- cloudsmith_cli/core/api/init.py +109 -0
- cloudsmith_cli/core/api/metadata.py +217 -0
- cloudsmith_cli/core/api/metrics.py +78 -0
- cloudsmith_cli/core/api/orgs.py +201 -0
- cloudsmith_cli/core/api/packages.py +309 -0
- cloudsmith_cli/core/api/quota.py +64 -0
- cloudsmith_cli/core/api/rates.py +28 -0
- cloudsmith_cli/core/api/repos.py +81 -0
- cloudsmith_cli/core/api/status.py +27 -0
- cloudsmith_cli/core/api/upstreams.py +72 -0
- cloudsmith_cli/core/api/user.py +109 -0
- cloudsmith_cli/core/api/version.py +15 -0
- cloudsmith_cli/core/api/vulnerabilities.py +230 -0
- cloudsmith_cli/core/cache_utils.py +160 -0
- cloudsmith_cli/core/config.py +140 -0
- cloudsmith_cli/core/credentials/__init__.py +0 -0
- cloudsmith_cli/core/credentials/chain.py +69 -0
- cloudsmith_cli/core/credentials/models.py +44 -0
- cloudsmith_cli/core/credentials/oidc/__init__.py +6 -0
- cloudsmith_cli/core/credentials/oidc/cache.py +220 -0
- cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +122 -0
- cloudsmith_cli/core/credentials/oidc/detectors/aws.py +85 -0
- cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +70 -0
- cloudsmith_cli/core/credentials/oidc/detectors/base.py +26 -0
- cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +35 -0
- cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +40 -0
- cloudsmith_cli/core/credentials/oidc/detectors/generic.py +42 -0
- cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +64 -0
- cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +49 -0
- cloudsmith_cli/core/credentials/oidc/exchange.py +87 -0
- cloudsmith_cli/core/credentials/provider.py +17 -0
- cloudsmith_cli/core/credentials/providers/__init__.py +15 -0
- cloudsmith_cli/core/credentials/providers/cli_flag.py +24 -0
- cloudsmith_cli/core/credentials/providers/credentials_file.py +24 -0
- cloudsmith_cli/core/credentials/providers/env_var.py +24 -0
- cloudsmith_cli/core/credentials/providers/keyring_provider.py +59 -0
- cloudsmith_cli/core/credentials/providers/oidc_provider.py +115 -0
- cloudsmith_cli/core/download.py +594 -0
- cloudsmith_cli/core/keyring.py +171 -0
- cloudsmith_cli/core/mcp/__init__.py +0 -0
- cloudsmith_cli/core/mcp/data.py +17 -0
- cloudsmith_cli/core/mcp/server.py +786 -0
- cloudsmith_cli/core/pagination.py +131 -0
- cloudsmith_cli/core/ratelimits.py +88 -0
- cloudsmith_cli/core/rest.py +255 -0
- cloudsmith_cli/core/utils.py +95 -0
- cloudsmith_cli/core/version.py +20 -0
- cloudsmith_cli/credential_helpers/__init__.py +7 -0
- cloudsmith_cli/credential_helpers/backends.py +41 -0
- cloudsmith_cli/credential_helpers/common.py +111 -0
- cloudsmith_cli/credential_helpers/custom_domains.py +281 -0
- cloudsmith_cli/credential_helpers/docker/__init__.py +4 -0
- cloudsmith_cli/credential_helpers/docker/installer.py +349 -0
- cloudsmith_cli/credential_helpers/docker/runtime.py +117 -0
- cloudsmith_cli/credential_helpers/launchers.py +175 -0
- cloudsmith_cli/data/VERSION +1 -0
- cloudsmith_cli/data/config.ini +23 -0
- cloudsmith_cli/data/credentials.ini +14 -0
- cloudsmith_cli/templates/__init__.py +3 -0
- cloudsmith_cli/templates/auth_error.html +45 -0
- cloudsmith_cli/templates/auth_success.html +37 -0
- cloudsmith_cli-1.20.0.dist-info/METADATA +610 -0
- cloudsmith_cli-1.20.0.dist-info/RECORD +130 -0
- cloudsmith_cli-1.20.0.dist-info/WHEEL +5 -0
- cloudsmith_cli-1.20.0.dist-info/entry_points.txt +2 -0
- cloudsmith_cli-1.20.0.dist-info/licenses/LICENSE +201 -0
- cloudsmith_cli-1.20.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Copyright 2026 Cloudsmith Ltd
|
|
2
|
+
"""
|
|
3
|
+
Shared utilities for credential helpers.
|
|
4
|
+
|
|
5
|
+
Provides domain checking used by all credential helpers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from .custom_domains import get_custom_domains, get_format_domains
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def extract_hostname(url):
|
|
17
|
+
"""
|
|
18
|
+
Extract bare hostname from any URL format.
|
|
19
|
+
|
|
20
|
+
Handles protocols, sparse+ prefix, ports, paths, and trailing slashes.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
url: URL in any format (e.g., "sparse+https://cargo.cloudsmith.io/org/repo/")
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
str: Lowercase hostname (e.g., "cargo.cloudsmith.io")
|
|
27
|
+
"""
|
|
28
|
+
if not url:
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
normalized = url.lower().strip()
|
|
32
|
+
|
|
33
|
+
# Remove sparse+ prefix (Cargo)
|
|
34
|
+
if normalized.startswith("sparse+"):
|
|
35
|
+
normalized = normalized[7:]
|
|
36
|
+
|
|
37
|
+
# Remove protocol
|
|
38
|
+
if "://" in normalized:
|
|
39
|
+
normalized = normalized.split("://", 1)[1]
|
|
40
|
+
|
|
41
|
+
# Remove userinfo (user@host)
|
|
42
|
+
if "@" in normalized.split("/")[0]:
|
|
43
|
+
normalized = normalized.split("@", 1)[1]
|
|
44
|
+
|
|
45
|
+
# Extract hostname (before first / or :)
|
|
46
|
+
hostname = normalized.split("/")[0].split(":")[0]
|
|
47
|
+
|
|
48
|
+
return hostname
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_cloudsmith_domain(
|
|
52
|
+
url, api_key=None, auth_type="api_key", api_host=None, backend_kind=None
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Check if a URL points to a Cloudsmith service.
|
|
56
|
+
|
|
57
|
+
Checks standard *.cloudsmith.io domains first (no auth needed).
|
|
58
|
+
If not a standard domain, queries the Cloudsmith API for custom domains.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
url: URL or hostname to check
|
|
62
|
+
api_key: API key/token for authenticating custom domain lookups
|
|
63
|
+
auth_type: "api_key" (X-Api-Key header) or "bearer" (Authorization: Bearer)
|
|
64
|
+
api_host: Cloudsmith API host URL
|
|
65
|
+
backend_kind: If given, custom domains only match when their backend_kind
|
|
66
|
+
equals it (standard *.cloudsmith.io domains always match regardless).
|
|
67
|
+
When None (default), any enabled+validated custom domain matches.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
bool: True if this is a Cloudsmith domain
|
|
71
|
+
"""
|
|
72
|
+
hostname = extract_hostname(url)
|
|
73
|
+
if not hostname:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# Standard Cloudsmith domains — no auth needed, always match regardless of backend_kind
|
|
77
|
+
if (
|
|
78
|
+
hostname in ("cloudsmith.io", "cloudsmith.com")
|
|
79
|
+
or hostname.endswith(".cloudsmith.io")
|
|
80
|
+
or hostname.endswith(".cloudsmith.com")
|
|
81
|
+
):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Custom domains require org + auth
|
|
85
|
+
org = os.environ.get("CLOUDSMITH_ORG", "").strip()
|
|
86
|
+
if not org:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
if not api_key:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
if backend_kind is not None:
|
|
93
|
+
hosts = {
|
|
94
|
+
host.lower()
|
|
95
|
+
for host in get_format_domains(
|
|
96
|
+
org,
|
|
97
|
+
backend_kind,
|
|
98
|
+
api_key=api_key,
|
|
99
|
+
auth_type=auth_type,
|
|
100
|
+
api_host=api_host,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
else:
|
|
104
|
+
hosts = {
|
|
105
|
+
d.host.lower()
|
|
106
|
+
for d in get_custom_domains(
|
|
107
|
+
org, api_key=api_key, auth_type=auth_type, api_host=api_host
|
|
108
|
+
)
|
|
109
|
+
if d.enabled and d.validated
|
|
110
|
+
}
|
|
111
|
+
return hostname in hosts
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Copyright 2026 Cloudsmith Ltd
|
|
2
|
+
"""
|
|
3
|
+
Helper for discovering Cloudsmith custom domains.
|
|
4
|
+
|
|
5
|
+
This module provides functions to fetch custom domains from the Cloudsmith API
|
|
6
|
+
for use in credential helpers. Results are cached on the filesystem.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from ..cli.config import get_default_config_path
|
|
17
|
+
from ..core.api.exceptions import ApiException
|
|
18
|
+
from ..core.api.init import initialise_api
|
|
19
|
+
from ..core.api.orgs import list_custom_domains
|
|
20
|
+
from ..core.cache_utils import atomic_write_json
|
|
21
|
+
from ..core.credentials.models import CredentialResult
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Cache custom domains for 1 hour
|
|
26
|
+
CACHE_TTL_SECONDS = 3600
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class CustomDomain:
|
|
31
|
+
"""A structured Cloudsmith custom domain record."""
|
|
32
|
+
|
|
33
|
+
host: str
|
|
34
|
+
backend_kind: int | None
|
|
35
|
+
enabled: bool
|
|
36
|
+
validated: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_cache_dir() -> Path:
|
|
40
|
+
"""
|
|
41
|
+
Get the cache directory for custom domains.
|
|
42
|
+
"""
|
|
43
|
+
cache_dir = Path(get_default_config_path()) / "custom_domains_cache"
|
|
44
|
+
cache_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
45
|
+
return cache_dir
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_cache_path(org: str) -> Path:
|
|
49
|
+
"""
|
|
50
|
+
Get the cache file path for an organization's custom domains.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
org: Organization slug
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Path to cache file
|
|
57
|
+
"""
|
|
58
|
+
cache_dir = get_cache_dir()
|
|
59
|
+
safe_org = "".join(c if c.isalnum() or c in "-_" else "_" for c in org)
|
|
60
|
+
return cache_dir / f"{safe_org}.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_cache_valid(cache_path: Path) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Check if a cache file exists and is still valid.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
cache_path: Path to cache file
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
bool: True if cache exists and hasn't expired
|
|
72
|
+
"""
|
|
73
|
+
if not cache_path.exists():
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
mtime = cache_path.stat().st_mtime
|
|
78
|
+
age = time.time() - mtime
|
|
79
|
+
return age < CACHE_TTL_SECONDS
|
|
80
|
+
except OSError:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_cache(cache_path: Path) -> list[CustomDomain] | None:
|
|
85
|
+
"""
|
|
86
|
+
Read custom domains from cache file.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
cache_path: Path to cache file
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of CustomDomain records or None if cache invalid/missing
|
|
93
|
+
"""
|
|
94
|
+
if not is_cache_valid(cache_path):
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
with open(cache_path, encoding="utf-8") as f:
|
|
99
|
+
data = json.load(f)
|
|
100
|
+
if isinstance(data, dict) and "domains" in data:
|
|
101
|
+
domains = data["domains"]
|
|
102
|
+
if isinstance(domains, list):
|
|
103
|
+
# Detect legacy format: non-empty list of strings (old build stored
|
|
104
|
+
# domains as plain strings, not dicts). Treat as a cache miss so the
|
|
105
|
+
# caller re-fetches and rewrites in the current dict format.
|
|
106
|
+
if domains and not any(isinstance(d, dict) for d in domains):
|
|
107
|
+
logger.debug(
|
|
108
|
+
"Stale string-format cache detected at %s, treating as miss",
|
|
109
|
+
cache_path,
|
|
110
|
+
)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
records = [
|
|
114
|
+
CustomDomain(
|
|
115
|
+
host=d["host"],
|
|
116
|
+
backend_kind=d.get("backend_kind"),
|
|
117
|
+
enabled=bool(d.get("enabled", False)),
|
|
118
|
+
validated=bool(d.get("validated", False)),
|
|
119
|
+
)
|
|
120
|
+
for d in domains
|
|
121
|
+
if isinstance(d, dict) and d.get("host")
|
|
122
|
+
]
|
|
123
|
+
logger.debug(
|
|
124
|
+
"Read %d domains from cache: %s", len(records), cache_path
|
|
125
|
+
)
|
|
126
|
+
return records
|
|
127
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
128
|
+
logger.debug("Failed to read cache %s: %s", cache_path, exc)
|
|
129
|
+
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def write_cache(cache_path: Path, domains: list[CustomDomain]) -> None:
|
|
134
|
+
"""Write custom domains to cache file."""
|
|
135
|
+
data = {
|
|
136
|
+
"domains": [
|
|
137
|
+
{
|
|
138
|
+
"host": d.host,
|
|
139
|
+
"backend_kind": d.backend_kind,
|
|
140
|
+
"enabled": d.enabled,
|
|
141
|
+
"validated": d.validated,
|
|
142
|
+
}
|
|
143
|
+
for d in domains
|
|
144
|
+
],
|
|
145
|
+
"cached_at": time.time(),
|
|
146
|
+
}
|
|
147
|
+
try:
|
|
148
|
+
atomic_write_json(cache_path, data)
|
|
149
|
+
logger.debug("Wrote %d domains to cache: %s", len(domains), cache_path)
|
|
150
|
+
except OSError as exc:
|
|
151
|
+
logger.debug("Failed to write cache %s: %s", cache_path, exc)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_custom_domains( # pylint: disable=too-many-return-statements
|
|
155
|
+
org: str,
|
|
156
|
+
*,
|
|
157
|
+
api_key: str | None = None,
|
|
158
|
+
auth_type: str = "api_key",
|
|
159
|
+
api_host: str | None = None,
|
|
160
|
+
refresh: bool = False,
|
|
161
|
+
) -> list[CustomDomain]:
|
|
162
|
+
"""
|
|
163
|
+
Fetch custom domains for a Cloudsmith organization.
|
|
164
|
+
|
|
165
|
+
Results are cached on the filesystem for 1 hour to avoid excessive API calls.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
org: Organization slug
|
|
169
|
+
api_key: Optional API key/token for authentication
|
|
170
|
+
auth_type: "api_key" (uses X-Api-Key header) or "bearer" (uses Authorization: Bearer)
|
|
171
|
+
api_host: Cloudsmith API host URL (including version). Taken from the SDK
|
|
172
|
+
configuration default when not provided.
|
|
173
|
+
refresh: When ``True``, skip the cache read and always fetch from the API.
|
|
174
|
+
The fresh result is still written to the cache.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List of CustomDomain records.
|
|
178
|
+
Empty list if API call fails or org has no custom domains.
|
|
179
|
+
|
|
180
|
+
Note:
|
|
181
|
+
Only ``ApiException`` is handled here (per-status). Network-layer errors
|
|
182
|
+
(DNS/timeout/SSL/urllib3) are intentionally NOT caught — they propagate to
|
|
183
|
+
the caller. The credential-helper protocol boundary (the click command) and
|
|
184
|
+
the installer are responsible for catching broadly and refusing gracefully,
|
|
185
|
+
so the library stays free of bare ``except Exception`` (reviewer feedback).
|
|
186
|
+
"""
|
|
187
|
+
cache_path = get_cache_path(org)
|
|
188
|
+
cached = None if refresh else read_cache(cache_path)
|
|
189
|
+
if cached is not None:
|
|
190
|
+
logger.debug("Using cached custom domains for %s", org)
|
|
191
|
+
return cached
|
|
192
|
+
|
|
193
|
+
logger.debug("Fetching custom domains from API for %s", org)
|
|
194
|
+
|
|
195
|
+
normalized_auth_type: Literal["api_key", "bearer"] = (
|
|
196
|
+
"bearer" if auth_type == "bearer" else "api_key"
|
|
197
|
+
)
|
|
198
|
+
credential = (
|
|
199
|
+
CredentialResult(
|
|
200
|
+
api_key=api_key,
|
|
201
|
+
source_name="credential-helper",
|
|
202
|
+
auth_type=normalized_auth_type,
|
|
203
|
+
)
|
|
204
|
+
if api_key
|
|
205
|
+
else None
|
|
206
|
+
)
|
|
207
|
+
initialise_api(host=api_host, credential=credential)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
raw_domains = list_custom_domains(org)
|
|
211
|
+
except ApiException as exc:
|
|
212
|
+
if exc.status in (401, 403):
|
|
213
|
+
# Don't cache auth failures - might work later once authenticated.
|
|
214
|
+
logger.debug(
|
|
215
|
+
"Custom domains API requires auth - assuming no custom domains for %s",
|
|
216
|
+
org,
|
|
217
|
+
)
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
if exc.status == 404:
|
|
221
|
+
# Cache empty result to avoid repeated lookups for a missing org.
|
|
222
|
+
logger.debug("Organization %s not found or has no custom domains", org)
|
|
223
|
+
write_cache(cache_path, [])
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
if exc.status == 402:
|
|
227
|
+
# Custom domains product feature not enabled - treat as none.
|
|
228
|
+
logger.debug("Custom domains not enabled for %s", org)
|
|
229
|
+
write_cache(cache_path, [])
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
logger.debug("Failed to fetch custom domains for %s: HTTP %s", org, exc.status)
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
records = [
|
|
236
|
+
CustomDomain(
|
|
237
|
+
host=d["host"],
|
|
238
|
+
backend_kind=d.get("backend_kind"),
|
|
239
|
+
enabled=bool(d.get("enabled", False)),
|
|
240
|
+
validated=bool(d.get("validated", False)),
|
|
241
|
+
)
|
|
242
|
+
for d in raw_domains
|
|
243
|
+
if d.get("host")
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
logger.debug("Fetched %d custom domains for %s", len(records), org)
|
|
247
|
+
write_cache(cache_path, records)
|
|
248
|
+
return records
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_format_domains(
|
|
252
|
+
org: str,
|
|
253
|
+
backend_kind: int,
|
|
254
|
+
*,
|
|
255
|
+
api_key: str | None = None,
|
|
256
|
+
auth_type: str = "api_key",
|
|
257
|
+
api_host: str | None = None,
|
|
258
|
+
refresh: bool = False,
|
|
259
|
+
) -> list[str]:
|
|
260
|
+
"""
|
|
261
|
+
Return enabled and validated custom domain hostnames for a specific backend format.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
org: Organization slug
|
|
265
|
+
backend_kind: BackendKind int value (e.g. BackendKind.DOCKER == 6)
|
|
266
|
+
api_key: Optional API key/token for authentication
|
|
267
|
+
auth_type: "api_key" or "bearer"
|
|
268
|
+
api_host: Cloudsmith API host URL
|
|
269
|
+
refresh: When ``True``, bypass the cache and fetch fresh data from the API.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of hostnames that are enabled, validated, and match the given backend_kind.
|
|
273
|
+
"""
|
|
274
|
+
domains = get_custom_domains(
|
|
275
|
+
org, api_key=api_key, auth_type=auth_type, api_host=api_host, refresh=refresh
|
|
276
|
+
)
|
|
277
|
+
return [
|
|
278
|
+
d.host
|
|
279
|
+
for d in domains
|
|
280
|
+
if d.backend_kind == int(backend_kind) and d.enabled and d.validated
|
|
281
|
+
]
|