def test_request_patch(self): client = KubeBase(self.config) with patch.object(client.session, 'request', return_value=helper.make_response(201, {})): data = client.request('patch', 'http://localhost:8080', [ {'op': 'replace', 'path': '/spec/replicas', 'value': 0} ]) self.assertEqual(data, {})
def test_request_no_data(self): client = KubeBase(self.config) with patch.object(client.session, 'request', return_value=helper.make_response(200, None)): data = client.request('get', 'http://localhost:8080') self.assertIsNone(data)
def test_request_response_error(self): client = KubeBase(self.config) with patch.object(client.session, 'request', return_value=helper.make_response(400, None)): with self.assertRaises(KubeRequestError): client.request('get', 'http://localhost:8080')
def test_request_connection_error(self): client = KubeBase(self.config) with patch.object(client.session, 'request', side_effect=requests.exceptions.ConnectionError): with self.assertRaises(KubeConnectionError): client.request('get', 'http://localhost:8080')
class KubeOpenshiftClient(object): def __init__(self, config): ''' Args: config (obj): Object of the configuration data ''' # The configuration data passed in will be .kube/config data, so process is accordingly. self.api = KubeBase(config) # Check the API url url = self.api.cluster['server'] if not re.match('(?:http|https)://', url): raise KubeOpenshiftError("OpenShift API URL does not include HTTP or HTTPS") # Gather what end-points we will be using self.k8s_api = urljoin(url, "api/v1/") self.oc_api = urljoin(url, "oapi/v1/") # Test the connection before proceeding self.api.test_connection(self.k8s_api) self.api.test_connection(self.oc_api) # Gather the resource names which will be used for the 'kind' API calls self.oc_api_resources = self.api.get_resources(self.oc_api) # Gather what API groups are available # TODO: refactor this (create function in kubebase.py) self.k8s_api_resources = {} self.k8s_api_resources['v1'] = self.api.get_resources(self.k8s_api) self.k8s_apis = urljoin(url, "apis/") # Gather the group names from which resource names will be derived self.k8s_api_groups = self.api.get_groups(self.k8s_apis) for (name, versions) in self.k8s_api_groups: for version in versions: api = "%s/%s" % (name, version) url = urljoin(self.k8s_apis, api) self.k8s_api_resources[api] = self.api.get_resources(url) def create(self, obj, namespace): ''' Create an object from the Kubernetes cluster ''' name = self._get_metadata_name(obj) kind, url = self._generate_kurl(obj, namespace) # Must process through each object if kind is a 'template' if kind is "template": self._process_template(obj, namespace, "create") else: self.api.request("post", url, data=obj) logger.info("%s '%s' successfully created", kind.capitalize(), name) def delete(self, obj, namespace): ''' Delete an object from the Kubernetes cluster Args: obj (object): Object of the artifact being modified namesapce (str): Namespace of the kubernetes cluster to be used replicates (int): Default 0, size of the amount of replicas to scale *Note* Replication controllers must scale to 0 in order to delete pods. Kubernetes 1.3 will implement server-side cascading deletion, but until then, it's mandatory to scale to 0 https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/garbage-collection.md ''' name = self._get_metadata_name(obj) kind, url = self._generate_kurl(obj, namespace, name) # Must process through each object if kind is a 'template' if kind is "template": self._process_template(obj, namespace, "create") else: if kind in ['rcs', 'replicationcontrollers']: self.scale(obj, namespace) self.api.request("delete", url) logger.info("%s '%s' successfully deleted", kind.capitalize(), name) def scale(self, obj, namespace, replicas=0): ''' By default we scale back down to 0. This function takes an object and scales said object down to a specified value on the Kubernetes cluster Args: obj (object): Object of the artifact being modified namesapce (str): Namespace of the kubernetes cluster to be used replicates (int): Default 0, size of the amount of replicas to scale ''' patch = [{"op": "replace", "path": "/spec/replicas", "value": replicas}] name = self._get_metadata_name(obj) _, url = self._generate_kurl(obj, namespace, name) self.api.request("patch", url, data=patch) logger.info("'%s' successfully scaled to %s", name, replicas) def namespaces(self): ''' Gathers a list of namespaces on the Kubernetes cluster ''' url = urljoin(self.oc_api, "projects") ns = self.api.request("get", url) return ns['items'] def _generate_kurl(self, obj, namespace, name=None, params=None): ''' Generate the required URL by extracting the 'kind' from the object as well as the namespace. Args: obj (obj): Object of the data being passed namespace (str): k8s namespace name (str): Name of the object being passed params (arr): Extra params passed such as timeout=300 Returns: kind (str): The kind used url (str): The URL to be used / artifact URL ''' if 'apiVersion' not in obj.keys(): raise KubeOpenshiftError("Error processing object. There is no apiVersion") if 'kind' not in obj.keys(): raise KubeOpenshiftError("Error processing object. There is no kind") api_version = obj['apiVersion'] kind = obj['kind'] resource = KubeBase.kind_to_resource_name(kind) if resource in self.k8s_api_resources[api_version]: if api_version == 'v1': url = self.k8s_api else: url = urljoin(self.k8s_apis, "%s/" % api_version) else: raise KubeOpenshiftError("No kind by that name: %s" % kind) url = urljoin(url, "namespaces/%s/%s/" % (namespace, resource)) if name: url = urljoin(url, name) if params: url = urljoin(url, "?%s" % urlencode(params)) return (resource, url) @staticmethod def _get_metadata_name(obj): ''' This looks at the object and grabs the metadata name of said object Args: obj (object): Object file of the artifact Returns: name (str): Returns the metadata name of the object ''' if "metadata" in obj and \ "name" in obj["metadata"]: name = obj["metadata"]["name"] else: raise KubeOpenshiftError("Cannot undeploy. There is no" " name in object metadata " "object=%s" % obj) return name # OPENSHIFT-SPECIFIC FUNCTIONS def extract(self, image, src, dest, namespace, update=True): """ Extract contents of a container image from 'src' in container to 'dest' in host. Args: image (str): Name of container image src (str): Source path in container dest (str): Destination path in host update (bool): Update existing destination, if True """ if os.path.exists(dest) and not update: return cleaned_image_name = Utils.sanitizeName(image) pod_name = '{}-{}'.format(cleaned_image_name, Utils.getUniqueUUID()) container_name = cleaned_image_name # Pull (if needed) image and bring up a container from it # with 'sleep 3600' entrypoint, just to extract content from it artifact = { 'apiVersion': 'v1', 'kind': 'Pod', 'metadata': { 'name': pod_name }, 'spec': { 'containers': [ { 'image': image, 'command': [ 'sleep', '3600' ], 'imagePullPolicy': 'IfNotPresent', 'name': container_name } ], 'restartPolicy': 'Always' } } self.create(artifact, namespace) try: self._wait_till_pod_runs(namespace, pod_name, timeout=300) # Archive content from the container and dump it to tmpfile tmpfile = '/tmp/atomicapp-{pod}.tar.gz'.format(pod=pod_name) self._execute( namespace, pod_name, container_name, 'tar -cz --directory {} ./'.format('/' + src), outfile=tmpfile ) finally: # Delete created pod self.delete(artifact, namespace) # Extract archive data tar = tarfile.open(tmpfile, 'r:gz') tar.extractall(dest) def _execute(self, namespace, pod, container, command, outfile=None): """ Execute a command in a container in an Openshift pod. Args: namespace (str): Namespace pod (str): Pod name container (str): Container name inside pod command (str): Command to execute outfile (str): Path to output file where results should be dumped Returns: Command output (str) or None in case results dumped to output file """ args = { 'token': self.api.token, 'namespace': namespace, 'pod': pod, 'container': container, 'command': ''.join(['command={}&'.format(word) for word in command.split()]) } url = urljoin( self.k8s_api, 'namespaces/{namespace}/pods/{pod}/exec?' 'access_token={token}&container={container}&' '{command}stdout=1&stdin=0&tty=0'.format(**args)) return self.api.websocket_request(url, outfile) def _process_template(self, obj, namespace, method): _, url = self._generate_kurl(obj, namespace) data = self.api.request("post", url, data=obj) if method is "create": for o in data[0]['objects']: name = self._get_metadata_name(o) _, object_url = self._generate_kurl(o, namespace) self.api.request("post", object_url, data=o) logger.debug("Created template object: %s" % name) elif method is "delete": for o in data[0]['objects']: name = self._get_metadata_name(o) _, object_url = self._generate_kurl(o, namespace, name) self.api.request("delete", object_url) logger.debug("Deleted template object: %s" % name) else: raise KubeOpenshiftError("No method by that name to process template") logger.debug("Processed object template successfully") def _get_pod_status(self, namespace, pod): """ Get pod status. Args: namespace (str): Openshift namespace pod (str): Pod name Returns: Status of pod (str) Raises: ProviderFailedException when unable to fetch Pod status. """ args = { 'namespace': namespace, 'pod': pod, 'access_token': self.api.token } url = urljoin( self.k8s_api, 'namespaces/{namespace}/pods/{pod}?' 'access_token={access_token}'.format(**args)) data = self.api.request("get", url) return data['status']['phase'].lower() def _wait_till_pod_runs(self, namespace, pod, timeout=300): """ Wait till pod runs, with a timeout. Args: namespace (str): Openshift namespace pod (str): Pod name timeout (int): Timeout in seconds. Raises: ProviderFailedException on timeout or when the pod goes to failed state. """ now = datetime.datetime.now() timeout_delta = datetime.timedelta(seconds=timeout) while datetime.datetime.now() - now < timeout_delta: status = self.oc.get_pod_status(namespace, pod) if status == 'running': break elif status == 'failed': raise KubeOpenshiftError( 'Unable to run pod for extracting content: ' '{namespace}/{pod}'.format(namespace=namespace, pod=pod)) time.sleep(1) if status != 'running': raise KubeOpenshiftError( 'Timed out to extract content from pod: ' '{namespace}/{pod}'.format(namespace=namespace, pod=pod))
class KubeKubernetesClient(object): def __init__(self, config): ''' Args: config (obj): Object of the configuration data ''' # The configuration data passed in will be .kube/config data, so process is accordingly. self.api = KubeBase(config) # Check the API url url = self.api.cluster['server'] if not re.match('(?:http|https)://', url): raise KubeKubernetesError( "Kubernetes API URL does not include HTTP or HTTPS") # Gather what end-points we will be using self.k8s_api = urljoin(url, "api/v1/") # Test the connection before proceeding self.api.test_connection(self.k8s_api) # Gather the resource names which will be used for the 'kind' API calls self.k8s_api_resources = {} self.k8s_api_resources['v1'] = self.api.get_resources(self.k8s_api) # Gather what API groups are available self.k8s_apis = urljoin(url, "apis/") # Gather the group names from which resource names will be derived self.k8s_api_groups = self.api.get_groups(self.k8s_apis) for (name, versions) in self.k8s_api_groups: for version in versions: api = "%s/%s" % (name, version) url = urljoin(self.k8s_apis, api) self.k8s_api_resources[api] = self.api.get_resources(url) def create(self, obj, namespace): ''' Create an object from the Kubernetes cluster ''' name = self._get_metadata_name(obj) kind, url = self._generate_kurl(obj, namespace) self.api.request("post", url, data=obj) logger.info("%s '%s' successfully created", kind.capitalize(), name) def delete(self, obj, namespace): ''' Delete an object from the Kubernetes cluster Args: obj (object): Object of the artifact being modified namesapce (str): Namespace of the kubernetes cluster to be used replicates (int): Default 0, size of the amount of replicas to scale *Note* Replication controllers must scale to 0 in order to delete pods. Kubernetes 1.3 will implement server-side cascading deletion, but until then, it's mandatory to scale to 0 https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/garbage-collection.md ''' name = self._get_metadata_name(obj) kind, url = self._generate_kurl(obj, namespace, name) if kind in ['rcs', 'replicationcontrollers']: self.scale(obj, namespace) self.api.request("delete", url) logger.info("%s '%s' successfully deleted", kind.capitalize(), name) def scale(self, obj, namespace, replicas=0): ''' By default we scale back down to 0. This function takes an object and scales said object down to a specified value on the Kubernetes cluster Args: obj (object): Object of the artifact being modified namesapce (str): Namespace of the kubernetes cluster to be used replicates (int): Default 0, size of the amount of replicas to scale ''' patch = [{ "op": "replace", "path": "/spec/replicas", "value": replicas }] name = self._get_metadata_name(obj) _, url = self._generate_kurl(obj, namespace, name) self.api.request("patch", url, data=patch) logger.info("'%s' successfully scaled to %s", name, replicas) def namespaces(self): ''' Gathers a list of namespaces on the Kubernetes cluster ''' url = urljoin(self.k8s_api, "namespaces") ns = self.api.request("get", url) return ns['items'] def _generate_kurl(self, obj, namespace, name=None, params=None): ''' Generate the required URL by extracting the 'kind' from the object as well as the namespace. Args: obj (obj): Object of the data being passed namespace (str): k8s namespace name (str): Name of the object being passed params (arr): Extra params passed such as timeout=300 Returns: kind (str): The kind used url (str): The URL to be used / artifact URL ''' if 'apiVersion' not in obj.keys(): raise KubeKubernetesError( "Error processing object. There is no apiVersion") if 'kind' not in obj.keys(): raise KubeKubernetesError( "Error processing object. There is no kind") api_version = obj['apiVersion'] kind = obj['kind'] resource = KubeBase.kind_to_resource_name(kind) if resource in self.k8s_api_resources[api_version]: if api_version == 'v1': url = self.k8s_api else: url = urljoin(self.k8s_apis, "%s/" % api_version) else: raise KubeKubernetesError("No kind by that name: %s" % kind) url = urljoin(url, "namespaces/%s/%s/" % (namespace, resource)) if name: url = urljoin(url, name) if params: url = urljoin(url, "?%s" % urlencode(params)) return (resource, url) @staticmethod def _get_metadata_name(obj): ''' This looks at the object and grabs the metadata name of said object Args: obj (object): Object file of the artifact Returns: name (str): Returns the metadata name of the object ''' if "metadata" in obj and \ "name" in obj["metadata"]: name = obj["metadata"]["name"] else: raise KubeKubernetesError("Cannot undeploy. There is no" " name in object metadata " "object=%s" % obj) return name