flakiness analyzer: Add apis.
BUG=chromium:818020
TEST=Ran integration tests.
Ran "./bin/run_demo" for demo.
Ran "./bin/deploy_staging", "./bin/deploy_prod" to deploy.
Change-Id: If9a7fd8ffc9f84c8d0e521c1ba31f3c1e71a7d60
Reviewed-on: https://chromium-review.googlesource.com/982516
Commit-Ready: Xixuan Wu <xixuan@chromium.org>
Tested-by: Xixuan Wu <xixuan@chromium.org>
Reviewed-by: Paul Hobbs <phobbs@google.com>
diff --git a/PRESUBMIT.cfg b/PRESUBMIT.cfg
index 97ffba2..8bf25a4 100644
--- a/PRESUBMIT.cfg
+++ b/PRESUBMIT.cfg
@@ -1,5 +1,5 @@
[Hook Scripts]
-hook0=../../chromite/bin/cros lint ${PRESUBMIT_FILES}
+hook0 = ../../chromite/bin/cros lint ${PRESUBMIT_FILES}
[Hook Overrides]
json_check: true
diff --git a/Pipfile b/Pipfile
index c351a0e..05ed531 100644
--- a/Pipfile
+++ b/Pipfile
@@ -16,10 +16,13 @@
[packages]
+flask = "~=0.12.2"
google-api-python-client = "~=1.6.5"
google-cloud-bigquery = "~=0.25.0"
+gunicorn = "~=19.7.1"
+requests = "~=2.18.4"
[requires]
-python_version = "2.7"
+python_version = "3.5"
diff --git a/Pipfile.lock b/Pipfile.lock
index 22d740f..6e6692b 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,24 +1,24 @@
{
"_meta": {
"hash": {
- "sha256": "c4c2efe8c83baf7df8434ce50a2244e384ed27afbcf389e0ea00da9459173b18"
+ "sha256": "5e4f52479571e06ac1c17f244f1ed42438a29e6d77dcca54a32a800551ea9d1d"
},
"host-environment-markers": {
"implementation_name": "cpython",
- "implementation_version": "0",
+ "implementation_version": "3.5.3",
"os_name": "posix",
"platform_machine": "x86_64",
"platform_python_implementation": "CPython",
"platform_release": "4.9.0-6-amd64",
"platform_system": "Linux",
- "platform_version": "#1 SMP Debian 4.9.82-1+deb9u2 (2018-02-21)",
- "python_full_version": "2.7.13",
- "python_version": "2.7",
- "sys_platform": "linux2"
+ "platform_version": "#1 SMP Debian 4.9.82-1+deb9u3 (2018-03-02)",
+ "python_full_version": "3.5.3",
+ "python_version": "3.5",
+ "sys_platform": "linux"
},
"pipfile-spec": 6,
"requires": {
- "python_version": "2.7"
+ "python_version": "3.5"
},
"sources": [
{
@@ -36,12 +36,40 @@
],
"version": "==2.0.1"
},
+ "certifi": {
+ "hashes": [
+ "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0",
+ "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7"
+ ],
+ "version": "==2018.4.16"
+ },
+ "chardet": {
+ "hashes": [
+ "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
+ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
+ ],
+ "version": "==3.0.4"
+ },
+ "click": {
+ "hashes": [
+ "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
+ "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
+ ],
+ "version": "==6.7"
+ },
+ "flask": {
+ "hashes": [
+ "sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856",
+ "sha256:49f44461237b69ecd901cc7ce66feea0319b9158743dd27a2899962ab214dac1"
+ ],
+ "version": "==0.12.2"
+ },
"google-api-python-client": {
"hashes": [
- "sha256:2cf9ab83fa62e06717363e8855fb027864caeb35a3197cadb7f0de38356881c4",
- "sha256:95ce394028754ec537e5791e811511fdd5fabe6f1f8879407a8daed71ecb0b4c"
+ "sha256:ed85c8e5c7533bfaaf05082bb9e3b13cec7aab50bb287ba69757b65f043fa8a6",
+ "sha256:ec72991f95201996a4edcea44a079cae0292798086beaadb054d91921632fe1b"
],
- "version": "==1.6.5"
+ "version": "==1.6.6"
},
"google-auth": {
"hashes": [
@@ -77,11 +105,44 @@
],
"version": "==1.5.3"
},
+ "gunicorn": {
+ "hashes": [
+ "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
+ "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
+ ],
+ "version": "==19.7.1"
+ },
"httplib2": {
"hashes": [
- "sha256:f2176149e1e1c59e0520db62c925715018b787b2ae901358803bae5d816fda0b"
+ "sha256:e71daed9a0e6373642db61166fa70beecc9bf04383477f84671348c02a04cbdf"
],
- "version": "==0.11.1"
+ "version": "==0.11.3"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4",
+ "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f"
+ ],
+ "version": "==2.6"
+ },
+ "itsdangerous": {
+ "hashes": [
+ "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
+ ],
+ "version": "==0.24"
+ },
+ "jinja2": {
+ "hashes": [
+ "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
+ "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
+ ],
+ "version": "==2.10"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
+ ],
+ "version": "==1.0"
},
"oauth2client": {
"hashes": [
@@ -149,6 +210,13 @@
],
"version": "==0.2.1"
},
+ "requests": {
+ "hashes": [
+ "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
+ "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
+ ],
+ "version": "==2.18.4"
+ },
"rsa": {
"hashes": [
"sha256:43f682fea81c452c98d09fc316aae12de6d30c4b5c84226642cf8f8fd1c93abd",
@@ -170,6 +238,20 @@
"sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d"
],
"version": "==3.0.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
+ "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
+ ],
+ "version": "==1.22"
+ },
+ "werkzeug": {
+ "hashes": [
+ "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b",
+ "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c"
+ ],
+ "version": "==0.14.1"
}
},
"develop": {
@@ -198,6 +280,8 @@
},
"pluggy": {
"hashes": [
+ "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
+ "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5",
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"
],
"version": "==0.6.0"
diff --git a/app_template.yaml b/app_template.yaml
new file mode 100644
index 0000000..090f859
--- /dev/null
+++ b/app_template.yaml
@@ -0,0 +1,19 @@
+runtime: python
+env: flex
+entrypoint: gunicorn -b :$PORT --chdir=test_analyzer main:app
+
+runtime_config:
+ python_version: 3
+
+automatic_scaling:
+ min_num_instances: 1
+ max_num_instances: 15
+ cool_down_period_sec: 180
+ cpu_utilization:
+ target_utilization: 0.6
+
+endpoints_api_service:
+ # The following values are to be replaced by information from the output of
+ # 'gcloud service-management deploy openapi-appengine.yaml' command.
+ name: {{endpoints_service}}
+ config_id: {{endpoints_service_config_id}}
diff --git a/bin/deploy_prod b/bin/deploy_prod
new file mode 100755
index 0000000..bd4a192
--- /dev/null
+++ b/bin/deploy_prod
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# Deploy test_analyzer to prod instance.
+set -eu
+cd "$(dirname "$0")/.."
+
+exec ../../chromite/appengine/deploy_appengine \
+ --project_path "$(pwd)" \
+ --project_id chromiumos-test-analyzer \
+ --skip_paths "test_analyzer/credentials" \
+ --skip_paths "venv" \
+ --endpoints_service chromiumos-test-analyzer.appspot.com \
+ --app_template "app_template.yaml" \
+ --openapi_template "openapi-appengine_template.yaml" "$@"
diff --git a/bin/deploy_staging b/bin/deploy_staging
new file mode 100755
index 0000000..97fddcb
--- /dev/null
+++ b/bin/deploy_staging
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# Deploy test_analyzer to prod instance.
+set -eu
+cd "$(dirname "$0")/.."
+
+exec ../../chromite/appengine/deploy_appengine \
+ --project_path "$(pwd)" \
+ --project_id chromeos-test-analyzer-staging \
+ --skip_paths "test_analyzer/credentials" \
+ --skip_paths "venv" \
+ --endpoints_service chromeos-test-analyzer-staging.appspot.com \
+ --app_template "app_template.yaml" \
+ --openapi_template "openapi-appengine_template.yaml" "$@"
diff --git a/bin/run_demo b/bin/run_demo
new file mode 100755
index 0000000..ba3c5a6
--- /dev/null
+++ b/bin/run_demo
@@ -0,0 +1,11 @@
+#!/bin/bash
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# Deploy test_analyzer to prod instance.
+set -eu
+cd "$(dirname "$0")/.."
+
+export PYTHONPATH="$(pwd)"
+exec pipenv run python test_analyzer/integration_test/demo_api_test.py "$@"
diff --git a/openapi-appengine_template.yaml b/openapi-appengine_template.yaml
new file mode 100644
index 0000000..ba675b1
--- /dev/null
+++ b/openapi-appengine_template.yaml
@@ -0,0 +1,56 @@
+# [START swagger]
+swagger: "2.0"
+info:
+ description: "APIs for ChromeOS test analyzer."
+ title: "ChromeOS test analyzer"
+ version: "1.0.0"
+host: {{endpoints_service}}
+# [END swagger]
+
+basePath: "/"
+consumes:
+- "application/json"
+produces:
+- "application/json"
+schemes:
+- "https"
+paths:
+ "/hello":
+ get:
+ summary: "Hello word."
+ operationId: "hello"
+ produces:
+ - "application/json"
+ responses:
+ 200:
+ description: "Hello to world."
+ schema:
+ $ref: "#/definitions/helloMessage"
+ "/internal_hello":
+ get:
+ description: "Private hello."
+ operationId: "internal_hello"
+ produces:
+ - "application/json"
+ responses:
+ 200:
+ description: "Hello to internal world"
+ schema:
+ $ref: "#/definitions/helloMessage"
+ security:
+ - api_key: []
+
+definitions:
+ helloMessage:
+ properties:
+ message:
+ type: "string"
+
+# [START securityDef]
+securityDefinitions:
+ # This section configures basic authentication with an API key.
+ api_key:
+ type: "apiKey"
+ name: "key"
+ in: "query"
+# [END securityDef]
diff --git a/test_analyzer/configs.py b/test_analyzer/configs.py
index b83f154..fa622f0 100644
--- a/test_analyzer/configs.py
+++ b/test_analyzer/configs.py
@@ -26,3 +26,10 @@
else:
return os.path.join(_CREDS_PATH,
'non_cipher/chromeos-test-analyzer-staging.json')
+
+
+def GetCipherAPIKeyFile(is_stage=True):
+ if not is_stage:
+ return os.path.join(_CREDS_PATH, 'cipher/cipher_api_key.txt')
+ else:
+ return os.path.join(_CREDS_PATH, 'cipher/cipher_api_key_staging.txt')
diff --git a/test_analyzer/integration_test/demo_api_test.py b/test_analyzer/integration_test/demo_api_test.py
new file mode 100644
index 0000000..41f8505
--- /dev/null
+++ b/test_analyzer/integration_test/demo_api_test.py
@@ -0,0 +1,71 @@
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Integration tests for demo APIs."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import argparse
+import requests
+
+from test_analyzer import configs
+from test_analyzer import kms
+
+
+_HOST_FORMAT = "%s.appspot.com"
+_REQUEST_FORMAT = 'https://%s/%s'
+
+
+def _PrintHttpResponse(request_url, response):
+ """Print responses from http request."""
+ print('url: %s' % request_url)
+ print(response.text + '\n')
+
+
+def _GetAPIKey(is_stage):
+ """Fetch decrypted API key."""
+ ciphertext_file_name = configs.GetCipherAPIKeyFile(is_stage=is_stage)
+ return kms.DecryptFromFile(kms.DEFAULT_CRYPTO_KEY_FOR_API,
+ ciphertext_file_name)
+
+
+def _GetHost(is_stage):
+ if is_stage:
+ return _HOST_FORMAT % configs.PROJECT_ID_STAGING
+ else:
+ return _HOST_FORMAT % configs.PROJECT_ID_PROD
+
+
+def MakeRequestToHello(is_stage):
+ api_method = 'hello'
+ request_url = _REQUEST_FORMAT % (_GetHost(is_stage), api_method)
+ response = requests.get(request_url)
+ _PrintHttpResponse(request_url, response)
+
+
+def MakeRequestToInternalHello(is_stage):
+ api_method = 'internal_hello'
+ request_url = _REQUEST_FORMAT % (_GetHost(is_stage), api_method)
+ response = requests.get(request_url, params={'key': _GetAPIKey(is_stage)})
+ _PrintHttpResponse(request_url, response)
+
+
+def _MakeParser():
+ """Return parser for testing APIs."""
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument(
+ '--prod', help='Test prod APIs. Default is to test staging APIs.',
+ action='store_true')
+ return parser
+
+
+if __name__ == '__main__':
+ demo_parser = _MakeParser()
+ args = demo_parser.parse_args()
+ MakeRequestToHello(not args.prod)
+ MakeRequestToInternalHello(not args.prod)
diff --git a/test_analyzer/kms.py b/test_analyzer/kms.py
index f51876d..93a5616 100644
--- a/test_analyzer/kms.py
+++ b/test_analyzer/kms.py
@@ -77,7 +77,7 @@
plaintext: The string plain text to be encrypted.
Returns:
- A string cipher text.
+ A string cipher text in bytes.
"""
kms_client = _CreateKMSClient()
resource_name = _CreateResourceName(crypto_key_info)
@@ -86,7 +86,7 @@
crypto_keys = kms_client.projects().locations().keyRings().cryptoKeys()
request = crypto_keys.encrypt(
name=resource_name,
- body={'plaintext': base64.b64encode(plaintext).decode('ascii')})
+ body={'plaintext': base64.b64encode(plaintext.encode()).decode('ascii')})
response = request.execute()
return base64.b64decode(response['ciphertext'].encode('ascii'))
@@ -99,7 +99,7 @@
Args:
crypto_key_info: A KMSCryptoKey object, including all required key infos.
- ciphertext: The string cipher text to be decrypted.
+ ciphertext: The string cipher text to be decrypted in bytes.
Returns:
A string plain text.
@@ -113,7 +113,7 @@
name=resource_name,
body={'ciphertext': base64.b64encode(ciphertext).decode('ascii')})
response = request.execute()
- return base64.b64decode(response['plaintext'].encode('ascii'))
+ return base64.b64decode(response['plaintext'].encode('ascii')).decode()
def DecryptFromFile(crypto_key_info, ciphertext_file_name):
diff --git a/test_analyzer/main.py b/test_analyzer/main.py
new file mode 100644
index 0000000..b5866ab
--- /dev/null
+++ b/test_analyzer/main.py
@@ -0,0 +1,31 @@
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Main API definitions of test_analyzer."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import flask # pylint: disable=import-error
+
+
+app = flask.Flask(__name__)
+
+
+@app.route('/hello', methods=['GET'])
+def Hello():
+ """Simple hello service."""
+ return 'hello, world'
+
+
+@app.route('/internal_hello', methods=['GET'])
+def InternalHello():
+ """Simple internal hello service."""
+ return 'hello, internal world'
+
+
+if __name__ == '__main__':
+ # This is used when running locally. Gunicorn is used to run the
+ # application on Google App Engine. See entrypoint in app.yaml.
+ app.run(host='127.0.0.1', port=8080, debug=True)