├── .github └── workflows │ ├── CI_CD_App.yml │ ├── CI_CD_Helm.yml │ └── CONTR_greetings.yml ├── .gitignore ├── .pylintrc ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── app ├── .version ├── Dockerfile ├── __init__.py ├── app.py ├── requirements.txt ├── tests │ ├── test_dockerapi.py │ └── test_helpers.py └── utils │ ├── __init__.py │ ├── config.py │ ├── dockerapi.py │ ├── general.py │ ├── helpers.py │ ├── kubernetes.py │ ├── model │ └── image.py │ └── persistence.py ├── artifacthub-repo.yml ├── asset ├── .$urunner.drawio.bkp ├── logo.png ├── urunner-aws.png ├── urunner-do.png ├── urunner-gitlab.png ├── urunner.drawio └── urunner.png └── helm └── urunner ├── .helmignore ├── Chart.yaml ├── README.md ├── index.yaml ├── templates ├── NOTES.txt ├── _helpers.tpl ├── configmap.yaml ├── deployment.yaml ├── secret.yaml └── serviceaccount.yaml └── values.yaml /.github/workflows/CI_CD_App.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # GitHub recommends pinning actions to a commit SHA. 7 | # To get a newer version, you will need to update the SHA. 8 | # You can also reference a tag or branch, but the action may change without warning. 9 | 10 | name: "[CI/CD] App" 11 | 12 | on: 13 | push: 14 | branches: ['main'] 15 | paths: 16 | - 'app/**' 17 | pull_request: 18 | paths: 19 | - 'app/**' 20 | 21 | env: 22 | REGISTRY: ghcr.io 23 | IMAGE_NAME: texano00/urunner 24 | 25 | jobs: 26 | build-and-push-image: 27 | if: ${{ !contains(github.event.head_commit.message, '#skip-actions') }} 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: read 31 | packages: write 32 | defaults: 33 | run: 34 | working-directory: app 35 | environment: production 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | - name: Set up Python 42 | uses: actions/setup-python@v4 43 | with: 44 | python-version: '3.x' 45 | cache: 'pip' 46 | 47 | - name: Install dependencies 48 | run: | 49 | python -m pip install --upgrade pip 50 | pip install -r requirements.txt 51 | 52 | - name: Lint 53 | run: | 54 | pylint $(git ls-files '*.py' | grep -v 'test_') --fail-under=10 55 | 56 | - name: Tests 57 | run: | 58 | python -m unittest discover -s tests 59 | 60 | - name: Get version 61 | id: metadata 62 | run: | 63 | version=$(cat .version) 64 | echo "version=${version}" >> $GITHUB_OUTPUT 65 | 66 | - name: Set up QEMU 67 | uses: docker/setup-qemu-action@v3 68 | 69 | - name: Set up Docker Buildx 70 | uses: docker/setup-buildx-action@v3 71 | 72 | - name: Login to container registry 73 | uses: docker/login-action@v3 74 | with: 75 | username: ${{ secrets.GHCR_USERNAME }} 76 | password: ${{ secrets.GHCR_PASSWORD }} 77 | registry: ${{ env.REGISTRY }} 78 | 79 | - name: Check if version already exists 80 | id: build_and_push 81 | run: | 82 | built_image="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.metadata.outputs.version }}" 83 | 84 | if docker manifest inspect ${built_image} > /dev/null; then 85 | echo "already exist -> do not push" 86 | exit 1 87 | else 88 | echo "does not exist -> going on" 89 | fi 90 | 91 | - name: Build and Push Image 92 | if: ${{ github.event_name != 'pull_request' }} 93 | uses: docker/build-push-action@v6 94 | with: 95 | context: app 96 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.metadata.outputs.version }} 97 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v6,linux/arm/v7 98 | push: true 99 | 100 | -------------------------------------------------------------------------------- /.github/workflows/CI_CD_Helm.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # GitHub recommends pinning actions to a commit SHA. 7 | # To get a newer version, you will need to update the SHA. 8 | # You can also reference a tag or branch, but the action may change without warning. 9 | 10 | name: "[CI/CD] Helm" 11 | 12 | on: 13 | push: 14 | branches: ['main'] 15 | paths: 16 | - 'helm/**' 17 | env: 18 | REGISTRY: ghcr.io 19 | OCI_CHART_NAME: texano00/urunner/helm 20 | 21 | jobs: 22 | build-and-push-image: 23 | if: ${{ !contains(github.event.head_commit.message, '#skip-actions') }} 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | packages: write 28 | defaults: 29 | run: 30 | working-directory: helm/urunner 31 | environment: production 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v3 36 | 37 | - name: Helm lint 38 | run: | 39 | helm lint 40 | 41 | - name: Helm lint 42 | run: | 43 | helm lint 44 | 45 | - name: Helm pack and push 46 | run: | 47 | helm package . 48 | helm registry login ${{ env.REGISTRY }} -u ${{ secrets.GHCR_USERNAME }} -p ${{ secrets.GHCR_PASSWORD }} 49 | helm push *.tgz oci://${{ env.REGISTRY }}/${{ env.OCI_CHART_NAME }} 50 | -------------------------------------------------------------------------------- /.github/workflows/CONTR_greetings.yml: -------------------------------------------------------------------------------- 1 | name: "[Support] Greetings" 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: "Thanks for your first issue on Urunner!" 16 | pr-message: "Thanks for your first PR on Urunner!" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | myenv 113 | .myenv 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | *.db 133 | docker/test.py 134 | 135 | # General files for the project 136 | pkg/* 137 | *.pyc 138 | bin/* 139 | .project 140 | /.bin 141 | /_test/secrets/*.json 142 | 143 | # OSX leaves these everywhere on SMB shares 144 | ._* 145 | 146 | # OSX trash 147 | .DS_Store 148 | 149 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 150 | .idea/ 151 | *.iml 152 | 153 | # Vscode files 154 | .vscode 155 | 156 | # Emacs save files 157 | *~ 158 | \#*\# 159 | .\#* 160 | 161 | # Vim-related files 162 | [._]*.s[a-w][a-z] 163 | [._]s[a-w][a-z] 164 | *.un~ 165 | Session.vim 166 | .netrwhist 167 | 168 | # Chart dependencies 169 | **/charts/*.tgz 170 | 171 | .history 172 | helm/urunner/test-values-*.yaml 173 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | init-hook='import sys; sys.path.append(".")' 3 | max-line-length=120 4 | 5 | [MESSAGES CONTROL] 6 | 7 | # Enable the message, report, category or checker with the given id(s). You can 8 | # either give multiple identifier separated by comma (,) or put this option 9 | # multiple time. 10 | #enable= 11 | 12 | # Disable the message, report, category or checker with the given id(s). You 13 | # can either give multiple identifier separated by comma (,) or put this option 14 | # multiple time (only on the command line, not in the configuration file where 15 | # it should appear only once). 16 | disable=R0402,E0401 17 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-python.pylint" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true, 3 | "python.linting.enabled": true, 4 | "editor.formatOnSave": true, 5 | "editor.renderWhitespace": "all", 6 | "files.autoSave": "afterDelay", 7 | "python.analysis.typeCheckingMode": "basic", 8 | "python.formatting.provider": "black", 9 | "python.formatting.blackArgs": [ 10 | "--line-length=120" 11 | ], 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # urunner 2 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/urunner)](https://artifacthub.io/packages/search?repo=urunner) 3 | ![CI_CD_Helm](https://github.com/texano00/urunner/actions/workflows/CI_CD_Helm.yml/badge.svg) 4 | ![CI_CD_App](https://github.com/texano00/urunner/actions/workflows/CI_CD_App.yml/badge.svg) 5 | 6 | ## Star History 7 | 8 | [![Star History Chart](https://api.star-history.com/svg?repos=texano00/urunner&type=Date)](https://star-history.com/#texano00/urunner&Date) 9 | 10 | ## Intro 11 | 12 | 13 | 14 | URunner is a lightweight **Kubernetes** utility in order to **auto restart** pods on image **tag digest change**.\ 15 | This is very useful on environments where it is commonly used the `latest` tag which frequently changes over time.\ 16 | Urunner auto detects the container image tag digest (for example the digest of tag `latest`) and automatically restart pods. 17 | 18 | ## Docker API V2 19 | 20 | Urunner integrates external container registry (ex. [Harbor](https://goharbor.io/)) using standard [Docker API V2](https://docs.docker.com/registry/spec/api/).\ 21 | Actually **Harbor**, **AWS ECR**, **Digital Ocean** and **GitLab** are the container registries officially supported.\ 22 | **Azure ACR** and **Dockerhub** support will be released soon. 23 | 24 | URunner use cases with specific how-to: 25 | 26 | - [AWS use case](https://www.yuribacciarini.com/automatically-pull-images-on-aws-ecr-latest-tag-change-from-aws-eks/) 27 | - [DigitalOcean container registry DOCR use case](https://www.yuribacciarini.com/automatically-pull-new-digitalocean-container-registry-docr-latest-tags-from-kubernetes/) 28 | - [GitLab container registry use case](https://www.yuribacciarini.com/k8s-automatically-pull-images-from-gitlab-container-registry-without-change-the-tag/) 29 | ## Configurable watcher 30 | 31 | Urunner is also **fully configurable** in order to **watch only specific namespaces** with specific label to manage exception.\ 32 | Add label `urunner=enable` to all namespaces in order to be watched by Urunner.\ 33 | `kubectl label ns mynamespace urunner=enable` 34 | 35 | ``` 36 | apiVersion: v1 37 | kind: Namespace 38 | metadata: 39 | labels: 40 | # add this label 41 | urunner: enable 42 | name: mynamespace 43 | ``` 44 | 45 | Also, you can add exceptions inside `mynamespace`, for example\ 46 | `kubectl label deployment mydeployment urunner=disable -n mynamespace` 47 | 48 | ``` 49 | apiVersion: apps/v1 50 | kind: Deployment 51 | metadata: 52 | labels: 53 | # add this label 54 | urunner: disable 55 | ... 56 | ``` 57 | 58 | Doing so, all deployments except `mydeployment` will be watched by Urunner. 59 | 60 | ## Helm 61 | 62 | ``` 63 | helm upgrade --install urunner oci://ghcr.io/texano00/urunner/helm/urunner --version 0.1.0 --values my-values.yaml -n urunner --create-namespace 64 | ``` 65 | 66 | ## Urunner env vars 67 | 68 | | Var | Description | Example | 69 | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | 70 | | DOCKER_API_ACCEPT_HEADER | Accept header to inject on Docker API v2 registry | application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json | 71 | | URUNNER_CONF_DOCKER_API_VERIFY | SSL verify to docker registry | True or False | 72 | | URUNNER_CONF_LOG_LEVEL | Log Level | DEBUG,INFO,WARNING | 73 | | URUNNER_CONF_KUBE_AUTH | Kubernetes client authentication strategy | incluster or kubeconfig | 74 | | URUNNER_CONF_SQLLIGHT_PATH | Path of sqlight DB | ./urunner.db | 75 | | URUNNER_CONF_FREQUENCY_CHECK_SECONDS | Frequency of urunner cron job (seconds) | 30 | 76 | | URUNNER_CONF_CONTAINER_REGISTRY_TO_WATCH | Which is the container registry to watch | registry.mycompanyhost.net:8080 | 77 | | URUNNER_CONF_CONTAINER_REGISTRY_TYPE | Kind of container registry | harbor,aws_ecr, digitalocean, gitlab | 78 | | URUNNER_SECR_HARBOR_USER | Harbor username, configure only if registry type is harbor | user | 79 | | URUNNER_SECR_HARBOR_PASS | Harbor password, configure only if registry type is harbor | pass | 80 | | URUNNER_SECR_AWS_ACCESS_KEY_ID | AWS credential in order to pull from AWS private ECR, configure only if registry type is aws_ecr | AKIAIOSFODNN7EXAMPLE | 81 | | URUNNER_SECR_AWS_REGION | AWS region | us-east-2 | 82 | | URUNNER_SECR_AWS_SECRET_ACCESS_KEY | AWS credential in order to pull from AWS private ECR, configure only if registry type is aws_ecr | wJalrXUtnFEMI/K7MDENG/xRfiCYEXAMPLEKEY | 83 | | URUNNER_SECR_DIGITAL_OCEAN_TOKEN | Digital Ocean token | xxxxx | 84 | | URUNNER_SECR_GITLAB_TOKEN | Gitlab token | xxxxx | 85 | | URUNNER_SECR_GITLAB_AUTH_URL | Gitlab URL where `/jwt/auth` API is exposed. This is usually the same URL where GitLab instance itself is exposed. | my-gitlab.com | 86 | 87 | ## Flow 88 | 89 | ### Generic 90 | 91 | 92 | 93 | ### AWS 94 | 95 | 96 | 97 | ### DigitalOCean 98 | 99 | 100 | ### GitLab 101 | 102 | 103 | 104 | ## ToDo 105 | 106 | - Test Azure ACR integration 107 | - manage sqlite persistence in Helm chart 108 | 109 | ## Notes 110 | 111 | Logo was generated using Fotor AI tool https://www.fotor.com/features/ai-image-generator/ 112 | -------------------------------------------------------------------------------- /app/.version: -------------------------------------------------------------------------------- 1 | 0.6.0 -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.17 2 | 3 | # This hack is widely applied to avoid python printing issues in docker containers. 4 | # See: https://github.com/Docker-Hub-frolvlad/docker-alpine-python3/pull/13 5 | ENV PYTHONUNBUFFERED=1 6 | 7 | RUN echo "**** install Python ****" && \ 8 | apk add --no-cache python3 && \ 9 | if [ ! -e /usr/bin/python ]; then ln -sf python3 /usr/bin/python ; fi && \ 10 | \ 11 | echo "**** install pip ****" && \ 12 | python3 -m ensurepip && \ 13 | rm -r /usr/lib/python*/ensurepip && \ 14 | pip3 install --no-cache --upgrade pip setuptools wheel && \ 15 | if [ ! -e /usr/bin/pip ]; then ln -s pip3 /usr/bin/pip ; fi 16 | 17 | ENV BASE_DIR=/app 18 | WORKDIR ${BASE_DIR} 19 | ADD requirements.txt $BASE_DIR 20 | RUN pip3 install -r requirements.txt 21 | 22 | ADD . $BASE_DIR 23 | CMD python -u app.py -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/texano00/urunner/6962449ce06b5fd4f332523a4ec2eef23d872d87/app/__init__.py -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | """Entrypoint module.""" 2 | import logging 3 | import time 4 | import schedule 5 | from app.utils.helpers import explode_image 6 | import utils.persistence as persistence 7 | import utils.kubernetes as k8s 8 | import utils.config as config 9 | import utils.general as general 10 | from utils.model.image import Image 11 | 12 | 13 | def job(): 14 | """Recurrent urunner Job""" 15 | logging.debug("%s start scheduler", general.get_date_time()) 16 | all_namespaces = kubernetes.get_namespaces() 17 | for namespace in all_namespaces.items: 18 | namespace = namespace.metadata.name 19 | 20 | logging.debug("Listing deployments in namespace %s", namespace) 21 | deployments = kubernetes.get_deployments(namespace=namespace) 22 | for deployment in deployments.items: 23 | logging.debug("Name: %s", deployment.metadata.name) 24 | for container in deployment.spec.template.spec.containers: 25 | image = Image( 26 | image_id="", 27 | namespace=namespace, 28 | resource=deployment.metadata.name, 29 | tag="", 30 | tag_digest="", 31 | image=container.image, 32 | ) 33 | 34 | image_tag = explode_image(image)[1] 35 | image.tag = image_tag 36 | image_id = f"{namespace}-{deployment.metadata.name}-{container.image}-{image_tag}" 37 | image.image_id = image_id 38 | logging.debug("Image: %s", image) 39 | 40 | general.process_resource(db_ref=db_ref, kubernetes=kubernetes, image=image) 41 | 42 | 43 | logging.basicConfig(encoding="utf-8", level=config.get_urunner_conf_log_level()) 44 | 45 | db_ref = persistence.Persistence(path=config.get_urunner_conf_sqlight_path()) 46 | db_ref.init() 47 | 48 | kubernetes = k8s.Kubernetes() 49 | schedule.every(config.get_urunner_conf_frequency_check_seconds()).seconds.do(job) 50 | 51 | while 1: 52 | schedule.run_pending() 53 | time.sleep(1) 54 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | kubernetes===26.1.0 2 | requests===2.28.2 3 | schedule===1.1.0 4 | # pytest===7.2.2 5 | # flake8===6.0.0 6 | pylint===2.16.3 7 | boto3===1.26.87 -------------------------------------------------------------------------------- /app/tests/test_dockerapi.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | from utils.model.image import Image 4 | from utils.dockerapi import get_dockerapi_digest 5 | 6 | 7 | class TestDockerAPIDigest(unittest.TestCase): 8 | 9 | @patch("utils.dockerapi.requests.get") 10 | @patch("utils.dockerapi.config.get_urunner_conf_docker_api_accept_header") 11 | @patch("utils.dockerapi.config.get_urunner_conf_docker_api_verify") 12 | @patch("utils.helpers.explode_image") 13 | @patch("utils.dockerapi.get_dockerapi_image_path") 14 | def test_get_dockerapi_digest_success(self, mock_get_image_path, mock_explode_image, mock_verify, mock_accept_header, mock_requests_get): 15 | # Mock configuration values 16 | mock_accept_header.return_value = "application/vnd.docker.distribution.manifest.v2+json" 17 | mock_verify.return_value = True 18 | 19 | # Mock explode_image 20 | mock_explode_image.return_value = ("test-image", "latest") 21 | 22 | # Mock get_dockerapi_image_path 23 | mock_get_image_path.return_value = "library/test-image" 24 | 25 | # Mock requests.get response 26 | mock_response = MagicMock() 27 | mock_response.status_code = 200 28 | mock_response.headers = {"docker-content-digest": "sha256:testdigest"} 29 | mock_requests_get.return_value = mock_response 30 | 31 | # Create a test image 32 | test_image = Image( 33 | image_id="test-id", 34 | namespace="test-namespace", 35 | resource="test-resource", 36 | image="test-image:latest", 37 | tag="latest", 38 | tag_digest="" 39 | ) 40 | 41 | # Call the function 42 | digest = get_dockerapi_digest( 43 | image=test_image, 44 | authorization="Bearer test-token", 45 | host="https://test-registry.com", 46 | container_registry_type="dockerhub" 47 | ) 48 | 49 | # Assertions 50 | self.assertEqual(digest, "sha256:testdigest") 51 | mock_requests_get.assert_called_once_with( 52 | "https://test-registry.com/v2/library/test-image/manifests/latest", 53 | headers={ 54 | "Authorization": "Bearer test-token", 55 | "Accept": "application/vnd.docker.distribution.manifest.v2+json", 56 | }, 57 | verify=True, 58 | timeout=60 59 | ) 60 | 61 | @patch("utils.dockerapi.requests.get") 62 | @patch("utils.dockerapi.config.get_urunner_conf_docker_api_accept_header") 63 | @patch("utils.dockerapi.config.get_urunner_conf_docker_api_verify") 64 | @patch("utils.helpers.explode_image") 65 | @patch("utils.dockerapi.get_dockerapi_image_path") 66 | def test_get_dockerapi_digest_failure(self, mock_get_image_path, mock_explode_image, mock_verify, mock_accept_header, mock_requests_get): 67 | # Mock configuration values 68 | mock_accept_header.return_value = "application/vnd.docker.distribution.manifest.v2+json" 69 | mock_verify.return_value = True 70 | 71 | # Mock explode_image 72 | mock_explode_image.return_value = ("test-image", "latest") 73 | 74 | # Mock get_dockerapi_image_path 75 | mock_get_image_path.return_value = "library/test-image" 76 | 77 | # Mock requests.get response 78 | mock_response = MagicMock() 79 | mock_response.status_code = 404 80 | mock_requests_get.return_value = mock_response 81 | 82 | # Create a test image 83 | test_image = Image( 84 | image_id="test-id", 85 | namespace="test-namespace", 86 | resource="test-resource", 87 | image="test-image:latest", 88 | tag="latest", 89 | tag_digest="" 90 | ) 91 | 92 | # Call the function 93 | digest = get_dockerapi_digest( 94 | image=test_image, 95 | authorization="Bearer test-token", 96 | host="https://test-registry.com", 97 | container_registry_type="dockerhub" 98 | ) 99 | 100 | # Assertions 101 | self.assertIsNone(digest) 102 | mock_requests_get.assert_called_once_with( 103 | "https://test-registry.com/v2/library/test-image/manifests/latest", 104 | headers={ 105 | "Authorization": "Bearer test-token", 106 | "Accept": "application/vnd.docker.distribution.manifest.v2+json", 107 | }, 108 | verify=True, 109 | timeout=60 110 | ) 111 | 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /app/tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from utils.model.image import Image 3 | from utils.helpers import explode_image 4 | 5 | 6 | class TestHelpers(unittest.TestCase): 7 | 8 | def test_explode_image_with_tag(self): 9 | image = Image( 10 | image_id="", 11 | namespace="", 12 | resource="", 13 | image="registry.harbor/image:latest-latest", 14 | tag="", 15 | tag_digest="" 16 | ) 17 | image_name, tag = explode_image(image) 18 | self.assertEqual(image_name, "registry.harbor/image") 19 | self.assertEqual(tag, "latest-latest") 20 | 21 | def test_explode_image_without_tag(self): 22 | image = Image( 23 | image_id="", 24 | namespace="", 25 | resource="", 26 | image="registry.harbor/image", 27 | tag="", 28 | tag_digest="" 29 | ) 30 | image_name, tag = explode_image(image) 31 | self.assertEqual(image_name, "registry.harbor/image") 32 | self.assertEqual(tag, "latest") 33 | 34 | def test_explode_image_with_default_registry(self): 35 | image = Image( 36 | image_id="", 37 | namespace="", 38 | resource="", 39 | image="nginx:latest", 40 | tag="", 41 | tag_digest="" 42 | ) 43 | image_name, tag = explode_image(image) 44 | self.assertEqual(image_name, "nginx") 45 | self.assertEqual(tag, "latest") 46 | 47 | def test_explode_image_without_tag_default_registry(self): 48 | image = Image( 49 | image_id="", 50 | namespace="", 51 | resource="", 52 | image="nginx", 53 | tag="", 54 | tag_digest="" 55 | ) 56 | image_name, tag = explode_image(image) 57 | self.assertEqual(image_name, "nginx") 58 | self.assertEqual(tag, "latest") 59 | 60 | def test_explode_image_with_complex_tag(self): 61 | image = Image( 62 | image_id="", 63 | namespace="", 64 | resource="", 65 | image="registry.harbor/mapineq/mapineqfrontend:latest-latest", 66 | tag="", 67 | tag_digest="" 68 | ) 69 | image_name, tag = explode_image(image) 70 | self.assertEqual(image_name, "registry.harbor/mapineq/mapineqfrontend") 71 | self.assertEqual(tag, "latest-latest") 72 | 73 | def test_explode_image_with_port(self): 74 | image = Image( 75 | image_id="", 76 | namespace="", 77 | resource="", 78 | image="harbor:8080/image:latest", 79 | tag="", 80 | tag_digest="" 81 | ) 82 | image_name, tag = explode_image(image) 83 | self.assertEqual(image_name, "harbor:8080/image") 84 | self.assertEqual(tag, "latest") 85 | 86 | def test_explode_image_with_registry_and_tag(self): 87 | image = Image( 88 | image_id="", 89 | namespace="", 90 | resource="", 91 | image="435734619587.dkr.ecr.us-east-2.amazonaws.com/urunner-test/nginx:latest", 92 | tag="", 93 | tag_digest="" 94 | ) 95 | image_name, tag = explode_image(image) 96 | self.assertEqual(image_name, "435734619587.dkr.ecr.us-east-2.amazonaws.com/urunner-test/nginx") 97 | self.assertEqual(tag, "latest") 98 | 99 | def test_explode_image_with_registry_without_tag(self): 100 | image = Image( 101 | image_id="", 102 | namespace="", 103 | resource="", 104 | image="435734619587.dkr.ecr.us-east-2.amazonaws.com/urunner-test/nginx", 105 | tag="", 106 | tag_digest="" 107 | ) 108 | image_name, tag = explode_image(image) 109 | self.assertEqual(image_name, "435734619587.dkr.ecr.us-east-2.amazonaws.com/urunner-test/nginx") 110 | self.assertEqual(tag, "latest") 111 | 112 | def test_explode_one_image_layer_with_registry_and_tag(self): 113 | image = Image( 114 | image_id="", 115 | namespace="", 116 | resource="", 117 | image="435734619587.dkr.ecr.us-east-2.amazonaws.com/nginx:latest", 118 | tag="", 119 | tag_digest="" 120 | ) 121 | image_name, tag = explode_image(image) 122 | self.assertEqual(image_name, "435734619587.dkr.ecr.us-east-2.amazonaws.com/nginx") 123 | self.assertEqual(tag, "latest") 124 | 125 | if __name__ == '__main__': 126 | unittest.main() 127 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/texano00/urunner/6962449ce06b5fd4f332523a4ec2eef23d872d87/app/utils/__init__.py -------------------------------------------------------------------------------- /app/utils/config.py: -------------------------------------------------------------------------------- 1 | """config module""" 2 | import os 3 | from ast import literal_eval 4 | 5 | 6 | def get_urunner_conf_docker_api_verify(): 7 | """get_urunner_conf_docker_api_verify""" 8 | return literal_eval(os.environ.get("URUNNER_CONF_DOCKER_API_VERIFY", "True").capitalize()) 9 | 10 | 11 | def get_urunner_conf_log_level(): 12 | """get_urunner_conf_log_level""" 13 | return os.environ.get("URUNNER_CONF_LOG_LEVEL", "INFO") 14 | 15 | 16 | def get_urunner_conf_k8s_auth_strategy(): 17 | """get_urunner_conf_k8s_auth_strategy""" 18 | return os.environ.get("URUNNER_CONF_KUBE_AUTH", "incluster") 19 | 20 | 21 | def get_urunner_conf_sqlight_path(): 22 | """get_urunner_conf_sqlight_path""" 23 | return os.environ.get("URUNNER_CONF_SQLLIGHT_PATH", "./urunner.db") 24 | 25 | 26 | def get_urunner_conf_frequency_check_seconds(): 27 | """get_urunner_conf_frequency_check_seconds""" 28 | return int(os.environ.get("URUNNER_CONF_FREQUENCY_CHECK_SECONDS", "30")) 29 | 30 | 31 | def get_urunner_conf_container_registry_to_watch(): 32 | """get_urunner_conf_container_registry_to_watch""" 33 | return os.environ.get("URUNNER_CONF_CONTAINER_REGISTRY_TO_WATCH", "harbor.default.svc.cluster.local:8080") 34 | 35 | 36 | def get_urunner_conf_container_registry_type(): 37 | """get_urunner_conf_container_registry_type""" 38 | return os.environ.get("URUNNER_CONF_CONTAINER_REGISTRY_TYPE", "harbor") 39 | 40 | 41 | def get_urunner_secr_harbor_user(): 42 | """get_urunner_secr_harbor_user""" 43 | return os.environ.get("URUNNER_SECR_HARBOR_USER", "admin") 44 | 45 | 46 | def get_urunner_secr_harbor_pass(): 47 | """get_urunner_secr_harbor_pass""" 48 | return os.environ.get("URUNNER_SECR_HARBOR_PASS", "Harbor12345") 49 | 50 | def get_urunner_secr_aws_region(): 51 | """def get_urunner_secr_aws_region""" 52 | return os.environ.get("URUNNER_SECR_AWS_REGION", "us-east-2") 53 | 54 | 55 | def get_urunner_secr_aws_access_key_id(): 56 | """def get_urunner_secr_aws_access_key_id""" 57 | return os.environ.get("URUNNER_SECR_AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE") 58 | 59 | 60 | def get_urunner_secr_aws_secret_access_key(): 61 | """def get_urunner_secr_aws_secret_access_key""" 62 | return os.environ.get("URUNNER_SECR_AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/xRfiCYEXAMPLEKEY") 63 | 64 | 65 | def get_urunner_secr_digital_ocean_token(): 66 | """def get_urunner_secr_digital_ocean_token""" 67 | return os.environ.get("URUNNER_SECR_DIGITAL_OCEAN_TOKEN", "xxxxx") 68 | 69 | def get_urunner_secr_gitlab_token(): 70 | """def get_urunner_secr_gitlab_token""" 71 | return os.environ.get("URUNNER_SECR_GITLAB_TOKEN", "xxxxx") 72 | 73 | def get_urunner_secr_gitlab_auth_url(): 74 | """def get_urunner_secr_gitlab_auth_url""" 75 | return os.environ.get("URUNNER_SECR_GITLAB_AUTH_URL", "https://gitlab.com") 76 | 77 | def get_urunner_conf_docker_api_accept_header(): 78 | """Get the custom Accept header for Docker API""" 79 | # pylint: disable=line-too-long 80 | return os.environ.get("DOCKER_API_ACCEPT_HEADER","application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json") 81 | -------------------------------------------------------------------------------- /app/utils/dockerapi.py: -------------------------------------------------------------------------------- 1 | """dockerapi module""" 2 | from base64 import b64encode 3 | import logging 4 | import hashlib 5 | import requests 6 | import boto3 7 | from utils.helpers import explode_image # Import from helpers 8 | import utils.config as config 9 | from utils.model.image import Image 10 | 11 | 12 | def get_dockerapi_image_path(image_name): 13 | """get_dockerapi_image_path""" 14 | if "/" in image_name: 15 | return image_name 16 | 17 | return "library/" + image_name 18 | 19 | 20 | def get_harbor_auth(image: Image): 21 | """get_harbor_auth""" 22 | logging.debug(image) 23 | username = config.get_urunner_secr_harbor_user() 24 | password = config.get_urunner_secr_harbor_pass() 25 | auth = f"{username}:{password}".encode("ascii") 26 | return f"Basic {auth}" 27 | 28 | 29 | def get_aws_auth(image: Image): 30 | """get_aws_auth""" 31 | logging.debug(image) 32 | # ex. 435734619587.dkr.ecr.us-east-2.amazonaws.com 33 | region_name = config.get_urunner_conf_container_registry_to_watch().split(".")[3] 34 | registry_id = config.get_urunner_conf_container_registry_to_watch().split(".")[0] 35 | session = boto3.Session( 36 | aws_access_key_id=config.get_urunner_secr_aws_access_key_id(), 37 | aws_secret_access_key=config.get_urunner_secr_aws_secret_access_key(), 38 | region_name=region_name, 39 | ) 40 | 41 | client = session.client("ecr", region_name=region_name) 42 | 43 | response = client.get_authorization_token( 44 | registryIds=[ 45 | registry_id, 46 | ] 47 | ) 48 | token = response["authorizationData"][0]["authorizationToken"] 49 | return f"Basic {token}" 50 | 51 | 52 | def get_dockerhub_auth(image: Image): 53 | """get_dockerhub_auth""" 54 | auth_service = "registry.docker.io" 55 | auth_url = "https://auth.docker.io/token" 56 | return get_docker_v2_api_auth_style(image=image, auth_url=auth_url, auth_service=auth_service) 57 | 58 | 59 | def get_digitalocean_auth(image: Image): 60 | """get_digitalocean_auth""" 61 | auth_service = "registry.digitalocean.com" 62 | auth_url = "https://api.digitalocean.com/v2/registry/auth" 63 | do_token = config.get_urunner_secr_digital_ocean_token() 64 | do_token = b64encode(f"{do_token}:{do_token}".encode("ascii")).decode("ascii") 65 | auth_header = f"Basic {do_token}" 66 | return get_docker_v2_api_auth_style( 67 | image=image, auth_url=auth_url, auth_service=auth_service, auth_header=auth_header 68 | ) 69 | 70 | def get_gitlab_auth(image: Image): 71 | """get_gitlab_auth""" 72 | auth_service = "container_registry" 73 | auth_url = f"{config.get_urunner_secr_gitlab_auth_url()}/jwt/auth" 74 | do_token = config.get_urunner_secr_gitlab_token() 75 | do_token = b64encode(f"{do_token}:{do_token}".encode("ascii")).decode("ascii") 76 | auth_header = f"Basic {do_token}" 77 | return get_docker_v2_api_auth_style( 78 | image=image, auth_url=auth_url, auth_service=auth_service, auth_header=auth_header 79 | ) 80 | 81 | def get_docker_v2_api_auth_style(image: Image, auth_service, auth_url, auth_header=None): 82 | """get_docker_v2_api_auth_style""" 83 | exploded_image = explode_image(image) 84 | image_name = exploded_image[0] 85 | dockerhub_image_path = get_dockerapi_image_path(image_name) 86 | auth_scope = f"repository:{dockerhub_image_path}:pull" 87 | headers = {"Authorization": auth_header} if auth_header else {} 88 | url = f"{auth_url}?service={auth_service}&scope={auth_scope}" 89 | logging.debug(url) 90 | response = requests.get(url, headers=headers, timeout=60) 91 | logging.debug(response) 92 | token = response.json()["token"] 93 | return f"Bearer {token}" 94 | 95 | 96 | def get_dockerhub_host(): 97 | """get_dockerhub_host""" 98 | return "https://registry-1.docker.io" 99 | 100 | 101 | def get_digitalocean_host(): 102 | """get_digitalocean_host""" 103 | return "https://registry.digitalocean.com" 104 | 105 | 106 | def get_configured_host(): 107 | """get_configured_host""" 108 | registry_host = config.get_urunner_conf_container_registry_to_watch() 109 | return f"https://{registry_host}" 110 | 111 | 112 | def get_dockerapi_digest(image: Image, authorization, host, container_registry_type): 113 | """get_dockerapi_digest""" 114 | exploded_image = explode_image(image) 115 | logging.debug("exploded_image: %s", exploded_image) 116 | image_name = exploded_image[0] 117 | image_tag = exploded_image[1] 118 | 119 | dockerhub_image_path = ( 120 | get_dockerapi_image_path(image_name) 121 | if container_registry_type == "dockerhub" 122 | else image_name 123 | ) 124 | 125 | logging.debug("dockerhub_image_path: %s", dockerhub_image_path) 126 | 127 | accept_header = config.get_urunner_conf_docker_api_accept_header() 128 | 129 | headers = { 130 | "Authorization": authorization, 131 | "Accept": accept_header, 132 | } 133 | logging.debug(headers) 134 | url = f"{host}/v2/{dockerhub_image_path}/manifests/{image_tag}" 135 | try: 136 | response = requests.get(url, headers=headers, verify=config.get_urunner_conf_docker_api_verify(), timeout=60) 137 | except requests.exceptions.RequestException as exception: 138 | logging.error(exception) 139 | return None 140 | 141 | logging.debug(response) 142 | if response.status_code == 200: 143 | logging.debug(response.headers) 144 | image_digest = ( 145 | response.headers["docker-content-digest"] 146 | if "docker-content-digest" in response.headers 147 | else hashlib.sha1(f"{response.content}".encode("utf-8")).hexdigest() 148 | ) 149 | logging.debug("image_digest: %s", image_digest) 150 | return image_digest 151 | 152 | logging.error("Error status code: %i", response.status_code) 153 | logging.debug(response) 154 | return None 155 | -------------------------------------------------------------------------------- /app/utils/general.py: -------------------------------------------------------------------------------- 1 | """"General module""" 2 | import datetime 3 | import logging 4 | from utils.dockerapi import ( 5 | get_dockerapi_digest, 6 | get_dockerhub_auth, 7 | get_harbor_auth, 8 | get_aws_auth, 9 | get_digitalocean_auth, 10 | get_gitlab_auth, 11 | get_dockerhub_host, 12 | get_configured_host, 13 | get_digitalocean_host, 14 | ) 15 | from utils.model.image import Image 16 | from utils.kubernetes import Kubernetes 17 | import utils.config as config 18 | import utils.persistence as persistence 19 | 20 | 21 | def get_date_time(): 22 | """get_date_time""" 23 | now = datetime.datetime.utcnow() 24 | return now.strftime("%Y-%m-%d %H:%M:%S") 25 | 26 | 27 | def get_container_registry(image): 28 | """get_container_registry""" 29 | registry_host = config.get_urunner_conf_container_registry_to_watch() 30 | if registry_host in image: 31 | return config.get_urunner_conf_container_registry_type() 32 | 33 | return None 34 | 35 | 36 | def process_resource(db_ref: persistence.Persistence, kubernetes: Kubernetes, image: Image): 37 | """process_resource""" 38 | docker_api_auth_mapper = { 39 | "dockerhub": get_dockerhub_auth, 40 | "harbor": get_harbor_auth, 41 | "aws_ecr": get_aws_auth, 42 | "digitalocean": get_digitalocean_auth, 43 | "gitlab" : get_gitlab_auth 44 | } 45 | 46 | docker_api_host_mapper = { 47 | "dockerhub": get_dockerhub_host, 48 | "harbor": get_configured_host, 49 | "aws_ecr": get_configured_host, 50 | "digitalocean": get_digitalocean_host, 51 | "gitlab": get_configured_host 52 | } 53 | container_registry_type = get_container_registry(image.image) 54 | if container_registry_type is None: 55 | logging.debug("Container registry not recognized") 56 | return 57 | 58 | auth = docker_api_auth_mapper[container_registry_type](image) 59 | docker_api_host = docker_api_host_mapper[container_registry_type]() 60 | tag_digest = get_dockerapi_digest(image, auth, docker_api_host, container_registry_type) 61 | if tag_digest is None: 62 | return 63 | 64 | result = db_ref.get_image_by_id(image.image_id) 65 | image.tag_digest = tag_digest 66 | logging.debug(image) 67 | 68 | if result is None: 69 | logging.debug("DB miss") 70 | db_ref.add_new_image(data=image) 71 | else: 72 | logging.debug("DB hit") 73 | if result["tag_digest"] != image.tag_digest: 74 | db_ref.update_image_digest(image_id=image.image_id, new_tag_digest=image.tag_digest) 75 | kubernetes.restart_deployment(image.namespace, image.resource) 76 | 77 | db_ref.update_image_last_check_data(image_id=image.image_id) 78 | -------------------------------------------------------------------------------- /app/utils/helpers.py: -------------------------------------------------------------------------------- 1 | """utils.helpers""" 2 | import re 3 | from utils.model.image import Image 4 | import utils.config as config 5 | 6 | 7 | def explode_image(image: Image): 8 | """explode_image""" 9 | # Example image formats: 10 | # harbor:8080/image:latest 11 | # harbor:8080/image 12 | # nginx:latest 13 | # nginx 14 | # 435734619587.dkr.ecr.us-east-2.amazonaws.com/urunner-test/nginx:latest 15 | image_name = image.image 16 | registry_to_watch = config.get_urunner_conf_container_registry_to_watch() 17 | image_name = image_name.replace(registry_to_watch, "") 18 | if image_name.startswith("/"): 19 | image_name = image_name[1:] 20 | 21 | # Regular expression to match image name and tag 22 | match = re.match(r'^(.*?)(?::([^:/]+))?$', image_name) 23 | if match: 24 | image_name = match.group(1) 25 | tag = match.group(2) if match.group(2) else "latest" 26 | return (image_name, tag) 27 | 28 | return (image_name, "latest") 29 | -------------------------------------------------------------------------------- /app/utils/kubernetes.py: -------------------------------------------------------------------------------- 1 | """Kubernetes module""" 2 | import datetime 3 | import logging 4 | from kubernetes.client.rest import ApiException 5 | from kubernetes import client, config 6 | import utils.config as urunnerConfig 7 | 8 | 9 | class Kubernetes: 10 | """Kubernetes class""" 11 | 12 | def __init__(self): 13 | auth_strategy_mapper = {"incluster": config.load_incluster_config, "kubeconfig": config.load_kube_config} 14 | auth_strategy_mapper[urunnerConfig.get_urunner_conf_k8s_auth_strategy()]() 15 | self.v1_apps = client.AppsV1Api() 16 | self.v1_core = client.CoreV1Api() 17 | 18 | def get_namespaces(self): 19 | """get_namespaces""" 20 | namespace = self.v1_core.list_namespace(label_selector="urunner=enable") 21 | return namespace 22 | 23 | def get_deployments(self, namespace): 24 | """get_deployments""" 25 | deployments = self.v1_apps.list_namespaced_deployment(namespace=namespace, label_selector="urunner!=disable") 26 | return deployments 27 | 28 | def restart_deployment(self, namespace, deployment): 29 | """restart_deployment""" 30 | logging.info("Restarting %s in ns %s", deployment, namespace) 31 | now = datetime.datetime.utcnow() 32 | now = str(now.isoformat("T") + "Z") 33 | body = {"spec": {"template": {"metadata": {"annotations": {"kubectl.kubernetes.io/restartedAt": now}}}}} 34 | try: 35 | self.v1_apps.patch_namespaced_deployment(deployment, namespace, body, pretty="true") 36 | except ApiException as exception: 37 | logging.error("Exception when calling AppsV1Api->read_namespaced_deployment_status: %s\n", exception) 38 | -------------------------------------------------------------------------------- /app/utils/model/image.py: -------------------------------------------------------------------------------- 1 | """Image model""" 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class Image: 7 | """Image model""" 8 | 9 | image_id: str 10 | namespace: str 11 | resource: str 12 | image: str 13 | tag: str 14 | tag_digest: str 15 | 16 | # def get_image_id(self): 17 | # """image_id""" 18 | # return self.image_id 19 | 20 | # def get_namespace(self): 21 | # """namespace""" 22 | # return self.namespace 23 | 24 | # def get_resource(self): 25 | # """get_resource""" 26 | # return self.resource 27 | 28 | # def get_image(self): 29 | # """get_image""" 30 | # return self.image 31 | 32 | # def get_tag(self): 33 | # """get_tag""" 34 | # return self.tag 35 | 36 | # def get_tag_digest(self): 37 | # """get_tag_digest""" 38 | # return self.tag_digest 39 | -------------------------------------------------------------------------------- /app/utils/persistence.py: -------------------------------------------------------------------------------- 1 | """persistence module""" 2 | import datetime 3 | import sqlite3 4 | from utils.model.image import Image 5 | 6 | 7 | class Persistence: 8 | """persistence class""" 9 | 10 | def __init__(self, path): 11 | self.path = path 12 | self.con = sqlite3.connect(self.path) 13 | self.con.row_factory = self.row_to_dict 14 | self.cur = self.con.cursor() 15 | 16 | def row_to_dict(self, cursor: sqlite3.Cursor, row: sqlite3.Row) -> dict: 17 | """row_to_dict""" 18 | data = {} 19 | for idx, col in enumerate(cursor.description): 20 | data[col[0]] = row[idx] 21 | return data 22 | 23 | def init(self): 24 | """init""" 25 | self.cur.execute( 26 | """ 27 | CREATE TABLE IF NOT EXISTS image 28 | ( 29 | id VARCHAR(700) PRIMARY KEY, 30 | namespace VARCHAR(100), 31 | resource VARCHAR(300), 32 | image VARCHAR(200), 33 | tag VARCHAR(100), 34 | tag_digest VARCHAR(500), 35 | last_check_date DATETIME 36 | ) 37 | """ 38 | ) 39 | self.con.commit() 40 | 41 | def get_image_by_id(self, image_id): 42 | """get_image_by_id""" 43 | result = self.cur.execute(f"SELECT * FROM image WHERE id='{image_id}'") 44 | return result.fetchone() 45 | 46 | def add_new_image(self, data: Image): 47 | """add_new_image""" 48 | 49 | self.cur.execute( 50 | f"""INSERT INTO image (id, namespace,resource, image, tag, tag_digest, last_check_date) 51 | VALUES ( 52 | '{data.image_id}', 53 | '{data.namespace}', 54 | '{data.resource}', 55 | '{data.image}', 56 | '{data.tag}', 57 | '{data.tag_digest}', 58 | '{get_date_time()}');""" 59 | ) 60 | self.con.commit() 61 | 62 | def update_image_digest(self, image_id, new_tag_digest): 63 | """update_image_digest""" 64 | self.cur.execute( 65 | f""" 66 | UPDATE image SET tag_digest='{new_tag_digest}' WHERE id='{image_id}' 67 | """ 68 | ) 69 | self.con.commit() 70 | 71 | def update_image_last_check_data(self, image_id): 72 | """update_image_last_check_data""" 73 | self.cur.execute( 74 | f""" 75 | UPDATE image SET last_check_date='{get_date_time()}' WHERE id='{image_id}' 76 | """ 77 | ) 78 | 79 | self.con.commit() 80 | 81 | 82 | def get_date_time(): 83 | """get_date_time""" 84 | now = datetime.datetime.utcnow() 85 | return now.strftime("%Y-%m-%d %H:%M:%S") 86 | -------------------------------------------------------------------------------- /artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | # Artifact Hub repository metadata file 2 | # 3 | # Some settings like the verified publisher flag or the ignored packages won't 4 | # be applied until the next time the repository is processed. Please keep in 5 | # mind that the repository won't be processed if it has not changed since the 6 | # last time it was processed. Depending on the repository kind, this is checked 7 | # in a different way. For Helm http based repositories, we consider it has 8 | # changed if the `index.yaml` file changes. For git based repositories, it does 9 | # when the hash of the last commit in the branch you set up changes. This does 10 | # NOT apply to ownership claim operations, which are processed immediately. 11 | # 12 | repositoryID: c7bd9b25-b6b6-408e-869c-24af9ea36c32 13 | -------------------------------------------------------------------------------- /asset/.$urunner.drawio.bkp: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /asset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/texano00/urunner/6962449ce06b5fd4f332523a4ec2eef23d872d87/asset/logo.png -------------------------------------------------------------------------------- /asset/urunner-aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/texano00/urunner/6962449ce06b5fd4f332523a4ec2eef23d872d87/asset/urunner-aws.png -------------------------------------------------------------------------------- /asset/urunner-do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/texano00/urunner/6962449ce06b5fd4f332523a4ec2eef23d872d87/asset/urunner-do.png -------------------------------------------------------------------------------- /asset/urunner-gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/texano00/urunner/6962449ce06b5fd4f332523a4ec2eef23d872d87/asset/urunner-gitlab.png -------------------------------------------------------------------------------- /asset/urunner.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | -------------------------------------------------------------------------------- /asset/urunner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/texano00/urunner/6962449ce06b5fd4f332523a4ec2eef23d872d87/asset/urunner.png -------------------------------------------------------------------------------- /helm/urunner/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm/urunner/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: urunner 3 | description: URunner is a lightweight Kubernetes utility in order to auto restart pods on image tag digest change while the tag remains the same (ex. `:latest`). It uses official Docker API v2. 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.7.0 19 | icon: https://raw.githubusercontent.com/texano00/urunner/main/asset/logo.png 20 | 21 | # This is the version number of the application being deployed. This version number should be 22 | # incremented each time you make changes to the application. Versions are not expected to 23 | # follow Semantic Versioning. They should reflect the version the application is using. 24 | # It is recommended to use it with quotes. 25 | appVersion: "0.6.0" 26 | -------------------------------------------------------------------------------- /helm/urunner/README.md: -------------------------------------------------------------------------------- 1 | # Urunner Helm chart 2 | 3 | URunner is a lightweight **Kubernetes** utility in order to **auto restart** pods on image **tag digest change**.\ 4 | This is very useful on environments where it is commonly used the `latest` tag which frequently changes over time.\ 5 | Urunner auto detects the container image tag digest (for example the digest of tag `latest`) and automatically restart pods. 6 | 7 | ## TL;DR 8 | 9 | ```console 10 | helm upgrade --install urunner oci://ghcr.io/texano00/urunner/helm/urunner --version 0.1.0 --values my-values.yaml -n urunner --create-namespace 11 | ``` 12 | 13 | ## Docker API V2 14 | 15 | Urunner integrates external container registry (ex. [Harbor](https://goharbor.io/)) using standard [Docker API V2](https://docs.docker.com/registry/spec/api/).\ 16 | Actually **Harbor** and **AWS ECR** are the container registries officially supported.\ 17 | **Azure ACR** and **Dockerhub** support will be released soon. 18 | 19 | ## Configurable watcher 20 | 21 | Urunner is also **fully configurable** in order to **watch only specific namespaces** with specific label to manage exception.\ 22 | Add label `urunner=enable` to all namespaces in order to be watched by Urunner.\ 23 | `kubectl label ns mynamespace urunner=enable` 24 | 25 | ``` 26 | apiVersion: v1 27 | kind: Namespace 28 | metadata: 29 | labels: 30 | # add this label 31 | urunner: enable 32 | name: mynamespace 33 | ``` 34 | 35 | Also, you can add exceptions inside `mynamespace`, for example\ 36 | `kubectl label deployment mydeployment urunner=disable -n mynamespace` 37 | 38 | ``` 39 | apiVersion: apps/v1 40 | kind: Deployment 41 | metadata: 42 | labels: 43 | # add this label 44 | urunner: disable 45 | ... 46 | ``` 47 | 48 | Doing so, all deployments except `mydeployment` will be watched by Urunner. 49 | -------------------------------------------------------------------------------- /helm/urunner/index.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | entries: {} 3 | generated: "2023-03-05T20:08:15.61211142+01:00" 4 | -------------------------------------------------------------------------------- /helm/urunner/templates/NOTES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/texano00/urunner/6962449ce06b5fd4f332523a4ec2eef23d872d87/helm/urunner/templates/NOTES.txt -------------------------------------------------------------------------------- /helm/urunner/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "urunner.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "urunner.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "urunner.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "urunner.labels" -}} 37 | helm.sh/chart: {{ include "urunner.chart" . }} 38 | {{ include "urunner.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "urunner.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "urunner.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "urunner.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "urunner.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* 65 | Create the name of the cluster role to use 66 | */}} 67 | {{- define "urunner.clusterRoleName" -}} 68 | {{- if .Values.serviceAccount.create }} 69 | {{- default (include "urunner.fullname" .) .Values.serviceAccount.name }} 70 | {{- else }} 71 | {{- default "default" .Values.serviceAccount.name }} 72 | {{- end }} 73 | {{- end }} 74 | -------------------------------------------------------------------------------- /helm/urunner/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ .Values.config.configMapName | quote }} 5 | labels: 6 | {{- include "urunner.labels" . | nindent 4 }} 7 | namespace: {{ .Release.Namespace | quote }} 8 | data: 9 | URUNNER_CONF_DOCKER_API_VERIFY: {{ .Values.config.URUNNER_CONF_DOCKER_API_VERIFY | quote }} 10 | URUNNER_CONF_LOG_LEVEL: {{ .Values.config.URUNNER_CONF_LOG_LEVEL }} 11 | URUNNER_CONF_KUBE_AUTH: {{ .Values.config.URUNNER_CONF_KUBE_AUTH }} 12 | URUNNER_CONF_SQLLIGHT_PATH: {{ .Values.config.URUNNER_CONF_SQLLIGHT_PATH }} 13 | URUNNER_CONF_FREQUENCY_CHECK_SECONDS: {{ .Values.config.URUNNER_CONF_FREQUENCY_CHECK_SECONDS | quote }} 14 | URUNNER_CONF_CONTAINER_REGISTRY_TO_WATCH: {{ .Values.config.URUNNER_CONF_CONTAINER_REGISTRY_TO_WATCH }} 15 | URUNNER_CONF_CONTAINER_REGISTRY_TYPE: {{ .Values.config.URUNNER_CONF_CONTAINER_REGISTRY_TYPE }} 16 | DOCKER_API_ACCEPT_HEADER: {{ .Values.config.DOCKER_API_ACCEPT_HEADER }} -------------------------------------------------------------------------------- /helm/urunner/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "urunner.fullname" . }} 5 | labels: 6 | {{- include "urunner.labels" . | nindent 4 }} 7 | namespace: {{ .Release.Namespace | quote }} 8 | spec: 9 | selector: 10 | matchLabels: 11 | {{- include "urunner.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | annotations: 15 | checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 16 | labels: 17 | {{- include "urunner.selectorLabels" . | nindent 8 }} 18 | spec: 19 | {{- with .Values.imagePullSecrets }} 20 | imagePullSecrets: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | serviceAccountName: {{ include "urunner.serviceAccountName" . }} 24 | securityContext: 25 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 26 | containers: 27 | - name: {{ .Chart.Name }} 28 | envFrom: 29 | - configMapRef: 30 | name: cm-urunner 31 | - secretRef: 32 | name: secret-urunner 33 | securityContext: 34 | {{- toYaml .Values.securityContext | nindent 12 }} 35 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 36 | imagePullPolicy: {{ .Values.image.pullPolicy }} 37 | # livenessProbe: 38 | # httpGet: 39 | # path: / 40 | # port: http 41 | # readinessProbe: 42 | # httpGet: 43 | # path: / 44 | # port: http 45 | resources: 46 | {{- toYaml .Values.resources | nindent 12 }} 47 | {{- with .Values.nodeSelector }} 48 | nodeSelector: 49 | {{- toYaml . | nindent 8 }} 50 | {{- end }} 51 | {{- with .Values.affinity }} 52 | affinity: 53 | {{- toYaml . | nindent 8 }} 54 | {{- end }} 55 | {{- with .Values.tolerations }} 56 | tolerations: 57 | {{- toYaml . | nindent 8 }} 58 | {{- end }} 59 | -------------------------------------------------------------------------------- /helm/urunner/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.secret.create }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ .Values.secret.secretName | quote }} 6 | namespace: {{ .Release.Namespace | quote }} 7 | type: Opaque 8 | stringData: 9 | {{- if .Values.secret.harbor.username }} 10 | URUNNER_SECR_HARBOR_USER: {{ .Values.secret.harbor.username | quote }} 11 | URUNNER_SECR_HARBOR_PASS: {{ .Values.secret.harbor.password | quote }} 12 | {{- end }} 13 | {{- if .Values.secret.aws.access_key_id }} 14 | URUNNER_SECR_AWS_ACCESS_KEY_ID: {{ .Values.secret.aws.access_key_id | quote }} 15 | URUNNER_SECR_AWS_SECRET_ACCESS_KEY: {{ .Values.secret.aws.secret_access_key | quote }} 16 | {{- end }} 17 | {{- if .Values.secret.aws.region }} 18 | URUNNER_SECR_AWS_REGION: {{ .Values.secret.aws.region | quote }} 19 | {{- end }} 20 | {{- if .Values.secret.digitalocean.token }} 21 | URUNNER_SECR_DIGITAL_OCEAN_TOKEN: {{ .Values.secret.digitalocean.token | quote }} 22 | {{- end }} 23 | {{- if .Values.secret.gitlab.token }} 24 | URUNNER_SECR_GITLAB_TOKEN: {{ .Values.secret.gitlab.token | quote }} 25 | {{- end }} 26 | {{- if .Values.secret.gitlab.url }} 27 | URUNNER_SECR_GITLAB_AUTH_URL: {{ .Values.secret.gitlab.auth_url | quote }} 28 | {{- end }} 29 | {{- end }} 30 | 31 | 32 | -------------------------------------------------------------------------------- /helm/urunner/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "urunner.serviceAccountName" . }} 6 | namespace: {{ .Release.Namespace | quote }} 7 | --- 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRoleBinding 10 | metadata: 11 | name: {{ include "urunner.serviceAccountName" . }} 12 | namespace: {{ .Release.Namespace | quote }} 13 | labels: 14 | {{- include "urunner.labels" . | nindent 4 }} 15 | {{- with .Values.serviceAccount.annotations }} 16 | annotations: 17 | {{- toYaml . | nindent 4 }} 18 | {{- end }} 19 | subjects: 20 | - kind: ServiceAccount 21 | name: {{ include "urunner.serviceAccountName" . }} 22 | namespace: {{ .Release.Namespace | quote }} 23 | apiGroup: "" 24 | roleRef: 25 | kind: ClusterRole 26 | name: {{ include "urunner.clusterRoleName" . }} 27 | apiGroup: "" 28 | --- 29 | kind: ClusterRole 30 | apiVersion: rbac.authorization.k8s.io/v1 31 | metadata: 32 | name: {{ include "urunner.clusterRoleName" . }} 33 | namespace: {{ .Release.Namespace | quote }} 34 | rules: 35 | - apiGroups: ["*"] 36 | resources: ["*"] 37 | verbs: ["*"] 38 | {{- end }} 39 | -------------------------------------------------------------------------------- /helm/urunner/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | repository: ghcr.io/texano00/urunner 3 | pullPolicy: IfNotPresent 4 | 5 | config: 6 | configMapName: "cm-urunner" 7 | # se to false in order to disallow ssl verify to container registry 8 | URUNNER_CONF_DOCKER_API_VERIFY: true 9 | # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). 10 | URUNNER_CONF_LOG_LEVEL: INFO 11 | # incluster, kubeconfig allowed 12 | URUNNER_CONF_KUBE_AUTH: incluster 13 | # path on which urunner store its sqllight database 14 | URUNNER_CONF_SQLLIGHT_PATH: /app/urunner.db 15 | # urunner check frequency 16 | URUNNER_CONF_FREQUENCY_CHECK_SECONDS: 20 17 | # container registry url 18 | URUNNER_CONF_CONTAINER_REGISTRY_TO_WATCH: 435734619587.dkr.ecr.us-east-2.amazonaws.com 19 | # harbor, aws_ecr, digitalocean, gitlab 20 | URUNNER_CONF_CONTAINER_REGISTRY_TYPE: aws_ecr 21 | # docker api accept header 22 | DOCKER_API_ACCEPT_HEADER: 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' 23 | secret: 24 | create: true 25 | # for expected keys see secret.yaml 26 | secretName: "secret-urunner" 27 | harbor: 28 | username: 29 | password: 30 | aws: 31 | access_key_id: 32 | secret_access_key: 33 | digitalocean: 34 | token: 35 | gitlab: 36 | token: 37 | auth_url: 38 | 39 | imagePullSecrets: [] 40 | nameOverride: "" 41 | fullnameOverride: "" 42 | 43 | serviceAccount: 44 | # Specifies whether a service account should be created 45 | create: true 46 | # Annotations to add to the service account 47 | annotations: {} 48 | # The name of the service account to use. 49 | # If not set and create is true, a name is generated using the fullname template 50 | name: "" 51 | 52 | podSecurityContext: {} 53 | # fsGroup: 2000 54 | 55 | securityContext: {} 56 | # capabilities: 57 | # drop: 58 | # - ALL 59 | # readOnlyRootFilesystem: true 60 | # runAsNonRoot: true 61 | # runAsUser: 1000 62 | 63 | resources: {} 64 | # We usually recommend not to specify default resources and to leave this as a conscious 65 | # choice for the user. This also increases chances charts run on environments with little 66 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 67 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 68 | # limits: 69 | # cpu: 100m 70 | # memory: 128Mi 71 | # requests: 72 | # cpu: 100m 73 | # memory: 128Mi 74 | 75 | nodeSelector: {} 76 | 77 | tolerations: [] 78 | 79 | affinity: {} 80 | --------------------------------------------------------------------------------