-
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.
- Loading branch information
1 parent
c4490fa
commit 42c17a4
Showing
10 changed files
with
515 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# CASED Configuration File | ||
# | ||
# This file contains the configuration for your project's DevOps processes. | ||
# Please read the following instructions carefully before editing this file. | ||
# | ||
# Instructions: | ||
# 1. Required fields are marked with [REQUIRED]. These must be filled out for the tool to function properly. | ||
# 2. Optional fields are marked with [OPTIONAL]. Fill these out if they apply to your project. | ||
# 3. Fields with default values are pre-filled. Modify them as needed. | ||
# 4. Do not change the structure of this file (i.e., don't remove or rename sections). | ||
# 5. Use quotes around string values, especially if they contain special characters. | ||
# 6. For boolean values, use true or false (lowercase, no quotes). | ||
# 7. For lists, maintain the dash (-) format for each item. | ||
# | ||
# Sections: | ||
# - Project Metadata: Basic information about your project. All fields are required. | ||
# - Environment Configuration: Specify your project's runtime environment. Python or Node version is required if applicable. | ||
# - Application Runtime Configuration: Define how your application runs. The entry_point is required. | ||
# - Docker Configuration: Required if you're using Docker. Set enabled to false if not using Docker. | ||
# - Cloud Deployment Configuration: Required if deploying to a cloud provider. | ||
# | ||
# After editing this file, run 'cased build' to generate your GitHub Actions workflow. | ||
# If you need help, refer to the documentation or run 'cased --help'. | ||
|
||
|
||
project: | ||
description: <[OPTIONAL] PROJECT_DESCRIPTION> | ||
name: bh | ||
version: <[OPTIONAL] PROJECT_VERSION> | ||
|
||
environment: | ||
dependency_files: | ||
- <[OPTIONAL] Cased build will smart detect these files if not provided here> | ||
dependency_manager: poetry | ||
framework: Flask | ||
language: Python | ||
python_version: '[REQUIRED] <PYTHON_VERSION>' | ||
|
||
runtime: | ||
commands: | ||
restart: <RESTART_COMMAND> | ||
start: <START_COMMAND> | ||
stop: <STOP_COMMAND> | ||
entry_point: '[REQUIRED]<The path to the file contains your main function>' | ||
flags: | ||
- '[OPTIONAL]' | ||
- <FLAG_A> | ||
- <FLAG_B> | ||
|
||
docker: | ||
enabled: false | ||
|
||
cloud_deployment: | ||
autoscaling: | ||
enabled: true | ||
max_instances: <MAX_INSTANCES> | ||
min_instances: <MIN_INSTANCES> | ||
instance_type: <INSTANCE_TYPE> | ||
load_balancer: | ||
enabled: true | ||
type: <LOAD_BALANCER_TYPE> | ||
provider: AWS | ||
region: <CLOUD_REGION> |
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. Going 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 | ||
" |
Oops, something went wrong.