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.
Files changed (95) hide show
  1. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/PKG-INFO +1 -1
  2. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/cli.py +149 -34
  3. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/backup.py +121 -2
  4. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/workflows.py +3 -3
  5. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/pyproject.toml +1 -1
  6. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/LICENSE +0 -0
  7. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/README.rst +0 -0
  8. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/__init__.py +0 -0
  9. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/command_options.py +0 -0
  10. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/api_keys.py +0 -0
  11. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/audit_logs.py +0 -0
  12. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/backup_commands/cortex_export.py +0 -0
  13. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/catalog.py +0 -0
  14. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/custom_data.py +0 -0
  15. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/custom_events.py +0 -0
  16. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/custom_metrics.py +0 -0
  17. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/dependencies.py +0 -0
  18. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/deploys.py +0 -0
  19. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/discovery_audit.py +0 -0
  20. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/docs.py +0 -0
  21. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/entity_relationship_types.py +0 -0
  22. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/entity_relationships.py +0 -0
  23. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/entity_types.py +0 -0
  24. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/gitops_logs.py +0 -0
  25. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/groups.py +0 -0
  26. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/initiatives.py +0 -0
  27. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations.py +0 -0
  28. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/apiiro.py +0 -0
  29. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/argocd.py +0 -0
  30. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/aws.py +0 -0
  31. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/azure_active_directory.py +0 -0
  32. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/azure_devops.py +0 -0
  33. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/azure_resources.py +0 -0
  34. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/bamboohr.py +0 -0
  35. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/bitbucket.py +0 -0
  36. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/bugsnag.py +0 -0
  37. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/buildkite.py +0 -0
  38. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/checkmarx_sast.py +0 -0
  39. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/circleci.py +0 -0
  40. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/clickup.py +0 -0
  41. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/codecov.py +0 -0
  42. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/coralogix.py +0 -0
  43. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/datadog.py +0 -0
  44. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/dynatrace.py +0 -0
  45. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/firehydrant.py +0 -0
  46. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/github.py +0 -0
  47. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/gitlab.py +0 -0
  48. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/incidentio.py +0 -0
  49. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/instana.py +0 -0
  50. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/jenkins.py +0 -0
  51. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/jira.py +0 -0
  52. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/launchdarkly.py +0 -0
  53. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/mend_sast.py +0 -0
  54. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/mend_sca.py +0 -0
  55. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/newrelic.py +0 -0
  56. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/okta.py +0 -0
  57. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/opsgenie.py +0 -0
  58. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/pagerduty.py +0 -0
  59. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/prometheus.py +0 -0
  60. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/rollbar.py +0 -0
  61. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/rootly.py +0 -0
  62. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/semgrep.py +0 -0
  63. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/sentry.py +0 -0
  64. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/servicenow.py +0 -0
  65. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/servicenow_cloud_observability.py +0 -0
  66. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/snyk.py +0 -0
  67. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/sonarqube.py +0 -0
  68. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/splunk_observability_cloud.py +0 -0
  69. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/splunk_on_call.py +0 -0
  70. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/sumologic.py +0 -0
  71. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/veracode.py +0 -0
  72. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/wiz.py +0 -0
  73. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/workday.py +0 -0
  74. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/integrations_commands/xmatters.py +0 -0
  75. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/ip_allowlist.py +0 -0
  76. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/on_call.py +0 -0
  77. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages.py +0 -0
  78. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/go.py +0 -0
  79. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/java.py +0 -0
  80. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/node.py +0 -0
  81. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/nuget.py +0 -0
  82. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/packages_commands/python.py +0 -0
  83. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/plugins.py +0 -0
  84. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/queries.py +0 -0
  85. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/rest.py +0 -0
  86. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/scim.py +0 -0
  87. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/scorecards.py +0 -0
  88. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/scorecards_commands/exemptions.py +0 -0
  89. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/secrets.py +0 -0
  90. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/teams.py +0 -0
  91. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/users.py +0 -0
  92. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/commands/users_commands/roles.py +0 -0
  93. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/cortex_client.py +0 -0
  94. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.0}/cortexapps_cli/models/team.py +0 -0
  95. {cortexapps_cli-1.20.0 → cortexapps_cli-1.21.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.20.0
3
+ Version: 1.21.0
4
4
  Summary: Command Line Interface for cortexapps
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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
- app = typer.Typer(
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
- # add subcommands
53
- app.add_typer(api_keys.app, name="api-keys")
54
- app.add_typer(audit_logs.app, name="audit-logs")
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
- @app.command()
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 in parallel
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 (Beta)",
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
- (Beta) Run a workflow. API key must have the Run workflows permission.
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
- (Beta) Get details of a workflow run. API key must have the View workflow runs permission.
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}")
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "cortexapps-cli"
3
3
  # version will be incremented via command line as part of github actions build
4
- version = "1.20.0"
4
+ version = "1.21.0"
5
5
  description = "Command Line Interface for cortexapps"
6
6
  license = "MIT"
7
7
  authors = [
File without changes