cortexapps-cli 1.3.0__tar.gz → 1.5.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 (59) hide show
  1. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/PKG-INFO +1 -1
  2. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/cli.py +9 -2
  3. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/backup.py +295 -38
  4. cortexapps_cli-1.5.0/cortexapps_cli/commands/entity_relationship_types.py +121 -0
  5. cortexapps_cli-1.5.0/cortexapps_cli/commands/entity_relationships.py +222 -0
  6. cortexapps_cli-1.5.0/cortexapps_cli/commands/secrets.py +105 -0
  7. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/cortex_client.py +90 -2
  8. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/pyproject.toml +1 -1
  9. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/LICENSE +0 -0
  10. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/README.rst +0 -0
  11. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/command_options.py +0 -0
  12. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/api_keys.py +0 -0
  13. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/audit_logs.py +0 -0
  14. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/backup_commands/cortex_export.py +0 -0
  15. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/catalog.py +0 -0
  16. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/custom_data.py +0 -0
  17. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/custom_events.py +0 -0
  18. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/custom_metrics.py +0 -0
  19. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/dependencies.py +0 -0
  20. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/deploys.py +0 -0
  21. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/discovery_audit.py +0 -0
  22. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/docs.py +0 -0
  23. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/entity_types.py +0 -0
  24. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/gitops_logs.py +0 -0
  25. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/groups.py +0 -0
  26. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/initiatives.py +0 -0
  27. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations.py +0 -0
  28. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/aws.py +0 -0
  29. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/azure_devops.py +0 -0
  30. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/azure_resources.py +0 -0
  31. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/circleci.py +0 -0
  32. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/coralogix.py +0 -0
  33. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/datadog.py +0 -0
  34. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/github.py +0 -0
  35. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/gitlab.py +0 -0
  36. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/incidentio.py +0 -0
  37. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/launchdarkly.py +0 -0
  38. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/newrelic.py +0 -0
  39. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/pagerduty.py +0 -0
  40. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/prometheus.py +0 -0
  41. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/sonarqube.py +0 -0
  42. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/ip_allowlist.py +0 -0
  43. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/on_call.py +0 -0
  44. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages.py +0 -0
  45. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/go.py +0 -0
  46. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/java.py +0 -0
  47. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/node.py +0 -0
  48. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/nuget.py +0 -0
  49. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/python.py +0 -0
  50. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/plugins.py +0 -0
  51. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/queries.py +0 -0
  52. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/rest.py +0 -0
  53. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/scim.py +0 -0
  54. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/scorecards.py +0 -0
  55. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/scorecards_commands/exemptions.py +0 -0
  56. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/teams.py +0 -0
  57. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/workflows.py +0 -0
  58. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/models/team.py +0 -0
  59. {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortexapps-cli
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Command Line Interface for cortexapps
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -24,6 +24,8 @@ import cortexapps_cli.commands.deploys as deploys
24
24
  import cortexapps_cli.commands.discovery_audit as discovery_audit
25
25
  import cortexapps_cli.commands.docs as docs
26
26
  import cortexapps_cli.commands.entity_types as entity_types
27
+ import cortexapps_cli.commands.entity_relationship_types as entity_relationship_types
28
+ import cortexapps_cli.commands.entity_relationships as entity_relationships
27
29
  import cortexapps_cli.commands.gitops_logs as gitops_logs
28
30
  import cortexapps_cli.commands.groups as groups
29
31
  import cortexapps_cli.commands.initiatives as initiatives
@@ -36,6 +38,7 @@ import cortexapps_cli.commands.queries as queries
36
38
  import cortexapps_cli.commands.rest as rest
37
39
  import cortexapps_cli.commands.scim as scim
38
40
  import cortexapps_cli.commands.scorecards as scorecards
41
+ import cortexapps_cli.commands.secrets as secrets
39
42
  import cortexapps_cli.commands.teams as teams
40
43
  import cortexapps_cli.commands.workflows as workflows
41
44
 
@@ -58,6 +61,8 @@ app.add_typer(deploys.app, name="deploys")
58
61
  app.add_typer(discovery_audit.app, name="discovery-audit")
59
62
  app.add_typer(docs.app, name="docs")
60
63
  app.add_typer(entity_types.app, name="entity-types")
64
+ app.add_typer(entity_relationship_types.app, name="entity-relationship-types")
65
+ app.add_typer(entity_relationships.app, name="entity-relationships")
61
66
  app.add_typer(gitops_logs.app, name="gitops-logs")
62
67
  app.add_typer(groups.app, name="groups")
63
68
  app.add_typer(initiatives.app, name="initiatives")
@@ -70,6 +75,7 @@ app.add_typer(queries.app, name="queries")
70
75
  app.add_typer(rest.app, name="rest")
71
76
  app.add_typer(scim.app, name="scim")
72
77
  app.add_typer(scorecards.app, name="scorecards")
78
+ app.add_typer(secrets.app, name="secrets")
73
79
  app.add_typer(teams.app, name="teams")
74
80
  app.add_typer(workflows.app, name="workflows")
75
81
 
@@ -81,7 +87,8 @@ def global_callback(
81
87
  url: str = typer.Option(None, "--url", "-u", help="Base URL for the API", envvar="CORTEX_BASE_URL"),
82
88
  config_file: str = typer.Option(os.path.join(os.path.expanduser('~'), '.cortex', 'config'), "--config", "-c", help="Config file path", envvar="CORTEX_CONFIG"),
83
89
  tenant: str = typer.Option("default", "--tenant", "-t", help="Tenant alias", envvar="CORTEX_TENANT_ALIAS"),
84
- log_level: Annotated[str, typer.Option("--log-level", "-l", help="Set the logging level")] = "INFO"
90
+ log_level: Annotated[str, typer.Option("--log-level", "-l", help="Set the logging level")] = "INFO",
91
+ rate_limit: int = typer.Option(None, "--rate-limit", "-r", help="API rate limit in requests per minute (default: 1000)", envvar="CORTEX_RATE_LIMIT")
85
92
  ):
86
93
  if not ctx.obj:
87
94
  ctx.obj = {}
@@ -129,7 +136,7 @@ def global_callback(
129
136
  api_key = api_key.strip('"\' ')
130
137
  url = url.strip('"\' /')
131
138
 
132
- ctx.obj["client"] = CortexClient(api_key, tenant, numeric_level, url)
139
+ ctx.obj["client"] = CortexClient(api_key, tenant, numeric_level, url, rate_limit)
133
140
 
134
141
  @app.command()
135
142
  def version():
@@ -5,6 +5,10 @@ from typing_extensions import Annotated
5
5
  import typer
6
6
  import json
7
7
  import os
8
+ import tempfile
9
+ import sys
10
+ from io import StringIO
11
+ from contextlib import redirect_stdout, redirect_stderr
8
12
  from rich import print, print_json
9
13
  from rich.console import Console
10
14
  from enum import Enum
@@ -14,6 +18,8 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
14
18
  import cortexapps_cli.commands.scorecards as scorecards
15
19
  import cortexapps_cli.commands.catalog as catalog
16
20
  import cortexapps_cli.commands.entity_types as entity_types
21
+ import cortexapps_cli.commands.entity_relationship_types as entity_relationship_types
22
+ import cortexapps_cli.commands.entity_relationships as entity_relationships
17
23
  import cortexapps_cli.commands.ip_allowlist as ip_allowlist
18
24
  import cortexapps_cli.commands.plugins as plugins
19
25
  import cortexapps_cli.commands.workflows as workflows
@@ -93,15 +99,39 @@ def _export_entity_types(ctx, directory):
93
99
 
94
100
  for definition in definitions_sorted:
95
101
  tag = definition['type']
96
- json_string = json.dumps(definition, indent=4)
97
- _file_name(directory, tag, json_string, "json")
102
+ _file_name(directory, tag, definition, "json")
98
103
 
99
104
  def _export_ip_allowlist(ctx, directory):
100
105
  directory = _directory_name(directory, "ip-allowlist")
101
106
  file = directory + "/ip-allowlist.json"
102
107
 
103
108
  content = ip_allowlist.get(ctx, page=None, page_size=None, _print=False)
104
- _file_name(directory, "ip-allowlist", str(content), "json")
109
+ _file_name(directory, "ip-allowlist", content, "json")
110
+
111
+ def _export_entity_relationship_types(ctx, directory):
112
+ directory = _directory_name(directory, "entity-relationship-types")
113
+
114
+ data = entity_relationship_types.list(ctx, page=None, page_size=250, _print=False)
115
+ relationship_types_sorted = sorted(data['relationshipTypes'], key=lambda x: x["tag"])
116
+
117
+ for rel_type in relationship_types_sorted:
118
+ tag = rel_type['tag']
119
+ _file_name(directory, tag, rel_type, "json")
120
+
121
+ def _export_entity_relationships(ctx, directory):
122
+ directory = _directory_name(directory, "entity-relationships")
123
+
124
+ # First get all relationship types
125
+ rel_types_data = entity_relationship_types.list(ctx, page=None, page_size=250, _print=False)
126
+ rel_types = [rt['tag'] for rt in rel_types_data['relationshipTypes']]
127
+
128
+ # For each relationship type, export all relationships
129
+ for rel_type in sorted(rel_types):
130
+ data = entity_relationships.list(ctx, relationship_type=rel_type, page=None, page_size=250, _print=False)
131
+ relationships = data.get('relationships', [])
132
+
133
+ if relationships:
134
+ _file_name(directory, rel_type, relationships, "json")
105
135
 
106
136
  def _export_plugins(ctx, directory):
107
137
  directory = _directory_name(directory, "plugins")
@@ -179,6 +209,8 @@ def _export_workflows(ctx, directory):
179
209
  backupTypes = {
180
210
  "catalog",
181
211
  "entity-types",
212
+ "entity-relationship-types",
213
+ "entity-relationships",
182
214
  "ip-allowlist",
183
215
  "plugins",
184
216
  "scorecards",
@@ -226,6 +258,8 @@ def export(
226
258
  Exports the following objects:
227
259
  - catalog
228
260
  - entity-types
261
+ - entity-relationship-types
262
+ - entity-relationships
229
263
  - ip-allowlist
230
264
  - plugins
231
265
  - scorecards
@@ -240,14 +274,13 @@ def export(
240
274
  cortex backup export --export-types catalog --catalog-types AWS::S3::Bucket
241
275
 
242
276
  It does not back up everything in the tenant. For example these objects are not backed up:
243
- - api-keys
277
+ - api-keys
244
278
  - custom-events
245
279
  - custom-metadata created by the public API
246
280
  - custom-metrics
247
281
  - dependencies created by the API
248
282
  - deploys
249
283
  - docs created by the API
250
- - entity-relationships created by the API
251
284
  - groups added by the API
252
285
  - packages
253
286
  - secrets
@@ -265,6 +298,10 @@ def export(
265
298
  _export_catalog(ctx, directory, catalog_types)
266
299
  if "entity-types" in export_types:
267
300
  _export_entity_types(ctx, directory)
301
+ if "entity-relationship-types" in export_types:
302
+ _export_entity_relationship_types(ctx, directory)
303
+ if "entity-relationships" in export_types:
304
+ _export_entity_relationships(ctx, directory)
268
305
  if "ip-allowlist" in export_types:
269
306
  _export_ip_allowlist(ctx, directory)
270
307
  if "plugins" in export_types:
@@ -278,22 +315,161 @@ def export(
278
315
  print("Contents available in " + directory)
279
316
 
280
317
  def _import_ip_allowlist(ctx, directory):
318
+ imported = 0
319
+ failed = []
281
320
  if os.path.isdir(directory):
282
321
  print("Processing: " + directory)
283
322
  for filename in os.listdir(directory):
284
323
  file_path = os.path.join(directory, filename)
285
324
  if os.path.isfile(file_path):
286
- print(" Importing: " + filename)
287
- ip_allowlist.replace(ctx, file_input=open(file_path), addresses=None, force=False, _print=False)
325
+ try:
326
+ print(" Importing: " + filename)
327
+ ip_allowlist.replace(ctx, file_input=open(file_path), addresses=None, force=False, _print=False)
328
+ imported += 1
329
+ except Exception as e:
330
+ print(f" Failed to import {filename}: {type(e).__name__} - {str(e)}")
331
+ failed.append((file_path, type(e).__name__, str(e)))
332
+ return ("ip-allowlist", imported, failed)
288
333
 
289
334
  def _import_entity_types(ctx, force, directory):
335
+ imported = 0
336
+ failed = []
290
337
  if os.path.isdir(directory):
291
338
  print("Processing: " + directory)
292
339
  for filename in sorted(os.listdir(directory)):
293
340
  file_path = os.path.join(directory, filename)
294
341
  if os.path.isfile(file_path):
295
- print(" Importing: " + filename)
296
- entity_types.create(ctx, file_input=open(file_path), force=force)
342
+ try:
343
+ print(" Importing: " + filename)
344
+ entity_types.create(ctx, file_input=open(file_path), force=force)
345
+ imported += 1
346
+ except Exception as e:
347
+ print(f" Failed to import {filename}: {type(e).__name__} - {str(e)}")
348
+ failed.append((file_path, type(e).__name__, str(e)))
349
+ return ("entity-types", imported, failed)
350
+
351
+ def _import_entity_relationship_types(ctx, directory):
352
+ results = []
353
+ failed_count = 0
354
+
355
+ if os.path.isdir(directory):
356
+ print("Processing: " + directory)
357
+
358
+ # Get list of existing relationship types
359
+ existing_rel_types_data = entity_relationship_types.list(ctx, page=None, page_size=250, _print=False)
360
+ existing_tags = {rt['tag'] for rt in existing_rel_types_data.get('relationshipTypes', [])}
361
+
362
+ files = [(filename, os.path.join(directory, filename))
363
+ for filename in sorted(os.listdir(directory))
364
+ if os.path.isfile(os.path.join(directory, filename))]
365
+
366
+ def import_rel_type_file(file_info):
367
+ filename, file_path = file_info
368
+ tag = filename.replace('.json', '')
369
+ try:
370
+ # Check if relationship type already exists
371
+ if tag in existing_tags:
372
+ # Update existing relationship type
373
+ entity_relationship_types.update(ctx, tag=tag, file_input=open(file_path), _print=False)
374
+ else:
375
+ # Create new relationship type
376
+ entity_relationship_types.create(ctx, file_input=open(file_path), _print=False)
377
+ return (tag, file_path, None, None)
378
+ except typer.Exit as e:
379
+ return (tag, file_path, "HTTP", "Validation or HTTP error")
380
+ except Exception as e:
381
+ return (tag, file_path, type(e).__name__, str(e))
382
+
383
+ # Import all files in parallel
384
+ with ThreadPoolExecutor(max_workers=30) as executor:
385
+ futures = {executor.submit(import_rel_type_file, file_info): file_info[0] for file_info in files}
386
+ results = []
387
+ for future in as_completed(futures):
388
+ results.append(future.result())
389
+
390
+ # Print results in alphabetical order
391
+ failed_count = 0
392
+ for tag, file_path, error_type, error_msg in sorted(results, key=lambda x: x[0]):
393
+ if error_type:
394
+ print(f" Failed to import {tag}: {error_type} - {error_msg}")
395
+ failed_count += 1
396
+ else:
397
+ print(f" Importing: {tag}")
398
+
399
+ if failed_count > 0:
400
+ print(f"\n Total entity relationship type import failures: {failed_count}")
401
+
402
+ return ("entity-relationship-types", len(results) - failed_count, [(fp, et, em) for tag, fp, et, em in results if et])
403
+
404
+ def _import_entity_relationships(ctx, directory):
405
+ results = []
406
+ failed_count = 0
407
+
408
+ if os.path.isdir(directory):
409
+ print("Processing: " + directory)
410
+
411
+ files = [(filename, os.path.join(directory, filename))
412
+ for filename in sorted(os.listdir(directory))
413
+ if os.path.isfile(os.path.join(directory, filename))]
414
+
415
+ def import_relationships_file(file_info):
416
+ filename, file_path = file_info
417
+ rel_type = filename.replace('.json', '')
418
+ try:
419
+ # Read the relationships file
420
+ with open(file_path) as f:
421
+ relationships = json.load(f)
422
+
423
+ # Convert list format to the format expected by update-bulk
424
+ # The export saves the raw relationships list, but update-bulk needs {"relationships": [...]}
425
+ if isinstance(relationships, list):
426
+ data = {"relationships": []}
427
+ for rel in relationships:
428
+ # Extract source and destination tags from sourceEntity and destinationEntity
429
+ source_tag = rel.get("sourceEntity", {}).get("tag")
430
+ dest_tag = rel.get("destinationEntity", {}).get("tag")
431
+ data["relationships"].append({
432
+ "source": source_tag,
433
+ "destination": dest_tag
434
+ })
435
+
436
+ # Use update-bulk to replace all relationships for this type
437
+ # Create a temporary file to pass the data
438
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as temp_file:
439
+ json.dump(data, temp_file)
440
+ temp_file_name = temp_file.name
441
+
442
+ try:
443
+ entity_relationships.update_bulk(ctx, relationship_type=rel_type, file_input=open(temp_file_name), force=True, _print=False)
444
+ finally:
445
+ os.unlink(temp_file_name)
446
+
447
+ return (rel_type, file_path, None, None)
448
+ except typer.Exit as e:
449
+ return (rel_type, file_path, "HTTP", "Validation or HTTP error")
450
+ except Exception as e:
451
+ return (rel_type, file_path, type(e).__name__, str(e))
452
+
453
+ # Import all files in parallel
454
+ with ThreadPoolExecutor(max_workers=30) as executor:
455
+ futures = {executor.submit(import_relationships_file, file_info): file_info[0] for file_info in files}
456
+ results = []
457
+ for future in as_completed(futures):
458
+ results.append(future.result())
459
+
460
+ # Print results in alphabetical order
461
+ failed_count = 0
462
+ for rel_type, file_path, error_type, error_msg in sorted(results, key=lambda x: x[0]):
463
+ if error_type:
464
+ print(f" Failed to import {rel_type}: {error_type} - {error_msg}")
465
+ failed_count += 1
466
+ else:
467
+ print(f" Importing: {rel_type}")
468
+
469
+ if failed_count > 0:
470
+ print(f"\n Total entity relationship import failures: {failed_count}")
471
+
472
+ return ("entity-relationships", len(results) - failed_count, [(fp, et, em) for rt, fp, et, em in results if et])
297
473
 
298
474
  def _import_catalog(ctx, directory):
299
475
  if os.path.isdir(directory):
@@ -304,12 +480,16 @@ def _import_catalog(ctx, directory):
304
480
 
305
481
  def import_catalog_file(file_info):
306
482
  filename, file_path = file_info
483
+ print(f" Importing: {filename}")
307
484
  try:
308
485
  with open(file_path) as f:
309
486
  catalog.create(ctx, file_input=f, _print=False)
310
- return (filename, None)
487
+ return (filename, file_path, None, None)
488
+ except typer.Exit as e:
489
+ # typer.Exit is raised by the HTTP client on errors
490
+ return (filename, file_path, "HTTP", "Validation or HTTP error")
311
491
  except Exception as e:
312
- return (filename, str(e))
492
+ return (filename, file_path, type(e).__name__, str(e))
313
493
 
314
494
  # Import all files in parallel
315
495
  with ThreadPoolExecutor(max_workers=30) as executor:
@@ -318,12 +498,13 @@ def _import_catalog(ctx, directory):
318
498
  for future in as_completed(futures):
319
499
  results.append(future.result())
320
500
 
321
- # Print results in alphabetical order
322
- for filename, error in sorted(results, key=lambda x: x[0]):
323
- if error:
324
- print(f" Failed to import {filename}: {error}")
325
- else:
326
- print(f" Importing: {filename}")
501
+ # Count failures
502
+ failed_count = sum(1 for filename, file_path, error_type, error_msg in results if error_type)
503
+
504
+ if failed_count > 0:
505
+ print(f"\n Total catalog import failures: {failed_count}")
506
+
507
+ return ("catalog", len(results) - failed_count, [(fp, et, em) for fn, fp, et, em in results if et])
327
508
 
328
509
  def _import_plugins(ctx, directory):
329
510
  if os.path.isdir(directory):
@@ -337,9 +518,11 @@ def _import_plugins(ctx, directory):
337
518
  try:
338
519
  with open(file_path) as f:
339
520
  plugins.create(ctx, file_input=f, force=True)
340
- return (filename, None)
521
+ return (filename, file_path, None, None)
522
+ except typer.Exit as e:
523
+ return (filename, file_path, "HTTP", "Validation or HTTP error")
341
524
  except Exception as e:
342
- return (filename, str(e))
525
+ return (filename, file_path, type(e).__name__, str(e))
343
526
 
344
527
  # Import all files in parallel
345
528
  with ThreadPoolExecutor(max_workers=30) as executor:
@@ -349,12 +532,16 @@ def _import_plugins(ctx, directory):
349
532
  results.append(future.result())
350
533
 
351
534
  # Print results in alphabetical order
352
- for filename, error in sorted(results, key=lambda x: x[0]):
353
- if error:
354
- print(f" Failed to import {filename}: {error}")
535
+ failed_count = 0
536
+ for filename, file_path, error_type, error_msg in sorted(results, key=lambda x: x[0]):
537
+ if error_type:
538
+ print(f" Failed to import {filename}: {error_type} - {error_msg}")
539
+ failed_count += 1
355
540
  else:
356
541
  print(f" Importing: {filename}")
357
542
 
543
+ return ("plugins", len(results) - failed_count, [(fp, et, em) for fn, fp, et, em in results if et])
544
+
358
545
  def _import_scorecards(ctx, directory):
359
546
  if os.path.isdir(directory):
360
547
  print("Processing: " + directory)
@@ -367,9 +554,11 @@ def _import_scorecards(ctx, directory):
367
554
  try:
368
555
  with open(file_path) as f:
369
556
  scorecards.create(ctx, file_input=f, dry_run=False)
370
- return (filename, None)
557
+ return (filename, file_path, None, None)
558
+ except typer.Exit as e:
559
+ return (filename, file_path, "HTTP", "Validation or HTTP error")
371
560
  except Exception as e:
372
- return (filename, str(e))
561
+ return (filename, file_path, type(e).__name__, str(e))
373
562
 
374
563
  # Import all files in parallel
375
564
  with ThreadPoolExecutor(max_workers=30) as executor:
@@ -379,12 +568,16 @@ def _import_scorecards(ctx, directory):
379
568
  results.append(future.result())
380
569
 
381
570
  # Print results in alphabetical order
382
- for filename, error in sorted(results, key=lambda x: x[0]):
383
- if error:
384
- print(f" Failed to import {filename}: {error}")
571
+ failed_count = 0
572
+ for filename, file_path, error_type, error_msg in sorted(results, key=lambda x: x[0]):
573
+ if error_type:
574
+ print(f" Failed to import {filename}: {error_type} - {error_msg}")
575
+ failed_count += 1
385
576
  else:
386
577
  print(f" Importing: {filename}")
387
578
 
579
+ return ("scorecards", len(results) - failed_count, [(fp, et, em) for fn, fp, et, em in results if et])
580
+
388
581
  def _import_workflows(ctx, directory):
389
582
  if os.path.isdir(directory):
390
583
  print("Processing: " + directory)
@@ -397,9 +590,11 @@ def _import_workflows(ctx, directory):
397
590
  try:
398
591
  with open(file_path) as f:
399
592
  workflows.create(ctx, file_input=f)
400
- return (filename, None)
593
+ return (filename, file_path, None, None)
594
+ except typer.Exit as e:
595
+ return (filename, file_path, "HTTP", "Validation or HTTP error")
401
596
  except Exception as e:
402
- return (filename, str(e))
597
+ return (filename, file_path, type(e).__name__, str(e))
403
598
 
404
599
  # Import all files in parallel
405
600
  with ThreadPoolExecutor(max_workers=30) as executor:
@@ -409,12 +604,16 @@ def _import_workflows(ctx, directory):
409
604
  results.append(future.result())
410
605
 
411
606
  # Print results in alphabetical order
412
- for filename, error in sorted(results, key=lambda x: x[0]):
413
- if error:
414
- print(f" Failed to import {filename}: {error}")
607
+ failed_count = 0
608
+ for filename, file_path, error_type, error_msg in sorted(results, key=lambda x: x[0]):
609
+ if error_type:
610
+ print(f" Failed to import {filename}: {error_type} - {error_msg}")
611
+ failed_count += 1
415
612
  else:
416
613
  print(f" Importing: {filename}")
417
614
 
615
+ return ("workflows", len(results) - failed_count, [(fp, et, em) for fn, fp, et, em in results if et])
616
+
418
617
  @app.command("import")
419
618
  def import_tenant(
420
619
  ctx: typer.Context,
@@ -427,9 +626,67 @@ def import_tenant(
427
626
 
428
627
  client = ctx.obj["client"]
429
628
 
430
- _import_ip_allowlist(ctx, directory + "/ip-allowlist")
431
- _import_entity_types(ctx, force, directory + "/entity-types")
432
- _import_catalog(ctx, directory + "/catalog")
433
- _import_plugins(ctx, directory + "/plugins")
434
- _import_scorecards(ctx, directory + "/scorecards")
435
- _import_workflows(ctx, directory + "/workflows")
629
+ # Collect statistics from each import
630
+ all_stats = []
631
+ all_stats.append(_import_ip_allowlist(ctx, directory + "/ip-allowlist"))
632
+ all_stats.append(_import_entity_types(ctx, force, directory + "/entity-types"))
633
+ all_stats.append(_import_entity_relationship_types(ctx, directory + "/entity-relationship-types"))
634
+ all_stats.append(_import_catalog(ctx, directory + "/catalog"))
635
+ all_stats.append(_import_entity_relationships(ctx, directory + "/entity-relationships"))
636
+ all_stats.append(_import_plugins(ctx, directory + "/plugins"))
637
+ all_stats.append(_import_scorecards(ctx, directory + "/scorecards"))
638
+ all_stats.append(_import_workflows(ctx, directory + "/workflows"))
639
+
640
+ # Print summary
641
+ print("\n" + "="*80)
642
+ print("IMPORT SUMMARY")
643
+ print("="*80)
644
+
645
+ total_imported = 0
646
+ total_failed = 0
647
+ all_failures = []
648
+
649
+ for import_type, imported, failed in all_stats:
650
+ if imported > 0 or len(failed) > 0:
651
+ total_imported += imported
652
+ total_failed += len(failed)
653
+ print(f"\n{import_type}:")
654
+ print(f" Imported: {imported}")
655
+ if len(failed) > 0:
656
+ print(f" Failed: {len(failed)}")
657
+ all_failures.extend([(import_type, f, e, m) for f, e, m in failed])
658
+
659
+ print(f"\nTOTAL: {total_imported} imported, {total_failed} failed")
660
+
661
+ if len(all_failures) > 0:
662
+ print("\n" + "="*80)
663
+ print("FAILED IMPORTS")
664
+ print("="*80)
665
+ print("\nThe following files failed to import:\n")
666
+
667
+ for import_type, file_path, error_type, error_msg in all_failures:
668
+ print(f" {file_path}")
669
+ print(f" Error: {error_type} - {error_msg}")
670
+
671
+ print("\n" + "="*80)
672
+ print("RETRY COMMANDS")
673
+ print("="*80)
674
+ print("\nTo retry failed imports, run these commands:\n")
675
+
676
+ for import_type, file_path, error_type, error_msg in all_failures:
677
+ if import_type == "catalog":
678
+ print(f"cortex catalog create -f \"{file_path}\"")
679
+ elif import_type == "entity-types":
680
+ print(f"cortex entity-types create --force -f \"{file_path}\"")
681
+ elif import_type == "entity-relationship-types":
682
+ tag = os.path.basename(file_path).replace('.json', '')
683
+ print(f"cortex entity-relationship-types create -f \"{file_path}\"")
684
+ elif import_type == "entity-relationships":
685
+ # These need special handling - would need the relationship type
686
+ print(f"# Manual retry needed for entity-relationships: {file_path}")
687
+ elif import_type == "plugins":
688
+ print(f"cortex plugins create --force -f \"{file_path}\"")
689
+ elif import_type == "scorecards":
690
+ print(f"cortex scorecards create -f \"{file_path}\"")
691
+ elif import_type == "workflows":
692
+ print(f"cortex workflows create -f \"{file_path}\"")
@@ -0,0 +1,121 @@
1
+ import typer
2
+ import json
3
+ from typing_extensions import Annotated
4
+ from cortexapps_cli.utils import print_output_with_context
5
+ from cortexapps_cli.command_options import CommandOptions, ListCommandOptions
6
+
7
+ app = typer.Typer(
8
+ help="Entity Relationship Types commands",
9
+ no_args_is_help=True
10
+ )
11
+
12
+ @app.command()
13
+ def list(
14
+ ctx: typer.Context,
15
+ _print: CommandOptions._print = True,
16
+ page: ListCommandOptions.page = None,
17
+ page_size: ListCommandOptions.page_size = 250,
18
+ table_output: ListCommandOptions.table_output = False,
19
+ csv_output: ListCommandOptions.csv_output = False,
20
+ columns: ListCommandOptions.columns = [],
21
+ no_headers: ListCommandOptions.no_headers = False,
22
+ filters: ListCommandOptions.filters = [],
23
+ sort: ListCommandOptions.sort = [],
24
+ ):
25
+ """
26
+ List entity relationship types
27
+ """
28
+ client = ctx.obj["client"]
29
+
30
+ params = {
31
+ "page": page,
32
+ "pageSize": page_size
33
+ }
34
+
35
+ if (table_output or csv_output) and not ctx.params.get('columns'):
36
+ ctx.params['columns'] = [
37
+ "Tag=tag",
38
+ "Name=name",
39
+ "Description=description",
40
+ ]
41
+
42
+ # remove any params that are None
43
+ params = {k: v for k, v in params.items() if v is not None}
44
+
45
+ if page is None:
46
+ r = client.fetch("api/v1/relationship-types", params=params)
47
+ else:
48
+ r = client.get("api/v1/relationship-types", params=params)
49
+
50
+ if _print:
51
+ print_output_with_context(ctx, r)
52
+ else:
53
+ return r
54
+
55
+ @app.command()
56
+ def get(
57
+ ctx: typer.Context,
58
+ tag: str = typer.Option(..., "--tag", "-t", help="Relationship type tag"),
59
+ ):
60
+ """
61
+ Get a relationship type by tag
62
+ """
63
+ client = ctx.obj["client"]
64
+ r = client.get(f"api/v1/relationship-types/{tag}")
65
+ print_output_with_context(ctx, r)
66
+
67
+ @app.command()
68
+ def create(
69
+ ctx: typer.Context,
70
+ file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing relationship type definition; can be passed as stdin with -, example: -f-")] = ...,
71
+ _print: CommandOptions._print = True,
72
+ ):
73
+ """
74
+ Create a relationship type
75
+
76
+ Provide a JSON file with the relationship type definition including required fields:
77
+ - tag: unique identifier
78
+ - name: human-readable name
79
+ - definitionLocation: SOURCE, DESTINATION, or BOTH
80
+ - allowCycles: boolean
81
+ - createCatalog: boolean
82
+ - isSingleSource: boolean
83
+ - isSingleDestination: boolean
84
+ - sourcesFilter: object with include/types configuration
85
+ - destinationsFilter: object with include/types configuration
86
+ - inheritances: array of inheritance settings
87
+ """
88
+ client = ctx.obj["client"]
89
+ data = json.loads("".join([line for line in file_input]))
90
+ r = client.post("api/v1/relationship-types", data=data)
91
+ if _print:
92
+ print_output_with_context(ctx, r)
93
+
94
+ @app.command()
95
+ def update(
96
+ ctx: typer.Context,
97
+ tag: str = typer.Option(..., "--tag", "-t", help="Relationship type tag"),
98
+ file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing relationship type definition; can be passed as stdin with -, example: -f-")] = ...,
99
+ _print: CommandOptions._print = True,
100
+ ):
101
+ """
102
+ Update a relationship type
103
+
104
+ Provide a JSON file with the relationship type definition to update.
105
+ """
106
+ client = ctx.obj["client"]
107
+ data = json.loads("".join([line for line in file_input]))
108
+ r = client.put(f"api/v1/relationship-types/{tag}", data=data)
109
+ if _print:
110
+ print_output_with_context(ctx, r)
111
+
112
+ @app.command()
113
+ def delete(
114
+ ctx: typer.Context,
115
+ tag: str = typer.Option(..., "--tag", "-t", help="Relationship type tag"),
116
+ ):
117
+ """
118
+ Delete a relationship type
119
+ """
120
+ client = ctx.obj["client"]
121
+ client.delete(f"api/v1/relationship-types/{tag}")