Skip to content

Commit

Permalink
hello world cased cli
Browse files Browse the repository at this point in the history
  • Loading branch information
tnm committed Dec 16, 2024
0 parents commit 2b1ce68
Show file tree
Hide file tree
Showing 24 changed files with 2,402 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .ci-pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.291
hooks:
- id: ruff
args: [--line-length=100]

- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
16 changes: 16 additions & 0 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: pre-commit

on:
pull_request:
push:
branches: [main]

jobs:
pre-commit:
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v3
- uses: useblacksmith/setup-python@v6
- uses: pre-commit/[email protected]
with:
extra_args: --all-files --config .ci-pre-commit-config.yaml
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# .gitignore
_pycache_/
__pycache__/
.egg-info/
env/
.env/
venv/
.DS_STORE
.pre-commit-config.yaml
.pem
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Cased CLI

## Overview

Cased CLI is a powerful command-line interface tool designed to streamline deployment processes and manage branches efficiently. It provides an intuitive way to interact with the Cased system, offering functionalities such as user authentication, deployment management, and branch oversight.

## Features

- User authentication (login/logout)
- View recent deployments
- Display active branches
- Interactive deployment process with branch and target selection
- Comprehensive help system

## Installation

You can install the Cased CLI tool using pip:

```
pip install cased
```

This will install the latest version of the tool from PyPI.

## Usage

After installation, you can use the CLI tool by running `cased` followed by a command:

```
cased --help
```

### Available Commands:

- `cased login`: Log in to the Cased system
- `cased logout`: Log out from the Cased system
- `cased deployments`: View recent deployments
- `cased branches`: View active branches
- `cased deploy`: Deploy a branch to a target environment

For more details on each command, use:

```
cased COMMAND --help
```

## Development

To set up the Cased CLI for development:

1. Clone the repository:
```
git clone https://github.com/cased/cli.git
cd cli
```

2. Install Poetry (if not already installed):
```
pip install poetry
```

3. Install dependencies:
```
poetry install
```

4. Activate the virtual environment:
```
poetry shell
```

5. Run the CLI in development mode:
```
poetry run cased
```

### Making Changes

1. Make your changes to the codebase.
2. Update tests if necessary.
3. Run tests:
```
poetry run pytest
```
4. Build the package:
```
poetry build
```

### Submitting Changes

1. Create a new branch for your changes:
```
git checkout -b feature/your-feature-name
```
2. Commit your changes:
```
git commit -am "Add your commit message"
```
3. Push to your branch:
```
git push origin feature/your-feature-name
```
4. Create a pull request on GitHub.

## Contact

For any questions or support, please contact [email protected]
17 changes: 17 additions & 0 deletions cased/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.trimAutoWhitespace": true,
"editor.formatOnSave": true,
"editor.formatOnType": true
},
"files.trimTrailingWhitespace": true,
"[html][django-html][handlebars][hbs][mustache][jinja][jinja-html][nj][njk][nunjucks][twig]": {
"editor.defaultFormatter": "monosans.djlint"
}
}
1 change: 1 addition & 0 deletions cased/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
38 changes: 38 additions & 0 deletions cased/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import click

from cased.commands.build import build
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, projects, targets

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


@click.group()
def cli():
"""
Cased CLI for authentication, target setup, and branch deployment.
Use 'cased COMMAND --help' for more information on a specific command.
"""

pass


cli.add_command(deploy)
cli.add_command(init)
cli.add_command(build)
cli.add_command(login)
cli.add_command(logout)
cli.add_command(deployments)
cli.add_command(branches)
cli.add_command(projects)
cli.add_command(targets)


if __name__ == "__main__":
try:
cli()
except Exception as _:
click.echo("\nProcess interrupted. Exiting.")
Empty file added cased/commands/__init__.py
Empty file.
177 changes: 177 additions & 0 deletions cased/commands/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import os
import sys
from pathlib import Path
from typing import Any, Dict, List

import click
import yaml
from jinja2 import Environment, FileSystemLoader
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn

from cased.utils.api import CasedAPI
from cased.utils.constants import CasedConstants
from cased.utils.git import get_repo_name

console = Console()
# Get the directory of the current script
CURRENT_DIR = Path(__file__).resolve().parent
# Go up one level to the 'cased' directory
CASED_DIR = CURRENT_DIR.parent
# Set the path to the templates directory
TEMPLATES_DIR = CASED_DIR / "templates"
CONFIG_PATH = ".cased/config.yaml"


@click.command()
def build() -> None:
"""
Generate a GitHub Actions workflow based on the configuration in .cased/config.yaml.
This command reads the configuration file, validates it, generates a workflow file
in the .github/workflows directory, and sets up necessary secrets.
"""
if not os.path.exists(CONFIG_PATH):
console.print(
"[red]Error: Configuration file not found at .cased/config.yaml[/red]"
)
console.print(
"Please run 'cased init' to generate the configuration file first."
)
sys.exit(1)

config = load_config(CONFIG_PATH)

with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
validate_task = progress.add_task(
"[cyan]Validating configuration...", total=100
)
try:
validate_config(config)
progress.update(
validate_task,
completed=100,
description="[bold green]Configuration validated successfully!",
)
except ValueError as e:
progress.update(validate_task, completed=100)
console.print(f"[red]Configuration validation failed: {str(e)}[/red]")
sys.exit(1)

generate_task = progress.add_task("[cyan]Generating workflow...", total=100)
workflow_content = generate_workflow(config)
save_workflow(workflow_content)
progress.update(
generate_task,
completed=100,
description="[bold green]Workflow generated successfully!",
)

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

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 {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 {CasedConstants.API_BASE_URL}/deployments/ to monitor the deployment status.
""", # noqa: E501
title="Success",
expand=False,
)
)


def load_config(file_path: str) -> Dict[str, Any]:
with open(file_path, "r") as file:
return yaml.safe_load(file)


def generate_workflow(config: Dict[str, Any]) -> str:
env = Environment(loader=FileSystemLoader(TEMPLATES_DIR))

if config["docker"]["enabled"]:
template = env.get_template("docker_ec2_template.yaml")
else:
template = env.get_template("non_docker_ec2_template.yaml")

return template.render(config=config)


def save_workflow(content: str) -> None:
os.makedirs(".github/workflows", exist_ok=True)
with open(".github/workflows/deploy.yaml", "w") as file:
file.write(content)


def extract_secrets_from_workflow(workflow_content: str) -> List[str]:
secrets = []
for line in workflow_content.split("\n"):
if "secrets." in line:
secret = line.split("secrets.")[1].split("}")[0].strip()
if secret not in secrets:
secrets.append(secret)
return secrets


def validate_config(config: Dict[str, Any]) -> None:
# Project validation
if "project" not in config:
raise ValueError("Missing 'project' section in config")
if "name" not in config["project"]:
raise ValueError("Missing 'name' in 'project' section")

# Environment validation
if "environment" not in config:
raise ValueError("Missing 'environment' section in config")
required_env_fields = ["language", "python_version"]
for field in required_env_fields:
if field not in config["environment"]:
raise ValueError(f"Missing '{field}' in 'environment' section")

# Docker validation
if "docker" not in config:
raise ValueError("Missing 'docker' section in config")
if "enabled" not in config["docker"]:
raise ValueError("Missing 'enabled' field in 'docker' section")

if config["docker"]["enabled"]:
required_docker_fields = [
"ECR Repository Name",
"dockerfile_path",
"image_name",
]
for field in required_docker_fields:
if field not in config["docker"]:
raise ValueError(f"Missing '{field}' in 'docker' section")

if not isinstance(config["docker"].get("ports", []), list):
raise ValueError("'ports' in 'docker' section must be a list")

if "environment" in config["docker"] and not isinstance(
config["docker"]["environment"], list
):
raise ValueError("'environment' in 'docker' section must be a list")
else:
# Non-docker validation
if "runtime" not in config:
raise ValueError("Missing 'runtime' section in config for non-docker setup")
required_runtime_fields = ["commands", "entry_point"]
for field in required_runtime_fields:
if field not in config["runtime"]:
raise ValueError(f"Missing '{field}' in 'runtime' section")

required_commands = ["start", "stop", "restart"]
for command in required_commands:
if command not in config["runtime"]["commands"]:
raise ValueError(f"Missing '{command}' in 'runtime.commands' section")
Loading

0 comments on commit 2b1ce68

Please sign in to comment.