cloudconsolelink 3.0.2__tar.gz → 4.0.0__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.
Files changed (29) hide show
  1. {cloudconsolelink-3.0.2/cloudconsolelink.egg-info → cloudconsolelink-4.0.0}/PKG-INFO +1 -1
  2. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/__init__.py +1 -1
  3. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/aws/__init__.py +49 -35
  4. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/aws/links.py +11 -0
  5. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/azure/__init__.py +7 -8
  6. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/gcp/__init__.py +39 -2
  7. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/gcp/links.py +376 -91
  8. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/oci/__init__.py +9 -3
  9. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/oci/links.py +261 -73
  10. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0/cloudconsolelink.egg-info}/PKG-INFO +1 -1
  11. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/tests/test_aws.py +23 -0
  12. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/LICENSE +0 -0
  13. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/MANIFEST.in +0 -0
  14. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/README.md +0 -0
  15. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/__init__.py +0 -0
  16. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink/clouds/aws/errors.py +0 -0
  17. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink.egg-info/SOURCES.txt +0 -0
  18. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink.egg-info/dependency_links.txt +0 -0
  19. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink.egg-info/requires.txt +0 -0
  20. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/cloudconsolelink.egg-info/top_level.txt +0 -0
  21. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/requirements-dev.txt +0 -0
  22. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/requirements.txt +0 -0
  23. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/setup.cfg +0 -0
  24. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/setup.py +0 -0
  25. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/tests/test_azure.py +0 -0
  26. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/tests/test_gcp.py +0 -0
  27. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/tests/test_oci.py +0 -0
  28. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/tests/test_packaging.py +0 -0
  29. {cloudconsolelink-3.0.2 → cloudconsolelink-4.0.0}/tests/test_provider_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudconsolelink
3
- Version: 3.0.2
3
+ Version: 4.0.0
4
4
  Summary: Generate console links for cloud resources
5
5
  Home-page: https://github.com/Cloudanix/cloud-console-links
6
6
  Maintainer: Cloudanix
@@ -1,2 +1,2 @@
1
1
  # version of the module
2
- __version__ = "3.0.2"
2
+ __version__ = "4.0.0"
@@ -1,18 +1,48 @@
1
1
  import logging
2
+ import re
2
3
  import urllib.parse
3
4
  from typing import Dict
4
5
 
5
6
  from .errors import ARNTooShortError, InvalidARNError, InvalidPartitionError, InvalidServiceError
6
- from .links import get_links
7
+ from .links import HOME_URLS, get_links
7
8
 
8
9
  logger = logging.getLogger(__name__)
9
10
 
10
- SERVICE_HOME_OVERRIDES = {
11
- "elasticloadbalancing": "https://{region}.{console}/ec2/home?region={region}#LoadBalancers:",
12
- "logs": "https://{region}.{console}/cloudwatch/home?region={region}#logsV2:log-groups",
13
- "sns": "https://{region}.{console}/sns/v3/home?region={region}",
14
- "sqs": "https://{region}.{console}/sqs/v2/home?region={region}",
15
- }
11
+ _EXPR_RE = re.compile(r"\{([^}]+)\}")
12
+
13
+
14
+ def _render(template: str, data: Dict, arn: str = "") -> str:
15
+ """Render a link template without eval.
16
+
17
+ Templates in links.py use patterns like {data.get("region", "")} and {arn}.
18
+ We substitute each {expr} by looking it up in a pre-built context dict after
19
+ stripping all internal whitespace (backslash-continuation lines in the source
20
+ produce spaces inside expressions). Unknown expressions render as empty string.
21
+ """
22
+ resource = str(data.get("resource", ""))
23
+ ctx: Dict[str, str] = {
24
+ 'data.get("region","")': str(data.get("region", "")),
25
+ 'data.get("console","")': str(data.get("console", "")),
26
+ 'data.get("resource","")': resource,
27
+ 'data.get("account","")': str(data.get("account", "")),
28
+ 'data.get("service","")': str(data.get("service", "")),
29
+ 'get_arn_string(data)': get_arn_string(data),
30
+ 'get_resource_path(data.get("resource",""))': get_resource_path(resource),
31
+ "arn": arn,
32
+ "region": str(data.get("region", "")),
33
+ "console": str(data.get("console", "")),
34
+ "resource": resource,
35
+ "account": str(data.get("account", "")),
36
+ "service": str(data.get("service", "")),
37
+ }
38
+
39
+ def _sub(m: re.Match) -> str:
40
+ expr = re.sub(r"\s+", "", m.group(1)) # collapse whitespace from line continuations
41
+ if expr not in ctx:
42
+ logger.debug("Unknown template expression %r", expr)
43
+ return ctx.get(expr, "")
44
+
45
+ return _EXPR_RE.sub(_sub, template).replace(" ", "")
16
46
 
17
47
 
18
48
  def get_console(partition: str) -> str:
@@ -62,11 +92,9 @@ def get_service_home_link(data: Dict) -> str:
62
92
  if not service or not console:
63
93
  return ""
64
94
 
65
- if region and service in SERVICE_HOME_OVERRIDES:
66
- return SERVICE_HOME_OVERRIDES[service].format(
67
- console=console,
68
- region=urllib.parse.quote(region),
69
- )
95
+ home_template = HOME_URLS.get(service)
96
+ if region and home_template:
97
+ return _render(home_template, data)
70
98
 
71
99
  host = f"{region}.{console}" if region else console
72
100
  path = f"/{service}/home" if region else f"/{service}"
@@ -83,7 +111,7 @@ class AWSLinker:
83
111
  tokens = arn.split(":")
84
112
 
85
113
  if len(tokens) < 5:
86
- logger.error(f"AWS ARN {arn} is too short")
114
+ logger.debug(f"AWS ARN {arn} is too short")
87
115
  raise ARNTooShortError(f"AWS ARN {arn} is too short")
88
116
 
89
117
  data: dict[str, str | bool] = {
@@ -101,7 +129,7 @@ class AWSLinker:
101
129
 
102
130
  console = get_console(str(data.get("partition", "")))
103
131
  if not console:
104
- logger.error(
132
+ logger.debug(
105
133
  f"AWS ARN {arn} has invalid partition (Valid Partitions: aws, aws-us-gov, aws-cn)",
106
134
  )
107
135
  raise InvalidPartitionError(
@@ -112,17 +140,17 @@ class AWSLinker:
112
140
  links = get_links()
113
141
 
114
142
  if data["prefix"] != "arn":
115
- logger.error(f"Invalid AWS ARN {arn}")
143
+ logger.debug(f"Invalid AWS ARN {arn}")
116
144
  raise InvalidARNError(f"Invalid AWS ARN {arn}")
117
145
 
118
146
  if data["service"] not in links:
119
- logger.error(f"AWS service {data['service']} unknown")
147
+ logger.debug(f"AWS service {data['service']} unknown")
120
148
  raise InvalidServiceError(f"AWS service {data['service']} unknown")
121
149
 
122
150
  if not resource_tokens or resource_tokens == [""]:
123
151
  return get_service_home_link(data)
124
152
 
125
- if len(resource_tokens) > 1:
153
+ if len(resource_tokens) > 1 and "/" not in resource_tokens[0]:
126
154
  data["resourceType"] = resource_tokens[0]
127
155
  data["resource"] = ":".join(resource_tokens[1:])
128
156
  data["hasPath"] = False
@@ -138,40 +166,26 @@ class AWSLinker:
138
166
  data["hasPath"] = False
139
167
 
140
168
  else: # pragma: no cover – defensive; unreachable via str.split
141
- logger.error(f"Invalid AWS ARN {arn}")
169
+ logger.debug(f"Invalid AWS ARN {arn}")
142
170
  raise InvalidARNError(f"Invalid AWS ARN {arn}")
143
171
 
144
172
  if not links.get(data["service"], {}).get(data["resourceType"], None):
145
- logger.error(
173
+ logger.debug(
146
174
  f"AWS service {data['service']} resource type {data['resourceType']} not supported",
147
175
  )
148
176
  return get_service_home_link(data)
149
177
 
150
- template_context = {
151
- "arn": arn,
152
- "data": data,
153
- "get_arn_string": get_arn_string,
154
- "get_qualifiers": get_qualifiers,
155
- "get_resource_path": get_resource_path,
156
- }
157
178
  data["resource"] = urllib.parse.quote(str(data["resource"]))
158
- return eval(
159
- f"f'{links[data['service']][data['resourceType']]}'",
160
- {"__builtins__": {}},
161
- template_context,
162
- ).replace(
163
- " ",
164
- "",
165
- )
179
+ return _render(links[data["service"]][data["resourceType"]], data, arn)
166
180
 
167
181
 
168
182
  __all__ = [
169
183
  "ARNTooShortError",
170
184
  "AWSLinker",
185
+ "HOME_URLS",
171
186
  "InvalidARNError",
172
187
  "InvalidPartitionError",
173
188
  "InvalidServiceError",
174
- "SERVICE_HOME_OVERRIDES",
175
189
  "get_arn_string",
176
190
  "get_console",
177
191
  "get_qualifiers",
@@ -1,5 +1,16 @@
1
1
  from typing import Dict
2
2
 
3
+ # Explicit home page URL for each service where the computed default is wrong.
4
+ # Template syntax matches links below: eval'd as an f-string with {"data": data}.
5
+ # Add an entry here whenever a service's console home does not follow the pattern
6
+ # https://{region}.console.aws.amazon.com/{service}/home?region={region}.
7
+ HOME_URLS: Dict[str, str] = {
8
+ "elasticloadbalancing": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region={data.get("region", "")}#LoadBalancers:',
9
+ "logs": 'https://{data.get("region", "")}.{data.get("console", "")}/cloudwatch/home?region={data.get("region", "")}#logsV2:log-groups',
10
+ "sns": 'https://{data.get("region", "")}.{data.get("console", "")}/sns/v3/home?region={data.get("region", "")}',
11
+ "sqs": 'https://{data.get("region", "")}.{data.get("console", "")}/sqs/v2/home?region={data.get("region", "")}',
12
+ }
13
+
3
14
 
4
15
  def get_links() -> Dict:
5
16
  links = {
@@ -13,7 +13,7 @@ class AzureLinker:
13
13
  iam_entity_type: str = "",
14
14
  ) -> str:
15
15
  if not primary_ad_domain_name and not iam_entity_type:
16
- logger.error(
16
+ logger.debug(
17
17
  "For IAM entity required `iam_entity_type` and for other entity required `primary_ad_domain_name`",
18
18
  )
19
19
  raise ValueError("Invalid parameters provided")
@@ -29,12 +29,11 @@ class AzureLinker:
29
29
  }
30
30
 
31
31
  if iam_entity_type and id and iam_entities.get(iam_entity_type):
32
- if iam_entity_type == "role" and primary_ad_domain_name:
33
- return eval(f"f'{iam_entities[iam_entity_type]}'").replace(" ", "")
34
- elif iam_entity_type == "service_principal" and app_id:
35
- return eval(f"f'{iam_entities[iam_entity_type]}'").replace(" ", "")
36
- else:
37
- return eval(f"f'{iam_entities[iam_entity_type]}'").replace(" ", "")
32
+ return (
33
+ iam_entities[iam_entity_type]
34
+ .format(id=id, primary_ad_domain_name=primary_ad_domain_name, app_id=app_id)
35
+ .replace(" ", "")
36
+ )
38
37
 
39
38
  elif primary_ad_domain_name and id:
40
39
  if id.startswith("https"):
@@ -48,5 +47,5 @@ class AzureLinker:
48
47
  return f"https://portal.azure.com/#@{primary_ad_domain_name}/resource{id}/overview"
49
48
 
50
49
  else:
51
- logger.error("entity id required")
50
+ logger.debug("entity id required")
52
51
  raise ValueError("Invalid parameters provided")
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from typing import Dict
3
3
 
4
- from .links import Resource
4
+ from .links import Resource, SERVICE_HOME_MAP
5
5
 
6
6
  logger = logging.getLogger(__name__)
7
7
 
@@ -212,6 +212,38 @@ class GCPLinker:
212
212
  "vpc_home": resource.vpc_home,
213
213
  "apigateway_home": resource.apigateway_home,
214
214
  "cloudrun_home": resource.cloudrun_home,
215
+ "compute_home": resource.compute_home,
216
+ "storage_home": resource.storage_home,
217
+ "bigtable_home": resource.bigtable_home,
218
+ "cloudfunction_home": resource.cloudfunction_home,
219
+ "kms_home": resource.kms_home,
220
+ "gke_home": resource.gke_home,
221
+ "sql_home": resource.sql_home,
222
+ "iam_home": resource.iam_home,
223
+ "firestore_home": resource.firestore_home,
224
+ "pubsub_home": resource.pubsub_home,
225
+ "logging_home": resource.logging_home,
226
+ "dataproc_home": resource.dataproc_home,
227
+ "monitoring_home": resource.monitoring_home,
228
+ "cloud_armor_home": resource.cloud_armor_home,
229
+ "cloud_build_home": resource.cloud_build_home,
230
+ "cloud_composer_home": resource.cloud_composer_home,
231
+ "cloud_scheduler_home": resource.cloud_scheduler_home,
232
+ "cloud_tasks_home": resource.cloud_tasks_home,
233
+ "spanner_home": resource.spanner_home,
234
+ "artifact_registry_home": resource.artifact_registry_home,
235
+ "secret_manager_home": resource.secret_manager_home,
236
+ "memorystore_home": resource.memorystore_home,
237
+ "vpn_home": resource.vpn_home,
238
+ "vertex_ai_home": resource.vertex_ai_home,
239
+ "workflows_home": resource.workflows_home,
240
+ "eventarc_home": resource.eventarc_home,
241
+ "alloydb_home": resource.alloydb_home,
242
+ "filestore_home": resource.filestore_home,
243
+ "cloud_deploy_home": resource.cloud_deploy_home,
244
+ "cloud_ids_home": resource.cloud_ids_home,
245
+ "dialogflow_home": resource.dialogflow_home,
246
+ "cloud_endpoints_home": resource.cloud_endpoints_home,
215
247
  "cloud_armor_policy": resource.cloud_armor_policy,
216
248
  "cloud_build_trigger": resource.cloud_build_trigger,
217
249
  "cloud_composer_environment": resource.cloud_composer_environment,
@@ -299,5 +331,10 @@ class GCPLinker:
299
331
  if resource_name in resources_list:
300
332
  return resources[resource_name](**param).replace(" ", "") # type: ignore
301
333
 
302
- logger.error(f"Invalid resource_name - {resource_name}")
334
+ home_name = SERVICE_HOME_MAP.get(resource_name)
335
+ if home_name and home_name in resources:
336
+ logger.debug(f"resource_name {resource_name!r} not found, falling back to {home_name!r}")
337
+ return resources[home_name](**param).replace(" ", "") # type: ignore
338
+
339
+ logger.debug(f"Invalid resource_name - {resource_name}")
303
340
  raise ValueError("Invalid parameters provided")