github.com/uber/kraken@v0.1.4/test/python/components.py (about)

     1  # Copyright (c) 2016-2019 Uber Technologies, Inc.
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #     http://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  from __future__ import absolute_import
    15  
    16  import os
    17  import platform
    18  import random
    19  import subprocess
    20  import time
    21  import urllib
    22  from contextlib import contextmanager
    23  from io import BytesIO
    24  from os.path import abspath
    25  from Queue import Queue
    26  from socket import socket
    27  from threading import Thread
    28  
    29  import requests
    30  
    31  from uploader import Uploader
    32  from utils import (
    33      PortReservation,
    34      dev_tag,
    35      find_free_port,
    36      format_insecure_curl,
    37      tls_opts,
    38  )
    39  
    40  
    41  def get_docker_bridge():
    42      system = platform.system()
    43      if system == 'Darwin':
    44          return 'host.docker.internal'
    45      elif system == 'Linux':
    46          return '172.17.0.1'
    47      else:
    48          raise Exception('unknown system: ' + system)
    49  
    50  
    51  def print_logs(container):
    52      title = ' {name} logs '.format(name=container.name)
    53      left_border = '<' * 20
    54      right_border = '>' * 20
    55      fill = ('<' * (len(title) / 2)) + ('>' * (len(title) / 2))
    56      print '{l}{title}{r}'.format(l=left_border, title=title, r=right_border)
    57      print container.logs()
    58      print '{l}{fill}{r}'.format(l=left_border, fill=fill, r=right_border)
    59  
    60  
    61  def yaml_list(l):
    62      return '[' + ','.join(map(lambda x: "'" + str(x) + "'", l)) + ']'
    63  
    64  
    65  def pull(source, image):
    66      cmd = [
    67          'tools/bin/puller/puller', '-source', source, '-image', image,
    68      ]
    69      assert subprocess.call(cmd, stderr=subprocess.STDOUT) == 0
    70  
    71  
    72  class HealthCheck(object):
    73  
    74      def __init__(self, cmd, interval=1, min_consecutive_successes=1, timeout=10):
    75          self.cmd = cmd
    76          self.interval = interval
    77          self.min_consecutive_successes = min_consecutive_successes
    78          self.timeout = timeout
    79  
    80      def run(self, container):
    81          start_time = time.time()
    82          successes = 0
    83          msg = ''
    84          while time.time() - start_time < self.timeout:
    85              try:
    86                  # We can't use container.exec_run since it doesn't expose exit code.
    87                  subprocess.check_output(
    88                      'docker exec {name} {cmd}'.format(name=container.name, cmd=self.cmd),
    89                      shell=True)
    90                  successes += 1
    91                  if successes >= self.min_consecutive_successes:
    92                      return
    93              except Exception as e:
    94                  msg = str(e)
    95                  successes = 0
    96              time.sleep(self.interval)
    97  
    98          raise RuntimeError('Health check failure: {msg}'.format(msg=msg))
    99  
   100  
   101  class DockerContainer(object):
   102  
   103      def __init__(self, name, image, command=None, ports=None, volumes=None, user=None):
   104          self.name = name
   105          self.image = image
   106  
   107          self.command = []
   108          if command:
   109              self.command = command
   110  
   111          self.ports = []
   112          if ports:
   113              for i, o in ports.iteritems():
   114                  self.ports.extend(['-p', '{o}:{i}'.format(i=i, o=o)])
   115  
   116          self.volumes = []
   117          if volumes:
   118              for o, i in volumes.iteritems():
   119                  bind = i['bind']
   120                  mode = i['mode']
   121                  self.volumes.extend(['-v', '{o}:{bind}:{mode}'.format(o=o, bind=bind, mode=mode)])
   122  
   123          self.user = ['-u', user] if user else []
   124  
   125      def run(self):
   126          cmd = [
   127              'docker', 'run',
   128              '-d',
   129              '--name=' + self.name,
   130          ]
   131          cmd.extend(self.ports)
   132          cmd.extend(self.volumes)
   133          cmd.extend(self.user)
   134          cmd.append(self.image)
   135          cmd.extend(self.command)
   136          assert subprocess.call(cmd) == 0
   137  
   138      def logs(self):
   139          subprocess.call(['docker', 'logs', self.name])
   140  
   141      def remove(self, force=False):
   142          cmd = [
   143              'docker', 'rm',
   144          ]
   145          if force:
   146              cmd.append('-f')
   147          cmd.append(self.name)
   148          assert subprocess.call(cmd) == 0
   149  
   150  
   151  def new_docker_container(name, image, command=None, environment=None, ports=None,
   152                           volumes=None, health_check=None, user=None):
   153      """
   154      Creates and starts a detached Docker container. If health_check is specified,
   155      ensures the container is healthy before returning.
   156      """
   157      if command:
   158          # Set umask so jenkins user can delete files created by non-jenkins user.
   159          command = ['bash', '-c', 'umask 0000 && {command}'.format(command=' '.join(command))]
   160  
   161      c = DockerContainer(
   162          name=name,
   163          image=image,
   164          command=command,
   165          ports=ports,
   166          volumes=volumes,
   167          user=user)
   168      c.run()
   169      print 'Starting container {}'.format(c.name)
   170      try:
   171          if health_check:
   172              health_check.run(c)
   173          else:
   174              print 'No health checks supplied for {name}'.format(name=c.name)
   175      except:
   176          print_logs(c)
   177          raise
   178      return c
   179  
   180  
   181  def populate_config_template(kname, filename, **kwargs):
   182      """
   183      Populates a test config template with kwargs for Kraken name `kname`
   184      and writes the result to the config directory of `kname` with filename.
   185      """
   186      template = abspath('config/{kname}/test.template'.format(kname=kname))
   187      yaml = abspath('config/{kname}/{filename}'.format(kname=kname, filename=filename))
   188  
   189      with open(template) as f:
   190          config = f.read().format(**kwargs)
   191  
   192      with open(yaml, 'w') as f:
   193          f.write(config)
   194  
   195  
   196  def init_cache(cname):
   197      """
   198      Wipes and initializes a cache dir for container name `cname`.
   199      """
   200      cache = abspath('.tmptest/test-kraken-integration/{cname}/cache'.format(cname=cname))
   201      if os.path.exists(cache):
   202          subprocess.check_call(['rm', '-rf', cache])
   203      os.makedirs(cache)
   204      os.chmod(cache, 0777)
   205      return cache
   206  
   207  
   208  def create_volumes(kname, cname, local_cache=True):
   209      """
   210      Creates volume bindings for Kraken name `kname` and container name `cname`.
   211      """
   212      volumes = {}
   213  
   214      # Mount configuration directory. This is necessary for components which
   215      # populate templates and need to mount the populated template into the
   216      # container.
   217      config = abspath('config/{kname}'.format(kname=kname))
   218      volumes[config] = {
   219          'bind': '/etc/kraken/config/{kname}'.format(kname=kname),
   220          'mode': 'ro',
   221      }
   222  
   223      if local_cache:
   224          # Mount local cache. Allows components to simulate unavailability whilst
   225          # retaining their state on disk.
   226          cache = init_cache(cname)
   227          volumes[cache] = {
   228              'bind': '/var/cache/kraken/kraken-{kname}/'.format(kname=kname),
   229              'mode': 'rw',
   230          }
   231  
   232      return volumes
   233  
   234  
   235  class Component(object):
   236      """
   237      Base class for all containerized Kraken components. Each subclass implements
   238      the container property for exposing its underlying Docker container, and Component
   239      provides utilities acting upon said container.
   240      """
   241      def new_container(self):
   242          """
   243          Initializes a new container. All subclasses must implement this method.
   244          """
   245          raise NotImplementedError
   246  
   247      def start(self):
   248          self.container = self.new_container()
   249  
   250      def stop(self, wipe_disk=False):
   251          self.container.remove(force=True)
   252          if wipe_disk:
   253              cache = init_cache(self.container.name)
   254  
   255      def restart(self, wipe_disk=False):
   256          self.stop(wipe_disk=wipe_disk)
   257          # When a container is removed, there is a race condition
   258          # when starting the container with the same command right away,
   259          # which causes the start command to fail.
   260          # Sleep for one second to make sure that the container is really
   261          # removed from docker.
   262          time.sleep(1)
   263          self.start()
   264  
   265      def print_logs(self):
   266          print_logs(self.container)
   267  
   268      def teardown(self):
   269          try:
   270              self.print_logs()
   271              self.stop()
   272          except Exception as e:
   273              print 'Teardown {name} failed: {e}'.format(name=self.container.name, e=e)
   274  
   275  
   276  class Redis(Component):
   277  
   278      def __init__(self, zone):
   279          self.zone = zone
   280          self.port = find_free_port()
   281          self.start()
   282  
   283      def new_container(self):
   284          return new_docker_container(
   285              name='kraken-redis-{zone}'.format(zone=self.zone),
   286              image='redis:latest',
   287              ports={6379: self.port},
   288              health_check=HealthCheck('redis-cli ping'))
   289  
   290      @property
   291      def addr(self):
   292          return '{}:{}'.format(get_docker_bridge(), self.port)
   293  
   294  
   295  class Tracker(Component):
   296  
   297      def __init__(self, zone, redis, origin_cluster):
   298          self.zone = zone
   299          self.redis = redis
   300          self.origin_cluster = origin_cluster
   301          self.port = find_free_port()
   302          self.config_file = 'test-{zone}.yaml'.format(zone=zone)
   303          self.name = 'kraken-tracker-{zone}'.format(zone=zone)
   304  
   305          populate_config_template(
   306              'tracker',
   307              self.config_file,
   308              redis=self.redis.addr,
   309              origins=yaml_list([o.addr for o in self.origin_cluster.origins]))
   310  
   311          self.volumes = create_volumes('tracker', self.name)
   312  
   313          self.start()
   314  
   315      def new_container(self):
   316          return new_docker_container(
   317              name=self.name,
   318              image=dev_tag('kraken-tracker'),
   319              environment={},
   320              ports={self.port: self.port},
   321              volumes=self.volumes,
   322              command=[
   323                  '/usr/bin/kraken-tracker',
   324                  '--config=/etc/kraken/config/tracker/{config}'.format(config=self.config_file),
   325                  '--port={port}'.format(port=self.port)],
   326              health_check=HealthCheck(format_insecure_curl('localhost:{port}/health'.format(port=self.port))))
   327  
   328      @property
   329      def addr(self):
   330          return '{}:{}'.format(get_docker_bridge(), self.port)
   331  
   332  
   333  class Origin(Component):
   334  
   335      class Instance(object):
   336  
   337          def __init__(self, name):
   338              self.name = name
   339              self.hostname = get_docker_bridge()
   340              self.port_rez = PortReservation()
   341              self.peer_port = find_free_port()
   342  
   343          @property
   344          def port(self):
   345              return self.port_rez.get()
   346  
   347          @property
   348          def addr(self):
   349              return '{}:{}'.format(self.hostname, self.port)
   350  
   351      def __init__(self, zone, instances, name, testfs):
   352          self.zone = zone
   353          self.instance = instances[name]
   354          self.testfs = testfs
   355          self.config_file = 'test-{zone}.yaml'.format(zone=zone)
   356          self.name = '{name}-{zone}'.format(name=self.instance.name, zone=zone)
   357  
   358          populate_config_template(
   359              'origin',
   360              self.config_file,
   361              origins=yaml_list([i.addr for i in instances.values()]),
   362              testfs=self.testfs.addr)
   363  
   364          self.volumes = create_volumes('origin', self.name)
   365  
   366          self.start()
   367  
   368      def new_container(self):
   369          self.instance.port_rez.release()
   370          return new_docker_container(
   371              name=self.name,
   372              image=dev_tag('kraken-origin'),
   373              volumes=self.volumes,
   374              environment={},
   375              ports={
   376                  self.instance.port: self.instance.port,
   377                  self.instance.peer_port: self.instance.peer_port,
   378              },
   379              command=[
   380                  '/usr/bin/kraken-origin',
   381                  '--config=/etc/kraken/config/origin/{config}'.format(config=self.config_file),
   382                  '--blobserver-port={port}'.format(port=self.instance.port),
   383                  '--blobserver-hostname={hostname}'.format(hostname=self.instance.hostname),
   384                  '--peer-ip={ip}'.format(ip=get_docker_bridge()),
   385                  '--peer-port={port}'.format(port=self.instance.peer_port),
   386              ],
   387              health_check=HealthCheck(format_insecure_curl('https://localhost:{}/health'.format(self.instance.port))))
   388  
   389      @property
   390      def addr(self):
   391          return self.instance.addr
   392  
   393  
   394  class OriginCluster(object):
   395  
   396      def __init__(self, origins):
   397          self.origins = origins
   398  
   399      def get_location(self, name):
   400          url = 'https://localhost:{port}/blobs/sha256:{name}/locations'.format(
   401              port=random.choice(self.origins).instance.port, name=name)
   402          res = requests.get(url, **tls_opts())
   403          res.raise_for_status()
   404          addr = random.choice(res.headers['Origin-Locations'].split(','))
   405          # Origin addresses are configured under the bridge network, but we
   406          # need to speak via localhost.
   407          addr = addr.replace(get_docker_bridge(), 'localhost')
   408          return addr
   409  
   410      def upload(self, name, blob):
   411          addr = self.get_location(name)
   412          Uploader(addr).upload(name, blob)
   413  
   414      def __iter__(self):
   415          return iter(self.origins)
   416  
   417  
   418  class Agent(Component):
   419  
   420      def __init__(self, zone, id, tracker, build_indexes, with_docker_socket=False):
   421          self.zone = zone
   422          self.id = id
   423          self.tracker = tracker
   424          self.build_indexes = build_indexes
   425          self.torrent_client_port = find_free_port()
   426          self.registry_port = find_free_port()
   427          self.port = find_free_port()
   428          self.config_file = 'test-{zone}.yaml'.format(zone=zone)
   429          self.name = 'kraken-agent-{id}-{zone}'.format(id=id, zone=zone)
   430          self.with_docker_socket = with_docker_socket
   431  
   432          populate_config_template(
   433              'agent',
   434              self.config_file,
   435              trackers=yaml_list([self.tracker.addr]),
   436              build_indexes=yaml_list([bi.addr for bi in self.build_indexes]))
   437  
   438          if self.with_docker_socket:
   439              # In aditional to the need to mount docker socket, also avoid using
   440              # local cache volume, otherwise the process would run as root and
   441              # create local cache files that's hard to clean outside of the
   442              # container.
   443              self.volumes = create_volumes('agent', self.name, local_cache=False)
   444              self.volumes['/var/run/docker.sock'] = {
   445                  'bind': '/var/run/docker.sock',
   446                  'mode': 'rw',
   447              }
   448          else:
   449              self.volumes = create_volumes('agent', self.name)
   450  
   451          self.start()
   452  
   453      def new_container(self):
   454          # Root user is needed for accessing docker socket.
   455          user = 'root' if self.with_docker_socket else None
   456          return new_docker_container(
   457              name=self.name,
   458              image=dev_tag('kraken-agent'),
   459              environment={},
   460              ports={
   461                  self.torrent_client_port: self.torrent_client_port,
   462                  self.registry_port: self.registry_port,
   463                  self.port: self.port,
   464              },
   465              volumes=self.volumes,
   466              command=[
   467                  '/usr/bin/kraken-agent',
   468                  '--config=/etc/kraken/config/agent/{config}'.format(config=self.config_file),
   469                  '--peer-ip={}'.format(get_docker_bridge()),
   470                  '--peer-port={port}'.format(port=self.torrent_client_port),
   471                  '--agent-server-port={port}'.format(port=self.port),
   472                  '--agent-registry-port={port}'.format(port=self.registry_port),
   473              ],
   474              health_check=HealthCheck('curl localhost:{port}/health'.format(port=self.port)),
   475              user=user)
   476  
   477      @property
   478      def registry(self):
   479          return '127.0.0.1:{port}'.format(port=self.registry_port)
   480  
   481      def download(self, name, expected):
   482          url = 'http://localhost:{port}/namespace/testfs/blobs/{name}'.format(
   483              port=self.port, name=name)
   484          s = requests.session()
   485          s.keep_alive = False
   486          res = s.get(url, stream=True, timeout=60)
   487          res.raise_for_status()
   488          assert res.content == expected
   489  
   490      def pull(self, image):
   491          return pull(self.registry, image)
   492  
   493      def preload(self, image):
   494          url = 'http://127.0.0.1:{port}/preload/tags/{image}'.format(
   495              port=self.port, image=urllib.quote(image, safe=''))
   496          s = requests.session()
   497          s.keep_alive = False
   498          res = s.get(url, stream=True, timeout=60)
   499          res.raise_for_status()
   500  
   501  
   502  class AgentFactory(object):
   503  
   504      def __init__(self, zone, tracker, build_indexes):
   505          self.zone = zone
   506          self.tracker = tracker
   507          self.build_indexes = build_indexes
   508  
   509      @contextmanager
   510      def create(self, n=1, with_docker_socket=False):
   511          agents = [Agent(self.zone, i, self.tracker, self.build_indexes, with_docker_socket) for i in range(n)]
   512          try:
   513              if len(agents) == 1:
   514                  yield agents[0]
   515              else:
   516                  yield agents
   517          finally:
   518              for agent in agents:
   519                  agent.teardown()
   520  
   521  
   522  class Proxy(Component):
   523  
   524      def __init__(self, zone, origin_cluster, build_indexes):
   525          self.zone = zone
   526          self.origin_cluster = origin_cluster
   527          self.build_indexes = build_indexes
   528          self.port = find_free_port()
   529          self.config_file = 'test-{zone}.yaml'.format(zone=zone)
   530          self.name = 'kraken-proxy-{zone}'.format(zone=zone)
   531  
   532          populate_config_template(
   533              'proxy',
   534              self.config_file,
   535              build_indexes=yaml_list([bi.addr for bi in self.build_indexes]),
   536              origins=yaml_list([o.addr for o in self.origin_cluster.origins]))
   537  
   538          self.volumes = create_volumes('proxy', self.name)
   539  
   540          self.start()
   541  
   542      def new_container(self):
   543          return new_docker_container(
   544              name=self.name,
   545              image=dev_tag('kraken-proxy'),
   546              ports={self.port: self.port},
   547              environment={},
   548              command=[
   549                  '/usr/bin/kraken-proxy',
   550                  '--config=/etc/kraken/config/proxy/{config}'.format(config=self.config_file),
   551                  '--port={port}'.format(port=self.port),
   552              ],
   553              volumes=self.volumes,
   554              health_check=HealthCheck('curl localhost:{port}/v2/'.format(port=self.port)))
   555  
   556      @property
   557      def registry(self):
   558          return '127.0.0.1:{port}'.format(port=self.port)
   559  
   560      def push(self, image):
   561          proxy_image = '{reg}/{img}'.format(reg=self.registry, img=image)
   562          for command in [
   563              ['docker', 'tag', image, proxy_image],
   564              ['docker', 'push', proxy_image],
   565          ]:
   566              subprocess.check_call(command)
   567  
   568      def push_as(self, image, new_tag):
   569          repo = image.split(':')[0]
   570          proxy_image = '{reg}/{repo}:{tag}'.format(reg=self.registry, repo=repo, tag=new_tag)
   571          for command in [
   572              ['docker', 'tag', image, proxy_image],
   573              ['docker', 'push', proxy_image],
   574          ]:
   575              subprocess.check_call(command)
   576  
   577      def list(self, repo):
   578          url = 'http://{reg}/v2/{repo}/tags/list'.format(reg=self.registry, repo=repo)
   579          res = requests.get(url)
   580          res.raise_for_status()
   581          return res.json()['tags']
   582  
   583      def catalog(self):
   584          url = 'http://{reg}/v2/_catalog'.format(reg=self.registry)
   585          res = requests.get(url)
   586          res.raise_for_status()
   587          return res.json()['repositories']
   588  
   589      def pull(self, image):
   590          pull(self.registry, image)
   591  
   592  
   593  class BuildIndex(Component):
   594  
   595      class Instance(object):
   596  
   597          def __init__(self, name):
   598              self.name = name
   599              self.hostname = get_docker_bridge()
   600              self.port_rez = PortReservation()
   601  
   602          @property
   603          def port(self):
   604              return self.port_rez.get()
   605  
   606          @property
   607          def addr(self):
   608              return '{}:{}'.format(self.hostname, self.port)
   609  
   610      def __init__(self, zone, instances, name, origin_cluster, testfs, remote_instances=None):
   611          self.zone = zone
   612          self.instance = instances[name]
   613          self.origin_cluster = origin_cluster
   614          self.testfs = testfs
   615          self.config_file = 'test-{zone}.yaml'.format(zone=zone)
   616          self.name = '{name}-{zone}'.format(name=self.instance.name, zone=zone)
   617  
   618          remotes = "remotes:\n{remotes}".format(remotes='\n'.join("  {addr}: [.*]".format(addr=i.addr) for i in (remote_instances or [])))
   619  
   620          populate_config_template(
   621              'build-index',
   622              self.config_file,
   623              testfs=testfs.addr,
   624              origins=yaml_list([o.addr for o in self.origin_cluster.origins]),
   625              cluster=yaml_list([i.addr for i in instances.values()]),
   626              remotes=remotes)
   627  
   628          self.volumes = create_volumes('build-index', self.name)
   629  
   630          self.start()
   631  
   632      def new_container(self):
   633          self.instance.port_rez.release()
   634          return new_docker_container(
   635              name=self.name,
   636              image=dev_tag('kraken-build-index'),
   637              ports={self.port: self.port},
   638              environment={},
   639              command=[
   640                  '/usr/bin/kraken-build-index',
   641                  '--config=/etc/kraken/config/build-index/{config}'.format(config=self.config_file),
   642                  '--port={port}'.format(port=self.port),
   643              ],
   644              volumes=self.volumes,
   645              health_check=HealthCheck(format_insecure_curl(
   646                  'https://localhost:{}/health'.format(self.port))))
   647  
   648      @property
   649      def port(self):
   650          return self.instance.port
   651  
   652      @property
   653      def addr(self):
   654          return self.instance.addr
   655  
   656      def list_repo(self, repo):
   657          url = 'https://localhost:{port}/repositories/{repo}/tags'.format(
   658                  port=self.port,
   659                  repo=urllib.quote(repo, safe=''))
   660          res = requests.get(url, **tls_opts())
   661          res.raise_for_status()
   662          return res.json()['result']
   663  
   664  
   665  class TestFS(Component):
   666  
   667      def __init__(self, zone):
   668          self.zone = zone
   669          self.port = find_free_port()
   670          self.start()
   671  
   672      def new_container(self):
   673          return new_docker_container(
   674              name='kraken-testfs-{zone}'.format(zone=self.zone),
   675              image=dev_tag('kraken-testfs'),
   676              ports={self.port: self.port},
   677              command=[
   678                  '/usr/bin/kraken-testfs',
   679                  '--port={port}'.format(port=self.port),
   680              ],
   681              health_check=HealthCheck('curl localhost:{port}/health'.format(port=self.port)))
   682  
   683      def upload(self, name, blob):
   684          url = 'http://localhost:{port}/files/blobs/{name}'.format(port=self.port, name=name)
   685          res = requests.post(url, data=BytesIO(blob))
   686          res.raise_for_status()
   687  
   688      @property
   689      def addr(self):
   690          return '{}:{}'.format(get_docker_bridge(), self.port)
   691  
   692  
   693  class Cluster(object):
   694  
   695      def __init__(
   696          self,
   697          zone,
   698          local_build_index_instances,
   699          remote_build_index_instances=None):
   700          """
   701          Initializes a full Kraken cluster.
   702  
   703          Note, only use a full cluster if you need to test multiple clusters. Otherwise,
   704          the default fixtures should suffice.
   705          """
   706          self.zone = zone
   707          self.components = []
   708  
   709          self.testfs = self._register(TestFS(zone))
   710  
   711          origin_instances = {
   712              name: Origin.Instance(name)
   713              for name in ('kraken-origin-01', 'kraken-origin-02', 'kraken-origin-03')
   714          }
   715          self.origin_cluster = OriginCluster([
   716              self._register(Origin(zone, origin_instances, name, self.testfs))
   717              for name in origin_instances
   718          ])
   719  
   720          self.redis = self._register(Redis(zone))
   721  
   722          self.tracker = self._register(Tracker(zone, self.redis, self.origin_cluster))
   723  
   724          self.build_indexes = []
   725          for name in local_build_index_instances:
   726              self.build_indexes.append(self._register(
   727                  BuildIndex(
   728                      zone, local_build_index_instances, name, self.origin_cluster, self.testfs,
   729                      remote_build_index_instances)))
   730  
   731          # TODO(codyg): Some tests rely on the fact that proxy and agents point
   732          # to the first build-index.
   733          self.proxy = self._register(Proxy(zone, self.origin_cluster, self.build_indexes))
   734  
   735          self.agent_factory = AgentFactory(zone, self.tracker, self.build_indexes)
   736  
   737      def _register(self, component):
   738          self.components.append(component)
   739          return component
   740  
   741      def teardown(self):
   742          for c in self.components:
   743              c.teardown()