Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions cortexapps_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import cortexapps_cli.commands.plugins as plugins
import cortexapps_cli.commands.queries as queries
import cortexapps_cli.commands.rest as rest
import cortexapps_cli.commands.scaffolders as scaffolders
import cortexapps_cli.commands.scim as scim
import cortexapps_cli.commands.scorecards as scorecards
import cortexapps_cli.commands.secrets as secrets
Expand Down Expand Up @@ -277,6 +278,7 @@ def version():
app.add_typer(plugins.app, name="plugins")
app.add_typer(queries.app, name="queries")
app.add_typer(rest.app, name="rest")
app.add_typer(scaffolders.app, name="scaffolders")
app.add_typer(scim.app, name="scim")
app.add_typer(scorecards.app, name="scorecards")
app.add_typer(secrets.app, name="secrets")
Expand Down
156 changes: 156 additions & 0 deletions cortexapps_cli/commands/scaffolders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
from cortexapps_cli.command_options import CommandOptions
from cortexapps_cli.command_options import ListCommandOptions
from cortexapps_cli.utils import print_output_with_context, print_output
from typing_extensions import Annotated
import json
import typer
import yaml

app = typer.Typer(
help="Scaffolder template commands",
no_args_is_help=True
)

def _is_valid_yaml(filepath):
try:
yaml.safe_load(filepath)
filepath.seek(0)
return True
except yaml.YAMLError:
return False

def _is_valid_json(filepath):
try:
json.load(filepath)
filepath.seek(0)
return True
except json.JSONDecodeError:
return False

def _read_definition(file_input):
if _is_valid_json(file_input):
content_type = "application/json"
data = json.loads("".join([line for line in file_input]))
elif _is_valid_yaml(file_input):
data = file_input.read()
content_type = "application/yaml"
else:
raise typer.BadParameter("Input file is neither valid JSON nor YAML.")
return data, content_type

@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 Scaffolder templates.
"""

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:
# if page is not specified, we want to fetch all pages
r = client.fetch("api/v1/scaffolders", params=params)
else:
# if page is specified, we want to fetch only that page
r = client.get("api/v1/scaffolders", 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="The tag or unique, auto-generated Cortex ID of the Scaffolder template"),
yaml: bool = typer.Option(False, "--yaml", "-y", help="When true, returns the YAML representation of the template."),
_print: CommandOptions._print = True,
):
"""
Retrieve Scaffolder template by tag or Cortex ID.
"""

client = ctx.obj["client"]

if yaml:
headers={'Accept': 'application/yaml'}
else:
headers={'Accept': 'application/json'}
r = client.get("api/v1/scaffolders/" + tag, headers=headers)

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

@app.command()
def create(
ctx: typer.Context,
file_input: Annotated[typer.FileText, typer.Option(..., "--file", "-f", help="File containing the Scaffolder template definition; can be passed as stdin with -, example: -f-")],
):
"""
Create or update a Scaffolder template. API key must have the Configure Scaffolder permission. Note: If a Scaffolder template with the same tag already exists, it will be updated.
"""

client = ctx.obj["client"]

data, content_type = _read_definition(file_input)
r = client.post("api/v1/scaffolders", data=data, content_type=content_type)
print_output(r)

@app.command()
def update(
ctx: typer.Context,
tag: Annotated[str, typer.Option(..., "--tag", "-t", help="The tag or unique, auto-generated Cortex ID of the Scaffolder template")],
file_input: Annotated[typer.FileText, typer.Option(..., "--file", "-f", help="File containing the Scaffolder template definition; can be passed as stdin with -, example: -f-")],
):
"""
Update a Scaffolder template by tag or Cortex ID. API key must have the Configure Scaffolder permission.
"""

client = ctx.obj["client"]

data, content_type = _read_definition(file_input)
r = client.put("api/v1/scaffolders/" + tag, data=data, content_type=content_type)
print_output(r)

@app.command()
def delete(
ctx: typer.Context,
tag: str = typer.Option(..., "--tag", "-t", help="The tag or unique, auto-generated Cortex ID of the Scaffolder template"),
):
"""
Delete Scaffolder template by tag or Cortex ID. API key must have the Configure Scaffolder permission.
"""

client = ctx.obj["client"]

r = client.delete("api/v1/scaffolders/" + tag)
14 changes: 14 additions & 0 deletions data/import/scaffolders/cli-test-scaffolder-updated.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
tag: cli-test-scaffolder
name: CLI Test Scaffolder Updated
description: Created by the cortex-cli test suite; safe to delete.
type: COOKIECUTTER
repoLocator:
type: GITHUB
repoName: cli-test-scaffolder-template
org: cortextests
url: https://github.com/cortextests/cli-test-scaffolder-template
requireNewService: true
requirePullRequest: false
createYamlFile: true
showReadme: false
strictAliasUsage: false
14 changes: 14 additions & 0 deletions data/import/scaffolders/cli-test-scaffolder.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
tag: cli-test-scaffolder
name: CLI Test Scaffolder
description: Created by the cortex-cli test suite; safe to delete.
type: COOKIECUTTER
repoLocator:
type: GITHUB
repoName: cli-test-scaffolder-template
org: cortextests
url: https://github.com/cortextests/cli-test-scaffolder-template
requireNewService: true
requirePullRequest: false
createYamlFile: true
showReadme: false
strictAliasUsage: false
44 changes: 44 additions & 0 deletions tests/test_scaffolders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from tests.helpers.utils import *

def _api_enabled():
# The public Scaffolder API is gated by a feature flag; skip rather than
# fail when the test tenant does not have it enabled.
raw = cli(["scaffolders", "list"], return_type=ReturnType.RAW)
return raw.exit_code == 0

def test_list():
if not _api_enabled():
pytest.skip("Public Scaffolder API is not enabled for this tenant")

response = cli(["scaffolders", "list"])
assert "scaffolders" in response

def test_crud():
if not _api_enabled():
pytest.skip("Public Scaffolder API is not enabled for this tenant")

tag = "cli-test-scaffolder"

# Creation validates the template repository through the tenant's git
# integration, so skip when the fixture repo is not reachable here.
raw = cli(["scaffolders", "create", "-f", "data/import/scaffolders/cli-test-scaffolder.yaml"], return_type=ReturnType.RAW)
if raw.exit_code != 0:
pytest.skip(f"Scaffolder create failed on this tenant (likely no git integration for the fixture repo): {raw.stdout}")

try:
response = cli(["scaffolders", "list"])
assert any(s['tag'] == tag for s in response['scaffolders']), f"Should find Scaffolder template with tag {tag}"

response = cli(["scaffolders", "get", "-t", tag])
assert response['tag'] == tag

# Idempotent re-apply: the same definition upserts onto the same tag.
cli(["scaffolders", "create", "-f", "data/import/scaffolders/cli-test-scaffolder.yaml"])
response = cli(["scaffolders", "get", "-t", tag])
assert response['tag'] == tag

cli(["scaffolders", "update", "-t", tag, "-f", "data/import/scaffolders/cli-test-scaffolder-updated.yaml"])
response = cli(["scaffolders", "get", "-t", tag])
assert response['name'] == "CLI Test Scaffolder Updated"
finally:
cli(["scaffolders", "delete", "-t", tag])