Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
21e678f
Merge pull request #156 from cortexapps/staging #minor
jeff-schnitter Nov 5, 2025
7a63bfe
chore: update HISTORY.md for main
actions-user Nov 5, 2025
8879fcf
perf: optimize test scheduling with --dist loadfile for 25% faster te…
jeff-schnitter Nov 5, 2025
8c1ba4f
refactor: separate trigger-evaluation test to avoid scorecard evaluat…
jeff-schnitter Nov 5, 2025
3e09a81
refactor: remove unnecessary mock decorator from _get_rule helper fun…
jeff-schnitter Nov 5, 2025
c03fa22
Revert "perf: optimize test scheduling with --dist loadfile for 25% f…
jeff-schnitter Nov 5, 2025
f36aae2
perf: rename test_deploys.py to test_000_deploys.py for early scheduling
jeff-schnitter Nov 5, 2025
ca1d215
feat: add entity relationships API support and fix backup export bug
jeff-schnitter Nov 5, 2025
426b142
fix: clean up entity relationships import output and fix bugs
jeff-schnitter Nov 5, 2025
ce09777
fix: support re-importing existing entity relationship types
jeff-schnitter Nov 5, 2025
5256f68
feat: improve error handling in backup import
jeff-schnitter Nov 5, 2025
2afaf8c
fix: improve catalog import error handling and make sequential
jeff-schnitter Nov 5, 2025
55a5453
perf: parallelize entity relationships and catalog imports
jeff-schnitter Nov 6, 2025
f16308a
feat: add comprehensive import summary and retry commands
jeff-schnitter Nov 6, 2025
26d1d4c
fix: show catalog filename before import attempt
jeff-schnitter Nov 6, 2025
792d03d
fix: improve test isolation for custom events list test
jeff-schnitter Nov 6, 2025
d4ea29c
fix: initialize variables before conditional to prevent NameError
jeff-schnitter Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

<!-- insertion marker -->
## [1.3.0](https://github.com/cortexapps/cli/releases/tag/1.3.0) - 2025-11-05

<small>[Compare with 1.2.0](https://github.com/cortexapps/cli/compare/1.2.0...1.3.0)</small>

### Fixed

- fix: add retry logic for scorecard create to handle active evaluations ([cc40b55](https://github.com/cortexapps/cli/commit/cc40b55ed9ef5af4146360b5a879afc6dc67fe06) by Jeff Schnitter).
- fix: use json.dump instead of Rich print for file writing ([c66c2fe](https://github.com/cortexapps/cli/commit/c66c2fe438cc95f8343fbd4ba3cecae605c435ea) by Jeff Schnitter).
- fix: ensure export/import output is in alphabetical order ([9055f78](https://github.com/cortexapps/cli/commit/9055f78cc4e1136da20e4e42883ff3c0f248825b) by Jeff Schnitter).
- fix: ensure CORTEX_BASE_URL is available in publish workflow ([743579d](https://github.com/cortexapps/cli/commit/743579d760e900da693696df2841e7b710b08d39) by Jeff Schnitter).

## [1.2.0](https://github.com/cortexapps/cli/releases/tag/1.2.0) - 2025-11-04

<small>[Compare with 1.1.0](https://github.com/cortexapps/cli/compare/1.1.0...1.2.0)</small>
Expand Down
4 changes: 4 additions & 0 deletions cortexapps_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import cortexapps_cli.commands.discovery_audit as discovery_audit
import cortexapps_cli.commands.docs as docs
import cortexapps_cli.commands.entity_types as entity_types
import cortexapps_cli.commands.entity_relationship_types as entity_relationship_types
import cortexapps_cli.commands.entity_relationships as entity_relationships
import cortexapps_cli.commands.gitops_logs as gitops_logs
import cortexapps_cli.commands.groups as groups
import cortexapps_cli.commands.initiatives as initiatives
Expand Down Expand Up @@ -58,6 +60,8 @@
app.add_typer(discovery_audit.app, name="discovery-audit")
app.add_typer(docs.app, name="docs")
app.add_typer(entity_types.app, name="entity-types")
app.add_typer(entity_relationship_types.app, name="entity-relationship-types")
app.add_typer(entity_relationships.app, name="entity-relationships")
app.add_typer(gitops_logs.app, name="gitops-logs")
app.add_typer(groups.app, name="groups")
app.add_typer(initiatives.app, name="initiatives")
Expand Down
333 changes: 295 additions & 38 deletions cortexapps_cli/commands/backup.py

Large diffs are not rendered by default.

121 changes: 121 additions & 0 deletions cortexapps_cli/commands/entity_relationship_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import typer
import json
from typing_extensions import Annotated
from cortexapps_cli.utils import print_output_with_context
from cortexapps_cli.command_options import CommandOptions, ListCommandOptions

app = typer.Typer(
help="Entity Relationship Types commands",
no_args_is_help=True
)

@app.command()
def list(
ctx: typer.Context,
_print: CommandOptions._print = True,
page: ListCommandOptions.page = None,
page_size: ListCommandOptions.page_size = 250,
table_output: ListCommandOptions.table_output = False,
csv_output: ListCommandOptions.csv_output = False,
columns: ListCommandOptions.columns = [],
no_headers: ListCommandOptions.no_headers = False,
filters: ListCommandOptions.filters = [],
sort: ListCommandOptions.sort = [],
):
"""
List entity relationship types
"""
client = ctx.obj["client"]

params = {
"page": page,
"pageSize": page_size
}

if (table_output or csv_output) and not ctx.params.get('columns'):
ctx.params['columns'] = [
"Tag=tag",
"Name=name",
"Description=description",
]

# remove any params that are None
params = {k: v for k, v in params.items() if v is not None}

if page is None:
r = client.fetch("api/v1/relationship-types", params=params)
else:
r = client.get("api/v1/relationship-types", params=params)

if _print:
print_output_with_context(ctx, r)
else:
return r

@app.command()
def get(
ctx: typer.Context,
tag: str = typer.Option(..., "--tag", "-t", help="Relationship type tag"),
):
"""
Get a relationship type by tag
"""
client = ctx.obj["client"]
r = client.get(f"api/v1/relationship-types/{tag}")
print_output_with_context(ctx, r)

@app.command()
def create(
ctx: typer.Context,
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing relationship type definition; can be passed as stdin with -, example: -f-")] = ...,
_print: CommandOptions._print = True,
):
"""
Create a relationship type

Provide a JSON file with the relationship type definition including required fields:
- tag: unique identifier
- name: human-readable name
- definitionLocation: SOURCE, DESTINATION, or BOTH
- allowCycles: boolean
- createCatalog: boolean
- isSingleSource: boolean
- isSingleDestination: boolean
- sourcesFilter: object with include/types configuration
- destinationsFilter: object with include/types configuration
- inheritances: array of inheritance settings
"""
client = ctx.obj["client"]
data = json.loads("".join([line for line in file_input]))
r = client.post("api/v1/relationship-types", data=data)
if _print:
print_output_with_context(ctx, r)

@app.command()
def update(
ctx: typer.Context,
tag: str = typer.Option(..., "--tag", "-t", help="Relationship type tag"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing relationship type definition; can be passed as stdin with -, example: -f-")] = ...,
_print: CommandOptions._print = True,
):
"""
Update a relationship type

Provide a JSON file with the relationship type definition to update.
"""
client = ctx.obj["client"]
data = json.loads("".join([line for line in file_input]))
r = client.put(f"api/v1/relationship-types/{tag}", data=data)
if _print:
print_output_with_context(ctx, r)

@app.command()
def delete(
ctx: typer.Context,
tag: str = typer.Option(..., "--tag", "-t", help="Relationship type tag"),
):
"""
Delete a relationship type
"""
client = ctx.obj["client"]
client.delete(f"api/v1/relationship-types/{tag}")
222 changes: 222 additions & 0 deletions cortexapps_cli/commands/entity_relationships.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import typer
import json
from typing_extensions import Annotated
from cortexapps_cli.utils import print_output_with_context
from cortexapps_cli.command_options import CommandOptions, ListCommandOptions

app = typer.Typer(
help="Entity Relationships commands (Beta)",
no_args_is_help=True
)

@app.command()
def list(
ctx: typer.Context,
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
_print: CommandOptions._print = True,
page: ListCommandOptions.page = None,
page_size: ListCommandOptions.page_size = 250,
table_output: ListCommandOptions.table_output = False,
csv_output: ListCommandOptions.csv_output = False,
columns: ListCommandOptions.columns = [],
no_headers: ListCommandOptions.no_headers = False,
filters: ListCommandOptions.filters = [],
sort: ListCommandOptions.sort = [],
):
"""
List all relationships for a given relationship type
"""
client = ctx.obj["client"]

params = {
"page": page,
"pageSize": page_size
}

if (table_output or csv_output) and not ctx.params.get('columns'):
ctx.params['columns'] = [
"Source=source.tag",
"Destination=destination.tag",
"Provider=providerType",
]

# remove any params that are None
params = {k: v for k, v in params.items() if v is not None}

if page is None:
r = client.fetch(f"api/v1/relationships/{relationship_type}", params=params)
else:
r = client.get(f"api/v1/relationships/{relationship_type}", params=params)

if _print:
print_output_with_context(ctx, r)
else:
return r

@app.command()
def list_destinations(
ctx: typer.Context,
entity_tag: str = typer.Option(..., "--entity-tag", "-e", help="Entity tag or ID"),
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
depth: int = typer.Option(1, "--depth", "-d", help="Maximum hierarchy depth"),
include_archived: bool = typer.Option(False, "--include-archived", help="Include archived entities"),
):
"""
List destination entities for a given source entity and relationship type
"""
client = ctx.obj["client"]

params = {
"depth": depth,
"includeArchived": include_archived
}

r = client.get(f"api/v1/catalog/{entity_tag}/relationships/{relationship_type}/destinations", params=params)
print_output_with_context(ctx, r)

@app.command()
def list_sources(
ctx: typer.Context,
entity_tag: str = typer.Option(..., "--entity-tag", "-e", help="Entity tag or ID"),
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
depth: int = typer.Option(1, "--depth", "-d", help="Maximum hierarchy depth"),
include_archived: bool = typer.Option(False, "--include-archived", help="Include archived entities"),
):
"""
List source entities for a given destination entity and relationship type
"""
client = ctx.obj["client"]

params = {
"depth": depth,
"includeArchived": include_archived
}

r = client.get(f"api/v1/catalog/{entity_tag}/relationships/{relationship_type}/sources", params=params)
print_output_with_context(ctx, r)

@app.command()
def add_destinations(
ctx: typer.Context,
entity_tag: str = typer.Option(..., "--entity-tag", "-e", help="Entity tag or ID"),
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing destinations array; can be passed as stdin with -, example: -f-")] = ...,
force: bool = typer.Option(False, "--force", help="Override catalog descriptor values"),
):
"""
Add destination entities for a given source entity

Provide a JSON file with: {"destinations": ["entity-1", "entity-2"]}
"""
client = ctx.obj["client"]
data = json.loads("".join([line for line in file_input]))

params = {"force": force} if force else {}

r = client.post(f"api/v1/catalog/{entity_tag}/relationships/{relationship_type}/destinations", data=data, params=params)
print_output_with_context(ctx, r)

@app.command()
def add_sources(
ctx: typer.Context,
entity_tag: str = typer.Option(..., "--entity-tag", "-e", help="Entity tag or ID"),
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing sources array; can be passed as stdin with -, example: -f-")] = ...,
force: bool = typer.Option(False, "--force", help="Override catalog descriptor values"),
):
"""
Add source entities for a given destination entity

Provide a JSON file with: {"sources": ["entity-1", "entity-2"]}
"""
client = ctx.obj["client"]
data = json.loads("".join([line for line in file_input]))

params = {"force": force} if force else {}

r = client.post(f"api/v1/catalog/{entity_tag}/relationships/{relationship_type}/sources", data=data, params=params)
print_output_with_context(ctx, r)

@app.command()
def update_destinations(
ctx: typer.Context,
entity_tag: str = typer.Option(..., "--entity-tag", "-e", help="Entity tag or ID"),
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing destinations array; can be passed as stdin with -, example: -f-")] = ...,
force: bool = typer.Option(False, "--force", help="Override catalog descriptor values"),
):
"""
Replace all destination entities for a given source entity

Provide a JSON file with: {"destinations": ["entity-1", "entity-2"]}
"""
client = ctx.obj["client"]
data = json.loads("".join([line for line in file_input]))

params = {"force": force} if force else {}

r = client.put(f"api/v1/catalog/{entity_tag}/relationships/{relationship_type}/destinations", data=data, params=params)
print_output_with_context(ctx, r)

@app.command()
def update_sources(
ctx: typer.Context,
entity_tag: str = typer.Option(..., "--entity-tag", "-e", help="Entity tag or ID"),
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing sources array; can be passed as stdin with -, example: -f-")] = ...,
force: bool = typer.Option(False, "--force", help="Override catalog descriptor values"),
):
"""
Replace all source entities for a given destination entity

Provide a JSON file with: {"sources": ["entity-1", "entity-2"]}
"""
client = ctx.obj["client"]
data = json.loads("".join([line for line in file_input]))

params = {"force": force} if force else {}

r = client.put(f"api/v1/catalog/{entity_tag}/relationships/{relationship_type}/sources", data=data, params=params)
print_output_with_context(ctx, r)

@app.command()
def add_bulk(
ctx: typer.Context,
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing relationships array; can be passed as stdin with -, example: -f-")] = ...,
force: bool = typer.Option(False, "--force", help="Override catalog descriptor values"),
):
"""
Add multiple relationships in bulk

Provide a JSON file with: {"relationships": [{"source": "tag1", "destination": "tag2"}]}
"""
client = ctx.obj["client"]
data = json.loads("".join([line for line in file_input]))

params = {"force": force} if force else {}

r = client.post(f"api/v1/relationships/{relationship_type}", data=data, params=params)
print_output_with_context(ctx, r)

@app.command()
def update_bulk(
ctx: typer.Context,
relationship_type: str = typer.Option(..., "--relationship-type", "-r", help="Relationship type tag"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing relationships array; can be passed as stdin with -, example: -f-")] = ...,
force: bool = typer.Option(False, "--force", help="Override catalog descriptor values"),
_print: CommandOptions._print = True,
):
"""
Replace all relationships for a given relationship type

Provide a JSON file with: {"relationships": [{"source": "tag1", "destination": "tag2"}]}
"""
client = ctx.obj["client"]
data = json.loads("".join([line for line in file_input]))

params = {"force": force} if force else {}

r = client.put(f"api/v1/relationships/{relationship_type}", data=data, params=params)
if _print:
print_output_with_context(ctx, r)
Loading