cloudconsolelink 4.0.0__tar.gz → 4.1.1__tar.gz
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.
- {cloudconsolelink-4.0.0/cloudconsolelink.egg-info → cloudconsolelink-4.1.1}/PKG-INFO +25 -3
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/README.md +23 -1
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/__init__.py +1 -1
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/aws/links.py +11 -4
- cloudconsolelink-4.1.1/cloudconsolelink/clouds/registry/__init__.py +230 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1/cloudconsolelink.egg-info}/PKG-INFO +25 -3
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink.egg-info/SOURCES.txt +4 -1
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink.egg-info/requires.txt +1 -1
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/requirements-dev.txt +1 -1
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/setup.py +17 -1
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/tests/test_aws.py +59 -1
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/tests/test_provider_coverage.py +38 -0
- cloudconsolelink-4.1.1/tests/test_registry_link.py +116 -0
- cloudconsolelink-4.1.1/tests/test_registry_parse.py +92 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/LICENSE +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/MANIFEST.in +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/__init__.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/aws/__init__.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/aws/errors.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/azure/__init__.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/gcp/__init__.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/gcp/links.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/oci/__init__.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink/clouds/oci/links.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink.egg-info/dependency_links.txt +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink.egg-info/top_level.txt +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/requirements.txt +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/setup.cfg +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/tests/test_azure.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/tests/test_gcp.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/tests/test_oci.py +0 -0
- {cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/tests/test_packaging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudconsolelink
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.1
|
|
4
4
|
Summary: Generate console links for cloud resources
|
|
5
5
|
Home-page: https://github.com/Cloudanix/cloud-console-links
|
|
6
6
|
Maintainer: Cloudanix
|
|
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Provides-Extra: dev
|
|
16
16
|
Requires-Dist: pre-commit~=4.2; extra == "dev"
|
|
17
|
-
Requires-Dist: pytest
|
|
17
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
18
18
|
Requires-Dist: pytest-cov==2.11; extra == "dev"
|
|
19
19
|
Requires-Dist: pytest-mock~=3.14; extra == "dev"
|
|
20
20
|
Requires-Dist: types-PyYAML~=6.0; extra == "dev"
|
|
@@ -65,6 +65,9 @@ make check
|
|
|
65
65
|
#### For GCP
|
|
66
66
|
```from cloudconsolelink.clouds.gcp import GCPLinker```
|
|
67
67
|
|
|
68
|
+
#### For container images (ECR, GAR/GCR, ACR, Docker Hub, OCIR)
|
|
69
|
+
```from cloudconsolelink.clouds.registry import image_console_link```
|
|
70
|
+
|
|
68
71
|
#### To get console link
|
|
69
72
|
call method ```get_console_link()```
|
|
70
73
|
|
|
@@ -119,7 +122,26 @@ resource_name = "storage_bucket"
|
|
|
119
122
|
console_link = gcp.get_console_link(bucket_name=bucket_name, resource_name=resource_name)
|
|
120
123
|
```
|
|
121
124
|
|
|
122
|
-
|
|
125
|
+
#### Container image (ECR, GAR/GCR, ACR, Docker Hub, OCIR):
|
|
126
|
+
```python
|
|
127
|
+
from cloudconsolelink.clouds.registry import image_console_link
|
|
128
|
+
|
|
129
|
+
# Provider is detected from the registry hostname of the image reference.
|
|
130
|
+
console_link = image_console_link(
|
|
131
|
+
"602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni-init"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Azure Container Registry needs subscription/resource-group hints for a deep
|
|
135
|
+
# link (they are not present in the image reference); without them it degrades
|
|
136
|
+
# to a registry browse link.
|
|
137
|
+
console_link = image_console_link(
|
|
138
|
+
"myregistry.azurecr.io/app",
|
|
139
|
+
subscription_id="5592e8dc-...",
|
|
140
|
+
resource_group="my-rg",
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## get_console_link() parameters description:
|
|
123
145
|
|
|
124
146
|
### AWS:
|
|
125
147
|
1) arn: arn of resource
|
|
@@ -33,6 +33,9 @@ make check
|
|
|
33
33
|
#### For GCP
|
|
34
34
|
```from cloudconsolelink.clouds.gcp import GCPLinker```
|
|
35
35
|
|
|
36
|
+
#### For container images (ECR, GAR/GCR, ACR, Docker Hub, OCIR)
|
|
37
|
+
```from cloudconsolelink.clouds.registry import image_console_link```
|
|
38
|
+
|
|
36
39
|
#### To get console link
|
|
37
40
|
call method ```get_console_link()```
|
|
38
41
|
|
|
@@ -87,7 +90,26 @@ resource_name = "storage_bucket"
|
|
|
87
90
|
console_link = gcp.get_console_link(bucket_name=bucket_name, resource_name=resource_name)
|
|
88
91
|
```
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
#### Container image (ECR, GAR/GCR, ACR, Docker Hub, OCIR):
|
|
94
|
+
```python
|
|
95
|
+
from cloudconsolelink.clouds.registry import image_console_link
|
|
96
|
+
|
|
97
|
+
# Provider is detected from the registry hostname of the image reference.
|
|
98
|
+
console_link = image_console_link(
|
|
99
|
+
"602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni-init"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Azure Container Registry needs subscription/resource-group hints for a deep
|
|
103
|
+
# link (they are not present in the image reference); without them it degrades
|
|
104
|
+
# to a registry browse link.
|
|
105
|
+
console_link = image_console_link(
|
|
106
|
+
"myregistry.azurecr.io/app",
|
|
107
|
+
subscription_id="5592e8dc-...",
|
|
108
|
+
resource_group="my-rg",
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## get_console_link() parameters description:
|
|
91
113
|
|
|
92
114
|
### AWS:
|
|
93
115
|
1) arn: arn of resource
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version of the module
|
|
2
|
-
__version__ = "4.0.
|
|
2
|
+
__version__ = "4.0.1"
|
|
@@ -6,6 +6,7 @@ from typing import Dict
|
|
|
6
6
|
# https://{region}.console.aws.amazon.com/{service}/home?region={region}.
|
|
7
7
|
HOME_URLS: Dict[str, str] = {
|
|
8
8
|
"elasticloadbalancing": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region={data.get("region", "")}#LoadBalancers:',
|
|
9
|
+
"network-firewall": 'https://{data.get("region", "")}.{data.get("console", "")}/vpc/home?region={data.get("region", "")}#Firewalls:',
|
|
9
10
|
"logs": 'https://{data.get("region", "")}.{data.get("console", "")}/cloudwatch/home?region={data.get("region", "")}#logsV2:log-groups',
|
|
10
11
|
"sns": 'https://{data.get("region", "")}.{data.get("console", "")}/sns/v3/home?region={data.get("region", "")}',
|
|
11
12
|
"sqs": 'https://{data.get("region", "")}.{data.get("console", "")}/sqs/v2/home?region={data.get("region", "")}',
|
|
@@ -407,10 +408,10 @@ def get_links() -> Dict:
|
|
|
407
408
|
home?region=u{data.get("region", "")}#LoadBalancers:search={data.get("resource", "")};sort=loadBalancerName',
|
|
408
409
|
},
|
|
409
410
|
"ecr": { # Amazon Elastic Container Registry
|
|
410
|
-
"repository": 'https://{data.get("region", "")}.{data.get("console", "")}/ecr/repositories/
|
|
411
|
-
("resource", "")}?region={data.get("region", "")}',
|
|
412
|
-
"image": 'https://{data.get("region", "")}.{data.get("console", "")}/ecr/repositories/
|
|
413
|
-
("resource", "")}?region={data.get("region", "")}',
|
|
411
|
+
"repository": 'https://{data.get("region", "")}.{data.get("console", "")}/ecr/repositories/private/\
|
|
412
|
+
{data.get("account", "")}/{data.get("resource", "")}?region={data.get("region", "")}',
|
|
413
|
+
"image": 'https://{data.get("region", "")}.{data.get("console", "")}/ecr/repositories/private/\
|
|
414
|
+
{data.get("account", "")}/{data.get("resource", "")}?region={data.get("region", "")}',
|
|
414
415
|
},
|
|
415
416
|
"ecs": { # Amazon Elastic Container Service
|
|
416
417
|
"cluster": 'https://{data.get("region", "")}.{data.get("console", "")}/ecs/home?region={data.get("region", "")}#/clusters/{data.get("resource", "")}',
|
|
@@ -828,6 +829,12 @@ def get_links() -> Dict:
|
|
|
828
829
|
},
|
|
829
830
|
"neptune-db": { # Amazon Neptune
|
|
830
831
|
},
|
|
832
|
+
"network-firewall": { # AWS Network Firewall
|
|
833
|
+
"firewall": 'https://{data.get("region", "")}.{data.get("console", "")}/vpc/home?region={data.get("region", "")}#FirewallDetails:firewallArn={arn}',
|
|
834
|
+
"firewall-policy": 'https://{data.get("region", "")}.{data.get("console", "")}/vpc/home?region={data.get("region", "")}#FirewallPolicyDetails:firewallPolicyArn={arn}',
|
|
835
|
+
"stateful-rulegroup": 'https://{data.get("region", "")}.{data.get("console", "")}/vpc/home?region={data.get("region", "")}#StatefulRuleGroupDetails:ruleGroupArn={arn}',
|
|
836
|
+
"stateless-rulegroup": 'https://{data.get("region", "")}.{data.get("console", "")}/vpc/home?region={data.get("region", "")}#StatelessRuleGroupDetails:ruleGroupArn={arn}',
|
|
837
|
+
},
|
|
831
838
|
"networkmanager": { # Network Manager
|
|
832
839
|
"device": 'https://{data.get("console", "")}/networkmanager/home?region={data.get("region", "")}#/networks/{data.get("resource", "")}/devices',
|
|
833
840
|
"global-network": 'https://{data.get("console", "")}/networkmanager/home?region={data.get("region", "")}#/networks/{data.get("resource", "")}',
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Container-registry (OCI image) console deep-links.
|
|
2
|
+
|
|
3
|
+
An image reference (``registry-host/repository[:tag][@digest]``) is a different
|
|
4
|
+
identifier axis than an ARN / resource_name / resource id: the *hostname*
|
|
5
|
+
identifies the provider. This module parses the reference, classifies the host,
|
|
6
|
+
and routes to a console link — delegating to the existing provider linkers where
|
|
7
|
+
a path already exists, and building the URL directly only where none does
|
|
8
|
+
(Docker Hub, ECR Public, GCR, OCIR, ACR, and image/digest drill-in).
|
|
9
|
+
|
|
10
|
+
See ``docs/oci-images/spec.md``.
|
|
11
|
+
"""
|
|
12
|
+
import re
|
|
13
|
+
from collections import namedtuple
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
from cloudconsolelink.clouds.aws import AWSLinker, get_console
|
|
17
|
+
from cloudconsolelink.clouds.gcp import GCPLinker
|
|
18
|
+
|
|
19
|
+
ImageRef = namedtuple("ImageRef", ["host", "repository", "tag", "digest"])
|
|
20
|
+
|
|
21
|
+
# <account>.dkr.ecr.<region>.amazonaws.com (optional .cn for the aws-cn partition)
|
|
22
|
+
_ECR_HOST = re.compile(r"^(\d+)\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com(\.cn)?$")
|
|
23
|
+
# <region>.gcr.io legacy hosts (gcr.io, us.gcr.io, eu.gcr.io, asia.gcr.io, ...)
|
|
24
|
+
_GCR_HOST = re.compile(r"^([a-z0-9-]+\.)?gcr\.io$")
|
|
25
|
+
|
|
26
|
+
_DOCKERHUB_HOSTS = {"", "docker.io", "registry-1.docker.io", "index.docker.io"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_image_ref(ref):
|
|
30
|
+
"""Split an OCI image reference into ``ImageRef(host, repository, tag, digest)``.
|
|
31
|
+
|
|
32
|
+
Grammar: ``[registry-host[:port]/]repository[:tag][@digest]``. A first path
|
|
33
|
+
segment that looks like a hostname (contains ``.`` or ``:``, or is
|
|
34
|
+
``localhost``) is treated as the registry host; otherwise the reference is
|
|
35
|
+
hostless (Docker Hub). ``repository`` keeps all remaining ``/`` segments.
|
|
36
|
+
"""
|
|
37
|
+
ref = (ref or "").strip()
|
|
38
|
+
if not ref:
|
|
39
|
+
raise ValueError("empty image reference")
|
|
40
|
+
|
|
41
|
+
digest = ""
|
|
42
|
+
if "@" in ref:
|
|
43
|
+
ref, digest = ref.split("@", 1)
|
|
44
|
+
if not digest:
|
|
45
|
+
raise ValueError(f"image reference has an empty digest: {ref!r}@")
|
|
46
|
+
|
|
47
|
+
first, slash, rest = ref.partition("/")
|
|
48
|
+
if slash and ("." in first or ":" in first or first == "localhost"):
|
|
49
|
+
host, path = first, rest
|
|
50
|
+
else:
|
|
51
|
+
host, path = "", ref
|
|
52
|
+
|
|
53
|
+
tag = ""
|
|
54
|
+
seg_start = path.rfind("/") + 1
|
|
55
|
+
colon = path.find(":", seg_start)
|
|
56
|
+
if colon != -1:
|
|
57
|
+
tag = path[colon + 1:]
|
|
58
|
+
path = path[:colon]
|
|
59
|
+
if not tag:
|
|
60
|
+
raise ValueError(f"image reference has an empty tag: {path!r}:")
|
|
61
|
+
|
|
62
|
+
if not path:
|
|
63
|
+
raise ValueError(f"image reference has no repository: {ref!r}")
|
|
64
|
+
|
|
65
|
+
return ImageRef(host=host, repository=path, tag=tag, digest=digest)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def classify(host):
|
|
69
|
+
"""Map a registry hostname to a registry family key.
|
|
70
|
+
|
|
71
|
+
Returns one of ``ecr`` | ``ecr-public`` | ``gar`` | ``gcr`` | ``ocir`` |
|
|
72
|
+
``acr`` | ``dockerhub``, or ``""`` for an unrecognized host.
|
|
73
|
+
"""
|
|
74
|
+
if host in _DOCKERHUB_HOSTS:
|
|
75
|
+
return "dockerhub"
|
|
76
|
+
if host == "public.ecr.aws":
|
|
77
|
+
return "ecr-public"
|
|
78
|
+
if _ECR_HOST.match(host):
|
|
79
|
+
return "ecr"
|
|
80
|
+
if host.endswith("-docker.pkg.dev"):
|
|
81
|
+
return "gar"
|
|
82
|
+
if _GCR_HOST.match(host):
|
|
83
|
+
return "gcr"
|
|
84
|
+
if host.endswith(".ocir.io"):
|
|
85
|
+
return "ocir"
|
|
86
|
+
if host.endswith(".azurecr.io"):
|
|
87
|
+
return "acr"
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- per-family builders ---------------------------------------------------
|
|
92
|
+
# Repo-level links delegate to the existing provider linker (no URL duplicated).
|
|
93
|
+
# Image/tag/digest drill-in URLs are built here because no existing identifier
|
|
94
|
+
# (ARN / resource_name) can carry image coordinates.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _ecr(ref, hints):
|
|
98
|
+
account, region, cn = _ECR_HOST.match(ref.host).groups()
|
|
99
|
+
if region.startswith("us-gov-"):
|
|
100
|
+
partition = "aws-us-gov"
|
|
101
|
+
elif cn:
|
|
102
|
+
partition = "aws-cn"
|
|
103
|
+
else:
|
|
104
|
+
partition = "aws"
|
|
105
|
+
|
|
106
|
+
if ref.digest:
|
|
107
|
+
# ECR console addresses images by digest — net-new, ARN can't carry it.
|
|
108
|
+
console = get_console(partition)
|
|
109
|
+
return (
|
|
110
|
+
f"https://{region}.{console}/ecr/repositories/private/{account}/"
|
|
111
|
+
f"{ref.repository}/_/image/{ref.digest}/details?region={region}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Repo view (tag-only refs land here too — tags are listed on the repo page).
|
|
115
|
+
arn = f"arn:{partition}:ecr:{region}:{account}:repository/{ref.repository}"
|
|
116
|
+
return AWSLinker().get_console_link(arn=arn)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _ecr_public(ref, hints):
|
|
120
|
+
return f"https://gallery.ecr.aws/{ref.repository}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _gar(ref, hints):
|
|
124
|
+
location = ref.host[: -len("-docker.pkg.dev")]
|
|
125
|
+
parts = ref.repository.split("/")
|
|
126
|
+
if len(parts) < 2:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"GAR image reference needs <project>/<repository>[/<image>]: {ref.repository!r}"
|
|
129
|
+
)
|
|
130
|
+
project, repo = parts[0], parts[1]
|
|
131
|
+
|
|
132
|
+
if len(parts) >= 3:
|
|
133
|
+
image = "/".join(parts[2:]) # net-new: image drill-in below the repo
|
|
134
|
+
return (
|
|
135
|
+
f"https://console.cloud.google.com/artifacts/docker/{project}/{location}/"
|
|
136
|
+
f"{repo}/{image}?project={project}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# project/repo -> repo view via the existing Artifact Registry template.
|
|
140
|
+
return GCPLinker().get_console_link(
|
|
141
|
+
resource_name="artifact_registry_repository",
|
|
142
|
+
project_id=project,
|
|
143
|
+
region=location,
|
|
144
|
+
instance_name=repo,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _gcr(ref, hints):
|
|
149
|
+
# gcr.io -> GLOBAL, us.gcr.io -> US, eu.gcr.io -> EU, asia.gcr.io -> ASIA
|
|
150
|
+
prefix = ref.host[: -len(".gcr.io")] if ref.host != "gcr.io" else ""
|
|
151
|
+
location = prefix.upper() if prefix else "GLOBAL"
|
|
152
|
+
parts = ref.repository.split("/")
|
|
153
|
+
project = parts[0]
|
|
154
|
+
image = "/".join(parts[1:])
|
|
155
|
+
if not image:
|
|
156
|
+
return f"https://console.cloud.google.com/gcr/images/{project}?project={project}"
|
|
157
|
+
return (
|
|
158
|
+
f"https://console.cloud.google.com/gcr/images/{project}/{location}/"
|
|
159
|
+
f"{image}?project={project}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _dockerhub(ref, hints):
|
|
164
|
+
parts = ref.repository.split("/")
|
|
165
|
+
if len(parts) == 1:
|
|
166
|
+
namespace, repo = "library", parts[0]
|
|
167
|
+
else:
|
|
168
|
+
namespace, repo = parts[0], "/".join(parts[1:])
|
|
169
|
+
|
|
170
|
+
if namespace == "library":
|
|
171
|
+
url = f"https://hub.docker.com/_/{repo}"
|
|
172
|
+
else:
|
|
173
|
+
url = f"https://hub.docker.com/r/{namespace}/{repo}"
|
|
174
|
+
if ref.tag:
|
|
175
|
+
url += f"/tags?name={ref.tag}"
|
|
176
|
+
return url
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _ocir(ref, hints):
|
|
180
|
+
# OCIR console needs an OCID (absent from the image ref) to reach a repo, so
|
|
181
|
+
# fall back to the Container Registry landing page. ponytail: no OCID lookup.
|
|
182
|
+
return "https://cloud.oracle.com/registry/containers/repos"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _acr(ref, hints):
|
|
186
|
+
name = ref.host[: -len(".azurecr.io")]
|
|
187
|
+
sub = hints.get("subscription_id")
|
|
188
|
+
rg = hints.get("resource_group")
|
|
189
|
+
if sub and rg:
|
|
190
|
+
arm_id = (
|
|
191
|
+
f"/subscriptions/{sub}/resourceGroups/{rg}/providers/"
|
|
192
|
+
f"Microsoft.ContainerRegistry/registries/{name}"
|
|
193
|
+
)
|
|
194
|
+
return (
|
|
195
|
+
"https://portal.azure.com/#view/Microsoft_Azure_ContainerRegistries/"
|
|
196
|
+
f"RepositoryBlade/registryId/{quote(arm_id, safe='')}/repository/{ref.repository}"
|
|
197
|
+
)
|
|
198
|
+
# No subscription/resource-group hints -> registry browse fallback.
|
|
199
|
+
return (
|
|
200
|
+
"https://portal.azure.com/#blade/HubsExtension/BrowseResource/"
|
|
201
|
+
"resourceType/Microsoft.ContainerRegistry%2Fregistries"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_BUILDERS = {
|
|
206
|
+
"ecr": _ecr,
|
|
207
|
+
"ecr-public": _ecr_public,
|
|
208
|
+
"gar": _gar,
|
|
209
|
+
"gcr": _gcr,
|
|
210
|
+
"dockerhub": _dockerhub,
|
|
211
|
+
"ocir": _ocir,
|
|
212
|
+
"acr": _acr,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def image_console_link(image_ref, **hints):
|
|
217
|
+
"""Return a cloud-console deep-link for a container image reference.
|
|
218
|
+
|
|
219
|
+
``hints`` supplies data absent from the reference itself — currently
|
|
220
|
+
``subscription_id`` and ``resource_group`` for Azure Container Registry.
|
|
221
|
+
An unrecognized registry host falls back to a Docker Hub search.
|
|
222
|
+
"""
|
|
223
|
+
ref = parse_image_ref(image_ref)
|
|
224
|
+
builder = _BUILDERS.get(classify(ref.host))
|
|
225
|
+
if builder is None:
|
|
226
|
+
return f"https://hub.docker.com/search?q={quote(ref.repository)}"
|
|
227
|
+
return builder(ref, hints)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
__all__ = ["ImageRef", "parse_image_ref", "classify", "image_console_link"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudconsolelink
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.1
|
|
4
4
|
Summary: Generate console links for cloud resources
|
|
5
5
|
Home-page: https://github.com/Cloudanix/cloud-console-links
|
|
6
6
|
Maintainer: Cloudanix
|
|
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Provides-Extra: dev
|
|
16
16
|
Requires-Dist: pre-commit~=4.2; extra == "dev"
|
|
17
|
-
Requires-Dist: pytest
|
|
17
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
18
18
|
Requires-Dist: pytest-cov==2.11; extra == "dev"
|
|
19
19
|
Requires-Dist: pytest-mock~=3.14; extra == "dev"
|
|
20
20
|
Requires-Dist: types-PyYAML~=6.0; extra == "dev"
|
|
@@ -65,6 +65,9 @@ make check
|
|
|
65
65
|
#### For GCP
|
|
66
66
|
```from cloudconsolelink.clouds.gcp import GCPLinker```
|
|
67
67
|
|
|
68
|
+
#### For container images (ECR, GAR/GCR, ACR, Docker Hub, OCIR)
|
|
69
|
+
```from cloudconsolelink.clouds.registry import image_console_link```
|
|
70
|
+
|
|
68
71
|
#### To get console link
|
|
69
72
|
call method ```get_console_link()```
|
|
70
73
|
|
|
@@ -119,7 +122,26 @@ resource_name = "storage_bucket"
|
|
|
119
122
|
console_link = gcp.get_console_link(bucket_name=bucket_name, resource_name=resource_name)
|
|
120
123
|
```
|
|
121
124
|
|
|
122
|
-
|
|
125
|
+
#### Container image (ECR, GAR/GCR, ACR, Docker Hub, OCIR):
|
|
126
|
+
```python
|
|
127
|
+
from cloudconsolelink.clouds.registry import image_console_link
|
|
128
|
+
|
|
129
|
+
# Provider is detected from the registry hostname of the image reference.
|
|
130
|
+
console_link = image_console_link(
|
|
131
|
+
"602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni-init"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Azure Container Registry needs subscription/resource-group hints for a deep
|
|
135
|
+
# link (they are not present in the image reference); without them it degrades
|
|
136
|
+
# to a registry browse link.
|
|
137
|
+
console_link = image_console_link(
|
|
138
|
+
"myregistry.azurecr.io/app",
|
|
139
|
+
subscription_id="5592e8dc-...",
|
|
140
|
+
resource_group="my-rg",
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## get_console_link() parameters description:
|
|
123
145
|
|
|
124
146
|
### AWS:
|
|
125
147
|
1) arn: arn of resource
|
|
@@ -20,9 +20,12 @@ cloudconsolelink/clouds/gcp/__init__.py
|
|
|
20
20
|
cloudconsolelink/clouds/gcp/links.py
|
|
21
21
|
cloudconsolelink/clouds/oci/__init__.py
|
|
22
22
|
cloudconsolelink/clouds/oci/links.py
|
|
23
|
+
cloudconsolelink/clouds/registry/__init__.py
|
|
23
24
|
tests/test_aws.py
|
|
24
25
|
tests/test_azure.py
|
|
25
26
|
tests/test_gcp.py
|
|
26
27
|
tests/test_oci.py
|
|
27
28
|
tests/test_packaging.py
|
|
28
|
-
tests/test_provider_coverage.py
|
|
29
|
+
tests/test_provider_coverage.py
|
|
30
|
+
tests/test_registry_link.py
|
|
31
|
+
tests/test_registry_parse.py
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import subprocess
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import setuptools
|
|
@@ -24,8 +25,23 @@ def read_requirements(path: str) -> list[str]:
|
|
|
24
25
|
return lines
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
def git_version(fallback: str) -> str:
|
|
29
|
+
# ponytail: build-time version from the current git tag; falls back to
|
|
30
|
+
# __init__.py when git/tags are absent (e.g. building from an sdist tarball).
|
|
31
|
+
try:
|
|
32
|
+
tag = subprocess.check_output(
|
|
33
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
34
|
+
cwd=ROOT,
|
|
35
|
+
stderr=subprocess.DEVNULL,
|
|
36
|
+
)
|
|
37
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
38
|
+
return fallback
|
|
39
|
+
return tag.decode().strip().lstrip("v") or fallback
|
|
40
|
+
|
|
41
|
+
|
|
27
42
|
package_info: dict[str, object] = {}
|
|
28
43
|
exec(read_text("cloudconsolelink/__init__.py"), package_info)
|
|
44
|
+
version = git_version(str(package_info["__version__"]))
|
|
29
45
|
long_description = read_text("README.md")
|
|
30
46
|
install_requires = read_requirements("requirements.txt")
|
|
31
47
|
dev_requirements = read_requirements("requirements-dev.txt")
|
|
@@ -35,7 +51,7 @@ setuptools.setup(
|
|
|
35
51
|
# Here is the module name.
|
|
36
52
|
name="cloudconsolelink",
|
|
37
53
|
# version of the module
|
|
38
|
-
version=
|
|
54
|
+
version=version,
|
|
39
55
|
# Name of Maintainer
|
|
40
56
|
maintainer="Cloudanix",
|
|
41
57
|
# Maintainer Email address
|
|
@@ -6,6 +6,7 @@ from cloudconsolelink.clouds.aws import InvalidARNError
|
|
|
6
6
|
from cloudconsolelink.clouds.aws import InvalidPartitionError
|
|
7
7
|
from cloudconsolelink.clouds.aws import InvalidServiceError
|
|
8
8
|
from cloudconsolelink.clouds.aws import get_service_home_link
|
|
9
|
+
from cloudconsolelink.clouds.aws import _render
|
|
9
10
|
from cloudconsolelink.clouds.aws.links import get_links
|
|
10
11
|
|
|
11
12
|
aws = AWSLinker()
|
|
@@ -23,7 +24,7 @@ def test_aws_ec2_instance():
|
|
|
23
24
|
|
|
24
25
|
def test_aws_ecr_repository():
|
|
25
26
|
arn = "arn:aws:ecr:us-east1:1234567890:repository/repo1"
|
|
26
|
-
expected_link = "https://us-east1.console.aws.amazon.com/ecr/repositories/repo1\
|
|
27
|
+
expected_link = "https://us-east1.console.aws.amazon.com/ecr/repositories/private/1234567890/repo1\
|
|
27
28
|
?region=us-east1"
|
|
28
29
|
|
|
29
30
|
out_link = aws.get_console_link(arn=arn)
|
|
@@ -283,3 +284,60 @@ def test_aws_ec2_key_pair_trailing_colon_falls_back_to_key_pair_page():
|
|
|
283
284
|
out_link = aws.get_console_link(arn=arn)
|
|
284
285
|
assert out_link == "https://us-east-1.console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:"
|
|
285
286
|
|
|
287
|
+
|
|
288
|
+
def test_aws_network_firewall_firewall():
|
|
289
|
+
arn = "arn:aws:network-firewall:us-east-1:123456789012:firewall/my-firewall"
|
|
290
|
+
out_link = aws.get_console_link(arn=arn)
|
|
291
|
+
assert out_link == (
|
|
292
|
+
"https://us-east-1.console.aws.amazon.com/vpc/home"
|
|
293
|
+
"?region=us-east-1"
|
|
294
|
+
"#FirewallDetails:firewallArn=arn:aws:network-firewall:us-east-1:123456789012:firewall/my-firewall"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_aws_network_firewall_policy():
|
|
299
|
+
arn = "arn:aws:network-firewall:us-east-1:123456789012:firewall-policy/my-policy"
|
|
300
|
+
out_link = aws.get_console_link(arn=arn)
|
|
301
|
+
assert out_link == (
|
|
302
|
+
"https://us-east-1.console.aws.amazon.com/vpc/home"
|
|
303
|
+
"?region=us-east-1"
|
|
304
|
+
"#FirewallPolicyDetails:firewallPolicyArn=arn:aws:network-firewall:us-east-1:123456789012:firewall-policy/my-policy"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_aws_network_firewall_stateful_rulegroup():
|
|
309
|
+
arn = "arn:aws:network-firewall:us-east-1:123456789012:stateful-rulegroup/my-rules"
|
|
310
|
+
out_link = aws.get_console_link(arn=arn)
|
|
311
|
+
assert out_link == (
|
|
312
|
+
"https://us-east-1.console.aws.amazon.com/vpc/home"
|
|
313
|
+
"?region=us-east-1"
|
|
314
|
+
"#StatefulRuleGroupDetails:ruleGroupArn=arn:aws:network-firewall:us-east-1:123456789012:stateful-rulegroup/my-rules"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_aws_network_firewall_stateless_rulegroup():
|
|
319
|
+
arn = "arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/my-rules"
|
|
320
|
+
out_link = aws.get_console_link(arn=arn)
|
|
321
|
+
assert out_link == (
|
|
322
|
+
"https://us-east-1.console.aws.amazon.com/vpc/home"
|
|
323
|
+
"?region=us-east-1"
|
|
324
|
+
"#StatelessRuleGroupDetails:ruleGroupArn=arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/my-rules"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_aws_network_firewall_home():
|
|
329
|
+
arn = "arn:aws:network-firewall:us-east-1:123456789012"
|
|
330
|
+
out_link = aws.get_console_link(arn=arn)
|
|
331
|
+
assert out_link == "https://us-east-1.console.aws.amazon.com/vpc/home?region=us-east-1#Firewalls:"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_aws_render_unknown_template_expression_returns_empty_string():
|
|
335
|
+
result = _render("{unknown_expr}", {"region": "us-east-1", "console": "console.aws.amazon.com"})
|
|
336
|
+
assert result == ""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_aws_network_firewall_unknown_resource_falls_back_to_home():
|
|
340
|
+
arn = "arn:aws:network-firewall:us-east-1:123456789012:unknown-type/res-1"
|
|
341
|
+
out_link = aws.get_console_link(arn=arn)
|
|
342
|
+
assert out_link == "https://us-east-1.console.aws.amazon.com/vpc/home?region=us-east-1#Firewalls:"
|
|
343
|
+
|
|
@@ -363,3 +363,41 @@ def test_oci_linker_supports_all_resource_builders(method_name: str):
|
|
|
363
363
|
def test_oci_linker_rejects_unknown_resource_name():
|
|
364
364
|
with pytest.raises(ValueError, match="Invalid parameters provided"):
|
|
365
365
|
OCILinker().get_console_link(resource_name="missing_resource", region="us-ashburn-1")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
# Container-registry (OCI image) coverage
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
from cloudconsolelink.clouds.registry import _BUILDERS, classify, image_console_link
|
|
373
|
+
|
|
374
|
+
# One representative image ref per registry family, exercising every builder.
|
|
375
|
+
REGISTRY_SAMPLE_REFS = {
|
|
376
|
+
"ecr": "123456789012.dkr.ecr.us-east-1.amazonaws.com/repo",
|
|
377
|
+
"ecr-public": "public.ecr.aws/alias/repo",
|
|
378
|
+
"gar": "us-docker.pkg.dev/proj/repo/img",
|
|
379
|
+
"gcr": "gcr.io/proj/img",
|
|
380
|
+
"dockerhub": "docker.io/library/nginx",
|
|
381
|
+
"ocir": "iad.ocir.io/tenancy/repo",
|
|
382
|
+
"acr": "myreg.azurecr.io/app",
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def test_every_builder_has_a_sample_ref():
|
|
387
|
+
# A new registry family must be wired into classify() *and* covered here.
|
|
388
|
+
assert set(_BUILDERS) == set(REGISTRY_SAMPLE_REFS)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@pytest.mark.parametrize("family", sorted(_BUILDERS))
|
|
392
|
+
def test_registry_family_is_reachable(family: str):
|
|
393
|
+
ref = REGISTRY_SAMPLE_REFS[family]
|
|
394
|
+
# The sample ref classifies to the family it stands for...
|
|
395
|
+
assert classify(classify_host(ref)) == family
|
|
396
|
+
# ...and routing it yields an https console link.
|
|
397
|
+
assert image_console_link(ref).startswith("https://")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def classify_host(ref: str) -> str:
|
|
401
|
+
from cloudconsolelink.clouds.registry import parse_image_ref
|
|
402
|
+
|
|
403
|
+
return parse_image_ref(ref).host
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from urllib.parse import quote
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from cloudconsolelink.clouds.registry import image_console_link
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.parametrize(
|
|
9
|
+
"ref, expected",
|
|
10
|
+
[
|
|
11
|
+
# --- ECR private (delegates to AWSLinker via a reconstructed ARN) ---
|
|
12
|
+
(
|
|
13
|
+
"602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni-init",
|
|
14
|
+
"https://us-east-2.console.aws.amazon.com/ecr/repositories/private/"
|
|
15
|
+
"602401143452/amazon-k8s-cni-init?region=us-east-2",
|
|
16
|
+
),
|
|
17
|
+
# digest drill-in (built directly, ARN cannot carry a digest)
|
|
18
|
+
(
|
|
19
|
+
"602401143452.dkr.ecr.us-east-2.amazonaws.com/repo@sha256:abc123",
|
|
20
|
+
"https://us-east-2.console.aws.amazon.com/ecr/repositories/private/"
|
|
21
|
+
"602401143452/repo/_/image/sha256:abc123/details?region=us-east-2",
|
|
22
|
+
),
|
|
23
|
+
# tag-only lands on the repo view
|
|
24
|
+
(
|
|
25
|
+
"602401143452.dkr.ecr.us-east-2.amazonaws.com/repo:v1",
|
|
26
|
+
"https://us-east-2.console.aws.amazon.com/ecr/repositories/private/"
|
|
27
|
+
"602401143452/repo?region=us-east-2",
|
|
28
|
+
),
|
|
29
|
+
# China partition
|
|
30
|
+
(
|
|
31
|
+
"123456789012.dkr.ecr.cn-north-1.amazonaws.com.cn/repo",
|
|
32
|
+
"https://cn-north-1.console.amazonaws.cn/ecr/repositories/private/"
|
|
33
|
+
"123456789012/repo?region=cn-north-1",
|
|
34
|
+
),
|
|
35
|
+
# GovCloud partition (detected from the region prefix)
|
|
36
|
+
(
|
|
37
|
+
"123456789012.dkr.ecr.us-gov-west-1.amazonaws.com/repo",
|
|
38
|
+
"https://us-gov-west-1.console.amazonaws-us-gov.com/ecr/repositories/private/"
|
|
39
|
+
"123456789012/repo?region=us-gov-west-1",
|
|
40
|
+
),
|
|
41
|
+
# --- ECR Public ---
|
|
42
|
+
("public.ecr.aws/nginx/nginx", "https://gallery.ecr.aws/nginx/nginx"),
|
|
43
|
+
# --- GAR: image drill-in vs repo-only delegation ---
|
|
44
|
+
(
|
|
45
|
+
"us-docker.pkg.dev/proj/repo/img",
|
|
46
|
+
"https://console.cloud.google.com/artifacts/docker/proj/us/repo/img?project=proj",
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
"europe-west1-docker.pkg.dev/proj/repo",
|
|
50
|
+
"https://console.cloud.google.com/artifacts/docker/proj/europe-west1/repo?project=proj",
|
|
51
|
+
),
|
|
52
|
+
# --- GCR legacy ---
|
|
53
|
+
(
|
|
54
|
+
"gcr.io/proj/img",
|
|
55
|
+
"https://console.cloud.google.com/gcr/images/proj/GLOBAL/img?project=proj",
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
"us.gcr.io/proj/img",
|
|
59
|
+
"https://console.cloud.google.com/gcr/images/proj/US/img?project=proj",
|
|
60
|
+
),
|
|
61
|
+
(
|
|
62
|
+
"gcr.io/proj",
|
|
63
|
+
"https://console.cloud.google.com/gcr/images/proj?project=proj",
|
|
64
|
+
),
|
|
65
|
+
# --- Docker Hub ---
|
|
66
|
+
("nginx", "https://hub.docker.com/_/nginx"),
|
|
67
|
+
("library/nginx", "https://hub.docker.com/_/nginx"),
|
|
68
|
+
("nginx:1.25", "https://hub.docker.com/_/nginx/tags?name=1.25"),
|
|
69
|
+
("bitnami/nginx", "https://hub.docker.com/r/bitnami/nginx"),
|
|
70
|
+
("bitnami/nginx:1.25", "https://hub.docker.com/r/bitnami/nginx/tags?name=1.25"),
|
|
71
|
+
# --- OCIR (no OCID in ref -> registry landing page) ---
|
|
72
|
+
("iad.ocir.io/tenancy/repo", "https://cloud.oracle.com/registry/containers/repos"),
|
|
73
|
+
# --- unknown registry -> Docker Hub search fallback ---
|
|
74
|
+
("quay.io/org/app", "https://hub.docker.com/search?q=org/app"),
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
def test_image_console_link(ref, expected):
|
|
78
|
+
assert image_console_link(ref) == expected
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_acr_with_hints():
|
|
82
|
+
url = image_console_link(
|
|
83
|
+
"myreg.azurecr.io/app",
|
|
84
|
+
subscription_id="SUB",
|
|
85
|
+
resource_group="RG",
|
|
86
|
+
)
|
|
87
|
+
arm_id = "/subscriptions/SUB/resourceGroups/RG/providers/Microsoft.ContainerRegistry/registries/myreg"
|
|
88
|
+
expected = (
|
|
89
|
+
"https://portal.azure.com/#view/Microsoft_Azure_ContainerRegistries/"
|
|
90
|
+
f"RepositoryBlade/registryId/{quote(arm_id, safe='')}/repository/app"
|
|
91
|
+
)
|
|
92
|
+
assert url == expected
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_acr_without_hints_browse_fallback():
|
|
96
|
+
url = image_console_link("myreg.azurecr.io/app")
|
|
97
|
+
assert url == (
|
|
98
|
+
"https://portal.azure.com/#blade/HubsExtension/BrowseResource/"
|
|
99
|
+
"resourceType/Microsoft.ContainerRegistry%2Fregistries"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_acr_partial_hints_falls_back():
|
|
104
|
+
# only one of the two required hints -> still the browse fallback
|
|
105
|
+
url = image_console_link("myreg.azurecr.io/app", subscription_id="SUB")
|
|
106
|
+
assert "BrowseResource" in url
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_invalid_ref_raises():
|
|
110
|
+
with pytest.raises(ValueError):
|
|
111
|
+
image_console_link("")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_gar_single_segment_raises_clear_error():
|
|
115
|
+
with pytest.raises(ValueError, match="GAR image reference needs"):
|
|
116
|
+
image_console_link("us-docker.pkg.dev/proj")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from cloudconsolelink.clouds.registry import ImageRef, classify, parse_image_ref
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.parametrize(
|
|
7
|
+
"ref, expected",
|
|
8
|
+
[
|
|
9
|
+
# bare name -> Docker Hub, no host
|
|
10
|
+
("nginx", ImageRef("", "nginx", "", "")),
|
|
11
|
+
("nginx:1.25", ImageRef("", "nginx", "1.25", "")),
|
|
12
|
+
("library/nginx", ImageRef("", "library/nginx", "", "")),
|
|
13
|
+
# explicit docker.io host
|
|
14
|
+
("docker.io/library/nginx:latest", ImageRef("docker.io", "library/nginx", "latest", "")),
|
|
15
|
+
# ECR private, the motivating example
|
|
16
|
+
(
|
|
17
|
+
"602401143452.dkr.ecr.us-east-2.amazonaws.com/amazon-k8s-cni-init",
|
|
18
|
+
ImageRef("602401143452.dkr.ecr.us-east-2.amazonaws.com", "amazon-k8s-cni-init", "", ""),
|
|
19
|
+
),
|
|
20
|
+
# tag + digest together
|
|
21
|
+
(
|
|
22
|
+
"reg.example.com/team/app:v1@sha256:abcdef",
|
|
23
|
+
ImageRef("reg.example.com", "team/app", "v1", "sha256:abcdef"),
|
|
24
|
+
),
|
|
25
|
+
# digest only
|
|
26
|
+
(
|
|
27
|
+
"602401143452.dkr.ecr.us-east-2.amazonaws.com/repo@sha256:deadbeef",
|
|
28
|
+
ImageRef("602401143452.dkr.ecr.us-east-2.amazonaws.com", "repo", "", "sha256:deadbeef"),
|
|
29
|
+
),
|
|
30
|
+
# host with port
|
|
31
|
+
("localhost:5000/my/img:dev", ImageRef("localhost:5000", "my/img", "dev", "")),
|
|
32
|
+
("localhost/img", ImageRef("localhost", "img", "", "")),
|
|
33
|
+
# multi-segment repository (GAR: project/repo/image)
|
|
34
|
+
(
|
|
35
|
+
"us-docker.pkg.dev/proj/repo/img:tag",
|
|
36
|
+
ImageRef("us-docker.pkg.dev", "proj/repo/img", "tag", ""),
|
|
37
|
+
),
|
|
38
|
+
# surrounding whitespace stripped
|
|
39
|
+
(" nginx:1 ", ImageRef("", "nginx", "1", "")),
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
def test_parse_image_ref(ref, expected):
|
|
43
|
+
assert parse_image_ref(ref) == expected
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize("bad", ["", " ", None])
|
|
47
|
+
def test_parse_image_ref_empty_raises(bad):
|
|
48
|
+
with pytest.raises(ValueError):
|
|
49
|
+
parse_image_ref(bad)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_parse_image_ref_host_only_no_repo_raises():
|
|
53
|
+
# host present but nothing after the slash
|
|
54
|
+
with pytest.raises(ValueError):
|
|
55
|
+
parse_image_ref("reg.example.com/")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize("bad", ["repo@", "reg.io/repo@", "reg.io/a/b:v1@"])
|
|
59
|
+
def test_parse_image_ref_empty_digest_raises(bad):
|
|
60
|
+
with pytest.raises(ValueError, match="empty digest"):
|
|
61
|
+
parse_image_ref(bad)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize("bad", ["repo:", "reg.io/repo:", "reg.io/a/b:"])
|
|
65
|
+
def test_parse_image_ref_empty_tag_raises(bad):
|
|
66
|
+
with pytest.raises(ValueError, match="empty tag"):
|
|
67
|
+
parse_image_ref(bad)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.parametrize(
|
|
71
|
+
"host, family",
|
|
72
|
+
[
|
|
73
|
+
("", "dockerhub"),
|
|
74
|
+
("docker.io", "dockerhub"),
|
|
75
|
+
("registry-1.docker.io", "dockerhub"),
|
|
76
|
+
("index.docker.io", "dockerhub"),
|
|
77
|
+
("public.ecr.aws", "ecr-public"),
|
|
78
|
+
("602401143452.dkr.ecr.us-east-2.amazonaws.com", "ecr"),
|
|
79
|
+
("123456789012.dkr.ecr.cn-north-1.amazonaws.com.cn", "ecr"),
|
|
80
|
+
("us-docker.pkg.dev", "gar"),
|
|
81
|
+
("europe-west1-docker.pkg.dev", "gar"),
|
|
82
|
+
("gcr.io", "gcr"),
|
|
83
|
+
("us.gcr.io", "gcr"),
|
|
84
|
+
("asia.gcr.io", "gcr"),
|
|
85
|
+
("iad.ocir.io", "ocir"),
|
|
86
|
+
("myregistry.azurecr.io", "acr"),
|
|
87
|
+
("unknown.example.com", ""),
|
|
88
|
+
("quay.io", ""),
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
def test_classify(host, family):
|
|
92
|
+
assert classify(host) == family
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudconsolelink-4.0.0 → cloudconsolelink-4.1.1}/cloudconsolelink.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|