├── rootfs ├── api │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── healthchecks.py │ │ │ └── load_db_state_to_k8s.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0005_auto_20160208_2156.py │ │ ├── 0020_release_failed.py │ │ ├── 0016_auto_20160830_0104.py │ │ ├── 0008_config_registry.py │ │ ├── 0021_appsettings_label.py │ │ ├── 0010_config_healthcheck.py │ │ ├── 0017_appsettings_autoscale.py │ │ ├── 0022_add_private_key_validation.py │ │ ├── 0014_appsettings_whitelist.py │ │ ├── 0018_auto_20160908_1748.py │ │ ├── 0023_app_k8s_name_length.py │ │ ├── 0004_auto_20160124_2134.py │ │ ├── 0013_auto_20160816_2122.py │ │ ├── 0007_auto_20160226_2335.py │ │ ├── 0019_auto_20160930_2351.py │ │ ├── 0015_auto_20160822_2103.py │ │ ├── 0011_auto_20160810_1603.py │ │ ├── 0012_auto_20160816_1934.py │ │ ├── 0002_auto_20151215_0352.py │ │ ├── 0009_auto_20160607_2259.py │ │ ├── 0003_auto_20160114_0310.py │ │ └── 0006_auto_20160114_0313.py │ ├── settings │ │ ├── __init__.py │ │ └── testing.py │ ├── __init__.py │ ├── fields.py │ ├── wsgi.py │ ├── authentication.py │ ├── middleware.py │ ├── viewsets.py │ ├── tests │ │ ├── certs │ │ │ ├── self-signed.csr │ │ │ ├── bar.com.csr │ │ │ ├── foo.com.csr │ │ │ ├── wildcard.foo.com.csr │ │ │ ├── www.foo.com.csr │ │ │ ├── self-signed.cert │ │ │ ├── autotest.example.com.cert │ │ │ ├── wildcard.foo.com.cert │ │ │ ├── bar.com.cert │ │ │ ├── foo.com.cert │ │ │ ├── bar.com.key │ │ │ ├── www.foo.com.cert │ │ │ ├── foo.com.key │ │ │ ├── self-signed.key │ │ │ ├── www.foo.com.key │ │ │ ├── autotest.example.com.key │ │ │ └── wildcard.foo.com.key │ │ ├── test_api_middleware.py │ │ ├── test_users.py │ │ ├── test_healthz.py │ │ ├── __init__.py │ │ ├── test_tls.py │ │ └── test_utils.py │ ├── fixtures │ │ ├── dev.json │ │ ├── test_auth.json │ │ ├── test_sharing.json │ │ └── tests.json │ ├── models │ │ ├── key.py │ │ ├── domain.py │ │ ├── tls.py │ │ └── build.py │ ├── exceptions.py │ ├── admin.py │ └── permissions.py ├── registry │ └── __init__.py ├── setup.cfg ├── bin │ ├── reload │ ├── test-style │ ├── test-unit │ └── boot ├── .dockerignore ├── deis │ ├── __init__.py │ ├── gunicorn │ │ ├── logging.py │ │ └── config.py │ └── urls.py ├── manage.py ├── scheduler │ ├── resources │ │ ├── __init__.py │ │ ├── node.py │ │ ├── __resource.py │ │ ├── replicaset.py │ │ ├── scale.py │ │ ├── events.py │ │ ├── quota.py │ │ ├── namespace.py │ │ ├── ingress.py │ │ ├── service.py │ │ ├── secret.py │ │ └── replicationcontroller.py │ ├── exceptions.py │ ├── states.py │ └── tests │ │ ├── test_quota.py │ │ ├── __init__.py │ │ ├── test_pod_states.py │ │ ├── test_nodes.py │ │ ├── test_namespaces.py │ │ ├── test_ingress.py │ │ ├── test_pod_resources.py │ │ ├── test_utils.py │ │ ├── test_scheduler.py │ │ ├── test_kubehttpclient.py │ │ ├── test_services.py │ │ ├── test_secrets.py │ │ └── test_replicationcontrollers.py ├── dev_requirements.txt ├── requirements.txt ├── .coveragerc ├── Dockerfile └── Dockerfile.test ├── _scripts ├── README.md └── util │ └── commit-msg ├── charts └── controller │ ├── templates │ ├── controller-service-account.yaml │ ├── deploy-hook-secret.yaml │ ├── controller-secret-django-secret-key.yaml │ ├── _helpers.tmpl │ ├── controller-service.yaml │ ├── controller-clusterrolebinding.yaml │ ├── controller-ingress-rule-http-80.yaml │ ├── controller-clusterrole.yaml │ └── controller-deployment.yaml │ ├── Chart.yaml │ └── values.yaml ├── CONTRIBUTING.md ├── codecov.yml ├── .editorconfig ├── .github └── PULL_REQUEST_TEMPLATE ├── versioning.mk ├── .gitignore ├── LICENSE ├── DCO ├── Makefile └── README.md /rootfs/api/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rootfs/api/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rootfs/api/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rootfs/api/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rootfs/registry/__init__.py: -------------------------------------------------------------------------------- 1 | from .dockerclient import publish_release, get_port, RegistryException # noqa 2 | -------------------------------------------------------------------------------- /_scripts/README.md: -------------------------------------------------------------------------------- 1 | # Workflow Contrib 2 | 3 | Scripts and tools that are not a core part of Deis Workflow v2. 4 | -------------------------------------------------------------------------------- /rootfs/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 99 3 | exclude = api/migrations,templates,venv 4 | max-complexity = 12 5 | -------------------------------------------------------------------------------- /rootfs/bin/reload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # gracefully reload gunicorn 4 | kill -HUP "$(cat /tmp/gunicorn.pid)" 5 | 6 | exit 0 7 | -------------------------------------------------------------------------------- /rootfs/api/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The **api** Django app presents a RESTful web API for interacting with the **deis** system. 3 | """ 4 | 5 | __version__ = '2.3.0' 6 | -------------------------------------------------------------------------------- /charts/controller/templates/controller-service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: deis-controller 5 | labels: 6 | heritage: deis 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Deis 2 | 3 | This project is part of Deis. You can find the latest contribution 4 | guidelines [in our documentation](https://deis.com/docs/workflow/contributing/overview/). 5 | -------------------------------------------------------------------------------- /rootfs/api/fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Deis API custom fields for representing data in Django forms. 3 | """ 4 | 5 | from django.db import models 6 | 7 | 8 | class UuidField(models.UUIDField): 9 | pass 10 | -------------------------------------------------------------------------------- /rootfs/.dockerignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .coveragerc 3 | .dockerignore 4 | Dockerfile 5 | Makefile 6 | logs 7 | venv 8 | */tests* 9 | */*/tests* 10 | */fixtures 11 | *.pyc 12 | */*.pyc 13 | */*/*.pyc 14 | */*/*/*.pyc 15 | -------------------------------------------------------------------------------- /rootfs/bin/test-style: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script is designed to be run inside the container 4 | # 5 | 6 | # fail hard and fast even on pipelines 7 | set -eou pipefail 8 | 9 | flake8 --show-source 10 | -------------------------------------------------------------------------------- /rootfs/deis/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Deis main package, including the top-level URLs, Django project 3 | settings, and WSGI setup. Most application domain-specific code lives in 4 | the api, provider, cm, and web Django apps. 5 | """ 6 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # documentation is at https://codecov.io/gh/deis/controller/settings/yaml 2 | comment: 3 | layout: header, diff 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | threshold: 0.2 9 | changes: false 10 | -------------------------------------------------------------------------------- /charts/controller/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: controller 2 | home: https://github.com/deis/controller 3 | version: 4 | description: Deis Workflow Controller (API). 5 | maintainers: 6 | - name: Deis Team 7 | email: engineering@deis.com 8 | -------------------------------------------------------------------------------- /charts/controller/templates/deploy-hook-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: deploy-hook-key 5 | labels: 6 | heritage: deis 7 | annotations: 8 | "helm.sh/hook": pre-install 9 | type: Opaque 10 | data: 11 | secret-key: {{ randAscii 64 | b64enc }} 12 | -------------------------------------------------------------------------------- /charts/controller/templates/controller-secret-django-secret-key.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: django-secret-key 5 | labels: 6 | heritage: deis 7 | annotations: 8 | "helm.sh/hook": pre-install 9 | type: Opaque 10 | data: 11 | secret-key: {{ randAscii 64 | b64enc }} 12 | -------------------------------------------------------------------------------- /charts/controller/templates/_helpers.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Set apiVersion based on Kubernetes version 3 | */}} 4 | {{- define "rbacAPIVersion" -}} 5 | {{- if ge .Capabilities.KubeVersion.Minor "6" -}} 6 | rbac.authorization.k8s.io/v1beta1 7 | {{- else -}} 8 | rbac.authorization.k8s.io/v1alpha1 9 | {{- end -}} 10 | {{- end -}} 11 | -------------------------------------------------------------------------------- /rootfs/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings.production") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [*.go] 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from scheduler.resources.__resource import Resource # noqa 2 | 3 | # Load in all resources 4 | import pkgutil 5 | import importlib 6 | import os 7 | pkgpath = os.path.dirname(__file__) 8 | for _, name, _ in pkgutil.iter_modules([pkgpath]): 9 | if not name.startswith('__'): 10 | importlib.import_module('.{}'.format(name), 'scheduler.resources') 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | If your change requires any additions or changes to the [documentation][docs] or to the [end-to-end test suite][e2e], please submit them as 1 or more pull requests against that repo and refer to them here. 2 | 3 | requires deis/workflow#1234 4 | requires deis/workflow-e2e#5678 5 | 6 | 7 | [docs]: https://github.com/deis/workflow 8 | [e2e]: https://github.com/deis/workflow-e2e 9 | -------------------------------------------------------------------------------- /rootfs/bin/test-unit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script is designed to be run inside the container 4 | # 5 | 6 | # fail hard and fast even on pipelines 7 | set -eou pipefail 8 | 9 | sudo -u postgres "$PGBIN"/pg_ctl -D "$PGDATA" -l /tmp/logfile start 10 | python3 manage.py check 11 | coverage run manage.py test --settings=api.settings.testing --noinput registry api scheduler.tests 12 | coverage report -m 13 | -------------------------------------------------------------------------------- /charts/controller/templates/controller-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: deis-controller 5 | labels: 6 | heritage: deis 7 | router.deis.io/routable: "true" 8 | annotations: 9 | router.deis.io/domains: deis 10 | router.deis.io/connectTimeout: "10" 11 | router.deis.io/tcpTimeout: "1200" 12 | spec: 13 | ports: 14 | - name: http 15 | port: 80 16 | targetPort: 8000 17 | selector: 18 | app: deis-controller 19 | -------------------------------------------------------------------------------- /rootfs/deis/gunicorn/logging.py: -------------------------------------------------------------------------------- 1 | import os 2 | from gunicorn.glogging import Logger 3 | 4 | 5 | class Logging(Logger): 6 | def access(self, resp, req, environ, request_time): 7 | # health check endpoints are only logged in debug mode 8 | if ( 9 | not os.environ.get('DEIS_DEBUG', False) and 10 | req.path in ['/readiness', '/healthz'] 11 | ): 12 | return 13 | 14 | Logger.access(self, resp, req, environ, request_time) 15 | -------------------------------------------------------------------------------- /rootfs/api/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for Deis Workflow Controller project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings.production") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /rootfs/deis/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL routing patterns for the Deis project. 3 | 4 | This is the "master" urls.py which then includes the urls.py files of 5 | installed apps. 6 | """ 7 | 8 | 9 | from django.conf.urls import include, url 10 | from api.views import LivenessCheckView 11 | from api.views import ReadinessCheckView 12 | 13 | urlpatterns = [ 14 | url(r'^healthz$', LivenessCheckView.as_view()), 15 | url(r'^readiness$', ReadinessCheckView.as_view()), 16 | url(r'^v2/', include('api.urls')), 17 | ] 18 | -------------------------------------------------------------------------------- /rootfs/dev_requirements.txt: -------------------------------------------------------------------------------- 1 | # Run "make test-unit" for the % of code exercised during tests 2 | coverage==4.4.1 3 | 4 | # Run "make test-style" to check python syntax and style 5 | flake8==3.4.1 6 | 7 | # code coverage report at https://codecov.io/github/deis/controller 8 | codecov==2.0.9 9 | 10 | # mock out python-requests, mostly k8s 11 | # requests-mock==1.3.0 12 | git+https://github.com/deis/requests-mock.git@class_adapter#egg=request_mock 13 | 14 | # tail a log and pipe into tbgrep to find all tracebacks 15 | tbgrep==0.3.0 16 | -------------------------------------------------------------------------------- /rootfs/requirements.txt: -------------------------------------------------------------------------------- 1 | # Deis controller requirements 2 | backoff==1.4.3 3 | Django==1.11.4 4 | django-auth-ldap==1.2.15 5 | django-cors-middleware==1.3.1 6 | django-guardian==1.4.9 7 | djangorestframework==3.6.4 8 | docker-py==1.10.6 9 | gunicorn==19.7.1 10 | idna==2.6 11 | jmespath==0.9.3 12 | jsonfield==2.0.2 13 | jsonschema==2.6.0 14 | morph==0.1.2 15 | ndg-httpsclient==0.4.2 16 | packaging==16.8 17 | pyasn1==0.3.2 18 | psycopg2==2.7.3 19 | pyldap==2.4.37 20 | pyOpenSSL==17.2.0 21 | pytz==2017.2 22 | requests==2.18.4 23 | requests-toolbelt==0.8.0 24 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0005_auto_20160208_2156.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-02-08 21:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('api', '0004_auto_20160124_2134'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='build', 17 | name='image', 18 | field=models.TextField(), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0020_release_failed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.1 on 2016-10-03 18:50 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('api', '0019_auto_20160930_2351'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='release', 17 | name='failed', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0016_auto_20160830_0104.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-08-30 01:04 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('api', '0015_auto_20160822_2103'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='certificate', 17 | name='common_name', 18 | field=models.TextField(editable=False, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0008_config_registry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-04-21 19:06 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | import jsonfield.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0007_auto_20160226_2335'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='config', 18 | name='registry', 19 | field=jsonfield.fields.JSONField(blank=True, default={}), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0021_appsettings_label.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.2 on 2016-11-19 16:11 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | import jsonfield.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0020_release_failed'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='appsettings', 18 | name='label', 19 | field=jsonfield.fields.JSONField(blank=True, default={}), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0010_config_healthcheck.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-06-17 20:18 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | import jsonfield.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0009_auto_20160607_2259'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='config', 18 | name='healthcheck', 19 | field=jsonfield.fields.JSONField(blank=True, default={}), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0017_appsettings_autoscale.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-08-30 17:32 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | import jsonfield.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0016_auto_20160830_0104'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='appsettings', 18 | name='autoscale', 19 | field=jsonfield.fields.JSONField(blank=True, default={}), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /charts/controller/templates/controller-clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if (.Values.global.use_rbac) -}} 2 | {{- if (.Capabilities.APIVersions.Has (include "rbacAPIVersion" .)) -}} 3 | kind: ClusterRoleBinding 4 | apiVersion: {{ template "rbacAPIVersion" . }} 5 | metadata: 6 | name: deis:deis-controller 7 | labels: 8 | app: deis-controller 9 | heritage: deis 10 | roleRef: 11 | apiGroup: rbac.authorization.k8s.io 12 | kind: ClusterRole 13 | name: deis:deis-controller 14 | subjects: 15 | - kind: ServiceAccount 16 | name: deis-controller 17 | namespace: {{ .Release.Namespace }} 18 | {{- end -}} 19 | {{- end -}} 20 | -------------------------------------------------------------------------------- /rootfs/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */venv/* 4 | */virtualenv/* 5 | *tests* 6 | api/__init__.py 7 | scheduler/mock.py 8 | api/migrations/* 9 | # osx library files when not running in virtualenv 10 | /Library/* 11 | /System/* 12 | /usr/local/lib/* 13 | branch = True 14 | 15 | [report] 16 | ignore_errors = True 17 | exclude_lines = 18 | pragma: no cover 19 | def __repr__ 20 | if self.debug: 21 | if settings.DEBUG 22 | raise AssertionError 23 | raise NotImplementedError 24 | if 0: 25 | if __name__ == .__main__.: 26 | 27 | [html] 28 | title = Controller Coverage Report 29 | -------------------------------------------------------------------------------- /rootfs/api/management/commands/healthchecks.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | import django.db 3 | import sys 4 | 5 | 6 | class Command(BaseCommand): 7 | """Management command for healthchecks""" 8 | def handle(self, *args, **options): 9 | """Ensure DB and other things are alive""" 10 | print("Checking if database is alive") 11 | try: 12 | django.db.connection.cursor() 13 | print("Database is alive!") 14 | except Exception as e: 15 | print("There was a problem connecting to the database") 16 | print(str(e)) 17 | sys.exit(1) 18 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0022_add_private_key_validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.4 on 2017-01-11 22:19 3 | from __future__ import unicode_literals 4 | 5 | import api.models.certificate 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0021_appsettings_label'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='certificate', 18 | name='key', 19 | field=models.TextField(validators=[api.models.certificate.validate_private_key]), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /charts/controller/templates/controller-ingress-rule-http-80.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.global.experimental_native_ingress }} 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | namespace: "deis" 6 | name: "controller-api-server-ingress-http" 7 | labels: 8 | app: "controller" 9 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 10 | release: "{{ .Release.Name }}" 11 | heritage: "{{ .Release.Service }}" 12 | spec: 13 | rules: 14 | - host: deis.{{ .Values.platform_domain }} 15 | http: 16 | paths: 17 | - path: / 18 | backend: 19 | serviceName: deis-controller 20 | servicePort: 80 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0014_appsettings_whitelist.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-08-23 22:58 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0013_auto_20160816_2122'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='appsettings', 18 | name='whitelist', 19 | field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), default=[], size=None), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0018_auto_20160908_1748.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-08 17:48 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0017_appsettings_autoscale'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='appsettings', 18 | name='whitelist', 19 | field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), default=None, size=None), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /rootfs/scheduler/exceptions.py: -------------------------------------------------------------------------------- 1 | class KubeException(Exception): 2 | def __init__(self, *args, **kwargs): 3 | Exception.__init__(self, *args, **kwargs) 4 | 5 | 6 | class KubeHTTPException(KubeException): 7 | def __init__(self, response, errmsg, *args, **kwargs): 8 | self.response = response 9 | 10 | data = response.json() 11 | message = data['message'] if 'message' in data else '' 12 | 13 | msg = errmsg.format(*args) 14 | msg = 'failed to {}: {} {} {}'.format( 15 | msg, 16 | response.status_code, 17 | response.reason, 18 | message 19 | ) 20 | KubeException.__init__(self, msg, *args, **kwargs) 21 | -------------------------------------------------------------------------------- /versioning.mk: -------------------------------------------------------------------------------- 1 | MUTABLE_VERSION ?= canary 2 | VERSION ?= git-$(shell git rev-parse --short HEAD) 3 | 4 | IMAGE := ${DEIS_REGISTRY}${IMAGE_PREFIX}/${SHORT_NAME}:${VERSION} 5 | MUTABLE_IMAGE := ${DEIS_REGISTRY}${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION} 6 | 7 | info: 8 | @echo "Build tag: ${VERSION}" 9 | @echo "Registry: ${DEIS_REGISTRY}" 10 | @echo "Immutable tag: ${IMAGE}" 11 | @echo "Mutable tag: ${MUTABLE_IMAGE}" 12 | 13 | .PHONY: docker-push 14 | docker-push: docker-mutable-push docker-immutable-push 15 | 16 | .PHONY: docker-immutable-push 17 | docker-immutable-push: 18 | docker push ${IMAGE} 19 | 20 | .PHONY: docker-mutable-push 21 | docker-mutable-push: 22 | docker push ${MUTABLE_IMAGE} 23 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0023_app_k8s_name_length.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.4 on 2017-01-11 22:15 3 | from __future__ import unicode_literals 4 | 5 | import api.models.app 6 | import api.models.certificate 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('api', '0022_add_private_key_validation'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='app', 19 | name='id', 20 | field=models.SlugField(max_length=63, null=True, unique=True, validators=[api.models.app.validate_app_id, api.models.app.validate_reserved_names]), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0004_auto_20160124_2134.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-01-24 21:34 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('api', '0003_auto_20160114_0310'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='certificate', 17 | name='expires', 18 | field=models.DateTimeField(editable=False), 19 | ), 20 | migrations.AlterField( 21 | model_name='key', 22 | name='fingerprint', 23 | field=models.CharField(editable=False, max_length=128), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | var 11 | sdist 12 | develop-eggs 13 | .installed.cfg 14 | lib 15 | lib64 16 | 17 | # Deis' config file 18 | controller/deis/local_settings.py 19 | controller/.secret_key 20 | 21 | # local binaries, installers, and artifacts 22 | client/deis 23 | client/client 24 | client/deis.exe 25 | client/*.run 26 | client/makeself/ 27 | _dist/ 28 | manifests/*.tmp.yml 29 | _tests/_tests.test 30 | _tests/example-*/ 31 | 32 | # coverage reports 33 | .coverage 34 | coverage.out 35 | coverage.xml 36 | htmlcov/ 37 | 38 | # python virtual environments for testing 39 | venv/ 40 | 41 | # vendored go source code 42 | vendor/ 43 | 44 | # generated bintray scripts during ci 45 | client/_scripts/ci/bintray-ci.json 46 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0013_auto_20160816_2122.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.8 on 2016-08-16 21:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('api', '0012_auto_20160816_1934'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterUniqueTogether( 16 | name='push', 17 | unique_together=set([]), 18 | ), 19 | migrations.RemoveField( 20 | model_name='push', 21 | name='app', 22 | ), 23 | migrations.RemoveField( 24 | model_name='push', 25 | name='owner', 26 | ), 27 | migrations.DeleteModel( 28 | name='Push', 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0007_auto_20160226_2335.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-02-26 23:35 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('api', '0006_auto_20160114_0313'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='container', 17 | name='app', 18 | ), 19 | migrations.RemoveField( 20 | model_name='container', 21 | name='owner', 22 | ), 23 | migrations.RemoveField( 24 | model_name='container', 25 | name='release', 26 | ), 27 | migrations.DeleteModel( 28 | name='Container', 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /rootfs/api/authentication.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AnonymousUser 2 | from rest_framework import authentication 3 | from rest_framework.authentication import TokenAuthentication 4 | 5 | 6 | class AnonymousAuthentication(authentication.BaseAuthentication): 7 | 8 | def authenticate(self, request): 9 | """ 10 | Authenticate the request for anyone! 11 | """ 12 | return AnonymousUser(), None 13 | 14 | 15 | class AnonymousOrAuthenticatedAuthentication(authentication.BaseAuthentication): 16 | 17 | def authenticate(self, request): 18 | """ 19 | Authenticate the request for anyone or if a valid token is provided, a user. 20 | """ 21 | try: 22 | return TokenAuthentication.authenticate(TokenAuthentication(), request) 23 | except: 24 | return AnonymousUser(), None 25 | -------------------------------------------------------------------------------- /rootfs/api/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTTP middleware for the Deis REST API. 3 | 4 | See https://docs.djangoproject.com/en/1.11/topics/http/middleware/ 5 | """ 6 | 7 | from api import __version__ 8 | 9 | 10 | class APIVersionMiddleware(object): 11 | """ 12 | Include that REST API version with each response. 13 | """ 14 | 15 | def __init__(self, get_response): 16 | self.get_response = get_response 17 | 18 | def __call__(self, request): 19 | """ 20 | Include the controller's REST API major and minor version in 21 | a response header. 22 | """ 23 | response = self.get_response(request) 24 | # clients shouldn't care about the patch release 25 | version = __version__.rsplit('.', 1)[0] 26 | response['DEIS_API_VERSION'] = version 27 | response['DEIS_PLATFORM_VERSION'] = __version__ 28 | return response 29 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/node.py: -------------------------------------------------------------------------------- 1 | from scheduler.resources import Resource 2 | from scheduler.exceptions import KubeHTTPException 3 | 4 | 5 | class Node(Resource): 6 | short_name = 'no' 7 | 8 | def get(self, name=None, **kwargs): 9 | """ 10 | Fetch a single Node or a list 11 | """ 12 | url = '/nodes' 13 | args = [] 14 | if name is not None: 15 | args.append(name) 16 | url += '/{}' 17 | message = 'get Node "{}" in Nodes' 18 | else: 19 | message = 'get Nodes' 20 | 21 | url = self.api(url, *args) 22 | response = self.http_get(url, params=self.query_params(**kwargs)) 23 | if self.unhealthy(response.status_code): 24 | args.reverse() # error msg is in reverse order 25 | raise KubeHTTPException(response, message, *args) 26 | 27 | return response 28 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/__resource.py: -------------------------------------------------------------------------------- 1 | from .. import KubeHTTPClient 2 | 3 | 4 | class ResourceRegistry(type): 5 | """ 6 | A registry of all Resources subclassed 7 | """ 8 | def __init__(cls, name, bases, nmspc): 9 | super().__init__(name, bases, nmspc) 10 | if not hasattr(cls, 'registry'): 11 | cls.registry = set() 12 | 13 | cls.registry.add(cls) 14 | cls.registry -= set(bases) # Remove base classes 15 | 16 | # Meta methods, called on class objects: 17 | def __iter__(cls): 18 | return iter(cls.registry) 19 | 20 | 21 | class Resource(KubeHTTPClient, metaclass=ResourceRegistry): 22 | api_version = 'v1' 23 | api_prefix = 'api' 24 | short_name = None 25 | 26 | def api(self, tmpl, *args): 27 | """Return a fully-qualified Kubernetes API URL from a string template with args.""" 28 | return "/{}/{}".format(self.api_prefix, self.api_version) + tmpl.format(*args) 29 | -------------------------------------------------------------------------------- /rootfs/api/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.permissions import IsAuthenticated 3 | 4 | from api import permissions 5 | 6 | 7 | class OwnerViewSet(viewsets.ModelViewSet): 8 | """ 9 | A simple ViewSet for objects filtered by their 'owner' attribute. 10 | 11 | To use it, at minimum you'll need to provide the `serializer_class` attribute and 12 | the `model` attribute shortcut. 13 | """ 14 | permission_classes = [IsAuthenticated, permissions.IsOwner] 15 | 16 | def get_queryset(self): 17 | return self.model.objects.filter(owner=self.request.user) 18 | 19 | def perform_create(self, serializer): 20 | obj = serializer.save(owner=self.request.user) 21 | self.post_save(obj) 22 | 23 | def post_save(self, obj): 24 | """A post_save hook for performing actions after the object has been pushed to the 25 | database. 26 | 27 | Leave it up to child classes to implement.""" 28 | pass 29 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/replicaset.py: -------------------------------------------------------------------------------- 1 | from scheduler.exceptions import KubeHTTPException 2 | from scheduler.resources import Resource 3 | 4 | 5 | class ReplicaSet(Resource): 6 | api_prefix = 'apis' 7 | api_version = 'extensions/v1beta1' 8 | short_name = 'rs' 9 | 10 | def get(self, namespace, name=None, **kwargs): 11 | """ 12 | Fetch a single ReplicaSet or a list 13 | """ 14 | url = '/namespaces/{}/replicasets' 15 | args = [namespace] 16 | if name is not None: 17 | args.append(name) 18 | url += '/{}' 19 | message = 'get ReplicaSet "{}" in Namespace "{}"' 20 | else: 21 | message = 'get ReplicaSets in Namespace "{}"' 22 | 23 | url = self.api(url, *args) 24 | response = self.http_get(url, params=self.query_params(**kwargs)) 25 | if self.unhealthy(response.status_code): 26 | args.reverse() # error msg is in reverse order 27 | raise KubeHTTPException(response, message, *args) 28 | 29 | return response 30 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/self-signed.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICujCCAaICAQAwdTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMQswCQYDVQQH 3 | EwJTRjERMA8GA1UEChMIRGVpcyBJbmMxFDASBgNVBAsTC0VuZ2luZWVyaW5nMSMw 4 | IQYJKoZIhvcNAQkBFhRlbmdpbmVlcmluZ0BkZWlzLmNvbTCCASIwDQYJKoZIhvcN 5 | AQEBBQADggEPADCCAQoCggEBAKMjs+IP0V1Fwgn4FNLvsGn1d4TlshXBEFeLUxFV 6 | PBKIRSy3TiVNpIFmkJfV+CarJH26e1I65oX0r58w1OvhCmepzGsvij63+u81XPx0 7 | xe6CffUiy36Sv6M6ezVF3mrCe4FvdM2eCCMYJvKYoQKpUFyIPOyfX6lZSjhDjcVd 8 | w6mLgboWh5hz9k3Vu1U7mwQdl2FJTuwXnexQ5cRU9HzcEnA3RJAhlzcw/Ns11HVK 9 | uDZHdbvqIy5hKF99bxf5XnNgQSI6KKALsFFKqCJsJ0MRXXQPuGK87meqjGnRbXVY 10 | h8/splN6DkCicTQ6pPrl0zRoXikRxxE6VviOHpHD6KDrR/8CAwEAAaAAMA0GCSqG 11 | SIb3DQEBBQUAA4IBAQB9yz4nqA1akJX+AtNZP/xDXgsfBB35zfOrzvuLVOU4S0kG 12 | Y488FwrhwI62HbOi6rRADQ0mCrgH4H2l+6seH8OEB12hI9KIPIBQCK+TJJPBlhgY 13 | rFDpG05n3M0oq86FQ0iOxSdDZ562E5fPVi3YaQZvgrWnX6S/YGB37m9Dblf5gzGz 14 | TftjOi34LA0LWkJCwMTARUGR943LURufYyduotQw8/3oKbQSOAWCub7beEPQenBB 15 | OXR3hBfQCtOPY0NbuBGSRqBuJbfoFJbMlK5TNI0bYBH+w7RHzw3y36cogHVz8ioT 16 | EMch2kgSeUTsKyjCIp3BS2hyk2PqHYvu2Eud++Er 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /rootfs/api/tests/test_api_middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis api app. 3 | 4 | Run the tests with "./manage.py test api" 5 | """ 6 | 7 | from django.contrib.auth.models import User 8 | from rest_framework.authtoken.models import Token 9 | from api.tests import DeisTestCase 10 | 11 | from api import __version__ 12 | 13 | 14 | class APIMiddlewareTest(DeisTestCase): 15 | 16 | """Tests middleware.py's business logic""" 17 | 18 | fixtures = ['tests.json'] 19 | 20 | def setUp(self): 21 | self.user = User.objects.get(username='autotest') 22 | self.token = Token.objects.get(user=self.user).key 23 | self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token) 24 | 25 | def test_deis_version_header_good(self): 26 | """ 27 | Test that when the version header is sent. 28 | """ 29 | response = self.client.get('/v2/apps') 30 | self.assertEqual(response.status_code, 200) 31 | self.assertEqual(response.has_header('DEIS_API_VERSION'), True) 32 | self.assertEqual(response['DEIS_API_VERSION'], __version__.rsplit('.', 1)[0]) 33 | -------------------------------------------------------------------------------- /rootfs/deis/gunicorn/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import dirname, realpath 3 | import faulthandler 4 | faulthandler.enable() 5 | 6 | 7 | bind = '0.0.0.0' 8 | try: 9 | workers = int(os.environ.get('GUNICORN_WORKERS', 'not set')) 10 | if workers < 1: 11 | raise ValueError() 12 | except (NameError, ValueError): 13 | workers = (os.cpu_count() or 4) * 4 + 1 14 | 15 | pythonpath = dirname(dirname(dirname(realpath(__file__)))) 16 | timeout = 1200 17 | pidfile = '/tmp/gunicorn.pid' 18 | logger_class = 'deis.gunicorn.logging.Logging' 19 | loglevel = 'info' 20 | errorlog = '-' 21 | accesslog = '-' 22 | access_log_format = '%(h)s "%(r)s" %(s)s %(b)s "%(a)s"' 23 | 24 | 25 | def worker_int(worker): 26 | """Print a stack trace when a worker receives a SIGINT or SIGQUIT signal.""" 27 | worker.log.warning('worker terminated') 28 | import traceback 29 | traceback.print_stack() 30 | 31 | 32 | def worker_abort(worker): 33 | """Print a stack trace when a worker receives a SIGABRT signal, generally on timeout.""" 34 | worker.log.warning('worker aborted') 35 | import traceback 36 | traceback.print_stack() 37 | -------------------------------------------------------------------------------- /rootfs/scheduler/states.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | 4 | class OrderedEnum(Enum): 5 | def __ge__(self, other): 6 | if self.__class__ is other.__class__: 7 | return self.value >= other.value 8 | 9 | return NotImplemented 10 | 11 | def __gt__(self, other): 12 | if self.__class__ is other.__class__: 13 | return self.value > other.value 14 | 15 | return NotImplemented 16 | 17 | def __le__(self, other): 18 | if self.__class__ is other.__class__: 19 | return self.value <= other.value 20 | 21 | return NotImplemented 22 | 23 | def __lt__(self, other): 24 | if self.__class__ is other.__class__: 25 | return self.value < other.value 26 | 27 | return NotImplemented 28 | 29 | 30 | @unique 31 | class PodState(OrderedEnum): 32 | initializing = 1 33 | creating = 2 34 | starting = 3 35 | up = 4 36 | terminating = 5 37 | down = 6 38 | destroyed = 7 39 | crashed = 8 40 | error = 9 41 | 42 | def __str__(self): 43 | """Return the name of the state""" 44 | return self.name 45 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/bar.com.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC6TCCAdECAQAwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UE 3 | BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMERGVpczEUMBIGA1UECxMLRW5naW5l 4 | ZXJpbmcxEDAOBgNVBAMTB2Jhci5jb20xIzAhBgkqhkiG9w0BCQEWFGVuZ2luZWVy 5 | aW5nQGRlaXMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApFWj 6 | z0G3dvFBX8Grzo+vsnjCQZZWLSmZZU4x4OzVRqKvKBVtfc1kDSCqG3IfSjAnrsdQ 7 | o3wqYQze4Bnr1h5ipg1NE47ow/msSwA4zpc8Kr5kC5yelGKISjOqcddXJs7xlkww 8 | MfmPonV+kAEFqnmoQpRJZ2ORkz1Rvxw+mJhPZkAjG0tGHZEQo9UYjnOmJt6rr1nk 9 | jIeI7yRnLWHSGQDe9up0b0vRbbzESx9uiAwqL0HSja7HYu/uGHywTHCTiAOq/uBM 10 | 0ESwFpE4Con4Kg0mX1IVydzapyZuegft+QJ/P6WVtVDnJK64FBukxlbp3li4bKO+ 11 | opWSfKiqKGtymv3o/wIDAQABoBUwEwYJKoZIhvcNAQkCMQYTBERlaXMwDQYJKoZI 12 | hvcNAQEFBQADggEBABS6cMdmwy+7aTiWNDGRzhWx+4H8GsQ/5syDAEMYXwgS/+sB 13 | xfxSC1jrq4yW/Pk66JrQU1kTCvziMUsk4dtAibAIgsTfkZIl1ev0RBs6pXQvMp7S 14 | qhrEaod8MNtlztgWz/lwE0V29r9orvFt4yeSAxr9l7XCFu3Xc6blB+/yGpNelPQn 15 | WBZBYAKzxMNltQCHc3A1OSd0b+OEHKYSc1DvCVNVFxFHjKPUuy97QyhSZa9qk87p 16 | jtJM9peJwpi3ZiGX8Ktf/QhROZBfGlIH4/GAIy3lu8dvlZ8XssYoPYQvMvzAFhgl 17 | 1iXwH1Swt/lMBsOv00vhdKmDGp+9yH+UEw6QKHs= 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/foo.com.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC6TCCAdECAQAwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UE 3 | BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMERGVpczEUMBIGA1UECxMLRW5naW5l 4 | ZXJpbmcxEDAOBgNVBAMTB2Zvby5jb20xIzAhBgkqhkiG9w0BCQEWFGVuZ2luZWVy 5 | aW5nQGRlaXMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyetP 6 | 8IJjDY0unbQbeIE8p8H2yIypT+2ahjORBb31EAnEv1ESm5zqjP21jphtJY10ECmp 7 | q4Tp94Q2na/1InbFyH/xAFw2ONfqBWhazgoCqjO1JhOV5+a6kBipPraPkiLLKnzr 8 | dyy6MhQlV+Sl+EfUDUVFyEslzp1VEO+c+2JoUugqz7dKxbMz2I6Kbq/8UJcvL3QQ 9 | MWT29mjpGmulv+luhDWaSae+PU0DKKUwFk3akhgcY0JSg7QUkjx0GBI+8S0UtnZq 10 | VDzWXN1ZI6YrjRqmcsqfUkeEGUhyg7zN3IeoRUYNTABftZgSsTXSYLBeLB7alT84 11 | 7o4XRWpialrphN3TBwIDAQABoBUwEwYJKoZIhvcNAQkCMQYTBERlaXMwDQYJKoZI 12 | hvcNAQEFBQADggEBABQJtFSeO+pjLmz9BtsN2yw7kxYD7rt0A578mlZMbKKHhZVA 13 | x9wgZt75DGZ/8FTqlq090VsobioodQDYUwuHKdFg3rADf0A1dO4VMYkuqKFv1Yei 14 | Kw0YibeqDIzElvLZu4HqVaNRZEfQfZR7umIWwaRyOu2J1VxN7He3dpde4XJfCQbE 15 | KGZFE3RKNnJTx0D7yvTJTIWDLRJkzwMjys5tTQ8SZV1yNZgbEEa05ggqgkGRcmX+ 16 | KoQ9xpPDEDaWn8m+6Ehiku+G76gFauTg4KB8zJaj8MSYRIjRcpZXi+GqaL2Ij+oW 17 | IukHD6hQVldYdsZvRPSLKh2lU+i4UNVc8wi/T8M= 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/wildcard.foo.com.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC5DCCAcwCAQAwgYkxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UE 3 | BwwCU0YxETAPBgNVBAoMCERlaXMgSW5jMRQwEgYDVQQLDAtFbmdpbmVlcmluZzES 4 | MBAGA1UEAwwJKi5mb28uY29tMSMwIQYJKoZIhvcNAQkBFhRlbmdpbmVlcmluZ0Bk 5 | ZWlzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALZISvXb5qPN 6 | v01OsDyzqo7/xHmFo1gjqarn6eLGp1M20DpyPKRw0i6xijor4y5dmxxvpi+Zk5rp 7 | YDZ25u2d8ElbFRT7hUH6pOgE0s2gAdfvLoq/CovqQOH4T7INMWs1ZEUilxeMZXfE 8 | K6EWMtCvpzJfct+SHOPccvTKJRXJa8EWNSS2aGkUbLJkfkGh6jnRmgrOCXHVOqRY 9 | 8jXZKr3KtviGIkh33mxiCmlhEnTR1CFxov4GH7qOhAnCGny2QGyDDXjKk9A3uPqa 10 | n9qW5/az6eDoRe7Ngvfno5QC9EwaBWUBLJzUXsU3P2KR4XUlt0iYEJj5+GikpGuv 11 | mi8cvdGborMCAwEAAaAVMBMGCSqGSIb3DQEJAjEGDAREZWlzMA0GCSqGSIb3DQEB 12 | BQUAA4IBAQBCypl/lb1F1fwtaRVvvsWG5JIpRzzGaevatsmN54tGKAr7ixYN38C0 13 | zgfSOQSefUN5yUd7d5UWJIA1A0hvVthtMwTKNv4giGy3ow+XC+VXPBsi668Q7hfP 14 | m19fmsDbV0v8RGSFBmY/qTUyVFKwFo8nveXnVyLurOIqz9c9Z+gy+HwyNL/u95L5 15 | DD2bXqvmjMqtKqO7b7cF0eBfTDuhv62vigFbm3JNi8UeLJrFV+wketh5hthY/Ujj 16 | xQfCfaA61bUjqSHHbIpXQ/bbsOHsAHtWAH06JOgDIOEpIyzOulmYay5IaYa4+wdS 17 | srDcCeiKNACra0GXtP1s86XLb01gsjkw 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/www.foo.com.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIC7TCCAdUCAQAwgZIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UE 3 | BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMERGVpczEUMBIGA1UECxMLRW5naW5l 4 | ZXJpbmcxFDASBgNVBAMTC3d3dy5mb28uY29tMSMwIQYJKoZIhvcNAQkBFhRlbmdp 5 | bmVlcmluZ0BkZWlzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 6 | AMotFeZDdY3ozrVkmTBQJ3sSJjlGfZVkFQxW2dOcd1dqGpCQeVgEZfqe8h0QcsoT 7 | CtrPIW+4xERoLFS96q6ykcfnbMAN6iMEx49ljRQX1sbJmwjKQLobvEL+vBiOZe3Q 8 | NYd99puDbVkEofXjO0+i4wXiZHNtgmJA/jJzqsLd28u/rffToz85czmIuPHowN5+ 9 | ZpwlGA3JcdVhAIaxsKPIbDYEWFIMTHb25BVinXrefWaCwODlP4gSGL2hSqxRCbvX 10 | xkDsyG+PeaHHVFy5x3hM/ZGDLwqupr4Qybfp7b10/sRQ5w5j2iU3ed82/TlI14bM 11 | rPMXItTTZiBLZ1KFSPAoo30CAwEAAaAVMBMGCSqGSIb3DQEJAjEGEwREZWlzMA0G 12 | CSqGSIb3DQEBBQUAA4IBAQDAnMAFZXdasWR32o/fakHltjDjISyWbH3rpM/kTLI5 13 | SmX08y7oJQFJe4b8WKwYFQ+sTixDveoDWzgzwJQo4kRPqFBnRFlo84hWfYMNQIdz 14 | pFSmgWJ5Cp1r4JsVm5Btvcag04QmtihgbMt5WT2T96HPdYkCjI3LxqTtXa2WLcGW 15 | QUiMB0Tk0WGn3CSLA4SXgM3U/dwrs8/KTP1sCE0Ne+zHeC7q1mIS35qNJzo4xkv4 16 | tmYRU8VYDYiYSbovULfIaozZpREo06ioVdztjXt/UnCOxE1mMl3xgPoesodiBMPD 17 | j3YMzpl72QD18EU8br36XmMqIYk/+qzLJTOS9Gfg7GzN 18 | -----END CERTIFICATE REQUEST----- 19 | -------------------------------------------------------------------------------- /rootfs/api/tests/test_users.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework.authtoken.models import Token 3 | from api.tests import DeisTestCase 4 | 5 | 6 | class TestUsers(DeisTestCase): 7 | """ Tests users endpoint""" 8 | 9 | fixtures = ['tests.json'] 10 | 11 | def test_super_user_can_list(self): 12 | user = User.objects.get(username='autotest') 13 | token = Token.objects.get(user=user) 14 | 15 | for url in ['/v2/users', '/v2/users/']: 16 | response = self.client.get(url, 17 | HTTP_AUTHORIZATION='token {}'.format(token)) 18 | self.assertEqual(response.status_code, 200, response.data) 19 | self.assertEqual(len(response.data['results']), 4) 20 | 21 | def test_non_super_user_cannot_list(self): 22 | user = User.objects.get(username='autotest2') 23 | token = Token.objects.get(user=user) 24 | 25 | for url in ['/v2/users', '/v2/users/']: 26 | response = self.client.get(url, 27 | HTTP_AUTHORIZATION='token {}'.format(token)) 28 | self.assertEqual(response.status_code, 403) 29 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_quota.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis scheduler module. 3 | 4 | Run the tests with './manage.py test scheduler' 5 | """ 6 | from scheduler.tests import TestCase 7 | from scheduler import KubeHTTPException 8 | 9 | 10 | class QuotaTest(TestCase): 11 | 12 | def test_create_quota(self): 13 | namespace_name = self.create_namespace() 14 | quota = { 15 | 'spec': { 16 | 'hard': { 17 | 'cpu': '3', 18 | 'pods': '10', 19 | 'secrets': '5' 20 | } 21 | } 22 | } 23 | self.scheduler.quota.create(namespace_name, 'test1', data=quota) 24 | 25 | response = self.scheduler.quota.get(namespace_name, 'test1') 26 | data = response.json() 27 | self.assertEqual(data.get('spec', {}), quota['spec']) 28 | self.assertEqual(data['metadata']['namespace'], namespace_name) 29 | 30 | def test_create_with_nonexistent_namespace(self): 31 | with self.assertRaises( 32 | KubeHTTPException, 33 | msg='failed to create quota test1 for namespace ghost-namespace: 404 Not Found' 34 | ): 35 | self.scheduler.quota.create('ghost-namespace', 'test1', data={}) 36 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0019_auto_20160930_2351.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-30 23:51 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | from django.db.models import Count 7 | 8 | 9 | def fix_duplicate_keys(apps, schema_editor): 10 | Keys = apps.get_model("api", "Key") 11 | 12 | # find duplicates 13 | duplicates = Keys.objects.values('id') \ 14 | .annotate(Count('id')) \ 15 | .order_by() \ 16 | .filter(id__count__gt=1) 17 | for dup in duplicates: 18 | # update all duplicates 19 | inc = 1 20 | for key in Keys.objects.filter(id=dup['id']): 21 | key_id = '{}-{}'.format(key.id, inc) 22 | key.id = key_id 23 | key.save() 24 | inc += 1 25 | 26 | 27 | class Migration(migrations.Migration): 28 | 29 | dependencies = [ 30 | ('api', '0018_auto_20160908_1748'), 31 | ] 32 | 33 | operations = [ 34 | migrations.RunPython(fix_duplicate_keys), 35 | migrations.AlterField( 36 | model_name='key', 37 | name='id', 38 | field=models.CharField(max_length=128, unique=True), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /rootfs/api/fixtures/dev.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": -1, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "AnonymousUser", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": false, 11 | "is_staff": false, 12 | "last_login": "2014-02-16T13:13:47.539Z", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "", 16 | "email": "", 17 | "date_joined": "2014-02-16T13:13:47.539Z" 18 | } 19 | }, 20 | { 21 | "pk": 1, 22 | "model": "auth.user", 23 | "fields": { 24 | "username": "devuser", 25 | "first_name": "", 26 | "last_name": "", 27 | "is_active": true, 28 | "is_superuser": true, 29 | "is_staff": true, 30 | "last_login": "2014-01-27T23:30:25.062Z", 31 | "groups": [], 32 | "user_permissions": [], 33 | "password": "pbkdf2_sha256$12000$sGISfpN866YM$+pWXpirYh4rSfa0XX9kxvH047GTycDa8U9axRLHuJhk=", 34 | "email": "dev@dev.com", 35 | "date_joined": "2014-01-27T22:23:49.793Z" 36 | } 37 | }, 38 | { 39 | "pk": 1, 40 | "model": "sites.site", 41 | "fields": { 42 | "domain": "local3.deisapp.com", 43 | "name": "local3.deisapp.com" 44 | } 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | from django.test import TestCase as DjangoTestCase 3 | from django.conf import settings 4 | 5 | from scheduler import mock 6 | from scheduler.utils import generate_random_name 7 | 8 | 9 | class TestCase(DjangoTestCase): 10 | def setUp(self): 11 | self.scheduler = mock.MockSchedulerClient(settings.SCHEDULER_URL) 12 | # have a namespace available at all times 13 | self.namespace = self.create_namespace() 14 | 15 | def tearDown(self): 16 | # make sure every test has a clean slate for k8s mocking 17 | cache.clear() 18 | 19 | def create_namespace(self): 20 | namespace = generate_random_name() 21 | response = self.scheduler.ns.create(namespace) 22 | self.assertEqual(response.status_code, 201, response.json()) 23 | # assert minimal amount data 24 | data = response.json() 25 | self.assertEqual(data['apiVersion'], 'v1') 26 | self.assertEqual(data['kind'], 'Namespace') 27 | self.assertDictContainsSubset( 28 | { 29 | 'name': namespace, 30 | 'labels': { 31 | 'heritage': 'deis' 32 | } 33 | }, 34 | data['metadata'] 35 | ) 36 | 37 | return namespace 38 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/self-signed.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZjCCAk4CCQDeqDhK+PmamDANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJV 3 | UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMREwDwYDVQQKEwhEZWlzIEluYzEU 4 | MBIGA1UECxMLRW5naW5lZXJpbmcxIzAhBgkqhkiG9w0BCQEWFGVuZ2luZWVyaW5n 5 | QGRlaXMuY29tMB4XDTE2MDgzMDAwNTE1NFoXDTE3MDgzMDAwNTE1NFowdTELMAkG 6 | A1UEBhMCVVMxCzAJBgNVBAgTAkNBMQswCQYDVQQHEwJTRjERMA8GA1UEChMIRGVp 7 | cyBJbmMxFDASBgNVBAsTC0VuZ2luZWVyaW5nMSMwIQYJKoZIhvcNAQkBFhRlbmdp 8 | bmVlcmluZ0BkZWlzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 9 | AKMjs+IP0V1Fwgn4FNLvsGn1d4TlshXBEFeLUxFVPBKIRSy3TiVNpIFmkJfV+Car 10 | JH26e1I65oX0r58w1OvhCmepzGsvij63+u81XPx0xe6CffUiy36Sv6M6ezVF3mrC 11 | e4FvdM2eCCMYJvKYoQKpUFyIPOyfX6lZSjhDjcVdw6mLgboWh5hz9k3Vu1U7mwQd 12 | l2FJTuwXnexQ5cRU9HzcEnA3RJAhlzcw/Ns11HVKuDZHdbvqIy5hKF99bxf5XnNg 13 | QSI6KKALsFFKqCJsJ0MRXXQPuGK87meqjGnRbXVYh8/splN6DkCicTQ6pPrl0zRo 14 | XikRxxE6VviOHpHD6KDrR/8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQjsDk1Bc 15 | +/fSM2ksuue+MPnXfpqTW+yZZ6HvyCsODt3R8Z5cHtsesBywWxtGXPGYAFuUKcHJ 16 | eibo7z2o71eO0vx0tQQH/+y6Dw+5h8RHb/XTj/vMvnYSeQ2R+yVbGP/v/PWfeni/ 17 | 13QlEZmW2Bi4v8D/z+FacumZ4nZF6LrXG/OmygoTA/UDB5yAiH0G8L2xHfS5xy8s 18 | gRNXFXIUScA1iTFQgYEPRorzLwYtKBlGsr6wEbehkbq4D+1KjJa7aEukAakxGHAR 19 | 000i9TMno4EDivZUC0xfQaXvBFfCHu/hrj1H3Obw1gWTKjsCe6QZwb+mWgbOcL4U 20 | 2Ul1nLoBiMR6Kg== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /rootfs/api/tests/test_healthz.py: -------------------------------------------------------------------------------- 1 | 2 | from api.tests import DeisTestCase 3 | 4 | 5 | class HealthCheckTest(DeisTestCase): 6 | 7 | def test_healthcheck_liveness(self): 8 | # GET and HEAD (no auth required) 9 | response = self.client.get('/healthz') 10 | self.assertContains(response, "OK", status_code=200) 11 | 12 | response = self.client.head('/healthz') 13 | self.assertEqual(response.status_code, 200) 14 | 15 | def test_healthcheck_liveness_invalid(self): 16 | for method in ('put', 'post', 'patch', 'delete'): 17 | response = getattr(self.client, method)('/healthz') 18 | # method not allowed 19 | self.assertEqual(response.status_code, 405) 20 | 21 | def test_healthcheck_readiness(self): 22 | # GET and HEAD (no auth required) 23 | response = self.client.get('/readiness') 24 | self.assertContains(response, "OK", status_code=200) 25 | 26 | response = self.client.head('/readiness') 27 | self.assertEqual(response.status_code, 200) 28 | 29 | def test_healthcheck_readiness_invalid(self): 30 | for method in ('put', 'post', 'patch', 'delete'): 31 | response = getattr(self.client, method)('/readiness') 32 | # method not allowed 33 | self.assertEqual(response.status_code, 405) 34 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_pod_states.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from scheduler.states import PodState 3 | 4 | 5 | class TestSchedulerStates(unittest.TestCase): 6 | """Test Scheduler States OrderedEnum""" 7 | 8 | def test_gt_comparison(self): 9 | self.assertTrue(PodState.up > PodState.starting) 10 | self.assertFalse(PodState.starting > PodState.up) 11 | with self.assertRaises(TypeError): 12 | self.assertTrue(PodState.up > 'starting') 13 | 14 | def test_ge_comparison(self): 15 | self.assertTrue(PodState.up >= PodState.starting) 16 | self.assertFalse(PodState.starting >= PodState.up) 17 | with self.assertRaises(TypeError): 18 | self.assertTrue(PodState.up >= 'starting') 19 | 20 | def test_lt_comparison(self): 21 | self.assertFalse(PodState.up < PodState.starting) 22 | self.assertTrue(PodState.starting < PodState.up) 23 | with self.assertRaises(TypeError): 24 | self.assertTrue(PodState.up < 'crashed') 25 | 26 | def test_le_comparison(self): 27 | self.assertFalse(PodState.up <= PodState.starting) 28 | self.assertTrue(PodState.starting <= PodState.up) 29 | with self.assertRaises(TypeError): 30 | self.assertTrue(PodState.up <= 'crashed') 31 | 32 | def test_str(self): 33 | self.assertEqual(str(PodState.up), 'up') 34 | -------------------------------------------------------------------------------- /rootfs/api/models/key.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from rest_framework.exceptions import ValidationError 6 | 7 | from api.models import UuidAuditedModel 8 | from api.utils import fingerprint 9 | 10 | 11 | def validate_base64(value): 12 | """Check that value contains only valid base64 characters.""" 13 | try: 14 | base64.b64decode(value.split()[1]) 15 | except Exception as e: 16 | raise ValidationError('Key contains invalid base64 chars') from e 17 | 18 | 19 | class Key(UuidAuditedModel): 20 | """An SSH public key.""" 21 | 22 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 23 | id = models.CharField(max_length=128, unique=True) 24 | public = models.TextField( 25 | unique=True, validators=[validate_base64], 26 | error_messages={ 27 | 'unique': 'Public Key is already in use' 28 | } 29 | ) 30 | fingerprint = models.CharField(max_length=128, editable=False) 31 | 32 | class Meta: 33 | verbose_name = 'SSH Key' 34 | unique_together = (('owner', 'fingerprint')) 35 | ordering = ['public'] 36 | 37 | def __str__(self): 38 | return "{}...{}".format(self.public[:18], self.public[-31:]) 39 | 40 | def save(self, *args, **kwargs): 41 | self.fingerprint = fingerprint(self.public) 42 | return super(Key, self).save(*args, **kwargs) 43 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_nodes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis scheduler module. 3 | 4 | Run the tests with "./manage.py test scheduler" 5 | """ 6 | from scheduler.tests import TestCase 7 | from scheduler import KubeHTTPException 8 | 9 | 10 | class NodesTest(TestCase): 11 | """Tests scheduler node calls""" 12 | 13 | def test_get_nodes(self): 14 | response = self.scheduler.node.get() 15 | data = response.json() 16 | self.assertEqual(response.status_code, 200, data) 17 | self.assertIn('items', data) 18 | # mock scheduler creates one node 19 | self.assertEqual(1, len(data['items'])) 20 | # simple verify of data 21 | self.assertEqual(data['items'][0]['metadata']['name'], '172.17.8.100') 22 | 23 | def test_get_node(self): 24 | with self.assertRaises( 25 | KubeHTTPException, 26 | msg='failed to get Node doesnotexist in Nodes: 404 Not Found' 27 | ): 28 | self.scheduler.node.get('doesnotexist') 29 | 30 | name = '172.17.8.100' 31 | response = self.scheduler.node.get(name) 32 | data = response.json() 33 | self.assertEqual(response.status_code, 200, data) 34 | self.assertEqual(data['apiVersion'], 'v1') 35 | self.assertEqual(data['kind'], 'Node') 36 | self.assertEqual(data['metadata']['name'], name) 37 | self.assertDictContainsSubset({'ssd': 'true'}, data['metadata']['labels']) 38 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/autotest.example.com.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID3jCCAsYCCQDg75CmAL+avjANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC 3 | Q0ExGTAXBgNVBAgTEEJyaXRpc2gtQ29sdW1iaWExEjAQBgNVBAcTCVZhbmNvdXZl 4 | cjEtMCsGA1UEChMkRmlzaHdvcmtzIERldmVsb3BtZW50IGFuZCBDb25zdWx0aW5n 5 | MR0wGwYDVQQDExRhdXRvdGVzdC5leGFtcGxlLmNvbTEkMCIGCSqGSIb3DQEJARYV 6 | bWF0dGhld2ZAZmlzaHdvcmtzLmlvMB4XDTE1MDMwNjE3MTQyN1oXDTE2MDMwNTE3 7 | MTQyN1owgbAxCzAJBgNVBAYTAkNBMRkwFwYDVQQIExBCcml0aXNoLUNvbHVtYmlh 8 | MRIwEAYDVQQHEwlWYW5jb3V2ZXIxLTArBgNVBAoTJEZpc2h3b3JrcyBEZXZlbG9w 9 | bWVudCBhbmQgQ29uc3VsdGluZzEdMBsGA1UEAxMUYXV0b3Rlc3QuZXhhbXBsZS5j 10 | b20xJDAiBgkqhkiG9w0BCQEWFW1hdHRoZXdmQGZpc2h3b3Jrcy5pbzCCASIwDQYJ 11 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMiyMI6VEJAJof8+ib0DALbzbv3Qbgr 12 | fkfHJ0DBMOFYJYJTMGCgJtmVwNNy2FpORDudmGg2SjwKo54PpuJPgdG5XuVxuLxU 13 | U5QYZZ9HEd1AGJZu2bWaf+iUdUk8KYfvedtA1LJ3T0zjtwKXDDLmqeci0WAKR3zs 14 | G8qImgglLmhP18mZ+hTf/WY9VdSDHHbvP/x7TNr2w5GeciW1Mw0qlCJx12VKezhf 15 | pFCsHptAjfkhpNtLngENy6MYPUVnF8APJHmdUrULDc5kc0Ez/1xhL2k6ySoAZJPR 16 | h58ZgtNC/avs29AWh2meSlwim8H9JgqjmZ4B7ik7yqQ+tk7z8+OsROMCAwEAATAN 17 | BgkqhkiG9w0BAQUFAAOCAQEAwYpXB8z4aOBedyHikbtVjDs1k0LEtWRAX/RXQY4I 18 | BAYTnO+eGs/p7o+e3LGrIt/pX8kJ0RgD7TLITUJCZ69KkG9GzZaJ/CgQgqEa4Goh 19 | JCI5u5a5nkTE6zZgAkkvpbA3Mj6WXGkGk7QEiO1e6e3y0jIBhDo1piD+DIppMWwM 20 | OI0/r46FDlPHnm+y7UmTx+GZB4RAxnFaJE5L76w63oIPaRc/zkhS49AYiSmlawxj 21 | thejiQz0ThCMBw7QMpVOiSvYAlQG0ATsRYwdTDqENIWKlerOLCSuxmbqe8XeDKhq 22 | 0ExzRJX9L9CjFIx9k+fIebIJWdv4Y4YUEtbLVmkKeghVJA== 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/scale.py: -------------------------------------------------------------------------------- 1 | from scheduler.resources import Resource 2 | from scheduler.exceptions import KubeHTTPException 3 | 4 | 5 | class Scale(Resource): 6 | def manifest(self, namespace, name, replicas): 7 | manifest = { 8 | 'kind': 'Scale', 9 | 'apiVersion': self.api_version, 10 | 'metadata': { 11 | 'namespace': namespace, 12 | 'name': name, 13 | }, 14 | 'spec': { 15 | 'replicas': replicas, 16 | } 17 | } 18 | 19 | return manifest 20 | 21 | def update(self, namespace, name, replicas, target): 22 | # use API version and prefix from target use pick the right endpoint 23 | resource_type = target['kind'].lower() + 's' # make plural for url 24 | self.api_version = getattr(self, resource_type).api_version 25 | self.api_prefix = getattr(self, resource_type).api_prefix 26 | 27 | manifest = self.manifest(namespace, name, replicas) 28 | url = self.api("/namespaces/{}/{}/{}/scale", namespace, resource_type, name) 29 | response = self.http_put(url, json=manifest) 30 | if self.unhealthy(response.status_code): 31 | raise KubeHTTPException( 32 | response, 33 | 'scale {} "{}" in Namespace "{}"', target['kind'], name, namespace 34 | ) 35 | self.log(namespace, 'template used: {}'.format(json.dumps(manifest, indent=4)), 'DEBUG') # noqa 36 | 37 | return response 38 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/wildcard.foo.com.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIECDCCAvCgAwIBAgIJAIHJ8mUaHGJhMA0GCSqGSIb3DQEBBQUAMIGJMQswCQYD 3 | VQQGEwJVUzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMREwDwYDVQQKDAhEZWlz 4 | IEluYzEUMBIGA1UECwwLRW5naW5lZXJpbmcxEjAQBgNVBAMMCSouZm9vLmNvbTEj 5 | MCEGCSqGSIb3DQEJARYUZW5naW5lZXJpbmdAZGVpcy5jb20wHhcNMTYwMTIyMjE1 6 | NjE1WhcNMTcwMTIxMjE1NjE1WjCBiTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNB 7 | MQswCQYDVQQHDAJTRjERMA8GA1UECgwIRGVpcyBJbmMxFDASBgNVBAsMC0VuZ2lu 8 | ZWVyaW5nMRIwEAYDVQQDDAkqLmZvby5jb20xIzAhBgkqhkiG9w0BCQEWFGVuZ2lu 9 | ZWVyaW5nQGRlaXMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 10 | tkhK9dvmo82/TU6wPLOqjv/EeYWjWCOpqufp4sanUzbQOnI8pHDSLrGKOivjLl2b 11 | HG+mL5mTmulgNnbm7Z3wSVsVFPuFQfqk6ATSzaAB1+8uir8Ki+pA4fhPsg0xazVk 12 | RSKXF4xld8QroRYy0K+nMl9y35Ic49xy9MolFclrwRY1JLZoaRRssmR+QaHqOdGa 13 | Cs4JcdU6pFjyNdkqvcq2+IYiSHfebGIKaWESdNHUIXGi/gYfuo6ECcIafLZAbIMN 14 | eMqT0De4+pqf2pbn9rPp4OhF7s2C9+ejlAL0TBoFZQEsnNRexTc/YpHhdSW3SJgQ 15 | mPn4aKSka6+aLxy90ZuiswIDAQABo3EwbzASBgNVHREECzAJggdmb28uY29tMAsG 16 | A1UdDwQEAwIFoDAdBgNVHQ4EFgQUEj9sR+4CBgj3XJnjvQMUO9jpkv0wHwYDVR0j 17 | BBgwFoAUEj9sR+4CBgj3XJnjvQMUO9jpkv0wDAYDVR0TBAUwAwEB/zANBgkqhkiG 18 | 9w0BAQUFAAOCAQEAQb6dbn55lpXEsBjr+KNCmJ0LPbTB64ILB7wwykdGmmO49x9r 19 | 6zq1vFPpLbgZmOQ/9tgpJ+NZ3cTzsnWo9Vt1c4LZQmvLHVSR+PHtreA3c3DqFGCu 20 | QeoWrf17WWFVQFR4WUQjYD2mDZtkAJhqa1aL0AfMxeq01PHLVWZ+iD8xLZ3MZKSH 21 | efk6U1Krs+Kxa8kPMRkltze0iLdyA8Oe+x2JT1tX26A+3PtM4wENfMmkLuWWbRR5 22 | ikAVEOmWeEsBvZqVnP+UR4s9ewIBL1RFlTeHXXn6ukMASnWwQAdDU5Aisk0z0T1b 23 | kZqEQ8Lio0ZLMCVJmHiQ4GqQbpIbKjNF3/riCQ== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0015_auto_20160822_2103.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.8 on 2016-08-22 21:03 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import uuid 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ('api', '0014_appsettings_whitelist'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='TLS', 21 | fields=[ 22 | ('uuid', models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID')), 23 | ('created', models.DateTimeField(auto_now_add=True)), 24 | ('updated', models.DateTimeField(auto_now=True)), 25 | ('https_enforced', models.NullBooleanField(default=None)), 26 | ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')), 27 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), 28 | ], 29 | options={ 30 | 'ordering': ['-created'], 31 | 'get_latest_by': 'created', 32 | }, 33 | ), 34 | migrations.AlterUniqueTogether( 35 | name='tls', 36 | unique_together=set([('app', 'uuid')]), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0011_auto_20160810_1603.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.8 on 2016-08-10 16:03 3 | from __future__ import unicode_literals 4 | 5 | import api.models.key 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0010_config_healthcheck'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='app', 18 | options={'ordering': ['id'], 'permissions': (('use_app', 'Can use app'),), 'verbose_name': 'Application'}, 19 | ), 20 | migrations.AlterModelOptions( 21 | name='certificate', 22 | options={'ordering': ['name', 'common_name', 'expires']}, 23 | ), 24 | migrations.AlterModelOptions( 25 | name='domain', 26 | options={'ordering': ['domain', 'certificate']}, 27 | ), 28 | migrations.AlterModelOptions( 29 | name='key', 30 | options={'ordering': ['public'], 'verbose_name': 'SSH Key'}, 31 | ), 32 | migrations.AlterField( 33 | model_name='domain', 34 | name='domain', 35 | field=models.TextField(error_messages={'unique': 'Domain is already in use by another application'}, unique=True), 36 | ), 37 | migrations.AlterField( 38 | model_name='key', 39 | name='public', 40 | field=models.TextField(error_messages={'unique': 'Public Key is already in use'}, unique=True, validators=[api.models.key.validate_base64]), 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0012_auto_20160816_1934.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.8 on 2016-08-16 19:34 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import uuid 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ('api', '0011_auto_20160810_1603'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='AppSettings', 21 | fields=[ 22 | ('uuid', models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID')), 23 | ('created', models.DateTimeField(auto_now_add=True)), 24 | ('updated', models.DateTimeField(auto_now=True)), 25 | ('maintenance', models.NullBooleanField(default=None)), 26 | ('routable', models.NullBooleanField(default=None)), 27 | ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')), 28 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), 29 | ], 30 | options={ 31 | 'get_latest_by': 'created', 32 | 'ordering': ['-created'], 33 | }, 34 | ), 35 | migrations.AlterUniqueTogether( 36 | name='appsettings', 37 | unique_together=set([('app', 'uuid')]), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/events.py: -------------------------------------------------------------------------------- 1 | from scheduler.exceptions import KubeHTTPException 2 | from scheduler.resources import Resource 3 | from datetime import datetime 4 | import uuid 5 | 6 | DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' 7 | 8 | 9 | class Events(Resource): 10 | """ 11 | Events resource. 12 | Warning! Used ONLY for testing purposes 13 | """ 14 | short_name = 'ev' 15 | 16 | def create(self, namespace, name, message, **kwargs): 17 | url = self.api('/namespaces/{}/events'.format(namespace)) 18 | data = { 19 | 'kind': 'Event', 20 | 'apiVersion': 'v1', 21 | 'count': kwargs.get('count', 1), 22 | 'metadata': { 23 | 'creationTimestamp': datetime.now().strftime(DATETIME_FORMAT), 24 | 'namespace': namespace, 25 | 'name': name, 26 | 'resourceVersion': kwargs.get('resourceVersion', ''), 27 | 'uid': str(uuid.uuid4()), 28 | }, 29 | 'message': message, 30 | 'type': kwargs.get('type', 'Normal'), 31 | 'firstTimestamp': datetime.now().strftime(DATETIME_FORMAT), 32 | 'lastTimestamp': datetime.now().strftime(DATETIME_FORMAT), 33 | 'reason': kwargs.get('reason', ''), 34 | 'source': { 35 | 'component': kwargs.get('component', ''), 36 | }, 37 | 'involvedObject': kwargs.get('involvedObject', {}) 38 | } 39 | 40 | response = self.http_post(url, json=data) 41 | if not response.status_code == 201: 42 | raise KubeHTTPException(response, 'create Event for namespace {}'.format(namespace)) # noqa 43 | 44 | return response 45 | -------------------------------------------------------------------------------- /rootfs/api/settings/testing.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import os 4 | 5 | from api.settings.production import DATABASES 6 | from api.settings.production import * # noqa 7 | 8 | # A boolean that turns on/off debug mode. 9 | # https://docs.djangoproject.com/en/1.11/ref/settings/#debug 10 | DEBUG = True 11 | 12 | # If set to True, Django's normal exception handling of view functions 13 | # will be suppressed, and exceptions will propagate upwards 14 | # https://docs.djangoproject.com/en/1.11/ref/settings/#debug-propagate-exceptions 15 | DEBUG_PROPAGATE_EXCEPTIONS = True 16 | 17 | # scheduler for testing 18 | SCHEDULER_MODULE = 'scheduler.mock' 19 | SCHEDULER_URL = 'http://test-scheduler.example.com' 20 | 21 | # router information 22 | ROUTER_HOST = 'deis-router.example.com' 23 | ROUTER_PORT = 80 24 | 25 | # randomize test database name so we can run multiple unit tests simultaneously 26 | DATABASES['default']['NAME'] = "unittest-{}".format(''.join( 27 | random.choice(string.ascii_letters + string.digits) for _ in range(8))) 28 | DATABASES['default']['USER'] = 'postgres' 29 | 30 | # use DB name to isolate the data for each test run 31 | CACHES = { 32 | 'default': { 33 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 34 | 'LOCATION': DATABASES['default']['NAME'], 35 | 'KEY_PREFIX': DATABASES['default']['NAME'], 36 | } 37 | } 38 | 39 | # How long k8s waits for a pod to finish work after a SIGTERM before sending SIGKILL 40 | KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS = int(os.environ.get('KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS', 2)) # noqa 41 | KUBERNETES_NAMESPACE_DEFAULT_QUOTA_SPEC = '{"spec":{"hard":{"pods":"10"}}}' 42 | 43 | DEIS_DEFAULT_CONFIG_TAGS = os.environ.get('DEIS_DEFAULT_CONFIG_TAGS', '') 44 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/bar.com.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEmDCCA4CgAwIBAgIJAN7RJoMZT8e4MA0GCSqGSIb3DQEBBQUAMIGOMQswCQYD 3 | VQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDTAL 4 | BgNVBAoTBERlaXMxFDASBgNVBAsTC0VuZ2luZWVyaW5nMRAwDgYDVQQDEwdiYXIu 5 | Y29tMSMwIQYJKoZIhvcNAQkBFhRlbmdpbmVlcmluZ0BkZWlzLmNvbTAeFw0xNjAx 6 | MTUyMzU3NTdaFw0xNzAxMTQyMzU3NTdaMIGOMQswCQYDVQQGEwJVUzELMAkGA1UE 7 | CBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDTALBgNVBAoTBERlaXMxFDAS 8 | BgNVBAsTC0VuZ2luZWVyaW5nMRAwDgYDVQQDEwdiYXIuY29tMSMwIQYJKoZIhvcN 9 | AQkBFhRlbmdpbmVlcmluZ0BkZWlzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP 10 | ADCCAQoCggEBAKRVo89Bt3bxQV/Bq86Pr7J4wkGWVi0pmWVOMeDs1UairygVbX3N 11 | ZA0gqhtyH0owJ67HUKN8KmEM3uAZ69YeYqYNTROO6MP5rEsAOM6XPCq+ZAucnpRi 12 | iEozqnHXVybO8ZZMMDH5j6J1fpABBap5qEKUSWdjkZM9Ub8cPpiYT2ZAIxtLRh2R 13 | EKPVGI5zpibeq69Z5IyHiO8kZy1h0hkA3vbqdG9L0W28xEsfbogMKi9B0o2ux2Lv 14 | 7hh8sExwk4gDqv7gTNBEsBaROAqJ+CoNJl9SFcnc2qcmbnoH7fkCfz+llbVQ5ySu 15 | uBQbpMZW6d5YuGyjvqKVknyoqihrcpr96P8CAwEAAaOB9jCB8zAdBgNVHQ4EFgQU 16 | hcUT7hWQJ++7+J9Oh4izif/uEnswgcMGA1UdIwSBuzCBuIAUhcUT7hWQJ++7+J9O 17 | h4izif/uEnuhgZSkgZEwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQG 18 | A1UEBxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMERGVpczEUMBIGA1UECxMLRW5n 19 | aW5lZXJpbmcxEDAOBgNVBAMTB2Jhci5jb20xIzAhBgkqhkiG9w0BCQEWFGVuZ2lu 20 | ZWVyaW5nQGRlaXMuY29tggkA3tEmgxlPx7gwDAYDVR0TBAUwAwEB/zANBgkqhkiG 21 | 9w0BAQUFAAOCAQEAkHlv2nay6/vsLQsjpB3t9QjcQAlbJLlWQjLjbcsuiuJJVDel 22 | ksFK8Qq/PlUaEyUXJJYZZJUPreRQFeC/A0iQ8FXAhjsJmqIeEoqjZ5Qc4WJhK9qJ 23 | CpGddTTjcKptGbdoyQTsa6bo4bNJy041zD8QCn7X4CPHIqeDtalsTZen+bTG7C/q 24 | rdShlgzI5xp3wYi5FS2EiNxgRcHGyxgRjtOGHwLibUK+UMYhXvVKaMxfWwDCA+3u 25 | mFyVWnTaVQ7ui8ybmUpsfqeaS/xSZG49gBxiTbhYrpxak3gdOm2u2FTkC/nH2s6u 26 | jKN/IBX4p/sWwffqBZ34olAoo71JpunO2weVhQ== 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/foo.com.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEmDCCA4CgAwIBAgIJAMCMT+W6VVrXMA0GCSqGSIb3DQEBBQUAMIGOMQswCQYD 3 | VQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDTAL 4 | BgNVBAoTBERlaXMxFDASBgNVBAsTC0VuZ2luZWVyaW5nMRAwDgYDVQQDEwdmb28u 5 | Y29tMSMwIQYJKoZIhvcNAQkBFhRlbmdpbmVlcmluZ0BkZWlzLmNvbTAeFw0xNjAx 6 | MTUyMzU1NTlaFw0xNzAxMTQyMzU1NTlaMIGOMQswCQYDVQQGEwJVUzELMAkGA1UE 7 | CBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDTALBgNVBAoTBERlaXMxFDAS 8 | BgNVBAsTC0VuZ2luZWVyaW5nMRAwDgYDVQQDEwdmb28uY29tMSMwIQYJKoZIhvcN 9 | AQkBFhRlbmdpbmVlcmluZ0BkZWlzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP 10 | ADCCAQoCggEBAMnrT/CCYw2NLp20G3iBPKfB9siMqU/tmoYzkQW99RAJxL9REpuc 11 | 6oz9tY6YbSWNdBApqauE6feENp2v9SJ2xch/8QBcNjjX6gVoWs4KAqoztSYTlefm 12 | upAYqT62j5Iiyyp863csujIUJVfkpfhH1A1FRchLJc6dVRDvnPtiaFLoKs+3SsWz 13 | M9iOim6v/FCXLy90EDFk9vZo6Rprpb/pboQ1mkmnvj1NAyilMBZN2pIYHGNCUoO0 14 | FJI8dBgSPvEtFLZ2alQ81lzdWSOmK40apnLKn1JHhBlIcoO8zdyHqEVGDUwAX7WY 15 | ErE10mCwXiwe2pU/OO6OF0VqYmpa6YTd0wcCAwEAAaOB9jCB8zAdBgNVHQ4EFgQU 16 | HaOOS3qDMj6pESgURyqFkH0vbBIwgcMGA1UdIwSBuzCBuIAUHaOOS3qDMj6pESgU 17 | RyqFkH0vbBKhgZSkgZEwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQG 18 | A1UEBxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMERGVpczEUMBIGA1UECxMLRW5n 19 | aW5lZXJpbmcxEDAOBgNVBAMTB2Zvby5jb20xIzAhBgkqhkiG9w0BCQEWFGVuZ2lu 20 | ZWVyaW5nQGRlaXMuY29tggkAwIxP5bpVWtcwDAYDVR0TBAUwAwEB/zANBgkqhkiG 21 | 9w0BAQUFAAOCAQEAsovJHN6SIw7qoVSjVgIqb5xJUHckMZANoiddAirx8hcdQ9IA 22 | rsEEgaL3DwvgWdF4RtCyNVMIRNqUpsmsqEWToaBrlZz1hWpOOkg+6igBkjm2W1E0 23 | OVpxWJL1g5QGh4hm+6m3KLg14FtRXl098ubpagKJzRMXcrX0yBWFps+2tkchbNJF 24 | OSjgvoNCGnFJRJpGyYp2Y2DaQtkevownzGicmx4poPciCPToi/Temy4BXYpFCJPb 25 | 4vk4Cu/8EuLzxg5XeuMLEg0+aytAm6oLnynVfwus8TwpOElLDGtAG+MogMgA6vJb 26 | KjXQzpBEAiZlaiIuEAN4mq6sfo0rKLsAg1S/AQ== 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/quota.py: -------------------------------------------------------------------------------- 1 | from scheduler.exceptions import KubeHTTPException 2 | from scheduler.resources import Resource 3 | from scheduler.utils import dict_merge 4 | 5 | 6 | class Quota(Resource): 7 | short_name = 'quota' 8 | 9 | def get(self, namespace_name, name): 10 | """ 11 | Fetch a single quota 12 | """ 13 | url = '/namespaces/{}/resourcequotas/{}'.format(namespace_name, name) 14 | message = 'get quota {} for namespace {}'.format(name, namespace_name) 15 | url = self.api(url) 16 | response = self.http_get(url) 17 | if self.unhealthy(response.status_code): 18 | raise KubeHTTPException(response, message) 19 | 20 | return response 21 | 22 | def create(self, namespace_name, name, **kwargs): 23 | """ 24 | Create resource quota for namespace 25 | """ 26 | url = self.api("/namespaces/{}/resourcequotas".format(namespace_name)) 27 | manifest = { 28 | "kind": "ResourceQuota", 29 | "apiVersion": "v1", 30 | "metadata": { 31 | "namespace": namespace_name, 32 | "name": name, 33 | 'labels': { 34 | 'app': namespace_name, 35 | 'heritage': 'deis' 36 | }, 37 | }, 38 | 'spec': {} 39 | } 40 | 41 | data = dict_merge(manifest, kwargs.get('data', {})) 42 | response = self.http_post(url, json=data) 43 | if not response.status_code == 201: 44 | raise KubeHTTPException(response, 45 | "create quota {} for namespace {}".format( 46 | name, namespace_name)) 47 | 48 | return response 49 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/bar.com.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEApFWjz0G3dvFBX8Grzo+vsnjCQZZWLSmZZU4x4OzVRqKvKBVt 3 | fc1kDSCqG3IfSjAnrsdQo3wqYQze4Bnr1h5ipg1NE47ow/msSwA4zpc8Kr5kC5ye 4 | lGKISjOqcddXJs7xlkwwMfmPonV+kAEFqnmoQpRJZ2ORkz1Rvxw+mJhPZkAjG0tG 5 | HZEQo9UYjnOmJt6rr1nkjIeI7yRnLWHSGQDe9up0b0vRbbzESx9uiAwqL0HSja7H 6 | Yu/uGHywTHCTiAOq/uBM0ESwFpE4Con4Kg0mX1IVydzapyZuegft+QJ/P6WVtVDn 7 | JK64FBukxlbp3li4bKO+opWSfKiqKGtymv3o/wIDAQABAoIBABnj2CPt8Y6OocMJ 8 | Sx0G7CJM/iXBHqCM3jrkn90U0uEG/lttTMu2ER40WDhsuVtBzO6vPhgTlsWldnOO 9 | AebA8L/CdrMvH6LIcgl65ng9wV/mkPJ3YVB1WY1/KEo5J+TYU51fMXSeIa/xnNfp 10 | IVBjTEv4+ruMJ0IwNfHK7F20GUY9ccev5w4X6tGgTKVCkcTFL1yNDXtvbHfGMbN9 11 | maYq+gqzY2sgYabupmpO96krSSrudipFdXIM1yZ85wtTaZVGwzzpRKKhK/LMUb4L 12 | Ko/cZ3Q4sx1tLTiJgMw4GP8dDpKtlHQat8us7s5dsPAuj7uBpGMR045gcMTM6KFc 13 | 5/11UDkCgYEA2qk8RwaC4w94zBMvDbjqGbSACKF6FLp5gwJt8yqYtVuR/my02f35 14 | SpzQdEf2QDN+nOcKvrrCNhT6z5LoV3fCSQAhPMW6tEhsIq/mI1tNVuKIfywKWl+I 15 | 1cVCyYAjt/KKGrfVyQ7YB+vc9SOr+SzqXep8NFShSwt2WBZcsd/fQKsCgYEAwGWF 16 | RWgPBfHn9SrUjL43HVRsEc8DPbpdvql4SwopLhpBn2D8dGQ6DquSWBfL3sVl6Bnx 17 | bGqIfEBmipvd80zeXemCjSFhFWscN+7WTBd/Ck79IIsextniJOY8sdfnr/n3+M2/ 18 | c+m8iwhr7Yy2ZbH3nef6mQCfCHV+c+7DHzARAP0CgYEAnvZBV/En3iI1U0bvAi7Y 19 | IW/TVHLv6XnXNKLjg9AHzHCRpkEpCQFV5iQydxaJswq8lRxx906WOfLuk1DdkBkE 20 | KUXq499rZ/zugBkYWcPaabuuN6WwsRqaw63wa8S4MtYkCGB1DwG3k6qoq54PO8qn 21 | Zzc8rF6KE6B1nHxFTxrNlpkCgYBZmm5ZBr+Ia0M2QT5AVg5hEIJMQPcndnZWZ6Lo 22 | f9Dx8bSCP68TneIUFv/PGzYNiC2PzRVNAsiR5YRcJX9W4oPlhO0SQWtviDTaL9eK 23 | FJ9L88GbuG8a+TqDKN83jHAQ2wAL1fbGSyNONRvexFvmPN4vomxpeYqXa/D6mUYy 24 | bjZdGQKBgDGMSJmyHSNiygPtM809kEOys7QZnXtO4hUTvSyOWxxfwM1FLfdRrXIq 25 | 1xvek/xuSmrkL3Sl2mg/y2h4SscTB6ltbdVpptCShaCVIb6Yo59Uy+EVBRlpXqql 26 | v18kyMq2SH8gpgGQKKtqh2C/E0vhITyE+czmzz/5/+ONZ+mpYAir 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/www.foo.com.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEpDCCA4ygAwIBAgIJAP/qzWOu0pEmMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYD 3 | VQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDTAL 4 | BgNVBAoTBERlaXMxFDASBgNVBAsTC0VuZ2luZWVyaW5nMRQwEgYDVQQDEwt3d3cu 5 | Zm9vLmNvbTEjMCEGCSqGSIb3DQEJARYUZW5naW5lZXJpbmdAZGVpcy5jb20wHhcN 6 | MTYwMTE1MjM1OTAyWhcNMTcwMTE0MjM1OTAyWjCBkjELMAkGA1UEBhMCVVMxCzAJ 7 | BgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwREZWlz 8 | MRQwEgYDVQQLEwtFbmdpbmVlcmluZzEUMBIGA1UEAxMLd3d3LmZvby5jb20xIzAh 9 | BgkqhkiG9w0BCQEWFGVuZ2luZWVyaW5nQGRlaXMuY29tMIIBIjANBgkqhkiG9w0B 10 | AQEFAAOCAQ8AMIIBCgKCAQEAyi0V5kN1jejOtWSZMFAnexImOUZ9lWQVDFbZ05x3 11 | V2oakJB5WARl+p7yHRByyhMK2s8hb7jERGgsVL3qrrKRx+dswA3qIwTHj2WNFBfW 12 | xsmbCMpAuhu8Qv68GI5l7dA1h332m4NtWQSh9eM7T6LjBeJkc22CYkD+MnOqwt3b 13 | y7+t99OjPzlzOYi48ejA3n5mnCUYDclx1WEAhrGwo8hsNgRYUgxMdvbkFWKdet59 14 | ZoLA4OU/iBIYvaFKrFEJu9fGQOzIb495ocdUXLnHeEz9kYMvCq6mvhDJt+ntvXT+ 15 | xFDnDmPaJTd53zb9OUjXhsys8xci1NNmIEtnUoVI8CijfQIDAQABo4H6MIH3MB0G 16 | A1UdDgQWBBTIHXCCSmUjbHRm85Y++VYKb5GGlTCBxwYDVR0jBIG/MIG8gBTIHXCC 17 | SmUjbHRm85Y++VYKb5GGlaGBmKSBlTCBkjELMAkGA1UEBhMCVVMxCzAJBgNVBAgT 18 | AkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwREZWlzMRQwEgYD 19 | VQQLEwtFbmdpbmVlcmluZzEUMBIGA1UEAxMLd3d3LmZvby5jb20xIzAhBgkqhkiG 20 | 9w0BCQEWFGVuZ2luZWVyaW5nQGRlaXMuY29tggkA/+rNY67SkSYwDAYDVR0TBAUw 21 | AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAnwL+sDHeGiX3vjoSrIB5nKpyIOI5bZvc 22 | BWV0BNSioe3ze2MIQAoYXKqHccsMbnSRMtO6xF+4NY/HCdYEtMh0fjyq9bmjcd4J 23 | 5UJGyNzDeWWMVo97Ue5loG2rEQXMwwLTAipPYLA9MLx2zZJ7j8NU+fH4WZPlg8AK 24 | +6yqXAwj/2wqzgzFz2lOxqtWpTf7/VgsvdgHBG7DPJe49YFZVUIByTCjkKhAyKO+ 25 | 5OAkouFhReRvPU4Q6VBsdKbpViSD7nAwaeb0Rzcb/3i7oH79l5mu3RNLkIXPAJui 26 | VayurtFbPvKsp/TLjZ/hJY/ocnwthBLVGwzm5X1lPnrFS5ycEOSQWw== 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/foo.com.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAyetP8IJjDY0unbQbeIE8p8H2yIypT+2ahjORBb31EAnEv1ES 3 | m5zqjP21jphtJY10ECmpq4Tp94Q2na/1InbFyH/xAFw2ONfqBWhazgoCqjO1JhOV 4 | 5+a6kBipPraPkiLLKnzrdyy6MhQlV+Sl+EfUDUVFyEslzp1VEO+c+2JoUugqz7dK 5 | xbMz2I6Kbq/8UJcvL3QQMWT29mjpGmulv+luhDWaSae+PU0DKKUwFk3akhgcY0JS 6 | g7QUkjx0GBI+8S0UtnZqVDzWXN1ZI6YrjRqmcsqfUkeEGUhyg7zN3IeoRUYNTABf 7 | tZgSsTXSYLBeLB7alT847o4XRWpialrphN3TBwIDAQABAoIBAF8UWxQZkaLz9Bt2 8 | j+jykik8gIR3F9L3Q2gmKAfYJulicC7WcjisbxXs8e0vgVXJgfmKZMbLU5ClxUID 9 | dR7BZui+tjFBOpcRtLTPKtMSi6axqn8/gbstPnRT6H4LYRejIp/jKs13VkX2jo8Q 10 | r8Z1rDiDghSKrkbYdxH+gqEs+YrvyKKtQOL/Nu3FNyCRZ09482MZ9D59mNui9mMr 11 | mi6w2q+BcXcOAmbp3ZKt4doMJVaAeIl5SGptt8zwvpTe37gmLhEP4rwWBqdPhg1N 12 | O5pBvGYTuUKCvzUx9Btjh1kjwK9TmcxTZdp+gG/il3tZVR7EArlyiyz1jAcO18o5 13 | Ww2CcaECgYEA/kq3dOZvMuw679PNJrZk/M6vGbPVPBuivHDD0YkOjnpw6OtU0O10 14 | aeuhKeDPXw+41U2SHET0UuvVfhjHblXWalYADfP+IqTISbATpQoC8diNiALvxPQM 15 | byjnuvjOggRDsQZ8JObCtWI5aaQgz1K10YSNH6rjYpSQLSbhLob+3nMCgYEAy0aI 16 | 8pKUuD8D5J6ZGtAdZqwuY0p6mSodMcFMRK6//Hjk55V6Gl2UzbHavcXi1IxP0qHv 17 | HAicf6D0r4nIRYP2a0VNnWnDCvGaG/zJTTX3XTPN770CdFH8ylY5GFvqk11HK7oK 18 | zFy3m+dESW4DD6DTTivk/rCIBbduiqpdHeq+4B0CgYEAwccEGBQFhuOXYeyft7Fk 19 | MXX63vY4Nw7EKx8vSXxM2GwboJK8Vl2syY5iiLwkqkcbzYfIILy3Bn1qeiW9y6mj 20 | s/KHJhrZfWLesbB4t9pyNgOUjqHWPtrOouKj+8nf7Bn9z2emsKQcmgYYxBTrX7Gi 21 | ld+RfyFFF3koiQ/IpyD+FVsCgYBNHT8KtuzQUKeLbVcrwtPEhYE7jZ+gx0c3/tqO 22 | G7UddEdyS1R8+A9hUR1obM+2TlxhzajF+8ZS7J6mkSB2rq8m1q2xD9Q8LJeIEofT 23 | UKu8odB4KD3sHsZFhBw4z3XX3cUII5XBHVNSQ5O2P5PNs/c1apV+wT143bODy0lz 24 | 9f/fSQKBgQCHaPr8K2nxy+JSauJLq1eez0+zS0Pqta8Z0qYI7yluHBJXRfwHFwxI 25 | WrKiOT4VHYjROdfp4Hgj8sZb5vYAuFE/5C/iU0/kPPP+rURYruJFEIgOUOGF4iO6 26 | uyc144xaMPEunf4/yTeqkmBYx9Z6Qyno7o3S7/P0ZKKMjf9Q8IQMdA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/self-signed.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAoyOz4g/RXUXCCfgU0u+wafV3hOWyFcEQV4tTEVU8EohFLLdO 3 | JU2kgWaQl9X4Jqskfbp7UjrmhfSvnzDU6+EKZ6nMay+KPrf67zVc/HTF7oJ99SLL 4 | fpK/ozp7NUXeasJ7gW90zZ4IIxgm8pihAqlQXIg87J9fqVlKOEONxV3DqYuBuhaH 5 | mHP2TdW7VTubBB2XYUlO7Bed7FDlxFT0fNwScDdEkCGXNzD82zXUdUq4Nkd1u+oj 6 | LmEoX31vF/lec2BBIjoooAuwUUqoImwnQxFddA+4YrzuZ6qMadFtdViHz+ymU3oO 7 | QKJxNDqk+uXTNGheKRHHETpW+I4ekcPooOtH/wIDAQABAoIBACi2STbaIbJ4LSNV 8 | wMSfQlQ/CNOmitm4834Va+aAcdxiG3k8SYkvpiUQ1na91A66WQHzXsE3p724QXel 9 | tQ0kfPc/vZ7mH0blnP7DP3BVJ+wMrqhVRZlRv/dZKdQymn3kCPRVPz3s+TTg2x9h 10 | jZTfcgmVija20yWs/cOqwB+H9cNCgkwC86DQOLVOL67+nKXt4lDra23gEf9lqNJB 11 | XtHQZhSFJQEGGetu6wTFTVw7nzKbtAnepwLGyG/mDm0z3rygzFmtw80jkEuhAALZ 12 | wwBVKVsKKMHow4VQi6mKEtZjEG42UxXIeWXOSiKNlq7pd9QZrxdH8CVSvNeV+aAT 13 | IMxXn4ECgYEA1CjLh2yvYPyB7FDoEObEcR+2uedIbOTKufTLLN8u6eDcQ+FtJwPb 14 | AP1s1TmoOcRDlvzTbFZj+PL3dZyakCqUY8QFIFL1viGVVhfYDGIfEFMQ5gFDibJI 15 | acGt3hvQvkCHxZgREWtLDkP8Oa8mHR/BVjKAHPB6nBwy9An/uIzSysECgYEAxNnC 16 | 1+p3mFiXhb5mrDaieoCIKueL7w/DRqAHfEbM61/0oDJPpsHrPbDbWwcWwWdveOqn 17 | sQOhySd0tFsC9OhOAGwu55qnonbQW7uRNijuRIxNYsqi9dLTNfyO3neJxzE3LtZe 18 | 95HWDJG8s5J/Kd7Ymai6RtK01UMGa0josSCwgr8CgYBP92hvjPm1trdJ2Vz/MdwN 19 | P4TiIVjdIod++9OxABZwtP6Q32EC+aMMhnkFDYxo6Z8IRBd0mENqTDoVrIddm47+ 20 | 452DB4H0vjfJkYcvc7R9tLGD4CoSto4wvn3IX/eYHj6OrbiRNj2+DMX/ABN/mr6G 21 | vNYpEkNEoCRcc4BdkUbKgQKBgCbIgIrptwZc7f17td7YJMrd5/YMCJXhFSgk/1SM 22 | 3nLBRQEK6IaCTkapQY59pw4TwvKfyMonXQi0rVmbVMnLuxJ6PgODhOONZR+tpL52 23 | 8fqvac+8/L5R+yr3x24tPwfvul+P/MXqBbIURIlco5EsRqB/jbPGb7pUqj8Y7j93 24 | oU8hAoGBAM2MnUkMY2in824b2qLZladhy+ng15tUpuyUjYNPkhW9OHcu69SzLjc4 25 | efHltxLv5rWh2U7++8T2wUx1wBywcMqfqBgCyXQSBvWgFsJlc2OXyrsdk+xzY+Le 26 | FhwsVy9OL/X2SHCxMersB/9wjaumjtIkehpONhDh/6h71VTkSfQa 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/www.foo.com.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAyi0V5kN1jejOtWSZMFAnexImOUZ9lWQVDFbZ05x3V2oakJB5 3 | WARl+p7yHRByyhMK2s8hb7jERGgsVL3qrrKRx+dswA3qIwTHj2WNFBfWxsmbCMpA 4 | uhu8Qv68GI5l7dA1h332m4NtWQSh9eM7T6LjBeJkc22CYkD+MnOqwt3by7+t99Oj 5 | PzlzOYi48ejA3n5mnCUYDclx1WEAhrGwo8hsNgRYUgxMdvbkFWKdet59ZoLA4OU/ 6 | iBIYvaFKrFEJu9fGQOzIb495ocdUXLnHeEz9kYMvCq6mvhDJt+ntvXT+xFDnDmPa 7 | JTd53zb9OUjXhsys8xci1NNmIEtnUoVI8CijfQIDAQABAoIBAF5CdPJdQ0J9Z1pk 8 | 45MF29JiXNXZSpXLCpEtMPObAH0N6AK8iQaDTnRxhJoOYCZciHQJnCI1d7QZCYoc 9 | 3XzDnnogKLDGDAJ1qQDvLL5Qev9FYXXQrirW4Ygusc2VHmqo5zwbe014EhQtt8En 10 | RzDS1ZuZuJGkXeSnPpyRFu0xeNdd2g588a1/XHosqnA4zFkaK4yVACq/+x0jcGDK 11 | OAAky95i205fGhpReTcRWPTpVWarpA5PdfA/JOSRaZpQAvFMcXLPilEdrcgiB6bF 12 | 29Tn4Mwg6usq5osy2ePW0M0eE8ZvG4+7zhuEQ5NdceFq2Fj4A1Svl44Hsrnb/Vts 13 | PxHO/jUCgYEA/6JrabRN6uNpNHpNoBg5gYRGa5y4yzhuhEgUxH1I7diHr+h9Y3Sw 14 | +/enDq6CHRpcXyOXI349wOF2ZXfMC1yFzQ2IgoMIG7SaIItAfkvqdXmlrajFSPE4 15 | mbIf/ugXqgvm+oWz6l9FIqCJNNKlwxquUMByOFOjJ71hn61lVeDIMEsCgYEAyncY 16 | rfKRbqK1USJUTRrnGA+Lp6BPKahYcgEcatYNC6Odq8uW91c3Vzd5fG5JVoUY3Ihi 17 | G0UhcIDMnM1I74PUavfQPmDAyNPEQ/120UouQNgEMBHs1mOCPezD4wVDuCImOiCE 18 | L7BYHJU1yUgkZr20K65xcAIYavIxh7Tc1bogblcCgYEApY2/eI6Po54xhQ3r9dGa 19 | dHmAzbKKrvnWAQ9Ze8MTlw2TGmY7xkxNTnEdnNGBbG2lAuxetlrMjXy2m5IQ8A60 20 | jI7GKJfJiX/WDVuBoglyRzBIDwZs9gdau5bzR7dxk+vvY7FxSkj20i0bjr0ZIxjF 21 | aYCouDfaQyNP9QRry0ku/K8CgYEAwRRwubpBDRQn+/bUFDAawGxaz4Hm3KBJsHb0 22 | xcHZ8QaYn7PpBXnsMcWampqGX/dP7Ug23zC/Ig4Ck2qGKrw6v8QSmNomH58sZXZ7 23 | cD3g/D/FRp5hkVaWZz261W441YnjkL1fsibm8GMvRwQAiuZQwvN6BMpKxPqxV2yY 24 | yU3WDcMCgYEAhEyNSTj+K5ZjZPGQdmmTUtrB9J0vT5h5RxoqaNY3w8lxZhmebN8w 25 | +6ndqzxjkdlRuSwiceqZnZZJfI7bTH9j9to+PR5ahJlr14zpUXQoCkkBqgJLfk1e 26 | VLKCH2pTUjDm/Q2CQ4AygR2hM1KmzQKDRMazynUGmMX+9yzCUNawquE= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/autotest.example.com.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAwyLIwjpUQkAmh/z6JvQMAtvNu/dBuCt+R8cnQMEw4VglglMw 3 | YKAm2ZXA03LYWk5EO52YaDZKPAqjng+m4k+B0ble5XG4vFRTlBhln0cR3UAYlm7Z 4 | tZp/6JR1STwph+9520DUsndPTOO3ApcMMuap5yLRYApHfOwbyoiaCCUuaE/XyZn6 5 | FN/9Zj1V1IMcdu8//HtM2vbDkZ5yJbUzDSqUInHXZUp7OF+kUKwem0CN+SGk20ue 6 | AQ3Loxg9RWcXwA8keZ1StQsNzmRzQTP/XGEvaTrJKgBkk9GHnxmC00L9q+zb0BaH 7 | aZ5KXCKbwf0mCqOZngHuKTvKpD62TvPz46xE4wIDAQABAoIBABr5HO0UKP97ZJgZ 8 | lO57f4mJnpej5vaxNGRxl/Bwg/QyPgUUwLQqjxQ2ig/waQ2akf33m9CT6JECG3nG 9 | yhewS86UpBRtMs79jQwEj0+EAGkn6f4pVniu4Y1hsBCue0MqDBsNjBkbOt/y/iIi 10 | hPIoRkYH3w86fIU9Ed5eIYSMtyx91wpGBwwpCh4ztfQ5jbBMZ0F5J+EnvzC41x2K 11 | 1o0bN6pr51epQBuyHz3SNAX0ce67f0jLhPSDl76nzsQsHem7rTPY4ZFTsRZE7lW0 12 | lSA0S0z/sGpdoo1g0qvzg6T73/x8g0pdtf0N2ckbbafMvX1lba86Su9/KDRpS0RK 13 | dymBkFkCgYEA6VQfKG2lZ1vEPq5JUQ8be1KbqzSEfvyqXd3Cb7iFcVVP3kNCRk6m 14 | O04NJYUxDuF1LpWemGt5UCUUdLxcGFTYDW6gAKyfTuve87PPVvuHNsnJcJWW77aV 15 | +yDhXgYUy9fCLMxtZwTwCCrqXEUtSgK5hvlwa8bYL/dE7YGhOa2ap/8CgYEA1hil 16 | ezP8REe+Z+M8tSt2hoZsxrBuso2pZRAxuMqiO0/trA3d0w2M51vSm1/NxM2JpW2y 17 | SPtE9CbngyGeHNdC/SvEkHOZxKacimoD2LUjAcVA+5r+shK+ssMqnniy9Qh13AGg 18 | Pj3ba9j10T3zzAhItefpIu5E+swhqs1xmhTQwx0CgYBYVzY4y1K9kFv702cE3rBr 19 | /7nal1a28ZjbUzPjsrwrTb6gi1yTXAHKIGIP257YYHpKefGDCeXzdyaIkCxaNf1b 20 | EJBZ0QG8EsfmAyU0bKUkFEBFdQ2hksK0Qx2wyKKlDvqAlaGySIdMwFrdNn/QLrnp 21 | pZVv6Og/OOKK/fJ58QXGJwKBgDOsmzRTZc3tKw3UEPEBXog1pceHChDalEoqUHXz 22 | opiCQDFI34NzP9EPnpOV2gpoOZLOGTv4ObpcMYC6+ninlCmbCMR8wl5ugFYAJJGH 23 | lr10qKyRymucjp6C8KRzKW5u7lN9qPmc4Hr1UM+CDnfuf+433VNrAwctgerBz2uL 24 | HqAZAoGAYbrDiueIFxHDrkCkefSyAn4Wlo6KhPSUiSqvM9k5gBWZedcvJrjbvCmW 25 | K1NefGc57cAb906Lwa3MpUmKEA5IYTGsO87iAFnDMcuu+w6RwiwV/DNY8xB6dtuz 26 | r8G+so0UVAch6q1OBBSBaKC1Vn3fzT72zvS7/e5BZ0p5KrqCIZg= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /rootfs/api/tests/certs/wildcard.foo.com.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAtkhK9dvmo82/TU6wPLOqjv/EeYWjWCOpqufp4sanUzbQOnI8 3 | pHDSLrGKOivjLl2bHG+mL5mTmulgNnbm7Z3wSVsVFPuFQfqk6ATSzaAB1+8uir8K 4 | i+pA4fhPsg0xazVkRSKXF4xld8QroRYy0K+nMl9y35Ic49xy9MolFclrwRY1JLZo 5 | aRRssmR+QaHqOdGaCs4JcdU6pFjyNdkqvcq2+IYiSHfebGIKaWESdNHUIXGi/gYf 6 | uo6ECcIafLZAbIMNeMqT0De4+pqf2pbn9rPp4OhF7s2C9+ejlAL0TBoFZQEsnNRe 7 | xTc/YpHhdSW3SJgQmPn4aKSka6+aLxy90ZuiswIDAQABAoIBAQCr/RIdYFsB+0Oh 8 | IbnPzIYFXvZ24sz4gM254BAiVOXT8kgOnXLyhTELtaCCup4kRVXxQrc++lz3MXQC 9 | b7X8RaVO0Y3WumtFkcS+1q5ALdUPdTIo35CH64NEsxIfIaubSElPog+FvIaQtpuj 10 | 5loT5WiQctbkc+ymYn5k0cakA+STzVQY0LK4Aoql7ZgVsexEVLe1eIU1ufOr3Ss5 11 | hg1/JjRJpzyCNv7yI8xr6CAfhUWtuGeGSQPYhK+f9SrypT5Fhot8kke9fCQvC3IG 12 | VbWurrO11RsuotRG5BRfln6fVK/v4ZmmAP41/vqyOwxJkcp/7q67vQkCIpYpwDAR 13 | MbHGwSKhAoGBAORa0qoC5oSm93l0wz2KAgLgK4MDsOl+q4B8290YUzYzOyBUTIH5 14 | 0wgF5waS0qOKPuGzut2z6PE44Aw+OMbxBye3Zjp1rN5c5CknDnK81QMUq2Iz+3xk 15 | PFIyxU2NEubrRzRoResFPQfT9WfdmJX1HPtMja8edvWbx8iUKy9oMGXHAoGBAMxZ 16 | l+rxQs+Edh5ONg2eiOmcANKldJw9LC5g99ezl/q39tdSkCPbhUtrSm+9m9/7JTht 17 | g8Sk9SB3ifYZue5Z1LAJ5o75yH/rsev86URn32NxNFtklaLYhiCYfUxv9P0rzjuK 18 | JNDBMqq+Mn9ebW8mkLuInornVXmjBLVU679+Wuu1AoGACGd/UVqB+Wfbu9CcTuuB 19 | X3G4qD2+iRlsXnI59U0r4tbH2ky/9Bipt6Xf9tH4hqRT5CKlQfuZGyeot0qi9E9y 20 | n/eT/5rNHfH1Q754PajNfiuIkziujMlznuLXeB81DuKh4D/mMtwifuNCKOxy7TH0 21 | Oxt6K3PHlQqCs9MgM8J15YkCgYBoov/NR9ikFfm9ruKyupj0tfMd6ab6UcCLxw9h 22 | ng6WTRjSTO6NzdxFMB0fdoGYgSsf58PvL2BtTYiRQb8ZM1pbAdbTI0ftaKzkX866 23 | Pk3+x3q9yZVtm12i1zJhr3pNIN8rUaNkWWkuUNHesmVq4t59iIlWKvpznGvOxjsp 24 | BPRdeQKBgHat/1jqhXgWZHskAYs9PTQpx/tcHOWWH1P3yxvxxa3Uo85OEnVJJLT6 25 | 2WYKbPWGzwYvJSm36n9/hK3vMuGEQb72/AXc6OW1/yGAeHbV1MOYs1zc6BM1RkyP 26 | EG/LxuEm6tZnswLehRcfR5/dZnd4ZwJzEfHGReIpbVXhxSHqAcyC 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /rootfs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/deis/base:v0.3.6 2 | 3 | RUN adduser --system \ 4 | --shell /bin/bash \ 5 | --disabled-password \ 6 | --home /app \ 7 | --group \ 8 | deis 9 | 10 | COPY requirements.txt /app/requirements.txt 11 | 12 | RUN buildDeps='gcc libffi-dev libpq-dev libldap2-dev libsasl2-dev python3-dev python3-pip python3-wheel python3-setuptools'; \ 13 | apt-get update && \ 14 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 15 | $buildDeps \ 16 | sudo \ 17 | libpq5 \ 18 | libldap-2.4 \ 19 | python3-minimal \ 20 | # cryptography package needs pkg_resources 21 | python3-pkg-resources && \ 22 | ln -s /usr/bin/python3 /usr/bin/python && \ 23 | mkdir -p /configs && chown -R deis:deis /configs && \ 24 | pip3 install --disable-pip-version-check --no-cache-dir -r /app/requirements.txt && \ 25 | # cleanup 26 | apt-get purge -y --auto-remove $buildDeps && \ 27 | apt-get autoremove -y && \ 28 | apt-get clean -y && \ 29 | # package up license files if any by appending to existing tar 30 | COPYRIGHT_TAR='/usr/share/copyrights.tar'; \ 31 | gunzip -f $COPYRIGHT_TAR.gz; tar -rf $COPYRIGHT_TAR /usr/share/doc/*/copyright; gzip $COPYRIGHT_TAR && \ 32 | rm -rf \ 33 | /usr/share/doc \ 34 | /usr/share/man \ 35 | /usr/share/info \ 36 | /usr/share/locale \ 37 | /var/lib/apt/lists/* \ 38 | /var/log/* \ 39 | /var/cache/debconf/* \ 40 | /etc/systemd \ 41 | /lib/lsb \ 42 | /lib/udev \ 43 | /usr/lib/x86_64-linux-gnu/gconv/IBM* \ 44 | /usr/lib/x86_64-linux-gnu/gconv/EBC* && \ 45 | bash -c "mkdir -p /usr/share/man/man{1..8}" 46 | 47 | COPY . /app 48 | 49 | # define execution environment 50 | WORKDIR /app 51 | CMD ["/app/bin/boot"] 52 | EXPOSE 8000 53 | -------------------------------------------------------------------------------- /rootfs/api/fixtures/test_auth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 7, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "autotest", 7 | "first_name": "Otto", 8 | "last_name": "Test", 9 | "is_active": true, 10 | "is_superuser": true, 11 | "is_staff": true, 12 | "last_login": "2013-05-10T16:08:09.357Z", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=", 16 | "email": "autotest@deis.io", 17 | "date_joined": "2013-05-10T16:08:09.357Z" 18 | } 19 | }, 20 | { 21 | "pk": 8, 22 | "model": "auth.user", 23 | "fields": { 24 | "username": "autotest2", 25 | "first_name": "Otto", 26 | "last_name": "Test", 27 | "is_active": true, 28 | "is_superuser": false, 29 | "is_staff": false, 30 | "last_login": "2013-05-10T16:08:09.357Z", 31 | "groups": [], 32 | "user_permissions": [], 33 | "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=", 34 | "email": "autotest@deis.io", 35 | "date_joined": "2013-05-10T16:08:09.357Z" 36 | } 37 | }, 38 | { 39 | "pk": 9, 40 | "model": "auth.user", 41 | "fields": { 42 | "username": "autotest3", 43 | "first_name": "Otto", 44 | "last_name": "Test", 45 | "is_active": true, 46 | "is_superuser": false, 47 | "is_staff": false, 48 | "last_login": "2013-05-10T16:08:09.357Z", 49 | "groups": [], 50 | "user_permissions": [], 51 | "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=", 52 | "email": "autotest@deis.io", 53 | "date_joined": "2013-05-10T16:08:09.357Z" 54 | } 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /rootfs/api/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | import logging 3 | from rest_framework.compat import set_rollback 4 | from rest_framework.exceptions import APIException, status 5 | from rest_framework.response import Response 6 | from rest_framework.views import exception_handler 7 | 8 | 9 | class HealthcheckException(APIException): 10 | """Exception class used for when the application's health check fails""" 11 | pass 12 | 13 | 14 | class DeisException(APIException): 15 | status_code = 400 16 | 17 | 18 | class AlreadyExists(APIException): 19 | status_code = 409 20 | 21 | 22 | class Conflict(AlreadyExists): 23 | pass 24 | 25 | 26 | class UnprocessableEntity(APIException): 27 | status_code = 422 28 | 29 | 30 | class ServiceUnavailable(APIException): 31 | status_code = 503 32 | default_detail = 'Service temporarily unavailable, try again later.' 33 | 34 | 35 | def custom_exception_handler(exc, context): 36 | # give more context on the error since DRF masks it as Not Found 37 | if isinstance(exc, Http404): 38 | set_rollback() 39 | return Response(str(exc), status=status.HTTP_404_NOT_FOUND) 40 | 41 | # Call REST framework's default exception handler after specific 404 handling, 42 | # to get the standard error response. 43 | response = exception_handler(exc, context) 44 | 45 | # No response means DRF couldn't handle it 46 | # Output a generic 500 in a JSON format 47 | if response is None: 48 | logging.exception('Uncaught Exception', exc_info=exc) 49 | set_rollback() 50 | return Response({'detail': 'Server Error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 51 | 52 | # log a few different types of exception instead of using APIException 53 | if isinstance(exc, (DeisException, ServiceUnavailable, HealthcheckException)): 54 | logging.exception(exc.__cause__, exc_info=exc) 55 | 56 | return response 57 | -------------------------------------------------------------------------------- /rootfs/bin/boot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script is designed to be run inside the container 4 | # 5 | 6 | # fail hard and fast even on pipelines 7 | set -eo pipefail 8 | 9 | # set debug based on envvar 10 | [[ $DEIS_DEBUG == "true" ]] && set -x 11 | 12 | echo system information: 13 | echo "Django Version: $(./manage.py --version)" 14 | python --version 15 | 16 | mkdir -p /app/data/logs 17 | chmod -R 777 /app/data/logs 18 | 19 | # modify deis user groups to grant access to Docker socket 20 | DOCKER_SOCKET_GID=$(stat -c "%g" /var/run/docker.sock) 21 | DOCKER_SOCKET_GROUP=$(getent group "$DOCKER_SOCKET_GID" | cut -d : -f 1 || :) 22 | if [[ -z "$DOCKER_SOCKET_GROUP" ]]; then 23 | DOCKER_SOCKET_GROUP=docker 24 | groupadd -g "$DOCKER_SOCKET_GID" "$DOCKER_SOCKET_GROUP" 25 | fi 26 | if [[ "$DOCKER_SOCKET_GROUP" != "deis" ]]; then 27 | usermod -a -G "$DOCKER_SOCKET_GROUP" deis 28 | fi 29 | 30 | echo "" 31 | echo "Django checks:" 32 | python /app/manage.py check --deploy api 33 | 34 | echo "" 35 | echo "Health Checks:" 36 | python /app/manage.py healthchecks 37 | 38 | echo "" 39 | echo "Database Migrations:" 40 | sudo -E -u deis python /app/manage.py migrate --noinput 41 | 42 | # spawn a gunicorn server in the background 43 | echo "" 44 | echo "Starting up Gunicorn" 45 | sudo -E -u deis gunicorn -c /app/deis/gunicorn/config.py api.wsgi & 46 | 47 | echo "" 48 | echo "Loading database information to Kubernetes in the background" 49 | echo "Log of the run can be found in /app/data/logs/load_db_state_to_k8s.log" 50 | # python -u avoids output buffering 51 | nohup python -u /app/manage.py load_db_state_to_k8s > /app/data/logs/load_db_state_to_k8s.log & 52 | 53 | # smart shutdown on SIGTERM (SIGINT is handled by gunicorn) 54 | function on_exit() { 55 | GUNICORN_PID=$(cat /tmp/gunicorn.pid) 56 | kill -TERM "$GUNICORN_PID" 2>/dev/null 57 | wait "$GUNICORN_PID" 2>/dev/null 58 | exit 0 59 | } 60 | trap on_exit TERM 61 | 62 | echo "" 63 | echo deis-controller running... 64 | 65 | wait 66 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0002_auto_20151215_0352.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import migrations, models 4 | import api.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('api', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='app', 16 | name='id', 17 | field=models.SlugField(max_length=24, unique=True, null=True, validators=[api.models.validate_app_id, api.models.validate_reserved_names]), 18 | ), 19 | migrations.AlterField( 20 | model_name='app', 21 | name='uuid', 22 | field=models.UUIDField(serialize=False, verbose_name='UUID', primary_key=True), 23 | ), 24 | migrations.AlterField( 25 | model_name='build', 26 | name='uuid', 27 | field=models.UUIDField(serialize=False, verbose_name='UUID', primary_key=True), 28 | ), 29 | migrations.AlterField( 30 | model_name='config', 31 | name='uuid', 32 | field=models.UUIDField(serialize=False, verbose_name='UUID', primary_key=True), 33 | ), 34 | migrations.AlterField( 35 | model_name='container', 36 | name='uuid', 37 | field=models.UUIDField(serialize=False, verbose_name='UUID', primary_key=True), 38 | ), 39 | migrations.AlterField( 40 | model_name='key', 41 | name='uuid', 42 | field=models.UUIDField(serialize=False, verbose_name='UUID', primary_key=True), 43 | ), 44 | migrations.AlterField( 45 | model_name='push', 46 | name='uuid', 47 | field=models.UUIDField(serialize=False, verbose_name='UUID', primary_key=True), 48 | ), 49 | migrations.AlterField( 50 | model_name='release', 51 | name='uuid', 52 | field=models.UUIDField(serialize=False, verbose_name='UUID', primary_key=True), 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /charts/controller/templates/controller-clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if (.Values.global.use_rbac) -}} 2 | {{- if (.Capabilities.APIVersions.Has (include "rbacAPIVersion" .)) -}} 3 | kind: ClusterRole 4 | apiVersion: {{ template "rbacAPIVersion" . }} 5 | metadata: 6 | name: deis:deis-controller 7 | labels: 8 | app: deis-controller 9 | heritage: deis 10 | rules: 11 | - apiGroups: [""] 12 | resources: ["namespaces"] 13 | verbs: ["get", "list", "create", "delete"] 14 | - apiGroups: [""] 15 | resources: ["services"] 16 | verbs: ["get", "list", "create", "update", "delete"] 17 | - apiGroups: [""] 18 | resources: ["nodes"] 19 | verbs: ["get", "list"] 20 | - apiGroups: [""] 21 | resources: ["events"] 22 | verbs: ["list", "create"] 23 | - apiGroups: [""] 24 | resources: ["secrets"] 25 | verbs: ["list", "get", "create", "update", "delete"] 26 | - apiGroups: [""] 27 | resources: ["replicationcontrollers"] 28 | verbs: ["get", "list", "create", "update", "delete"] 29 | - apiGroups: [""] 30 | resources: ["replicationcontrollers/scale"] 31 | verbs: ["get", "update"] 32 | - apiGroups: [""] 33 | resources: ["pods/log"] 34 | verbs: ["get"] 35 | - apiGroups: [""] 36 | resources: ["pods"] 37 | verbs: ["get", "list", "delete"] 38 | - apiGroups: [""] 39 | resources: ["resourcequotas"] 40 | verbs: ["get", "create"] 41 | - apiGroups: ["extensions"] 42 | resources: ["replicasets"] 43 | verbs: ["get", "list", "delete", "update"] 44 | - apiGroups: ["extensions", "apps"] 45 | resources: ["deployments"] 46 | verbs: ["get", "list", "create", "update", "delete"] 47 | - apiGroups: ["extensions"] 48 | resources: ["deployments/scale", "replicasets/scale"] 49 | verbs: ["get", "update"] 50 | - apiGroups: ["extensions", "autoscaling"] 51 | resources: ["horizontalpodautoscalers"] 52 | verbs: ["get", "list", "create", "update", "delete"] 53 | {{ if .Values.global.experimental_native_ingress }} 54 | - apiGroups: ["extensions"] 55 | resources: ["ingresses"] 56 | verbs: ["get", "list", "watch", "create", "update", "delete"] 57 | {{- end -}} 58 | {{- end -}} 59 | {{- end -}} 60 | -------------------------------------------------------------------------------- /rootfs/api/fixtures/test_sharing.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 2, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "autotest-1", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": true, 11 | "is_staff": true, 12 | "last_login": "2013-11-25T21:58:47.420Z", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "pbkdf2_sha256$10000$SLr4X1T9L3QA$NB4d4a0d+3NZuAwLbdnKGb2z3P/hQrKQHVaGG3zAaMw=", 16 | "email": "autotest@deis.io", 17 | "date_joined": "2013-11-25T21:58:46.208Z" 18 | } 19 | }, 20 | { 21 | "pk": 3, 22 | "model": "auth.user", 23 | "fields": { 24 | "username": "autotest-2", 25 | "first_name": "", 26 | "last_name": "", 27 | "is_active": true, 28 | "is_superuser": false, 29 | "is_staff": false, 30 | "last_login": "2013-11-25T21:59:31.404Z", 31 | "groups": [], 32 | "user_permissions": [], 33 | "password": "pbkdf2_sha256$10000$FrfwTVAtWPMD$HUfDokMeY37YshdyS3uhDZ+d/r8galU7kNuBfZxJl2s=", 34 | "email": "autotest@deis.io", 35 | "date_joined": "2013-11-25T21:59:30.760Z" 36 | } 37 | }, 38 | { 39 | "pk": 4, 40 | "model": "auth.user", 41 | "fields": { 42 | "username": "autotest-3", 43 | "first_name": "", 44 | "last_name": "", 45 | "is_active": true, 46 | "is_superuser": false, 47 | "is_staff": false, 48 | "last_login": "2013-11-25T21:59:31.404Z", 49 | "groups": [], 50 | "user_permissions": [], 51 | "password": "pbkdf2_sha256$10000$FrfwTVAtWPMD$HUfDokMeY37YshdyS3uhDZ+d/r8galU7kNuBfZxJl2s=", 52 | "email": "autotest@deis.io", 53 | "date_joined": "2013-11-25T21:59:30.760Z" 54 | } 55 | }, 56 | { 57 | "pk": "5a09a1e0-a27e-4839-928b-449310ed90f0", 58 | "model": "api.app", 59 | "fields": { 60 | "updated": "2013-11-25T22:09:36.726Z", 61 | "created": "2013-11-25T22:09:36.726Z", 62 | "owner": 2, 63 | "id": "autotest-1-app" 64 | } 65 | }, 66 | { 67 | "pk": "5a09a1e0-a27e-4839-928b-449310ed90f1", 68 | "model": "api.app", 69 | "fields": { 70 | "updated": "2013-11-25T22:09:36.726Z", 71 | "created": "2013-11-25T22:09:36.726Z", 72 | "owner": 3, 73 | "id": "autotest-2-app" 74 | } 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/namespace.py: -------------------------------------------------------------------------------- 1 | from scheduler.exceptions import KubeHTTPException 2 | from scheduler.resources import Resource 3 | 4 | 5 | class Namespace(Resource): 6 | short_name = 'ns' 7 | 8 | def get(self, name=None, **kwargs): 9 | """ 10 | Fetch a single Namespace or a list 11 | """ 12 | url = '/namespaces' 13 | args = [] 14 | if name is not None: 15 | args.append(name) 16 | url += '/{}' 17 | message = 'get Namespace "{}"' 18 | else: 19 | message = 'get Namespaces' 20 | 21 | url = self.api(url, *args) 22 | response = self.http_get(url, params=self.query_params(**kwargs)) 23 | if self.unhealthy(response.status_code): 24 | args.reverse() # error msg is in reverse order 25 | raise KubeHTTPException(response, message, *args) 26 | 27 | return response 28 | 29 | def create(self, namespace): 30 | url = self.api("/namespaces") 31 | data = { 32 | "kind": "Namespace", 33 | "apiVersion": "v1", 34 | "metadata": { 35 | "name": namespace, 36 | "labels": { 37 | 'heritage': 'deis' 38 | } 39 | } 40 | } 41 | 42 | response = self.http_post(url, json=data) 43 | if not response.status_code == 201: 44 | raise KubeHTTPException(response, "create Namespace {}".format(namespace)) 45 | 46 | return response 47 | 48 | def delete(self, namespace): 49 | url = self.api("/namespaces/{}", namespace) 50 | response = self.http_delete(url) 51 | if self.unhealthy(response.status_code): 52 | raise KubeHTTPException(response, 'delete Namespace "{}"', namespace) 53 | 54 | return response 55 | 56 | def events(self, namespace, **kwargs): 57 | url = self.api("/namespaces/{}/events", namespace) 58 | response = self.http_get(url, params=self.query_params(**kwargs)) 59 | if self.unhealthy(response.status_code): 60 | raise KubeHTTPException(response, "get Events in Namespace {}", namespace) 61 | 62 | return response 63 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_namespaces.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis scheduler module. 3 | 4 | Run the tests with './manage.py test scheduler' 5 | """ 6 | from scheduler import KubeHTTPException 7 | from scheduler.tests import TestCase 8 | 9 | 10 | class NamespacesTest(TestCase): 11 | """Tests scheduler namespace calls""" 12 | 13 | def test_create_namespace(self): 14 | # subclassed function does all the checking 15 | self.create_namespace() 16 | 17 | def test_get_namespaces(self): 18 | response = self.scheduler.ns.get() 19 | data = response.json() 20 | self.assertEqual(response.status_code, 200, data) 21 | self.assertIn('items', data) 22 | # mock scheduler already creates deis and duplicate 23 | self.assertEqual(3, len(data['items']), data['items']) 24 | # simple verify of data 25 | self.assertEqual(data['items'][2]['metadata']['name'], self.namespace) 26 | 27 | def test_get_namespace(self): 28 | with self.assertRaises( 29 | KubeHTTPException, 30 | msg='failed to get Namespace doesnotexist: 404 Not Found' 31 | ): 32 | self.scheduler.node.get('doesnotexist') 33 | 34 | response = self.scheduler.ns.get(self.namespace) 35 | data = response.json() 36 | self.assertEqual(response.status_code, 200, data) 37 | self.assertEqual(data['apiVersion'], 'v1') 38 | self.assertEqual(data['kind'], 'Namespace') 39 | self.assertDictContainsSubset( 40 | { 41 | 'name': self.namespace, 42 | 'labels': { 43 | 'heritage': 'deis' 44 | } 45 | }, 46 | data['metadata'] 47 | ) 48 | 49 | def test_delete_failure(self): 50 | # test failure 51 | with self.assertRaises( 52 | KubeHTTPException, 53 | msg='failed to delete Namespace doesnotexist: 404 Not Found' 54 | ): 55 | self.scheduler.ns.delete('doesnotexist') 56 | 57 | def test_delete_namespace(self): 58 | response = self.scheduler.ns.delete(self.namespace) 59 | self.assertEqual(response.status_code, 200, response.json()) 60 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0009_auto_20160607_2259.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2016-06-07 22:59 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('api', '0008_config_registry'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='app', 19 | name='owner', 20 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), 21 | ), 22 | migrations.AlterField( 23 | model_name='build', 24 | name='owner', 25 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), 26 | ), 27 | migrations.AlterField( 28 | model_name='certificate', 29 | name='owner', 30 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), 31 | ), 32 | migrations.AlterField( 33 | model_name='config', 34 | name='owner', 35 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), 36 | ), 37 | migrations.AlterField( 38 | model_name='domain', 39 | name='owner', 40 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), 41 | ), 42 | migrations.AlterField( 43 | model_name='key', 44 | name='owner', 45 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 46 | ), 47 | migrations.AlterField( 48 | model_name='push', 49 | name='owner', 50 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), 51 | ), 52 | migrations.AlterField( 53 | model_name='release', 54 | name='owner', 55 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_ingress.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis ingress module. 3 | 4 | Run the tests with './manage.py test ingress' 5 | """ 6 | from scheduler import KubeHTTPException 7 | from scheduler.tests import TestCase 8 | 9 | 10 | class IngressTest(TestCase): 11 | """Tests scheduler ingress calls""" 12 | 13 | def test_create_ingress(self): 14 | # Ingress assumes that the namespace and ingress name are always the same 15 | self.scheduler.ns.create("test-ingress") 16 | self.scheduler.ingress.create("test-ingress", "test-ingress", "test-ingress") 17 | 18 | def test_get_ingresses(self): 19 | response = self.scheduler.ingress.get() 20 | data = response.json() 21 | self.assertEqual(response.status_code, 200, data) 22 | self.assertIn('items', data) 23 | 24 | def test_get_ingress(self): 25 | with self.assertRaises( 26 | KubeHTTPException, 27 | msg="failed to get Ingress doesnotexist: 404 Not Found" 28 | ): 29 | self.scheduler.ingress.get('doesnotexist') 30 | 31 | self.scheduler.ns.create("test-ingress-create") 32 | self.scheduler.ingress.create("test-ingress-create", 33 | "test-ingress-create", "test-ingress-create") 34 | response = self.scheduler.ingress.get("test-ingress-create") 35 | data = response.json() 36 | 37 | self.assertEqual(response.status_code, 200, data) 38 | self.assertEqual(data['apiVersion'], 'extensions/v1beta1') 39 | self.assertEqual(data['kind'], 'Ingress') 40 | 41 | def test_delete_failure(self): 42 | # test failure 43 | with self.assertRaises( 44 | KubeHTTPException, 45 | msg="failed to delete Ingress doesnotexist: 404 Not Found" 46 | ): 47 | self.scheduler.ns.delete('doesnotexist') 48 | 49 | def test_delete_namespace(self): 50 | self.scheduler.ns.create("test-ingress-delete") 51 | self.scheduler.ingress.create("test-ingress-delete", 52 | "test-ingress-delete", "test-ingress-delete") 53 | response = self.scheduler.ingress.delete("test-ingress-delete", "test-ingress-delete") 54 | self.assertEqual(response.status_code, 200, response.json()) 55 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0003_auto_20160114_0310.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-14 03:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('api', '0002_auto_20151215_0352'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='app', 18 | name='uuid', 19 | field=models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID'), 20 | ), 21 | migrations.AlterField( 22 | model_name='build', 23 | name='uuid', 24 | field=models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID'), 25 | ), 26 | migrations.AlterField( 27 | model_name='config', 28 | name='uuid', 29 | field=models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID'), 30 | ), 31 | migrations.AlterField( 32 | model_name='container', 33 | name='uuid', 34 | field=models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID'), 35 | ), 36 | migrations.AlterField( 37 | model_name='key', 38 | name='uuid', 39 | field=models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID'), 40 | ), 41 | migrations.AlterField( 42 | model_name='push', 43 | name='uuid', 44 | field=models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID'), 45 | ), 46 | migrations.AlterField( 47 | model_name='release', 48 | name='uuid', 49 | field=models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID'), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /_scripts/util/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This hook verifies that the commit message follows deis commit style 4 | # To install this hook run the following command from the deis git root 5 | # cp contrib/util/commit-msg .git/hooks/commit-msg 6 | set -eo pipefail 7 | 8 | RED=$(tput setaf 1) 9 | YELLOW=$(tput setaf 3) 10 | NORMAL=$(tput sgr0) 11 | subject_regex="^(feat|fix|docs|style|ref|test|chore)\(.+\): [\w\s\d]*" 12 | capital_regex="^.+\): [a-z][\w\s\d]*" 13 | 14 | MESSAGE[1]="file" 15 | 16 | i=1 # the first array variable is at index 1 17 | while read line 18 | do 19 | MESSAGE[$i]=$line 20 | let i++ 21 | done < "$1" 22 | 23 | SUBJECT=${MESSAGE[1]} 24 | 25 | if ! [[ $SUBJECT =~ $subject_regex ]]; then 26 | echo "${RED}ERROR - Invalid subject line." 27 | echo "" 28 | echo "$SUBJECT" 29 | echo "" 30 | echo "It must be in the format: {type}({scope}): {subject}" 31 | echo "" 32 | echo "The following {type}s are allowed:" 33 | echo "feat" 34 | echo "fix" 35 | echo "docs" 36 | echo "style" 37 | echo "ref" 38 | echo "test" 39 | echo "chore" 40 | echo "" 41 | echo "Read more at http://docs.deis.io/en/latest/contributing/standards/$NORMAL" 42 | exit 0 43 | fi 44 | 45 | if ! [[ $SUBJECT =~ $capital_regex ]]; then 46 | echo "${RED}ERROR - Don't the capitalize commit message." 47 | echo "" 48 | echo "$SUBJECT" 49 | echo "" 50 | echo "Read more at http://docs.deis.io/en/latest/contributing/standards/$NORMAL" 51 | exit 0 52 | fi 53 | 54 | if [[ ${#SUBJECT} -gt 50 ]]; then 55 | echo "${YELLOW}WARNING - Subject shouldn't be longer than 50 characters." 56 | echo "" 57 | echo "Read more at http://docs.deis.io/en/latest/contributing/standards/$NORMAL" 58 | exit 0 59 | fi 60 | 61 | if [[ ${#MESSAGE[2]} -gt 0 ]]; then 62 | echo "${RED}ERROR - Second line must be blank" 63 | echo "" 64 | echo "Read more at http://docs.deis.io/en/latest/contributing/standards/$NORMAL" 65 | exit 0 66 | fi 67 | 68 | cnt=${#MESSAGE[@]} 69 | for (( i = 3 ; i <= cnt ; i++ )) 70 | do 71 | if [[ ${#MESSAGE[$i]} -gt 72 ]] && [[ ${MESSAGE[$i]:0:1} != '#' ]]; then 72 | echo "${RED}ERROR on line $i - can't be longer than 72 characters." 73 | echo "" 74 | echo "Read more at http://docs.deis.io/en/latest/contributing/standards/$NORMAL" 75 | exit 0 76 | fi 77 | done 78 | 79 | echo "Your commit message follows the deis commit style." 80 | 81 | exit 0 82 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/ingress.py: -------------------------------------------------------------------------------- 1 | from scheduler.exceptions import KubeHTTPException 2 | from scheduler.resources import Resource 3 | 4 | 5 | class Ingress(Resource): 6 | short_name = 'ingress' 7 | 8 | def get(self, name=None, **kwargs): 9 | """ 10 | Fetch a single Ingress or a list of Ingresses 11 | """ 12 | if name is not None: 13 | url = "/apis/extensions/v1beta1/namespaces/%s/ingresses/%s" % (name, name) 14 | message = 'get Ingress ' + name 15 | else: 16 | url = "/apis/extensions/v1beta1/namespaces/%s/ingresses" % name 17 | message = 'get Ingresses' 18 | 19 | response = self.http_get(url, params=self.query_params(**kwargs)) 20 | if self.unhealthy(response.status_code): 21 | raise KubeHTTPException(response, message) 22 | 23 | return response 24 | 25 | def create(self, ingress, namespace, hostname): 26 | url = "/apis/extensions/v1beta1/namespaces/%s/ingresses" % namespace 27 | 28 | data = { 29 | "kind": "Ingress", 30 | "apiVersion": "extensions/v1beta1", 31 | "metadata": { 32 | "name": ingress 33 | }, 34 | "spec": { 35 | "rules": [ 36 | {"host": ingress + "." + hostname, 37 | "http": { 38 | "paths": [ 39 | {"path": "/", 40 | "backend": { 41 | "serviceName": ingress, 42 | "servicePort": 80 43 | }} 44 | ] 45 | } 46 | } 47 | ] 48 | } 49 | } 50 | response = self.http_post(url, json=data) 51 | 52 | if not response.status_code == 201: 53 | raise KubeHTTPException(response, "create Ingress {}".format(namespace)) 54 | 55 | return response 56 | 57 | def delete(self, namespace, ingress): 58 | url = "/apis/extensions/v1beta1/namespaces/%s/ingresses/%s" % (namespace, ingress) 59 | response = self.http_delete(url) 60 | if self.unhealthy(response.status_code): 61 | raise KubeHTTPException(response, 'delete Ingress "{}"', namespace) 62 | 63 | return response 64 | -------------------------------------------------------------------------------- /rootfs/Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM quay.io/deis/base:v0.3.6 2 | 3 | RUN adduser --system \ 4 | --shell /bin/bash \ 5 | --disabled-password \ 6 | --home /app \ 7 | --group \ 8 | deis 9 | 10 | COPY requirements.txt /app/requirements.txt 11 | COPY dev_requirements.txt /app/dev_requirements.txt 12 | 13 | RUN buildDeps='gcc libffi-dev libpq-dev libldap2-dev libsasl2-dev python3-dev python3-pip python3-wheel python3-setuptools'; \ 14 | apt-get update && \ 15 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 16 | $buildDeps \ 17 | sudo \ 18 | libpq5 \ 19 | libldap-2.4 \ 20 | python3-minimal \ 21 | # cryptography package needs pkg_resources 22 | python3-pkg-resources && \ 23 | ln -s /usr/bin/python3 /usr/bin/python && \ 24 | mkdir -p /configs && chown -R deis:deis /configs && \ 25 | pip3 install --disable-pip-version-check --no-cache-dir -r /app/requirements.txt && \ 26 | # cleanup 27 | apt-get purge -y --auto-remove $buildDeps && \ 28 | apt-get autoremove -y && \ 29 | apt-get clean -y && \ 30 | # package up license files if any by appending to existing tar 31 | COPYRIGHT_TAR='/usr/share/copyrights.tar'; \ 32 | gunzip -f $COPYRIGHT_TAR.gz; tar -rf $COPYRIGHT_TAR /usr/share/doc/*/copyright; gzip $COPYRIGHT_TAR && \ 33 | rm -rf \ 34 | /usr/share/doc \ 35 | /usr/share/man \ 36 | /usr/share/info \ 37 | /usr/share/locale \ 38 | /var/lib/apt/lists/* \ 39 | /var/log/* \ 40 | /var/cache/debconf/* \ 41 | /etc/systemd \ 42 | /lib/lsb \ 43 | /lib/udev \ 44 | /usr/lib/x86_64-linux-gnu/gconv/IBM* \ 45 | /usr/lib/x86_64-linux-gnu/gconv/EBC* && \ 46 | bash -c "mkdir -p /usr/share/man/man{1..8}" 47 | 48 | # define execution environment 49 | WORKDIR /app 50 | 51 | # test-unit additions to the main Dockerfile 52 | ENV PGBIN=/usr/lib/postgresql/9.5/bin PGDATA=/var/lib/postgresql/data 53 | RUN apt-get update && \ 54 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 55 | git \ 56 | postgresql \ 57 | postgresql-contrib \ 58 | python3-pip \ 59 | python3-setuptools \ 60 | python3-wheel && \ 61 | pip3 install --disable-pip-version-check --no-cache-dir -r dev_requirements.txt && \ 62 | sudo -u postgres -E $PGBIN/initdb 63 | 64 | CMD ["/app/bin/test-unit"] 65 | -------------------------------------------------------------------------------- /rootfs/api/fixtures/tests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 7, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "autotest", 7 | "first_name": "Otto", 8 | "last_name": "Test", 9 | "is_active": true, 10 | "is_superuser": true, 11 | "is_staff": true, 12 | "last_login": "2013-05-10T16:08:09.357Z", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=", 16 | "email": "autotest@deis.io", 17 | "date_joined": "2013-05-10T16:08:09.357Z" 18 | } 19 | }, 20 | { 21 | "pk": 8, 22 | "model": "auth.user", 23 | "fields": { 24 | "username": "autotest2", 25 | "first_name": "Otto", 26 | "last_name": "Test", 27 | "is_active": true, 28 | "is_superuser": false, 29 | "is_staff": false, 30 | "last_login": "2013-05-10T16:08:09.357Z", 31 | "groups": [], 32 | "user_permissions": [], 33 | "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=", 34 | "email": "autotest2@deis.io", 35 | "date_joined": "2013-05-10T16:08:09.357Z" 36 | } 37 | }, 38 | { 39 | "pk": 9, 40 | "model": "auth.user", 41 | "fields": { 42 | "username": "autotest3", 43 | "first_name": "Otto", 44 | "last_name": "Test", 45 | "is_active": true, 46 | "is_superuser": false, 47 | "is_staff": false, 48 | "last_login": "2013-05-10T16:08:09.357Z", 49 | "groups": [], 50 | "user_permissions": [], 51 | "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=", 52 | "email": "autotest3@deis.io", 53 | "date_joined": "2013-05-10T16:08:09.357Z" 54 | } 55 | }, 56 | { 57 | "pk": 10, 58 | "model": "auth.user", 59 | "fields": { 60 | "username": "autotest4", 61 | "first_name": "Otto", 62 | "last_name": "Test", 63 | "is_active": true, 64 | "is_superuser": false, 65 | "is_staff": false, 66 | "last_login": "2013-05-10T16:08:09.357Z", 67 | "groups": [], 68 | "user_permissions": [], 69 | "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=", 70 | "email": "autotest4@deis.io", 71 | "date_joined": "2013-05-10T16:08:09.357Z" 72 | } 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /rootfs/api/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Django admin app configuration for Deis API models. 5 | """ 6 | 7 | 8 | from django.contrib import admin 9 | from guardian.admin import GuardedModelAdmin 10 | 11 | from .models import App 12 | from .models import Build 13 | from .models import Config 14 | from .models import Domain 15 | from .models import Key 16 | from .models import Release 17 | 18 | 19 | class AppAdmin(GuardedModelAdmin): 20 | """Set presentation options for :class:`~api.models.App` models 21 | in the Django admin. 22 | """ 23 | date_hierarchy = 'created' 24 | list_display = ('id', 'owner') 25 | list_filter = ('owner',) 26 | 27 | 28 | admin.site.register(App, AppAdmin) 29 | 30 | 31 | class BuildAdmin(admin.ModelAdmin): 32 | """Set presentation options for :class:`~api.models.Build` models 33 | in the Django admin. 34 | """ 35 | date_hierarchy = 'created' 36 | list_display = ('created', 'owner', 'app') 37 | list_filter = ('owner', 'app') 38 | 39 | 40 | admin.site.register(Build, BuildAdmin) 41 | 42 | 43 | class ConfigAdmin(admin.ModelAdmin): 44 | """Set presentation options for :class:`~api.models.Config` models 45 | in the Django admin. 46 | """ 47 | date_hierarchy = 'created' 48 | list_display = ('created', 'owner', 'app') 49 | list_filter = ('owner', 'app') 50 | 51 | 52 | admin.site.register(Config, ConfigAdmin) 53 | 54 | 55 | class DomainAdmin(admin.ModelAdmin): 56 | """Set presentation options for :class:`~api.models.Domain` models 57 | in the Django admin. 58 | """ 59 | date_hierarchy = 'created' 60 | list_display = ('owner', 'app', 'domain') 61 | list_filter = ('owner', 'app') 62 | 63 | 64 | admin.site.register(Domain, DomainAdmin) 65 | 66 | 67 | class KeyAdmin(admin.ModelAdmin): 68 | """Set presentation options for :class:`~api.models.Key` models 69 | in the Django admin. 70 | """ 71 | date_hierarchy = 'created' 72 | list_display = ('id', 'owner', '__str__') 73 | list_filter = ('owner',) 74 | 75 | 76 | admin.site.register(Key, KeyAdmin) 77 | 78 | 79 | class ReleaseAdmin(admin.ModelAdmin): 80 | """Set presentation options for :class:`~api.models.Release` models 81 | in the Django admin. 82 | """ 83 | date_hierarchy = 'created' 84 | list_display = ('created', 'version', 'owner', 'app') 85 | list_display_links = ('created', 'version') 86 | list_filter = ('owner', 'app') 87 | 88 | 89 | admin.site.register(Release, ReleaseAdmin) 90 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # If DEIS_REGISTRY is not set, try to populate it from legacy DEV_REGISTRY 2 | DEIS_REGISTRY ?= $(DEV_REGISTRY) 3 | IMAGE_PREFIX ?= deis 4 | COMPONENT ?= controller 5 | SHORT_NAME ?= $(COMPONENT) 6 | 7 | include versioning.mk 8 | 9 | SHELLCHECK_PREFIX := docker run -v ${CURDIR}:/workdir -w /workdir quay.io/deis/shell-dev shellcheck 10 | SHELL_SCRIPTS = $(wildcard rootfs/bin/*) $(shell find "rootfs" -name '*.sh') $(wildcard _scripts/*.sh) 11 | 12 | # Test processes used in quick unit testing 13 | TEST_PROCS ?= 4 14 | 15 | check-kubectl: 16 | @if [ -z $$(which kubectl) ]; then \ 17 | echo "kubectl binary could not be located"; \ 18 | exit 2; \ 19 | fi 20 | 21 | check-docker: 22 | @if [ -z $$(which docker) ]; then \ 23 | echo "Missing \`docker\` client which is required for development"; \ 24 | exit 2; \ 25 | fi 26 | 27 | build: docker-build 28 | 29 | docker-build: check-docker 30 | docker build ${DOCKER_BUILD_FLAGS} -t ${IMAGE} rootfs 31 | docker tag ${IMAGE} ${MUTABLE_IMAGE} 32 | 33 | docker-build-test: check-docker 34 | docker build ${DOCKER_BUILD_FLAGS} -t ${IMAGE}.test -f rootfs/Dockerfile.test rootfs 35 | 36 | deploy: check-kubectl docker-build docker-push 37 | kubectl --namespace=deis patch deployment deis-$(COMPONENT) --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"$(IMAGE)"}]' 38 | 39 | clean: check-docker 40 | docker rmi $(IMAGE) 41 | 42 | commit-hook: 43 | cp _scripts/util/commit-msg .git/hooks/commit-msg 44 | 45 | full-clean: check-docker 46 | docker images -q $(IMAGE_PREFIX)$(COMPONENT) | xargs docker rmi -f 47 | 48 | test: test-style test-unit test-functional 49 | 50 | test-style: docker-build-test 51 | docker run -v ${CURDIR}:/test -w /test/rootfs ${IMAGE}.test /test/rootfs/bin/test-style 52 | ${SHELLCHECK_PREFIX} $(SHELL_SCRIPTS) 53 | 54 | test-unit: docker-build-test 55 | docker run -v ${CURDIR}:/test -w /test/rootfs ${IMAGE}.test /test/rootfs/bin/test-unit 56 | 57 | test-functional: 58 | @echo "Implement functional tests in _tests directory" 59 | 60 | test-integration: 61 | @echo "Check https://github.com/deis/workflow-e2e for the complete integration test suite" 62 | 63 | upload-coverage: 64 | $(eval CI_ENV := $(shell curl -s https://codecov.io/env | bash)) 65 | docker run ${CI_ENV} -v ${CURDIR}:/test -w /test/rootfs ${IMAGE}.test codecov --required 66 | 67 | .PHONY: check-kubectl check-docker build docker-build docker-build-test deploy clean commit-hook full-clean test test-style test-unit test-functional test-integration upload-coverage 68 | -------------------------------------------------------------------------------- /rootfs/api/models/domain.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | from api.models import AuditedModel 5 | 6 | 7 | class Domain(AuditedModel): 8 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) 9 | app = models.ForeignKey('App', on_delete=models.CASCADE) 10 | domain = models.TextField( 11 | blank=False, null=False, unique=True, 12 | error_messages={ 13 | 'unique': 'Domain is already in use by another application' 14 | } 15 | ) 16 | certificate = models.ForeignKey( 17 | 'Certificate', 18 | on_delete=models.SET_NULL, 19 | blank=True, 20 | null=True 21 | ) 22 | 23 | class Meta: 24 | ordering = ['domain', 'certificate'] 25 | 26 | def save(self, *args, **kwargs): 27 | app = str(self.app) 28 | domain = str(self.domain) 29 | 30 | # get config for the service 31 | config = self._load_service_config(app, 'router') 32 | 33 | # See if domains are available 34 | if 'domains' not in config: 35 | config['domains'] = '' 36 | 37 | # convert from string to list to work with and filter out empty strings 38 | domains = [_f for _f in config['domains'].split(',') if _f] 39 | if domain not in domains: 40 | domains.append(domain) 41 | config['domains'] = ','.join(domains) 42 | 43 | self._save_service_config(app, 'router', config) 44 | 45 | # Save to DB 46 | return super(Domain, self).save(*args, **kwargs) 47 | 48 | def delete(self, *args, **kwargs): 49 | app = str(self.app) 50 | domain = str(self.domain) 51 | 52 | # Deatch cert, updates k8s 53 | if self.certificate: 54 | self.certificate.detach(domain=domain) 55 | 56 | # get config for the service 57 | config = self._load_service_config(app, 'router') 58 | 59 | # See if domains are available 60 | if 'domains' not in config: 61 | config['domains'] = '' 62 | 63 | # convert from string to list to work with and filter out empty strings 64 | domains = [_f for _f in config['domains'].split(',') if _f] 65 | if domain in domains: 66 | domains.remove(domain) 67 | config['domains'] = ','.join(domains) 68 | 69 | self._save_service_config(app, 'router', config) 70 | 71 | # Delete from DB 72 | return super(Domain, self).delete(*args, **kwargs) 73 | 74 | def __str__(self): 75 | return self.domain 76 | -------------------------------------------------------------------------------- /charts/controller/values.yaml: -------------------------------------------------------------------------------- 1 | org: "deisci" 2 | pull_policy: "Always" 3 | docker_tag: canary 4 | app_pull_policy: "Always" 5 | # A comma-separated list of URLs to send app release information to 6 | # See https://deis.com/docs/workflow/managing-workflow/deploy-hooks 7 | deploy_hook_urls: "" 8 | # limits_cpu: "100m" 9 | # limits_memory: "50Mi" 10 | # Possible values are: 11 | # enabled - allows for open registration 12 | # disabled - turns off open registration 13 | # admin_only - allows for registration by an admin only. 14 | registration_mode: "admin_only" 15 | # Option to disable ssl verification to connect to k8s api server 16 | k8s_api_verify_tls: "true" 17 | # The public resolvable hostname to build your cluster with. 18 | # 19 | # This will be the hostname that is used to build endpoints such as "deis.$HOSTNAME" 20 | platform_domain: "" 21 | 22 | global: 23 | # Set the storage backend 24 | # 25 | # Valid values are: 26 | # - s3: Store persistent data in AWS S3 (configure in S3 section) 27 | # - azure: Store persistent data in Azure's object storage 28 | # - gcs: Store persistent data in Google Cloud Storage 29 | # - minio: Store persistent data on in-cluster Minio server 30 | storage: minio 31 | # Set the location of Workflow's PostgreSQL database 32 | # 33 | # Valid values are: 34 | # - on-cluster: Run PostgreSQL within the Kubernetes cluster (credentials are generated 35 | # automatically; backups are sent to object storage 36 | # configured above) 37 | # - off-cluster: Run PostgreSQL outside the Kubernetes cluster (configure in database section) 38 | database_location: "on-cluster" 39 | 40 | # Set the location of Workflow's Registry 41 | # 42 | # Valid values are: 43 | # - on-cluster: Run registry within the Kubernetes cluster 44 | # - off-cluster: Use registry outside the Kubernetes cluster (example: dockerhub,quay.io,self-hosted) 45 | # - ecr: Use Amazon's ECR 46 | # - gcr: Use Google's GCR 47 | registry_location: "on-cluster" 48 | # The host port to which registry proxy binds to 49 | host_port: 5555 50 | # Prefix for the imagepull secret created when using private registry 51 | secret_prefix: "private-registry" 52 | # Experimental feature to toggle using kubernetes ingress instead of the Deis router. 53 | # 54 | # Valid values are: 55 | # - true: The deis controller will now create Kubernetes ingress rules for each app, and ingress rules will automatically be created for the controller itself. 56 | # - false: The default mode, and the default behavior of Deis workflow. 57 | experimental_native_ingress: false 58 | # Role-Based Access Control for Kubernetes >= 1.5 59 | use_rbac: false 60 | -------------------------------------------------------------------------------- /rootfs/api/models/tls.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | from api.exceptions import AlreadyExists 5 | from api.models import UuidAuditedModel 6 | 7 | 8 | class TLS(UuidAuditedModel): 9 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) 10 | app = models.ForeignKey('App', on_delete=models.CASCADE) 11 | https_enforced = models.NullBooleanField(default=None) 12 | 13 | class Meta: 14 | get_latest_by = 'created' 15 | unique_together = (('app', 'uuid')) 16 | ordering = ['-created'] 17 | 18 | def __str__(self): 19 | return "{}-{}".format(self.app.id, str(self.uuid)[:7]) 20 | 21 | def _load_service_config(self, app, component): 22 | config = super()._load_service_config(app, component) 23 | 24 | # See if the ssl.enforce annotation is available 25 | if 'ssl' not in config: 26 | config['ssl'] = {} 27 | if 'enforce' not in config['ssl']: 28 | config['ssl']['enforce'] = 'false' 29 | 30 | return config 31 | 32 | def _check_previous_tls_settings(self): 33 | try: 34 | previous_tls_settings = self.app.tls_set.latest() 35 | 36 | if ( 37 | previous_tls_settings.https_enforced is not None and 38 | self.https_enforced == previous_tls_settings.https_enforced 39 | ): 40 | self.delete() 41 | raise AlreadyExists("{} changed nothing".format(self.owner)) 42 | except TLS.DoesNotExist: 43 | pass 44 | 45 | def save(self, *args, **kwargs): 46 | self._check_previous_tls_settings() 47 | 48 | app = str(self.app) 49 | https_enforced = bool(self.https_enforced) 50 | 51 | # get config for the service 52 | config = self._load_service_config(app, 'router') 53 | 54 | # convert from bool to string 55 | config['ssl']['enforce'] = str(https_enforced) 56 | 57 | self._save_service_config(app, 'router', config) 58 | 59 | # Save to DB 60 | return super(TLS, self).save(*args, **kwargs) 61 | 62 | def sync(self): 63 | try: 64 | app = str(self.app) 65 | 66 | config = self._load_service_config(app, 'router') 67 | if ( 68 | config['ssl']['enforce'] != str(self.https_enforced) and 69 | self.https_enforced is not None 70 | ): 71 | config['ssl']['enforce'] = str(self.https_enforced) 72 | self._save_service_config(app, 'router', config) 73 | except TLS.DoesNotExist: 74 | pass 75 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_pod_resources.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from scheduler.resources.pod import Pod 3 | 4 | 5 | class TestSchedulerPodResources(unittest.TestCase): 6 | def test_manifest_limits(self): 7 | cpu_cases = [ 8 | {"app_type": "web", "cpu": {"cmd": "2"}, 9 | "expected": None}, 10 | {"app_type": "web", "cpu": {"web": "2"}, 11 | "expected": {"limits": {"cpu": "2"}}}, 12 | {"app_type": "web", "cpu": {"web": "0/3"}, 13 | "expected": {"requests": {"cpu": "0"}, "limits": {"cpu": "3"}}}, 14 | {"app_type": "web", "cpu": {"web": "4/5"}, 15 | "expected": {"requests": {"cpu": "4"}, "limits": {"cpu": "5"}}}, 16 | {"app_type": "web", "cpu": {"web": "400m/500m"}, 17 | "expected": {"requests": {"cpu": "400m"}, "limits": {"cpu": "500m"}}}, 18 | {"app_type": "web", "cpu": {"web": "0.6/0.7"}, 19 | "expected": {"requests": {"cpu": "0.6"}, "limits": {"cpu": "0.7"}}}, 20 | ] 21 | 22 | mem_cases = [ 23 | {"app_type": "web", "memory": {"cmd": "2G"}, 24 | "expected": None}, 25 | {"app_type": "web", "memory": {"web": "200M"}, 26 | "expected": {"limits": {"memory": "200Mi"}}}, 27 | {"app_type": "web", "memory": {"web": "0/3G"}, 28 | "expected": {"requests": {"memory": "0"}, "limits": {"memory": "3Gi"}}}, 29 | {"app_type": "web", "memory": {"web": "400M/500MB"}, 30 | "expected": {"requests": {"memory": "400Mi"}, "limits": {"memory": "500Mi"}}}, 31 | ] 32 | 33 | for caze in cpu_cases: 34 | manifest = Pod("").manifest("", 35 | "", 36 | "", 37 | app_type=caze["app_type"], 38 | cpu=caze["cpu"]) 39 | self._assert_resources(caze, manifest) 40 | 41 | for caze in mem_cases: 42 | manifest = Pod("").manifest("", 43 | "", 44 | "", 45 | app_type=caze["app_type"], 46 | memory=caze["memory"]) 47 | self._assert_resources(caze, manifest) 48 | 49 | def _assert_resources(self, caze, manifest): 50 | resources_parent = manifest["spec"]["containers"][0] 51 | expected = caze["expected"] 52 | if expected: 53 | self.assertEqual(resources_parent["resources"], expected, caze) 54 | else: 55 | self.assertTrue("resources" not in resources_parent, caze) 56 | -------------------------------------------------------------------------------- /rootfs/api/management/commands/load_db_state_to_k8s.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.shortcuts import get_object_or_404 3 | 4 | from api.models import Key, App, Domain, Certificate 5 | from api.exceptions import DeisException, AlreadyExists 6 | 7 | 8 | class Command(BaseCommand): 9 | """Management command for publishing Deis platform state from the database 10 | to k8s. 11 | """ 12 | def handle(self, *args, **options): 13 | """Publishes Deis platform state from the database to kubernetes.""" 14 | print("Publishing DB state to kubernetes...") 15 | 16 | self.save_apps() 17 | 18 | # certificates have to be attached to domains to create k8s secrets 19 | for cert in Certificate.objects.all(): 20 | for domain in cert.domains: 21 | domain = get_object_or_404(Domain, domain=domain) 22 | cert.attach_in_kubernetes(domain) 23 | 24 | # deploy applications 25 | print("Deploying available applications") 26 | for application in App.objects.all(): 27 | rel = application.release_set.filter(failed=False).latest() 28 | if rel.build is None: 29 | print('WARNING: {} has no build associated with ' 30 | 'its latest release. Skipping deployment...'.format(application)) 31 | continue 32 | 33 | try: 34 | application.deploy(rel) 35 | except AlreadyExists as error: 36 | print('WARNING: {} has a deployment in progress. ' 37 | 'Skipping deployment...'.format(application)) 38 | continue 39 | except DeisException as error: 40 | print('ERROR: There was a problem deploying {} ' 41 | 'due to {}'.format(application, str(error))) 42 | 43 | print("Done Publishing DB state to kubernetes.") 44 | 45 | def save_apps(self): 46 | """Saves important Django data models to the database.""" 47 | for app in App.objects.all(): 48 | try: 49 | app.save() 50 | app.config_set.latest().save() 51 | app.tls_set.latest().sync() 52 | except DeisException as error: 53 | print('ERROR: Problem saving to model {} for {}' 54 | 'due to {}'.format(str(App.__name__), str(app), str(error))) 55 | for model in (Key, Domain, Certificate): 56 | for obj in model.objects.all(): 57 | try: 58 | obj.save() 59 | except DeisException as error: 60 | print('ERROR: Problem saving to model {} for {}' 61 | 'due to {}'.format(str(model.__name__), str(obj), str(error))) 62 | -------------------------------------------------------------------------------- /rootfs/api/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import requests_mock 4 | import time 5 | from os.path import dirname, realpath 6 | 7 | from django.conf import settings 8 | from django.test.runner import DiscoverRunner 9 | from rest_framework.test import APITestCase, APITransactionTestCase 10 | 11 | 12 | def mock_port(*args, **kwargs): 13 | return 5000 14 | 15 | 16 | # Mock out router requests and add in some jitter 17 | # Used for application is available in router checks 18 | def fake_responses(request, context): 19 | responses = [ 20 | # increasing the chance of 404 21 | {'text': 'Not Found', 'status_code': 404}, 22 | {'text': 'Not Found', 'status_code': 404}, 23 | {'text': 'Not Found', 'status_code': 404}, 24 | {'text': 'Not Found', 'status_code': 404}, 25 | {'text': 'OK', 'status_code': 200}, 26 | {'text': 'Gateway timeout', 'status_code': 504}, 27 | {'text': 'Bad gateway', 'status_code': 502}, 28 | ] 29 | random.shuffle(responses) 30 | response = responses.pop() 31 | 32 | context.status_code = response['status_code'] 33 | context.reason = response['text'] 34 | # Random float x, 1.0 <= x < 4.0 for some sleep jitter 35 | time.sleep(random.uniform(1, 4)) 36 | return response['text'] 37 | 38 | 39 | url = 'http://{}:{}'.format(settings.ROUTER_HOST, settings.ROUTER_PORT) 40 | adapter = requests_mock.Adapter() 41 | adapter.register_uri('GET', url + '/', text=fake_responses) 42 | adapter.register_uri('GET', url + '/health', text=fake_responses) 43 | adapter.register_uri('GET', url + '/healthz', text=fake_responses) 44 | 45 | # Root of the test directory (for files and such) 46 | TEST_ROOT = dirname(realpath(__file__)) 47 | 48 | 49 | class SilentDjangoTestSuiteRunner(DiscoverRunner): 50 | """Prevents api log messages from cluttering the console during tests.""" 51 | 52 | def run_tests(self, test_labels, extra_tests=None, **kwargs): 53 | """Run tests with all but critical log messages disabled.""" 54 | # hide any log messages less than critical 55 | logging.disable(logging.ERROR) 56 | return super(SilentDjangoTestSuiteRunner, self).run_tests( 57 | test_labels, extra_tests, **kwargs) 58 | 59 | 60 | class DeisTestCase(APITestCase): 61 | def create_app(self, name=None): 62 | body = {} 63 | if name: 64 | body = {'id': name} 65 | 66 | response = self.client.post('/v2/apps', body) 67 | self.assertEqual(response.status_code, 201, response.data) 68 | self.assertIn('id', response.data) 69 | return response.data['id'] 70 | 71 | 72 | class DeisTransactionTestCase(APITransactionTestCase): 73 | def create_app(self, name=None): 74 | body = {} 75 | if name: 76 | body = {'id': name} 77 | 78 | response = self.client.post('/v2/apps', body) 79 | self.assertEqual(response.status_code, 201, response.data) 80 | self.assertIn('id', response.data) 81 | return response.data['id'] 82 | -------------------------------------------------------------------------------- /rootfs/api/tests/test_tls.py: -------------------------------------------------------------------------------- 1 | import requests_mock 2 | 3 | from django.core.cache import cache 4 | from django.contrib.auth.models import User 5 | from rest_framework.authtoken.models import Token 6 | 7 | from api.models import App 8 | from api.tests import adapter, DeisTransactionTestCase 9 | 10 | 11 | @requests_mock.Mocker(real_http=True, adapter=adapter) 12 | class TestTLS(DeisTransactionTestCase): 13 | """Tests setting and updating config values""" 14 | 15 | fixtures = ['tests.json'] 16 | 17 | def setUp(self): 18 | self.user = User.objects.get(username='autotest') 19 | self.token = Token.objects.get(user=self.user).key 20 | self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token) 21 | 22 | def tearDown(self): 23 | # make sure every test has a clean slate for k8s mocking 24 | cache.clear() 25 | 26 | def test_tls_enforced(self, mock_requests): 27 | """ 28 | Test that tls redirection can be enforced 29 | """ 30 | app_id = self.create_app() 31 | app = App.objects.get(id=app_id) 32 | 33 | data = {'https_enforced': True} 34 | response = self.client.post( 35 | '/v2/apps/{app_id}/tls'.format(**locals()), 36 | data) 37 | self.assertEqual(response.status_code, 201, response.data) 38 | self.assertTrue(response.data.get('https_enforced'), response.data) 39 | self.assertTrue(app.tls_set.latest().https_enforced) 40 | 41 | data = {'https_enforced': False} 42 | response = self.client.post( 43 | '/v2/apps/{app_id}/tls'.format(**locals()), 44 | data) 45 | self.assertEqual(response.status_code, 201, response.data) 46 | self.assertFalse(app.tls_set.latest().https_enforced) 47 | 48 | # when the same data is sent again, a 409 is returned 49 | conflict_response = self.client.post( 50 | '/v2/apps/{app_id}/tls'.format(**locals()), 51 | data) 52 | self.assertEqual(conflict_response.status_code, 409, conflict_response.data) 53 | self.assertFalse(app.tls_set.latest().https_enforced) 54 | # also ensure that the previous tls UUID matches the latest, 55 | # confirming this conflicting TLS object was deleted 56 | self.assertEqual(response.data['uuid'], str(app.tls_set.latest().uuid)) 57 | 58 | # sending bad data returns a 400 59 | data['https_enforced'] = "test" 60 | response = self.client.post( 61 | '/v2/apps/{app_id}/tls'.format(**locals()), 62 | data) 63 | self.assertEqual(response.status_code, 400, response.data) 64 | 65 | def test_tls_created_on_app_create(self, mock_requests): 66 | """ 67 | Ensure that a TLS object is created for an App with default values. 68 | 69 | See https://github.com/deis/controller/issues/1042 70 | """ 71 | app_id = self.create_app() 72 | response = self.client.get('/v2/apps/{}/tls'.format(app_id)) 73 | self.assertEqual(response.status_code, 200, response.data) 74 | self.assertEqual(response.data['https_enforced'], None) 75 | -------------------------------------------------------------------------------- /rootfs/api/migrations/0006_auto_20160114_0313.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-14 03:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import api.models 8 | import uuid 9 | import datetime 10 | from django.utils.timezone import utc 11 | 12 | 13 | def fix_cert(apps, schema_editor): 14 | # We can't import the Person model directly as it may be a newer 15 | # version than this migration expects. We use the historical version. 16 | certificate = apps.get_model("api", "Certificate") 17 | for cert in certificate.objects.all(): 18 | # many new fields are updated 19 | cert.save() 20 | 21 | 22 | class Migration(migrations.Migration): 23 | 24 | dependencies = [ 25 | ('api', '0005_auto_20160208_2156'), 26 | ] 27 | 28 | operations = [ 29 | migrations.AddField( 30 | model_name='certificate', 31 | name='fingerprint', 32 | field=models.CharField(default=datetime.datetime(2016, 1, 28, 5, 52, 47, 586393, tzinfo=utc), editable=False, max_length=96), 33 | preserve_default=False, 34 | ), 35 | migrations.AddField( 36 | model_name='certificate', 37 | name='name', 38 | field=models.CharField(default=datetime.datetime(2016, 1, 28, 5, 52, 47, 586393, tzinfo=utc), max_length=253, unique=True, validators=[api.models.validate_label]), 39 | preserve_default=False, 40 | ), 41 | migrations.AddField( 42 | model_name='certificate', 43 | name='san', 44 | field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=253), null=True, size=None), 45 | ), 46 | migrations.AlterField( 47 | model_name='certificate', 48 | name='common_name', 49 | field=models.TextField(editable=False, unique=False), 50 | ), 51 | migrations.AddField( 52 | model_name='domain', 53 | name='certificate', 54 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.Certificate'), 55 | ), 56 | migrations.AddField( 57 | model_name='certificate', 58 | name='issuer', 59 | field=models.TextField(default=datetime.datetime(2016, 1, 28, 5, 52, 47, 586393, tzinfo=utc), editable=False), 60 | preserve_default=False, 61 | ), 62 | migrations.AddField( 63 | model_name='certificate', 64 | name='starts', 65 | field=models.DateTimeField(default=datetime.datetime(2016, 1, 28, 5, 52, 47, 586393, tzinfo=utc), editable=False), 66 | preserve_default=False, 67 | ), 68 | migrations.AddField( 69 | model_name='certificate', 70 | name='subject', 71 | field=models.TextField(default=datetime.datetime(2016, 1, 28, 5, 52, 47, 586393, tzinfo=utc), editable=False), 72 | preserve_default=False, 73 | ), 74 | migrations.RunPython(fix_cert), 75 | ] 76 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/service.py: -------------------------------------------------------------------------------- 1 | from scheduler.exceptions import KubeHTTPException 2 | from scheduler.resources import Resource 3 | from scheduler.utils import dict_merge 4 | 5 | 6 | class Service(Resource): 7 | short_name = 'svc' 8 | 9 | def get(self, namespace, name=None, **kwargs): 10 | """ 11 | Fetch a single Service or a list 12 | """ 13 | url = '/namespaces/{}/services' 14 | args = [namespace] 15 | if name is not None: 16 | args.append(name) 17 | url += '/{}' 18 | message = 'get Service "{}" in Namespace "{}"' 19 | else: 20 | message = 'get Services in Namespace "{}"' 21 | 22 | url = self.api(url, *args) 23 | response = self.http_get(url, params=self.query_params(**kwargs)) 24 | if self.unhealthy(response.status_code): 25 | args.reverse() # error msg is in reverse order 26 | raise KubeHTTPException(response, message, *args) 27 | 28 | return response 29 | 30 | def create(self, namespace, name, **kwargs): 31 | # Ports and app type will be overwritten as required 32 | manifest = { 33 | 'kind': 'Service', 34 | 'apiVersion': 'v1', 35 | 'metadata': { 36 | 'name': name, 37 | 'labels': { 38 | 'app': namespace, 39 | 'heritage': 'deis' 40 | }, 41 | 'annotations': {} 42 | }, 43 | 'spec': { 44 | 'ports': [{ 45 | 'name': 'http', 46 | 'port': 80, 47 | 'targetPort': 5000, 48 | 'protocol': 'TCP' 49 | }], 50 | 'selector': { 51 | 'app': namespace, 52 | 'heritage': 'deis' 53 | } 54 | } 55 | } 56 | 57 | data = dict_merge(manifest, kwargs.get('data', {})) 58 | url = self.api("/namespaces/{}/services", namespace) 59 | response = self.http_post(url, json=data) 60 | if self.unhealthy(response.status_code): 61 | raise KubeHTTPException( 62 | response, 63 | 'create Service "{}" in Namespace "{}"', namespace, namespace 64 | ) 65 | 66 | return response 67 | 68 | def update(self, namespace, name, data): 69 | url = self.api("/namespaces/{}/services/{}", namespace, name) 70 | response = self.http_put(url, json=data) 71 | if self.unhealthy(response.status_code): 72 | raise KubeHTTPException( 73 | response, 74 | 'update Service "{}" in Namespace "{}"', namespace, name 75 | ) 76 | 77 | return response 78 | 79 | def delete(self, namespace, name): 80 | url = self.api("/namespaces/{}/services/{}", namespace, name) 81 | response = self.http_delete(url) 82 | if self.unhealthy(response.status_code): 83 | raise KubeHTTPException( 84 | response, 85 | 'delete Service "{}" in Namespace "{}"', name, namespace 86 | ) 87 | 88 | return response 89 | -------------------------------------------------------------------------------- /rootfs/api/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from api import utils 3 | 4 | 5 | class TestUtils(unittest.TestCase): 6 | """Test utils functions""" 7 | def test_dict_merge_not_dict(self): 8 | """ 9 | second item is not a dict, which dict_merge will just return 10 | """ 11 | a = {'key': 'value'} 12 | b = 'somethig' 13 | c = utils.dict_merge(a, b) 14 | self.assertEqual(c, b) 15 | 16 | def test_dict_merge_simple(self): 17 | a = {'key': 'value'} 18 | b = {'key': 'value'} 19 | 20 | c = utils.dict_merge(a, b) 21 | self.assertEqual(c, {'key': 'value'}) 22 | 23 | a = {'key': 'value'} 24 | b = {'key2': 'value'} 25 | 26 | c = utils.dict_merge(a, b) 27 | self.assertEqual(c, {'key': 'value', 'key2': 'value'}) 28 | 29 | def test_dict_merge_deeper(self): 30 | a = {'key': 'value', 'here': {'without': 'you'}} 31 | b = {'this': 'that', 'here': {'with': 'me'}, 'other': {'magic', 'unicorn'}} 32 | 33 | c = utils.dict_merge(a, b) 34 | self.assertEqual(c, { 35 | 'key': 'value', 36 | 'this': 'that', 37 | 'here': { 38 | 'with': 'me', 39 | 'without': 'you' 40 | }, 41 | 'other': {'magic', 'unicorn'} 42 | }) 43 | 44 | def test_dict_merge_even_deeper(self): 45 | a = { 46 | 'key': 'value', 47 | 'here': {'without': 'you'}, 48 | 'other': {'scrubs': {'char3': 'Cox'}} 49 | 50 | } 51 | 52 | b = { 53 | 'this': 'that', 54 | 'here': {'with': 'me'}, 55 | 'other': {'magic': 'unicorn', 'scrubs': {'char1': 'JD', 'char2': 'Turk'}} 56 | } 57 | 58 | c = utils.dict_merge(a, b) 59 | self.assertEqual(c, { 60 | 'key': 'value', 61 | 'this': 'that', 62 | 'here': {'with': 'me', 'without': 'you'}, 63 | 'other': { 64 | 'magic': 'unicorn', 65 | 'scrubs': { 66 | 'char1': 'JD', 67 | 'char2': 'Turk', 68 | 'char3': 'Cox' 69 | } 70 | } 71 | }) 72 | 73 | def test_dict_merge_with_list(self): 74 | a = {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 'jimbo']} 75 | b = {'key': 'value', 'names': ['kenny', 'cartman', 'stan']} 76 | 77 | c = utils.dict_merge(a, b) 78 | self.assertEqual(c, {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 79 | 'jimbo', 'cartman', 'stan']}) 80 | 81 | a = {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 'jimbo']} 82 | b = {'key': 'value', 'last_names': ['kenny', 'cartman', 'stan']} 83 | 84 | c = utils.dict_merge(a, b) 85 | self.assertEqual(c, {'key': 'value', 86 | 'names': ['bob', 'kyle', 'kenny', 'jimbo'], 87 | 'last_names': ['kenny', 'cartman', 'stan']}) 88 | 89 | def test_dict_merge_bad_merge(self): 90 | """Returns b because it isn't a dict""" 91 | a = {'key': 'value'} 92 | b = 'duh' 93 | 94 | c = utils.dict_merge(a, b) 95 | self.assertEqual(c, b) 96 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from scheduler import utils 3 | 4 | 5 | class TestUtils(unittest.TestCase): 6 | """Test utils functions""" 7 | def test_dict_merge_not_dict(self): 8 | """ 9 | second item is not a dict, which dict_merge will just return 10 | """ 11 | a = {'key': 'value'} 12 | b = 'somethig' 13 | c = utils.dict_merge(a, b) 14 | self.assertEqual(c, b) 15 | 16 | def test_dict_merge_simple(self): 17 | a = {'key': 'value'} 18 | b = {'key': 'value'} 19 | 20 | c = utils.dict_merge(a, b) 21 | self.assertEqual(c, {'key': 'value'}) 22 | 23 | a = {'key': 'value'} 24 | b = {'key2': 'value'} 25 | 26 | c = utils.dict_merge(a, b) 27 | self.assertEqual(c, {'key': 'value', 'key2': 'value'}) 28 | 29 | def test_dict_merge_deeper(self): 30 | a = {'key': 'value', 'here': {'without': 'you'}} 31 | b = {'this': 'that', 'here': {'with': 'me'}, 'other': {'magic', 'unicorn'}} 32 | 33 | c = utils.dict_merge(a, b) 34 | self.assertEqual(c, { 35 | 'key': 'value', 36 | 'this': 'that', 37 | 'here': { 38 | 'with': 'me', 39 | 'without': 'you' 40 | }, 41 | 'other': {'magic', 'unicorn'} 42 | }) 43 | 44 | def test_dict_merge_even_deeper(self): 45 | a = { 46 | 'key': 'value', 47 | 'here': {'without': 'you'}, 48 | 'other': {'scrubs': {'char3': 'Cox'}} 49 | 50 | } 51 | 52 | b = { 53 | 'this': 'that', 54 | 'here': {'with': 'me'}, 55 | 'other': {'magic': 'unicorn', 'scrubs': {'char1': 'JD', 'char2': 'Turk'}} 56 | } 57 | 58 | c = utils.dict_merge(a, b) 59 | self.assertEqual(c, { 60 | 'key': 'value', 61 | 'this': 'that', 62 | 'here': {'with': 'me', 'without': 'you'}, 63 | 'other': { 64 | 'magic': 'unicorn', 65 | 'scrubs': { 66 | 'char1': 'JD', 67 | 'char2': 'Turk', 68 | 'char3': 'Cox' 69 | } 70 | } 71 | }) 72 | 73 | def test_dict_merge_with_list(self): 74 | a = {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 'jimbo']} 75 | b = {'key': 'value', 'names': ['kenny', 'cartman', 'stan']} 76 | 77 | c = utils.dict_merge(a, b) 78 | self.assertEqual(c, {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 79 | 'jimbo', 'cartman', 'stan']}) 80 | 81 | a = {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 'jimbo']} 82 | b = {'key': 'value', 'last_names': ['kenny', 'cartman', 'stan']} 83 | 84 | c = utils.dict_merge(a, b) 85 | self.assertEqual(c, {'key': 'value', 86 | 'names': ['bob', 'kyle', 'kenny', 'jimbo'], 87 | 'last_names': ['kenny', 'cartman', 'stan']}) 88 | 89 | def test_dict_merge_bad_merge(self): 90 | """Returns b because it isn't a dict""" 91 | a = {'key': 'value'} 92 | b = 'duh' 93 | 94 | c = utils.dict_merge(a, b) 95 | self.assertEqual(c, b) 96 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_scheduler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis scheduler module. 3 | 4 | Run the tests with "./manage.py test scheduler" 5 | """ 6 | from scheduler.tests import TestCase 7 | 8 | 9 | class SchedulerTest(TestCase): 10 | """Tests scheduler calls""" 11 | 12 | def test_set_container_applies_healthcheck_with_routable(self): 13 | """ 14 | Test that when _set_container is called with the 'routable' kwarg set to True, 15 | a healthcheck is attached to the dictionary. 16 | """ 17 | data = {} 18 | healthcheck = { 19 | 'livenessProbe': { 20 | 'httpGet': { 21 | 'port': 80, 22 | } 23 | } 24 | } 25 | 26 | readinessHealthCheck = { 27 | # an exec probe 28 | 'exec': { 29 | "command": [ 30 | "bash", 31 | "-c", 32 | "[[ '$(ps -p 1 -o args)' != *'bash /runner/init'* ]]" 33 | ] 34 | }, 35 | # length of time to wait for a pod to initialize 36 | # after pod startup, before applying health checking 37 | 'initialDelaySeconds': 30, 38 | 'timeoutSeconds': 5, 39 | 'periodSeconds': 5, 40 | 'successThreshold': 1, 41 | 'failureThreshold': 1, 42 | } 43 | 44 | self.scheduler.pod._set_container( 45 | 'foo', 'bar', data, routable=True, healthcheck=healthcheck 46 | ) 47 | self.assertDictContainsSubset(healthcheck, data) 48 | data = {} 49 | self.scheduler.pod._set_container( 50 | 'foo', 'bar', data, routable=True, build_type="buildpack", healthcheck={} 51 | ) 52 | self.assertEqual(data.get('livenessProbe'), None) 53 | self.assertEqual(data.get('readinessProbe'), readinessHealthCheck) 54 | 55 | data = {} 56 | self.scheduler.pod._set_container( 57 | 'foo', 'bar', data, routable=False, healthcheck={} 58 | ) 59 | self.assertEqual(data.get('livenessProbe'), None) 60 | self.assertEqual(data.get('readinessProbe'), None) 61 | 62 | # clear the dict to call again with routable as false 63 | data = {} 64 | self.scheduler.pod._set_container( 65 | 'foo', 'bar', data, 66 | routable=False, healthcheck=healthcheck 67 | ) 68 | self.assertDictContainsSubset(healthcheck, data) 69 | self.assertEqual(data.get('readinessProbe'), None) 70 | 71 | # now call without setting 'routable', should default to False 72 | data = {} 73 | self.scheduler.pod._set_container( 74 | 'foo', 'bar', data, healthcheck=healthcheck 75 | ) 76 | self.assertDictContainsSubset(healthcheck, data) 77 | self.assertEqual(data.get('readinessProbe'), None) 78 | 79 | data = {} 80 | livenessProbe = { 81 | 'livenessProbe': { 82 | 'httpGet': { 83 | 'port': None, 84 | } 85 | } 86 | } 87 | self.scheduler.pod._set_health_checks( 88 | data, {'PORT': 80}, healthcheck=livenessProbe 89 | ) 90 | self.assertDictContainsSubset(healthcheck, data) 91 | self.assertEqual(data.get('readinessProbe'), None) 92 | 93 | def test_set_container_limits(self): 94 | """ 95 | Test that when _set_container has limits that is sets them properly 96 | """ 97 | data = {} 98 | self.scheduler.pod._set_container( 99 | 'foo', 'bar', data, app_type='fake', 100 | cpu={'fake': '500M'}, memory={'fake': '1024m'} 101 | ) 102 | # make sure CPU gets lower cased 103 | self.assertEqual(data['resources']['limits']['cpu'], '500m', 'CPU should be lower cased') 104 | # make sure first char of Memory is upper cased 105 | self.assertEqual(data['resources']['limits']['memory'], '1024Mi', 'Memory should be upper cased') # noqa 106 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_kubehttpclient.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis scheduler module. 3 | 4 | Run the tests with "./manage.py test scheduler" 5 | """ 6 | import json 7 | import requests 8 | import requests_mock 9 | from unittest import mock 10 | 11 | from django.conf import settings 12 | from django.test import TestCase 13 | 14 | import scheduler 15 | from scheduler import exceptions 16 | 17 | 18 | def mock_session(): 19 | return requests.Session() 20 | 21 | 22 | def connection_refused_matcher(request): 23 | raise requests.ConnectionError("connection refused") 24 | 25 | 26 | @mock.patch('scheduler.get_session', mock_session) 27 | class KubeHTTPClientTest(TestCase): 28 | """Tests kubernetes HTTP client calls""" 29 | 30 | def setUp(self): 31 | self.adapter = requests_mock.Adapter() 32 | self.path = '/foo' 33 | self.url = settings.SCHEDULER_URL + self.path 34 | # use the real scheduler client. 35 | self.scheduler = scheduler.KubeHTTPClient(settings.SCHEDULER_URL) 36 | self.scheduler.session.mount(self.url, self.adapter) 37 | 38 | def test_head(self): 39 | """ 40 | Test that calling .http_head() uses the client session to make a HEAD request. 41 | """ 42 | self.adapter.register_uri('HEAD', self.url) 43 | response = self.scheduler.http_head(self.path) 44 | assert response is not None 45 | self.assertTrue(self.adapter.called) 46 | self.assertEqual(self.adapter.call_count, 1) 47 | # ensure that connection errors get raised as a KubeException 48 | self.adapter.add_matcher(connection_refused_matcher) 49 | with self.assertRaises(exceptions.KubeException): 50 | self.scheduler.http_head(self.path) 51 | 52 | def test_get(self): 53 | """ 54 | Test that calling .http_get() uses the client session to make a GET request. 55 | """ 56 | self.adapter.register_uri('GET', self.url) 57 | response = self.scheduler.http_get(self.path) 58 | assert response is not None 59 | self.assertTrue(self.adapter.called) 60 | self.assertEqual(self.adapter.call_count, 1) 61 | # ensure that connection errors get raised as a KubeException 62 | self.adapter.add_matcher(connection_refused_matcher) 63 | with self.assertRaises(exceptions.KubeException): 64 | self.scheduler.http_get(self.path) 65 | 66 | def test_post(self): 67 | """ 68 | Test that calling .http_post() uses the client session to make a POST request. 69 | """ 70 | self.adapter.register_uri('POST', self.url) 71 | response = self.scheduler.http_post(self.path, data=json.dumps({'hello': 'world'})) 72 | assert response is not None 73 | self.assertTrue(self.adapter.called) 74 | self.assertEqual(self.adapter.call_count, 1) 75 | # ensure that connection errors get raised as a KubeException 76 | self.adapter.add_matcher(connection_refused_matcher) 77 | with self.assertRaises(exceptions.KubeException): 78 | self.scheduler.http_post(self.path) 79 | 80 | def test_put(self): 81 | """ 82 | Test that calling .http_put() uses the client session to make a PUT request. 83 | """ 84 | self.adapter.register_uri('PUT', self.url) 85 | response = self.scheduler.http_put(self.path, data=json.dumps({'hello': 'world'})) 86 | assert response is not None 87 | self.assertTrue(self.adapter.called) 88 | self.assertEqual(self.adapter.call_count, 1) 89 | # ensure that connection errors get raised as a KubeException 90 | self.adapter.add_matcher(connection_refused_matcher) 91 | with self.assertRaises(exceptions.KubeException): 92 | self.scheduler.http_put(self.path) 93 | 94 | def test_delete(self): 95 | """ 96 | Test that calling .http_delete() uses the client session to make a DELETE request. 97 | """ 98 | self.adapter.register_uri('DELETE', self.url) 99 | response = self.scheduler.http_delete(self.path) 100 | assert response is not None 101 | self.assertTrue(self.adapter.called) 102 | self.assertEqual(self.adapter.call_count, 1) 103 | # ensure that connection errors get raised as a KubeException 104 | self.adapter.add_matcher(connection_refused_matcher) 105 | with self.assertRaises(exceptions.KubeException): 106 | self.scheduler.http_delete(self.path) 107 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/secret.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | from scheduler.resources import Resource 4 | from scheduler.exceptions import KubeHTTPException, KubeException 5 | 6 | 7 | class Secret(Resource): 8 | def get(self, namespace, name=None, **kwargs): 9 | """ 10 | Fetch a single Secret or a list 11 | """ 12 | url = '/namespaces/{}/secrets' 13 | args = [namespace] 14 | if name is not None: 15 | args.append(name) 16 | url += '/{}' 17 | message = 'get Secret "{}" in Namespace "{}"' 18 | else: 19 | message = 'get Secrets in Namespace "{}"' 20 | 21 | url = self.api(url, *args) 22 | response = self.http_get(url, params=self.query_params(**kwargs)) 23 | if self.unhealthy(response.status_code): 24 | args.reverse() # error msg is in reverse order 25 | raise KubeHTTPException(response, message, *args) 26 | 27 | # return right away if it is a list 28 | if name is None: 29 | return response 30 | 31 | # decode the base64 data 32 | secrets = response.json() 33 | for key, value in secrets['data'].items(): 34 | if value is None: 35 | secrets['data'][key] = '' 36 | continue 37 | 38 | value = base64.b64decode(value) 39 | value = value if isinstance(value, bytes) else bytes(str(value), 'UTF-8') 40 | secrets['data'][key] = value.decode(encoding='UTF-8') 41 | 42 | # tell python-requests it actually hasn't consumed the data 43 | response._content = bytes(json.dumps(secrets), 'UTF-8') 44 | 45 | return response 46 | 47 | def manifest(self, namespace, name, data, secret_type='Opaque', labels={}): 48 | secret_types = ['Opaque', 'kubernetes.io/dockerconfigjson'] 49 | if secret_type not in secret_types: 50 | raise KubeException('{} is not a supported secret type. Use one of the following: '.format(secret_type, ', '.join(secret_types))) # noqa 51 | 52 | manifest = { 53 | 'kind': 'Secret', 54 | 'apiVersion': 'v1', 55 | 'metadata': { 56 | 'name': name, 57 | 'namespace': namespace, 58 | 'labels': { 59 | 'app': namespace, 60 | 'heritage': 'deis' 61 | } 62 | }, 63 | 'type': secret_type, 64 | 'data': {} 65 | } 66 | 67 | # add in any additional label info 68 | manifest['metadata']['labels'].update(labels) 69 | 70 | for key, value in data.items(): 71 | if value is None: 72 | manifest['data'].update({key: ''}) 73 | continue 74 | 75 | value = value if isinstance(value, bytes) else bytes(str(value), 'UTF-8') 76 | item = base64.b64encode(value).decode(encoding='UTF-8') 77 | manifest['data'].update({key: item}) 78 | 79 | return manifest 80 | 81 | def create(self, namespace, name, data, secret_type='Opaque', labels={}): 82 | manifest = self.manifest(namespace, name, data, secret_type, labels) 83 | url = self.api("/namespaces/{}/secrets", namespace) 84 | response = self.http_post(url, json=manifest) 85 | if self.unhealthy(response.status_code): 86 | raise KubeHTTPException( 87 | response, 88 | 'failed to create Secret "{}" in Namespace "{}"', name, namespace 89 | ) 90 | 91 | return response 92 | 93 | def update(self, namespace, name, data, secret_type='Opaque', labels={}): 94 | manifest = self.manifest(namespace, name, data, secret_type, labels) 95 | url = self.api("/namespaces/{}/secrets/{}", namespace, name) 96 | response = self.http_put(url, json=manifest) 97 | if self.unhealthy(response.status_code): 98 | raise KubeHTTPException( 99 | response, 100 | 'failed to update Secret "{}" in Namespace "{}"', 101 | name, namespace 102 | ) 103 | 104 | return response 105 | 106 | def delete(self, namespace, name): 107 | url = self.api("/namespaces/{}/secrets/{}", namespace, name) 108 | response = self.http_delete(url) 109 | if self.unhealthy(response.status_code): 110 | raise KubeHTTPException( 111 | response, 112 | 'delete Secret "{}" in Namespace "{}"', name, namespace 113 | ) 114 | 115 | return response 116 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_services.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis scheduler module. 3 | 4 | Run the tests with './manage.py test scheduler' 5 | """ 6 | from scheduler import KubeHTTPException 7 | from scheduler.tests import TestCase 8 | from scheduler.utils import generate_random_name 9 | 10 | 11 | class ServicesTest(TestCase): 12 | """Tests scheduler service calls""" 13 | 14 | def create(self, data={}): 15 | """ 16 | Helper function to create and verify a service on the namespace 17 | """ 18 | name = generate_random_name() 19 | service = self.scheduler.svc.create(self.namespace, name, data=data) 20 | data = service.json() 21 | self.assertEqual(service.status_code, 201, data) 22 | self.assertEqual(data['metadata']['name'], name) 23 | return name 24 | 25 | def test_create_failure(self): 26 | # Kubernetes does not throw a 404 if queried on a non-existant Namespace 27 | with self.assertRaises( 28 | KubeHTTPException, 29 | msg='failed to create Service doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 30 | ): 31 | self.scheduler.svc.create('doesnotexist', 'doesnotexist') 32 | 33 | def test_create(self): 34 | # helper method takes care of the verification 35 | self.create() 36 | 37 | # create with more ports 38 | name = self.create(data={ 39 | 'spec': { 40 | 'ports': [{ 41 | 'name': 'http', 42 | 'port': 80, 43 | 'targetPort': 5001, 44 | 'protocol': 'TCP' 45 | }], 46 | } 47 | }) 48 | 49 | service = self.scheduler.svc.get(self.namespace, name).json() 50 | self.assertEqual(service['spec']['ports'][0]['targetPort'], 5000, service) 51 | self.assertEqual(service['spec']['ports'][1]['targetPort'], 5001, service) 52 | 53 | def test_update_failure(self): 54 | # test failure 55 | with self.assertRaises( 56 | KubeHTTPException, 57 | msg='failed to update Service foo in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 58 | ): 59 | self.scheduler.svc.update(self.namespace, 'foo', {}) 60 | 61 | def test_update(self): 62 | # test success 63 | name = self.create() 64 | service = self.scheduler.svc.get(self.namespace, name).json() 65 | self.assertEqual(service['spec']['ports'][0]['targetPort'], 5000, service) 66 | 67 | service['spec']['ports'][0]['targetPort'] = 5001 68 | response = self.scheduler.svc.update(self.namespace, name, service) 69 | self.assertEqual(response.status_code, 200, response.json()) 70 | 71 | service = self.scheduler.svc.get(self.namespace, name).json() 72 | self.assertEqual(service['spec']['ports'][0]['targetPort'], 5001, service) 73 | 74 | def test_delete_failure(self): 75 | # test failure 76 | with self.assertRaises( 77 | KubeHTTPException, 78 | msg='failed to delete Service foo in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 79 | ): 80 | self.scheduler.svc.delete(self.namespace, 'foo') 81 | 82 | def test_delete(self): 83 | # test success 84 | name = self.create() 85 | response = self.scheduler.svc.delete(self.namespace, name) 86 | data = response.json() 87 | self.assertEqual(response.status_code, 200, data) 88 | 89 | def test_get_services(self): 90 | # test success 91 | name = self.create() 92 | response = self.scheduler.svc.get(self.namespace) 93 | data = response.json() 94 | self.assertEqual(response.status_code, 200, data) 95 | self.assertIn('items', data) 96 | self.assertEqual(1, len(data['items']), data['items']) 97 | # simple verify of data 98 | self.assertEqual(data['items'][0]['metadata']['name'], name) 99 | 100 | def test_get_service_failure(self): 101 | # test failure 102 | with self.assertRaises( 103 | KubeHTTPException, 104 | msg='failed to get Service doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 105 | ): 106 | self.scheduler.svc.get(self.namespace, 'doesnotexist') 107 | 108 | def test_get_service(self): 109 | # test success 110 | name = self.create() 111 | response = self.scheduler.svc.get(self.namespace, name) 112 | data = response.json() 113 | self.assertEqual(response.status_code, 200, data) 114 | self.assertEqual(data['apiVersion'], 'v1') 115 | self.assertEqual(data['kind'], 'Service') 116 | self.assertDictContainsSubset( 117 | { 118 | 'name': name, 119 | 'labels': { 120 | 'app': self.namespace, 121 | 'heritage': 'deis' 122 | } 123 | }, 124 | data['metadata'] 125 | ) 126 | self.assertEqual(data['spec']['ports'][0]['targetPort'], 5000) 127 | -------------------------------------------------------------------------------- /rootfs/api/permissions.py: -------------------------------------------------------------------------------- 1 | 2 | from rest_framework import exceptions 3 | from rest_framework import permissions 4 | from django.conf import settings 5 | from django.contrib.auth.models import AnonymousUser, User 6 | 7 | from api import models 8 | 9 | 10 | def is_app_user(request, obj): 11 | if request.user.is_superuser or \ 12 | isinstance(obj, models.App) and obj.owner == request.user or \ 13 | hasattr(obj, 'app') and obj.app.owner == request.user: 14 | return True 15 | elif request.user.has_perm('use_app', obj) or \ 16 | hasattr(obj, 'app') and request.user.has_perm('use_app', obj.app): 17 | return request.method != 'DELETE' 18 | else: 19 | return False 20 | 21 | 22 | class IsAnonymous(permissions.BasePermission): 23 | """ 24 | View permission to allow anonymous users. 25 | """ 26 | 27 | def has_permission(self, request, view): 28 | """ 29 | Return `True` if permission is granted, `False` otherwise. 30 | """ 31 | return type(request.user) is AnonymousUser 32 | 33 | 34 | class IsOwner(permissions.BasePermission): 35 | """ 36 | Object-level permission to allow only owners of an object to access it. 37 | Assumes the model instance has an `owner` attribute. 38 | """ 39 | 40 | def has_object_permission(self, request, view, obj): 41 | if hasattr(obj, 'owner'): 42 | return obj.owner == request.user 43 | else: 44 | return False 45 | 46 | 47 | class IsOwnerOrAdmin(permissions.BasePermission): 48 | """ 49 | Object-level permission to allow only owners of an object or administrators to access it. 50 | Assumes the model instance has an `owner` attribute. 51 | """ 52 | def has_object_permission(self, request, view, obj): 53 | if request.user.is_superuser: 54 | return True 55 | if hasattr(obj, 'owner'): 56 | return obj.owner == request.user 57 | else: 58 | return False 59 | 60 | 61 | class IsAppUser(permissions.BasePermission): 62 | """ 63 | Object-level permission to allow owners or collaborators to access 64 | an app-related model. 65 | """ 66 | def has_object_permission(self, request, view, obj): 67 | return is_app_user(request, obj) 68 | 69 | 70 | class IsAdmin(permissions.BasePermission): 71 | """ 72 | View permission to allow only admins. 73 | """ 74 | 75 | def has_permission(self, request, view): 76 | """ 77 | Return `True` if permission is granted, `False` otherwise. 78 | """ 79 | return request.user.is_superuser 80 | 81 | 82 | class IsAdminOrSafeMethod(permissions.BasePermission): 83 | """ 84 | View permission to allow only admins to use unsafe methods 85 | including POST, PUT, DELETE. 86 | 87 | This allows 88 | """ 89 | 90 | def has_permission(self, request, view): 91 | """ 92 | Return `True` if permission is granted, `False` otherwise. 93 | """ 94 | return request.method in permissions.SAFE_METHODS or request.user.is_superuser 95 | 96 | 97 | class HasRegistrationAuth(permissions.BasePermission): 98 | """ 99 | Checks to see if registration is enabled 100 | """ 101 | def has_permission(self, request, view): 102 | """ 103 | If settings.REGISTRATION_MODE does not exist, such as during a test, return True 104 | Return `True` if permission is granted, `False` otherwise. 105 | """ 106 | try: 107 | if settings.REGISTRATION_MODE == 'disabled': 108 | raise exceptions.PermissionDenied('Registration is disabled') 109 | if settings.REGISTRATION_MODE == 'enabled': 110 | return True 111 | elif settings.REGISTRATION_MODE == 'admin_only': 112 | if not User.objects.filter(is_superuser=True).exists(): 113 | return True 114 | return request.user.is_superuser 115 | else: 116 | raise Exception("{} is not a valid registation mode" 117 | .format(settings.REGISTRATION_MODE)) 118 | except AttributeError: 119 | return True 120 | 121 | 122 | class HasBuilderAuth(permissions.BasePermission): 123 | """ 124 | View permission to allow builder to perform actions 125 | with a special HTTP header 126 | """ 127 | 128 | def has_permission(self, request, view): 129 | """ 130 | Return `True` if permission is granted, `False` otherwise. 131 | """ 132 | auth_header = request.environ.get('HTTP_X_DEIS_BUILDER_AUTH') 133 | if not auth_header: 134 | return False 135 | return auth_header == settings.BUILDER_KEY 136 | 137 | 138 | class CanRegenerateToken(permissions.BasePermission): 139 | """ 140 | Checks if a user can regenerate a token 141 | """ 142 | 143 | def has_permission(self, request, view): 144 | """ 145 | Return `True` if permission is granted, `False` otherwise. 146 | """ 147 | if 'username' in request.data or 'all' in request.data: 148 | return request.user.is_superuser 149 | else: 150 | return True 151 | -------------------------------------------------------------------------------- /rootfs/api/models/build.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from jsonfield import JSONField 4 | 5 | from api.models import UuidAuditedModel 6 | from api.exceptions import DeisException, Conflict 7 | 8 | import logging 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Build(UuidAuditedModel): 13 | """ 14 | Instance of a software build used by runtime nodes 15 | """ 16 | 17 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) 18 | app = models.ForeignKey('App', on_delete=models.CASCADE) 19 | image = models.TextField() 20 | 21 | # optional fields populated by builder 22 | sha = models.CharField(max_length=40, blank=True) 23 | procfile = JSONField(default={}, blank=True) 24 | dockerfile = models.TextField(blank=True) 25 | 26 | class Meta: 27 | get_latest_by = 'created' 28 | ordering = ['-created'] 29 | unique_together = (('app', 'uuid'),) 30 | 31 | @property 32 | def type(self): 33 | """Figures out what kind of build type is being deal it with""" 34 | if self.dockerfile: 35 | return 'dockerfile' 36 | elif self.sha: 37 | return 'buildpack' 38 | else: 39 | # docker image (or any sort of image) used via deis pull 40 | return 'image' 41 | 42 | @property 43 | def source_based(self): 44 | """ 45 | Checks if a build is source (has a sha) based or not 46 | If True then the Build is coming from the deis builder or something that 47 | built from git / svn / hg / etc directly 48 | """ 49 | return self.sha != '' 50 | 51 | @property 52 | def version(self): 53 | return 'git-{}'.format(self.sha) if self.source_based else 'latest' 54 | 55 | def create(self, user, *args, **kwargs): 56 | latest_release = self.app.release_set.filter(failed=False).latest() 57 | latest_version = self.app.release_set.latest().version 58 | try: 59 | new_release = latest_release.new( 60 | user, 61 | build=self, 62 | config=latest_release.config, 63 | source_version=self.version 64 | ) 65 | self.app.deploy(new_release) 66 | return new_release 67 | except Exception as e: 68 | # check if the exception is during create or publish 69 | if ('new_release' not in locals() and 70 | self.app.release_set.latest().version == latest_version+1): 71 | new_release = self.app.release_set.latest() 72 | if 'new_release' in locals(): 73 | new_release.failed = True 74 | new_release.summary = "{} deployed {} which failed".format(self.owner, str(self.uuid)[:7]) # noqa 75 | new_release.save() 76 | else: 77 | self.delete() 78 | 79 | raise DeisException(str(e)) from e 80 | 81 | def save(self, **kwargs): 82 | previous_release = self.app.release_set.filter(failed=False).latest() 83 | 84 | if ( 85 | settings.DEIS_DEPLOY_REJECT_IF_PROCFILE_MISSING is True and 86 | # previous release had a Procfile and the current one does not 87 | ( 88 | previous_release.build is not None and 89 | len(previous_release.build.procfile) > 0 and 90 | len(self.procfile) == 0 91 | ) 92 | ): 93 | # Reject deployment 94 | raise Conflict( 95 | 'Last deployment had a Procfile but is missing in this deploy. ' 96 | 'For a successful deployment provide a Procfile.' 97 | ) 98 | 99 | # See if processes are permitted to be removed 100 | remove_procs = ( 101 | # If set to True then contents of Procfile does not affect the outcome 102 | settings.DEIS_DEPLOY_PROCFILE_MISSING_REMOVE is True or 103 | # previous release had a Procfile and the current one does as well 104 | ( 105 | previous_release.build is not None and 106 | len(previous_release.build.procfile) > 0 and 107 | len(self.procfile) > 0 108 | ) 109 | ) 110 | 111 | # spin down any proc type removed between the last procfile and the newest one 112 | if remove_procs and previous_release.build is not None: 113 | removed = {} 114 | for proc in previous_release.build.procfile: 115 | if proc not in self.procfile: 116 | # Scale proc type down to 0 117 | removed[proc] = 0 118 | 119 | self.app.scale(self.owner, removed) 120 | 121 | # make sure the latest build has procfile if the intent is to 122 | # allow empty Procfile without removals 123 | if ( 124 | settings.DEIS_DEPLOY_PROCFILE_MISSING_REMOVE is False and 125 | previous_release.build is not None and 126 | len(previous_release.build.procfile) > 0 and 127 | len(self.procfile) == 0 128 | ): 129 | self.procfile = previous_release.build.procfile 130 | 131 | return super(Build, self).save(**kwargs) 132 | 133 | def __str__(self): 134 | return "{0}-{1}".format(self.app.id, str(self.uuid)[:7]) 135 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_secrets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis scheduler module. 3 | 4 | Run the tests with './manage.py test scheduler' 5 | """ 6 | from scheduler import KubeHTTPException, KubeException 7 | from scheduler.tests import TestCase 8 | from scheduler.utils import generate_random_name 9 | 10 | 11 | class SecretsTest(TestCase): 12 | """Tests scheduler secret calls""" 13 | 14 | def create(self): 15 | """ 16 | Helper function to create and verify a secret on the namespace 17 | """ 18 | name = generate_random_name() 19 | data = { 20 | 'foo': 'bar', 21 | 'this': 'that', 22 | 'empty': None, 23 | } 24 | secret = self.scheduler.secret.create(self.namespace, name, data) 25 | data = secret.json() 26 | self.assertEqual(secret.status_code, 201, data) 27 | self.assertEqual(data['metadata']['name'], name) 28 | self.assertIn('foo', data['data']) 29 | self.assertIn('this', data['data']) 30 | return name 31 | 32 | def test_create_failure(self): 33 | with self.assertRaises( 34 | KubeHTTPException, 35 | msg='failed to create Secret doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 36 | ): 37 | self.scheduler.secret.create('doesnotexist', 'doesnotexist', {}) 38 | 39 | with self.assertRaises( 40 | KubeException, 41 | msg='invlaid is not a supported secret type. Use one of the following: Opaque, kubernetes.io/dockerconfigjson' # noqa 42 | ): 43 | self.scheduler.secret.create(self.namespace, 'foo', {}, secret_type='invalid') 44 | 45 | def test_create(self): 46 | name = self.create() 47 | secret = self.scheduler.secret.get(self.namespace, name).json() 48 | self.assertEqual(secret['data']['foo'], 'bar', secret) 49 | self.assertEqual(secret['data']['this'], 'that', secret) 50 | self.assertEqual(secret['type'], 'Opaque') 51 | 52 | def test_update_secret_failure(self): 53 | # test failure 54 | with self.assertRaises( 55 | KubeHTTPException, 56 | msg='failed to update Secret foo in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 57 | ): 58 | self.scheduler.secret.update(self.namespace, 'foo', {}) 59 | 60 | def test_update(self): 61 | # test success 62 | name = self.create() 63 | secret = self.scheduler.secret.get(self.namespace, name).json() 64 | self.assertEqual(secret['data']['foo'], 'bar', secret) 65 | self.assertEqual(secret['data']['this'], 'that', secret) 66 | self.assertEqual(secret['type'], 'Opaque') 67 | 68 | secret['data']['foo'] = 5001 69 | response = self.scheduler.secret.update(self.namespace, name, secret['data']) 70 | self.assertEqual(response.status_code, 200, response.json()) 71 | 72 | secret = self.scheduler.secret.get(self.namespace, name).json() 73 | self.assertEqual(secret['data']['foo'], '5001', secret) 74 | 75 | def test_delete_failure(self): 76 | # test failure 77 | with self.assertRaises( 78 | KubeHTTPException, 79 | msg='failed to delete Secret foo in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 80 | ): 81 | self.scheduler.secret.delete(self.namespace, 'foo') 82 | 83 | def test_delete(self): 84 | # test success 85 | name = self.create() 86 | response = self.scheduler.secret.delete(self.namespace, name) 87 | data = response.json() 88 | self.assertEqual(response.status_code, 200, data) 89 | 90 | def test_get_secrets(self): 91 | # test success 92 | name = self.create() 93 | response = self.scheduler.secret.get(self.namespace) 94 | data = response.json() 95 | self.assertEqual(response.status_code, 200, data) 96 | self.assertIn('items', data) 97 | self.assertEqual(1, len(data['items']), data['items']) 98 | # simple verify of data 99 | self.assertEqual(data['items'][0]['metadata']['name'], name) 100 | 101 | def test_get_secret_failure(self): 102 | # test failure 103 | with self.assertRaises( 104 | KubeHTTPException, 105 | msg='failed to get Secret doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 106 | ): 107 | self.scheduler.secret.get(self.namespace, 'doesnotexist') 108 | 109 | def test_get_secret(self): 110 | # test success 111 | name = self.create() 112 | response = self.scheduler.secret.get(self.namespace, name) 113 | data = response.json() 114 | self.assertEqual(response.status_code, 200, data) 115 | self.assertEqual(data['apiVersion'], 'v1') 116 | self.assertEqual(data['kind'], 'Secret') 117 | self.assertDictContainsSubset( 118 | { 119 | 'name': name, 120 | 'labels': { 121 | 'app': self.namespace, 122 | 'heritage': 'deis' 123 | } 124 | }, 125 | data['metadata'] 126 | ) 127 | self.assertEqual(data['data']['foo'], 'bar', data) 128 | self.assertEqual(data['data']['this'], 'that', data) 129 | self.assertEqual(data['type'], 'Opaque') 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | |![](https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Warning.svg/156px-Warning.svg.png) | Deis Workflow is no longer maintained.
Please [read the announcement](https://deis.com/blog/2017/deis-workflow-final-release/) for more detail. | 3 | |---:|---| 4 | | 09/07/2017 | Deis Workflow [v2.18][] final release before entering maintenance mode | 5 | | 03/01/2018 | End of Workflow maintenance: critical patches no longer merged | 6 | | | [Hephy](https://github.com/teamhephy/workflow) is a fork of Workflow that is actively developed and accepts code contributions. | 7 | 8 | # Deis Controller 9 | 10 | [![Build Status](https://ci.deis.io/job/controller/badge/icon)](https://ci.deis.io/job/controller) 11 | [![codecov.io](https://codecov.io/github/deis/controller/coverage.svg?branch=master)](https://codecov.io/github/deis/controller?branch=master) 12 | [![Docker Repository on Quay](https://quay.io/repository/deisci/controller/status "Docker Repository on Quay")](https://quay.io/repository/deisci/controller) 13 | [![Dependency Status](https://www.versioneye.com/user/projects/5863f1de6f4bf900128fa95a/badge.svg?style=flat)](https://www.versioneye.com/user/projects/5863f1de6f4bf900128fa95a) 14 | 15 | Deis (pronounced DAY-iss) Workflow is an open source Platform as a Service (PaaS) that adds a developer-friendly layer to any [Kubernetes](http://kubernetes.io) cluster, making it easy to deploy and manage applications on your own servers. 16 | 17 | For more information about the Deis Workflow, please visit the main project page at https://github.com/deis/workflow. 18 | 19 | We welcome your input! If you have feedback, please [submit an issue][issues]. If you'd like to participate in development, please read the "Development" section below and [submit a pull request][prs]. 20 | 21 | # About 22 | 23 | The Controller is the central API server for [Deis Workflow][workflow]. It is installed on a [Kubernetes](http://kubernetes.io) cluster, making it easy to deploy and manage applications on your own cluster. Below is a non-exhaustive list of things it can do: 24 | 25 | * Create a new application 26 | * Delete an application 27 | * Scale an application 28 | * Configure an application 29 | * Create a new user 30 | 31 | # Development 32 | 33 | The Deis project welcomes contributions from all developers. The high-level process for development matches many other open source projects. See below for an outline. 34 | 35 | * Fork this repository 36 | * Make your changes 37 | * [Submit a pull request][prs] (PR) to this repository with your changes, and unit tests whenever possible. 38 | * If your PR fixes any [issues][issues], make sure you write Fixes #1234 in your PR description (where #1234 is the number of the issue you're closing) 39 | * Deis project maintainers will review your code. 40 | * After two maintainers approve it, they will merge your PR. 41 | 42 | ## Prerequisites 43 | 44 | ### Docker 45 | 46 | Unit tests and code linters for controller run in a Docker container with your local code directory 47 | mounted in. You need [Docker][] to run `make test`. 48 | 49 | ### Kubernetes 50 | 51 | You'll want to test your code changes interactively in a working Kubernetes cluster. Follow the 52 | [installation instructions][install-k8s] if you need Kubernetes. 53 | 54 | ### Workflow Installation 55 | 56 | After you have a working Kubernetes cluster, you're ready to [install Workflow](https://deis.com/docs/workflow/installing-workflow/). 57 | 58 | ## Testing Your Code 59 | 60 | When you've built your new feature or fixed a bug, make sure you've added appropriate unit tests and run `make test` to ensure your code works properly. 61 | 62 | Also, since this component is central to the platform, it's recommended that you manually test and verify that your feature or fix works as expected. To do so, ensure the following environment variables are set: 63 | 64 | * `DEIS_REGISTRY` - A Docker registry that you have push access to and your Kubernetes cluster can pull from 65 | * If this is [Docker Hub](https://hub.docker.com/), leave this variable empty 66 | * Otherwise, ensure it has a trailing `/`. For example, if you're using [Quay.io](https://quay.io), use `quay.io/` 67 | * `IMAGE_PREFIX` - The organization in the Docker repository. This defaults to `deis`, but if you don't have access to that organization, set this to one you have push access to. 68 | * `SHORT_NAME` (optional) - The name of the image. This defaults to `controller` 69 | * `VERSION` (optional) - The tag of the Docker image. This defaults to the current Git SHA (the output of `git rev-parse --short HEAD`) 70 | 71 | Then, run `make deploy` to build and push a new Docker image with your changes and replace the existing one with your new one in the Kubernetes cluster. See below for an example with appropriate environment variables. 72 | 73 | ```console 74 | export DEIS_REGISTRY=quay.io/ 75 | export IMAGE_PREFIX=arschles 76 | make deploy 77 | ``` 78 | 79 | After the `make deploy` finishes, a new pod will be launched but may not be running. You'll need to wait until the pod is listed as `Running` and the value in its `Ready` column is `1/1`. Use the following command watch the pod's status: 80 | 81 | ```console 82 | kubectl get pod --namespace=deis -w | grep deis-controller 83 | ``` 84 | 85 | [install-k8s]: https://kubernetes.io/docs/setup/pick-right-solution 86 | [issues]: https://github.com/deis/controller/issues 87 | [prs]: https://github.com/deis/controller/pulls 88 | [workflow]: https://github.com/deis/workflow 89 | [Docker]: https://www.docker.com/ 90 | [v2.18]: https://github.com/deis/workflow/releases/tag/v2.18.0 91 | -------------------------------------------------------------------------------- /charts/controller/templates/controller-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: deis-controller 5 | labels: 6 | heritage: deis 7 | annotations: 8 | component.deis.io/version: {{ .Values.docker_tag }} 9 | spec: 10 | replicas: 1 11 | strategy: 12 | rollingUpdate: 13 | maxSurge: 1 14 | maxUnavailable: 0 15 | type: RollingUpdate 16 | selector: 17 | matchLabels: 18 | app: deis-controller 19 | template: 20 | metadata: 21 | labels: 22 | app: deis-controller 23 | spec: 24 | serviceAccount: deis-controller 25 | containers: 26 | - name: deis-controller 27 | image: quay.io/{{.Values.org}}/controller:{{.Values.docker_tag}} 28 | imagePullPolicy: {{.Values.pull_policy}} 29 | livenessProbe: 30 | httpGet: 31 | path: /healthz 32 | port: 8000 33 | initialDelaySeconds: 30 34 | timeoutSeconds: 10 35 | readinessProbe: 36 | httpGet: 37 | path: /readiness 38 | port: 8000 39 | initialDelaySeconds: 30 40 | timeoutSeconds: 10 41 | periodSeconds: 5 42 | ports: 43 | - containerPort: 8000 44 | name: http 45 | {{- if or (.Values.limits_cpu) (.Values.limits_memory) }} 46 | resources: 47 | limits: 48 | {{- if (.Values.limits_cpu) }} 49 | cpu: {{.Values.limits_cpu}} 50 | {{- end }} 51 | {{- if (.Values.limits_memory) }} 52 | memory: {{.Values.limits_memory}} 53 | {{- end }} 54 | {{- end }} 55 | env: 56 | - name: REGISTRATION_MODE 57 | value: {{ .Values.registration_mode }} 58 | # NOTE(bacongobbler): use deis/registry_proxy to work around Docker --insecure-registry requirements 59 | - name: "DEIS_REGISTRY_SERVICE_HOST" 60 | value: "127.0.0.1" 61 | # Environmental variable value for $EXPERIMENTAL_NATIVE_INGRESS 62 | - name: "EXPERIMENTAL_NATIVE_INGRESS" 63 | value: "{{ .Values.global.experimental_native_ingress }}" 64 | - name: "EXPERIMENTAL_NATIVE_INGRESS_HOSTNAME" 65 | value: "{{ .Values.platform_domain }}" 66 | - name: "K8S_API_VERIFY_TLS" 67 | value: "{{ .Values.k8s_api_verify_tls }}" 68 | - name: "DEIS_REGISTRY_SERVICE_PORT" 69 | value: "{{ .Values.global.host_port }}" 70 | - name: "APP_STORAGE" 71 | value: "{{ .Values.global.storage}}" 72 | - name: "DEIS_REGISTRY_LOCATION" 73 | value: "{{ .Values.global.registry_location }}" 74 | - name: "DEIS_REGISTRY_SECRET_PREFIX" 75 | value: "{{ .Values.global.secret_prefix }}" 76 | - name: "SLUGRUNNER_IMAGE_NAME" 77 | valueFrom: 78 | configMapKeyRef: 79 | name: slugrunner-config 80 | key: image 81 | - name: "IMAGE_PULL_POLICY" 82 | value: "{{ .Values.app_pull_policy }}" 83 | - name: "TZ" 84 | value: {{ .Values.time_zone | default "UTC" | quote }} 85 | {{- if (.Values.deploy_hook_urls) }} 86 | - name: DEIS_DEPLOY_HOOK_URLS 87 | value: "{{ .Values.deploy_hook_urls }}" 88 | - name: DEIS_DEPLOY_HOOK_SECRET_KEY 89 | valueFrom: 90 | secretKeyRef: 91 | name: deploy-hook-key 92 | key: secret-key 93 | {{- end }} 94 | - name: DEIS_SECRET_KEY 95 | valueFrom: 96 | secretKeyRef: 97 | name: django-secret-key 98 | key: secret-key 99 | - name: DEIS_BUILDER_KEY 100 | valueFrom: 101 | secretKeyRef: 102 | name: builder-key-auth 103 | key: builder-key 104 | {{- if eq .Values.global.database_location "off-cluster" }} 105 | - name: DEIS_DATABASE_NAME 106 | valueFrom: 107 | secretKeyRef: 108 | name: database-creds 109 | key: name 110 | - name: DEIS_DATABASE_SERVICE_HOST 111 | valueFrom: 112 | secretKeyRef: 113 | name: database-creds 114 | key: host 115 | - name: DEIS_DATABASE_SERVICE_PORT 116 | valueFrom: 117 | secretKeyRef: 118 | name: database-creds 119 | key: port 120 | {{- end }} 121 | - name: DEIS_DATABASE_USER 122 | valueFrom: 123 | secretKeyRef: 124 | name: database-creds 125 | key: user 126 | - name: DEIS_DATABASE_PASSWORD 127 | valueFrom: 128 | secretKeyRef: 129 | name: database-creds 130 | key: password 131 | - name: RESERVED_NAMES 132 | value: "deis, deis-builder, deis-workflow-manager, grafana" 133 | - name: WORKFLOW_NAMESPACE 134 | valueFrom: 135 | fieldRef: 136 | fieldPath: metadata.namespace 137 | volumeMounts: 138 | - mountPath: /var/run/docker.sock 139 | name: docker-socket 140 | volumes: 141 | - name: docker-socket 142 | hostPath: 143 | path: /var/run/docker.sock 144 | -------------------------------------------------------------------------------- /rootfs/scheduler/tests/test_replicationcontrollers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for the Deis scheduler module. 3 | 4 | Run the tests with './manage.py test scheduler' 5 | """ 6 | from scheduler import KubeHTTPException 7 | from scheduler.tests import TestCase 8 | from scheduler.utils import generate_random_name 9 | 10 | 11 | class ReplicationControllersTest(TestCase): 12 | """Tests scheduler rc calls""" 13 | 14 | def create(self, namespace=None, name=generate_random_name(), **kwargs): 15 | """ 16 | Helper function to create and verify a rc on the namespace 17 | """ 18 | namespace = self.namespace if namespace is None else namespace 19 | # these are all required even if it is kwargs... 20 | kwargs = { 21 | 'app_type': kwargs.get('app_type', 'web'), 22 | 'version': kwargs.get('version', 'v99'), 23 | 'replicas': kwargs.get('replicas', 4), 24 | 'pod_termination_grace_period_seconds': 2, 25 | 'image': 'quay.io/fake/image', 26 | 'entrypoint': 'sh', 27 | 'command': 'start', 28 | } 29 | 30 | rc = self.scheduler.rc.create(namespace, name, **kwargs) 31 | data = rc.json() 32 | self.assertEqual(rc.status_code, 201, data) 33 | return name 34 | 35 | def scale_rc(self, namespace=None, name=generate_random_name(), **kwargs): 36 | """ 37 | Helper function to scale and verify a deployment on the namespace 38 | """ 39 | namespace = self.namespace if namespace is None else namespace 40 | # these are all required even if it is kwargs... 41 | kwargs = { 42 | 'app_type': kwargs.get('app_type', 'web'), 43 | 'version': kwargs.get('version', 'v99'), 44 | 'replicas': kwargs.get('replicas', 4), 45 | 'deploy_timeout': 120, 46 | 'pod_termination_grace_period_seconds': 2, 47 | 'image': 'quay.io/fake/image', 48 | 'entrypoint': 'sh', 49 | 'command': 'start', 50 | } 51 | 52 | self.scheduler.scale_rc(namespace, name, **kwargs) 53 | return name 54 | 55 | def test_create_failure(self): 56 | with self.assertRaises( 57 | KubeHTTPException, 58 | msg='failed to create ReplicationController doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 59 | ): 60 | self.create('doesnotexist', 'doesnotexist') 61 | 62 | def test_create(self): 63 | self.create() 64 | 65 | def test_update_rc_failure(self): 66 | # test failure 67 | with self.assertRaises( 68 | KubeHTTPException, 69 | msg='failed to update ReplicationController foo in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 70 | ): 71 | self.scheduler.rc.update(self.namespace, 'foo', {}) 72 | 73 | def test_update(self): 74 | # test success 75 | name = self.create() 76 | rc = self.scheduler.rc.get(self.namespace, name).json() 77 | self.assertEqual(rc['spec']['replicas'], 4, rc) 78 | 79 | rc['spec']['replicas'] = 2 80 | response = self.scheduler.rc.update(self.namespace, name, rc) 81 | self.assertEqual(response.status_code, 200, response.json()) 82 | 83 | rc = self.scheduler.rc.get(self.namespace, name).json() 84 | self.assertEqual(rc['spec']['replicas'], 2, rc) 85 | 86 | def test_delete_failure(self): 87 | # test failure 88 | with self.assertRaises( 89 | KubeHTTPException, 90 | msg='failed to delete ReplicationController foo in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 91 | ): 92 | self.scheduler.rc.delete(self.namespace, 'foo') 93 | 94 | def test_delete(self): 95 | # test success 96 | name = self.create() 97 | response = self.scheduler.rc.delete(self.namespace, name) 98 | data = response.json() 99 | self.assertEqual(response.status_code, 200, data) 100 | 101 | def test_get_rcs(self): 102 | # test success 103 | name = self.create() 104 | response = self.scheduler.rc.get(self.namespace) 105 | data = response.json() 106 | self.assertEqual(response.status_code, 200, data) 107 | self.assertIn('items', data) 108 | self.assertEqual(1, len(data['items']), data['items']) 109 | # simple verify of data 110 | self.assertEqual(data['items'][0]['metadata']['name'], name) 111 | 112 | def test_get_rc_failure(self): 113 | # test failure 114 | with self.assertRaises( 115 | KubeHTTPException, 116 | msg='failed to get ReplicationController doesnotexist in Namespace {}: 404 Not Found'.format(self.namespace) # noqa 117 | ): 118 | self.scheduler.rc.get(self.namespace, 'doesnotexist') 119 | 120 | def test_get_rc(self): 121 | # test success 122 | name = self.create() 123 | response = self.scheduler.rc.get(self.namespace, name) 124 | data = response.json() 125 | self.assertEqual(response.status_code, 200, data) 126 | self.assertEqual(data['apiVersion'], 'v1') 127 | self.assertEqual(data['kind'], 'ReplicationController') 128 | self.assertEqual(data['metadata']['name'], name) 129 | self.assertDictContainsSubset( 130 | { 131 | 'app': self.namespace, 132 | 'heritage': 'deis' 133 | }, 134 | data['metadata']['labels'] 135 | ) 136 | -------------------------------------------------------------------------------- /rootfs/scheduler/resources/replicationcontroller.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | from scheduler.exceptions import KubeHTTPException 4 | from scheduler.resources import Resource 5 | 6 | 7 | class ReplicationController(Resource): 8 | short_name = 'rc' 9 | 10 | def get(self, namespace, name=None, **kwargs): 11 | """ 12 | Fetch a single ReplicationController or a list 13 | """ 14 | url = '/namespaces/{}/replicationcontrollers' 15 | args = [namespace] 16 | if name is not None: 17 | args.append(name) 18 | url += '/{}' 19 | message = 'get ReplicationController "{}" in Namespace "{}"' 20 | else: 21 | message = 'get ReplicationControllers in Namespace "{}"' 22 | 23 | url = self.api(url, *args) 24 | response = self.http_get(url, params=self.query_params(**kwargs)) 25 | if self.unhealthy(response.status_code): 26 | args.reverse() # error msg is in reverse order 27 | raise KubeHTTPException(response, message, *args) 28 | 29 | return response 30 | 31 | def create(self, namespace, name, image, entrypoint, command, **kwargs): 32 | manifest = { 33 | 'kind': 'ReplicationController', 34 | 'apiVersion': 'v1', 35 | 'metadata': { 36 | 'name': name, 37 | 'labels': { 38 | 'app': namespace, 39 | 'version': kwargs.get('version'), 40 | 'type': kwargs.get('app_type'), 41 | 'heritage': 'deis', 42 | } 43 | }, 44 | 'spec': { 45 | 'replicas': kwargs.get('replicas', 0) 46 | } 47 | } 48 | 49 | # tell pod how to execute the process 50 | kwargs['command'] = entrypoint 51 | kwargs['args'] = command 52 | 53 | # pod manifest spec 54 | manifest['spec']['template'] = self.pod.manifest(namespace, name, image, **kwargs) 55 | 56 | url = self.api("/namespaces/{}/replicationcontrollers", namespace) 57 | resp = self.http_post(url, json=manifest) 58 | if self.unhealthy(resp.status_code): 59 | self.log(namespace, 'template: {}'.format(json.dumps(manifest, indent=4)), 'DEBUG') 60 | raise KubeHTTPException( 61 | resp, 62 | 'create ReplicationController "{}" in Namespace "{}"', name, namespace 63 | ) 64 | 65 | self.wait_until_updated(namespace, name) 66 | 67 | return resp 68 | 69 | def update(self, namespace, name, data): 70 | url = self.api("/namespaces/{}/replicationcontrollers/{}", namespace, name) 71 | response = self.http_put(url, json=data) 72 | if self.unhealthy(response.status_code): 73 | raise KubeHTTPException(response, 'scale ReplicationController "{}"', name) 74 | 75 | return response 76 | 77 | def delete(self, namespace, name): 78 | url = self.api("/namespaces/{}/replicationcontrollers/{}", namespace, name) 79 | response = self.http_delete(url) 80 | if self.unhealthy(response.status_code): 81 | raise KubeHTTPException( 82 | response, 83 | 'delete ReplicationController "{}" in Namespace "{}"', name, namespace 84 | ) 85 | 86 | return response 87 | 88 | def scale(self, namespace, name, desired, timeout): 89 | rc = self.get(namespace, name).json() 90 | 91 | current = int(rc['spec']['replicas']) 92 | if desired == current: 93 | self.log(namespace, "Not scaling RC {} to {} replicas. Already at desired replicas".format(name, desired)) # noqa 94 | return 95 | elif desired != rc['spec']['replicas']: # RC needs new replica count 96 | self.log(namespace, "scaling RC {} from {} to {} replicas".format(name, current, desired)) # noqa 97 | self.scales.update(namespace, name, desired, rc) 98 | self.wait_until_updated(namespace, name) 99 | 100 | # Double check enough pods are in the required state to service the application 101 | labels = rc['metadata']['labels'] 102 | containers = rc['spec']['template']['spec']['containers'] 103 | self.pods.wait_until_ready(namespace, containers, labels, desired, timeout) 104 | 105 | # if it was a scale down operation, wait until terminating pods are done 106 | if int(desired) < int(current): 107 | self.pods.wait_until_terminated(namespace, labels, current, desired) 108 | 109 | def wait_until_updated(self, namespace, name): 110 | """ 111 | Looks at status/observedGeneration and metadata/generation and 112 | waits for observedGeneration >= generation to happen, indicates RC is ready 113 | 114 | More information is also available at: 115 | https://github.com/kubernetes/kubernetes/blob/master/docs/devel/api-conventions.md#metadata 116 | """ 117 | self.log(namespace, "waiting for ReplicationController {} to get a newer generation (30s timeout)".format(name), 'DEBUG') # noqa 118 | for _ in range(30): 119 | try: 120 | rc = self.get(namespace, name).json() 121 | if ( 122 | "observedGeneration" in rc["status"] and 123 | rc["status"]["observedGeneration"] >= rc["metadata"]["generation"] 124 | ): 125 | self.log(namespace, "ReplicationController {} got a newer generation (30s timeout)".format(name), 'DEBUG') # noqa 126 | break 127 | 128 | time.sleep(1) 129 | except KubeHTTPException as e: 130 | if e.response.status_code == 404: 131 | time.sleep(1) 132 | --------------------------------------------------------------------------------