commit c5c7b99caa275737f8eba8307d2a0f54b84e0f40 Author: Henrik Jess Nielsen Date: Tue Dec 10 14:45:01 2024 +0100 Initial commit diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 0000000..6c1b572 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,3 @@ +[core] + hooksPath = .githooks + diff --git a/.gitea/installers/renders/README.md b/.gitea/installers/renders/README.md new file mode 100644 index 0000000..5e02b2f --- /dev/null +++ b/.gitea/installers/renders/README.md @@ -0,0 +1 @@ +shiv -c project-installer -o ../../../project-installer . diff --git a/.gitea/installers/renders/__init__.py b/.gitea/installers/renders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.gitea/installers/renders/project_installer.py b/.gitea/installers/renders/project_installer.py new file mode 100644 index 0000000..cc4b1f8 --- /dev/null +++ b/.gitea/installers/renders/project_installer.py @@ -0,0 +1,61 @@ +import os +import random +import pystache +import argparse +import re + +def render_templates(template_dir, project_name): + import pystache # Import after ensuring it's installed + + # Generate a random port number + port = random.randint(8900, 9400) + + # Mustache data (dynamic placeholders) + data = { + "PROJECT_NAME": str(project_name).lower(), + "PORT": port + } + + # Define a regex for detecting [[ ... ]] custom placeholders + pattern = re.compile(r"\[\[(.*?)\]\]") + + # Process all .tmpl files in the directory + for root, _, files in os.walk(template_dir): + for file in files: + if file.endswith(".tmpl"): + tmpl_file = os.path.join(root, file) + output_file = os.path.join(root, file.replace(".tmpl", "")) + + # Read the template file + with open(tmpl_file, 'r') as f: + template_content = f.read() + + # Replace placeholders wrapped with [[ ... ]] + def replace_placeholder(match): + key = match.group(1) + return str(data.get(key, match.group(0))) # Replace only if key exists + + rendered_content = pattern.sub(replace_placeholder, template_content) + + # Write to output file + with open(output_file, 'w') as f: + f.write(rendered_content) + print(f"Generated: {output_file}") + + +def main(): + # Parse arguments + parser = argparse.ArgumentParser(description="Render Mustache templates in a directory.") + parser.add_argument('--project-name', type=str, help='Project name (defaults to current directory name)') + parser.add_argument('--template-dir', type=str, default='.gitea/workflows', help='Template directory') + args = parser.parse_args() + + # Get the project name (current directory name if not provided) + project_name = args.project_name or os.path.basename(os.getcwd()) + + # Render templates + render_templates(args.template_dir, project_name) + +if __name__ == "__main__": + main() + diff --git a/.gitea/installers/renders/setup.py b/.gitea/installers/renders/setup.py new file mode 100644 index 0000000..67e2c1e --- /dev/null +++ b/.gitea/installers/renders/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +setup( + name="project-installer", # Name of your package + version="0.1.0", # Version of your package + description="Project Installer Tool", # Short description + py_modules=["project_installer"], # Module(s) to include (no .py extension) + install_requires=["pystache"], # List of dependencies + entry_points={ + "console_scripts": [ + "project-installer=project_installer:main", # Entry point + ], + }, +) diff --git a/.gitea/workflows/README-nomad-job.md b/.gitea/workflows/README-nomad-job.md new file mode 100644 index 0000000..44ad95d --- /dev/null +++ b/.gitea/workflows/README-nomad-job.md @@ -0,0 +1,88 @@ + +### **Top-Level Job Configuration** +#### `job` +- **Purpose**: Defines the Nomad job. +- **Attributes**: + - `region`: Specifies the Nomad region where the job should run. [Reference: Nomad Regions](https://developer.hashicorp.com/nomad/docs/regions) + - `datacenters`: Lists the datacenters in which the job is eligible to run. [Reference: Job Placement](https://developer.hashicorp.com/nomad/docs/job-specification/datacenters) + - `type`: Defines the job type (`service` in this case). Service jobs are typically long-running. [Reference: Job Types](https://developer.hashicorp.com/nomad/docs/job-specification/job) + +--- + +### **Update Block** +#### `update` +- **Purpose**: Specifies rolling update strategy for service jobs. +- **Attributes**: + - `stagger`: Time between starting updates for each allocation. + - `max_parallel`: Number of allocations to update simultaneously. + - `progress_deadline`: Time to wait for a deployment to progress before failing. +- [Reference: Update Stanza](https://developer.hashicorp.com/nomad/docs/job-specification/update) + +--- + +### **Group Configuration** +#### `group` +- **Purpose**: Defines a group of tasks that should be placed together on the same node. +- **Attributes**: + - `count`: Number of task groups to run. +- [Reference: Task Group](https://developer.hashicorp.com/nomad/docs/job-specification/group) + +--- + +### **Network Configuration** +#### `network` +- **Purpose**: Specifies networking requirements for the task group. +- **Attributes**: + - `port`: Defines a named port mapping for the task group. + - `to`: Specifies the internal port the application listens to within the container. +- [Reference: Network Stanza](https://developer.hashicorp.com/nomad/docs/job-specification/network) + +--- + +### **Consul Integration** +#### `service` +- **Purpose**: Registers the service with Consul for discovery. +- **Attributes**: + - `provider`: Specifies the service discovery provider (`consul`). + - `name`: The name of the service in Consul. + - `port`: References the named port defined in the `network` block. + - `tags`: Metadata tags to annotate the service (e.g., for routing in Traefik). + - `check`: Defines health check parameters for the service. + - `name`: Name of the health check. + - `type`: Health check type (`tcp` in this case). + - `interval`: Frequency of the health check. + - `timeout`: Maximum duration for the health check. +- [Reference: Consul Service Stanza](https://developer.hashicorp.com/nomad/docs/job-specification/service) + +--- + +### **Task Configuration** +#### `task` +- **Purpose**: Defines a single executable unit of work. +- **Attributes**: + - `driver`: Specifies the driver (`docker` in this case). + - `config`: Driver-specific configuration. + - `image`: Docker image for the task. + - `ports`: References the named port defined in the `network` block. + - `env`: Sets environment variables for the task. + - `resources`: Specifies resource requirements for the task. + - `cpu`: CPU allocation in MHz. + - `memory`: Memory allocation in MB. +- [Reference: Task Stanza](https://developer.hashicorp.com/nomad/docs/job-specification/task) + +--- + +### **Dynamic Port Allocation** +#### `${NOMAD_PORT_}` +- **Purpose**: Refers to the dynamically allocated host port mapped to the internal container port. +- **Usage**: This is used in the `env` and `tags` sections to dynamically configure the application and Consul registration. +- [Reference: Port Variables](https://developer.hashicorp.com/nomad/docs/runtime/environment#ports) + +--- + +### **Key Takeaways** +1. This job defines a **service** that runs as a Docker container, listens on a dynamically assigned port, and registers itself in **Consul** for service discovery. +2. The `network` stanza ensures that Nomad assigns a dynamic host port, while the `${NOMAD_PORT_}` variable is used to pass this information to the container and Consul. +3. The `update` stanza ensures smooth rolling updates for the service. + +For further details and advanced configurations, refer to the [Nomad Job Specification Documentation](https://developer.hashicorp.com/nomad/docs/job-specification). \ No newline at end of file diff --git a/.gitea/workflows/main.yml.tmpl b/.gitea/workflows/main.yml.tmpl new file mode 100644 index 0000000..cce4463 --- /dev/null +++ b/.gitea/workflows/main.yml.tmpl @@ -0,0 +1,63 @@ +name: Build, Push, and Deploy to Nomad + +on: + push: + branches: + - main + +jobs: + docker-nomad: + runs-on: self-hosted + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Log in to Container Registry + run: echo ${{ secrets.password }} | docker login registry.i80.dk -u ${{ secrets.username }} --password-stdin + + - name: Build Docker Image + run: | + COMMIT_HASH=$(git rev-parse --short HEAD) + docker build -t registry.i80.dk/gitea/[[PROJECT_NAME]]:latest -t registry.i80.dk/gitea/[[PROJECT_NAME]]:${COMMIT_HASH} . + + + - name: Push Docker Image + run: | + COMMIT_HASH=$(git rev-parse --short HEAD) + echo "registry.i80.dk/gitea/[[PROJECT_NAME]]:latest" + echo "registry.i80.dk/gitea/[[PROJECT_NAME]]:${COMMIT_HASH}" + docker push registry.i80.dk/gitea/[[PROJECT_NAME]]:${COMMIT_HASH} + docker push registry.i80.dk/gitea/[[PROJECT_NAME]]:latest + + + - name: Validate Nomad Job + env: + NOMAD_ADDR: https://nomad.i80.dk + run: nomad job validate .gitea/workflows/nomad-job.hcl + + - name: Stop old deployment + env: + NOMAD_ADDR: https://nomad.i80.dk + run: nomad job stop -purge -no-shutdown-delay [[PROJECT_NAME]] + continue-on-error: true + + + - name: Apply Nomad Job + env: + NOMAD_ADDR: https://nomad.i80.dk + run: nomad job run .gitea/workflows/nomad-job.hcl + + - name: Update Nginx Configuration + run: ssh runner@nomad sudo /opt/nginx_updater/venv/bin/python3 /opt/nginx_updater/nginx_updater.py [[PROJECT_NAME]] + + - name: Update Forwarder Configuration + run: ssh runner@nomad sudo /opt/nginx_updater/venv/bin/python3 /opt/nginx_updater/update_forwarder.py [[PROJECT_NAME]] + + +# - name: Restart Nomad Job +# env: +# NOMAD_ADDR: https://nomad.i80.dk +# run: | +# nomad job stop [[PROJECT_NAME]] +# sleep 5 # Optional: Wait to ensure the old allocation is stopped +# nomad job run .gitea/workflows/nomad-job.hcl diff --git a/.gitea/workflows/nomad-job.hcl.tmpl b/.gitea/workflows/nomad-job.hcl.tmpl new file mode 100644 index 0000000..71756e0 --- /dev/null +++ b/.gitea/workflows/nomad-job.hcl.tmpl @@ -0,0 +1,60 @@ +job "[[PROJECT_NAME]]" { + region = "global" + datacenters = ["dc1"] + type = "service" + + update { + stagger = "60s" + max_parallel = 1 + progress_deadline = "6m" + } + + group "[[PROJECT_NAME]]-group" { + count = 1 + + network { + port "port-app" { + to = [[PORT]] # Internal application port + } + } + + # Register the service with Consul + service { + provider = "consul" + name = "[[PROJECT_NAME]]" + port = "port-app" + + # Traefik-specific tags for routing + tags = [ + "PORT=${NOMAD_PORT_port-app}" + ] + + # Define a health check using TCP + check { + name = "tcp_check" + type = "tcp" + interval = "10s" + timeout = "2s" + } + } + + task "[[PROJECT_NAME]]-task" { + driver = "docker" + + config { + image = "registry.i80.dk/gitea/[[PROJECT_NAME]]:latest" + ports = ["port-app"] + } + + env { + APP_ENV = "production" + PORT = "${NOMAD_PORT_port-app}" + } + + resources { + cpu = 250 + memory = 80 + } + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82fef80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.venv +.venv/* +.ídea +.idea/* +.gitea/**/build*/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc33f2b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Base image with Python 3.11 +FROM python:3.11-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements file to the working directory +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code +COPY . . + +# Expose the port the app runs on (default Flask port) +EXPOSE 5000 + +# Define environment variables +ENV FLASK_APP=app.py +ENV FLASK_RUN_HOST=0.0.0.0 +ENV PORT=5000 +# Command to run the application +#CMD ["flask", "run", "--port", "${PORT}"] +CMD flask run --port ${PORT} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/project-installer b/project-installer new file mode 100755 index 0000000..83e9ac0 Binary files /dev/null and b/project-installer differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29