cortexapps-cli 1.20.0__tar.gz → 1.21.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.20.0 → cortexapps_cli-1.21.0}/PKG-INFO +1 -1
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/cli.py +149 -34
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/backup.py +121 -2
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/workflows.py +3 -3
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/pyproject.toml +1 -1
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/LICENSE +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/README.rst +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/__init__.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/command_options.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/api_keys.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/audit_logs.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/backup_commands/cortex_export.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/catalog.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/custom_data.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/custom_events.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/custom_metrics.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/dependencies.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/deploys.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/discovery_audit.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/docs.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/entity_relationship_types.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/entity_relationships.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/entity_types.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/gitops_logs.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/groups.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/initiatives.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/apiiro.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/argocd.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/aws.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/azure_active_directory.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/azure_devops.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/azure_resources.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/bamboohr.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/bitbucket.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/bugsnag.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/buildkite.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/checkmarx_sast.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/circleci.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/clickup.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/codecov.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/coralogix.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/datadog.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/dynatrace.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/firehydrant.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/github.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/gitlab.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/incidentio.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/instana.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/jenkins.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/jira.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/launchdarkly.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/mend_sast.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/mend_sca.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/newrelic.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/okta.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/opsgenie.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/pagerduty.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/prometheus.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/rollbar.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/rootly.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/semgrep.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/sentry.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/servicenow.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/servicenow_cloud_observability.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/snyk.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/sonarqube.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/splunk_observability_cloud.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/splunk_on_call.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/sumologic.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/veracode.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/wiz.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/workday.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/xmatters.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/ip_allowlist.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/on_call.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/go.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/java.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/node.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/nuget.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/python.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/plugins.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/queries.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/rest.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/scim.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/scorecards.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/scorecards_commands/exemptions.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/secrets.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/teams.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/users.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/users_commands/roles.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/cortex_client.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/models/team.py +0 -0
- {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/utils.py +0 -0
|
@@ -9,6 +9,8 @@ import importlib.metadata
|
|
|
9
9
|
import tomllib
|
|
10
10
|
import configparser
|
|
11
11
|
import logging
|
|
12
|
+
import webbrowser
|
|
13
|
+
import requests
|
|
12
14
|
|
|
13
15
|
from cortexapps_cli.cortex_client import CortexClient
|
|
14
16
|
|
|
@@ -43,43 +45,23 @@ import cortexapps_cli.commands.teams as teams
|
|
|
43
45
|
import cortexapps_cli.commands.users as users
|
|
44
46
|
import cortexapps_cli.commands.workflows as workflows
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
class _SortedTyper(typer.Typer):
|
|
49
|
+
"""Typer subclass that sorts all commands alphabetically in --help output."""
|
|
50
|
+
def __call__(self, *args, **kwargs):
|
|
51
|
+
import typer.main as _tm
|
|
52
|
+
click_app = _tm.get_command(self)
|
|
53
|
+
click_app.commands = dict(sorted(click_app.commands.items()))
|
|
54
|
+
return click_app(*args, **kwargs)
|
|
55
|
+
|
|
56
|
+
app = _SortedTyper(
|
|
47
57
|
no_args_is_help=True,
|
|
48
58
|
rich_markup_mode="rich",
|
|
49
59
|
context_settings={"help_option_names": ["-h", "--help"]}
|
|
50
60
|
)
|
|
51
61
|
|
|
52
|
-
#
|
|
53
|
-
app.
|
|
54
|
-
|
|
55
|
-
app.add_typer(backup.app, name="backup")
|
|
56
|
-
app.add_typer(catalog.app, name="catalog")
|
|
57
|
-
app.add_typer(custom_data.app, name="custom-data")
|
|
58
|
-
app.add_typer(custom_events.app, name="custom-events")
|
|
59
|
-
app.add_typer(custom_metrics.app, name="custom-metrics")
|
|
60
|
-
app.add_typer(dependencies.app, name="dependencies")
|
|
61
|
-
app.add_typer(deploys.app, name="deploys")
|
|
62
|
-
app.add_typer(discovery_audit.app, name="discovery-audit")
|
|
63
|
-
app.add_typer(docs.app, name="docs")
|
|
64
|
-
app.add_typer(entity_types.app, name="entity-types")
|
|
65
|
-
app.add_typer(entity_relationship_types.app, name="entity-relationship-types")
|
|
66
|
-
app.add_typer(entity_relationships.app, name="entity-relationships")
|
|
67
|
-
app.add_typer(gitops_logs.app, name="gitops-logs")
|
|
68
|
-
app.add_typer(groups.app, name="groups")
|
|
69
|
-
app.add_typer(initiatives.app, name="initiatives")
|
|
70
|
-
app.add_typer(integrations.app, name="integrations")
|
|
71
|
-
app.add_typer(ip_allowlist.app, name="ip-allowlist")
|
|
72
|
-
app.add_typer(on_call.app, name="on-call")
|
|
73
|
-
app.add_typer(packages.app, name="packages")
|
|
74
|
-
app.add_typer(plugins.app, name="plugins")
|
|
75
|
-
app.add_typer(queries.app, name="queries")
|
|
76
|
-
app.add_typer(rest.app, name="rest")
|
|
77
|
-
app.add_typer(scim.app, name="scim")
|
|
78
|
-
app.add_typer(scorecards.app, name="scorecards")
|
|
79
|
-
app.add_typer(secrets.app, name="secrets")
|
|
80
|
-
app.add_typer(teams.app, name="teams")
|
|
81
|
-
app.add_typer(users.app, name="users")
|
|
82
|
-
app.add_typer(workflows.app, name="workflows")
|
|
62
|
+
# Commands and subcommands are registered below in alphabetical order.
|
|
63
|
+
# Direct commands (login, version) are registered via app.command() calls
|
|
64
|
+
# interleaved with add_typer() calls so they appear alphabetically in --help.
|
|
83
65
|
|
|
84
66
|
# global options
|
|
85
67
|
@app.callback()
|
|
@@ -95,6 +77,14 @@ def global_callback(
|
|
|
95
77
|
if not ctx.obj:
|
|
96
78
|
ctx.obj = {}
|
|
97
79
|
|
|
80
|
+
# Store config file path and tenant for use by the login command
|
|
81
|
+
ctx.obj["config_file"] = config_file
|
|
82
|
+
ctx.obj["tenant"] = tenant
|
|
83
|
+
|
|
84
|
+
# login command handles its own auth setup
|
|
85
|
+
if ctx.invoked_subcommand == "login":
|
|
86
|
+
return
|
|
87
|
+
|
|
98
88
|
numeric_level = getattr(logging, log_level.upper(), None)
|
|
99
89
|
if not isinstance(numeric_level, int):
|
|
100
90
|
raise ValueError(f"Invalid log level: {log_level}")
|
|
@@ -140,11 +130,102 @@ def global_callback(
|
|
|
140
130
|
|
|
141
131
|
ctx.obj["client"] = CortexClient(api_key, tenant, numeric_level, url, rate_limit)
|
|
142
132
|
|
|
143
|
-
|
|
133
|
+
DEFAULT_API_URL = "https://api.getcortexapp.com"
|
|
134
|
+
DEFAULT_UI_URL = "https://app.getcortexapp.com"
|
|
135
|
+
DEFAULT_UI_KEYS_PATH = "/admin/settings/personal-access-tokens"
|
|
136
|
+
|
|
137
|
+
def login(
|
|
138
|
+
ctx: typer.Context,
|
|
139
|
+
open_browser: bool = typer.Option(True, "--open-browser/--no-open-browser", help="Open browser to Cortex login page"),
|
|
140
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing tenant config without prompting"),
|
|
141
|
+
):
|
|
142
|
+
"""
|
|
143
|
+
Log in to Cortex and save credentials to the config file
|
|
144
|
+
"""
|
|
145
|
+
config_file = ctx.obj.get("config_file")
|
|
146
|
+
|
|
147
|
+
# Prompt for workspace name — this is both the Cortex workspace slug and the local config alias
|
|
148
|
+
default_tenant = ctx.obj.get("tenant", "default")
|
|
149
|
+
tenant = typer.prompt("Workspace name", default=default_tenant).strip()
|
|
150
|
+
|
|
151
|
+
# Prompt for base URL
|
|
152
|
+
url = typer.prompt("Base URL", default=DEFAULT_API_URL).strip('"\' /')
|
|
153
|
+
|
|
154
|
+
# Check existing config for this tenant
|
|
155
|
+
config = configparser.ConfigParser()
|
|
156
|
+
if os.path.isfile(config_file):
|
|
157
|
+
config.read(config_file)
|
|
158
|
+
if tenant in config and not force:
|
|
159
|
+
if not typer.confirm(f"Workspace '{tenant}' already exists in config. Overwrite?"):
|
|
160
|
+
raise typer.Abort()
|
|
161
|
+
|
|
162
|
+
# Derive UI URL, prefilling the workspace name on the login page
|
|
163
|
+
if url == DEFAULT_API_URL:
|
|
164
|
+
ui_base = DEFAULT_UI_URL
|
|
165
|
+
else:
|
|
166
|
+
ui_base = url.replace("://api.", "://app.")
|
|
167
|
+
|
|
168
|
+
pat_url = f"{ui_base}{DEFAULT_UI_KEYS_PATH}?tenantCode={tenant}"
|
|
169
|
+
|
|
170
|
+
if open_browser:
|
|
171
|
+
typer.echo(f"\nOpening browser to the Personal Access Tokens page for workspace '{tenant}'.")
|
|
172
|
+
typer.echo(f"Create or copy a personal access token, then come back here and paste it below.\n")
|
|
173
|
+
webbrowser.open(pat_url)
|
|
174
|
+
else:
|
|
175
|
+
typer.echo(f"\nGo to {pat_url} to create or copy a personal access token, then paste it below.\n")
|
|
176
|
+
|
|
177
|
+
# Prompt for API key (hidden input)
|
|
178
|
+
api_key = typer.prompt("API key", hide_input=True).strip('"\' ')
|
|
179
|
+
|
|
180
|
+
# Validate the key with a lightweight API call
|
|
181
|
+
typer.echo("Validating API key...", nl=False)
|
|
182
|
+
try:
|
|
183
|
+
resp = requests.get(
|
|
184
|
+
f"{url}/api/v1/catalog",
|
|
185
|
+
headers={
|
|
186
|
+
"Authorization": f"Bearer {api_key}",
|
|
187
|
+
"User-Agent": "cortexapps-cli/login",
|
|
188
|
+
},
|
|
189
|
+
params={"pageSize": 1},
|
|
190
|
+
timeout=10,
|
|
191
|
+
)
|
|
192
|
+
if resp.status_code == 401:
|
|
193
|
+
typer.echo(" invalid.")
|
|
194
|
+
typer.echo("Error: API key was not accepted (HTTP 401). Please check the key and try again.")
|
|
195
|
+
raise typer.Exit(1)
|
|
196
|
+
elif not resp.ok:
|
|
197
|
+
typer.echo(f" failed (HTTP {resp.status_code}).")
|
|
198
|
+
typer.echo(f"Error: Unexpected response from {url}. Please check the URL and try again.")
|
|
199
|
+
raise typer.Exit(1)
|
|
200
|
+
else:
|
|
201
|
+
typer.echo(" OK.")
|
|
202
|
+
except requests.exceptions.ConnectionError:
|
|
203
|
+
typer.echo(" connection failed.")
|
|
204
|
+
typer.echo(f"Error: Could not connect to {url}. Please check the URL and try again.")
|
|
205
|
+
raise typer.Exit(1)
|
|
206
|
+
except requests.exceptions.Timeout:
|
|
207
|
+
typer.echo(" timed out.")
|
|
208
|
+
typer.echo(f"Error: Request to {url} timed out. Please check the URL and try again.")
|
|
209
|
+
raise typer.Exit(1)
|
|
210
|
+
|
|
211
|
+
# Write / update config
|
|
212
|
+
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
|
213
|
+
config[tenant] = {"api_key": api_key}
|
|
214
|
+
if url != DEFAULT_API_URL:
|
|
215
|
+
config[tenant]["base_url"] = url
|
|
216
|
+
|
|
217
|
+
with open(config_file, "w") as f:
|
|
218
|
+
config.write(f)
|
|
219
|
+
|
|
220
|
+
tenant_flag = f" -t {tenant}" if tenant != "default" else ""
|
|
221
|
+
typer.echo(f"\nCredentials saved to {config_file}")
|
|
222
|
+
typer.echo(f"You can now run: cortex{tenant_flag} catalog list")
|
|
223
|
+
|
|
224
|
+
|
|
144
225
|
def version():
|
|
145
226
|
|
|
146
227
|
"""
|
|
147
|
-
Show the version and exit
|
|
228
|
+
Show the version and exit
|
|
148
229
|
"""
|
|
149
230
|
try:
|
|
150
231
|
with open("pyproject.toml", "rb") as f:
|
|
@@ -154,5 +235,39 @@ def version():
|
|
|
154
235
|
version = importlib.metadata.version('cortexapps_cli')
|
|
155
236
|
print(version)
|
|
156
237
|
|
|
238
|
+
# Register all commands alphabetically so they appear in order in --help
|
|
239
|
+
app.add_typer(api_keys.app, name="api-keys")
|
|
240
|
+
app.add_typer(audit_logs.app, name="audit-logs")
|
|
241
|
+
app.add_typer(backup.app, name="backup")
|
|
242
|
+
app.add_typer(catalog.app, name="catalog")
|
|
243
|
+
app.add_typer(custom_data.app, name="custom-data")
|
|
244
|
+
app.add_typer(custom_events.app, name="custom-events")
|
|
245
|
+
app.add_typer(custom_metrics.app, name="custom-metrics")
|
|
246
|
+
app.add_typer(dependencies.app, name="dependencies")
|
|
247
|
+
app.add_typer(deploys.app, name="deploys")
|
|
248
|
+
app.add_typer(discovery_audit.app, name="discovery-audit")
|
|
249
|
+
app.add_typer(docs.app, name="docs")
|
|
250
|
+
app.add_typer(entity_relationship_types.app, name="entity-relationship-types")
|
|
251
|
+
app.add_typer(entity_relationships.app, name="entity-relationships")
|
|
252
|
+
app.add_typer(entity_types.app, name="entity-types")
|
|
253
|
+
app.add_typer(gitops_logs.app, name="gitops-logs")
|
|
254
|
+
app.add_typer(groups.app, name="groups")
|
|
255
|
+
app.add_typer(initiatives.app, name="initiatives")
|
|
256
|
+
app.add_typer(integrations.app, name="integrations")
|
|
257
|
+
app.add_typer(ip_allowlist.app, name="ip-allowlist")
|
|
258
|
+
app.command()(login)
|
|
259
|
+
app.add_typer(on_call.app, name="on-call")
|
|
260
|
+
app.add_typer(packages.app, name="packages")
|
|
261
|
+
app.add_typer(plugins.app, name="plugins")
|
|
262
|
+
app.add_typer(queries.app, name="queries")
|
|
263
|
+
app.add_typer(rest.app, name="rest")
|
|
264
|
+
app.add_typer(scim.app, name="scim")
|
|
265
|
+
app.add_typer(scorecards.app, name="scorecards")
|
|
266
|
+
app.add_typer(secrets.app, name="secrets")
|
|
267
|
+
app.add_typer(teams.app, name="teams")
|
|
268
|
+
app.add_typer(users.app, name="users")
|
|
269
|
+
app.command()(version)
|
|
270
|
+
app.add_typer(workflows.app, name="workflows")
|
|
271
|
+
|
|
157
272
|
if __name__ == "__main__":
|
|
158
273
|
app()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
import time
|
|
2
3
|
from typing import Optional
|
|
3
4
|
from typing import List
|
|
4
5
|
from typing_extensions import Annotated
|
|
@@ -7,7 +8,6 @@ import json
|
|
|
7
8
|
import os
|
|
8
9
|
import tempfile
|
|
9
10
|
import sys
|
|
10
|
-
from io import StringIO
|
|
11
11
|
from contextlib import redirect_stdout, redirect_stderr
|
|
12
12
|
from rich import print, print_json
|
|
13
13
|
from rich.console import Console
|
|
@@ -471,6 +471,18 @@ def _import_entity_relationships(ctx, directory):
|
|
|
471
471
|
|
|
472
472
|
return ("entity-relationships", len(results) - failed_count, [(fp, et, em) for rt, fp, et, em in results if et])
|
|
473
473
|
|
|
474
|
+
def _has_relationships(file_path):
|
|
475
|
+
"""Check if a catalog YAML file contains x-cortex-relationships."""
|
|
476
|
+
try:
|
|
477
|
+
with open(file_path) as f:
|
|
478
|
+
content = yaml.safe_load(f)
|
|
479
|
+
info = content.get('info', {})
|
|
480
|
+
relationships = info.get('x-cortex-relationships')
|
|
481
|
+
return relationships is not None and len(relationships) > 0
|
|
482
|
+
except Exception:
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
|
|
474
486
|
def _import_catalog(ctx, directory):
|
|
475
487
|
results = []
|
|
476
488
|
failed_count = 0
|
|
@@ -493,7 +505,7 @@ def _import_catalog(ctx, directory):
|
|
|
493
505
|
except Exception as e:
|
|
494
506
|
return (filename, file_path, type(e).__name__, str(e))
|
|
495
507
|
|
|
496
|
-
# Import all files
|
|
508
|
+
# Pass 1: Import all files
|
|
497
509
|
with ThreadPoolExecutor(max_workers=30) as executor:
|
|
498
510
|
futures = {executor.submit(import_catalog_file, file_info): file_info[0] for file_info in files}
|
|
499
511
|
results = []
|
|
@@ -506,6 +518,113 @@ def _import_catalog(ctx, directory):
|
|
|
506
518
|
if failed_count > 0:
|
|
507
519
|
print(f"\n Total catalog import failures: {failed_count}")
|
|
508
520
|
|
|
521
|
+
# Pass 2: Delete and re-create entities with x-cortex-relationships.
|
|
522
|
+
# On first creation (pass 1), the relationship processor runs synchronously but
|
|
523
|
+
# the entity hasn't been committed to the DB yet, so it caches a failed state.
|
|
524
|
+
# Subsequent updates don't clear this cache. Deleting the entity clears the cache,
|
|
525
|
+
# so the fresh re-create triggers proper relationship processing.
|
|
526
|
+
#
|
|
527
|
+
# Two-wave approach handles multi-tier hierarchies: Wave 1 re-creates all
|
|
528
|
+
# relationship entities (e.g., clusters → services, where services are stable from
|
|
529
|
+
# pass 1). Wave 2 re-creates entities whose destinations were also recreated in
|
|
530
|
+
# wave 1 (e.g., accounts → clusters), ensuring destinations exist before the
|
|
531
|
+
# relationship processor runs for those dependent sources.
|
|
532
|
+
relationship_files = [(fn, fp) for fn, fp in files if _has_relationships(fp)]
|
|
533
|
+
if relationship_files:
|
|
534
|
+
print(f"\n Re-importing {len(relationship_files)} entities with relationships...")
|
|
535
|
+
|
|
536
|
+
# Collect all tags being recreated in pass 2
|
|
537
|
+
pass2_tags = set()
|
|
538
|
+
for fn, fp in relationship_files:
|
|
539
|
+
try:
|
|
540
|
+
with open(fp) as f:
|
|
541
|
+
content = yaml.safe_load(f)
|
|
542
|
+
tag = content.get('info', {}).get('x-cortex-tag')
|
|
543
|
+
if tag:
|
|
544
|
+
pass2_tags.add(tag)
|
|
545
|
+
except Exception:
|
|
546
|
+
pass
|
|
547
|
+
|
|
548
|
+
def delete_entity(file_info):
|
|
549
|
+
filename, file_path = file_info
|
|
550
|
+
try:
|
|
551
|
+
with open(file_path) as f:
|
|
552
|
+
content = yaml.safe_load(f)
|
|
553
|
+
tag = content.get('info', {}).get('x-cortex-tag')
|
|
554
|
+
if tag:
|
|
555
|
+
catalog.delete(ctx, tag=tag)
|
|
556
|
+
except Exception:
|
|
557
|
+
pass # Entity may not exist; that's fine
|
|
558
|
+
|
|
559
|
+
def create_entity(file_info):
|
|
560
|
+
filename, file_path = file_info
|
|
561
|
+
print(f" Re-importing: {filename}")
|
|
562
|
+
try:
|
|
563
|
+
with open(file_path) as f:
|
|
564
|
+
catalog.create(ctx, file_input=f, _print=False)
|
|
565
|
+
return (filename, file_path, None, None)
|
|
566
|
+
except typer.Exit as e:
|
|
567
|
+
return (filename, file_path, "HTTP", "Validation or HTTP error")
|
|
568
|
+
except Exception as e:
|
|
569
|
+
return (filename, file_path, type(e).__name__, str(e))
|
|
570
|
+
|
|
571
|
+
def has_pass2_destination(file_path):
|
|
572
|
+
"""Return True if any of this entity's relationship destinations are
|
|
573
|
+
also being recreated in pass 2 (i.e., they need to exist first)."""
|
|
574
|
+
try:
|
|
575
|
+
with open(file_path) as f:
|
|
576
|
+
content = yaml.safe_load(f)
|
|
577
|
+
info = content.get('info', {})
|
|
578
|
+
for rel in info.get('x-cortex-relationships', []):
|
|
579
|
+
for dest in rel.get('destinations', []):
|
|
580
|
+
if dest.get('tag') in pass2_tags:
|
|
581
|
+
return True
|
|
582
|
+
except Exception:
|
|
583
|
+
pass
|
|
584
|
+
return False
|
|
585
|
+
|
|
586
|
+
reprocess_results = []
|
|
587
|
+
|
|
588
|
+
# Wave 1: Delete all, then create all. This establishes every entity
|
|
589
|
+
# in the DB and resolves relationships whose destinations are stable
|
|
590
|
+
# (i.e., not being recreated in this pass).
|
|
591
|
+
with ThreadPoolExecutor(max_workers=30) as executor:
|
|
592
|
+
futures = {executor.submit(delete_entity, fi): fi[0] for fi in relationship_files}
|
|
593
|
+
for future in as_completed(futures):
|
|
594
|
+
future.result()
|
|
595
|
+
|
|
596
|
+
time.sleep(2) # Allow deletes to commit before re-creating
|
|
597
|
+
|
|
598
|
+
with ThreadPoolExecutor(max_workers=30) as executor:
|
|
599
|
+
futures = {executor.submit(create_entity, fi): fi[0] for fi in relationship_files}
|
|
600
|
+
for future in as_completed(futures):
|
|
601
|
+
reprocess_results.append(future.result())
|
|
602
|
+
|
|
603
|
+
# Wave 2: For entities whose destinations were also recreated in wave 1,
|
|
604
|
+
# do another delete+create now that those destinations exist in the DB.
|
|
605
|
+
dependent_files = [(fn, fp) for fn, fp in relationship_files if has_pass2_destination(fp)]
|
|
606
|
+
if dependent_files:
|
|
607
|
+
time.sleep(5) # Let wave 1 entities settle in the DB
|
|
608
|
+
|
|
609
|
+
with ThreadPoolExecutor(max_workers=30) as executor:
|
|
610
|
+
futures = {executor.submit(delete_entity, fi): fi[0] for fi in dependent_files}
|
|
611
|
+
for future in as_completed(futures):
|
|
612
|
+
future.result()
|
|
613
|
+
|
|
614
|
+
time.sleep(2)
|
|
615
|
+
|
|
616
|
+
with ThreadPoolExecutor(max_workers=30) as executor:
|
|
617
|
+
futures = {executor.submit(create_entity, fi): fi[0] for fi in dependent_files}
|
|
618
|
+
for future in as_completed(futures):
|
|
619
|
+
reprocess_results.append(future.result())
|
|
620
|
+
|
|
621
|
+
reprocess_failed = sum(1 for fn, fp, et, em in reprocess_results if et)
|
|
622
|
+
if reprocess_failed > 0:
|
|
623
|
+
print(f"\n Total catalog re-import failures: {reprocess_failed}")
|
|
624
|
+
failed_count += reprocess_failed
|
|
625
|
+
else:
|
|
626
|
+
print(f" Note: relationships are processed asynchronously and will be visible within 30 seconds.")
|
|
627
|
+
|
|
509
628
|
return ("catalog", len(results) - failed_count, [(fp, et, em) for fn, fp, et, em in results if et])
|
|
510
629
|
|
|
511
630
|
def _import_plugins(ctx, directory):
|
|
@@ -7,7 +7,7 @@ import typer
|
|
|
7
7
|
import yaml
|
|
8
8
|
|
|
9
9
|
app = typer.Typer(
|
|
10
|
-
help="Workflows commands
|
|
10
|
+
help="Workflows commands",
|
|
11
11
|
no_args_is_help=True
|
|
12
12
|
)
|
|
13
13
|
|
|
@@ -156,7 +156,7 @@ def run(
|
|
|
156
156
|
timeout: int = typer.Option(300, "--timeout", help="Max seconds to wait when --wait is used"),
|
|
157
157
|
):
|
|
158
158
|
"""
|
|
159
|
-
|
|
159
|
+
Run a workflow. API key must have the Run workflows permission.
|
|
160
160
|
The workflow must have isRunnableViaApi set to true.
|
|
161
161
|
"""
|
|
162
162
|
import time as time_module
|
|
@@ -214,7 +214,7 @@ def get_run(
|
|
|
214
214
|
run_id: str = typer.Option(..., "--run-id", "-r", help="The run ID"),
|
|
215
215
|
):
|
|
216
216
|
"""
|
|
217
|
-
|
|
217
|
+
Get details of a workflow run. API key must have the View workflow runs permission.
|
|
218
218
|
"""
|
|
219
219
|
client = ctx.obj["client"]
|
|
220
220
|
r = client.get(f"api/v1/workflows/{tag}/runs/{run_id}")
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/entity_relationship_types.py
RENAMED
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/entity_relationships.py
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
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/aws.py
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/wiz.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/go.py
RENAMED
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/java.py
RENAMED
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/node.py
RENAMED
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/nuget.py
RENAMED
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/python.py
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
|
|
File without changes
|
|
File without changes
|
{cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/users_commands/roles.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|