11#!/usr/bin/env python
2- """Keytar flask app."""
2+ """Keytar flask app.
3+
4+ This program is responsible for exposing an interface to trigger cluster level
5+ tests. For instance, docker webhooks can be configured to point to this
6+ application in order to trigger tests upon pushing new docker images.
7+ """
38
49import argparse
510import datetime
2227results = {}
2328
2429
30+ class KeytarError (Exception ):
31+ pass
32+
33+
2534def run_test_config (config ):
2635 """Runs a single test iteration from a configuration."""
2736 tempdir = tempfile .mkdtemp ()
2837 logging .info ('Fetching github repository' )
2938
30- # Get the github repo
39+ # Get the github repo and clone it.
3140 github_config = config ['github' ]
3241 github_clone_args , github_repo_dir = _get_download_github_repo_args (
3342 tempdir , github_config )
@@ -40,7 +49,8 @@ def run_test_config(config):
4049 _add_new_result (timestamp )
4150 results [timestamp ]['docker_image' ] = config ['docker_image' ]
4251
43- # Run initialization
52+ # Generate a test script with the steps described in the configuration,
53+ # as well as the command to execute the test_runner.
4454 with tempfile .NamedTemporaryFile (dir = tempdir , delete = False ) as f :
4555 tempscript = f .name
4656 f .write ('#!/bin/bash\n ' )
@@ -71,6 +81,7 @@ def index():
7181
7282@app .route ('/test_results' )
7383def test_results ():
84+ # Get all test results or a specific test result in JSON.
7485 if 'name' not in flask .request .args :
7586 return json .dumps ([results [x ] for x in sorted (results )])
7687 else :
@@ -80,13 +91,15 @@ def test_results():
8091
8192@app .route ('/test_log' )
8293def test_log ():
94+ # Fetch the output from a test.
8395 log = '%s.log' % os .path .basename (flask .request .values ['log_name' ])
8496 return (flask .send_from_directory ('/tmp/testlogs' , log ), 200 ,
8597 {'Content-Type' : 'text/css' })
8698
8799
88100@app .route ('/update_results' , methods = ['POST' ])
89101def update_results ():
102+ # Update the results dict, called from the test_runner.
90103 update_args = flask .request .get_json ()
91104 time = update_args ['time' ]
92105 for k , v in update_args .iteritems ():
@@ -119,59 +132,74 @@ def test_request():
119132 flask .abort (400 , validation_error )
120133 webhook_data = flask .request .get_json ()
121134 repo_name = webhook_data ['repository' ]['repo_name' ]
122- configs = [c for c in keytar_config if c ['docker_image' ] == repo_name ]
123- if not configs :
135+ test_configs = [
136+ c for c in keytar_config ['config' ] if c ['docker_image' ] == repo_name ]
137+ if not test_configs :
124138 return 'No config found for repo_name: %s' % repo_name
125- for config in configs :
126- test_worker .add_test (config )
139+ for test_config in test_configs :
140+ test_worker .add_test (test_config )
127141 return 'OK'
128142
129143
130- def process_config (config ):
131- global keytar_config
132- keytar_config = config ['config' ]
133- if 'install' in config :
134- install_config = config ['install' ]
135- if 'cluster_setup' in install_config :
136- for cluster_setup in install_config ['cluster_setup' ]:
137- if cluster_setup ['type' ] == 'gke' :
138- gcloud_args = ['gcloud' , 'auth' , 'activate-service-account' ,
139- '--key-file' , cluster_setup ['keyfile' ]]
140- logging .info ('authenticating using keyfile: %s' ,
141- cluster_setup ['keyfile' ])
142- subprocess .call (gcloud_args )
143- if 'project_name' in cluster_setup :
144- logging .info ('Setting gcloud project to %s' ,
145- cluster_setup ['project_name' ])
146- subprocess .call (
147- ['gcloud' , 'config' , 'set' , 'project' ,
148- cluster_setup ['project_name' ]])
149- else :
150- config = subprocess .check_output (
151- ['gcloud' , 'config' , 'list' , '--format' , 'json' ])
152- project_name = json .loads (config )['core' ]['project' ]
153- if not project_name :
154- projects = subprocess .check_output (
155- ['gcloud' , 'projects' , 'list' ])
156- first_project = projects [0 ]['projectId' ]
157- logging .info ('gcloud project is unset, setting it to %s' ,
158- first_project )
159- subprocess .check_output (
160- ['gcloud' , 'config' , 'set' , 'project' , first_project ])
161- os .environ ['GOOGLE_APPLICATION_CREDENTIALS' ] = (
162- cluster_setup ['keyfile' ])
163- if 'dependencies' in install_config :
164- subprocess .call (['apt-get' , 'update' ])
165- os .environ ['DEBIAN_FRONTEND' ] = 'noninteractive'
166- for dep in install_config ['dependencies' ]:
167- subprocess .call (
168- ['apt-get' , 'install' , '-y' , '--no-install-recommends' , dep ])
169- if 'extra' in install_config :
170- for step in install_config ['extra' ]:
171- os .system (step )
172- if 'path' in install_config :
173- for path in install_config ['path' ]:
174- os .environ ['PATH' ] = '%s:%s' % (path , os .environ ['PATH' ])
144+ def handle_install_steps ():
145+ """Runs all config installation/setup steps."""
146+ if 'install' not in keytar_config :
147+ return
148+ install_config = keytar_config ['install' ]
149+ if 'cluster_setup' in install_config :
150+ # Handle any cluster setup steps, currently only GKE is supported.
151+ for cluster_setup in install_config ['cluster_setup' ]:
152+ if cluster_setup ['type' ] == 'gke' :
153+ if 'keyfile' not in cluster_setup :
154+ raise KeytarError ('No keyfile found in GKE cluster setup!' )
155+ # Add authentication steps to allow keytar to start clusters on GKE.
156+ gcloud_args = ['gcloud' , 'auth' , 'activate-service-account' ,
157+ '--key-file' , cluster_setup ['keyfile' ]]
158+ logging .info ('authenticating using keyfile: %s' ,
159+ cluster_setup ['keyfile' ])
160+ subprocess .call (gcloud_args )
161+ os .environ ['GOOGLE_APPLICATION_CREDENTIALS' ] = (
162+ cluster_setup ['keyfile' ])
163+
164+ # Ensure that a project name is correctly set. Use the name if provided
165+ # in the configuration, otherwise use the current project name, or else
166+ # the first available project name.
167+ if 'project_name' in cluster_setup :
168+ logging .info ('Setting gcloud project to %s' ,
169+ cluster_setup ['project_name' ])
170+ subprocess .call (
171+ ['gcloud' , 'config' , 'set' , 'project' ,
172+ cluster_setup ['project_name' ]])
173+ else :
174+ config = subprocess .check_output (
175+ ['gcloud' , 'config' , 'list' , '--format' , 'json' ])
176+ project_name = json .loads (config )['core' ]['project' ]
177+ if not project_name :
178+ projects = subprocess .check_output (
179+ ['gcloud' , 'projects' , 'list' ])
180+ first_project = projects [0 ]['projectId' ]
181+ logging .info ('gcloud project is unset, setting it to %s' ,
182+ first_project )
183+ subprocess .check_output (
184+ ['gcloud' , 'config' , 'set' , 'project' , first_project ])
185+
186+ # Install any dependencies using apt-get.
187+ if 'dependencies' in install_config :
188+ subprocess .call (['apt-get' , 'update' ])
189+ os .environ ['DEBIAN_FRONTEND' ] = 'noninteractive'
190+ for dep in install_config ['dependencies' ]:
191+ subprocess .call (
192+ ['apt-get' , 'install' , '-y' , '--no-install-recommends' , dep ])
193+
194+ # Run any additional commands if provided.
195+ if 'extra' in install_config :
196+ for step in install_config ['extra' ]:
197+ os .system (step )
198+
199+ # Update path environment variable.
200+ if 'path' in install_config :
201+ for path in install_config ['path' ]:
202+ os .environ ['PATH' ] = '%s:%s' % (path , os .environ ['PATH' ])
175203
176204
177205def _add_new_result (timestamp ):
@@ -180,6 +208,16 @@ def _add_new_result(timestamp):
180208
181209
182210def _get_download_github_repo_args (tempdir , github_config ):
211+ """Get arguments for github actions.
212+
213+ Args:
214+ tempdir: Base directory to git clone into.
215+ github_config: Configuration describing the repo, branches, etc.
216+
217+ Returns:
218+ ([string], string) for arguments to pass to git, and the directory to
219+ clone into.
220+ """
183221 repo_prefix = 'github'
184222 if 'repo_prefix' in github_config :
185223 repo_prefix = github_config ['repo_prefix' ]
@@ -192,9 +230,9 @@ def _get_download_github_repo_args(tempdir, github_config):
192230
193231
194232class TestWorker (object ):
233+ """A simple test queue. HTTP requests append to this work queue."""
195234
196235 def __init__ (self ):
197- # Create a simple test queue. HTTP requests simply append to the queue.
198236 self .test_queue = Queue .Queue ()
199237 self .worker_thread = threading .Thread (target = self .worker_loop )
200238 self .worker_thread .daemon = True
@@ -226,7 +264,8 @@ def add_test(self, config):
226264 yaml_config = yaml_file .read ()
227265 if not yaml_config :
228266 raise ValueError ('No valid yaml config!' )
229- process_config (yaml .load (yaml_config ))
267+ keytar_config = yaml .load (yaml_config )
268+ handle_install_steps ()
230269
231270 if not os .path .isdir ('/tmp/testlogs' ):
232271 os .mkdir ('/tmp/testlogs' )
0 commit comments