diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..3b3098110 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,33 @@ +# EditorConfig is awesome: https://editorconfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py}] +charset = utf-8 + +# 4 space indentation +[*.py] +indent_style = space +indent_size = 4 + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab + +# Indentation override for all JS under lib directory +[lib/**.js] +indent_style = space +indent_size = 2 + +# Matches the exact files either package.json or .travis.yml +[{package.json,.travis.yml}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml new file mode 100644 index 000000000..7ffeb2391 --- /dev/null +++ b/.github/workflows/ci-pipeline.yml @@ -0,0 +1,195 @@ +--- +name: GitHub Actions Homework Demo +run-name: ${{ github.actor }} is testing out GitHub Actions +on: + push: + branches: + - github-actions-practice + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v3 + with: + python-version: '3.10' + cache: 'pip' + - name: Install pre-commit + run: | + python -m pip install pre-commit + pre-commit install + - name: Run pre-commit + run: pre-commit run --all-files + + editorconfig-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Check .editorconfig exists + run: | + if [ ! -f .editorconfig ]; then + echo ".editorconfig file not found in root directory" + exit 1 + fi + - name: Install editorconfig-checker + run: | + npm install -g editorconfig-checker + editorconfig-checker + + secrets-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Check for hardcoded secrets + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + markdown-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run markdown lint with npx + run: npx markdownlint-cli . + + code-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + cache: 'pip' + - name: Install linting tools + run: pip install pylint flake8 flask + - name: Lint Python code + run: | + find . -name '*.py' | xargs pylint + flake8 . + + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + cache: 'pip' + - name: Run unit tests + run: | + cd app + python -m unittest discover -v + + docker-build: + needs: + - pre-commit + - editorconfig-check + - secrets-check + - markdown-check + - code-lint + - unit-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build Docker image + uses: docker/build-push-action@v2 + with: + context: . + push: false + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/u34:${{ github.sha }} + ${{ secrets.DOCKERHUB_USERNAME }}/u34:latest + + trivy: + needs: docker-build + runs-on: ubuntu-latest + outputs: + has_critical_vulnerabilities: ${{ steps.trivy.outputs.exit_code }} + steps: + - uses: actions/checkout@v2 + - name: Scan Docker image with Trivy + uses: aquasecurity/trivy-action@master + continue-on-error: true + with: + image-ref: ${{ secrets.DOCKERHUB_USERNAME }}/u34:${{ github.sha }} + format: 'table' + exit-code: '1' + ignore-unfixed: true + severity: 'CRITICAL' + + snyk: + needs: docker-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/python@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + command: test + + docker-compose: + needs: docker-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Docker Compose + run: | + BASEURL="https://github.com/docker/compose/releases/latest/download" + FILENAME="docker-compose-$(uname -s)-$(uname -m)" + sudo curl -L "${BASEURL}/${FILENAME}" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + - name: Build and Run Container with Docker Compose + run: | + docker-compose up -d --build + docker-compose ps + docker-compose logs + + push-to-docker: + needs: + - trivy + - snyk + runs-on: ubuntu-latest + if: ${{ needs.trivy.outputs.has_critical_vulnerabilities == '0' }} + steps: + - uses: actions/checkout@v2 + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Push Docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/u34:${{ github.sha }} + ${{ secrets.DOCKERHUB_USERNAME }}/u34:latest + + sonarcloud: + needs: [push-to-docker] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000..23feb0510 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,6 @@ +{ + "default": true, + "MD013": { + "line_length": 120 + } + } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..af2b535b8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-merge-conflict + - id: check-added-large-files + args: ['--maxkb=500'] + - id: detect-private-key + + - repo: https://github.com/zricethezav/gitleaks + rev: v8.18.1 + hooks: + - id: gitleaks + + - repo: https://github.com/adrienverge/yamllint + rev: v1.33.0 + hooks: + - id: yamllint + args: [--format, parsable, --no-warnings] diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..b6d8b7612 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.8 diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 000000000..280a59718 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,37 @@ +# Contributing to U34 +Thank you for your interest in contributing to U34! We welcome and appreciate all contributions, whether they are bug reports, feature requests, or code changes. + +Before you start, please take a moment to review the following guidelines to ensure a smooth and effective contribution process. + +## Code of Conduct + +By participating in this project, you agree to abide by the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). Please familiarize yourself with the code of conduct and ensure your contributions align with it. + +## How to Contribute + +There are several ways you can contribute to U34: + +1. **Report Bugs**: If you encounter any bugs or issues, please report them by [opening a new issue](https://github.com/u34-georgi-telerik/devops-programme/issues/new/choose) on the project's GitHub repository. Be sure to provide a clear and detailed description of the problem, including steps to reproduce the issue. + +2. **Suggest Features**: Have an idea for a new feature or an improvement to an existing one? [Open a new issue](https://github.com/u34-georgi-telerik/devops-programme/issues/new/choose) and describe your proposal. We'll be happy to discuss it with you. + +3. **Submit Code Changes**: If you'd like to contribute code changes, follow these steps: + - Fork the repository and create a new branch for your changes. + - Make your changes and ensure they align with the project's coding style and guidelines. + - Write tests for your changes, if applicable. + - Commit your changes and push them to your forked repository. + - [Open a pull request](https://github.com/u34-georgi-telerik/devops-programme/pulls) against the main branch of the original repository. + +4. **Provide Feedback**: Even if you don't have a specific bug report or feature request, we welcome any feedback or suggestions you may have about the project. You can [open a new issue](https://github.com/u34-georgi-telerik/devops-programme/issues/new/choose) or reach out to the project maintainers directly. + +## Development Environment Setup + +To set up your development environment, please follow these steps: + +1. Clone the repository: `git clone https://github.com/u34-georgi-telerik/devops-programme.git` +2. Install the required dependencies: `pip install -r requirements.txt` +3. Run the test suite: `pytest` + +If you have any questions or need further assistance, don't hesitate to [open an issue](https://github.com/u34-georgi-telerik/devops-programme/issues/new/choose) or reach out to the project maintainers. + +We look forward to your contributions and appreciate your involvement in making U34 even better! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..8b24cbdce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:22.04 +RUN apt-get update && \ + apt-get install -y \ + python3 -y \ + python3-pip -y && \ + groupadd -g 1234 notroot && \ + useradd -m -u 1234 -g notroot notroot +USER notroot +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY app . +EXPOSE 5000 +CMD ["python3", "app.py"] diff --git a/README.md b/README.md index d19dfd95a..25e0e448e 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# devops-programme \ No newline at end of file +# devops-programme diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 000000000..9e1611817 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,33 @@ +# Configuration Management with Ansible + +Create an Ansible playbook that build, push and then run the Docker image for the Python +application. Let your playbook has the following variables: + +* `image_name` - contains the name of your image without the tag, i.e. `vutoff/python-app` +* `image_tag` - contains the tag you tagged your image with, i.e. `v0.2` +* `listen_port` - contains the listening port you're binding your app to. + +Make sure that you set environment variable `PORT` when you define your container +in the Ansible playbook that takes its value from `listen_port` variable. + +Use Ansible modules. Do not shell out. + +## Requirements + +* Make sure you have Python installed. Any version above 3.8 would suffice. +* The `requirements.txt` file in this directory contains the required Ansible version. Run + +```sh +pip install -r requirements.txt +``` + +* Make sure that Docker is running on your local machine. + +## Mind the following + +* If you're running Docker Desktop, mind the location of the `docker.sock` file. The location of the socket file is + * Docker Desktop - `${HOME}/.docker/run/docker.sock` + +* If you're using one of the above, when you write your Ansible playbook you +must specify the path to the docker socket with the parameter `docker_host`, +i.e. `docker_host: "unix://{{ ansible_env.HOME }}/.rd/docker.sock"`. diff --git a/ansible/playbook.yml b/ansible/playbook.yml new file mode 100644 index 000000000..c2bbbc1d5 --- /dev/null +++ b/ansible/playbook.yml @@ -0,0 +1,76 @@ +- name: Build, push and run Docker container + hosts: localhost + vars: + image_name: "ghristov/practice1" + image_tag: "e934bdd" + listen_port: 5000 + full_image_name: "{{ image_name }}:{{ image_tag }}" + dockerfile_path: "../" + + tasks: + - name: Is Docker Python SDK installed + pip: + name: docker + state: present + become: true + + - name: Is Dockerfile exists in parent dir + stat: + path: "{{ dockerfile_path }}/Dockerfile" + register: dockerfile_check + + - name: Fail if Dockerfile is missing + fail: + msg: "Dockerfile not found" + when: not dockerfile_check.stat.exists + + - name: Build Docker image + community.docker.docker_image: + name: "{{ image_name }}" + tag: "{{ image_tag }}" + source: build + build: + path: "{{ dockerfile_path }}" + pull: yes + force_source: yes + state: present + register: build_result + + - name: Log into Docker registry + community.docker.docker_login: + username: "{{ docker_username }}" + password: "{{ docker_password }}" + when: docker_username is defined and docker_password is defined + register: login_result + + - name: Push Docker image to registry + community.docker.docker_image: + name: "{{ full_image_name }}" + push: yes + source: local + when: + - docker_username is defined + - docker_password is defined + - build_result is succeeded + register: push_result + + - name: If existing container then remove + community.docker.docker_container: + name: ansible-test + state: absent + force_kill: yes + ignore_errors: yes + + - name: Run Docker container + community.docker.docker_container: + name: ansible-test + image: "{{ full_image_name }}" + state: started + recreate: yes + pull: false + ports: + - "{{ listen_port }}:{{ listen_port }}" + env: + PORT: "{{ listen_port | string }}" + restart_policy: unless-stopped + when: build_result is succeeded diff --git a/app/README.md b/app/README.md new file mode 100644 index 000000000..362f6f575 --- /dev/null +++ b/app/README.md @@ -0,0 +1,41 @@ +# Simple Flask Application + +This is a basic Flask application that demonstrates setting up a route and returning a response. + +Getting Started + +Prerequisites: + +Python 3.x installed. +Flask package installed: pip install Flask +Running the application: + +Save the code as app.py. +Open your terminal and navigate to the directory containing app.py. +Run the application: python app.py +This will start the Flask development server, typically accessible at localhost port 5000 by default. + +Explanation: + +The app.py file initializes a Flask application instance using Flask(__name__). +The @app.route("/") decorator defines a route handler for the root path (/). +The hello_world() function is the view function associated with the root route. It returns a simple "Hello, World!" message. +The if __name__ == "__main__": block ensures the application code only runs when executed directly (not imported as a module). +Inside this block, app.run() starts the development server. It listens on all interfaces (0.0.0.0) and uses the port +specified by the environment variable PORT (defaulting to 5000). + +Deployment: + +For production deployment, consider using a WSGI server like Gunicorn. +Refer to the Flask documentation for more details on deployment strategies at +flask.palletsprojects.com project + +Further Development: + +This application serves as a basic example. You can explore Flask's rich features to build more complex web applications: + +Define additional routes for different functionalities. +Use templates for dynamic content generation. +Handle HTTP methods (GET, POST, etc.) for user interaction. +Integrate with databases for persistent data storage. +By building upon this foundation, you can create robust and scalable web applications using Flask. diff --git a/app/app.py b/app/app.py new file mode 100644 index 000000000..94dffc93c --- /dev/null +++ b/app/app.py @@ -0,0 +1,26 @@ +""" +app.py +This module initializes and runs the Flask application. +""" + +import os +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def hello_world(): + """ + Return a simple greeting message. + """ + return "Hello, World!" + + +if __name__ == "__main__": + + # Entry point for the Flask application. + # The app runs on the port specified in the environment variable 'PORT' + # or defaults to 5000 and listens on all interfaces. + + app.run(port=os.environ.get("PORT", 5000), host="0.0.0.0") diff --git a/app/app_test.py b/app/app_test.py new file mode 100644 index 000000000..aad6eb6d4 --- /dev/null +++ b/app/app_test.py @@ -0,0 +1,34 @@ +""" +app_test.py +This module contains unit tests for the Flask application defined in app.py. +""" + +import unittest +from app import app + + +class TestApp(unittest.TestCase): + """ + Unit test case for the Flask application. + """ + + def setUp(self): + """ + Set up a test client for the Flask application. + """ + self.client = app.test_client() + + def test_hello_world(self): + """ + Test the '/' route to ensure it returns the correct response. + """ + response = self.client.get("/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, b"Hello, World!") + + +if __name__ == "__main__": + + # Entry point for running the unit tests. + + unittest.main() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..bd271a1c5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +--- +version: '3.8' +services: + app: + build: . + ports: + - "5000:5000" + volumes: + - .:/app diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..851e9685f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +ansible==10.3.0 +ansible-compat==24.9.1 +ansible-core==2.17.5 +ansible-lint==24.9.2 +blinker==1.6.3 ; python_version >= "3.10" and python_version < "4.0" +click==8.1.7 ; python_version >= "3.10" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" +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" +pre-commit==4.0.1 diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..a7d9262cc --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,22 @@ +sonar.projectKey=u34-georgi-telerik_devops-programme +sonar.organization=u34-georgi-telerik + +# This is the name and version displayed in the SonarCloud UI. +sonar.projectName=devops-programme +sonar.projectVersion=1.0 + +# Path is relative to the sonar-project.properties file +sonar.sources=app +sonar.tests=app + +# Test patterns +sonar.test.inclusions=app/**/*_test.py,app/**/test_*.py + +# Python version +sonar.python.version=3.10 + +# Coverage reports +sonar.python.coverage.reportPaths=app/coverage-reports/coverage.xml + +# Encoding of the source code. Default is default system encoding +sonar.sourceEncoding=UTF-8