-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from cased/rick/cas-1889-cased-build-command-fo…
…r-generating-functional-workflow-files Build command for workflow generation
- Loading branch information
Showing
9 changed files
with
452 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
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 API_BASE_URL, create_secrets | ||
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) | ||
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 {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. | ||
""", # 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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
name: Deploy to EC2 | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
workflow_dispatch: | ||
inputs: | ||
branch: | ||
description: 'Branch to deploy' | ||
required: true | ||
default: 'main' | ||
target_name: | ||
description: 'Target name' | ||
required: true | ||
default: 'prod' | ||
|
||
jobs: | ||
deploy: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v2 | ||
|
||
- name: Configure AWS credentials | ||
uses: aws-actions/configure-aws-credentials@v1 | ||
with: | ||
aws-access-key-id: {% raw %}${{ secrets.AWS_ACCESS_KEY_ID }}{% endraw %} | ||
aws-secret-access-key: {% raw %}${{ secrets.AWS_SECRET_ACCESS_KEY }}{% endraw %} | ||
aws-region: {% raw %}${{ secrets.AWS_REGION }}{% endraw %} | ||
|
||
- name: Login to Amazon ECR | ||
id: login-ecr | ||
uses: aws-actions/amazon-ecr-login@v1 | ||
|
||
- name: Build, tag, and push image to Amazon ECR | ||
env: | ||
ECR_REGISTRY: {% raw %}${{ steps.login-ecr.outputs.registry }}{% endraw %} | ||
ECR_REPOSITORY: {% raw %}${{ secrets.ECR_REPOSITORY }}{% endraw %} | ||
IMAGE_TAG: {% raw %}${{ github.sha }}{% endraw %} | ||
run: | | ||
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG {{ config.docker.dockerfile_path }} | ||
{% if config.docker.build_args %} | ||
{% for arg in config.docker.build_args %} | ||
--build-arg {{ arg }} \ | ||
{% endfor %} | ||
{% endif %} | ||
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG | ||
- name: Deploy to EC2 | ||
env: | ||
PRIVATE_KEY: {% raw %}${{ secrets.EC2_SSH_PRIVATE_KEY }}{% endraw %} | ||
ECR_REGISTRY: {% raw %}${{ steps.login-ecr.outputs.registry }}{% endraw %} | ||
EC2_PUBLIC_IP: {% raw %}${{ secrets.EC2_PUBLIC_IP }}{% endraw %} | ||
ECR_REPOSITORY: {% raw %}${{ secrets.ECR_REPOSITORY }}{% endraw %} | ||
IMAGE_TAG: {% raw %}${{ github.sha }}{% endraw %} | ||
AWS_ACCOUNT_NUMBER: {% raw %}${{ secrets.AWS_ACCOUNT_NUMBER }}{% endraw %} | ||
AWS_ACCESS_KEY_ID: {% raw %}${{ secrets.AWS_ACCESS_KEY_ID }}{% endraw %} | ||
AWS_SECRET_ACCESS_KEY: {% raw %}${{ secrets.AWS_SECRET_ACCESS_KEY }}{% endraw %} | ||
AWS_REGION: {% raw %}${{ secrets.AWS_REGION }}{% endraw %} | ||
run: | | ||
echo "$PRIVATE_KEY" > private_key && chmod 600 private_key | ||
ssh -o StrictHostKeyChecking=no -i private_key $EC2_PUBLIC_IP " | ||
export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID | ||
export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY | ||
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_NUMBER.dkr.ecr.$AWS_REGION.amazonaws.com | ||
docker pull $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG | ||
docker stop {{ config.docker.image_name }} || true | ||
docker rm {{ config.docker.image_name }} || true | ||
docker run -d --name {{ config.docker.image_name }} \ | ||
{% if config.docker.ports %} | ||
{% for port in config.docker.ports %} | ||
-p {{ port }} \ | ||
{% endfor %} | ||
{% endif %} | ||
{% if config.docker.environment %} | ||
{% for env in config.docker.environment %} | ||
-e {{ env }} \ | ||
{% endfor %} | ||
{% endif %} | ||
$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG | ||
" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
name: Deploy to EC2 | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
workflow_dispatch: | ||
inputs: | ||
branch: | ||
description: 'Branch to deploy' | ||
required: true | ||
default: 'main' | ||
target_name: | ||
description: 'Target name' | ||
required: true | ||
default: 'prod' | ||
|
||
jobs: | ||
deploy: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v2 | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: '{{ config.environment.python_version }}' | ||
|
||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install {{ config.environment.dependency_manager }} | ||
{{ config.environment.dependency_manager }} install | ||
- name: Run tests | ||
run: | | ||
# Add your test commands here | ||
- name: Deploy to EC2 | ||
env: | ||
PRIVATE_KEY: {% raw %}${{ secrets.EC2_SSH_PRIVATE_KEY }}{% endraw %} | ||
EC2_PUBLIC_IP: {% raw %}${{ secrets.EC2_PUBLIC_IP }}{% endraw %} | ||
run: | | ||
echo "$PRIVATE_KEY" > private_key && chmod 600 private_key | ||
scp -i private_key -o StrictHostKeyChecking=no -r ./* ec2-user@$EC2_PUBLIC_IP:~/app | ||
ssh -i private_key -o StrictHostKeyChecking=no ec2-user@$EC2_PUBLIC_IP ' | ||
cd ~/app | ||
{{ config.runtime.commands.stop }} | ||
{{ config.runtime.commands.start }} | ||
' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import subprocess | ||
|
||
from rich.console import Console | ||
|
||
console = Console() | ||
|
||
|
||
def get_repo_name() -> str: | ||
try: | ||
result = subprocess.run( | ||
["git", "config", "--get", "remote.origin.url"], | ||
capture_output=True, | ||
text=True, | ||
check=True, | ||
) | ||
repo_url = result.stdout.strip() | ||
return repo_url.split("/")[-1].replace(".git", "") | ||
except subprocess.CalledProcessError: | ||
console.print( | ||
"[bold yellow]Warning: Unable to get repository name from git. Using 'unknown' as project name.[/bold yellow]" # noqa: E501 | ||
) | ||
return "unknown" |
Oops, something went wrong.