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/commands/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import cortexapps_cli.commands.integrations_commands.firehydrant as firehydrant
import cortexapps_cli.commands.integrations_commands.github as github
import cortexapps_cli.commands.integrations_commands.gitlab as gitlab
import cortexapps_cli.commands.integrations_commands.harness as harness
import cortexapps_cli.commands.integrations_commands.incidentio as incidentio
import cortexapps_cli.commands.integrations_commands.instana as instana
import cortexapps_cli.commands.integrations_commands.jenkins as jenkins
Expand Down Expand Up @@ -72,6 +73,7 @@
app.add_typer(firehydrant.app, name="firehydrant")
app.add_typer(github.app, name="github")
app.add_typer(gitlab.app, name="gitlab")
app.add_typer(harness.app, name="harness")
app.add_typer(incidentio.app, name="incidentio")
app.add_typer(instana.app, name="instana")
app.add_typer(jenkins.app, name="jenkins")
Expand Down
160 changes: 160 additions & 0 deletions cortexapps_cli/commands/integrations_commands/harness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import json
from rich import print_json
import typer
from typing_extensions import Annotated

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


@app.command()
def add(
ctx: typer.Context,
alias: str = typer.Option(..., "--alias", "-a", help="Alias for this configuration"),
api_key: str = typer.Option(..., "--api-key", "-k", help="Harness API key"),
account_id: str = typer.Option(..., "--account-id", "-id", help="Harness account ID"),
host: str = typer.Option(None, "--host", "-h", help="Harness host URL (optional; defaults to https://app.harness.io)"),
is_default: bool = typer.Option(False, "--is-default", "-i", help="Set as the default configuration"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="JSON file containing configuration; use - for stdin")] = None,
):
"""
Add a Harness configuration
"""
client = ctx.obj["client"]

if file_input:
if alias or api_key or account_id or host or is_default:
raise typer.BadParameter("When providing a configuration file, do not specify any other attributes")
data = json.loads("".join([line for line in file_input]))
else:
data = {
"alias": alias,
"apiKey": api_key,
"accountId": account_id,
"isDefault": is_default,
}
if host is not None:
data["host"] = host

r = client.post("api/v1/harness/configuration", data=data)
print_json(data=r)


@app.command()
def get(
ctx: typer.Context,
alias: str = typer.Option(..., "--alias", "-a", help="Alias of the configuration to retrieve"),
):
"""
Get a single Harness configuration by alias
"""
client = ctx.obj["client"]
r = client.get(f"api/v1/harness/configuration/{alias}")
print_json(data=r)


@app.command()
def list(
ctx: typer.Context,
):
"""
List all Harness configurations
"""
client = ctx.obj["client"]
r = client.get("api/v1/harness/configurations")
print_json(data=r)


@app.command()
def get_default(
ctx: typer.Context,
):
"""
Get the default Harness configuration
"""
client = ctx.obj["client"]
r = client.get("api/v1/harness/default-configuration")
print_json(data=r)


@app.command()
def update(
ctx: typer.Context,
alias: str = typer.Option(..., "--alias", "-a", help="Alias of the configuration to update"),
api_key: str = typer.Option(None, "--api-key", "-k", help="New Harness API key"),
account_id: str = typer.Option(None, "--account-id", "-id", help="New Harness account ID"),
host: str = typer.Option(None, "--host", "-h", help="New Harness host URL"),
is_default: bool = typer.Option(None, "--is-default", "-i", help="Set as the default configuration"),
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="JSON file containing update fields; use - for stdin")] = None,
):
"""
Update a Harness configuration
"""
client = ctx.obj["client"]

if file_input:
if alias or api_key or account_id or host or is_default is not None:
raise typer.BadParameter("When providing a configuration file, do not specify any other attributes")
data = json.loads("".join([line for line in file_input]))
else:
data = {}
if api_key is not None:
data["apiKey"] = api_key
if account_id is not None:
data["accountId"] = account_id
if host is not None:
data["host"] = host
if is_default is not None:
data["isDefault"] = is_default

r = client.put(f"api/v1/harness/configuration/{alias}", data=data)
print_json(data=r)


@app.command()
def delete(
ctx: typer.Context,
alias: str = typer.Option(..., "--alias", "-a", help="Alias of the configuration to delete"),
):
"""
Delete a single Harness configuration
"""
client = ctx.obj["client"]
r = client.delete(f"api/v1/harness/configuration/{alias}")
print_json(data=r)


@app.command()
def delete_all(
ctx: typer.Context,
):
"""
Delete all Harness configurations
"""
client = ctx.obj["client"]
r = client.delete("api/v1/harness/configurations")
print_json(data=r)


@app.command()
def validate(
ctx: typer.Context,
alias: str = typer.Option(..., "--alias", "-a", help="Alias of the configuration to validate"),
):
"""
Validate a single Harness configuration
"""
client = ctx.obj["client"]
r = client.post(f"api/v1/harness/configuration/validate/{alias}")
print_json(data=r)


@app.command()
def validate_all(
ctx: typer.Context,
):
"""
Validate all Harness configurations
"""
client = ctx.obj["client"]
r = client.post("api/v1/harness/configuration/validate")
print_json(data=r)
6 changes: 3 additions & 3 deletions cortexapps_cli/commands/solutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,10 +400,10 @@ def _run_uninstall(client, path: Path, yes: bool) -> None:
total = sum(len(v) for v in resources.values())

if total == 0:
typer.echo("No resources found to remove.")
typer.echo("No entities found to remove.")
return

typer.echo("\nThis will remove the following resources:")
typer.echo("\nThis will remove the following entities:")
for kind in ("workflows", "scorecards", "plugins", "catalog", "entity-relationship-types", "entity-types"):
count = len(resources[kind])
if count:
Expand Down Expand Up @@ -701,7 +701,7 @@ def _do_import() -> None:
typer.echo(failed_m.group(0))
typer.echo(f"\n {total_imported} imported, {total_failed} failed")
else:
typer.echo(f" {total_imported} resources imported")
typer.echo(f" {total_imported} entities imported")
else:
typer.echo(output)

Expand Down
40 changes: 27 additions & 13 deletions cortexapps_cli/solutions/_lib/setup_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class SolutionSetup(ABC):

def __init__(self, state_dir: Optional[Path] = None, no_prompt: bool = False):
self._no_prompt = no_prompt
self._save_answers = True
self._secret_keys: set = set()
self._answers: dict = {}

Expand All @@ -39,10 +40,12 @@ def _load_file(self) -> dict:
return {}

def _save_file(self) -> None:
data = {
"answers": {k: v for k, v in self._answers.items() if k not in self._secret_keys},
"state": self._state,
}
saved_answers = (
{k: v for k, v in self._answers.items() if k not in self._secret_keys}
if self._save_answers
else {}
)
data = {"answers": saved_answers, "state": self._state}
self._state_file.write_text(json.dumps(data, indent=2))

def _save_state(self) -> None:
Expand All @@ -69,23 +72,29 @@ def prompt(
env_var: Optional[str] = None,
default: Optional[str] = None,
secret: bool = False,
hidden: bool = False,
) -> str:
"""Prompt for a value. Uses saved answer or env var when available."""
"""Prompt for a value. Uses saved answer or env var when available.

secret=True — mask input AND exclude from saved JSON
hidden=True — mask input only; value is still saved to JSON
"""
use_getpass = secret or hidden
if secret:
self._secret_keys.add(key)

# Non-secret: use saved answer when --no-prompt
# Use saved answer when --no-prompt (works for both plain and hidden keys)
if self._no_prompt and not secret and key in self._answers:
return self._answers[key]

# Non-secret: saved answer takes precedence over any derived default
# Saved answer takes precedence over any derived default
if not secret and key in self._answers:
default = self._answers[key]

if env_var:
env_val = os.environ.get(env_var)
if env_val:
masked = "********" if secret else env_val
masked = "********" if use_getpass else env_val
if self._no_prompt or self.confirm(f"{message} [{masked} from {env_var}]", default=True):
self._answers[key] = env_val
return env_val
Expand All @@ -94,12 +103,15 @@ def prompt(
if self._no_prompt and secret and key not in self._answers:
print(f" (secret required — no env var set for {key})", file=sys.stderr)

prompt_str = message
if default:
prompt_str += f" [{default}]"
prompt_str += ": "
if use_getpass and default:
hint = f"********{default[-4:]}"
elif default:
hint = default
else:
hint = None
prompt_str = f"{message} [{hint}]: " if hint else f"{message}: "

if secret:
if use_getpass:
value = getpass.getpass(prompt_str).strip()
else:
value = input(prompt_str).strip()
Expand Down Expand Up @@ -147,6 +159,8 @@ def run(self) -> None:
for k, v in saved.items():
print(f" {k}: {v}")
print()
elif not self._no_prompt:
self._save_answers = self.confirm(f"Save answers for future runs to {self._state_file}", default=True)

self.collect_prompts()
self._save_file()
Expand Down
15 changes: 6 additions & 9 deletions cortexapps_cli/solutions/github-actions-deploy/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,20 +163,17 @@ def collect_prompts(self) -> None:
self.prompt("github_owner", "GitHub org or username", default=default_owner)
self.prompt("repo_name", "Repository name", default="cortex-deploy-demo")

# 3. Cortex credentials from CLI session
# 3. Cortex credentials — use CLI session silently; only prompt when running standalone
self._secret_keys.add("cortex_api_key")
if self._session_api_key:
if self.confirm("Use Cortex API key used by this CLI session?", default=True):
self._answers["cortex_api_key"] = self._session_api_key
else:
self.prompt("cortex_api_key", "Cortex API key", secret=True)
self._answers["cortex_api_key"] = self._session_api_key
else:
self.prompt("cortex_api_key", "Cortex API key", env_var="CORTEX_API_KEY", secret=True)

if self._session_base_url:
if self.confirm(f"Use Cortex base URL used by this CLI session [{self._session_base_url}]?", default=True):
self._answers["cortex_base_url"] = self._session_base_url
else:
self.prompt("cortex_base_url", "Cortex base URL", default=self._session_base_url)
self._answers["cortex_base_url"] = self._session_base_url
else:
self.prompt("cortex_base_url", "Cortex base URL", default=self._session_base_url)
else:
self.prompt(
"cortex_base_url",
Expand Down
Loading
Loading