├── .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 | [](https://artifacthub.io/packages/search?repo=urunner)
3 | 
4 | 
5 |
6 | ## Star History
7 |
8 | [](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 |
--------------------------------------------------------------------------------