Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modifying cli to be compatible with api. #9

Merged
merged 7 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 1 addition & 3 deletions .ci-pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ repos:
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
exclude: "^comet/core/migrations/.*"

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.291
hooks:
- id: ruff
exclude: "^comet/core/migrations/.*"
args: [--line-length=100]

- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
exclude: "^comet/core/migrations/.*"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ env/
venv/
.DS_STORE
.pre-commit-config.yaml
.pem
4 changes: 3 additions & 1 deletion cased/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from cased.commands.deploy import deploy
from cased.commands.init import init
from cased.commands.login import login, logout
from cased.commands.resources import branches, deployments
from cased.commands.resources import branches, deployments, projects, targets

CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])

Expand All @@ -27,6 +27,8 @@ def cli():
cli.add_command(logout)
cli.add_command(deployments)
cli.add_command(branches)
cli.add_command(projects)
cli.add_command(targets)

# ... (keep the login and setup_target commands as they were) ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# ... (keep the login and setup_target commands as they were) ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol should ask this LLM to not generate those comments


Expand Down
9 changes: 5 additions & 4 deletions cased/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn

from cased.utils.api import API_BASE_URL, create_secrets
from cased.utils.api import CasedAPI
from cased.utils.constants import CasedConstants
from cased.utils.git import get_repo_name

console = Console()
Expand Down Expand Up @@ -73,17 +74,17 @@ def build() -> None:

project_name = get_repo_name()
secrets = extract_secrets_from_workflow(workflow_content)
create_secrets(project_name, secrets)
CasedAPI().create_secrets(project_name, secrets)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts about exporting the empty constructor form of this as just cased_api? cc @ivan-cased

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could have cased_api = CasedAI() in another file, so is can be imported as cased_api. Not a big deal either way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah could be, may choose to do that in the branch_deploy PR, wanting to organize the error messages from api a bit and determine the final structure, right now we sys.exit on non 200 response, that's not a good way to implement or debug.


console.print(
Panel(
f"""
[bold green]GitHub Actions workflow generated successfully![/bold green]
[bold green]Please complete the following steps for the workflow to work correctly: [/bold green]
1. Review the generated workflow file in .github/workflows/deploy.yaml
2. Go to {API_BASE_URL}/secrets/{project_name} to update the secrets.
2. Go to {CasedConstants.API_BASE_URL}/secrets/{project_name} to update the secrets.
3. Commit the changes to your repository, and the workflow will be triggered.
4. Go to {API_BASE_URL}/deployments/ to monitor the deployment status.
4. Go to {CasedConstants.API_BASE_URL}/deployments/ to monitor the deployment status.
""", # noqa: E501
title="Success",
expand=False,
Expand Down
17 changes: 8 additions & 9 deletions cased/commands/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
import questionary
from rich.console import Console

from cased.utils.api import deploy_branch, get_branches
from cased.utils.auth import get_token
from cased.utils.api import CasedAPI
from cased.utils.auth import validate_credentials
from cased.utils.progress import run_process_with_status_bar

console = Console()


def _build_questionary_choices():
data = run_process_with_status_bar(get_branches, "Fetching branches...", timeout=10)
api_client = CasedAPI()
data = run_process_with_status_bar(
api_client.get_branches, "Fetching branches...", timeout=10
)
branches = data.get("pull_requests", [])
deployable_branches = [
branch for branch in branches if branch["deployable"] is True
Expand Down Expand Up @@ -51,6 +54,7 @@ def _build_questionary_choices():
@click.command()
@click.option("--branch", help="Branch to deploy")
@click.option("--target", help="Target environment for deployment")
@validate_credentials(check_project_set=False)
def deploy(branch, target):
"""
Deploy a branch to a target environment.
Expand All @@ -65,11 +69,6 @@ def deploy(branch, target):
cased deploy
cased deploy --branch feature-branch-1 --target dev
""" # noqa: E501
token = get_token()
if not token:
console.print("[red]Please log in first using 'cased login'[/red]")
return

if not branch and not target:
branch, target = _build_questionary_choices()

Expand All @@ -78,7 +77,7 @@ def deploy(branch, target):
)

if branch and target:
deploy_branch(branch, target)
CasedAPI().deploy_branch(branch, target)
console.print("[green]Dispatch succeeded. Starting deployment...[/green]")
else:
console.print("[red]Deployment dispatch failed. Please try again later.[/red]")
2 changes: 2 additions & 0 deletions cased/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
from rich.console import Console
from rich.panel import Panel

from cased.utils.auth import validate_credentials
from cased.utils.progress import run_process_with_status_bar

console = Console()


@click.command()
@validate_credentials(check_project_set=False)
def init():
"""Initialize a new project configuration."""
console.print(Panel.fit("Welcome to Cased", style="bold magenta"))
Expand Down
167 changes: 69 additions & 98 deletions cased/commands/login.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,18 @@
import json
import os
import random
import stat
import tempfile
import time
from datetime import datetime, timedelta

import click
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress
from rich.prompt import Prompt

from cased.utils.auth import CONFIG_DIR, TOKEN_FILE
from cased.commands.resources import projects
from cased.utils.api import validate_tokens
from cased.utils.auth import validate_credentials
from cased.utils.config import delete_config, save_config
from cased.utils.constants import CasedConstants

console = Console()


def ensure_config_dir():
# Create dir with restricted permissions if it doesn't exist
os.makedirs(CONFIG_DIR, mode=0o700, exist_ok=True)


def secure_write(file_path, data):
ensure_config_dir()
# Write data to a file with restricted permissions
with open(file_path, "w") as f:
json.dump(data, f)
os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR) # Read/write only for the owner


def create_temp_token_file(token_data):
# Create a temporary file that will be automatically deleted after 24 hours
temp_dir = tempfile.gettempdir()
temp_file = tempfile.NamedTemporaryFile(
mode="w+", delete=False, dir=temp_dir, prefix="cased_token_", suffix=".json"
)

with temp_file:
json.dump(token_data, temp_file)

os.chmod(
temp_file.name, stat.S_IRUSR | stat.S_IWUSR
) # Read/write only for the owner

# Schedule file for deletion after 24 hours
deletion_time = datetime.now() + timedelta(hours=24)
deletion_command = f"(sleep {(deletion_time - datetime.now()).total_seconds()} && rm -f {temp_file.name}) &" # noqa: E501
os.system(deletion_command)

return temp_file.name


@click.command()
def login():
"""
Expand All @@ -59,69 +21,78 @@ def login():
This command initiates a login process, stores a session token,
and provides information about the session expiration.
"""
with Progress() as progress:
task1 = progress.add_task("[green]Initiating handshake...", total=100)
task2 = progress.add_task("[yellow]Authenticating...", total=100)

while not progress.finished:
progress.update(task1, advance=100)
time.sleep(0.25)
progress.update(task2, advance=15)

# Simulate getting a session token
session_token = "fake_session_token_" + str(random.randint(1000, 9999))
expiry = datetime.now() + timedelta(hours=24) # Token expires in 1 hour

token_data = {"token": session_token, "expiry": expiry.isoformat()}

# Create a temporary token file
temp_token_file = create_temp_token_file(token_data)
console.print(Panel("Welcome to Cased CLI", style="bold blue"))

# Store the path to the temporary file
secure_write(TOKEN_FILE, {"temp_file": temp_token_file})
org_name = Prompt.ask("Enter your organization name")
api_key = Prompt.ask("Enter your API key", password=True)

# Calculate time until expiration
time_until_expiry = expiry - datetime.now()
hours, remainder = divmod(time_until_expiry.seconds, 3600)
minutes, _ = divmod(remainder, 60)

console.print(
Panel(
f"[green]Login successful![/green]\nSession token stored securely.\nSession expires in {hours} hours {minutes} minutes.", # noqa: E501
title="Login Status",
with Progress() as progress:
task = progress.add_task("[cyan]Validating credentials...", total=100)

# Simulate API call with progress
for i in range(0, 101, 10):
progress.update(task, advance=10)
if i == 50:
response = validate_tokens(api_key, org_name)
progress.update(task, completed=100)

# 200 would mean success,
# 403 would mean validation success but necessary integration is not set up.
# (E.g. Github)
if response.status_code == 200 or response.status_code == 403:
data = response.json()
elif response.status_code == 401:
console.print(
Panel(
f"[bold red]Unauthorized:[/bold red] Invalid API token. Please try again or check your API token at {CasedConstants.BASE_URL}/settings/", # noqa: E501
expand=False,
)
)
)


@click.command()
def logout():
"""
Log out from the Cased system.

This command removes the stored session token and ends the current session.
"""
if os.path.exists(TOKEN_FILE):
with open(TOKEN_FILE, "r") as f:
data = json.load(f)
temp_file = data.get("temp_file")

# Remove the temporary token file if it exists
if temp_file and os.path.exists(temp_file):
os.remove(temp_file)

# Remove the token file in the config directory
os.remove(TOKEN_FILE)

return
elif response.status_code == 404:
console.print(
Panel(
"[green]Logout successful![/green]\nSession token has been removed.",
title="Logout Status",
f"[bold red]Organization not found:[/bold red] Please check your organization name at {CasedConstants.BASE_URL}/settings/", # noqa: E501
expand=False,
)
)
return
else:
click.echo("Sorry, something went wrong. Please try again later.")
return

if data.get("validation"):
org_id = data.get("org_id")
data = {
CasedConstants.CASED_API_AUTH_KEY: api_key,
CasedConstants.CASED_ORG_ID: org_id,
CasedConstants.CASED_ORG_NAME: org_name,
}
save_config(data)
console.print(Panel("[bold green]Login successful![/bold green]", expand=False))
# Ask user to select a project.
ctx = click.get_current_context()
ctx.invoke(projects, details=False)
else:
console.print(
Panel(
"[yellow]No active session found.[/yellow]\nYou are already logged out.", # noqa: E501
title="Logout Status",
f"[bold red]Login failed:[/bold red] {data.get('reason', 'Unknown error')}",
title="Error",
expand=False,
)
)


@click.command()
@validate_credentials
def logout():
"""
Log out from your Cased account.

This command removes all locally stored credentials,
effectively logging you out of the Cased CLI.
"""
delete_config()
console.print(
Panel("[bold green]Logged out successfully![/bold green]", expand=False)
)
Loading
Loading