#!/usr/bin/env python # -*- coding: utf-8 -*- # # This file is subject to the terms and conditions defined in # file 'LICENSE.md', which is part of this source code package. # import time from kubernetes_py.K8sConfig import K8sConfig from kubernetes_py.K8sContainer import K8sContainer from kubernetes_py.K8sExceptions import BadRequestException from kubernetes_py.K8sExceptions import TimedOutException, NotFoundException from kubernetes_py.K8sObject import K8sObject from kubernetes_py.K8sVolume import K8sVolume from kubernetes_py.K8sPod import K8sPod from kubernetes_py.K8sReplicaSet import K8sReplicaSet from kubernetes_py.models.v1beta1.Deployment import Deployment from kubernetes_py.models.v1beta1.DeploymentRollback import DeploymentRollback from kubernetes_py.models.v1beta1.RollbackConfig import RollbackConfig from kubernetes_py.models.v1beta1.LabelSelector import LabelSelector from kubernetes_py.utils import is_valid_list class K8sDeployment(K8sObject): SCALE_WAIT_TIMEOUT_SECONDS = 120 REVISION_ANNOTATION = "deployment.kubernetes.io/revision" def __init__(self, config=None, name=None, image=None, replicas=0): super(K8sDeployment, self).__init__(config=config, obj_type="Deployment", name=name) self.desired_replicas = replicas labels = {"name": name} sel = LabelSelector() sel.match_labels = labels self.selector = sel self.labels = labels self.pod_labels = labels if image is not None: container = K8sContainer(name=name, image=image) self.add_container(container) if self.config.pull_secret is not None: self.add_image_pull_secrets(self.config.pull_secret) # ------------------------------------------------------------------------------------- override def create(self): super(K8sDeployment, self).create() self.get() if self.desired_replicas > 0: self._wait_for_desired_replicas() return self def update(self): super(K8sDeployment, self).update() self.get() if self.desired_replicas > 0: self._wait_for_desired_replicas() return self def list(self, pattern=None, labels=None): ls = super(K8sDeployment, self).list(labels=labels) deploys = list(map(lambda d: Deployment(d), ls)) if pattern is not None: deploys = list(filter(lambda dep: pattern in dep.name, deploys)) k8s = list() for x in deploys: j = K8sDeployment(config=self.config, name=x.name).from_model(m=x) k8s.append(j) return k8s def delete(self, cascade=False): # delete cascade on top level super(K8sDeployment, self).delete(cascade) if cascade: rsets = K8sReplicaSet(config=self.config, name="yo").list(pattern=self.name) # delete cascade on replicasets for rset in rsets: try: rset.delete(cascade=cascade) except NotFoundException: pass pods = K8sPod(config=self.config, name="yo").list(pattern=self.name) # delete cascade on pods for pod in pods: try: pod.delete(cascade=cascade) except NotFoundException: pass return self # ------------------------------------------------------------------------------------- wait def _wait_for_desired_replicas(self): start_time = time.time() while not self._has_desired_replicas(): time.sleep(0.5) self.get() self._check_timeout(start_time) def _has_desired_replicas(self): if ( self.updated_replicas == self.desired_replicas and self.current_replicas == self.desired_replicas and self.available_replicas == self.desired_replicas ): return True return False def _check_timeout(self, start_time=None): elapsed_time = time.time() - start_time if elapsed_time >= self.SCALE_WAIT_TIMEOUT_SECONDS: # timeout raise TimedOutException( "Timed out scaling Deployment: [ {} ] to replica count: [ {} ]".format(self.name, self.desired_replicas) ) # ------------------------------------------------------------------------------------- add def add_container(self, container=None): if not isinstance(container, K8sContainer): raise SyntaxError("K8sDeployment.add_container() container: [ {0} ] is invalid.".format(container)) containers = self.model.spec.template.spec.containers if container.model not in containers: containers.append(container.model) self.model.spec.template.spec.containers = containers return self def add_image_pull_secrets(self, secret=None): self.model.spec.template.spec.add_image_pull_secrets(secret) return self def add_volume(self, volume=None): if not isinstance(volume, K8sVolume): raise SyntaxError("K8sDeployment.add_volume() volume: [ {0} ] is invalid.".format(volume)) volumes = self.model.spec.template.spec.volumes if volume.model not in volumes: volumes.append(volume.model) self.model.spec.template.spec.volumes = volumes return self # ------------------------------------------------------------------------------------- get def get(self): self.model = Deployment(self.get_model()) return self # ------------------------------------------------------------------------------------- namespace @property def namespace(self): return self.model.metadata.namespace @namespace.setter def namespace(self, nspace=None): self.model.metadata.namespace = nspace # ------------------------------------------------------------------------------------- pod annotations @property def pod_annotations(self): return self.model.spec.template.metadata.annotations @pod_annotations.setter def pod_annotations(self, anns=None): self.model.spec.template.metadata.annotations = anns # ------------------------------------------------------------------------------------- pod labels @property def pod_labels(self): return self.model.spec.template.metadata.labels @pod_labels.setter def pod_labels(self, labels=None): self.model.spec.template.metadata.labels = labels # ------------------------------------------------------------------------------------- current replicas @property def current_replicas(self): return self.model.status.replicas @current_replicas.setter def current_replicas(self, reps=None): self.model.status.replicas = reps # ------------------------------------------------------------------------------------- desired replicas @property def desired_replicas(self): return self.model.spec.replicas @desired_replicas.setter def desired_replicas(self, reps=None): self.model.spec.replicas = reps # ------------------------------------------------------------------------------------- updated replicas @property def updated_replicas(self): return self.model.status.updated_replicas @updated_replicas.setter def updated_replicas(self, reps=None): self.model.status.updated_replicas = reps # ------------------------------------------------------------------------------------- available replicas @property def available_replicas(self): return self.model.status.available_replicas @available_replicas.setter def available_replicas(self, reps=None): self.model.status.available_replicas = reps # ------------------------------------------------------------------------------------- unavailable replicas @property def unavailable_replicas(self): return self.model.status.unavailable_replicas @unavailable_replicas.setter def unavailable_replicas(self, reps=None): self.model.status.unavailable_replicas = reps # ------------------------------------------------------------------------------------- revision @property def revision(self): if "deployment.kubernetes.io/revision" in self.annotations: return int(self.annotations["deployment.kubernetes.io/revision"]) return None @revision.setter def revision(self, r=None): raise NotImplementedError("K8sDeployment: revision is read-only.") # ------------------------------------------------------------------------------------- selector @property def selector(self): return self.model.spec.selector @selector.setter def selector(self, sel=None): self.model.spec.selector = sel # ------------------------------------------------------------------------------------- nodeSelector @property def node_selector(self): return self.model.spec.template.spec.node_selector @node_selector.setter def node_selector(self, sel=None): self.model.spec.template.spec.node_selector = sel # ------------------------------------------------------------------------------------- containers @property def containers(self): objs = [] for c in self.model.spec.template.spec.containers: k8scontainer = K8sContainer(name=c.name, image=c.image) k8scontainer.model = c objs.append(k8scontainer) return objs @containers.setter def containers(self, containers=None): models = [] for obj in containers: models.append(obj.model) self.model.spec.template.spec.containers = models # ------------------------------------------------------------------------------------- container_image @property def container_image(self, name=None): if name is None and len(self.containers) > 1: raise SyntaxError("K8sDeployment.container_image() Please specify a container name.") if len(self.containers) == 1: return self.containers[0].image else: filtered = list(filter(lambda x: x.name == name, self.containers)) if filtered: return filtered[0].image return None @container_image.setter def container_image(self, tup=None): if not isinstance(tup, tuple): raise SyntaxError("K8sDeployment.container_image() must be a tuple of the form (name, image)") name, image = tup found = list(filter(lambda x: x.name == name, self.containers)) if found: new = list(filter(lambda x: x.name != name, self.containers)) found[0].image = image new.append(found[0]) self.containers = new # ------------------------------------------------------------------------------------- volumes @property def volumes(self): return self.model.spec.template.spec.volumes @volumes.setter def volumes(self, v=None): if not is_valid_list(v, K8sVolume): self.model.spec.template.spec.volumes = v # ------------------------------------------------------------------------------------- get by name @staticmethod def get_by_name(config=None, name=None, name_label="name"): """ Fetches a K8sDeployment by name. :param config: A K8sConfig object. :param name: The name we want. :param name_label: The label key to use for name. :return: A list of K8sDeployment objects. """ if name is None: raise SyntaxError("Deployment: name: [ {0} ] cannot be None.".format(name)) if not isinstance(name, str): raise SyntaxError("Deployment: name: [ {0} ] must be a string.".format(name)) if config is not None and not isinstance(config, K8sConfig): raise SyntaxError("Deployment: config: [ {0} ] must be a K8sConfig".format(config)) deps = K8sDeployment(config=config, name=name).list(labels={name_label: name}) return deps # ------------------------------------------------------------------------------------- rollback def rollback(self, revision=None, annotations=None): """ Performs a rollback of the Deployment. If the 'revision' parameter is omitted, we fetch the Deployment's system-generated annotation containing the current revision, and revert to the version immediately preceding the current version. :param revision: The revision to rollback to. :param annotations: Annotations we'd like to update. :return: self """ rollback = DeploymentRollback() rollback.name = self.name rollback_config = RollbackConfig() # to the specified revision if revision is not None: rollback_config.revision = revision # to the revision immediately preceding the current revision else: current_revision = int(self.get_annotation(self.REVISION_ANNOTATION)) rev = max(current_revision - 1, 0) rollback_config.revision = rev rollback.rollback_to = rollback_config if annotations is not None: rollback.updated_annotations = annotations url = "{base}/{name}/rollback".format(base=self.base_url, name=self.name) state = self.request(method="POST", url=url, data=rollback.serialize()) if not state.get("success"): status = state.get("status", "") reason = state.get("data", dict()).get("message", None) message = "K8sDeployment: ROLLBACK failed : HTTP {0} : {1}".format(status, reason) raise BadRequestException(message) time.sleep(0.2) self._wait_for_desired_replicas() self.get() return self # ------------------------------------------------------------------------------------- scale def scale(self, replicas=None): """ Scales up or down. :param replicas: The number of desired replicas. :return: self """ self.desired_replicas = replicas self.update() return self # ------------------------------------------------------------------------------------- purge replica sets def purge_replica_sets(self, keep=3): """ Builds a list of ReplicaSets, sorted from newest to oldest. Slices the array, keeping the most X most recent ReplicaSets. :param keep: The number of ReplicaSets to keep. :return: None """ rsets = K8sReplicaSet(config=self.config, name="yo").list(pattern=self.name, reverse=True) to_purge = rsets[keep:] for rset in to_purge: rset.delete(cascade=True)