├── tests ├── test_roots │ ├── configmaps │ │ └── k8s │ │ │ └── base.py │ ├── simple │ │ ├── k8s │ │ │ ├── dev.py │ │ │ ├── templates │ │ │ │ ├── from.include │ │ │ │ ├── Dockerfile2 │ │ │ │ ├── Dockerfile │ │ │ │ └── deployment.yaml │ │ │ ├── versions.json │ │ │ └── base.py │ │ └── Dockerfile │ ├── configmap │ │ └── k8s │ │ │ ├── dev.py │ │ │ ├── versions.json │ │ │ ├── templates │ │ │ ├── file_config.conf │ │ │ └── binary_config.conf │ │ │ └── base.py │ ├── convert │ │ ├── k8s │ │ │ └── versions.json │ │ └── deployment.yaml │ ├── obj-config │ │ └── k8s │ │ │ ├── versions.json │ │ │ ├── dev.py │ │ │ └── base.py │ └── obj-config-wo-init │ │ └── k8s │ │ ├── versions.json │ │ ├── dev.py │ │ └── base.py ├── utils.py ├── test_select_env.py ├── conftest.py ├── vindaloo_conf.py ├── test_help.py ├── test_internals.py ├── test_edit_secret.py ├── test_convert.py ├── test_pull.py ├── test_build.py ├── test_push.py ├── test_versions.py └── test_deploy.py ├── version.json ├── examples ├── class-config │ └── k8s │ │ ├── versions.json │ │ ├── dev.py │ │ ├── test.py │ │ └── base.py ├── cron-jobs │ └── k8s │ │ ├── versions.json │ │ ├── templates │ │ ├── pod-preset.yaml │ │ ├── Dockerfile │ │ ├── job.yaml │ │ └── cron-job.yaml │ │ ├── dev.py │ │ ├── test.py │ │ ├── crontab.py │ │ └── base.py └── multi-image │ └── k8s │ ├── versions.json │ ├── templates │ ├── service.yaml │ ├── deployment.outage.yaml │ ├── Dockerfile.web │ ├── Dockerfile.outage │ ├── Dockerfile.proxy │ └── deployment.yaml │ ├── dev.py │ ├── staging.py │ ├── prerelease.py │ ├── test.py │ ├── stable.py │ └── base.py ├── latest └── vindaloo.pex ├── main.py ├── vindaloo ├── __init__.py ├── utils.py ├── examples.py ├── convert.py └── objects.py ├── .gitignore ├── .idea └── vcs.xml ├── Pipfile ├── setup.cfg ├── setup.py ├── Makefile ├── .github └── workflows │ └── test.yml ├── CHANGELOG.md ├── README.cs.md ├── README.md ├── pylintrc └── Pipfile.lock /tests/test_roots/configmaps/k8s/base.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.5.0" 3 | } 4 | -------------------------------------------------------------------------------- /tests/test_roots/simple/k8s/dev.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | -------------------------------------------------------------------------------- /tests/test_roots/configmap/k8s/dev.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | -------------------------------------------------------------------------------- /tests/test_roots/simple/k8s/templates/from.include: -------------------------------------------------------------------------------- 1 | FROM debian 2 | -------------------------------------------------------------------------------- /tests/test_roots/convert/k8s/versions.json: -------------------------------------------------------------------------------- 1 | {"docker.repo/bar/foo-web": "1.0.0"} -------------------------------------------------------------------------------- /examples/class-config/k8s/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "avengers/server": "3.8.17" 3 | } 4 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "avengers/robot": "3.3.4-dev4" 3 | } 4 | -------------------------------------------------------------------------------- /tests/test_roots/configmap/k8s/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "test/foo": "1.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /tests/test_roots/obj-config/k8s/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "test/foo": "1.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /latest/vindaloo.pex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seznam/vindaloo/HEAD/latest/vindaloo.pex -------------------------------------------------------------------------------- /tests/test_roots/obj-config-wo-init/k8s/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "test/foo": "1.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | from vindaloo.vindaloo import run 3 | 4 | if __name__ == '__main__': 5 | run() 6 | -------------------------------------------------------------------------------- /tests/test_roots/simple/k8s/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "test/foo": "{{git}}-dev", 3 | "test/bar": "2.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /vindaloo/__init__.py: -------------------------------------------------------------------------------- 1 | from .vindaloo import VERSION, app 2 | from .objects import * 3 | 4 | __version__ = VERSION 5 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "avengers/web": "1.2.0", 3 | "avengers/web-proxy": "1.2.0", 4 | "avengers/web-outage": "1.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /tests/test_roots/configmap/k8s/templates/file_config.conf: -------------------------------------------------------------------------------- 1 | some_config_value=123 2 | another_config=one,two,three 3 | template_config={{variable_1}} 4 | -------------------------------------------------------------------------------- /tests/test_roots/simple/k8s/templates/Dockerfile2: -------------------------------------------------------------------------------- 1 | LABEL maintainer="{{{maintainer}}}" 2 | LABEL description="Bar" 3 | LABEL version="{{version}}" 4 | -------------------------------------------------------------------------------- /tests/test_roots/configmap/k8s/templates/binary_config.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seznam/vindaloo/HEAD/tests/test_roots/configmap/k8s/templates/binary_config.conf -------------------------------------------------------------------------------- /tests/test_roots/simple/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | 3 | LABEL maintainer="Test Test " 4 | LABEL description="Foo" 5 | LABEL version="d6ee34ae-dev" 6 | -------------------------------------------------------------------------------- /tests/test_roots/simple/k8s/templates/Dockerfile: -------------------------------------------------------------------------------- 1 | {{#includes}}{{&from}}{{/includes}} 2 | LABEL maintainer="{{{maintainer}}}" 3 | LABEL description="Foo" 4 | LABEL version="{{version}}" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | build 3 | dist 4 | __pycache__ 5 | vindaloo.egg-info 6 | htmlcov 7 | .coverage 8 | .mypy_cache 9 | .idea 10 | .vscode 11 | szn_vindaloo.egg-info 12 | tests/Dockerfile 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import os 3 | 4 | 5 | @contextmanager 6 | def chdir(path): 7 | orig_dir = os.getcwd() 8 | try: 9 | os.chdir(path) 10 | yield 11 | finally: 12 | os.chdir(orig_dir) 13 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/templates/pod-preset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: settings.k8s.io/v1alpha1 2 | kind: PodPreset 3 | metadata: 4 | name: cluster-name 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: avengers-robot 9 | env: 10 | - name: KUBERNETES_CLUSTER 11 | value: "ko1" 12 | -------------------------------------------------------------------------------- /examples/class-config/k8s/dev.py: -------------------------------------------------------------------------------- 1 | import vindaloo 2 | from base import * 3 | 4 | DEPLOYMENT_PUBLIC.spec.template.spec.containers['avengers-server'].env.ENVIRONMENT = "avengers-dev" 5 | DEPLOYMENT_PRIVATE.spec.template.spec.containers['avengers-server'].env.ENVIRONMENT = "avengers-dev" 6 | 7 | SERVICE_PUBLIC.spec.ports['rpc']['nodePort'] = 30007 8 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | argcomplete = ">=1.9.5" 8 | chevron = "*" 9 | pyyaml = "*" 10 | 11 | [dev-packages] 12 | pytest = "*" 13 | pytest-cov = "*" 14 | codecov = "==2.1.13" 15 | importlib-metadata = "*" 16 | typing-extensions = "*" 17 | coverage = "==6.2" 18 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ident_label}} 5 | labels: 6 | name: {{ident_label}} 7 | spec: 8 | type: NodePort 9 | ports: 10 | {{#ports}} 11 | - name: {{name}} 12 | nodePort: {{nodePort}} 13 | port: {{port}} 14 | protocol: TCP 15 | {{/ports}} 16 | selector: 17 | app: {{app_name}} 18 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/dev.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | 4 | CRON_JOB.update({ 5 | 'env': [ 6 | { 7 | 'key': 'ENVIRONMENT', 8 | 'val': "avengers-dev" 9 | }, 10 | ], 11 | }) 12 | 13 | for job in K8S_OBJECTS.get('cronjob', []): 14 | job['config'].update(CRON_JOB) 15 | for job in K8S_OBJECTS.get('job', []): 16 | job['config'].update(CRON_JOB) 17 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/test.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | 4 | CRON_JOB.update({ 5 | 'env': [ 6 | { 7 | 'key': 'ENVIRONMENT', 8 | 'val': "avengers-test" 9 | }, 10 | ], 11 | }) 12 | 13 | for job in K8S_OBJECTS.get('cronjob', []): 14 | job['config'].update(CRON_JOB) 15 | for job in K8S_OBJECTS.get('job', []): 16 | job['config'].update(CRON_JOB) 17 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/templates/Dockerfile: -------------------------------------------------------------------------------- 1 | {{#includes}}{{&base_image}}{{/includes}} 2 | LABEL maintainer="{{{maintainer}}}" 3 | LABEL description="avengers robot" 4 | 5 | COPY robot/Pipfile robot/Pipfile.lock /home/avengers/robot/ 6 | RUN cd /home/avengers/robot && https_proxy={{https_proxy}} pipenv install --system --deploy 7 | 8 | COPY robot /home/avengers/robot 9 | 10 | LABEL version="{{version}}" 11 | -------------------------------------------------------------------------------- /examples/class-config/k8s/test.py: -------------------------------------------------------------------------------- 1 | import vindaloo 2 | from base import * 3 | 4 | ENV = { 5 | 'ENVIRONMENT' = "avengers-test", 6 | 'WEB_CONCURRENCY': "10", 7 | } 8 | 9 | DEPLOYMENT_PUBLIC.spec.template.spec.containers['avengers-server'].env.update(ENV) 10 | DEPLOYMENT_PRIVATE.spec.template.spec.containers['avengers-server'].env.update(ENV) 11 | 12 | SERVICE_PUBLIC.spec.ports['rpc']['nodePort'] = 30013 13 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/dev.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | 4 | DEPLOYMENT_ADMINWEB.update({ 5 | 'env': [ 6 | { 7 | 'key': 'ENVIRONMENT', 8 | 'val': "avengers-dev" 9 | }, 10 | { 11 | 'key': 'PORT', 12 | 'val': "8001" 13 | }, 14 | ], 15 | }) 16 | 17 | SERVICE.update({ 18 | 'ports': [ 19 | {'port': 8000, 'nodePort': 30005, 'name': 'http'}, 20 | ] 21 | }) 22 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/staging.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | 4 | DEPLOYMENT_ADMINWEB.update({ 5 | 'env': [ 6 | { 7 | 'key': 'ENVIRONMENT', 8 | 'val': "avengers-staging" 9 | }, 10 | { 11 | 'key': 'PORT', 12 | 'val': "8001" 13 | }, 14 | ], 15 | }) 16 | 17 | SERVICE.update({ 18 | 'ports': [ 19 | {'port': 8000, 'nodePort': 30055, 'name': 'http'}, 20 | ] 21 | }) 22 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/prerelease.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | 4 | DEPLOYMENT_ADMINWEB.update({ 5 | 'env': [ 6 | { 7 | 'key': 'ENVIRONMENT', 8 | 'val': "avengers-prerelease" 9 | }, 10 | { 11 | 'key': 'PORT', 12 | 'val': "8001" 13 | }, 14 | ], 15 | }) 16 | 17 | SERVICE.update({ 18 | 'ports': [ 19 | {'port': 8000, 'nodePort': 30065, 'name': 'http'}, 20 | ] 21 | }) 22 | -------------------------------------------------------------------------------- /vindaloo/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class NamespaceWithDefaultValue: 4 | """ 5 | Argparse namespace class returning `default_value` if attribute is not defined 6 | """ 7 | def __init__(self, namespace, default_value=None): 8 | self.namespace = namespace 9 | self.default_value = default_value 10 | 11 | def __getattr__(self, name): 12 | if hasattr(self.namespace, name): 13 | return getattr(self.namespace, name) 14 | return self.default_value 15 | -------------------------------------------------------------------------------- /tests/test_roots/obj-config-wo-init/k8s/dev.py: -------------------------------------------------------------------------------- 1 | from base import CRONJOB, DEPLOYMENT, SERVICE, K8S_OBJECTS 2 | import vindaloo 3 | 4 | CRONJOB.spec.jobTemplate.spec.template.spec.containers.foo.command = ['echo', 'z'] 5 | 6 | DEPLOYMENT.spec.template.spec.containers.foo.env.ENV = "dev" 7 | DEPLOYMENT.metadata.annotations['deploy-cluster'] = 'cluster2' 8 | 9 | SERVICE.spec.ports['http']['nodePort'] = 30666 10 | SERVICE.spec.loadBalancerIP = '10.1.1.1' if vindaloo.app.args.cluster == 'cluster1' else '10.2.1.1' 11 | SERVICE.metadata.annotations['certificate/https'] = 'foo.com' 12 | -------------------------------------------------------------------------------- /tests/test_roots/obj-config/k8s/dev.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | import vindaloo 3 | 4 | ENV = { 5 | 'ENV': "dev", 6 | } 7 | 8 | DEPLOYMENT.spec.template.spec.containers.foo.env.update(ENV) 9 | DEPLOYMENT.metadata['annotations']['deploy-cluster'] = 'cluster2' 10 | 11 | CRONJOB.spec.jobTemplate.spec.template.spec.containers.foo.command = ['echo', 'z'] 12 | 13 | SERVICE.spec.ports['http']['nodePort'] = 30666 14 | SERVICE.spec.loadBalancerIP = '10.1.1.1' if vindaloo.app.args.cluster == 'cluster1' else '10.2.1.1' 15 | SERVICE.metadata.annotations['certificate/https'] = 'foo.com' 16 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/templates/deployment.outage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ident_label}} 5 | spec: 6 | replicas: {{replicas}} 7 | template: 8 | metadata: 9 | name: {{ident_label}} 10 | labels: 11 | app: {{ident_label}} 12 | spec: 13 | containers: 14 | - name: avengers-web-outage 15 | image: {{registry}}/{{image}} 16 | ports: 17 | - containerPort: {{port}} 18 | livenessProbe: 19 | initialDelaySeconds: 5 20 | periodSeconds: 5 21 | httpGet: 22 | path: / 23 | port: {{port}} 24 | -------------------------------------------------------------------------------- /tests/test_roots/simple/k8s/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ident_label}} 5 | spec: 6 | replicas: {{replicas}} 7 | template: 8 | metadata: 9 | name: "{{ident_label}}" 10 | labels: 11 | app: "{{ident_label}}" 12 | annotations: 13 | {{#spec_annotations}} 14 | {{key}}: "{{val}}" 15 | {{/spec_annotations}} 16 | spec: 17 | containers: 18 | - name: "{{ident_label}}" 19 | image: {{image}} 20 | imagePullPolicy: IfNotPresent 21 | env: 22 | {{#env}} 23 | - name: {{key}} 24 | value: {{val}} 25 | {{/env}} 26 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/test.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | 4 | DEPLOYMENT_ADMINWEB.update({ 5 | 'env': [ 6 | { 7 | 'key': 'ENVIRONMENT', 8 | 'val': "avengers-test" 9 | }, 10 | { 11 | 'key': 'PORT', 12 | 'val': "8001" 13 | }, 14 | { 15 | 'key': 'http_proxy', 16 | 'val': "http://proxy.com:3128", 17 | }, 18 | { 19 | 'key': 'https_proxy', 20 | 'val': "http://proxy.com:3128", 21 | }, 22 | ], 23 | }) 24 | 25 | SERVICE.update({ 26 | 'ports': [ 27 | {'port': 8000, 'nodePort': 30020, 'name': 'http'}, 28 | ] 29 | }) 30 | -------------------------------------------------------------------------------- /tests/test_roots/convert/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foo-web 5 | labels: 6 | app: foo-web 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: foo-web 12 | template: 13 | metadata: 14 | labels: 15 | app: foo-web 16 | annotations: 17 | please-redeploy-dis: "14" 18 | spec: 19 | containers: 20 | - name: nginx 21 | image: docker.repo/bar/foo-web:1.0.0 22 | imagePullPolicy: "Always" 23 | ports: 24 | - containerPort: 80 25 | - containerPort: 8080 26 | env: 27 | - name: APP_ENV 28 | value: production 29 | -------------------------------------------------------------------------------- /tests/test_select_env.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from utils import chdir 4 | 5 | 6 | def test_select_dev_cluster1(loo): 7 | # fake arguments 8 | sys.argv = ['vindaloo', 'kubeenv', 'dev', 'c1'] 9 | 10 | with chdir('tests/test_roots/simple'): 11 | loo.main() 12 | 13 | assert loo.cmd.call_args[0][0] == ['kubectl', 'config', 'use-context', 'foo-dev:cluster1'] 14 | 15 | 16 | def test_select_dev_cluster2(loo): 17 | # fake arguments 18 | sys.argv = ['vindaloo', 'kubeenv', 'dev', 'c2'] 19 | 20 | with chdir('tests/test_roots/simple'): 21 | loo.main() 22 | 23 | assert loo.cmd.call_args[0][0] == ['kubectl', 'config', 'use-context', 'foo-dev:cluster2'] 24 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import sys 3 | import os 4 | import tempfile 5 | import uuid 6 | from unittest import mock 7 | 8 | import pytest 9 | 10 | 11 | sys.path.insert( 12 | 0, 13 | os.path.abspath( 14 | os.path.join( 15 | os.path.dirname(__file__), 16 | '..' 17 | ) 18 | ) 19 | ) 20 | 21 | from vindaloo.vindaloo import Vindaloo 22 | 23 | 24 | @pytest.fixture 25 | def loo(): 26 | loo = Vindaloo() 27 | loo.cmd = mock.Mock() 28 | loo.cmd.return_value.returncode = 0 29 | loo._check_version = mock.Mock() 30 | return loo 31 | 32 | 33 | @pytest.yield_fixture(scope='function') 34 | def test_temp_dir(): 35 | with tempfile.TemporaryDirectory() as name: 36 | yield name 37 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 160 3 | max-complexity = 15 4 | #ignore = F401,E402,F403,D 5 | 6 | # F401 - imported but unused 7 | # E402 - module level import not at top of file 8 | # F403 - unable to detect undefined names 9 | 10 | [mypy] 11 | python_version = 3.6 12 | ignore_missing_imports = True 13 | follow_imports = skip 14 | disallow_any_decorated = False 15 | 16 | [yapf] 17 | based_on_style = pep8 18 | split_before_logical_operator = True 19 | coalesce_brackets = True 20 | dedent_closing_brackets = True 21 | each_dict_entry_on_separate_line = False 22 | join_multiple_lines = False 23 | split_before_expression_after_opening_paren = True 24 | split_before_first_argument = True 25 | column_limit = 120 26 | 27 | [pycodestyle] 28 | max-line-length = 160 29 | -------------------------------------------------------------------------------- /tests/vindaloo_conf.py: -------------------------------------------------------------------------------- 1 | DEV_REGISTRY = 'foo-registry.com' 2 | PROD_REGISTRY = 'foo-prog-registry.com' 3 | 4 | ENVS = { 5 | 'dev': { 6 | 'k8s_namespace': 'foo-dev', 7 | 'k8s_clusters': ['cluster1', 'cluster2'], 8 | 'docker_registry': DEV_REGISTRY, 9 | }, 10 | 'test': { 11 | 'k8s_namespace': 'foo-test', 12 | 'k8s_clusters': ['cluster1', 'cluster2'], 13 | 'docker_registry': DEV_REGISTRY, 14 | }, 15 | 'staging': { 16 | 'k8s_namespace': 'foo-staging', 17 | 'k8s_clusters': ['cluster1', 'cluster2'], 18 | 'docker_registry': PROD_REGISTRY, 19 | }, 20 | 'stable': { 21 | 'k8s_namespace': 'foo-stable', 22 | 'k8s_clusters': ['cluster1', 'cluster2'], 23 | 'docker_registry': PROD_REGISTRY, 24 | }, 25 | } 26 | 27 | K8S_CLUSTER_ALIASES = { 28 | 'c1': 'cluster1', 29 | 'c2': 'cluster2', 30 | } 31 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/templates/Dockerfile.web: -------------------------------------------------------------------------------- 1 | {{#includes}}{{&base_image}}{{/includes}} 2 | LABEL maintainer="{{{maintainer}}}" 3 | LABEL description="avengers web" 4 | 5 | EXPOSE 8000 6 | ENV prometheus_multiproc_dir=/srv/http/web/log/prometheus 7 | 8 | RUN apt-get install -y \ 9 | uwsgi \ 10 | uwsgi-core \ 11 | uwsgi-plugin-python 12 | 13 | RUN mkdir -p /srv/http/web 14 | COPY web/Pipfile web/Pipfile.lock /srv/http/web/ 15 | RUN cd /srv/http/web && https_proxy={{https_proxy}} pipenv install --system --deploy 16 | 17 | RUN groupadd -r --gid=1000 avengers-web && \ 18 | useradd -r --uid=1000 -b /srv/http/web -d /srv/http/web -m -s /bin/bash -g avengers-web avengers-web 19 | 20 | COPY web /srv/http/web 21 | RUN chown -R avengers-web:avengers-web /srv/http/web 22 | ENV USER="avengers-web" 23 | 24 | ENTRYPOINT ["uwsgi", "/srv/http/web/conf/uwsgi.ini"] 25 | LABEL version="{{version}}" 26 | -------------------------------------------------------------------------------- /tests/test_roots/configmap/k8s/base.py: -------------------------------------------------------------------------------- 1 | import vindaloo 2 | from vindaloo.objects import ConfigMap 3 | 4 | versions = vindaloo.app.versions 5 | 6 | 7 | CONTEXT = {'variable_1': 'This value depends on the selected environment.'} 8 | 9 | DOCKER_FILES = [] 10 | 11 | CONFIG_MAP = ConfigMap( 12 | name='test-config-map', 13 | metadata={ 14 | 'labels': {'custom-labels': '123'}, 15 | 'annotations': {'custom-annotations': '...'}, 16 | }, 17 | immutable=True, 18 | data={ 19 | 'simple_config_key': 123, 20 | 'file_config_key': { 21 | 'file': 'templates/file_config.conf', 22 | 'config': CONTEXT, 23 | }, 24 | }, 25 | binary_data={ 26 | 'simple_binary_key': b'\x76\x69\x6b\x79', 27 | 'binary_file_config_key': { 28 | 'file': 'templates/binary_config.conf', 29 | }, 30 | }, 31 | ) 32 | 33 | K8S_OBJECTS = { 34 | "configmap": [CONFIG_MAP], 35 | } 36 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/crontab.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | CronLine = namedtuple( 4 | 'CronLine', 5 | ( 6 | 'schedule', 7 | 'name', 8 | 'command', 9 | 'description', 10 | 'expected_duration', 11 | 'allowed_environments', 12 | 'disabled', 13 | ) 14 | ) 15 | 16 | CRONS = [ 17 | CronLine( 18 | schedule='50 23 * * *', 19 | name='campaignRunManager', 20 | command='/etc/init.d/avengers-robot campaign_run_manager --mode expire', 21 | description='campaign expiration', 22 | expected_duration=5, 23 | disabled=False 24 | ), 25 | CronLine( 26 | schedule='0 * * * *', 27 | name='campaignRunManager', 28 | command='/etc/init.d/avengers-robot campaign_run_manager --mode all', 29 | description='move all campaigns to desired states', 30 | expected_duration=10, 31 | disabled=False 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/templates/Dockerfile.outage: -------------------------------------------------------------------------------- 1 | FROM foo-registry.com/debian:stretch 2 | LABEL maintainer="{{{maintainer}}}" 3 | LABEL description="avengers web outage" 4 | 5 | EXPOSE 8000 6 | 7 | # Nasetujeme český UTF-8 locale a globální jazyk. 8 | RUN echo "cs_CZ.UTF-8 UTF-8" >> /etc/locale.gen 9 | RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 10 | RUN locale-gen 11 | # Tohle je nasetovani jazyka pouze lokalne behem buildu. 12 | ENV LANG="en_US.UTF-8" 13 | ENV LC_CTYPE="en_US.UTF-8" 14 | 15 | RUN apt-get update && apt-get upgrade -y 16 | 17 | RUN apt-get install -y nginx 18 | 19 | RUN groupadd -r --gid=1000 avengers-web-outage && \ 20 | useradd -r --uid=1000 -b /srv/http/web -d /srv/http/web -m -s /bin/bash -g avengers-web-outage avengers-web-outage 21 | ENV USER="avengers-web-outage" 22 | 23 | RUN mkdir -p /srv/http/web 24 | COPY web-proxy /srv/http/web 25 | 26 | ENTRYPOINT ["nginx", "-c", "/srv/http/web/conf/nginx-outage.conf"] 27 | LABEL version="{{version}}" 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from setuptools import setup 4 | 5 | 6 | with open("vindaloo/vindaloo.py", "rt") as f: 7 | version = re.search(r'VERSION = \'(.*?)\'', f.read()).group(1) 8 | 9 | with open("README.md", "r") as fh: 10 | long_description = fh.read() 11 | 12 | setup( 13 | name='vindaloo', 14 | version=version, 15 | description='K8S deployer', 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/seznam/vindaloo", 19 | author='Daniel Milde', 20 | author_email='daniel.milde@firma.seznam.cz', 21 | install_requires=[ 22 | 'argcomplete>=1.9.5', 23 | 'chevron', 24 | ], 25 | entry_points={ 26 | 'console_scripts': [ 27 | 'vindaloo = vindaloo.vindaloo:run' 28 | ] 29 | }, 30 | packages=['vindaloo'], 31 | classifiers=[ 32 | 'Programming Language :: Python :: 3', 33 | ], 34 | python_requires='>=3.6', 35 | ) 36 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/templates/Dockerfile.proxy: -------------------------------------------------------------------------------- 1 | FROM foo-registry.com/debian:stretch 2 | LABEL maintainer="{{{maintainer}}}" 3 | LABEL description="avengers web proxy" 4 | 5 | EXPOSE 8000 6 | 7 | # Nasetujeme český UTF-8 locale a globální jazyk. 8 | RUN echo "cs_CZ.UTF-8 UTF-8" >> /etc/locale.gen 9 | RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 10 | RUN locale-gen 11 | # Tohle je nasetovani jazyka pouze lokalne behem buildu. 12 | ENV LANG="en_US.UTF-8" 13 | ENV LC_CTYPE="en_US.UTF-8" 14 | 15 | RUN apt-get update && apt-get upgrade -y 16 | 17 | RUN apt-get install -y nginx 18 | 19 | RUN groupadd -r --gid=1000 avengers-web-proxy && \ 20 | useradd -r --uid=1000 -b /home/avengers-web-proxy -d /home/avengers-web-proxy -m -s /bin/bash -g avengers-web-proxy avengers-web-proxy 21 | ENV USER="avengers-web-proxy" 22 | 23 | RUN mkdir -p /srv/http/web 24 | COPY web-proxy /srv/http/web 25 | 26 | ENTRYPOINT ["nginx", "-c", "/srv/http/web/conf/nginx-proxy-docker.conf"] 27 | LABEL version="{{version}}" 28 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/stable.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | 4 | DEPLOYMENT_ADMINWEB.update({ 5 | 'env': [ 6 | { 7 | 'key': 'ENVIRONMENT', 8 | 'val': "avengers-stable" 9 | }, 10 | { 11 | 'key': 'PORT', 12 | 'val': "8001" 13 | }, 14 | ], 15 | # Anotace pro produkcniho promethea admins3 16 | 'spec_annotations': [ 17 | { 18 | 'key': 'metrics.scrape', 19 | 'val': "true" 20 | }, 21 | { 22 | 'key': 'metrics.port', 23 | 'val': "8000" 24 | }, 25 | { 26 | 'key': 'metrics.path', 27 | 'val': "/monitoring/prometheus" 28 | }, 29 | { 30 | 'key': 'log-retention', 31 | 'val': "3w" 32 | }, 33 | { 34 | 'key': 'team', 35 | 'val': "avengers" 36 | }, 37 | ] 38 | }) 39 | 40 | SERVICE.update({ 41 | 'ports': [ 42 | {'port': 8000, 'nodePort': 30056, 'name': 'http'}, 43 | ] 44 | }) 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test pex-in-docker 2 | 3 | cache: 4 | # create pycache 5 | python main.py >/dev/null || true 6 | 7 | pex-in-docker: 8 | docker run --rm -v $(PWD):/x python:3.6-alpine sh -c "apk add --no-cache make && cd /x && make install-dev pex-local" 9 | 10 | pex-local: cache 11 | pex --python=python3 . argcomplete setuptools chevron -e vindaloo.vindaloo:run -o latest/vindaloo.pex --python-shebang='/usr/bin/env python3' --disable-cache 12 | 13 | test: 14 | pipenv run py.test tests 15 | 16 | coverage: 17 | pipenv run py.test --cov=vindaloo --cov-report html tests 18 | 19 | clean: 20 | sudo find . -name '__pycache__' -exec rm -rf {} +; 21 | sudo find . -name '*.pyc' -exec rm -rf {} +; 22 | -rm -rf build dist 23 | 24 | install-dev: 25 | pip install argcomplete pex chevron 26 | 27 | test-all: clean 3.9-alpine 3.8-alpine 3.7-alpine 3.6-alpine 28 | 29 | upload: 30 | python setup.py sdist bdist_wheel 31 | python -m twine upload dist/* 32 | 33 | %-alpine: 34 | docker run --rm -v $(PWD):/x python:$@ sh -c "apk add git; pip install pipenv; cd /x; pipenv install --dev; pipenv run pytest tests" 35 | 36 | .PHONY: all cache pex-local pex-in-docker test coverage clean test-all upload 37 | -------------------------------------------------------------------------------- /tests/test_help.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import mock 3 | 4 | import pytest 5 | 6 | from utils import chdir 7 | from vindaloo.vindaloo import Vindaloo 8 | 9 | ALL_CMDS_STRING = 'build,pull,push,kubeenv,version,versions,deploy,deploy-dir,build-push-deploy' 10 | 11 | 12 | @mock.patch('argparse._sys.exit') 13 | def test_help(mock_sys_exit, capsys): 14 | sys.argv = ['vindaloo', '-h'] 15 | 16 | with chdir('tests/test_roots/simple'): 17 | Vindaloo().main() 18 | 19 | captured = capsys.readouterr() 20 | output = captured.out 21 | 22 | assert mock_sys_exit.call_args[0][0] == 0 23 | assert 'usage' in output 24 | assert ALL_CMDS_STRING in output 25 | 26 | 27 | @mock.patch('argparse._sys.exit') 28 | def test_help_build(mock_sys_exit, capsys): 29 | sys.argv = ['vindaloo', 'build', '-h'] 30 | 31 | mock_sys_exit.side_effect = Exception() 32 | 33 | with chdir('tests/test_roots/simple'): 34 | with pytest.raises(Exception): 35 | Vindaloo().main() 36 | 37 | captured = capsys.readouterr() 38 | output = captured.out 39 | 40 | assert 'usage' in output 41 | assert '--latest' in output 42 | assert ALL_CMDS_STRING not in output 43 | -------------------------------------------------------------------------------- /tests/test_roots/simple/k8s/base.py: -------------------------------------------------------------------------------- 1 | import vindaloo 2 | 3 | versions = vindaloo.app.versions 4 | 5 | CONFIG = { 6 | 'maintainer': "Test Test ", 7 | 'version': versions['test/foo'], 8 | 'image_name': 'foo-registry.com/test/foo', 9 | } 10 | 11 | CONFIG_SECOND = { 12 | 'maintainer': "Test Test ", 13 | 'version': versions['test/bar'], 14 | 'image_name': 'foo-registry.com/test/bar', 15 | } 16 | 17 | DEPLOYMENT = { 18 | 'replicas': 1, 19 | 'ident_label': "foobar", 20 | 'image': "{}:{}".format(CONFIG['image_name'], CONFIG['version']), 21 | 'image_second': "{}:{}".format(CONFIG_SECOND['image_name'], CONFIG_SECOND['version']), 22 | 'spec_annotations': [], 23 | } 24 | 25 | DOCKER_FILES = [ 26 | { 27 | 'config': CONFIG, 28 | 'template': "Dockerfile", 29 | 'includes': { 30 | 'from': "k8s/templates/from.include", 31 | } 32 | }, 33 | { 34 | 'config': CONFIG_SECOND, 35 | 'template': "Dockerfile2", 36 | }, 37 | ] 38 | 39 | K8S_OBJECTS = { 40 | "deployment": [ 41 | { 42 | 'config': DEPLOYMENT, 43 | 'template': "deployment.yaml", 44 | }, 45 | ], 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: run tests 10 | jobs: 11 | test: 12 | strategy: 13 | matrix: 14 | python-version: ['3.6', '3.7', '3.8', '3.9'] 15 | runs-on: ubuntu-20.04 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | architecture: x64 22 | - run: sudo apt update 23 | - run: sudo apt install git 24 | - run: pip install -U pip 25 | - run: pip install -U pipenv 26 | - run: pipenv install --dev 27 | - name: Run tests 28 | run: pipenv run pytest tests 29 | 30 | coverage: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-python@v2 35 | with: 36 | python-version: '3.9' 37 | architecture: 'x64' 38 | - run: sudo apt update 39 | - run: sudo apt install git 40 | - run: pip install -U pip 41 | - run: pip install -U pipenv 42 | - run: pipenv install --dev 43 | - run: pipenv run pytest --cov=vindaloo --cov-report=xml tests 44 | - name: Upload coverage to Codecov 45 | uses: codecov/codecov-action@v2 46 | -------------------------------------------------------------------------------- /tests/test_internals.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from vindaloo.vindaloo import Vindaloo 4 | 5 | 6 | def test_cmd(): 7 | loo = Vindaloo() 8 | loo.args = mock.Mock() 9 | loo.args.debug = False 10 | loo.args.dryrun = False 11 | res = loo.cmd(['true']) 12 | assert res.returncode == 0 13 | 14 | 15 | def test_cmd_dryrun(capsys): 16 | loo = Vindaloo() 17 | loo.args = mock.Mock() 18 | loo.args.debug = False 19 | loo.args.dryrun = True 20 | loo.args.quiet = False 21 | res = loo.cmd(['kubectl', 'get', 'pod']) 22 | assert res.returncode == 0 23 | 24 | captured = capsys.readouterr() 25 | output = captured.out 26 | assert output.strip() == 'CALL: kubectl get pod' 27 | 28 | 29 | def test_quiet(capsys): 30 | loo = Vindaloo() 31 | loo.args = mock.Mock() 32 | loo.args.debug = False 33 | loo.args.dryrun = True 34 | loo.args.quiet = True 35 | res = loo.cmd(['kubectl', 'get', 'pod']) 36 | assert res.returncode == 0 37 | 38 | captured = capsys.readouterr() 39 | output = captured.out 40 | assert not output.strip() 41 | 42 | 43 | @mock.patch('argparse._sys.exit') 44 | def test_fail(mock_sys_exit, capsys): 45 | Vindaloo().fail('msg') 46 | 47 | output = capsys.readouterr().out.strip() 48 | assert output == 'msg' 49 | assert mock_sys_exit.call_args[0][0] == -1 50 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/templates/job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: {{ident_label}} 5 | spec: 6 | template: 7 | metadata: 8 | labels: 9 | app: avengers-robot 10 | spec: 11 | restartPolicy: Never 12 | volumes: 13 | - name: localconfig 14 | secret: 15 | secretName: avengers-local-conf 16 | - name: robotpasswords 17 | secret: 18 | secretName: avengers-robot-pass-conf 19 | - name: robotconf 20 | configMap: 21 | name: avengers-robot-conf 22 | items: 23 | - key: robot.env.conf 24 | path: robot.conf.dev 25 | containers: 26 | - name: {{ident_label}} 27 | image: {{registry}}/{{image}} 28 | command: ["sh", "-c", "{{{command}}}"] 29 | env: 30 | {{#env}} 31 | - name: {{key}} 32 | value: {{val}} 33 | {{/env}} 34 | volumeMounts: 35 | - name: localconfig 36 | mountPath: "/home/avengers/conf/app.local.conf" 37 | subPath: "app.local.conf" 38 | - name: robotpasswords 39 | mountPath: "/home/avengers/robot/conf/robot.passwd.conf" 40 | subPath: "robot.passwd.conf" 41 | - name: robotconf 42 | mountPath: "/home/avengers/robot/conf/robot.conf.dev" 43 | subPath: robot.conf.dev 44 | resources: 45 | requests: 46 | cpu: "0.5" 47 | memory: 500Mi 48 | limits: 49 | cpu: 4 50 | memory: 8000Mi 51 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ident_label}} 5 | spec: 6 | replicas: {{replicas}} 7 | template: 8 | metadata: 9 | name: {{ident_label}} 10 | labels: 11 | app: {{ident_label}} 12 | annotations: 13 | {{#spec_annotations}} 14 | {{key}}: "{{val}}" 15 | {{/spec_annotations}} 16 | spec: 17 | volumes: 18 | - name: localconfig 19 | secret: 20 | secretName: avengers-local-conf 21 | containers: 22 | - name: avengers-web-proxy 23 | image: {{registry}}/{{image_proxy}} 24 | imagePullPolicy: IfNotPresent 25 | ports: 26 | - containerPort: {{port_proxy}} 27 | resources: 28 | limits: 29 | cpu: "1" 30 | memory: 500Mi 31 | requests: 32 | cpu: "0.1" 33 | memory: 50Mi 34 | - name: avengers-web 35 | image: {{registry}}/{{image_web}} 36 | imagePullPolicy: IfNotPresent 37 | volumeMounts: 38 | - name: localconfig 39 | mountPath: "/srv/http/web/conf/app.local.conf" 40 | subPath: "app.local.conf" 41 | ports: 42 | - containerPort: {{port_web}} 43 | env: 44 | {{#env}} 45 | - name: {{key}} 46 | value: "{{val}}" 47 | {{/env}} 48 | livenessProbe: 49 | initialDelaySeconds: 10 50 | periodSeconds: 5 51 | httpGet: 52 | path: /system/loadbalancer 53 | port: {{port_proxy}} 54 | -------------------------------------------------------------------------------- /tests/test_edit_secret.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | import pytest 5 | from utils import chdir 6 | 7 | from vindaloo.vindaloo import RefreshException 8 | 9 | TEST_JSON = """{ 10 | "apiVersion": "v1", 11 | "items": [ 12 | { 13 | "apiVersion": "v1", 14 | "data": { 15 | "app.local.conf": "ZmVqaw==" 16 | }, 17 | "kind": "Secret", 18 | "metadata": { 19 | "name": "sos-core-local-conf" 20 | }, 21 | "type": "Opaque" 22 | }, 23 | { 24 | "apiVersion": "v1", 25 | "data": { 26 | "password": "ZmVqaw==" 27 | }, 28 | "kind": "Secret", 29 | "metadata": { 30 | "creationTimestamp": "2019-02-06T13:29:55Z", 31 | "name": "sos-db-master" 32 | }, 33 | "type": "Opaque" 34 | } 35 | ], 36 | "kind": "List", 37 | "metadata": { 38 | "resourceVersion": "", 39 | "selfLink": "" 40 | } 41 | } 42 | """ 43 | 44 | 45 | def test_parse_secrets(loo): 46 | with chdir('tests/test_roots/simple'): 47 | res = loo._parse_secrets(json.loads(TEST_JSON)) 48 | assert len(res) == 2 49 | 50 | 51 | def test_commit_secret_values(loo): 52 | 53 | with chdir('tests/test_roots/simple'): 54 | loo.changed_secrets = {"a": b"b"} 55 | with pytest.raises(RefreshException): 56 | loo._commit_secret_values({"name": "XXX"}) 57 | assert loo.cmd.call_args[0][0] == ['kubectl', 'patch', 'secret', 'XXX', '-p', '{"data":{"a":"Yg=="}}'] -------------------------------------------------------------------------------- /tests/test_convert.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from unittest import mock 4 | 5 | import yaml 6 | 7 | from utils import chdir 8 | from vindaloo.vindaloo import Vindaloo 9 | from vindaloo.convert import get_obj_repr_from_dict 10 | 11 | EXPECTED = """ 12 | import vindaloo 13 | from vindaloo.objects import Deployment 14 | 15 | versions = vindaloo.app.versions 16 | 17 | DEPLOYMENT = Deployment( 18 | name="nginx", 19 | replicas=3, 20 | containers={ 21 | 'nginx': { 22 | 'image': "docker.repo/bar/foo-web:" + versions['docker.repo/bar/foo-web'], 23 | 'ports': [{'containerPort': 80}, {'containerPort': 8080}], 24 | 'env': {'APP_ENV': 'production'}, 25 | }, 26 | }, 27 | volumes=None, 28 | annotations=None, 29 | metadata={'labels': {}}, 30 | labels=None, 31 | spec_annotations={'please-redeploy-dis': '14'}, 32 | termination_grace_period=30, 33 | ) 34 | 35 | K8S_OBJECTS = { 36 | "deployment": [DEPLOYMENT], 37 | } 38 | """ 39 | 40 | 41 | def test_convert_deployment(): 42 | with chdir(f'tests/test_roots/convert'): 43 | with open('deployment.yaml', 'r') as fp: 44 | manifest_data = yaml.load(fp, Loader=yaml.Loader) 45 | res = get_obj_repr_from_dict(manifest_data) 46 | 47 | assert res == EXPECTED 48 | 49 | 50 | def test_convert_deployment2(capsys): 51 | loo = Vindaloo() 52 | loo.args = mock.Mock() 53 | loo.args.quiet = False 54 | loo.args.manifest = 'deployment.yaml' 55 | 56 | with chdir(f'tests/test_roots/convert'): 57 | loo.convert_manifest() 58 | 59 | output = capsys.readouterr().out.strip() 60 | assert output == EXPECTED.strip() 61 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/templates/cron-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: {{ident_label}} 5 | spec: 6 | schedule: "{{schedule}}" 7 | concurrencyPolicy: Forbid 8 | jobTemplate: 9 | spec: 10 | template: 11 | metadata: 12 | labels: 13 | app: avengers-robot 14 | spec: 15 | restartPolicy: Never 16 | volumes: 17 | - name: localconfig 18 | secret: 19 | secretName: avengers-local-conf 20 | - name: robotpasswords 21 | secret: 22 | secretName: avengers-robot-pass-conf 23 | - name: robotconf 24 | configMap: 25 | name: avengers-robot-conf 26 | items: 27 | - key: robot.env.conf 28 | path: robot.conf.dev 29 | containers: 30 | - name: {{ident_label}} 31 | image: {{registry}}/{{image}} 32 | command: ["sh", "-c", "{{{command}}}"] 33 | env: 34 | {{#env}} 35 | - name: {{key}} 36 | value: {{val}} 37 | {{/env}} 38 | volumeMounts: 39 | - name: localconfig 40 | mountPath: "/home/avengers/conf/app.local.conf" 41 | subPath: "app.local.conf" 42 | - name: robotpasswords 43 | mountPath: "/home/avengers/robot/conf/robot.passwd.conf" 44 | subPath: "robot.passwd.conf" 45 | - name: robotconf 46 | mountPath: "/home/avengers/robot/conf/robot.conf.dev" 47 | subPath: robot.conf.dev 48 | resources: 49 | requests: 50 | cpu: "0.5" 51 | memory: 500Mi 52 | limits: 53 | cpu: 4 54 | memory: 8000Mi 55 | -------------------------------------------------------------------------------- /tests/test_pull.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import mock 3 | 4 | from utils import chdir 5 | 6 | 7 | def test_pull_all(loo): 8 | # fake arguments 9 | sys.argv = ['vindaloo', '--noninteractive', 'pull', 'dev'] 10 | 11 | rev_parse_mock = mock.Mock() 12 | rev_parse_mock.stdout = b'd6ee34ae' 13 | 14 | images_mock = mock.Mock() 15 | images_mock.stdout.decode.return_value.split.return_value = [] 16 | 17 | pull_mock = mock.Mock() 18 | pull_mock.returncode = 0 19 | 20 | loo.cmd.side_effect = [rev_parse_mock, images_mock, pull_mock, pull_mock] 21 | 22 | with chdir('tests/test_roots/simple'): 23 | loo.main() 24 | 25 | # check the arguments docker was called with 26 | assert len(loo.cmd.call_args_list) == 4 27 | pull_cmd = loo.cmd.call_args_list[2][0][0] 28 | pull2_cmd = loo.cmd.call_args_list[3][0][0] 29 | 30 | assert pull_cmd == [ 31 | 'docker', 32 | 'pull', 33 | 'foo-registry.com/test/foo:d6ee34ae-dev', 34 | ] 35 | assert pull2_cmd == [ 36 | 'docker', 37 | 'pull', 38 | 'foo-registry.com/test/bar:2.0.0', 39 | ] 40 | 41 | 42 | def test_pull_one(loo): 43 | sys.argv = ['vindaloo', '--noninteractive', 'pull', 'dev', 'test/foo'] 44 | 45 | rev_parse_mock = mock.Mock() 46 | rev_parse_mock.stdout = b'd6ee34ae' 47 | 48 | images_mock = mock.Mock() 49 | images_mock.stdout.decode.return_value.split.return_value = [] 50 | 51 | pull_mock = mock.Mock() 52 | pull_mock.returncode = 0 53 | 54 | loo.cmd.side_effect = [rev_parse_mock, images_mock, pull_mock] 55 | 56 | with chdir('tests/test_roots/simple'): 57 | loo.main() 58 | 59 | # check the arguments docker was called with 60 | assert len(loo.cmd.call_args_list) == 3 61 | assert loo.cmd.call_args_list[2][0][0] == [ 62 | 'docker', 63 | 'pull', 64 | 'foo-registry.com/test/foo:d6ee34ae-dev', 65 | ] 66 | 67 | 68 | def test_pull_already_present(loo): 69 | sys.argv = ['vindaloo', '--noninteractive', 'pull', 'dev', 'test/foo'] 70 | 71 | rev_parse_mock = mock.Mock() 72 | rev_parse_mock.stdout = b'd6ee34ae' 73 | 74 | images_mock = mock.Mock() 75 | images_mock.stdout.decode.return_value.split.return_value = [ 76 | 'foo-registry.com/test/foo:d6ee34ae-dev', 77 | ] 78 | 79 | loo.cmd.side_effect = [rev_parse_mock, images_mock] 80 | 81 | with chdir('tests/test_roots/simple'): 82 | loo.main() 83 | 84 | # check the arguments docker was called with 85 | assert len(loo.cmd.call_args_list) == 2 86 | assert loo.cmd.call_args_list[1][0][0] == [ 87 | 'docker', 'images', '--format', '{{.Repository}}:{{.Tag}}' 88 | ] 89 | -------------------------------------------------------------------------------- /examples/multi-image/k8s/base.py: -------------------------------------------------------------------------------- 1 | import vindaloo 2 | 3 | versions = vindaloo.app.versions 4 | 5 | MAINTAINER = "Daniel Milde " 6 | 7 | CONFIG_WEB = { 8 | 'maintainer': MAINTAINER, 9 | 'version': versions['avengers/web'], 10 | 'image_name': 'avengers/web', 11 | 'https_proxy': "http://proxy.com:3128", 12 | } 13 | CONFIG_PROXY = { 14 | 'maintainer': MAINTAINER, 15 | 'version': versions['avengers/web-proxy'], 16 | 'image_name': 'avengers/web-proxy', 17 | } 18 | CONFIG_OUTAGE = { 19 | 'maintainer': MAINTAINER, 20 | 'version': versions['avengers/web-outage'], 21 | 'image_name': 'avengers/web-outage', 22 | } 23 | 24 | DEPLOYMENT_ADMINWEB = { 25 | 'replicas': 2, 26 | 'ident_label': "avengers-web", 27 | 'image_web': "{}:{}".format(CONFIG_WEB['image_name'], CONFIG_WEB['version']), 28 | 'image_proxy': "{}:{}".format(CONFIG_PROXY['image_name'], CONFIG_PROXY['version']), 29 | 'env': [ 30 | { 31 | 'key': 'ENVIRONMENT', 32 | 'val': "avengers-stable" 33 | }, 34 | { 35 | 'key': 'PORT', 36 | 'val': "8001" 37 | }, 38 | { 39 | 'key': 'http_proxy', 40 | 'val': "http://proxy:3128", 41 | }, 42 | { 43 | 'key': 'https_proxy', 44 | 'val': "http://proxy:3128", 45 | }, 46 | ], 47 | 'port_web': "8001", 48 | 'port_proxy': "8000", 49 | } 50 | 51 | DEPLOYMENT_OUTAGE = { 52 | 'replicas': 1, 53 | 'ident_label': "avengers-web-outage", 54 | 'image': "{}:{}".format(CONFIG_OUTAGE['image_name'], CONFIG_OUTAGE['version']), 55 | 'port': "8000", 56 | } 57 | 58 | SERVICE = { 59 | 'app_name': "avengers-web", 60 | 'ident_label': "avengers-web", 61 | 'ports': [ 62 | {'port': 8000, 'name': 'http'}, 63 | ] 64 | } 65 | 66 | DOCKER_FILES = [ 67 | { 68 | 'context_dir': "..", 69 | 'config': CONFIG_WEB, 70 | 'template': "Dockerfile.web", 71 | 'includes': { 72 | 'base_image': "../k8s-includes/BaseImage.include", 73 | }, 74 | 'pre_build_msg': """Prosim nejdriv spust (v Dockeru): 75 | 76 | make clean compile-messages 77 | cd web; make rights compile-production 78 | """ 79 | }, 80 | { 81 | 'context_dir': "..", 82 | 'config': CONFIG_PROXY, 83 | 'template': "Dockerfile.proxy", 84 | 'pre_build_msg': """Prosim nejdriv spust (v Dockeru): 85 | 86 | make clean 87 | cd web; make compile-production 88 | """ 89 | }, 90 | { 91 | 'context_dir': "..", 92 | 'config': CONFIG_OUTAGE, 93 | 'template': "Dockerfile.outage", 94 | } 95 | ] 96 | 97 | K8S_OBJECTS = { 98 | "deployment": [ 99 | { 100 | 'config': DEPLOYMENT_ADMINWEB, 101 | 'template': "deployment.yaml", 102 | }, 103 | { 104 | 'config': DEPLOYMENT_OUTAGE, 105 | 'template': "deployment.outage.yaml", 106 | }, 107 | ], 108 | "service": [ 109 | { 110 | 'config': SERVICE, 111 | 'template': "service.yaml", 112 | }, 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /examples/cron-jobs/k8s/base.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | import sys 4 | import os 5 | 6 | import versions 7 | 8 | from crontab import CRONS 9 | 10 | CAMEL_FORM = re.compile('([a-z0-9])([A-Z])') 11 | 12 | 13 | def get_uniq_name(name, cron_jobs_names): 14 | appendix = 0 15 | 16 | # camelCase to snake-case 17 | name = CAMEL_FORM.sub(r'\1-\2', name).lower().replace('_', '-') 18 | 19 | while True: 20 | full_name = '{}{}'.format( 21 | name, 22 | '-{}'.format(appendix) if appendix else '', 23 | ) 24 | if full_name in cron_jobs_names: 25 | appendix += 1 26 | else: 27 | break 28 | 29 | return full_name 30 | 31 | 32 | cron_jobs = {} 33 | 34 | for job in CRONS: 35 | if job.disabled: 36 | continue 37 | 38 | # we need unique name for every cron job 39 | name = get_uniq_name(job.name, list(cron_jobs.keys())) 40 | 41 | command = job.command.replace('\\', '\\\\').replace('"', '\"') 42 | 43 | cron_jobs[name] = { 44 | 'name': name, 45 | 'schedule': job.schedule, 46 | 'command': command, 47 | } 48 | 49 | CONFIG = { 50 | 'maintainer': "Daniel Milde ", 51 | 'version': versions['avengers/robot'], 52 | 'image_name': 'avengers/robot', 53 | } 54 | 55 | CRON_JOB = { 56 | 'image': "{}:{}".format(CONFIG['image_name'], CONFIG['version']), 57 | 'env': [ 58 | { 59 | 'key': 'ENVIRONMENT', 60 | 'val': "avengers-stable" 61 | }, 62 | ], 63 | } 64 | 65 | DOCKER_FILES = [ 66 | { 67 | 'context_dir': "..", 68 | 'config': CONFIG, 69 | 'template': "Dockerfile", 70 | 'includes': { 71 | 'base_image': "../k8s-includes/BaseImage.include", 72 | }, 73 | 'pre_build_msg': """Please run first: 74 | 75 | make clean 76 | """ 77 | }, 78 | ] 79 | 80 | K8S_OBJECTS = { 81 | "cronjob": [ 82 | { 83 | 'config': { 84 | 'ident_label': job['name'], 85 | 'image': CRON_JOB['image'], 86 | 'env': CRON_JOB['env'], 87 | 'schedule': job['schedule'], 88 | 'command': job['command'], 89 | }, 90 | 'template': "cron-job.yaml", 91 | } for job in cron_jobs.values() 92 | ] 93 | } 94 | 95 | # deploy one of cron-jobs as one time k8s job 96 | job_name = os.getenv('DEPLOY_JOB', '') 97 | if job_name: 98 | if job_name not in cron_jobs: 99 | raise Exception('Cron job "{}" not found. Use some of: {}'.format(job_name, list(cron_jobs.keys()))) 100 | 101 | job = cron_jobs[job_name] 102 | 103 | K8S_OBJECTS = { 104 | "job": [ 105 | { 106 | 'config': { 107 | 'ident_label': '{}-{}'.format(job['name'], int(datetime.datetime.now().timestamp())), 108 | 'image': CRON_JOB['image'], 109 | 'env': CRON_JOB['env'], 110 | 'schedule': job['schedule'], 111 | 'command': job['command'], 112 | }, 113 | 'template': "job.yaml", 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /vindaloo/examples.py: -------------------------------------------------------------------------------- 1 | EXAMPLE_VINDALOO_CONF = """ 2 | ENVS = {{ 3 | 'dev': {{ 4 | 'k8s_namespace': '{k8s_prefix}-dev', 5 | 'k8s_clusters': {k8s_clusters}, 6 | 'docker_registry': '{docker_registry}', 7 | }}, 8 | 'test': {{ 9 | 'k8s_namespace': '{k8s_prefix}-test', 10 | 'k8s_clusters': {k8s_clusters}, 11 | 'docker_registry': '{docker_registry}', 12 | }}, 13 | 'staging': {{ 14 | 'k8s_namespace': '{k8s_prefix}-staging', 15 | 'k8s_clusters': {k8s_clusters}, 16 | 'docker_registry': '{docker_registry}', 17 | }}, 18 | 'stable': {{ 19 | 'k8s_namespace': '{k8s_prefix}-stable', 20 | 'k8s_clusters': {k8s_clusters}, 21 | 'docker_registry': '{docker_registry}', 22 | }}, 23 | }} 24 | """ 25 | 26 | EXAMPLE_BASE = """ 27 | import vindaloo 28 | 29 | versions = vindaloo.app.versions 30 | 31 | CONFIG = {{ 32 | 'maintainer': "{maintainer_name} <{maintainer_email}>", 33 | 'version': versions['{image_name}'], 34 | 'image_name': '{image_name}', 35 | }} 36 | 37 | DEPLOYMENT = {{ 38 | 'replicas': 1, 39 | 'ident_label': "{ident_label}", 40 | 'image': "{{}}:{{}}".format(CONFIG['image_name'], CONFIG['version']), 41 | 'env': [ 42 | {{ 43 | 'key': 'ENVIRONMENT', 44 | 'val': "stable", # Pretizit v tech co podedi. 45 | }}, 46 | ], 47 | 'spec_annotations': [], 48 | }} 49 | 50 | DOCKER_FILES = [ 51 | {{ 52 | 'config': CONFIG, 53 | 'template': "Dockerfile", 54 | }}, 55 | ] 56 | 57 | K8S_OBJECTS = {{ 58 | "deployment": [ 59 | {{ 60 | 'config': DEPLOYMENT, 61 | 'template': "deployment.yaml", 62 | }}, 63 | ], 64 | }} 65 | 66 | """ 67 | 68 | EXAMPLE_DEV = """ 69 | from base import * 70 | 71 | 72 | DEPLOYMENT.update({ 73 | 'env': [ 74 | { 75 | 'key': 'ENVIRONMENT', 76 | 'val': "dev" 77 | }, 78 | ], 79 | }) 80 | 81 | """ 82 | 83 | EXAMPLE_DOCKERFILE = """FROM foo-registry.com/debian:stretch 84 | LABEL maintainer="{{{{{{maintainer}}}}}}" 85 | LABEL description="" 86 | 87 | EXPOSE 8000 88 | 89 | # Nasetujeme český UTF-8 locale a globální jazyk. 90 | RUN echo "cs_CZ.UTF-8 UTF-8" >> /etc/locale.gen 91 | RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 92 | RUN locale-gen 93 | # Tohle je nasetovani jazyka pouze lokalne behem buildu. 94 | ENV LANG="en_US.UTF-8" 95 | ENV LC_CTYPE="en_US.UTF-8" 96 | 97 | RUN apt-get update && apt-get upgrade -y 98 | 99 | LABEL version="{{{{version}}}}" 100 | """ 101 | 102 | EXAMPLE_DEPLOYMENT = """apiVersion: apps/v1 103 | kind: Deployment 104 | metadata: 105 | name: {{ident_label}} 106 | spec: 107 | replicas: {{replicas}} 108 | template: 109 | metadata: 110 | name: "{{ident_label}}" 111 | labels: 112 | app: "{{ident_label}}" 113 | annotations: 114 | {{#spec_annotations}} 115 | {{key}}: "{{val}}" 116 | {{/spec_annotations}} 117 | spec: 118 | containers: 119 | - name: "{{ident_label}}" 120 | image: {{registry}}/{{image}} 121 | env: 122 | {{#env}} 123 | - name: {{key}} 124 | value: {{val}} 125 | {{/env}} 126 | """ 127 | 128 | EXAMPLE_SERVICE = """apiVersion: v1 129 | kind: Service 130 | metadata: 131 | name: {{ident_label}} 132 | labels: 133 | app: {{app_label}} 134 | spec: 135 | type: NodePort 136 | ports: 137 | - port: 9213 138 | nodePort: 31515 139 | protocol: TCP 140 | selector: 141 | app: {{app_label}} 142 | """ 143 | -------------------------------------------------------------------------------- /examples/class-config/k8s/base.py: -------------------------------------------------------------------------------- 1 | import vindaloo 2 | from vindaloo.objects import Deployment, Service 3 | 4 | versions = vindaloo.app.versions 5 | 6 | CONFIG = { 7 | 'maintainer': "Avengers ", 8 | 'version': versions['avengers/server'], 9 | 'image_name': 'avengers/server', 10 | } 11 | 12 | ENV_PUBLIC = { 13 | 'ENVIRONMENT': "avengers-stable", 14 | 'WEB_CONCURRENCY': "15", 15 | 'VERSION': CONFIG['version'], 16 | } 17 | 18 | ENV_PRIVATE = { 19 | **ENV_PUBLIC, 20 | 'WEB_CONCURRENCY': "40", 21 | 'REGISTER_PRIVATE_METHODS': "1", 22 | } 23 | 24 | DEPLOYMENT_PUBLIC = Deployment( 25 | name="avengers-server", 26 | replicas=4, 27 | annotations={ 28 | 'team': "avengers@domain.com", 29 | }, 30 | volumes={ 31 | 'localconfig': { 32 | 'secret': { 33 | 'secretName': "avengers-local-conf", 34 | } 35 | } 36 | }, 37 | containers={ 38 | 'avengers-server': { 39 | 'image': "{}:{}".format(CONFIG['image_name'], CONFIG['version']), 40 | 'ports': { 41 | 'proxy': 3550, 42 | }, 43 | 'env': ENV_PUBLIC, 44 | 'volumeMounts': { 45 | 'localconfig': { 46 | 'mountPath': "/local.conf", 47 | 'subPath': "local.conf", 48 | } 49 | }, 50 | 'lifecycle': { 51 | 'preStop': { 52 | 'exec': { 53 | 'command': ["/bin/sh", "-c", "sleep 20; kill -s TERM `cat /gunicorn.pid`"] 54 | } 55 | } 56 | }, 57 | 'livenessProbe': { 58 | 'initialDelaySeconds': 15, 59 | 'periodSeconds': 10, 60 | 'timeoutSeconds': 3, 61 | 'httpGet': { 62 | 'path': "/selfcheck", 63 | 'port': 3550, 64 | } 65 | }, 66 | 'readinessProbe': { 67 | 'initialDelaySeconds': 15, 68 | 'periodSeconds': 10, 69 | 'timeoutSeconds': 3, 70 | 'httpGet': { 71 | 'path': "/ready", 72 | 'port': 3550, 73 | } 74 | }, 75 | 'resources': { 76 | 'limits': { 77 | 'cpu': "6", 78 | 'memory': "6000Mi", 79 | }, 80 | 'requests': { 81 | 'cpu': "3", 82 | 'memory': "3000Mi", 83 | } 84 | }, 85 | }, 86 | }, 87 | ) 88 | 89 | DEPLOYMENT_PRIVATE = DEPLOYMENT_PUBLIC.clone() 90 | DEPLOYMENT_PRIVATE.set_name("avengers-server-private") 91 | DEPLOYMENT_PRIVATE.spec.template.spec.containers['avengers-server']['env'] = ENV_PRIVATE 92 | 93 | SERVICE_PUBLIC = Service( 94 | name="avengers-server", 95 | service_type="NodePort", 96 | selector={ 97 | 'app': "avengers-server", 98 | }, 99 | ports={ 100 | 'rpc': {'port': 3550, 'protocol': 'TCP'}, 101 | } 102 | ) 103 | 104 | SERVICE_PRIVATE = SERVICE_PUBLIC.clone() 105 | SERVICE_PRIVATE.set_name("avengers-server-private") 106 | 107 | DOCKER_FILES = [ 108 | { 109 | 'context_dir': "..", 110 | 'config': CONFIG, 111 | 'template': "Dockerfile", 112 | 'includes': { 113 | 'base_image': "../k8s-includes/BaseImage.include", 114 | }, 115 | }, 116 | ] 117 | 118 | K8S_OBJECTS = { 119 | "deployment": [ 120 | DEPLOYMENT_PUBLIC, 121 | DEPLOYMENT_PRIVATE, 122 | ], 123 | "service": [ 124 | SERVICE_PUBLIC, 125 | SERVICE_PRIVATE, 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 4.5.0 2 | 3 | * Added support for PodMonitor and ServiceMonitor objects 4 | 5 | # Version 4.4.0 6 | 7 | * Waiting for deployments to rollout now checks if succesfully rolled out. 8 | 9 | # Version 4.3.0 10 | 11 | * Creation of a Kube Job will now wait until the job is completed (either successfully or unsuccessfully) in 12 | the same manner as it is waiting for deployments to rollout. 13 | 14 | # Version 4.2.0 15 | 16 | * added support for current git commit hash as image tag 17 | 18 | E.g. using `versions.json` 19 | ``` 20 | { 21 | "test/foo": "{{git}}-dev" 22 | } 23 | ``` 24 | 25 | vindaloo will replace the `{{git}}` placeholder with 8 letters of current git hash 26 | resulting in e.g. `test/foo:d6ee34ae-dev`. 27 | 28 | 29 | # Version 4.1.1 30 | 31 | * fix: `ConfigMap` added to `__all__` in `objects.py` 32 | 33 | # Version 4.1.0 34 | 35 | * added support for ConfigMaps objects 36 | 37 | ```python 38 | from vindaloo.objects import ConfigMap 39 | 40 | CONTEXT = {'variable_1': 'This value depends on the selected environment.'} 41 | 42 | CONFIG_MAP = ConfigMap( 43 | name='test-config-map', 44 | metadata={ 45 | 'labels': {'custom-labels': '123'}, 46 | 'annotations': {'custom-annotations': '...'}, 47 | }, 48 | data={ 49 | 'simple_config_key': 123, 50 | 'file_config_key': { 51 | 'file': 'templates/file_config.conf', 52 | 'config': CONTEXT, 53 | }, 54 | }, 55 | binary_data={ 56 | 'simple_binary_key': b'\x76\x69\x6b\x79', 57 | 'binary_file_config_key': { 58 | 'file': 'templates/binary_config.conf', 59 | }, 60 | }, 61 | immutable=True, 62 | ) 63 | 64 | K8S_OBJECTS = { 65 | "configmap": [CONFIG_MAP], 66 | } 67 | ``` 68 | 69 | * orphaned `pystache` library replaced with `chevron` 70 | 71 | # Version 4.0.0 72 | 73 | ## Breaking changes 74 | 75 | * support for Python 3.5 is dropped 76 | 77 | * `import versions` is replaced by: 78 | 79 | ```python 80 | import vindaloo 81 | versions = vindaloo.app.versions 82 | ``` 83 | 84 | * attributes (`containers`, `ports` ...) on `Deployment`, `Service`, `CronJob`, 85 | e.g. are moved to full path corresponding to structure of k8s manifest: 86 | 87 | ```python 88 | -JOB.containers['foo']['command'] 89 | +JOB.spec.template.spec.containers['foo']['command'] 90 | -SERVICE.load_balancer_ip 91 | +SERVICE.spec.loadBalancerIP 92 | -SERVICE.ports['http']['nodePort' 93 | +SERVICE.spec.ports['http']['nodePort'] 94 | ``` 95 | 96 | * init parameter `annotations` of `Deployment`, `CronJob` and `Job` now sets 97 | `metadata.annotations` in manifest. 98 | 99 | * init parameter `spec_annotations` to `Deployment`, `CronJob` and `Job` is added 100 | which sets: 101 | * `spec.template.metadata.annotations` in `Deployment` 102 | * `spec.jobTemplate.spec.template.metadata.annotations` in `CronJob` 103 | 104 | ## Improvements 105 | 106 | * Any parameter in manifest spec can now be set, user is not limited to attributes on `Deployment` e.g. 107 | 108 | ```python 109 | DEPLOYMENT.spec.something.foo.bar = 'boo' 110 | ``` 111 | 112 | * Objects can be created without init params: 113 | 114 | ```python 115 | SERVICE = Service() 116 | SERVICE.set_name("foo") 117 | SERVICE.spec.type = "NodePort" 118 | SERVICE.spec.selector = {'app': "foo"} 119 | SERVICE.spec.ports = List({ 120 | 'http': {'port': 5001, 'targetPort': 5001, 'protocol': 'TCP'}, 121 | }) 122 | SERVICE.metadata.annotations.loadbalancer = "enabled" 123 | ``` 124 | 125 | * Method `set_name` is added to ease renaming and cloning: 126 | 127 | ```python 128 | DEPLOYMENT2 = DEPLOYMENT.clone() 129 | -DEPLOYMENT2.name = "sos-adminserver-private" 130 | -DEPLOYMENT2.metadata = {'name': DEPLOYMENT2.name} 131 | -DEPLOYMENT2.labels = {'app': DEPLOYMENT2.name} 132 | +DEPLOYMENT2.set_name("foo2") 133 | ``` 134 | -------------------------------------------------------------------------------- /tests/test_roots/obj-config-wo-init/k8s/base.py: -------------------------------------------------------------------------------- 1 | import vindaloo 2 | from vindaloo import Container, CronJob, Deployment, Job, List, Service 3 | 4 | versions = vindaloo.app.versions 5 | 6 | CONFIG = { 7 | 'maintainer': "Foo ", 8 | 'version': versions['test/foo'], 9 | 'image_name': 'test/foo', 10 | } 11 | 12 | DEPLOYMENT = Deployment() 13 | DEPLOYMENT.set_name("foo") 14 | DEPLOYMENT.spec.template.spec.volumes = List({ 15 | 'localconfig': {'secret': {'secretName': "local-conf"}}, 16 | 'cert': {'secret': {'secretName': "cert"}}, 17 | }) 18 | 19 | CONTAINER = Container() 20 | CONTAINER.image = f"{CONFIG['image_name']}:{CONFIG['version']}" 21 | CONTAINER.volumeMounts = List({ 22 | 'cert': [ 23 | {'mountPath': "/cert.pem", 'subPath': "tls.crt"}, 24 | {'mountPath': "/key.pem", 'subPath': "tls.key"}, 25 | ], 26 | }) 27 | CONTAINER.env = List({ 28 | 'ENV': 'stable', 29 | }) 30 | CONTAINER.ports = List({ 31 | 'proxy': { 32 | 'containerPort': 5001 33 | }, 34 | 'server': { 35 | 'containerPort': 5000, 36 | 'protocol': 'UDP', 37 | } 38 | }) 39 | 40 | DEPLOYMENT.spec.template.spec.containers = List({'foo': CONTAINER}) 41 | DEPLOYMENT.spec.template.metadata.annotations['log-retention'] = '3w' 42 | DEPLOYMENT.metadata.annotations['deploy-cluster'] = 'cluster2' 43 | DEPLOYMENT.spec.something.foo = 'boo' 44 | 45 | CRONJOB_CONTAINER = Container() 46 | CRONJOB_CONTAINER.image = "!registry.hub.docker.com/library/busybox:latest" 47 | CRONJOB_CONTAINER.command = ['echo', 'x'] 48 | CRONJOB_CONTAINER.env = List({ 49 | 'ENV': "stable", 50 | 'DB_PASSWORD': { 51 | 'valueFrom': { 52 | 'secretKeyRef': { 53 | 'name': 'db-master', 54 | 'key': 'password', 55 | } 56 | } 57 | }, 58 | }) 59 | CRONJOB_CONTAINER.volumeMounts = List({ 60 | 'localconfig': { 61 | 'mountPath': "/app.local.conf", 62 | 'subPath': "app.local.conf", 63 | }, 64 | }) 65 | 66 | CRONJOB = CronJob() 67 | CRONJOB.set_name("foo") 68 | CRONJOB.spec.schedule = "0 0 * * *" 69 | CRONJOB.spec.jobTemplate.spec.template.spec.volumes = List({ 70 | 'localconfig': { 71 | 'secret': { 72 | 'secretName': "local-conf", 73 | } 74 | } 75 | }) 76 | CRONJOB.spec.jobTemplate.spec.template.spec.containers = List({'foo': CRONJOB_CONTAINER}) 77 | 78 | JOB_CONTAINER = Container() 79 | JOB_CONTAINER.image = f"{CONFIG['image_name']}:{CONFIG['version']}" 80 | JOB_CONTAINER.command = ['echo', 'x'] 81 | JOB_CONTAINER.env = List({ 82 | 'ENV': "stable", 83 | 'DB_PASSWORD': { 84 | 'valueFrom': { 85 | 'secretKeyRef': { 86 | 'name': 'db-master', 87 | 'key': 'password', 88 | } 89 | } 90 | }, 91 | }) 92 | 93 | JOB1 = Job() 94 | JOB1.set_name("foo") 95 | JOB1.spec.template.spec.volumes = List({ 96 | 'localconfig': { 97 | 'secret': { 98 | 'secretName': "local-conf", 99 | } 100 | } 101 | }) 102 | JOB1.spec.template.spec.containers = List({'foo': JOB_CONTAINER}) 103 | 104 | JOB2 = JOB1.clone() 105 | JOB2.set_name('bar') 106 | JOB2.spec.template.spec.containers['foo']['command'] = ['echo', 'y'] 107 | 108 | SERVICE = Service() 109 | SERVICE.set_name("foo") 110 | SERVICE.spec.type = "NodePort" 111 | SERVICE.spec.selector = {'app': "foo"} 112 | SERVICE.spec.ports = List({ 113 | 'http': {'port': 5001, 'targetPort': 5001, 'protocol': 'TCP'}, 114 | }) 115 | SERVICE.metadata.annotations.loadbalancer = "enabled" 116 | 117 | DOCKER_FILES = [ 118 | { 119 | 'context_dir': "..", 120 | 'config': CONFIG, 121 | 'template': "Dockerfile", 122 | }, 123 | ] 124 | 125 | K8S_OBJECTS = { 126 | 'deployment': [DEPLOYMENT], 127 | 'cronjob': [CRONJOB], 128 | 'job': [JOB1, JOB2], 129 | 'service': [ 130 | SERVICE, 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /vindaloo/convert.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | import vindaloo 5 | 6 | from .objects import ( 7 | JsonSerializable, 8 | Deployment, 9 | Service, 10 | ) 11 | 12 | TAB = " " 13 | 14 | 15 | def get_obj_repr_from_dict(data) -> JsonSerializable: 16 | kind = data.get('kind') 17 | 18 | if kind == 'Deployment': 19 | res = get_deployment_obj_repr_from_dict(data) 20 | elif kind == 'Service': 21 | res = get_service_obj_repr_from_dict(data) 22 | else: 23 | raise Exception(f"Kind '{kind}' is not supported yet.") 24 | 25 | content = f""" 26 | import vindaloo 27 | from vindaloo.objects import {kind} 28 | 29 | versions = vindaloo.app.versions 30 | 31 | {kind.upper()} = {res} 32 | 33 | K8S_OBJECTS = {{ 34 | "{kind.lower()}": [{kind.upper()}], 35 | }} 36 | """ 37 | return content 38 | 39 | 40 | def get_deployment_obj_repr_from_dict(data) -> Deployment: 41 | metadata = data.get('metadata', {}) 42 | metadata.get('labels', {}).pop('app', None) 43 | name = metadata.pop('name', None) 44 | 45 | template = data.get('spec', {}).get('template', {}) 46 | template_metadata = template.get('metadata', {}) 47 | template_metadata.get('labels', {}).pop('app') 48 | 49 | template_spec = template.get('spec', {}) 50 | 51 | containers = create_dict_from_named_list(template_spec.get('containers', [])) 52 | containers_string = "{\n" 53 | 54 | for name, container in containers.items(): 55 | image = '' 56 | if container.get('image'): 57 | try: 58 | v = vindaloo.vindaloo.Vindaloo() 59 | v._import_envs_config() 60 | container['image'] = v._strip_image_name(container['image']) 61 | except Exception as ex: 62 | logging.warning(str(ex)) 63 | image, version = container['image'].split(':') 64 | add_version(image, version) 65 | 66 | dict_ports = {} 67 | for port in container.get('ports', []): 68 | if port.get('name'): 69 | dict_ports[port['name']] = port 70 | port.pop('name') 71 | 72 | containers_string += TAB * 2 + f"'{name}': {{\n" 73 | containers_string += TAB * 3 + f"""'image': "{image}:" + versions['{image}'],\n""" 74 | containers_string += TAB * 3 + f"'ports': {dict_ports or container.get('ports', [])},\n" 75 | containers_string += TAB * 3 + f"'env': {create_dict_from_name_value_list(container.get('env', []))},\n" 76 | containers_string += TAB * 2 + "},\n" 77 | containers_string += TAB + "}" 78 | 79 | termination = template_spec.get('terminationGracePeriodSeconds', 30) 80 | 81 | res = f"""Deployment( 82 | name="{name}", 83 | replicas={data.get('spec', {}).get('replicas')}, 84 | containers={containers_string or None}, 85 | volumes=None, 86 | annotations=None, 87 | metadata={metadata or None}, 88 | labels={template_metadata.get('labels') or None}, 89 | spec_annotations={template_metadata.get('annotations') or None}, 90 | termination_grace_period={termination}, 91 | )""" 92 | return res 93 | 94 | 95 | def get_service_obj_repr_from_dict(data) -> Service: 96 | pass 97 | 98 | 99 | def create_dict_from_named_list(data): 100 | res = {} 101 | for item in data: 102 | name = item.pop('name') 103 | res[name] = item 104 | return res 105 | 106 | 107 | def create_dict_from_name_value_list(data): 108 | res = {} 109 | for item in data: 110 | name = item.pop('name') 111 | res[name] = item.get('value') 112 | return res 113 | 114 | 115 | def add_version(image, version): 116 | file = f'{vindaloo.vindaloo.CONFIG_DIR}/versions.json' 117 | 118 | with open(file, 'r+') as fp: 119 | versions = json.load(fp) 120 | versions[image] = version 121 | fp.seek(0) 122 | json.dump(versions, fp) 123 | -------------------------------------------------------------------------------- /tests/test_roots/obj-config/k8s/base.py: -------------------------------------------------------------------------------- 1 | import vindaloo 2 | from vindaloo.objects import Deployment, Service, CronJob, Job 3 | 4 | versions = vindaloo.app.versions 5 | 6 | CONFIG = { 7 | 'maintainer': "Foo ", 8 | 'version': versions['test/foo'], 9 | 'image_name': 'test/foo', 10 | } 11 | 12 | ENV = { 13 | 'ENV': "stable", 14 | 'DB_PASSWORD': { 15 | 'valueFrom': { 16 | 'secretKeyRef': { 17 | 'name': 'db-master', 18 | 'key': 'password', 19 | } 20 | } 21 | }, 22 | } 23 | 24 | DEPLOYMENT = Deployment( 25 | name="foo", 26 | replicas=2, 27 | volumes={ 28 | 'localconfig': { 29 | 'secret': { 30 | 'secretName': "local-conf", 31 | } 32 | }, 33 | 'cert': { 34 | 'secret': { 35 | 'secretName': "cert", 36 | } 37 | }, 38 | }, 39 | containers={ 40 | 'foo': { 41 | 'image': "{}:{}".format(CONFIG['image_name'], CONFIG['version']), 42 | 'ports': { 43 | 'proxy': 5001, 44 | 'server': { 45 | 'protocol': 'UDP', 46 | 'containerPort': 5000, 47 | } 48 | }, 49 | 'env': ENV, 50 | 'volumeMounts': { 51 | 'cert': [ 52 | { 53 | 'mountPath': "/cert.pem", 54 | 'subPath': "tls.crt", 55 | }, 56 | { 57 | 'mountPath': "/key.pem", 58 | 'subPath': "tls.key", 59 | }, 60 | ], 61 | }, 62 | 'livenessProbe': { 63 | 'initialDelaySeconds': 30, 64 | 'periodSeconds': 30, 65 | 'timeoutSeconds': 10, 66 | 'httpGet': { 67 | 'path': "/", 68 | 'port': 5001, 69 | } 70 | }, 71 | }, 72 | }, 73 | annotations={"deploy-cluster": "cluster1"}, 74 | spec_annotations={"log-retention": "3w"} 75 | ) 76 | DEPLOYMENT.spec.something = {'foo': 'boo'} 77 | 78 | CRONJOB = CronJob( 79 | name="foo", 80 | schedule="0 0 * * *", 81 | volumes={ 82 | 'localconfig': { 83 | 'secret': { 84 | 'secretName': "local-conf", 85 | } 86 | } 87 | }, 88 | containers={ 89 | 'foo': { 90 | 'image': "!registry.hub.docker.com/library/busybox:latest", 91 | 'command': ['echo', 'x'], 92 | 'env': { 93 | 'ENV': "stable", 94 | 'DB_PASSWORD': { 95 | 'valueFrom': { 96 | 'secretKeyRef': { 97 | 'name': 'db-master', 98 | 'key': 'password', 99 | } 100 | } 101 | }, 102 | }, 103 | 'volumeMounts': { 104 | 'localconfig': { 105 | 'mountPath': "/app.local.conf", 106 | 'subPath': "app.local.conf", 107 | }, 108 | }, 109 | }, 110 | }, 111 | ) 112 | 113 | JOB1 = Job( 114 | name="foo", 115 | volumes={ 116 | 'localconfig': { 117 | 'secret': { 118 | 'secretName': "local-conf", 119 | } 120 | } 121 | }, 122 | containers={ 123 | 'foo': { 124 | 'image': "{}:{}".format(CONFIG['image_name'], CONFIG['version']), 125 | 'command': ['echo', 'x'], 126 | 'env': { 127 | 'ENV': "stable", 128 | 'DB_PASSWORD': { 129 | 'valueFrom': { 130 | 'secretKeyRef': { 131 | 'name': 'db-master', 132 | 'key': 'password', 133 | } 134 | } 135 | }, 136 | }, 137 | }, 138 | }, 139 | ) 140 | 141 | JOB2 = JOB1.clone() 142 | JOB2.name = 'bar' 143 | JOB2.spec.template.metadata.name = 'bar' 144 | JOB2.spec.template.spec.containers['foo']['command'] = ['echo', 'y'] 145 | 146 | SERVICE = Service( 147 | name="foo", 148 | service_type="NodePort", 149 | selector={ 150 | 'app': "foo", 151 | }, 152 | ports={ 153 | 'http': {'port': 5001, 'targetPort': 5001, 'protocol': 'TCP'}, 154 | }, 155 | annotations={'loadbalancer': "enabled"}, 156 | ) 157 | 158 | DOCKER_FILES = [ 159 | { 160 | 'context_dir': "..", 161 | 'config': CONFIG, 162 | 'template': "Dockerfile", 163 | }, 164 | ] 165 | 166 | K8S_OBJECTS = { 167 | "deployment": [DEPLOYMENT], 168 | "cronjob": [CRONJOB], 169 | "job": [JOB1, JOB2], 170 | "service": [ 171 | SERVICE, 172 | ] 173 | } 174 | -------------------------------------------------------------------------------- /tests/test_build.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import mock 3 | 4 | from utils import chdir 5 | 6 | 7 | def test_build_all(loo): 8 | # fake parameters 9 | sys.argv = ['vindaloo', '--noninteractive', 'build', 'dev'] 10 | 11 | rev_parse_mock = mock.Mock() 12 | rev_parse_mock.stdout = b'd6ee34ae' 13 | build_mock = mock.Mock() 14 | build_mock.returncode = 0 15 | loo.cmd.side_effect = [rev_parse_mock, build_mock, build_mock] 16 | 17 | with chdir('tests/test_roots/simple'): 18 | loo.main() 19 | 20 | # check the parameters docker was called with 21 | rev_parse_cmd = loo.cmd.call_args_list[0][0][0] 22 | build_cmd = loo.cmd.call_args_list[1][0][0] 23 | build_cmd2 = loo.cmd.call_args_list[2][0][0] 24 | 25 | assert rev_parse_cmd == [ 26 | 'git', 27 | 'rev-parse', 28 | '--short=8', 29 | 'HEAD' 30 | ] 31 | assert build_cmd == [ 32 | 'docker', 33 | 'build', 34 | '-t', 'foo-registry.com/test/foo:d6ee34ae-dev', 35 | '--no-cache', 36 | '-f', 'Dockerfile', 37 | '.' 38 | ] 39 | assert build_cmd2 == [ 40 | 'docker', 41 | 'build', 42 | '-t', 'foo-registry.com/test/bar:2.0.0', 43 | '--no-cache', 44 | '-f', 'Dockerfile', 45 | '.' 46 | ] 47 | 48 | # check generated Dockerfile 49 | with open('tests/test_roots/simple/Dockerfile', 'r') as fp: 50 | assert fp.read() == """LABEL maintainer="Test Test " 51 | LABEL description="Bar" 52 | LABEL version="2.0.0" 53 | """ 54 | 55 | 56 | def test_build_one(loo): 57 | sys.argv = ['vindaloo', '--noninteractive', 'build', 'dev', 'test/foo'] 58 | 59 | rev_parse_mock = mock.Mock() 60 | rev_parse_mock.stdout = b'd6ee34ae' 61 | build_mock = mock.Mock() 62 | build_mock.returncode = 0 63 | loo.cmd.side_effect = [rev_parse_mock, build_mock] 64 | 65 | with chdir('tests/test_roots/simple'): 66 | loo.main() 67 | 68 | # check the parameters docker was called with 69 | rev_parse_cmd = loo.cmd.call_args_list[0][0][0] 70 | build_cmd = loo.cmd.call_args_list[1][0][0] 71 | 72 | assert rev_parse_cmd == [ 73 | 'git', 74 | 'rev-parse', 75 | '--short=8', 76 | 'HEAD' 77 | ] 78 | assert build_cmd == [ 79 | 'docker', 80 | 'build', 81 | '-t', 'foo-registry.com/test/foo:d6ee34ae-dev', 82 | '--no-cache', 83 | '-f', 'Dockerfile', 84 | '.' 85 | ] 86 | 87 | # check generated Dockerfile 88 | with open('tests/test_roots/simple/Dockerfile', 'r') as fp: 89 | assert fp.read() == """FROM debian 90 | 91 | LABEL maintainer="Test Test " 92 | LABEL description="Foo" 93 | LABEL version="d6ee34ae-dev" 94 | """ 95 | 96 | 97 | def test_build_latest(loo): 98 | sys.argv = ['vindaloo', '--noninteractive', 'build', '--latest', 'dev', 'test/foo'] 99 | 100 | rev_parse_mock = mock.Mock() 101 | rev_parse_mock.stdout = b'd6ee34ae' 102 | build_mock = mock.Mock() 103 | build_mock.returncode = 0 104 | loo.cmd.side_effect = [rev_parse_mock, build_mock] 105 | 106 | with chdir('tests/test_roots/simple'): 107 | loo.main() 108 | 109 | # check the parameters docker was called with 110 | rev_parse_cmd = loo.cmd.call_args_list[0][0][0] 111 | build_cmd = loo.cmd.call_args_list[1][0][0] 112 | 113 | assert rev_parse_cmd == [ 114 | 'git', 115 | 'rev-parse', 116 | '--short=8', 117 | 'HEAD' 118 | ] 119 | assert build_cmd == [ 120 | 'docker', 121 | 'build', 122 | '-t', 'foo-registry.com/test/foo:d6ee34ae-dev', 123 | '--no-cache', 124 | '-t', 'foo-registry.com/test/foo:latest', 125 | '-f', 'Dockerfile', 126 | '.' 127 | ] 128 | 129 | # check generated Dockerfile 130 | with open('tests/test_roots/simple/Dockerfile', 'r') as fp: 131 | assert fp.read() == """FROM debian 132 | 133 | LABEL maintainer="Test Test " 134 | LABEL description="Foo" 135 | LABEL version="d6ee34ae-dev" 136 | """ 137 | 138 | 139 | def test_build_latest_with_cache(loo): 140 | sys.argv = ['vindaloo', '--noninteractive', 'build', '--cache', '--latest', 'dev', 'test/foo'] 141 | 142 | rev_parse_mock = mock.Mock() 143 | rev_parse_mock.stdout = b'd6ee34ae' 144 | build_mock = mock.Mock() 145 | build_mock.returncode = 0 146 | loo.cmd.side_effect = [rev_parse_mock, build_mock] 147 | 148 | with chdir('tests/test_roots/simple'): 149 | loo.main() 150 | 151 | # check the parameters docker was called with 152 | rev_parse_cmd = loo.cmd.call_args_list[0][0][0] 153 | build_cmd = loo.cmd.call_args_list[1][0][0] 154 | 155 | assert rev_parse_cmd == [ 156 | 'git', 157 | 'rev-parse', 158 | '--short=8', 159 | 'HEAD' 160 | ] 161 | assert build_cmd == [ 162 | 'docker', 163 | 'build', 164 | '-t', 'foo-registry.com/test/foo:d6ee34ae-dev', 165 | '-t', 'foo-registry.com/test/foo:latest', 166 | '-f', 'Dockerfile', 167 | '.' 168 | ] 169 | 170 | # check generated Dockerfile 171 | with open('tests/test_roots/simple/Dockerfile', 'r') as fp: 172 | assert fp.read() == """FROM debian 173 | 174 | LABEL maintainer="Test Test " 175 | LABEL description="Foo" 176 | LABEL version="d6ee34ae-dev" 177 | """ 178 | -------------------------------------------------------------------------------- /tests/test_push.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import mock 3 | 4 | from utils import chdir 5 | 6 | 7 | def test_push_all(loo): 8 | # fake arguments 9 | sys.argv = ['vindaloo', '--noninteractive', 'push', 'dev'] 10 | 11 | rev_parse_mock = mock.Mock() 12 | rev_parse_mock.stdout = b'd6ee34ae' 13 | 14 | images_mock = mock.Mock() 15 | images_mock.stdout.decode.return_value.split.return_value = [ 16 | 'foo-registry.com/test/foo:d6ee34ae-dev', 17 | 'foo-registry.com/test/bar:2.0.0', 18 | ] 19 | 20 | push_mock = mock.Mock() 21 | push_mock.returncode = 0 22 | 23 | loo.cmd.side_effect = [rev_parse_mock, images_mock, push_mock, push_mock] 24 | 25 | with chdir('tests/test_roots/simple'): 26 | loo.main() 27 | 28 | # check the arguments docker was called with 29 | assert len(loo.cmd.call_args_list) == 4 30 | push_cmd = loo.cmd.call_args_list[2][0][0] 31 | push2_cmd = loo.cmd.call_args_list[3][0][0] 32 | assert push_cmd == [ 33 | 'docker', 34 | 'push', 35 | 'foo-registry.com/test/foo:d6ee34ae-dev', 36 | ] 37 | assert push2_cmd == [ 38 | 'docker', 39 | 'push', 40 | 'foo-registry.com/test/bar:2.0.0', 41 | ] 42 | 43 | 44 | def test_push_one(loo): 45 | sys.argv = ['vindaloo', '--noninteractive', 'push', 'dev', 'test/foo'] 46 | 47 | rev_parse_mock = mock.Mock() 48 | rev_parse_mock.stdout = b'd6ee34ae' 49 | 50 | images_mock = mock.Mock() 51 | images_mock.stdout.decode.return_value.split.return_value = [ 52 | 'foo-registry.com/test/foo:d6ee34ae-dev', 53 | 'foo-registry.com/test/bar:2.0.0', 54 | ] 55 | 56 | push_mock = mock.Mock() 57 | push_mock.returncode = 0 58 | 59 | loo.cmd.side_effect = [rev_parse_mock, images_mock, push_mock] 60 | 61 | with chdir('tests/test_roots/simple'): 62 | loo.main() 63 | 64 | # check the arguments docker was called with 65 | assert len(loo.cmd.call_args_list) == 3 66 | assert loo.cmd.call_args_list[2][0][0] == [ 67 | 'docker', 68 | 'push', 69 | 'foo-registry.com/test/foo:d6ee34ae-dev', 70 | ] 71 | 72 | 73 | def test_push_not_built_image(loo): 74 | sys.argv = ['vindaloo', '--noninteractive', 'push', 'dev', 'test/foo'] 75 | 76 | rev_parse_mock = mock.Mock() 77 | rev_parse_mock.stdout = b'd6ee34ae' 78 | 79 | images_mock = mock.Mock() 80 | images_mock.stdout.decode.return_value.split.return_value = [ 81 | 'foo-registry.com/test/foo:0.0.9', # je ubildena jina verze 82 | 'foo-registry.com/test/bar:2.0.0', 83 | ] 84 | 85 | loo.cmd.side_effect = [rev_parse_mock, images_mock] 86 | 87 | with chdir('tests/test_roots/simple'): 88 | loo.main() 89 | 90 | # check the arguments docker was called with 91 | assert len(loo.cmd.call_args_list) == 2 92 | assert loo.cmd.call_args_list[0][0][0] == [ 93 | 'git', 'rev-parse', '--short=8', 'HEAD' 94 | ] 95 | assert loo.cmd.call_args_list[1][0][0] == [ 96 | 'docker', 'images', '--format', '{{.Repository}}:{{.Tag}}' 97 | ] 98 | 99 | 100 | def test_push_latest(loo): 101 | sys.argv = ['vindaloo', '--noninteractive', 'push', '--latest', 'dev', 'test/foo'] 102 | 103 | rev_parse_mock = mock.Mock() 104 | rev_parse_mock.stdout = b'd6ee34ae' 105 | 106 | images_mock = mock.Mock() 107 | images_mock.stdout.decode.return_value.split.return_value = [ 108 | 'foo-registry.com/test/foo:d6ee34ae-dev', 109 | 'foo-registry.com/test/bar:2.0.0', 110 | ] 111 | 112 | push_mock = mock.Mock() 113 | push_mock.returncode = 0 114 | 115 | loo.cmd.side_effect = [rev_parse_mock, images_mock, push_mock, push_mock] 116 | 117 | with chdir('tests/test_roots/simple'): 118 | loo.main() 119 | 120 | # check the arguments docker was called with 121 | assert len(loo.cmd.call_args_list) == 4 122 | push_cmd = loo.cmd.call_args_list[2][0][0] 123 | push2_cmd = loo.cmd.call_args_list[3][0][0] 124 | 125 | assert push_cmd == [ 126 | 'docker', 127 | 'push', 128 | 'foo-registry.com/test/foo:d6ee34ae-dev', 129 | ] 130 | assert push2_cmd == [ 131 | 'docker', 132 | 'push', 133 | 'foo-registry.com/test/foo:latest', 134 | ] 135 | 136 | 137 | def test_push_with_registry(loo): 138 | sys.argv = ['vindaloo', '--noninteractive', 'push', '--registry', 'foo-prog-registry.com', 'dev', 'test/foo'] 139 | 140 | rev_parse_mock = mock.Mock() 141 | rev_parse_mock.stdout = b'd6ee34ae' 142 | 143 | images_mock = mock.Mock() 144 | images_mock.stdout.decode.return_value.split.return_value = [ 145 | 'foo-registry.com/test/foo:d6ee34ae-dev', 146 | 'foo-registry.com/test/bar:2.0.0', 147 | ] 148 | 149 | tag_mock = mock.Mock() 150 | tag_mock.returncode = 0 151 | 152 | push_mock = mock.Mock() 153 | push_mock.returncode = 0 154 | 155 | loo.cmd.side_effect = [rev_parse_mock, images_mock, tag_mock, push_mock] 156 | 157 | with chdir('tests/test_roots/simple'): 158 | loo.main() 159 | 160 | # check the arguments docker was called with 161 | assert len(loo.cmd.call_args_list) == 4 162 | tag_cmd = loo.cmd.call_args_list[2][0][0] 163 | push_cmd = loo.cmd.call_args_list[3][0][0] 164 | 165 | assert tag_cmd == [ 166 | 'docker', 167 | 'tag', 168 | 'foo-registry.com/test/foo:d6ee34ae-dev', 169 | 'foo-prog-registry.com/test/foo:d6ee34ae-dev', 170 | ] 171 | assert push_cmd == [ 172 | 'docker', 173 | 'push', 174 | 'foo-prog-registry.com/test/foo:d6ee34ae-dev', 175 | ] 176 | -------------------------------------------------------------------------------- /tests/test_versions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from unittest import mock 4 | 5 | from utils import chdir 6 | from vindaloo.vindaloo import Vindaloo 7 | 8 | 9 | def test_versions_match(capsys): 10 | # fake arguments 11 | sys.argv = ['vindaloo', 'versions'] 12 | 13 | calls = [mock.Mock() for _ in range(6)] 14 | for call in calls: 15 | call.returncode = 0 16 | calls[0].stdout = b'd6ee34ae' 17 | calls[3].stdout = b'foo-registry.com/test/foo:d6ee34ae-dev foo-registry.com/test/bar:2.0.0' # cluster1 18 | calls[5].stdout = b'foo-registry.com/test/foo:d6ee34ae-dev foo-registry.com/test/bar:2.0.0' # cluster2 19 | 20 | loo = Vindaloo() 21 | loo.cmd = mock.Mock() 22 | loo.cmd.side_effect = calls 23 | 24 | with chdir('tests/test_roots/simple'): 25 | loo.main() 26 | 27 | # check the arguments kubectl was called with 28 | assert len(loo.cmd.call_args_list) == 6 29 | 30 | assert loo.cmd.call_args_list[0][0][0] == [ 31 | 'git', 32 | 'rev-parse', 33 | '--short=8', 34 | 'HEAD' 35 | ] 36 | assert loo.cmd.call_args_list[1][0][0] == [ 37 | 'kubectl', 38 | 'auth', 39 | 'can-i', 40 | 'get', 41 | 'deployment' 42 | ] 43 | assert loo.cmd.call_args_list[2][0][0][:3] == [ 44 | 'kubectl', 45 | 'config', 46 | 'use-context', 47 | ] 48 | assert loo.cmd.call_args_list[2][0][0][3] == 'foo-dev:cluster1' 49 | assert loo.cmd.call_args_list[3][0][0] == [ 50 | 'kubectl', 51 | 'get', 52 | 'deployment', 53 | 'foobar', 54 | '-o=jsonpath=\'{$.spec.template.spec.containers[*].image}\'' 55 | ] 56 | assert loo.cmd.call_args_list[4][0][0][:3] == [ 57 | 'kubectl', 58 | 'config', 59 | 'use-context', 60 | ] 61 | assert loo.cmd.call_args_list[4][0][0][3] == 'foo-dev:cluster2' 62 | assert loo.cmd.call_args_list[5][0][0] == [ 63 | 'kubectl', 64 | 'get', 65 | 'deployment', 66 | 'foobar', 67 | '-o=jsonpath=\'{$.spec.template.spec.containers[*].image}\'' 68 | ] 69 | 70 | output = capsys.readouterr().out.strip() 71 | 72 | assert '[DIFFERS]' not in output 73 | assert 'test/foo' in output 74 | assert 'test/bar' in output 75 | 76 | 77 | def test_versions_not_match(capsys): 78 | # fake arguments 79 | sys.argv = ['vindaloo', 'versions'] 80 | 81 | def x(*args, **kwargs): 82 | z = mock.Mock() 83 | z.returncode = 0 84 | return z 85 | 86 | calls = [mock.Mock() for _ in range(6)] 87 | for call in calls: 88 | call.returncode = 0 89 | calls[0].stdout = b'd6ee34ae' 90 | calls[3].stdout = b'foo-registry.com/test/foo:d6ee34ae-dev foo-registry.com/test/bar:2.0.0' # cluster1 91 | calls[5].stdout = b'foo-registry.com/test/foo:0.0.9 foo-registry.com/test/bar:2.0.0' # cluster2 DIFFERS 92 | 93 | loo = Vindaloo() 94 | loo.cmd = mock.Mock() 95 | loo.cmd.side_effect = calls 96 | 97 | with chdir('tests/test_roots/simple'): 98 | loo.main() 99 | 100 | # check the arguments kubectl was called with 101 | assert len(loo.cmd.call_args_list) == 6 102 | 103 | assert loo.cmd.call_args_list[0][0][0] == [ 104 | 'git', 105 | 'rev-parse', 106 | '--short=8', 107 | 'HEAD' 108 | ] 109 | assert loo.cmd.call_args_list[1][0][0] == [ 110 | 'kubectl', 111 | 'auth', 112 | 'can-i', 113 | 'get', 114 | 'deployment' 115 | ] 116 | assert loo.cmd.call_args_list[2][0][0][:3] == [ 117 | 'kubectl', 118 | 'config', 119 | 'use-context', 120 | ] 121 | assert loo.cmd.call_args_list[2][0][0][3] == 'foo-dev:cluster1' 122 | assert loo.cmd.call_args_list[3][0][0] == [ 123 | 'kubectl', 124 | 'get', 125 | 'deployment', 126 | 'foobar', 127 | '-o=jsonpath=\'{$.spec.template.spec.containers[*].image}\'' 128 | ] 129 | assert loo.cmd.call_args_list[4][0][0][:3] == [ 130 | 'kubectl', 131 | 'config', 132 | 'use-context', 133 | ] 134 | assert loo.cmd.call_args_list[4][0][0][3] == 'foo-dev:cluster2' 135 | assert loo.cmd.call_args_list[5][0][0] == [ 136 | 'kubectl', 137 | 'get', 138 | 'deployment', 139 | 'foobar', 140 | '-o=jsonpath=\'{$.spec.template.spec.containers[*].image}\'' 141 | ] 142 | 143 | output = capsys.readouterr().out.strip() 144 | 145 | assert '[DIFFERS]' in output 146 | assert 'test/foo' in output 147 | assert 'test/bar' in output 148 | 149 | 150 | def test_versions_json(capsys): 151 | # fake arguments 152 | sys.argv = ['vindaloo', 'versions', '--json'] 153 | 154 | def x(*args, **kwargs): 155 | z = mock.Mock() 156 | z.returncode = 0 157 | return z 158 | 159 | calls = [mock.Mock() for _ in range(6)] 160 | for call in calls: 161 | call.returncode = 0 162 | calls[0].stdout = b'd6ee34ae' 163 | calls[3].stdout = b'foo-registry.com/test/foo:1.0.0 foo-registry.com/test/bar:2.0.0' # c1 164 | calls[5].stdout = b'foo-registry.com/test/foo:0.0.9 foo-registry.com/test/bar:2.0.0' # c2 DIFFERS 165 | 166 | loo = Vindaloo() 167 | loo.cmd = mock.Mock() 168 | loo.cmd.side_effect = calls 169 | 170 | with chdir('tests/test_roots/simple'): 171 | loo.main() 172 | 173 | # check the arguments kubectl was called with 174 | assert len(loo.cmd.call_args_list) == 6 175 | 176 | output = capsys.readouterr().out.strip() 177 | data = json.loads(output) 178 | assert data 179 | assert data['dev']['test/foo']['local'] == 'd6ee34ae-dev' 180 | assert data['dev']['test/bar']['local'] == '2.0.0' 181 | 182 | assert data['dev']['test/bar']['remote']['cluster1'] == '2.0.0' 183 | assert data['dev']['test/foo']['remote']['cluster1'] == '1.0.0' 184 | assert data['dev']['test/foo']['remote']['cluster2'] == '0.0.9' 185 | -------------------------------------------------------------------------------- /tests/test_deploy.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import os 4 | import sys 5 | 6 | import pytest 7 | import vindaloo 8 | 9 | from utils import chdir 10 | 11 | 12 | def test_deploy(loo): 13 | # fake arguments 14 | sys.argv = ['vindaloo', '--noninteractive', 'deploy', 'dev', 'cluster1'] 15 | 16 | loo.cmd.return_value.stdout.decode.return_value.split.return_value = [ 17 | 'foo-registry.com/test/foo:1.0.0', 18 | 'foo-registry.com/test/bar:2.0.0', 19 | ] 20 | 21 | with chdir('tests/test_roots/simple'): 22 | loo.main() 23 | 24 | assert vindaloo.app.args.cluster == 'cluster1' 25 | 26 | # check arguments docker and kubectl was called with 27 | assert len(loo.cmd.call_args_list) == 4 28 | rev_parse_cmd = loo.cmd.call_args_list[0][0][0] 29 | auth_cmd = loo.cmd.call_args_list[1][0][0] 30 | use_context_cmd = loo.cmd.call_args_list[2][0][0] 31 | apply_cmd = loo.cmd.call_args_list[3][0][0][0:3] 32 | 33 | assert rev_parse_cmd == [ 34 | 'git', 35 | 'rev-parse', 36 | '--short=8', 37 | 'HEAD' 38 | ] 39 | assert auth_cmd == [ 40 | 'kubectl', 41 | 'auth', 42 | 'can-i', 43 | 'get', 44 | 'deployment' 45 | ] 46 | assert use_context_cmd == [ 47 | 'kubectl', 48 | 'config', 49 | 'use-context', 50 | 'foo-dev:cluster1', 51 | ] 52 | assert apply_cmd == [ 53 | 'kubectl', 54 | 'apply', 55 | '-f', 56 | ] 57 | 58 | 59 | def test_deploy_one_cluster(loo): 60 | # fake arguments 61 | sys.argv = ['vindaloo', '--noninteractive', 'deploy', 'dev', 'cluster2'] 62 | 63 | loo.cmd.return_value.stdout.decode.return_value.split.return_value = [ 64 | 'foo-registry.com/test/foo:1.0.0', 65 | 'foo-registry.com/test/bar:2.0.0', 66 | ] 67 | 68 | with chdir('tests/test_roots/simple'): 69 | loo.main() 70 | 71 | # check arguments docker and kubectl was called with 72 | assert len(loo.cmd.call_args_list) == 4 73 | rev_parse_cmd = loo.cmd.call_args_list[0][0][0] 74 | auth_cmd = loo.cmd.call_args_list[1][0][0] 75 | use_context_cmd = loo.cmd.call_args_list[2][0][0] 76 | apply_cmd = loo.cmd.call_args_list[3][0][0][0:3] 77 | 78 | assert rev_parse_cmd == [ 79 | 'git', 80 | 'rev-parse', 81 | '--short=8', 82 | 'HEAD' 83 | ] 84 | assert auth_cmd == [ 85 | 'kubectl', 86 | 'auth', 87 | 'can-i', 88 | 'get', 89 | 'deployment' 90 | ] 91 | assert use_context_cmd == [ 92 | 'kubectl', 93 | 'config', 94 | 'use-context', 95 | 'foo-dev:cluster2', 96 | ] 97 | assert apply_cmd == [ 98 | 'kubectl', 99 | 'apply', 100 | '-f', 101 | ] 102 | 103 | 104 | def test_deploy_watch(loo): 105 | # fake arguments 106 | sys.argv = ['vindaloo', '--noninteractive', 'deploy', '--watch', 'dev', 'cluster1'] 107 | 108 | loo.cmd.return_value.stdout.decode.return_value.split.return_value = [ 109 | 'foo-registry.com/test/foo:1.0.0', 110 | 'foo-registry.com/test/bar:2.0.0', 111 | ] 112 | 113 | with chdir('tests/test_roots/simple'): 114 | loo.main() 115 | 116 | # check arguments docker and kubectl was called with 117 | assert len(loo.cmd.call_args_list) == 5 118 | rev_parse_cmd = loo.cmd.call_args_list[0][0][0] 119 | auth_cmd = loo.cmd.call_args_list[1][0][0] 120 | use_context_cmd = loo.cmd.call_args_list[2][0][0] 121 | apply_cmd = loo.cmd.call_args_list[3][0][0][0:3] 122 | rollout_cmd = loo.cmd.call_args_list[4][0][0] 123 | 124 | assert rev_parse_cmd == [ 125 | 'git', 126 | 'rev-parse', 127 | '--short=8', 128 | 'HEAD' 129 | ] 130 | assert auth_cmd == [ 131 | 'kubectl', 132 | 'auth', 133 | 'can-i', 134 | 'get', 135 | 'deployment' 136 | ] 137 | assert use_context_cmd == [ 138 | 'kubectl', 139 | 'config', 140 | 'use-context', 141 | 'foo-dev:cluster1', 142 | ] 143 | assert apply_cmd == [ 144 | 'kubectl', 145 | 'apply', 146 | '-f', 147 | ] 148 | assert rollout_cmd == [ 149 | 'kubectl', 150 | 'rollout', 151 | 'status', 152 | 'deployment', 153 | 'foobar' 154 | ] 155 | 156 | 157 | def test_configmap(loo): 158 | # fake arguments 159 | sys.argv = ['vindaloo', '--noninteractive', 'deploy', 'dev', 'cluster1'] 160 | 161 | loo.cmd.return_value.stdout = b'{}' 162 | 163 | with chdir('tests/test_roots/configmap'): 164 | loo.main() 165 | 166 | # check arguments docker and kubectl was called with 167 | assert len(loo.cmd.call_args_list) == 3 168 | auth_cmd = loo.cmd.call_args_list[0][0][0] 169 | use_context_cmd = loo.cmd.call_args_list[1][0][0] 170 | apply_cmd = loo.cmd.call_args_list[2][0][0][0:3] 171 | 172 | assert auth_cmd == [ 173 | 'kubectl', 174 | 'auth', 175 | 'can-i', 176 | 'get', 177 | 'deployment' 178 | ] 179 | assert use_context_cmd == [ 180 | 'kubectl', 181 | 'config', 182 | 'use-context', 183 | 'foo-dev:cluster1', 184 | ] 185 | assert apply_cmd == [ 186 | 'kubectl', 187 | 'apply', 188 | '-f', 189 | ] 190 | 191 | 192 | def test_deploy_to_outdir(loo, test_temp_dir): 193 | # fake arguments 194 | 195 | sys.argv = ['vindaloo', '--noninteractive', 'deploy-dir', '--apply-output-dir={}'.format(test_temp_dir), 'dev', 'cluster1'] 196 | 197 | loo.cmd.return_value.stdout = b'{}' 198 | 199 | with chdir('tests/test_roots/configmap'): 200 | loo.main() 201 | 202 | assert os.path.isfile(os.path.join(test_temp_dir, "test-config-map_configmap.json")) 203 | with open(os.path.join(test_temp_dir, "test-config-map_configmap.json"), 'r') as file: 204 | configmap = json.loads(file.read()) 205 | assert configmap['metadata']['name'] == 'test-config-map' 206 | assert configmap['data']['file_config_key'] == ( 207 | 'some_config_value=123\n' 208 | 'another_config=one,two,three\n' 209 | 'template_config=This value depends on the selected environment.\n' 210 | ) 211 | assert base64.decodebytes(configmap['binaryData']['simple_binary_key'].encode()) == b'\x76\x69\x6b\x79' 212 | with open('tests/test_roots/configmap/k8s/templates/binary_config.conf', 'br') as binary_file: 213 | base64_content = configmap['binaryData']['binary_file_config_key'] 214 | assert base64.decodebytes(base64_content.encode()) == binary_file.read() 215 | 216 | 217 | @pytest.mark.parametrize('test_root_dir', ['obj-config', 'obj-config-wo-init']) 218 | def test_deploy_config_obj(loo, test_temp_dir, test_root_dir): 219 | # fake arguments 220 | sys.argv = ['vindaloo', '--noninteractive', 'deploy-dir', '--apply-output-dir={}'.format(test_temp_dir), 'dev', 'cluster1'] 221 | 222 | loo.cmd.return_value.stdout.decode.return_value.split.return_value = [ 223 | 'foo-registry.com/test/foo:1.0.0', 224 | 'foo-registry.com/test/bar:2.0.0', 225 | ] 226 | 227 | with chdir(f'tests/test_roots/{test_root_dir}'): 228 | loo.main() 229 | 230 | assert vindaloo.app.args.cluster == 'cluster1' 231 | 232 | data = json.loads(open(os.path.join(test_temp_dir, 'foo_deployment.json'), 'r').read()) 233 | 234 | assert data['apiVersion'] == 'apps/v1' 235 | assert data['kind'] == 'Deployment' 236 | assert data['spec']['selector']['matchLabels']['app'] == 'foo' 237 | assert data['spec']['template']['spec']['volumes'][0]['secret']['secretName'] == 'local-conf' 238 | assert data['spec']['template']['spec']['terminationGracePeriodSeconds'] == 30 239 | assert data['spec']['something'] == {'foo': 'boo'} 240 | assert data['spec']['template']['spec']['containers'][0]['image'] == 'foo-registry.com/test/foo:1.0.0' 241 | assert data['spec']['template']['spec']['containers'][0]['volumeMounts'][0] == { 242 | 'mountPath': '/cert.pem', 243 | 'name': 'cert', 244 | 'subPath': 'tls.crt' 245 | } 246 | assert data['spec']['template']['spec']['containers'][0]['env'][0] == { 247 | 'name': 'ENV', 248 | 'value': 'dev' 249 | } 250 | assert data['spec']['template']['spec']['containers'][0]['ports'][0] == { 251 | 'name': 'proxy', 252 | 'containerPort': 5001, 253 | } 254 | assert data['spec']['template']['spec']['containers'][0]['ports'][1] == { 255 | 'name': 'server', 256 | 'containerPort': 5000, 257 | 'protocol': 'UDP', 258 | } 259 | assert data['metadata']['annotations']['deploy-cluster'] == 'cluster2' 260 | assert data['spec']['template']['metadata']['annotations']['log-retention'] == '3w' 261 | 262 | data = json.loads(open(os.path.join(test_temp_dir, 'foo_cronjob.json'), 'r').read()) 263 | assert data['apiVersion'] == 'batch/v1beta1' 264 | assert data['kind'] == 'CronJob' 265 | assert ( 266 | data['spec']['jobTemplate']['spec']['template']['spec']['containers'][0]['image'] 267 | == 268 | 'registry.hub.docker.com/library/busybox:latest' 269 | ) 270 | assert data['spec']['schedule'] == "0 0 * * *" 271 | assert data['spec']['jobTemplate']['spec']['template']['spec']['containers'][0]['volumeMounts'][0] == { 272 | 'name': 'localconfig', 273 | 'mountPath': "/app.local.conf", 274 | 'subPath': "app.local.conf", 275 | } 276 | assert data['spec']['jobTemplate']['spec']['template']['spec']['containers'][0]['command'] == ['echo', 'z'] 277 | 278 | data = json.loads(open(os.path.join(test_temp_dir, 'foo_job.json'), 'r').read()) 279 | assert data['apiVersion'] == 'batch/v1' 280 | assert data['kind'] == 'Job' 281 | assert data['spec']['template']['metadata']['name'] == 'foo' 282 | 283 | data = json.loads(open(os.path.join(test_temp_dir, 'bar_job.json'), 'r').read()) 284 | assert data['apiVersion'] == 'batch/v1' 285 | assert data['kind'] == 'Job' 286 | assert data['spec']['template']['metadata']['name'] == 'bar' 287 | assert data['spec']['template']['spec']['terminationGracePeriodSeconds'] == 30 288 | 289 | data = json.loads(open(os.path.join(test_temp_dir, 'foo_service.json'), 'r').read()) 290 | assert data['spec']['type'] == 'NodePort' 291 | assert data['spec']['ports'][0]['nodePort'] == 30666 292 | assert data['spec']['ports'][0]['targetPort'] == 5001 293 | assert data['spec']['loadBalancerIP'] == '10.1.1.1' 294 | assert data['metadata']['name'] == "foo" 295 | assert data['metadata']['annotations']['loadbalancer'] == "enabled" 296 | -------------------------------------------------------------------------------- /README.cs.md: -------------------------------------------------------------------------------- 1 | # Vyndá Lů 2 | 3 | `Lů` je univerzální vyndavač do kubernetu, který umožňuje snadno pracovat v jednom projektu s více docker registry, docker repozitáři, kubernetes clustery a namespacy, aniž by bylo nutné duplikovat konfiguraci. 4 | 5 | Požadavky 6 | --------- 7 | 8 | Vyžadován je Python 3.6 a vyšší. 9 | 10 | 11 | Instalace 12 | --------- 13 | 14 | Stáhnout poslední [binárku](`https://github.com/seznam/vindaloo/raw/master/latest/vindaloo.pex`) 15 | 16 | ``` 17 | sudo wget -O /usr/bin/vindaloo https://github.com/seznam/vindaloo/raw/master/latest/vindaloo.pex 18 | sudo chmod +x /usr/bin/vindaloo 19 | ``` 20 | 21 | nebo přes naší pypi proxy: 22 | 23 | ``` 24 | pip3 install vindaloo 25 | ``` 26 | 27 | Co to umí 28 | --------- 29 | 30 | - Ubuildit dockerové image 31 | - Pushnout 32 | - deploynout do K8S 33 | - zkontrolovat verze v K8S 34 | - editovat secrety v K8S 35 | - napovídat v bashi 36 | 37 | Proč použít právě Lů a ne jiný nástroj 38 | -------------------------------------- 39 | 40 | - distribuuje se jako jeden spustitelný soubor, netřeba instalace 41 | - konfiguruje se pomocí Pythoních souborů a umožňuje tak velmi malou duplikaci kódu a značnou expresivitu 42 | - umožňuje šablonování pomocí jazyka Mustache (chevron) 43 | - umožňuje includování částí šablon v Dockerfile 44 | - umí z jedné komponenty vybuildit několik docker imagů 45 | - umí měnit kontext pro build docker image 46 | - je téměř kompletně pokrytý testy 47 | - umí napovídat přepínače, prostředí i image použité v komponentě 48 | 49 | Konfigurace 50 | ----------- 51 | 52 | `Lů` používá dvě úrovně konfigurace. 53 | 54 | "Globální" konfigurace je očekávána v souboru `vindaloo_conf.py` a definuje 55 | seznam prostředí, jím odpovídající k8s namespacy a seznam k8s clusterů. 56 | `vindaloo_conf.py` je možné umístit přímo do adresáře nasazované komponenty a 57 | nebo do jakéhokoliv nadřazeného adresáře, např. do domovské složky, pokud 58 | používáme pro všechny projekty stejné k8s prostředí (namespacy a clustery). 59 | 60 | Každý projekt/komponenta/služba pak obsahuje adresář `k8s`, který obsahuje konfiguraci 61 | pro build a nasazení této komponenty a z ní `Lů` generuje jednotlivé deploymenty, Dockerfiles, atd. 62 | a ty pak předává `kubectl`. 63 | 64 | 65 | Typycké použití 66 | --------------- 67 | 68 | ``` 69 | cd projekt 70 | vindaloo init . 71 | 72 | vindaloo build 73 | vindaloo push 74 | vindaloo deploy dev ko 75 | vindaloo deploy dev ng 76 | 77 | vindaloo versions 78 | ``` 79 | 80 | Napovídání v bashi 81 | ------------------ 82 | 83 | Pro zprovoznění napovídání je potřeba přidat do `~/.bashrc`: 84 | 85 | ``` 86 | source <(vindaloo completion) 87 | ``` 88 | 89 | Konfigurace projektu 90 | -------------------- 91 | 92 | ``` 93 | vindaloo_conf.py 94 | k8s 95 | templates 96 | - Dockerfile 97 | - deployment.yaml 98 | - service.yaml 99 | 100 | - base.py 101 | - dev.py 102 | - test.py 103 | - stable.py 104 | - versions.json 105 | ``` 106 | 107 | `vindaloo_conf.py` obsahuje definici běhových prostředí (dev, test, stable), jím odpovídající k8s namespacy a seznam k8s clusterů, 108 | do kterých budeme nasazovat. 109 | Soubor může být umístěn v adresáří komponenty nebo kdekoliv výše (pokud konfiguraci sdílíme napříč více komponentami). 110 | 111 | Konfigurace deploymentu komponenty je umístěna ve složce `k8s`, kde najdeme několik souborů a složku: 112 | 113 | `templates` obsahuje šablony generovaných souborů. Uvnitř mají mustache syntaxi. 114 | Dovoluje i jednoduché cyckly atp. Je to takový neseznamý TENG :-) 115 | Syntaxe: https://mustache.github.io/mustache.5.html 116 | 117 | `base.py` je základ konfigurace a měl by obsahovat to, co budou mít společné 118 | konfigurace pro všechna prostředí. 119 | 120 | `[dev/test/...].py` jsou konfiguráky pro jednotlivá prostředí a obsahují věci pro ně specifické (např. nodePorty). 121 | 122 | `versions.json` je konfigurák definující verze imagů, které chceme buildit/nasadit. 123 | Jelikož je to JSON, můžeme ho snadno číst i zapisovat programově. 124 | 125 | 126 | Ukázkový vindaloo_conf.py 127 | ------------------------- 128 | 129 | ``` 130 | ENVS = { 131 | 'dev': { 132 | 'k8s_namespace': 'avengers-dev', 133 | 'k8s_clusters': ['cluster1', 'cluster2'], 134 | 'docker_registry': 'foo-registry.com', 135 | }, 136 | 'test': { 137 | 'k8s_namespace': 'avengers-test', 138 | 'k8s_clusters': ['cluster1', 'cluster2'], 139 | 'docker_registry': 'foo-registry.com', 140 | }, 141 | 'staging': { 142 | 'k8s_namespace': 'avengers-staging', 143 | 'k8s_clusters': ['cluster1', 'cluster2'], 144 | 'docker_registry': 'foo-registry.com', 145 | }, 146 | 'stable': { 147 | 'k8s_namespace': 'avengers-stable', 148 | 'k8s_clusters': ['cluster1', 'cluster2'], 149 | 'docker_registry': 'foo-registry.com', 150 | }, 151 | } 152 | 153 | K8S_CLUSTER_ALIASES = { 154 | 'c1': 'cluster1', 155 | 'c2': 'cluster2', 156 | } 157 | ``` 158 | 159 | 160 | Ukázkový base.py 161 | ---------------- 162 | 163 | ``` 164 | # importujeme verze imagu z versions.json jako slovnik (hnusny hack) 165 | import versions 166 | 167 | # bude jen pozit dal 168 | CONFIG = { 169 | 'maintainer': "Pepa Zdepa ", 170 | 'version': versions['avengers/cool_app'], 171 | 'image_name': 'avengers/coo_app', 172 | } 173 | 174 | # Bude jen pouzit dale 175 | DEPLOYMENT = { 176 | 'replicas': 2, 177 | 'ident_label': "cool-app", 178 | 'image': "{}:{}".format(CONFIG['image_name'], CONFIG['version']), 179 | 'container_port': 6666, 180 | 'env': [ 181 | { 182 | 'key': 'BACKEND', 183 | 'val': "some-url.com" 184 | }, 185 | ] 186 | } 187 | 188 | # Bude jen pouzit dale 189 | SERVICE = { 190 | 'app_name': "cool-app", 191 | 'ident_label': "cool-app", 192 | 'container_port': 6666, 193 | 'port': 31666, 194 | } 195 | 196 | # Konfigurace docker files 197 | # pole dictu s klici: 198 | # context_dir - adresar ktery se preda dockeru 199 | # config - dict s konfiguraci 200 | # template - soubor se sablonou dockerfile z templates 201 | # pre_build_msg - hlaska, ktera se zobrazi pred spustenim buildu 202 | # includes - volitelne includy souboru, na kazdem bude take zavolan mustache se stejnym 203 | kontextem, jako hlavni sablona. Podle promenne je pak mozne vlozit do hlavni 204 | sablony. 205 | DOCKER_FILES = [ 206 | { 207 | 'context_dir': ".", 208 | 'config': CONFIG, 209 | 'template': "Dockerfile", 210 | 'pre_build_msg': """Prosim nejdriv spust: 211 | 212 | make clean 213 | """ 214 | 'includes': { # Nepovinne 215 | 'promenna': 'cesta/k/souboru/se/sablonou', 216 | } 217 | } 218 | ] 219 | 220 | # Popis deploy yamlu, podobne jako docker files 221 | K8S_OBJECTS = { 222 | "deployment": [ 223 | { 224 | 'config': DEPLOYMENT, 225 | 'template': "deployment.yaml", 226 | }, 227 | ], 228 | "service": [ 229 | { 230 | 'config': SERVICE, 231 | 'template': "service.yaml", 232 | }, 233 | ] 234 | } 235 | ``` 236 | 237 | Ukázkový dev.py 238 | ---------------- 239 | 240 | ``` 241 | from base import * # Tim podedime konfiguraci 242 | 243 | # Tady muzeme prekryt, nebo modifikovat cokoliv, co je nadefinovano v base.py 244 | 245 | 246 | DEPLOYMENT.update({ 247 | 'env': [ 248 | { 249 | 'key': 'BACKEND', 250 | 'val': "some-other-url.com" 251 | }, 252 | ] 253 | }) 254 | 255 | SERVICE.update({ 256 | 'port': 32666, 257 | }) 258 | ``` 259 | 260 | Ukázkový versions.json 261 | ---------------------- 262 | 263 | ``` 264 | { 265 | "avengers/cool_app": "1.0.0" 266 | } 267 | ``` 268 | 269 | 270 | Ukázkový Dockerfile 271 | ------------------- 272 | 273 | ``` 274 | {{#includes}}{{&base_image}}{{/includes}} 275 | LABEL maintainer="{{{maintainer}}}" 276 | LABEL description="SOS adminweb" 277 | 278 | COPY ... 279 | RUN ... 280 | CMD ... 281 | 282 | LABEL version="{{version}}" 283 | ``` 284 | 285 | Ukázkový deployment.yaml 286 | ------------------------ 287 | 288 | ``` 289 | apiVersion: apps/v1 290 | kind: Deployment 291 | metadata: 292 | name: {{ident_label}} 293 | spec: 294 | replicas: {{replicas}} 295 | template: 296 | metadata: 297 | name: {{ident_label}} 298 | labels: 299 | app: {{ident_label}} 300 | spec: 301 | containers: 302 | - name: {{ident_label}} 303 | image: {{registry}}/{{image}} 304 | ports: 305 | - containerPort: {{container_port}} 306 | livenessProbe: 307 | initialDelaySeconds: 5 308 | periodSeconds: 5 309 | httpGet: 310 | path: / 311 | port: {{container_port}} 312 | ``` 313 | 314 | Ukázkový service.yaml 315 | ------------------------ 316 | 317 | ``` 318 | apiVersion: v1 319 | kind: Service 320 | metadata: 321 | name: {{ident_label}} 322 | labels: 323 | name: {{ident_label}} 324 | spec: 325 | type: NodePort 326 | ports: 327 | - name: http 328 | nodePort: {{port}} 329 | port: {{container_port}} 330 | protocol: TCP 331 | selector: 332 | app: {{app_name}} 333 | ``` 334 | 335 | Pokročilejší konfigurace 336 | ------------------------ 337 | 338 | Konfigurace zapsaná v Pythonu umožňuje snadno vyřešit i daleko složitější scénáře. 339 | 340 | Můžeme například z jedné komponenty buildit více imagů a ty poté nasazovat v jednom podu - [ukázka konfigurace](examples/multi-image/k8s). 341 | 342 | Můžeme také vytvořit soubor podobný crontabu a pomocí něj dynamicky generovat CronJoby. 343 | Navíc pokud bychom potřebovali spustit nějaký skript mimo běžné naplánování, můžeme např. předáním ENV proměnné nasadit jeden Job. 344 | 345 | ``` 346 | DEPLOY_JOB=campaign-run-manager vindaloo deploy dev 347 | ``` 348 | 349 | Více viz [ukázka s cronjoby](examples/cron-jobs/k8s). 350 | 351 | Experimentální konfigurace pomocí tříd 352 | -------------------------------------- 353 | 354 | Kubernetí manifesty mohou být nakonfigurovány použitím slovníků (klasický způsob) nebo tříd (experimentální). 355 | Tato vlastnost není ještě stabilní a měla by být použita jen s opatrností. 356 | Výhodou je, že základ konfigurace může být snadněji měněn v konfigurácích pro jednotlivá prostředí. 357 | 358 | Datová struktura použitá ve třídách je v podstatě stejná jako v K8S JSON manifestech, s několika výjimkami. 359 | Všechny `name`, `value` seznamy používané v K8S (např. `env`) jsou uložené jako `key`: `value` pythonní slovníky. 360 | 361 | Více v [ukázce](examples/class-config/k8s). 362 | 363 | Jak `Lůa` ubuildit 364 | ------------------ 365 | 366 | Nový virtualenv z nějaké rozumně staré trojky. 367 | 368 | ``` 369 | pip install pystache pex 370 | 371 | make 372 | 373 | # success 374 | ``` 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Vindaloo 4 | [![codecov](https://codecov.io/gh/seznam/vindaloo/branch/master/graph/badge.svg)](https://codecov.io/gh/seznam/vindaloo) 5 | 6 | `Vindaloo` is universal deployer into kubernetes. It easily provides one project to work with multiple docker registries, repositories, kubernetes clusters and namespaces without the need of duplicating configuration. 7 | 8 | Requirements 9 | ------------ 10 | 11 | Python 3.6 and higher is required. 12 | 13 | 14 | Installation 15 | ------------ 16 | 17 | Download latest [pex binary](https://github.com/seznam/vindaloo/releases/latest/download/vindaloo.pex) 18 | 19 | ``` 20 | sudo wget -O /usr/bin/vindaloo https://github.com/seznam/vindaloo/releases/latest/download/vindaloo.pex 21 | sudo chmod +x /usr/bin/vindaloo 22 | ``` 23 | 24 | or use pip: 25 | 26 | ``` 27 | pip3 install vindaloo 28 | ``` 29 | 30 | What can it do 31 | -------------- 32 | 33 | - build docker images 34 | - push to docker registry 35 | - deploy to k8s 36 | - check versions in k8s 37 | - edit k8s secrets 38 | - bash completion 39 | 40 | Why to use Vindaloo and not X 41 | ----------------------------- 42 | 43 | - can be distributed as one executable file, no need to install 44 | - configuration using Python files which implies little code duplication and huge expressivity 45 | - powerful templating using Mustache language (chevron) 46 | - can include parts of templates in Dockerfiles 47 | - can build multiple images from one component 48 | - can change docker context dir for building an image 49 | - high test coverage 50 | - can bash-complete options, environments and images used in the component 51 | 52 | Configuration 53 | ------------- 54 | 55 | `Vindaloo` uses two levels of configuration. 56 | 57 | "Global" configuration is expected to be placed in `vindaloo_conf.py` and 58 | defines a list of environments, corresponding k8s namespaces and k8s clusters. 59 | `vindaloo_conf.py` can be placed into the directory of the deployed component or 60 | any parent directory, e.g. into your home folder if you share the same k8s environment (namespaces, clusters) 61 | for all your projects. 62 | 63 | Every project/component/service using `Vindaloo` must have directory `k8s`, 64 | which contains configuration for build and deployment of this component. 65 | `Vindaloo` generates deployments, Dockerfiles, etc. and calls `kubectl` and `docker` using this configuration. 66 | 67 | 68 | Typical usage 69 | ------------- 70 | 71 | ``` 72 | cd projekt 73 | vindaloo init . 74 | 75 | vindaloo build 76 | vindaloo push 77 | vindaloo deploy dev cluster1 78 | vindaloo deploy dev cluster2 79 | 80 | vindaloo versions 81 | ``` 82 | 83 | Bash completion 84 | --------------- 85 | 86 | Add this to your `~/.bashrc` to enable bash completion: 87 | 88 | ``` 89 | source <(vindaloo completion) 90 | ``` 91 | 92 | 93 | Component configuration 94 | ----------------------- 95 | 96 | ``` 97 | vindaloo_conf.py 98 | k8s 99 | templates 100 | - Dockerfile 101 | - deployment.yaml 102 | - service.yaml 103 | 104 | - base.py 105 | - dev.py 106 | - test.py 107 | - stable.py 108 | - versions.json 109 | ``` 110 | 111 | `vindaloo_conf.py` contains the definition of deployment environments (dev, test, prerelease, stable), corresponding k8s namespaces and list of k8s clusters. 112 | The file can be placed in the directory of the component or in any parent dir (if we share the configuration among more components). 113 | 114 | Configuration of the component's deployment is placed in the `k8s` directory, where a couple of files is located: 115 | 116 | `templates` contains templates of generated files (yamls, Dockerfiles). 117 | They use mustache syntax, which provides simple loops etc. 118 | Syntax: https://mustache.github.io/mustache.5.html 119 | 120 | `base.py` is the basis of configuration and should contain everything, what will have all the environment configurations in common. 121 | 122 | `[dev/test/...].py` are config files for individual k8s environments and contain settings specific for them (e.g. nodePort). 123 | 124 | `versions.json` is a configuration file defining versions of images we want to build/deploy. 125 | We can easily read and modify it programmatically because it's valid JSON. 126 | 127 | 128 | Example vindaloo_conf.py 129 | ------------------------ 130 | 131 | ``` 132 | ENVS = { 133 | 'dev': { 134 | 'k8s_namespace': 'avengers-dev', 135 | 'k8s_clusters': ['cluster1', 'cluster2'], 136 | 'docker_registry': 'foo-registry.com', 137 | }, 138 | 'test': { 139 | 'k8s_namespace': 'avengers-test', 140 | 'k8s_clusters': ['cluster1', 'cluster2'], 141 | 'docker_registry': 'foo-registry.com', 142 | }, 143 | 'staging': { 144 | 'k8s_namespace': 'avengers-staging', 145 | 'k8s_clusters': ['cluster1', 'cluster2'], 146 | 'docker_registry': 'foo-registry.com', 147 | }, 148 | 'stable': { 149 | 'k8s_namespace': 'avengers-stable', 150 | 'k8s_clusters': ['cluster1', 'cluster2'], 151 | 'docker_registry': 'foo-registry.com', 152 | }, 153 | } 154 | 155 | K8S_CLUSTER_ALIASES = { 156 | 'c1': 'cluster1', 157 | 'c2': 'cluster2', 158 | } 159 | ``` 160 | 161 | 162 | Example base.py 163 | --------------- 164 | 165 | ``` 166 | # import image versions from versions.json as a dict (awfull hack) 167 | import versions 168 | 169 | # will be used further 170 | CONFIG = { 171 | 'maintainer': "John Doe ", 172 | 'version': versions['avengers/cool_app'], 173 | 'image_name': 'avengers/cool_app', 174 | } 175 | 176 | # will be used further 177 | DEPLOYMENT = { 178 | 'replicas': 2, 179 | 'ident_label': "cool-app", 180 | 'image': "{}:{}".format(CONFIG['image_name'], CONFIG['version']), 181 | 'container_port': 6666, 182 | 'env': [ 183 | { 184 | 'key': 'BACKEND', 185 | 'val': "some-url.com" 186 | }, 187 | ] 188 | } 189 | 190 | # will be used further 191 | SERVICE = { 192 | 'app_name': "cool-app", 193 | 'ident_label': "cool-app", 194 | 'container_port': 6666, 195 | 'port': 31666, 196 | } 197 | 198 | # Dockerfiles configuration 199 | # list of dicts with keys: 200 | # config - dict with configuration 201 | # template - file with Dockerfile template 202 | # context_dir - directory passed to docker (optional) 203 | # pre_build_msg - message shown before the build is started (optional) 204 | # includes - loading of files to be included into Dockerfile. 205 | Every file will be processed with mustache with same context as main template. 206 | 207 | DOCKER_FILES = [ 208 | { 209 | 'context_dir': ".", 210 | 'config': CONFIG, 211 | 'template': "Dockerfile", 212 | 'pre_build_msg': """Please run first: 213 | 214 | make clean 215 | """ 216 | 'includes': { 217 | 'some_name': 'path/to/file/with/template', 218 | } 219 | } 220 | ] 221 | 222 | # K8S configuration 223 | K8S_OBJECTS = { 224 | "deployment": [ 225 | { 226 | 'config': DEPLOYMENT, 227 | 'template': "deployment.yaml", 228 | }, 229 | ], 230 | "service": [ 231 | { 232 | 'config': SERVICE, 233 | 'template': "service.yaml", 234 | }, 235 | ] 236 | } 237 | ``` 238 | 239 | Example dev.py 240 | -------------- 241 | 242 | ``` 243 | from base import * # config inheritance 244 | 245 | # Here we can overload or modify everything what was defined in base.py 246 | 247 | 248 | DEPLOYMENT.update({ 249 | 'env': [ 250 | { 251 | 'key': 'BACKEND', 252 | 'val': "some-other-url.com" 253 | }, 254 | ] 255 | }) 256 | 257 | SERVICE.update({ 258 | 'port': 32666, 259 | }) 260 | ``` 261 | 262 | Example versions.json 263 | --------------------- 264 | 265 | ``` 266 | { 267 | "avengers/cool_app": "1.0.0" 268 | } 269 | ``` 270 | 271 | 272 | Example Dockerfile 273 | ------------------ 274 | 275 | ``` 276 | {{#includes}}{{&base_image}}{{/includes}} 277 | LABEL maintainer="{{{maintainer}}}" 278 | LABEL description="Avengers cool app" 279 | 280 | COPY ... 281 | RUN ... 282 | CMD ... 283 | 284 | LABEL version="{{version}}" 285 | ``` 286 | 287 | Example deployment.yaml 288 | ----------------------- 289 | 290 | ``` 291 | apiVersion: apps/v1 292 | kind: Deployment 293 | metadata: 294 | name: {{ident_label}} 295 | spec: 296 | replicas: {{replicas}} 297 | template: 298 | metadata: 299 | name: {{ident_label}} 300 | labels: 301 | app: {{ident_label}} 302 | spec: 303 | containers: 304 | - name: {{ident_label}} 305 | image: {{registry}}/{{image}} 306 | ports: 307 | - containerPort: {{container_port}} 308 | livenessProbe: 309 | initialDelaySeconds: 5 310 | periodSeconds: 5 311 | httpGet: 312 | path: / 313 | port: {{container_port}} 314 | ``` 315 | 316 | Example service.yaml 317 | -------------------- 318 | 319 | ``` 320 | apiVersion: v1 321 | kind: Service 322 | metadata: 323 | name: {{ident_label}} 324 | labels: 325 | name: {{ident_label}} 326 | spec: 327 | type: NodePort 328 | ports: 329 | - name: http 330 | nodePort: {{port}} 331 | port: {{container_port}} 332 | protocol: TCP 333 | selector: 334 | app: {{app_name}} 335 | ``` 336 | 337 | Advanced configuration 338 | --------------------------- 339 | 340 | Configuration written in Python allows us to solve a far more complicated scenarios. 341 | 342 | We can build more images from one component and the deploy them into one pod. 343 | See the [example](examples/multi-image/k8s). 344 | 345 | We can create python module similar to crontab and generate CronJobs dynamically using it. 346 | Then if we need to run some task out of schedule, we can deploy one k8s Job by passing ENV variable for example. 347 | 348 | ``` 349 | DEPLOY_JOB=campaign-run-manager vindaloo deploy dev 350 | ``` 351 | 352 | See the [example](examples/cron-jobs/k8s). 353 | 354 | Experimental configuration using classes 355 | ---------------------------------------- 356 | 357 | Kubernetes manifests can be configured using dicts (classical way) or classes (experimental). 358 | This feature is not considered stable yet and should be used with caution. 359 | The advantage is that base configuration can be more easily changed in environment config files. 360 | 361 | The data structure used in classes is basically the same as in K8S JSON manifests with some exceptions. 362 | All `name`, `value` lists used in K8S (i.e. `env`) are stored as `key`: `value` python dicts. 363 | 364 | See the [example](examples/class-config/k8s). 365 | 366 | How to build `vindaloo` 367 | ----------------------- 368 | 369 | ``` 370 | pip install pystache pex 371 | 372 | make 373 | 374 | # success 375 | ``` 376 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=.git 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # Allow loading of arbitrary C extensions. Extensions are imported into the 34 | # active Python interpreter and may run arbitrary code. 35 | unsafe-load-any-extension=no 36 | 37 | 38 | [MESSAGES CONTROL] 39 | 40 | # Only show warnings with the listed confidence levels. Leave empty to show 41 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 42 | confidence= 43 | 44 | # Disable the message, report, category or checker with the given id(s). You 45 | # can either give multiple identifiers separated by comma (,) or put this 46 | # option multiple times (only on the command line, not in the configuration 47 | # file where it should appear only once).You can also use "--disable=all" to 48 | # disable everything first and then reenable specific checks. For example, if 49 | # you want to run only the similarities checker, you can use "--disable=all 50 | # --enable=similarities". If you want to run only the classes checker, but have 51 | # no Warning level messages displayed, use"--disable=all --enable=classes 52 | # --disable=W" 53 | # 54 | # * (C) convention, for programming standard violation 55 | # * (R) refactor, for bad code smell 56 | # * (W) warning, for python specific problems 57 | # * (E) error, for probable bugs in the code 58 | # * (F) fatal, if an error occurred which prevented pylint from processing 59 | # 60 | # F403 unable to detect undefined names 61 | # E0401 unable to import (F0401) 62 | # E0402 module level import not at top of file 63 | # E0611 No name .. in module .. 64 | # E1101 method .. has no .. member 65 | # E1102 .. is not callable 66 | # E1120 No value for argument .. in constructor call 67 | # E1123 unexpected keyword argument .. in constructor call 68 | # W0201 attribute .. define outside __init__ 69 | # W0221 arguments number differs from overridden method 70 | # W0223 method is abstract but is not overridden 71 | # W0401 wildcard import 72 | # W0403 relative import .. should be .. 73 | # W0511 TODO 74 | # W0611 unused .. - because of typing imports 75 | # W0613 unused argument 76 | # W0614 unused import from wildcart 77 | # W0622 redefining built-in .. 78 | # W0702 no exception type specified 79 | # W0703 catching too general exception 80 | # W1202 Use % for logging functions 81 | # C0111 missing docstring 82 | # C0411 standard import .. comes before .. 83 | # C0412 imports from package .. are not grouped 84 | # C0413 import .. should be placed at the top of the module 85 | # R0201 method could be a function 86 | # R0204 redefinition with another type 87 | # R0901 too many ancestors 88 | # R1705 unnecessary "else" after "return" 89 | # I0011 locally disabling pylint message 90 | disable=F403,F0401,E0401,E0402,E0611,E1101,E1102,E1120,E1123,W0201,W0221,W0223,W0401,W0403,W0511,W0611,W0613,W0614,W0622,W0702,W0703,W1202,C0111,C0411,C0412,C0413,R0201,R0204,R0901,R1705,I0011 91 | 92 | # Enable the message, report, category or checker with the given id(s). You can 93 | # either give multiple identifier separated by comma (,) or put this option 94 | # multiple time (only on the command line, not in the configuration file where 95 | # it should appear only once). See also the "--disable" option for examples. 96 | enable= 97 | 98 | 99 | [REPORTS] 100 | 101 | # Python expression which should return a note less than 10 (10 is the highest 102 | # note). You have access to the variables errors warning, statement which 103 | # respectively contain the number of errors / warnings messages and the total 104 | # number of statements analyzed. This is used by the global evaluation report 105 | # (RP0004). 106 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 107 | 108 | # Template used to display messages. This is a python new-style format string 109 | # used to format the message information. See doc for all details 110 | #msg-template= 111 | 112 | # Set the output format. Available formats are text, parseable, colorized, json 113 | # and msvs (visual studio).You can also give a reporter class, eg 114 | # mypackage.mymodule.MyReporterClass. 115 | output-format=text 116 | 117 | # Tells whether to display a full report or only the messages 118 | reports=yes 119 | 120 | # Activate the evaluation score. 121 | score=yes 122 | 123 | 124 | [REFACTORING] 125 | 126 | # Maximum number of nested blocks for function / method body 127 | max-nested-blocks=5 128 | 129 | 130 | [FORMAT] 131 | 132 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 133 | expected-line-ending-format= 134 | 135 | # Regexp for a line that is allowed to be longer than the limit. 136 | ignore-long-lines=^\s*(# )??$ 137 | 138 | # Number of spaces of indent required inside a hanging or continued line. 139 | indent-after-paren=4 140 | 141 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 142 | # tab). 143 | indent-string=' ' 144 | 145 | # Maximum number of characters on a single line. 146 | max-line-length=160 147 | 148 | # Maximum number of lines in a module 149 | max-module-lines=1000 150 | 151 | # List of optional constructs for which whitespace checking is disabled. `dict- 152 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 153 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 154 | # `empty-line` allows space-only lines. 155 | no-space-check=trailing-comma,dict-separator 156 | 157 | # Allow the body of a class to be on the same line as the declaration if body 158 | # contains single statement. 159 | single-line-class-stmt=no 160 | 161 | # Allow the body of an if to be on the same line as the test if there is no 162 | # else. 163 | single-line-if-stmt=no 164 | 165 | 166 | [LOGGING] 167 | 168 | # Logging modules to check that the string format arguments are in logging 169 | # function parameter format 170 | logging-modules=logging 171 | 172 | 173 | [MISCELLANEOUS] 174 | 175 | # List of note tags to take in consideration, separated by a comma. 176 | notes=FIXME,XXX,TODO 177 | 178 | 179 | [SIMILARITIES] 180 | 181 | # Ignore comments when computing similarities. 182 | ignore-comments=yes 183 | 184 | # Ignore docstrings when computing similarities. 185 | ignore-docstrings=yes 186 | 187 | # Ignore imports when computing similarities. 188 | ignore-imports=no 189 | 190 | # Minimum lines number of a similarity. 191 | min-similarity-lines=4 192 | 193 | 194 | [VARIABLES] 195 | 196 | # List of additional names supposed to be defined in builtins. Remember that 197 | # you should avoid to define new builtins when possible. 198 | additional-builtins= 199 | 200 | # Tells whether unused global variables should be treated as a violation. 201 | allow-global-unused-variables=yes 202 | 203 | # List of strings which can identify a callback function by name. A callback 204 | # name must start or end with one of those strings. 205 | callbacks=cb_,_cb 206 | 207 | # A regular expression matching the name of dummy variables (i.e. expectedly 208 | # not used). 209 | dummy-variables-rgx=_|dummy 210 | 211 | # Argument names that match this expression will be ignored. Default to name 212 | # with leading underscore 213 | ignored-argument-names=_.* 214 | 215 | # Tells whether we should check for unused import in __init__ files. 216 | init-import=no 217 | 218 | # List of qualified module names which can have objects that can redefine 219 | # builtins. 220 | redefining-builtins-modules=six.moves,future.builtins 221 | 222 | 223 | [SPELLING] 224 | 225 | # Spelling dictionary name. Available dictionaries: none. To make it working 226 | # install python-enchant package. 227 | spelling-dict= 228 | 229 | # List of comma separated words that should not be checked. 230 | spelling-ignore-words= 231 | 232 | # A path to a file that contains private dictionary; one word per line. 233 | spelling-private-dict-file= 234 | 235 | # Tells whether to store unknown words to indicated private dictionary in 236 | # --spelling-private-dict-file option instead of raising a message. 237 | spelling-store-unknown-words=no 238 | 239 | 240 | [BASIC] 241 | 242 | # Naming hint for argument names 243 | argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 244 | 245 | # Regular expression matching correct argument names 246 | argument-rgx=[a-z_][a-z0-9_]{2,50}$ 247 | 248 | # Naming hint for attribute names 249 | attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 250 | 251 | # Regular expression matching correct attribute names 252 | attr-rgx=[a-z_][a-z0-9_]{2,50}$ 253 | 254 | # Bad variable names which should always be refused, separated by a comma 255 | bad-names=foo,bar,baz,toto,tutu,tata 256 | 257 | # Naming hint for class attribute names 258 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 259 | 260 | # Regular expression matching correct class attribute names 261 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,50}|(__.*__))$ 262 | 263 | # Naming hint for class names 264 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 265 | 266 | # Regular expression matching correct class names 267 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 268 | 269 | # Naming hint for constant names 270 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 271 | 272 | # Regular expression matching correct constant names 273 | const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ 274 | 275 | # Minimum line length for functions/classes that require docstrings, shorter 276 | # ones are exempt. 277 | docstring-min-length=-1 278 | 279 | # Naming hint for function names 280 | function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 281 | 282 | # Regular expression matching correct function names 283 | function-rgx=[a-z_][a-z0-9_]{2,50}$ 284 | 285 | # Good variable names which should always be accepted, separated by a comma 286 | good-names=i,j,k,l,ex,e,_,q,u,id,log,fp,v,h,m,f,ok 287 | 288 | # Include a hint for the correct naming format with invalid-name 289 | include-naming-hint=no 290 | 291 | # Naming hint for inline iteration names 292 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 293 | 294 | # Regular expression matching correct inline iteration names 295 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 296 | 297 | # Naming hint for method names 298 | method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 299 | 300 | # Regular expression matching correct method names 301 | method-rgx=[a-z_][a-z0-9_]{2,50}$ 302 | 303 | # Naming hint for module names 304 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 305 | 306 | # Regular expression matching correct module names 307 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 308 | 309 | # Colon-delimited sets of names that determine each other's naming style when 310 | # the name regexes allow several styles. 311 | name-group= 312 | 313 | # Regular expression which should only match function or class names that do 314 | # not require a docstring. 315 | no-docstring-rgx=__.*__ 316 | 317 | # List of decorators that produce properties, such as abc.abstractproperty. Add 318 | # to this list to register other decorators that produce valid properties. 319 | property-classes=abc.abstractproperty 320 | 321 | # Naming hint for variable names 322 | variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 323 | 324 | # Regular expression matching correct variable names 325 | variable-rgx=[a-z_][a-z0-9_]{2,50}$ 326 | 327 | 328 | [TYPECHECK] 329 | 330 | # List of decorators that produce context managers, such as 331 | # contextlib.contextmanager. Add to this list to register other decorators that 332 | # produce valid context managers. 333 | contextmanager-decorators=contextlib.contextmanager 334 | 335 | # List of members which are set dynamically and missed by pylint inference 336 | # system, and so shouldn't trigger E1101 when accessed. Python regular 337 | # expressions are accepted. 338 | generated-members=REQUEST,acl_users,aq_parent 339 | 340 | # Tells whether missing members accessed in mixin class should be ignored. A 341 | # mixin class is detected if its name ends with "mixin" (case insensitive). 342 | ignore-mixin-members=yes 343 | 344 | # This flag controls whether pylint should warn about no-member and similar 345 | # checks whenever an opaque object is returned when inferring. The inference 346 | # can return multiple potential results while evaluating a Python object, but 347 | # some branches might not be evaluated, which results in partial inference. In 348 | # that case, it might be useful to still emit no-member and other checks for 349 | # the rest of the inferred objects. 350 | ignore-on-opaque-inference=yes 351 | 352 | # List of class names for which member attributes should not be checked (useful 353 | # for classes with dynamically set attributes). This supports the use of 354 | # qualified names. 355 | ignored-classes=SQLObject,pytest 356 | 357 | # List of module names for which member attributes should not be checked 358 | # (useful for modules/projects where namespaces are manipulated during runtime 359 | # and thus existing member attributes cannot be deduced by static analysis. It 360 | # supports qualified module names, as well as Unix pattern matching. 361 | ignored-modules= 362 | 363 | # Show a hint with possible names when a member name was not found. The aspect 364 | # of finding the hint is based on edit distance. 365 | missing-member-hint=yes 366 | 367 | # The minimum edit distance a name should have in order to be considered a 368 | # similar match for a missing member name. 369 | missing-member-hint-distance=1 370 | 371 | # The total number of similar names that should be taken in consideration when 372 | # showing a hint for a missing member. 373 | missing-member-max-choices=1 374 | 375 | 376 | [CLASSES] 377 | 378 | # List of method names used to declare (i.e. assign) instance attributes. 379 | defining-attr-methods=__init__,__new__,setUp 380 | 381 | # List of member names, which should be excluded from the protected access 382 | # warning. 383 | exclude-protected=_asdict,_fields,_replace,_source,_make 384 | 385 | # List of valid names for the first argument in a class method. 386 | valid-classmethod-first-arg=cls 387 | 388 | # List of valid names for the first argument in a metaclass class method. 389 | valid-metaclass-classmethod-first-arg=mcs 390 | 391 | 392 | [DESIGN] 393 | 394 | # Maximum number of arguments for function / method 395 | max-args=10 396 | 397 | # Maximum number of attributes for a class (see R0902). 398 | max-attributes=10 399 | 400 | # Maximum number of boolean expressions in a if statement 401 | max-bool-expr=5 402 | 403 | # Maximum number of branch for function / method body 404 | max-branches=15 405 | 406 | # Maximum number of locals for function / method body 407 | max-locals=15 408 | 409 | # Maximum number of parents for a class (see R0901). 410 | max-parents=7 411 | 412 | # Maximum number of public methods for a class (see R0904). 413 | max-public-methods=20 414 | 415 | # Maximum number of return / yield for function / method body 416 | max-returns=15 417 | 418 | # Maximum number of statements in function / method body 419 | max-statements=50 420 | 421 | # Minimum number of public methods for a class (see R0903). 422 | min-public-methods=0 423 | 424 | 425 | [IMPORTS] 426 | 427 | # Allow wildcard imports from modules that define __all__. 428 | allow-wildcard-with-all=no 429 | 430 | # Analyse import fallback blocks. This can be used to support both Python 2 and 431 | # 3 compatible code, which means that the block might have code that exists 432 | # only in one or another interpreter, leading to false positives when analysed. 433 | analyse-fallback-blocks=no 434 | 435 | # Deprecated modules which should not be used, separated by a comma 436 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 437 | 438 | # Create a graph of external dependencies in the given file (report RP0402 must 439 | # not be disabled) 440 | ext-import-graph= 441 | 442 | # Create a graph of every (i.e. internal and external) dependencies in the 443 | # given file (report RP0402 must not be disabled) 444 | import-graph= 445 | 446 | # Create a graph of internal dependencies in the given file (report RP0402 must 447 | # not be disabled) 448 | int-import-graph= 449 | 450 | # Force import order to recognize a module as part of the standard 451 | # compatibility libraries. 452 | known-standard-library= 453 | 454 | # Force import order to recognize a module as part of a third party library. 455 | known-third-party=enchant 456 | 457 | 458 | [EXCEPTIONS] 459 | 460 | # Exceptions that will emit a warning when being caught. Defaults to 461 | # "Exception" 462 | overgeneral-exceptions=Exception 463 | -------------------------------------------------------------------------------- /vindaloo/objects.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import copy 3 | from typing import Any, Union, Dict as DictType, List as ListType 4 | 5 | __all__ = ( 6 | 'Dict', 7 | 'List', 8 | 'PortsList', 9 | 'ConfigMap', 10 | 'Container', 11 | 'Deployment', 12 | 'CronJob', 13 | 'Job', 14 | 'Service', 15 | 'ServiceMonitor', 16 | 'PodMonitor', 17 | ) 18 | 19 | 20 | class JsonSerializable: 21 | NAME = "undefined" 22 | 23 | def serialize(self, *args, **kwargs): 24 | raise NotImplementedError() 25 | 26 | def clone(self): 27 | return copy.deepcopy(self) 28 | 29 | def __str__(self): 30 | return str(f'{self.NAME}({self.serialize()})') 31 | 32 | def __setattr__(self, key, value): 33 | if key not in self.__slots__: 34 | raise NotImplementedError(f"Property {key} is not supported") 35 | super().__setattr__(key, value) 36 | 37 | 38 | class Dict(JsonSerializable): 39 | __slots__ = ('children',) 40 | 41 | NAME = 'Dict' 42 | 43 | def __init__(self, *args, **kwargs): 44 | if kwargs: 45 | self.children = kwargs 46 | elif args: 47 | self.children = args[0] or {} 48 | else: 49 | self.children = {} 50 | 51 | def serialize(self, *args, **kwargs): 52 | return { 53 | key: val.serialize(*args, **kwargs) if isinstance(val, Dict) else val 54 | for key, val in self.children.items() 55 | } 56 | 57 | def update(self, data): 58 | self.children.update(data) 59 | 60 | def __getattr__(self, key): 61 | if key not in self.children: 62 | self.children[key] = Dict() 63 | return self.children[key] 64 | 65 | def __setattr__(self, key, val): 66 | if key in ('children', 'NAME'): 67 | super().__setattr__(key, val) 68 | else: 69 | if isinstance(val, dict): 70 | val = Dict(val) 71 | 72 | self.children[key] = val 73 | 74 | def __getitem__(self, key): 75 | return self.__getattr__(key) 76 | 77 | def __setitem__(self, key, value): 78 | self.children[key] = value 79 | 80 | def __deepcopy__(self, memo): 81 | return self.__class__(copy.deepcopy(self.children, memo)) 82 | 83 | def __str__(self): 84 | return f'' 85 | 86 | def __repr__(self): 87 | return f'vindaloo.objects.Dict({self.children})' 88 | 89 | 90 | class List(Dict): 91 | NAME = 'List' 92 | VALUE_KEY = 'value' 93 | 94 | def serialize(self, *args, **kwargs): 95 | items = [] 96 | 97 | for key, val in self.children.items(): 98 | if isinstance(val, Dict): 99 | items.append({ 100 | 'name': key, 101 | **val.serialize(*args, **kwargs), 102 | }) 103 | elif isinstance(val, dict): 104 | items.append({ 105 | 'name': key, 106 | **val 107 | }) 108 | elif isinstance(val, list): 109 | for item in val: 110 | items.append({ 111 | 'name': key, 112 | **item 113 | }) 114 | else: 115 | items.append({ 116 | 'name': key, 117 | self.VALUE_KEY: val, 118 | }) 119 | return items 120 | 121 | 122 | class PortsList(List): 123 | NAME = 'PortsList' 124 | VALUE_KEY = 'containerPort' 125 | 126 | 127 | class Container(Dict): 128 | NAME = 'Container' 129 | 130 | volumeMounts: Dict 131 | env: Dict 132 | ports: Dict 133 | image: str 134 | command: Union[str, ListType[str]] 135 | 136 | def serialize(self, *args, **kwargs): 137 | data = super().serialize(*args, **kwargs) 138 | 139 | # Prepend default registry if image does not starts with "!" 140 | if data["image"].startswith("!"): 141 | data["image"] = data["image"][1:] 142 | elif kwargs.get('app'): 143 | data['image'] = '{registry}/{image}'.format( 144 | registry=kwargs['app'].registry, 145 | image=data['image'], 146 | ) 147 | 148 | return data 149 | 150 | 151 | class ContainersMixin: 152 | @staticmethod 153 | def _prepare_containers(containers): 154 | containers = containers or {} 155 | for key, val in containers.items(): 156 | if 'volumeMounts' in val: 157 | val['volumeMounts'] = List(val['volumeMounts']) 158 | if 'env' in val: 159 | val['env'] = List(val['env']) 160 | if 'ports' in val and isinstance(val['ports'], dict): 161 | val['ports'] = PortsList(val['ports']) 162 | containers[key] = Container(val) 163 | 164 | return List(containers) 165 | 166 | 167 | class KubernetesManifestMixin(JsonSerializable): 168 | __slots__ = ('name', 'metadata', 'spec') 169 | obj_type = "" 170 | api_version = "" 171 | 172 | def __init__(self, metadata, annotations): 173 | metadata = metadata or {} 174 | metadata.setdefault('annotations', Dict(annotations or {})) 175 | self.metadata = Dict(metadata) 176 | self.spec = Dict() 177 | self.name = '' 178 | 179 | def serialize(self, *args, **kwargs): 180 | res = { 181 | 'apiVersion': self.api_version, 182 | 'kind': self.obj_type.capitalize(), 183 | 'metadata': self.metadata.serialize(*args, **kwargs), 184 | 'spec': self.spec.serialize(*args, **kwargs) 185 | } 186 | 187 | return res 188 | 189 | 190 | class Metadata(Dict): 191 | name: str 192 | annotations: Dict 193 | 194 | 195 | class PodMetadata(Dict): 196 | name: str 197 | labels: Dict 198 | annotations: Dict 199 | 200 | 201 | class PodSpec(Dict): 202 | volumes: List 203 | containers: DictType[str, Container] 204 | terminationGracePeriodSeconds: int 205 | restartPolicy: str 206 | 207 | 208 | class Pod(Dict): 209 | metadata: PodMetadata 210 | spec: PodSpec 211 | 212 | 213 | class ReplicaSet(Dict): 214 | template: Pod 215 | replicas: int 216 | 217 | 218 | class Deployment(ContainersMixin, KubernetesManifestMixin): 219 | obj_type = "deployment" 220 | api_version = "apps/v1" 221 | 222 | metadata: Metadata 223 | spec: ReplicaSet 224 | 225 | def __init__( 226 | self, name='', containers: DictType[str, dict] = None, 227 | volumes: DictType[str, dict] = None, replicas=1, termination_grace_period=30, 228 | annotations: DictType[str, str] = None, metadata=None, labels=None, 229 | spec_annotations: DictType[str, str] = None, 230 | ): 231 | """ 232 | :param annotations: Sets metadata.annotations in manifest 233 | :param spec_annotations: Sets spec.template.metadata.annotations in manifest 234 | :param termination_grace_period: Sets spec.template.spec.terminationGracePeriodSeconds in manifest 235 | """ 236 | super().__init__(metadata, annotations) 237 | 238 | self.spec = ReplicaSet( 239 | replicas=replicas, 240 | template=Dict( 241 | metadata=Dict( 242 | labels=Dict(labels), 243 | annotations=Dict(spec_annotations), 244 | ), 245 | spec=Dict( 246 | volumes=List(volumes), 247 | containers=self._prepare_containers(containers), 248 | terminationGracePeriodSeconds=termination_grace_period, 249 | ), 250 | ) 251 | ) 252 | self.set_name(name) 253 | 254 | def set_name(self, name): 255 | """ 256 | Sets name in: 257 | * metadata.name 258 | * metadata.annotations.name 259 | * spec.template.metadata.name 260 | * spec.template.metadata.labels.app 261 | """ 262 | self.name = name 263 | self.metadata.name = name 264 | self.metadata.annotations.name = name 265 | self.spec.template.metadata.name = name 266 | self.spec.template.metadata.labels.app = name 267 | self.spec.selector.matchLabels.app = name 268 | 269 | 270 | class CronJobTemplateSpec(Dict): 271 | template: Pod 272 | 273 | 274 | class CronJobTemplate(Dict): 275 | spec: CronJobTemplateSpec 276 | metadata: Dict 277 | 278 | 279 | class CronJobSpec(Dict): 280 | jobTemplate: CronJobTemplate 281 | schedule: str 282 | concurrencyPolicy: str 283 | 284 | 285 | class CronJob(ContainersMixin, KubernetesManifestMixin): 286 | obj_type = "cronjob" 287 | api_version = "batch/v1beta1" 288 | 289 | metadata: Metadata 290 | spec: CronJobSpec 291 | 292 | def __init__( 293 | self, name='', schedule='', containers: DictType[str, dict] = None, 294 | termination_grace_period=30, 295 | restart_policy='Never', 296 | concurrency_policy='Allow', 297 | volumes: DictType[str, dict] = None, 298 | annotations=None, metadata=None, labels=None, 299 | spec_annotations=None, 300 | ): 301 | """ 302 | :param annotations: Sets metadata.annotations in manifest 303 | :param spec_annotations: Sets spec.jobTemplate.spec.template.metadata.annotations in manifest 304 | """ 305 | super().__init__(metadata, annotations) 306 | 307 | self.spec = CronJobSpec( 308 | schedule=schedule, 309 | concurrencyPolicy=concurrency_policy, 310 | jobTemplate=Dict( 311 | spec=Dict( 312 | template=Dict( 313 | metadata=Dict( 314 | name=name, 315 | labels=Dict(labels), 316 | annotations=Dict(spec_annotations), 317 | ), 318 | spec=Dict( 319 | volumes=List(volumes), 320 | containers=self._prepare_containers(containers), 321 | terminationGracePeriodSeconds=termination_grace_period, 322 | restartPolicy=restart_policy, 323 | ), 324 | ) 325 | ) 326 | ) 327 | ) 328 | self.set_name(name) 329 | 330 | def set_name(self, name: str): 331 | """ 332 | Sets name in: 333 | * spec.jobTemplate.spec.template.metadata.name 334 | * spec.jobTemplate.spec.template.metadata.labels.app 335 | """ 336 | self.name = name 337 | self.metadata.name = name 338 | self.spec.jobTemplate.spec.template.metadata.name = name 339 | self.spec.jobTemplate.spec.template.metadata.labels.app = name 340 | 341 | def serialize(self, *args, **kwargs): 342 | res = { 343 | 'apiVersion': self.api_version, 344 | 'kind': "CronJob", 345 | 'metadata': self.metadata.serialize(*args, **kwargs), 346 | 'spec': self.spec.serialize(*args, **kwargs) 347 | } 348 | 349 | return res 350 | 351 | 352 | class JobSpec(Dict): 353 | template: Pod 354 | backoffLimit: int 355 | parallelism: int 356 | 357 | 358 | class Job(ContainersMixin, KubernetesManifestMixin): 359 | obj_type = "job" 360 | api_version = "batch/v1" 361 | 362 | metadata: Metadata 363 | spec: JobSpec 364 | 365 | def __init__( 366 | self, name='', containers: DictType[str, dict] = None, 367 | termination_grace_period=30, 368 | restart_policy='Never', 369 | volumes: DictType[str, dict] = None, 370 | annotations=None, metadata=None, labels=None, 371 | spec_annotations=None, 372 | ): 373 | """ 374 | :param annotations: Sets metadata.annotations in manifest 375 | :param spec_annotations: Sets spec.template.metadata.annotations in manifest 376 | """ 377 | super().__init__(metadata, annotations) 378 | 379 | self.spec = JobSpec( 380 | template=Dict( 381 | metadata=Dict( 382 | name=name, 383 | labels=Dict(labels), 384 | annotations=Dict(spec_annotations), 385 | ), 386 | spec=Dict( 387 | volumes=List(volumes), 388 | containers=self._prepare_containers(containers), 389 | terminationGracePeriodSeconds=termination_grace_period, 390 | restartPolicy=restart_policy, 391 | ), 392 | ) 393 | ) 394 | self.set_name(name) 395 | 396 | def set_name(self, name: str): 397 | """ 398 | Sets name in: 399 | * spec.template.metadata.name 400 | * spec.template.metadata.labels.app 401 | """ 402 | self.name = name 403 | self.metadata.name = name 404 | self.spec.template.metadata.name = name 405 | self.spec.template.metadata.labels.app = name 406 | 407 | 408 | class ServiceSpec(Dict): 409 | ports: List 410 | selector: Dict 411 | clusterIP: str 412 | loadBalancerIP: str 413 | type: str 414 | 415 | 416 | class Service(KubernetesManifestMixin): 417 | obj_type = "service" 418 | api_version = "v1" 419 | 420 | metadata: Metadata 421 | spec: ServiceSpec 422 | 423 | def __init__( 424 | self, name='', ports: DictType[str, dict] = None, selector: DictType[str, str] = None, 425 | service_type='ClusterIP', load_balancer_ip=None, cluster_ip=None, 426 | annotations: DictType[str, str] = None, metadata=None, 427 | ): 428 | """ 429 | :param selector: {'app': "foo"} 430 | :param ports: {'port_name': {'port': 1234, 'targetPort': 4321, 'protocol': 'TCP'}} 431 | """ 432 | super().__init__(metadata, annotations) 433 | 434 | self.spec = ServiceSpec( 435 | type=service_type, 436 | ports=List(ports), 437 | selector=Dict(selector), 438 | ) 439 | self.set_name(name) 440 | 441 | if load_balancer_ip: 442 | self.spec['loadBalancerIP'] = load_balancer_ip 443 | if cluster_ip: 444 | self.spec['clusterIP'] = cluster_ip 445 | 446 | def set_name(self, name): 447 | self.name = name 448 | self.metadata.name = name 449 | 450 | 451 | class ConfigMap(KubernetesManifestMixin): 452 | obj_type = 'configmap' 453 | api_version = 'v1' 454 | __slots__ = ( 455 | 'name', 'metadata', 'spec', 'data', 'binary_data', 'immutable', 456 | ) 457 | 458 | def __init__( 459 | self, 460 | name: str, 461 | data: DictType[str, Union[None, int, float, str, dict]] = None, 462 | binary_data: DictType[str, Union[bytes, dict]] = None, 463 | immutable: bool = False, 464 | annotations: DictType[str, str] = None, 465 | metadata: DictType[str, Any] = None, 466 | ): 467 | super().__init__(metadata, annotations) 468 | self.set_name(name) 469 | self.data = data or {} 470 | self.binary_data = binary_data or {} 471 | self.immutable = immutable 472 | 473 | def set_name(self, name): 474 | self.name = name 475 | self.metadata.name = name 476 | 477 | @staticmethod 478 | def _binary_value_prep(value: bytes) -> str: 479 | return base64.encodebytes(value).decode() 480 | 481 | def _file_prep(self, app, filename: str, config: DictType, is_binary: bool) -> str: 482 | if not filename: 483 | raise ValueError(f'Value of a config key can be either atomic value or dict with a `file` key.') 484 | 485 | if is_binary: 486 | with open(f'k8s/{filename}', 'br') as file: 487 | return self._binary_value_prep(file.read()) 488 | else: 489 | temp_file = app.create_file(filename, config, no_edit=True) 490 | file_content = temp_file.read() 491 | temp_file.close() 492 | return file_content.decode('utf-8') 493 | 494 | def prepare_data(self, data: DictType[str, Any], app, is_binary: bool = False) -> DictType[str, str]: 495 | new_data = {} 496 | for key, value in data.items(): 497 | if isinstance(value, dict): 498 | new_data[key] = self._file_prep( 499 | app, 500 | value.get('file'), 501 | value.get('config', {}), 502 | is_binary, 503 | ) 504 | else: 505 | if is_binary: 506 | value = self._binary_value_prep(value) 507 | new_data[key] = str(value) 508 | return new_data 509 | 510 | def serialize(self, *args, **kwargs): 511 | keys_intersection = set(self.data.keys()) & set(self.binary_data.keys()) 512 | if keys_intersection: 513 | raise ValueError(f"`data` and `binary_data` cannot contain same keys: {keys_intersection}") 514 | 515 | res = { 516 | 'apiVersion': self.api_version, 517 | 'kind': 'ConfigMap', 518 | 'metadata': self.metadata.serialize(*args, **kwargs), 519 | } 520 | if self.data: 521 | res['data'] = self.prepare_data(self.data, kwargs.get('app')) 522 | if self.binary_data: 523 | res['binaryData'] = self.prepare_data(self.binary_data, kwargs.get('app'), is_binary=True) 524 | if self.immutable: 525 | res['immutable'] = True 526 | 527 | return res 528 | 529 | 530 | class MonitorMixin: 531 | api_version = "monitoring.coreos.com/v1" 532 | kind = "Monitor" 533 | 534 | def serialize(self, *args, **kwargs): 535 | res = { 536 | 'apiVersion': self.api_version, 537 | 'kind': self.kind, 538 | 'metadata': self.metadata.serialize(*args, **kwargs), 539 | 'spec': self.spec.serialize(*args, **kwargs), 540 | } 541 | return res 542 | 543 | def set_name(self, name): 544 | self.name = name 545 | self.metadata.name = name 546 | 547 | 548 | class MonitorSpec(Dict): 549 | selector: Dict() 550 | endpoints: List() 551 | 552 | 553 | class ServiceMonitor(MonitorMixin, KubernetesManifestMixin): 554 | obj_type = "servicemonitor" 555 | kind = "ServiceMonitor" 556 | 557 | def __init__(self, name: str, path: str, port: str, service_name: str, metadata: DictType[str, Any] = None): 558 | super().__init__(metadata, None) 559 | self.spec = MonitorSpec( 560 | selector=Dict({ 561 | 'matchLabels': Dict({'app': service_name}) 562 | }), 563 | endpoints=[{ 564 | 'port': port, 565 | 'path': path, 566 | 'followRedirects': True, 567 | }] 568 | ) 569 | self.set_name(name) 570 | 571 | 572 | class PodMonitor(MonitorMixin, KubernetesManifestMixin): 573 | obj_type = "podmonitor" 574 | kind = "PodMonitor" 575 | 576 | def __init__(self, name: str, path: str, port: str, pod_name: str, metadata: DictType[str, Any] = None): 577 | super().__init__(metadata, None) 578 | self.spec = MonitorSpec( 579 | selector=Dict({ 580 | 'matchLabels': Dict({'app': pod_name}) 581 | }), 582 | podMetricsEndpoints=[{ 583 | 'port': port, 584 | 'path': path, 585 | 'followRedirects': True, 586 | }] 587 | ) 588 | self.set_name(name) 589 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d9bb7f675122078df010a761726a58bdce0df09a9e93eef62663ae1995a2a051" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "argcomplete": { 18 | "hashes": [ 19 | "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81", 20 | "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445" 21 | ], 22 | "index": "pypi", 23 | "version": "==1.12.3" 24 | }, 25 | "chevron": { 26 | "hashes": [ 27 | "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf", 28 | "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443" 29 | ], 30 | "index": "pypi", 31 | "version": "==0.14.0" 32 | }, 33 | "pyyaml": { 34 | "hashes": [ 35 | "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", 36 | "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", 37 | "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", 38 | "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", 39 | "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", 40 | "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", 41 | "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", 42 | "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", 43 | "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", 44 | "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", 45 | "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", 46 | "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", 47 | "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", 48 | "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", 49 | "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", 50 | "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", 51 | "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", 52 | "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", 53 | "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", 54 | "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", 55 | "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", 56 | "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", 57 | "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", 58 | "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", 59 | "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", 60 | "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", 61 | "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", 62 | "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", 63 | "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", 64 | "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", 65 | "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", 66 | "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", 67 | "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" 68 | ], 69 | "index": "pypi", 70 | "version": "==6.0" 71 | } 72 | }, 73 | "develop": { 74 | "attrs": { 75 | "hashes": [ 76 | "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", 77 | "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" 78 | ], 79 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 80 | "version": "==21.2.0" 81 | }, 82 | "certifi": { 83 | "hashes": [ 84 | "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", 85 | "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" 86 | ], 87 | "markers": "python_version >= '3.6'", 88 | "version": "==2023.11.17" 89 | }, 90 | "charset-normalizer": { 91 | "hashes": [ 92 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 93 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 94 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 95 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 96 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 97 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 98 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 99 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 100 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 101 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 102 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 103 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 104 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 105 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 106 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 107 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 108 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 109 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 110 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 111 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 112 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 113 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 114 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 115 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 116 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 117 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 118 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 119 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 120 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 121 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 122 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 123 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 124 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 125 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 126 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 127 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 128 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 129 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 130 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 131 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 132 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 133 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 134 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 135 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 136 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 137 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 138 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 139 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 140 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 141 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 142 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 143 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 144 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 145 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 146 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 147 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 148 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 149 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 150 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 151 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 152 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 153 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 154 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 155 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 156 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 157 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 158 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 159 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 160 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 161 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 162 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 163 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 164 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 165 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 166 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 167 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 168 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 169 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 170 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 171 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 172 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 173 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 174 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 175 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 176 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 177 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 178 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 179 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 180 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 181 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 182 | ], 183 | "markers": "python_full_version >= '3.7.0'", 184 | "version": "==3.3.2" 185 | }, 186 | "codecov": { 187 | "hashes": [ 188 | "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c", 189 | "sha256:7d2b16c1153d01579a89a94ff14f9dbeb63634ee79e18c11036f34e7de66cbc9", 190 | "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5" 191 | ], 192 | "index": "pypi", 193 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 194 | "version": "==2.1.13" 195 | }, 196 | "coverage": { 197 | "extras": [ 198 | "toml" 199 | ], 200 | "hashes": [ 201 | "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", 202 | "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", 203 | "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", 204 | "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", 205 | "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", 206 | "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", 207 | "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", 208 | "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", 209 | "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", 210 | "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", 211 | "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", 212 | "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", 213 | "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", 214 | "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", 215 | "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", 216 | "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", 217 | "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", 218 | "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", 219 | "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", 220 | "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", 221 | "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", 222 | "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", 223 | "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", 224 | "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", 225 | "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", 226 | "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", 227 | "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", 228 | "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", 229 | "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", 230 | "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", 231 | "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", 232 | "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", 233 | "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", 234 | "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", 235 | "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", 236 | "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", 237 | "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", 238 | "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", 239 | "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", 240 | "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", 241 | "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", 242 | "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", 243 | "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", 244 | "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", 245 | "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", 246 | "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", 247 | "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" 248 | ], 249 | "index": "pypi", 250 | "markers": "python_version >= '3.6'", 251 | "version": "==6.2" 252 | }, 253 | "idna": { 254 | "hashes": [ 255 | "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", 256 | "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" 257 | ], 258 | "markers": "python_version >= '3.5'", 259 | "version": "==3.6" 260 | }, 261 | "importlib-metadata": { 262 | "hashes": [ 263 | "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", 264 | "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" 265 | ], 266 | "index": "pypi", 267 | "version": "==4.8.2" 268 | }, 269 | "iniconfig": { 270 | "hashes": [ 271 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 272 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 273 | ], 274 | "version": "==1.1.1" 275 | }, 276 | "packaging": { 277 | "hashes": [ 278 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 279 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 280 | ], 281 | "markers": "python_version >= '3.6'", 282 | "version": "==21.3" 283 | }, 284 | "pluggy": { 285 | "hashes": [ 286 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 287 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 288 | ], 289 | "markers": "python_version >= '3.6'", 290 | "version": "==1.0.0" 291 | }, 292 | "py": { 293 | "hashes": [ 294 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 295 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 296 | ], 297 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 298 | "version": "==1.11.0" 299 | }, 300 | "pyparsing": { 301 | "hashes": [ 302 | "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", 303 | "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" 304 | ], 305 | "markers": "python_version >= '3.6'", 306 | "version": "==3.0.6" 307 | }, 308 | "pytest": { 309 | "hashes": [ 310 | "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", 311 | "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" 312 | ], 313 | "index": "pypi", 314 | "version": "==6.2.5" 315 | }, 316 | "pytest-cov": { 317 | "hashes": [ 318 | "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", 319 | "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" 320 | ], 321 | "index": "pypi", 322 | "version": "==3.0.0" 323 | }, 324 | "requests": { 325 | "hashes": [ 326 | "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", 327 | "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" 328 | ], 329 | "markers": "python_version >= '3.7'", 330 | "version": "==2.31.0" 331 | }, 332 | "toml": { 333 | "hashes": [ 334 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 335 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 336 | ], 337 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 338 | "version": "==0.10.2" 339 | }, 340 | "tomli": { 341 | "hashes": [ 342 | "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", 343 | "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" 344 | ], 345 | "markers": "python_version >= '3.6'", 346 | "version": "==1.2.2" 347 | }, 348 | "typing-extensions": { 349 | "hashes": [ 350 | "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", 351 | "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" 352 | ], 353 | "index": "pypi", 354 | "version": "==4.0.1" 355 | }, 356 | "urllib3": { 357 | "hashes": [ 358 | "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", 359 | "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" 360 | ], 361 | "markers": "python_version >= '3.8'", 362 | "version": "==2.1.0" 363 | }, 364 | "zipp": { 365 | "hashes": [ 366 | "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", 367 | "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" 368 | ], 369 | "markers": "python_version >= '3.6'", 370 | "version": "==3.6.0" 371 | } 372 | } 373 | } 374 | --------------------------------------------------------------------------------