| Index: third_party/grpc/tools/gcp/utils/kubernetes_api.py
|
| diff --git a/third_party/grpc/tools/gcp/utils/kubernetes_api.py b/third_party/grpc/tools/gcp/utils/kubernetes_api.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..e8ddd2f1b3502dff664f61a1933833d6fc271108
|
| --- /dev/null
|
| +++ b/third_party/grpc/tools/gcp/utils/kubernetes_api.py
|
| @@ -0,0 +1,269 @@
|
| +#!/usr/bin/env python2.7
|
| +# Copyright 2015-2016, Google Inc.
|
| +# All rights reserved.
|
| +#
|
| +# Redistribution and use in source and binary forms, with or without
|
| +# modification, are permitted provided that the following conditions are
|
| +# met:
|
| +#
|
| +# * Redistributions of source code must retain the above copyright
|
| +# notice, this list of conditions and the following disclaimer.
|
| +# * Redistributions in binary form must reproduce the above
|
| +# copyright notice, this list of conditions and the following disclaimer
|
| +# in the documentation and/or other materials provided with the
|
| +# distribution.
|
| +# * Neither the name of Google Inc. nor the names of its
|
| +# contributors may be used to endorse or promote products derived from
|
| +# this software without specific prior written permission.
|
| +#
|
| +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
| +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
| +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
| +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
| +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
| +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| +
|
| +import requests
|
| +import json
|
| +
|
| +_REQUEST_TIMEOUT_SECS = 10
|
| +
|
| +
|
| +def _make_pod_config(pod_name, image_name, container_port_list, cmd_list,
|
| + arg_list, env_dict):
|
| + """Creates a string containing the Pod defintion as required by the Kubernetes API"""
|
| + body = {
|
| + 'kind': 'Pod',
|
| + 'apiVersion': 'v1',
|
| + 'metadata': {
|
| + 'name': pod_name,
|
| + 'labels': {'name': pod_name}
|
| + },
|
| + 'spec': {
|
| + 'containers': [
|
| + {
|
| + 'name': pod_name,
|
| + 'image': image_name,
|
| + 'ports': [{'containerPort': port,
|
| + 'protocol': 'TCP'}
|
| + for port in container_port_list],
|
| + 'imagePullPolicy': 'Always'
|
| + }
|
| + ]
|
| + }
|
| + }
|
| +
|
| + env_list = [{'name': k, 'value': v} for (k, v) in env_dict.iteritems()]
|
| + if len(env_list) > 0:
|
| + body['spec']['containers'][0]['env'] = env_list
|
| +
|
| + # Add the 'Command' and 'Args' attributes if they are passed.
|
| + # Note:
|
| + # - 'Command' overrides the ENTRYPOINT in the Docker Image
|
| + # - 'Args' override the CMD in Docker image (yes, it is confusing!)
|
| + if len(cmd_list) > 0:
|
| + body['spec']['containers'][0]['command'] = cmd_list
|
| + if len(arg_list) > 0:
|
| + body['spec']['containers'][0]['args'] = arg_list
|
| + return json.dumps(body)
|
| +
|
| +
|
| +def _make_service_config(service_name, pod_name, service_port_list,
|
| + container_port_list, is_headless):
|
| + """Creates a string containing the Service definition as required by the Kubernetes API.
|
| +
|
| + NOTE:
|
| + This creates either a Headless Service or 'LoadBalancer' service depending on
|
| + the is_headless parameter. For Headless services, there is no 'type' attribute
|
| + and the 'clusterIP' attribute is set to 'None'. Also, if the service is
|
| + Headless, Kubernetes creates DNS entries for Pods - i.e creates DNS A-records
|
| + mapping the service's name to the Pods' IPs
|
| + """
|
| + if len(container_port_list) != len(service_port_list):
|
| + print(
|
| + 'ERROR: container_port_list and service_port_list must be of same size')
|
| + return ''
|
| + body = {
|
| + 'kind': 'Service',
|
| + 'apiVersion': 'v1',
|
| + 'metadata': {
|
| + 'name': service_name,
|
| + 'labels': {
|
| + 'name': service_name
|
| + }
|
| + },
|
| + 'spec': {
|
| + 'ports': [],
|
| + 'selector': {
|
| + 'name': pod_name
|
| + }
|
| + }
|
| + }
|
| + # Populate the 'ports' list in the 'spec' section. This maps service ports
|
| + # (port numbers that are exposed by Kubernetes) to container ports (i.e port
|
| + # numbers that are exposed by your Docker image)
|
| + for idx in range(len(container_port_list)):
|
| + port_entry = {
|
| + 'port': service_port_list[idx],
|
| + 'targetPort': container_port_list[idx],
|
| + 'protocol': 'TCP'
|
| + }
|
| + body['spec']['ports'].append(port_entry)
|
| +
|
| + # Make this either a LoadBalancer service or a headless service depending on
|
| + # the is_headless parameter
|
| + if is_headless:
|
| + body['spec']['clusterIP'] = 'None'
|
| + else:
|
| + body['spec']['type'] = 'LoadBalancer'
|
| + return json.dumps(body)
|
| +
|
| +
|
| +def _print_connection_error(msg):
|
| + print('ERROR: Connection failed. Did you remember to run Kubenetes proxy on '
|
| + 'localhost (i.e kubectl proxy --port=<proxy_port>) ?. Error: %s' % msg)
|
| +
|
| +
|
| +def _do_post(post_url, api_name, request_body):
|
| + """Helper to do HTTP POST.
|
| +
|
| + Note:
|
| + 1) On success, Kubernetes returns a success code of 201(CREATED) not 200(OK)
|
| + 2) A response code of 509(CONFLICT) is interpreted as a success code (since
|
| + the error is most likely due to the resource already existing). This makes
|
| + _do_post() idempotent which is semantically desirable.
|
| + """
|
| + is_success = True
|
| + try:
|
| + r = requests.post(post_url,
|
| + data=request_body,
|
| + timeout=_REQUEST_TIMEOUT_SECS)
|
| + if r.status_code == requests.codes.conflict:
|
| + print('WARN: Looks like the resource already exists. Api: %s, url: %s' %
|
| + (api_name, post_url))
|
| + elif r.status_code != requests.codes.created:
|
| + print('ERROR: %s API returned error. HTTP response: (%d) %s' %
|
| + (api_name, r.status_code, r.text))
|
| + is_success = False
|
| + except (requests.exceptions.Timeout,
|
| + requests.exceptions.ConnectionError) as e:
|
| + is_success = False
|
| + _print_connection_error(str(e))
|
| + return is_success
|
| +
|
| +
|
| +def _do_delete(del_url, api_name):
|
| + """Helper to do HTTP DELETE.
|
| +
|
| + Note: A response code of 404(NOT_FOUND) is treated as success to keep
|
| + _do_delete() idempotent.
|
| + """
|
| + is_success = True
|
| + try:
|
| + r = requests.delete(del_url, timeout=_REQUEST_TIMEOUT_SECS)
|
| + if r.status_code == requests.codes.not_found:
|
| + print('WARN: The resource does not exist. Api: %s, url: %s' %
|
| + (api_name, del_url))
|
| + elif r.status_code != requests.codes.ok:
|
| + print('ERROR: %s API returned error. HTTP response: %s' %
|
| + (api_name, r.text))
|
| + is_success = False
|
| + except (requests.exceptions.Timeout,
|
| + requests.exceptions.ConnectionError) as e:
|
| + is_success = False
|
| + _print_connection_error(str(e))
|
| + return is_success
|
| +
|
| +
|
| +def create_service(kube_host, kube_port, namespace, service_name, pod_name,
|
| + service_port_list, container_port_list, is_headless):
|
| + """Creates either a Headless Service or a LoadBalancer Service depending
|
| + on the is_headless parameter.
|
| + """
|
| + post_url = 'http://%s:%d/api/v1/namespaces/%s/services' % (
|
| + kube_host, kube_port, namespace)
|
| + request_body = _make_service_config(service_name, pod_name, service_port_list,
|
| + container_port_list, is_headless)
|
| + return _do_post(post_url, 'Create Service', request_body)
|
| +
|
| +
|
| +def create_pod(kube_host, kube_port, namespace, pod_name, image_name,
|
| + container_port_list, cmd_list, arg_list, env_dict):
|
| + """Creates a Kubernetes Pod.
|
| +
|
| + Note that it is generally NOT considered a good practice to directly create
|
| + Pods. Typically, the recommendation is to create 'Controllers' to create and
|
| + manage Pods' lifecycle. Currently Kubernetes only supports 'Replication
|
| + Controller' which creates a configurable number of 'identical Replicas' of
|
| + Pods and automatically restarts any Pods in case of failures (for eg: Machine
|
| + failures in Kubernetes). This makes it less flexible for our test use cases
|
| + where we might want slightly different set of args to each Pod. Hence we
|
| + directly create Pods and not care much about Kubernetes failures since those
|
| + are very rare.
|
| + """
|
| + post_url = 'http://%s:%d/api/v1/namespaces/%s/pods' % (kube_host, kube_port,
|
| + namespace)
|
| + request_body = _make_pod_config(pod_name, image_name, container_port_list,
|
| + cmd_list, arg_list, env_dict)
|
| + return _do_post(post_url, 'Create Pod', request_body)
|
| +
|
| +
|
| +def delete_service(kube_host, kube_port, namespace, service_name):
|
| + del_url = 'http://%s:%d/api/v1/namespaces/%s/services/%s' % (
|
| + kube_host, kube_port, namespace, service_name)
|
| + return _do_delete(del_url, 'Delete Service')
|
| +
|
| +
|
| +def delete_pod(kube_host, kube_port, namespace, pod_name):
|
| + del_url = 'http://%s:%d/api/v1/namespaces/%s/pods/%s' % (kube_host, kube_port,
|
| + namespace, pod_name)
|
| + return _do_delete(del_url, 'Delete Pod')
|
| +
|
| +
|
| +def create_pod_and_service(kube_host, kube_port, namespace, pod_name,
|
| + image_name, container_port_list, cmd_list, arg_list,
|
| + env_dict, is_headless_service):
|
| + """A helper function that creates a pod and a service (if pod creation was successful)."""
|
| + is_success = create_pod(kube_host, kube_port, namespace, pod_name, image_name,
|
| + container_port_list, cmd_list, arg_list, env_dict)
|
| + if not is_success:
|
| + print 'Error in creating Pod'
|
| + return False
|
| +
|
| + is_success = create_service(
|
| + kube_host,
|
| + kube_port,
|
| + namespace,
|
| + pod_name, # Use pod_name for service
|
| + pod_name,
|
| + container_port_list, # Service port list same as container port list
|
| + container_port_list,
|
| + is_headless_service)
|
| + if not is_success:
|
| + print 'Error in creating Service'
|
| + return False
|
| +
|
| + print 'Successfully created the pod/service %s' % pod_name
|
| + return True
|
| +
|
| +
|
| +def delete_pod_and_service(kube_host, kube_port, namespace, pod_name):
|
| + """ A helper function that calls delete_pod and delete_service """
|
| + is_success = delete_pod(kube_host, kube_port, namespace, pod_name)
|
| + if not is_success:
|
| + print 'Error in deleting pod %s' % pod_name
|
| + return False
|
| +
|
| + # Note: service name assumed to the the same as pod name
|
| + is_success = delete_service(kube_host, kube_port, namespace, pod_name)
|
| + if not is_success:
|
| + print 'Error in deleting service %s' % pod_name
|
| + return False
|
| +
|
| + print 'Successfully deleted the Pod/Service: %s' % pod_name
|
| + return True
|
|
|