diff --git a/.github/workflows/pr-lint-filtered.yml b/.github/workflows/pr-lint-filtered.yml new file mode 100644 index 000000000..d3f17fb5b --- /dev/null +++ b/.github/workflows/pr-lint-filtered.yml @@ -0,0 +1,32 @@ +name: PR Lint (only when specific files are changed) + +on: + pull_request: + paths: + - 'app/**' + - 'Dockerfile' + - 'playbook.yml' + - '.github/workflows/**' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install lint dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + + - name: Run flake8 on app + run: | + flake8 app || echo "Lint warnings (non-fatal for now)" + diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 000000000..3e540da18 --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,26 @@ +name: PR Lint + +on: + pull_request: # run on every pull request event targeting this repo + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install lint dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + + - name: Run flake8 on app + run: | + flake8 app || echo "Lint warnings (non-fatal for now)" diff --git a/.gitignore b/.gitignore index 68bc17f9f..c55dc97f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,160 +1,4 @@ -# Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +*.pyc .env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.venv/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..07fd85226 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM ubuntu:22.04 +RUN apt-get update && apt-get install -y python3 python3-pip +WORKDIR /app +COPY app/ /app/ +RUN pip install -r requirements.txt +EXPOSE 5000 +CMD ["python3", "app.py"] diff --git a/M4-Kubernetes/programme-nginx-pod.yaml b/M4-Kubernetes/programme-nginx-pod.yaml new file mode 100644 index 000000000..d322fdc36 --- /dev/null +++ b/M4-Kubernetes/programme-nginx-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + run: programme-nginx-pod + name: programme-nginx-pod +spec: + containers: + - image: nginx:1.27 + name: programme-nginx-pod + resources: {} + dnsPolicy: ClusterFirst + restartPolicy: Never +status: {} diff --git a/README.md b/README.md index d19dfd95a..35861a840 100644 --- a/README.md +++ b/README.md @@ -1 +1,101 @@ -# devops-programme \ No newline at end of file +# Ansible Playbook --- Build & Run Python App (Docker) + +This playbook builds a Docker image for the Python application, removes +any running containers that use the configured host port, and starts the +container with the PORT environment variable that the app requires. + +The playbook uses **Ansible modules only** (`community.docker`) and is +**fully idempotent**. + +## What the playbook does + +1. **Builds** the Docker image using your local `Dockerfile`.\ +2. **Finds and removes** any container currently using the configured + host port.\ +3. **Removes** the previous `python-app` container if present + (idempotent).\ +4. **Runs** the container with: + - the correct environment variable (`PORT`)\ + - port mapping (`host_port:listen_port`)\ + - `restart_policy: unless-stopped` + +## Requirements + +- Python ≥ 3.8\ +- Docker installed and running\ +- Ansible installed\ +- The `community.docker` collection installed + +Install dependencies: + + pip install -r requirements.txt + ansible-galaxy collection install community.docker + +## Variables + + ------------------------------------------------------------------------------------- + Variable Description Default + --------------- ---------------------------------------------- ---------------------- + `image_name` The Docker image name (no tag). `cyrixrr/python-app` + + `image_tag` The tag for the Docker image. `v2` + + `listen_port` The internal port that the Python app listens `8000` + on. + + `host_port` The port on the host that will be published `{{ listen_port }}` + and checked for conflicts. + ------------------------------------------------------------------------------------- + +You can override them when running the playbook: + + ansible-playbook playbook.yml -e image_tag=v3 + ansible-playbook playbook.yml -e host_port=8001 + +## How to run + +From the project root: + + ansible-playbook playbook.yml + +Test the application: + + curl http://localhost:8000/ + +## Idempotency + +This playbook is **fully idempotent**: + +- If no container uses the configured host port, nothing is removed.\ +- If the image has not changed, it will not rebuild.\ +- If the `python-app` container is already running with the correct + settings, no changes occur.\ +- If a different container is occupying the selected host port, it + will be removed automatically. + +## Troubleshooting + +### Port already in use + +This playbook automatically removes Docker containers that use +`host_port`. + +If a **non-Docker** process is using the port: + + sudo lsof -i :8000 -nP + sudo kill -9 + +### Environment variable must be a string + +`PORT` must be passed as a string: + + PORT: "{{ listen_port | string }}" + +## Summary + +This playbook demonstrates: - Use of Ansible variables\ +- Docker image building via `docker_image`\ +- Running containers via `docker_container`\ +- Idempotent cleanup logic\ +- Environment variable injection\ +- Port mapping and collision handling diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 000000000..9d440edf1 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,6 @@ +flask==3.0.0 ; python_version >= "3.10" and python_version < "4.0" +itsdangerous==2.1.2 ; python_version >= "3.10" and python_version < "4.0" +jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0" +markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0" +werkzeug==3.0.0 ; python_version >= "3.10" and python_version < "4.0" + diff --git a/bio.md b/bio.md new file mode 100644 index 000000000..901b0c8b8 --- /dev/null +++ b/bio.md @@ -0,0 +1,4 @@ +# Professional Background + +I have experience in IT support and system administration. +Currently learning DevOps practices such as CI/CD, Docker, and cloud automation. diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 000000000..ef3d01491 --- /dev/null +++ b/playbook.yml @@ -0,0 +1,62 @@ +--- +- name: Build and run Python app container + hosts: localhost + connection: local + + vars: + image_name: "cyrixrr/python-app" + image_tag: "v2" + listen_port: 8000 # Here we set our variables + host_port: "{{ listen_port }}" # New variable – this is the port we check and remove other containers from + + collections: + - community.docker # Here we gave acces to Docker modules + + tasks: + + - name: Build the Docker image + community.docker.docker_image: + name: "{{ image_name }}" + tag: "{{ image_tag }}" + build: + path: "{{ playbook_dir }}" + source: build #This would like this in shell "docker build -t cyrixrr/python-app:v2 . + + # ---------------- NEW BLOCK START ---------------- + - name: Get Docker host info (list of all containers) # New – we gather container info so we can check used ports + community.docker.docker_host_info: + containers: true + register: host_info + + - name: Remove ANY container using the host port # New – this removes any container that is using listen_port + community.docker.docker_container: + name: "{{ (item.Names is string) | ternary(item.Names, (item.Names | first | regex_replace('^/', ''))) }}" + state: absent + loop: "{{ host_info.containers | default([]) }}" + when: > + item.Ports is defined and + ( + item.Ports + | selectattr('PublicPort','defined') + | selectattr('PublicPort','equalto', host_port | int) + | list | length + ) > 0 + # This loop checks every container, and if it is using 8000 on the host, + # we remove it. This makes the playbook idempotent and avoids port conflicts. + # ---------------- NEW BLOCK END ------------------ + + - name: Remove existing container if present + community.docker.docker_container: + name: python-app + state: absent #That will make it idempotent as we spoke in cource + + - name: Run the Docker container + community.docker.docker_container: + name: python-app + image: "{{ image_name }}:{{ image_tag }}" + env: + PORT: "{{ listen_port | string }}" + published_ports: + - "{{ host_port }}:{{ listen_port }}" # Updated to use host_port so we can remove others first + state: started + restart_policy: unless-stopped #All this would like this in shell "docker run -d --name python-app -e PORT=8000 -p 8000:8000 cyrixrr/python-app:v2" diff --git a/requirements.txt b/requirements.txt index 9d440edf1..7b9104f2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ itsdangerous==2.1.2 ; python_version >= "3.10" and python_version < "4.0" jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0" markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0" werkzeug==3.0.0 ; python_version >= "3.10" and python_version < "4.0" - +ansible==9.2.0