1# Licensed to the Apache Software Foundation (ASF) under one or more 2# contributor license agreements. See the NOTICE file distributed with 3# this work for additional information regarding copyright ownership. 4# The ASF licenses this file to You under the Apache License, Version 2.0 5# (the "License"); you may not use this file except in compliance with 6# the License. You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16try: 17 import simplejson as json 18except ImportError: 19 import json 20 21from libcloud.container.base import (ContainerDriver, Container, 22 ContainerCluster, ContainerImage) 23from libcloud.container.types import ContainerState 24from libcloud.container.utils.docker import RegistryClient 25from libcloud.common.aws import SignedAWSConnection, AWSJsonResponse 26 27__all__ = [ 28 'ElasticContainerDriver' 29] 30 31 32ECS_VERSION = '2014-11-13' 33ECR_VERSION = '2015-09-21' 34ECS_HOST = 'ecs.%s.amazonaws.com' 35ECR_HOST = 'ecr.%s.amazonaws.com' 36ROOT = '/' 37ECS_TARGET_BASE = 'AmazonEC2ContainerServiceV%s' % \ 38 (ECS_VERSION.replace('-', '')) 39ECR_TARGET_BASE = 'AmazonEC2ContainerRegistry_V%s' % \ 40 (ECR_VERSION.replace('-', '')) 41 42 43class ECSJsonConnection(SignedAWSConnection): 44 version = ECS_VERSION 45 host = ECS_HOST 46 responseCls = AWSJsonResponse 47 service_name = 'ecs' 48 49 50class ECRJsonConnection(SignedAWSConnection): 51 version = ECR_VERSION 52 host = ECR_HOST 53 responseCls = AWSJsonResponse 54 service_name = 'ecr' 55 56 57class ElasticContainerDriver(ContainerDriver): 58 name = 'Amazon Elastic Container Service' 59 website = 'https://aws.amazon.com/ecs/details/' 60 ecr_repository_host = '%s.dkr.ecr.%s.amazonaws.com' 61 connectionCls = ECSJsonConnection 62 ecrConnectionClass = ECRJsonConnection 63 supports_clusters = False 64 status_map = { 65 'RUNNING': ContainerState.RUNNING 66 } 67 68 def __init__(self, access_id, secret, region): 69 super(ElasticContainerDriver, self).__init__(access_id, secret) 70 self.region = region 71 self.region_name = region 72 self.connection.host = ECS_HOST % (region) 73 74 # Setup another connection class for ECR 75 conn_kwargs = self._ex_connection_class_kwargs() 76 self.ecr_connection = self.ecrConnectionClass( 77 access_id, secret, **conn_kwargs) 78 self.ecr_connection.host = ECR_HOST % (region) 79 self.ecr_connection.driver = self 80 self.ecr_connection.connect() 81 82 def _ex_connection_class_kwargs(self): 83 return {'signature_version': '4'} 84 85 def list_images(self, ex_repository_name): 86 """ 87 List the images in an ECR repository 88 89 :param ex_repository_name: The name of the repository to check 90 defaults to the default repository. 91 :type ex_repository_name: ``str`` 92 93 :return: a list of images 94 :rtype: ``list`` of :class:`libcloud.container.base.ContainerImage` 95 """ 96 request = {} 97 request['repositoryName'] = ex_repository_name 98 list_response = self.ecr_connection.request( 99 ROOT, 100 method='POST', 101 data=json.dumps(request), 102 headers=self._get_ecr_headers('ListImages') 103 ).object 104 repository_id = self.ex_get_repository_id(ex_repository_name) 105 host = self._get_ecr_host(repository_id) 106 return self._to_images(list_response['imageIds'], 107 host, 108 ex_repository_name) 109 110 def list_clusters(self): 111 """ 112 Get a list of potential locations to deploy clusters into 113 114 :param location: The location to search in 115 :type location: :class:`libcloud.container.base.ClusterLocation` 116 117 :rtype: ``list`` of :class:`libcloud.container.base.ContainerCluster` 118 """ 119 listdata = self.connection.request( 120 ROOT, 121 method='POST', 122 data=json.dumps({}), 123 headers=self._get_headers('ListClusters') 124 ).object 125 request = {'clusters': listdata['clusterArns']} 126 data = self.connection.request( 127 ROOT, 128 method='POST', 129 data=json.dumps(request), 130 headers=self._get_headers('DescribeClusters') 131 ).object 132 return self._to_clusters(data) 133 134 def create_cluster(self, name, location=None): 135 """ 136 Create a container cluster 137 138 :param name: The name of the cluster 139 :type name: ``str`` 140 141 :param location: The location to create the cluster in 142 :type location: :class:`libcloud.container.base.ClusterLocation` 143 144 :rtype: :class:`libcloud.container.base.ContainerCluster` 145 """ 146 request = {'clusterName': name} 147 response = self.connection.request( 148 ROOT, 149 method='POST', 150 data=json.dumps(request), 151 headers=self._get_headers('CreateCluster') 152 ).object 153 return self._to_cluster(response['cluster']) 154 155 def destroy_cluster(self, cluster): 156 """ 157 Delete a cluster 158 159 :return: ``True`` if the destroy was successful, otherwise ``False``. 160 :rtype: ``bool`` 161 """ 162 request = {'cluster': cluster.id} 163 data = self.connection.request( 164 ROOT, 165 method='POST', 166 data=json.dumps(request), 167 headers=self._get_headers('DeleteCluster') 168 ).object 169 return data['cluster']['status'] == 'INACTIVE' 170 171 def list_containers(self, image=None, cluster=None): 172 """ 173 List the deployed container images 174 175 :param image: Filter to containers with a certain image 176 :type image: :class:`libcloud.container.base.ContainerImage` 177 178 :param cluster: Filter to containers in a cluster 179 :type cluster: :class:`libcloud.container.base.ContainerCluster` 180 181 :rtype: ``list`` of :class:`libcloud.container.base.Container` 182 """ 183 request = {'cluster': 'default'} 184 if cluster is not None: 185 request['cluster'] = cluster.id 186 if image is not None: 187 request['family'] = image.name 188 list_response = self.connection.request( 189 ROOT, 190 method='POST', 191 data=json.dumps(request), 192 headers=self._get_headers('ListTasks') 193 ).object 194 if len(list_response['taskArns']) == 0: 195 return [] 196 containers = self.ex_list_containers_for_task( 197 list_response['taskArns']) 198 return containers 199 200 def deploy_container(self, name, image, cluster=None, 201 parameters=None, start=True, ex_cpu=10, ex_memory=500, 202 ex_container_port=None, ex_host_port=None): 203 """ 204 Creates a task definition from a container image that can be run 205 in a cluster. 206 207 :param name: The name of the new container 208 :type name: ``str`` 209 210 :param image: The container image to deploy 211 :type image: :class:`libcloud.container.base.ContainerImage` 212 213 :param cluster: The cluster to deploy to, None is default 214 :type cluster: :class:`libcloud.container.base.ContainerCluster` 215 216 :param parameters: Container Image parameters 217 :type parameters: ``str`` 218 219 :param start: Start the container on deployment 220 :type start: ``bool`` 221 222 :rtype: :class:`libcloud.container.base.Container` 223 """ 224 data = {} 225 if ex_container_port is None and ex_host_port is None: 226 port_maps = [] 227 else: 228 port_maps = [ 229 { 230 "containerPort": ex_container_port, 231 "hostPort": ex_host_port 232 } 233 ] 234 data['containerDefinitions'] = [ 235 { 236 "mountPoints": [], 237 "name": name, 238 "image": image.name, 239 "cpu": ex_cpu, 240 "environment": [], 241 "memory": ex_memory, 242 "portMappings": port_maps, 243 "essential": True, 244 "volumesFrom": [] 245 } 246 ] 247 data['family'] = name 248 response = self.connection.request( 249 ROOT, 250 method='POST', 251 data=json.dumps(data), 252 headers=self._get_headers('RegisterTaskDefinition') 253 ).object 254 if start: 255 return self.ex_start_task( 256 response['taskDefinition']['taskDefinitionArn'])[0] 257 else: 258 return Container( 259 id=None, 260 name=name, 261 image=image, 262 state=ContainerState.RUNNING, 263 ip_addresses=[], 264 extra={ 265 'taskDefinitionArn': 266 response['taskDefinition']['taskDefinitionArn'] 267 }, 268 driver=self.connection.driver 269 ) 270 271 def get_container(self, id): 272 """ 273 Get a container by ID 274 275 :param id: The ID of the container to get 276 :type id: ``str`` 277 278 :rtype: :class:`libcloud.container.base.Container` 279 """ 280 containers = self.ex_list_containers_for_task([id]) 281 return containers[0] 282 283 def start_container(self, container, count=1): 284 """ 285 Start a deployed task 286 287 :param container: The container to start 288 :type container: :class:`libcloud.container.base.Container` 289 290 :param count: Number of containers to start 291 :type count: ``int`` 292 293 :rtype: :class:`libcloud.container.base.Container` 294 """ 295 return self.ex_start_task(container.extra['taskDefinitionArn'], count) 296 297 def stop_container(self, container): 298 """ 299 Stop a deployed container 300 301 :param container: The container to stop 302 :type container: :class:`libcloud.container.base.Container` 303 304 :rtype: :class:`libcloud.container.base.Container` 305 """ 306 request = {'task': container.extra['taskArn']} 307 response = self.connection.request( 308 ROOT, 309 method='POST', 310 data=json.dumps(request), 311 headers=self._get_headers('StopTask') 312 ).object 313 containers = [] 314 containers.extend(self._to_containers( 315 response['task'], 316 container.extra['taskDefinitionArn'])) 317 return containers 318 319 def restart_container(self, container): 320 """ 321 Restart a deployed container 322 323 :param container: The container to restart 324 :type container: :class:`libcloud.container.base.Container` 325 326 :rtype: :class:`libcloud.container.base.Container` 327 """ 328 self.stop_container(container) 329 return self.start_container(container) 330 331 def destroy_container(self, container): 332 """ 333 Destroy a deployed container 334 335 :param container: The container to destroy 336 :type container: :class:`libcloud.container.base.Container` 337 338 :rtype: :class:`libcloud.container.base.Container` 339 """ 340 return self.stop_container(container) 341 342 def ex_start_task(self, task_arn, count=1): 343 """ 344 Run a task definition and get the containers 345 346 :param task_arn: The task ARN to Run 347 :type task_arn: ``str`` 348 349 :param count: The number of containers to start 350 :type count: ``int`` 351 352 :rtype: ``list`` of :class:`libcloud.container.base.Container` 353 """ 354 request = None 355 request = {'count': count, 356 'taskDefinition': task_arn} 357 response = self.connection.request( 358 ROOT, 359 method='POST', 360 data=json.dumps(request), 361 headers=self._get_headers('RunTask') 362 ).object 363 containers = [] 364 for task in response['tasks']: 365 containers.extend(self._to_containers(task, task_arn)) 366 return containers 367 368 def ex_list_containers_for_task(self, task_arns): 369 """ 370 Get a list of containers by ID collection (ARN) 371 372 :param task_arns: The list of ARNs 373 :type task_arns: ``list`` of ``str`` 374 375 :rtype: ``list`` of :class:`libcloud.container.base.Container` 376 """ 377 describe_request = {'tasks': task_arns} 378 descripe_response = self.connection.request( 379 ROOT, 380 method='POST', 381 data=json.dumps(describe_request), 382 headers=self._get_headers('DescribeTasks') 383 ).object 384 containers = [] 385 for task in descripe_response['tasks']: 386 containers.extend(self._to_containers( 387 task, task['taskDefinitionArn'])) 388 return containers 389 390 def ex_create_service(self, name, cluster, 391 task_definition, desired_count=1): 392 """ 393 Runs and maintains a desired number of tasks from a specified 394 task definition. If the number of tasks running in a service 395 drops below desired_count, Amazon ECS spawns another 396 instantiation of the task in the specified cluster. 397 398 :param name: the name of the service 399 :type name: ``str`` 400 401 :param cluster: The cluster to run the service on 402 :type cluster: :class:`libcloud.container.base.ContainerCluster` 403 404 :param task_definition: The task definition name or ARN for the 405 service 406 :type task_definition: ``str`` 407 408 :param desired_count: The desired number of tasks to be running 409 at any one time 410 :type desired_count: ``int`` 411 412 :rtype: ``object`` The service object 413 """ 414 request = { 415 'serviceName': name, 416 'taskDefinition': task_definition, 417 'desiredCount': desired_count, 418 'cluster': cluster.id} 419 response = self.connection.request( 420 ROOT, 421 method='POST', 422 data=json.dumps(request), 423 headers=self._get_headers('CreateService') 424 ).object 425 return response['service'] 426 427 def ex_list_service_arns(self, cluster=None): 428 """ 429 List the services 430 431 :param cluster: The cluster hosting the services 432 :type cluster: :class:`libcloud.container.base.ContainerCluster` 433 434 :rtype: ``list`` of ``str`` 435 """ 436 request = {} 437 if cluster is not None: 438 request['cluster'] = cluster.id 439 response = self.connection.request( 440 ROOT, 441 method='POST', 442 data=json.dumps(request), 443 headers=self._get_headers('ListServices') 444 ).object 445 return response['serviceArns'] 446 447 def ex_describe_service(self, service_arn): 448 """ 449 Get the details of a service 450 451 :param cluster: The hosting cluster 452 :type cluster: :class:`libcloud.container.base.ContainerCluster` 453 454 :param service_arn: The service ARN to describe 455 :type service_arn: ``str`` 456 457 :return: The service object 458 :rtype: ``object`` 459 """ 460 request = {'services': [service_arn]} 461 response = self.connection.request( 462 ROOT, 463 method='POST', 464 data=json.dumps(request), 465 headers=self._get_headers('DescribeServices') 466 ).object 467 return response['services'][0] 468 469 def ex_destroy_service(self, service_arn): 470 """ 471 Deletes a service 472 473 :param cluster: The target cluster 474 :type cluster: :class:`libcloud.container.base.ContainerCluster` 475 476 :param service_arn: The service ARN to destroy 477 :type service_arn: ``str`` 478 """ 479 request = { 480 'service': service_arn} 481 response = self.connection.request( 482 ROOT, 483 method='POST', 484 data=json.dumps(request), 485 headers=self._get_headers('DeleteService') 486 ).object 487 return response['service'] 488 489 def ex_get_registry_client(self, repository_name): 490 """ 491 Get a client for an ECR repository 492 493 :param repository_name: The unique name of the repository 494 :type repository_name: ``str`` 495 496 :return: a docker registry API client 497 :rtype: :class:`libcloud.container.utils.docker.RegistryClient` 498 """ 499 repository_id = self.ex_get_repository_id(repository_name) 500 token = self.ex_get_repository_token(repository_id) 501 host = self._get_ecr_host(repository_id) 502 return RegistryClient( 503 host=host, 504 username='AWS', 505 password=token 506 ) 507 508 def ex_get_repository_token(self, repository_id): 509 """ 510 Get the authorization token (12 hour expiry) for a repository 511 512 :param repository_id: The ID of the repository 513 :type repository_id: ``str`` 514 515 :return: A token for login 516 :rtype: ``str`` 517 """ 518 request = {'RegistryIds': [repository_id]} 519 response = self.ecr_connection.request( 520 ROOT, 521 method='POST', 522 data=json.dumps(request), 523 headers=self._get_ecr_headers('GetAuthorizationToken') 524 ).object 525 return response['authorizationData'][0]['authorizationToken'] 526 527 def ex_get_repository_id(self, repository_name): 528 """ 529 Get the ID of a repository 530 531 :param repository_name: The unique name of the repository 532 :type repository_name: ``str`` 533 534 :return: The repository ID 535 :rtype: ``str`` 536 """ 537 request = {'repositoryNames': [repository_name]} 538 list_response = self.ecr_connection.request( 539 ROOT, 540 method='POST', 541 data=json.dumps(request), 542 headers=self._get_ecr_headers('DescribeRepositories') 543 ).object 544 repository_id = list_response['repositories'][0]['registryId'] 545 return repository_id 546 547 def _get_ecr_host(self, repository_id): 548 return self.ecr_repository_host % ( 549 repository_id, 550 self.region) 551 552 def _get_headers(self, action): 553 """ 554 Get the default headers for a request to the ECS API 555 """ 556 return {'x-amz-target': '%s.%s' % 557 (ECS_TARGET_BASE, action), 558 'Content-Type': 'application/x-amz-json-1.1' 559 } 560 561 def _get_ecr_headers(self, action): 562 """ 563 Get the default headers for a request to the ECR API 564 """ 565 return {'x-amz-target': '%s.%s' % 566 (ECR_TARGET_BASE, action), 567 'Content-Type': 'application/x-amz-json-1.1' 568 } 569 570 def _to_clusters(self, data): 571 clusters = [] 572 for cluster in data['clusters']: 573 clusters.append(self._to_cluster(cluster)) 574 return clusters 575 576 def _to_cluster(self, data): 577 return ContainerCluster( 578 id=data['clusterArn'], 579 name=data['clusterName'], 580 driver=self.connection.driver 581 ) 582 583 def _to_containers(self, data, task_definition_arn): 584 clusters = [] 585 for cluster in data['containers']: 586 clusters.append(self._to_container(cluster, task_definition_arn)) 587 return clusters 588 589 def _to_container(self, data, task_definition_arn): 590 return Container( 591 id=data['containerArn'], 592 name=data['name'], 593 image=ContainerImage( 594 id=None, 595 name=data['name'], 596 path=None, 597 version=None, 598 driver=self.connection.driver 599 ), 600 ip_addresses=None, 601 state=self.status_map.get(data['lastStatus'], None), 602 extra={ 603 'taskArn': data['taskArn'], 604 'taskDefinitionArn': task_definition_arn 605 }, 606 driver=self.connection.driver 607 ) 608 609 def _to_images(self, data, host, repository_name): 610 images = [] 611 for image in data: 612 images.append(self._to_image(image, host, repository_name)) 613 return images 614 615 def _to_image(self, data, host, repository_name): 616 path = '%s/%s:%s' % ( 617 host, 618 repository_name, 619 data['imageTag'] 620 ) 621 return ContainerImage( 622 id=None, 623 name=path, 624 path=path, 625 version=data['imageTag'], 626 driver=self.connection.driver 627 ) 628