Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python2.7 |
Sree Kuchibhotla | e1dd18a | 2016-02-29 13:28:55 -0800 | [diff] [blame] | 2 | # Copyright 2015-2016, Google Inc. |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 3 | # All rights reserved. |
| 4 | # |
| 5 | # Redistribution and use in source and binary forms, with or without |
| 6 | # modification, are permitted provided that the following conditions are |
| 7 | # met: |
| 8 | # |
| 9 | # * Redistributions of source code must retain the above copyright |
| 10 | # notice, this list of conditions and the following disclaimer. |
| 11 | # * Redistributions in binary form must reproduce the above |
| 12 | # copyright notice, this list of conditions and the following disclaimer |
| 13 | # in the documentation and/or other materials provided with the |
| 14 | # distribution. |
| 15 | # * Neither the name of Google Inc. nor the names of its |
| 16 | # contributors may be used to endorse or promote products derived from |
| 17 | # this software without specific prior written permission. |
| 18 | # |
| 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 20 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 21 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 22 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 23 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 24 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 25 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 27 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 30 | |
| 31 | import requests |
| 32 | import json |
| 33 | |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 34 | _REQUEST_TIMEOUT_SECS = 10 |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 35 | |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 36 | |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 37 | def _make_pod_config(pod_name, image_name, container_port_list, cmd_list, |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 38 | arg_list, env_dict): |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 39 | """Creates a string containing the Pod defintion as required by the Kubernetes API""" |
| 40 | body = { |
| 41 | 'kind': 'Pod', |
| 42 | 'apiVersion': 'v1', |
| 43 | 'metadata': { |
| 44 | 'name': pod_name, |
| 45 | 'labels': {'name': pod_name} |
| 46 | }, |
| 47 | 'spec': { |
| 48 | 'containers': [ |
| 49 | { |
| 50 | 'name': pod_name, |
| 51 | 'image': image_name, |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 52 | 'ports': [{'containerPort': port, |
Sree Kuchibhotla | 61c134f | 2016-02-26 11:03:29 -0800 | [diff] [blame] | 53 | 'protocol': 'TCP'} |
| 54 | for port in container_port_list], |
Sree Kuchibhotla | 559e45b | 2016-02-19 03:02:16 -0800 | [diff] [blame] | 55 | 'imagePullPolicy': 'Always' |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 56 | } |
| 57 | ] |
| 58 | } |
| 59 | } |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 60 | |
| 61 | env_list = [{'name': k, 'value': v} for (k, v) in env_dict.iteritems()] |
| 62 | if len(env_list) > 0: |
| 63 | body['spec']['containers'][0]['env'] = env_list |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 64 | |
| 65 | # Add the 'Command' and 'Args' attributes if they are passed. |
| 66 | # Note: |
| 67 | # - 'Command' overrides the ENTRYPOINT in the Docker Image |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 68 | # - 'Args' override the CMD in Docker image (yes, it is confusing!) |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 69 | if len(cmd_list) > 0: |
| 70 | body['spec']['containers'][0]['command'] = cmd_list |
| 71 | if len(arg_list) > 0: |
| 72 | body['spec']['containers'][0]['args'] = arg_list |
| 73 | return json.dumps(body) |
| 74 | |
| 75 | |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 76 | def _make_service_config(service_name, pod_name, service_port_list, |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 77 | container_port_list, is_headless): |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 78 | """Creates a string containing the Service definition as required by the Kubernetes API. |
| 79 | |
| 80 | NOTE: |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 81 | This creates either a Headless Service or 'LoadBalancer' service depending on |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 82 | the is_headless parameter. For Headless services, there is no 'type' attribute |
| 83 | and the 'clusterIP' attribute is set to 'None'. Also, if the service is |
| 84 | Headless, Kubernetes creates DNS entries for Pods - i.e creates DNS A-records |
| 85 | mapping the service's name to the Pods' IPs |
| 86 | """ |
| 87 | if len(container_port_list) != len(service_port_list): |
| 88 | print( |
| 89 | 'ERROR: container_port_list and service_port_list must be of same size') |
| 90 | return '' |
| 91 | body = { |
| 92 | 'kind': 'Service', |
| 93 | 'apiVersion': 'v1', |
| 94 | 'metadata': { |
| 95 | 'name': service_name, |
| 96 | 'labels': { |
| 97 | 'name': service_name |
| 98 | } |
| 99 | }, |
| 100 | 'spec': { |
| 101 | 'ports': [], |
| 102 | 'selector': { |
| 103 | 'name': pod_name |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | # Populate the 'ports' list in the 'spec' section. This maps service ports |
| 108 | # (port numbers that are exposed by Kubernetes) to container ports (i.e port |
| 109 | # numbers that are exposed by your Docker image) |
| 110 | for idx in range(len(container_port_list)): |
| 111 | port_entry = { |
| 112 | 'port': service_port_list[idx], |
| 113 | 'targetPort': container_port_list[idx], |
| 114 | 'protocol': 'TCP' |
| 115 | } |
| 116 | body['spec']['ports'].append(port_entry) |
| 117 | |
| 118 | # Make this either a LoadBalancer service or a headless service depending on |
| 119 | # the is_headless parameter |
| 120 | if is_headless: |
| 121 | body['spec']['clusterIP'] = 'None' |
| 122 | else: |
| 123 | body['spec']['type'] = 'LoadBalancer' |
| 124 | return json.dumps(body) |
| 125 | |
| 126 | |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 127 | def _print_connection_error(msg): |
Sree Kuchibhotla | 6a7bd16 | 2015-11-17 11:46:04 -0800 | [diff] [blame] | 128 | print('ERROR: Connection failed. Did you remember to run Kubenetes proxy on ' |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 129 | 'localhost (i.e kubectl proxy --port=<proxy_port>) ?. Error: %s' % msg) |
| 130 | |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 131 | |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 132 | def _do_post(post_url, api_name, request_body): |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 133 | """Helper to do HTTP POST. |
| 134 | |
| 135 | Note: |
| 136 | 1) On success, Kubernetes returns a success code of 201(CREATED) not 200(OK) |
| 137 | 2) A response code of 509(CONFLICT) is interpreted as a success code (since |
| 138 | the error is most likely due to the resource already existing). This makes |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 139 | _do_post() idempotent which is semantically desirable. |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 140 | """ |
| 141 | is_success = True |
| 142 | try: |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 143 | r = requests.post(post_url, |
| 144 | data=request_body, |
| 145 | timeout=_REQUEST_TIMEOUT_SECS) |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 146 | if r.status_code == requests.codes.conflict: |
| 147 | print('WARN: Looks like the resource already exists. Api: %s, url: %s' % |
| 148 | (api_name, post_url)) |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 149 | elif r.status_code != requests.codes.created: |
| 150 | print('ERROR: %s API returned error. HTTP response: (%d) %s' % |
| 151 | (api_name, r.status_code, r.text)) |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 152 | is_success = False |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 153 | except (requests.exceptions.Timeout, |
| 154 | requests.exceptions.ConnectionError) as e: |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 155 | is_success = False |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 156 | _print_connection_error(str(e)) |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 157 | return is_success |
| 158 | |
| 159 | |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 160 | def _do_delete(del_url, api_name): |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 161 | """Helper to do HTTP DELETE. |
| 162 | |
| 163 | Note: A response code of 404(NOT_FOUND) is treated as success to keep |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 164 | _do_delete() idempotent. |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 165 | """ |
| 166 | is_success = True |
| 167 | try: |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 168 | r = requests.delete(del_url, timeout=_REQUEST_TIMEOUT_SECS) |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 169 | if r.status_code == requests.codes.not_found: |
| 170 | print('WARN: The resource does not exist. Api: %s, url: %s' % |
| 171 | (api_name, del_url)) |
| 172 | elif r.status_code != requests.codes.ok: |
| 173 | print('ERROR: %s API returned error. HTTP response: %s' % |
| 174 | (api_name, r.text)) |
| 175 | is_success = False |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 176 | except (requests.exceptions.Timeout, |
| 177 | requests.exceptions.ConnectionError) as e: |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 178 | is_success = False |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 179 | _print_connection_error(str(e)) |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 180 | return is_success |
| 181 | |
| 182 | |
| 183 | def create_service(kube_host, kube_port, namespace, service_name, pod_name, |
| 184 | service_port_list, container_port_list, is_headless): |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 185 | """Creates either a Headless Service or a LoadBalancer Service depending |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 186 | on the is_headless parameter. |
| 187 | """ |
| 188 | post_url = 'http://%s:%d/api/v1/namespaces/%s/services' % ( |
| 189 | kube_host, kube_port, namespace) |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 190 | request_body = _make_service_config(service_name, pod_name, service_port_list, |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 191 | container_port_list, is_headless) |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 192 | return _do_post(post_url, 'Create Service', request_body) |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 193 | |
| 194 | |
| 195 | def create_pod(kube_host, kube_port, namespace, pod_name, image_name, |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 196 | container_port_list, cmd_list, arg_list, env_dict): |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 197 | """Creates a Kubernetes Pod. |
| 198 | |
| 199 | Note that it is generally NOT considered a good practice to directly create |
| 200 | Pods. Typically, the recommendation is to create 'Controllers' to create and |
| 201 | manage Pods' lifecycle. Currently Kubernetes only supports 'Replication |
| 202 | Controller' which creates a configurable number of 'identical Replicas' of |
| 203 | Pods and automatically restarts any Pods in case of failures (for eg: Machine |
| 204 | failures in Kubernetes). This makes it less flexible for our test use cases |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 205 | where we might want slightly different set of args to each Pod. Hence we |
| 206 | directly create Pods and not care much about Kubernetes failures since those |
| 207 | are very rare. |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 208 | """ |
| 209 | post_url = 'http://%s:%d/api/v1/namespaces/%s/pods' % (kube_host, kube_port, |
| 210 | namespace) |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 211 | request_body = _make_pod_config(pod_name, image_name, container_port_list, |
Sree Kuchibhotla | 44ca2c2 | 2016-02-16 09:48:36 -0800 | [diff] [blame] | 212 | cmd_list, arg_list, env_dict) |
Sree Kuchibhotla | 3abacfb | 2015-11-17 11:42:43 -0800 | [diff] [blame] | 213 | return _do_post(post_url, 'Create Pod', request_body) |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 214 | |
| 215 | |
| 216 | def delete_service(kube_host, kube_port, namespace, service_name): |
| 217 | del_url = 'http://%s:%d/api/v1/namespaces/%s/services/%s' % ( |
| 218 | kube_host, kube_port, namespace, service_name) |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 219 | return _do_delete(del_url, 'Delete Service') |
Sree Kuchibhotla | 7b73966 | 2015-11-05 10:28:16 -0800 | [diff] [blame] | 220 | |
| 221 | |
| 222 | def delete_pod(kube_host, kube_port, namespace, pod_name): |
| 223 | del_url = 'http://%s:%d/api/v1/namespaces/%s/pods/%s' % (kube_host, kube_port, |
| 224 | namespace, pod_name) |
sreek | bfe37a8 | 2015-11-16 18:57:33 -0800 | [diff] [blame] | 225 | return _do_delete(del_url, 'Delete Pod') |
Sree Kuchibhotla | 61c134f | 2016-02-26 11:03:29 -0800 | [diff] [blame] | 226 | |
| 227 | |
| 228 | def create_pod_and_service(kube_host, kube_port, namespace, pod_name, |
| 229 | image_name, container_port_list, cmd_list, arg_list, |
| 230 | env_dict, is_headless_service): |
Sree Kuchibhotla | da25fdb | 2016-02-26 13:41:06 -0800 | [diff] [blame] | 231 | """A helper function that creates a pod and a service (if pod creation was successful).""" |
Sree Kuchibhotla | 61c134f | 2016-02-26 11:03:29 -0800 | [diff] [blame] | 232 | is_success = create_pod(kube_host, kube_port, namespace, pod_name, image_name, |
| 233 | container_port_list, cmd_list, arg_list, env_dict) |
| 234 | if not is_success: |
| 235 | print 'Error in creating Pod' |
| 236 | return False |
| 237 | |
| 238 | is_success = create_service( |
| 239 | kube_host, |
| 240 | kube_port, |
| 241 | namespace, |
| 242 | pod_name, # Use pod_name for service |
| 243 | pod_name, |
| 244 | container_port_list, # Service port list same as container port list |
| 245 | container_port_list, |
| 246 | is_headless_service) |
| 247 | if not is_success: |
| 248 | print 'Error in creating Service' |
| 249 | return False |
| 250 | |
| 251 | print 'Successfully created the pod/service %s' % pod_name |
| 252 | return True |
| 253 | |
| 254 | |
| 255 | def delete_pod_and_service(kube_host, kube_port, namespace, pod_name): |
Sree Kuchibhotla | da25fdb | 2016-02-26 13:41:06 -0800 | [diff] [blame] | 256 | """ A helper function that calls delete_pod and delete_service """ |
Sree Kuchibhotla | 61c134f | 2016-02-26 11:03:29 -0800 | [diff] [blame] | 257 | is_success = delete_pod(kube_host, kube_port, namespace, pod_name) |
| 258 | if not is_success: |
| 259 | print 'Error in deleting pod %s' % pod_name |
| 260 | return False |
| 261 | |
| 262 | # Note: service name assumed to the the same as pod name |
| 263 | is_success = delete_service(kube_host, kube_port, namespace, pod_name) |
| 264 | if not is_success: |
| 265 | print 'Error in deleting service %s' % pod_name |
| 266 | return False |
| 267 | |
| 268 | print 'Successfully deleted the Pod/Service: %s' % pod_name |
| 269 | return True |