CSCfi/pebbles

View on GitHub
pebbles/drivers/provisioning/openshift_template_driver.py

Summary

Maintainability
B
4 hrs
Test Coverage
import datetime
import json

import dpath
import requests
import yaml
from dateutil import parser as dateutil_parser
from openshift.dynamic.exceptions import ConflictError

from pebbles.drivers.provisioning.kubernetes_driver import OpenShiftRemoteDriver
from pebbles.models import ApplicationSession


class OpenShiftTemplateDriver(OpenShiftRemoteDriver):
    """ OpenShift Template Driver allows provisioning application_sessions in an existing OpenShift cluster,
        using an Openshift Template. All the templates require a label defined in the template,
        like - "label: app: <app_label>"

        Similar to the openshift driver, it needs credentials for the cluster in the cluster config

        Since this driver is subclassed from OpenShiftRemoteDriver, it uses a lot of methods from it.
    """

    def do_provision(self, token, application_session_id):
        """ Implements provisioning, called by superclass.
            A namespace is created if necessary.
        """
        application_session = self.fetch_and_populate_application_session(token, application_session_id)
        namespace = self.get_application_session_namespace(application_session)
        self.ensure_namespace(namespace)

        self.logger.info(
            'provisioning %s in namespace %s on cluster %s',
            application_session['name'],
            namespace,
            self.cluster_config['name']
        )

        template_objects = self.render_template_objects(namespace, application_session)
        for template_object in template_objects:
            # label resources to be able to query them later
            dpath.util.new(template_object, '/metadata/labels/sessionName', application_session['name'])
            # add label also to templates (in Deployments, DeploymentConfigs, StatefulSets...) to have labels on pods
            if dpath.search(template_object, '/spec/template/metadata'):
                dpath.util.new(template_object, '/spec/template/metadata/labels/sessionName', application_session['name'])

            try:
                client = self.dynamic_client.resources.get(
                    api_version=template_object['apiVersion'],
                    kind=template_object['kind']
                )
                resp = client.create(body=template_object, namespace=namespace)
                self.logger.debug('created %s %s', resp.kind, resp.metadata.name)
            except ConflictError:
                self.logger.info(
                    "%s %s already exists",
                    template_object['kind'],
                    template_object['metadata']['name']
                )

        # tell base_driver that we need to check on the readiness later by explicitly returning STATE_STARTING
        return ApplicationSession.STATE_STARTING

    def do_check_readiness(self, token, application_session_id):
        """ Implements readiness checking, called by superclass. Checks that all the pods are ready.
        """
        application_session = self.fetch_and_populate_application_session(token, application_session_id)

        # warn if application_session is taking longer than 5 minutes to start
        createts = datetime.datetime.utcfromtimestamp(dateutil_parser.parse(application_session['created_at']).timestamp())
        if datetime.datetime.utcnow().timestamp() - createts.timestamp() > 300:
            self.logger.warning(
                'application_session %s created at %s, is taking a long time to start',
                application_session['name'],
                application_session['created_at']
            )

        namespace = self.get_application_session_namespace(application_session)
        api = self.dynamic_client.resources.get(api_version='v1', kind='Pod')
        pods = api.get(
            namespace=namespace,
            label_selector='application_sessionName=%s' % application_session['name']
        )

        if len(pods.items) < 1:
            self.logger.warning('no pods for application_session %s found', application_session['name'])
            return None

        # check readiness of all pods
        for pod in pods.items:
            if pod.status.phase != 'Running':
                self.logger.debug('pod %s not ready for %s', pod.metadata.name, application_session['name'])
                return None

        # application_session ready, create and publish an endpoint url.
        # This assumes that the template creates a route to the main application in
        # a compatible way (https://application_session-name.application-domain)
        return dict(
            namespace=namespace,
            endpoints=[dict(
                name='https',
                access='%s://%s' % (self.endpoint_protocol, self.get_application_session_hostname(application_session))
            )]
        )

    def do_deprovision(self, token, application_session_id):
        """ Implements deprovisioning, called by superclass.
            Iterates through template object types, queries them by label and deletes all objects.
        """
        application_session = self.fetch_and_populate_application_session(token, application_session_id)
        namespace = self.get_application_session_namespace(application_session)

        self.logger.info(
            'deprovisioning %s in namespace %s on cluster %s',
            application_session['name'],
            namespace,
            self.cluster_config['name']
        )

        template_objects = self.render_template_objects(namespace, application_session)
        processed_types = list()

        for template_object in template_objects:
            # check and record if we have already processed these kind of objects
            object_type = '%s/%s' % (template_object['apiVersion'], template_object['kind'])
            if object_type in processed_types:
                continue
            processed_types.append(object_type)

            client = self.dynamic_client.resources.get(
                api_version=template_object['apiVersion'],
                kind=template_object['kind']
            )
            label_selector = 'sessionName=%s' % application_session['name']
            # noinspection PyBroadException
            try:
                # we need to list objects and delete them individually, because not all object types
                # support deletion by label
                # https://github.com/kubernetes/client-go/issues/409
                res = client.get(namespace=namespace, label_selector=label_selector)

                for obj in res.items:
                    client.delete(namespace=namespace, name=obj.metadata.name)
                    self.logger.debug('deleted %s/%s in namespace %s', object_type, obj.metadata.name, namespace)
                else:
                    self.logger.debug('no %s found by label %s in namespace %s', object_type, label_selector, namespace)
            except Exception:
                self.logger.warning(
                    'failed deleting %s by label %s in namespace %s',
                    object_type, label_selector, namespace
                )
                # retry
                return ApplicationSession.STATE_DELETING

    def render_template_objects(self, namespace, application_session):
        """ Render the template for the application session. This is done on OpenShift server.
        """
        application_config = application_session['provisioning_config']

        template_url = application_config['os_template']
        if not template_url:
            raise RuntimeError('No template url given')

        try:
            template_url_data = requests.get(template_url)
            template_yaml = yaml.safe_load(template_url_data.text)
        except Exception as e:
            raise RuntimeError(e)

        if not template_yaml:
            raise RuntimeError('No template yaml could be loaded')

        if 'parameters' in template_yaml:
            # create a dict out of space separated list of VAR=VAL entries
            env_var_array = application_config.get('environment_vars', '').split()
            env_var_dict = {k: v for k, v in [x.split('=') for x in env_var_array]}

            # fill template parameters from application application variables
            for template_param in template_yaml['parameters']:
                if template_param['name'] in env_var_dict:
                    env_template_param_val = env_var_dict[template_param['name']]
                    # magic key to fill in the application session name dynamically
                    if env_template_param_val == 'application_session_name':
                        template_param['value'] = application_session['name']
                    else:
                        template_param['value'] = env_template_param_val

        # post the filled template to server to get objects back
        template_objects_resp = self.dynamic_client.request(
            method='POST',
            path='/apis/template.openshift.io/v1/namespaces/%s/processedtemplates' % namespace,
            body=template_yaml,
        )
        template_objects_json = json.loads(template_objects_resp.data)
        return template_objects_json['objects']