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.
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/PKG-INFO +1 -1
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/cli.py +9 -2
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/backup.py +295 -38
- cortexapps_cli-1.5.0/cortexapps_cli/commands/entity_relationship_types.py +121 -0
- cortexapps_cli-1.5.0/cortexapps_cli/commands/entity_relationships.py +222 -0
- cortexapps_cli-1.5.0/cortexapps_cli/commands/secrets.py +105 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/cortex_client.py +90 -2
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/pyproject.toml +1 -1
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/LICENSE +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/README.rst +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/command_options.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/api_keys.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/audit_logs.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/backup_commands/cortex_export.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/catalog.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/custom_data.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/custom_events.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/custom_metrics.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/dependencies.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/deploys.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/discovery_audit.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/docs.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/entity_types.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/gitops_logs.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/groups.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/initiatives.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/aws.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/azure_devops.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/azure_resources.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/circleci.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/coralogix.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/datadog.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/github.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/gitlab.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/incidentio.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/launchdarkly.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/newrelic.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/pagerduty.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/prometheus.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/integrations_commands/sonarqube.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/ip_allowlist.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/on_call.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/go.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/java.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/node.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/nuget.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/packages_commands/python.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/plugins.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/queries.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/rest.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/scim.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/scorecards.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/scorecards_commands/exemptions.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/teams.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/commands/workflows.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/models/team.py +0 -0
- {cortexapps_cli-1.3.0 → cortexapps_cli-1.5.0}/cortexapps_cli/utils.py +0 -0
|
@@ -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
|
-
|
|
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",
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
#
|
|
322
|
-
for filename,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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}")
|