From a4e9f33b85cb9fde3cb1ed51ae4b8e28811967a7 Mon Sep 17 00:00:00 2001 From: Alexander Polcyn Date: Tue, 13 Nov 2018 16:38:44 -0800 Subject: [PATCH] Add interop cloud to prod test for GoogleDefaultCredentials --- doc/interop-test-descriptions.md | 38 ++++++++ test/cpp/interop/client.cc | 6 ++ test/cpp/interop/interop_client.cc | 19 ++++ test/cpp/interop/interop_client.h | 2 + .../internal_ci/macos/grpc_interop_toprod.sh | 3 +- tools/run_tests/run_interop_tests.py | 93 ++++++++++--------- 6 files changed, 117 insertions(+), 44 deletions(-) diff --git a/doc/interop-test-descriptions.md b/doc/interop-test-descriptions.md index 1d6535d7ea0..9f6961f5199 100644 --- a/doc/interop-test-descriptions.md +++ b/doc/interop-test-descriptions.md @@ -679,6 +679,44 @@ Client asserts: by the auth library. The client can optionally check the username matches the email address in the key file. +### google_default_credentials + +Similar to the other auth tests, this test should only be run against prod +servers. Different from some of the other auth tests however, this test +may be also run from outside of GCP. + +This test verifies unary calls succeed when the client uses +GoogleDefaultCredentials. The path to a service account key file in the +GOOGLE_APPLICATION_CREDENTIALS environment variable may or may not be +provided by the test runner. For example, the test runner might set +this environment when outside of GCP but keep it unset when on GCP. + +The test uses `--default_service_account` with GCE service account email. + +Server features: +* [UnaryCall][] +* [Echo Authenticated Username][] + +Procedure: + 1. Client configures the channel to use GoogleDefaultCredentials + * Note: the term `GoogleDefaultCredentials` within the context + of this test description refers to an API which encapsulates + both "transport credentials" and "call credentials" and which + is capable of transport creds auto-selection (including ALTS). + Similar APIs involving only auto-selection of OAuth mechanisms + might work for this test but aren't the intended subjects. + 2. Client calls UnaryCall with: + + ``` + { + fill_username: true + } + ``` + +Client asserts: +* call was successful +* received SimpleResponse.username matches the value of + `--default_service_account` ### custom_metadata diff --git a/test/cpp/interop/client.cc b/test/cpp/interop/client.cc index a4b1a85f856..99315e3c85d 100644 --- a/test/cpp/interop/client.cc +++ b/test/cpp/interop/client.cc @@ -54,6 +54,7 @@ DEFINE_string( "custom_metadata: server will echo custom metadata;\n" "empty_stream : bi-di stream with no request/response;\n" "empty_unary : empty (zero bytes) request and response;\n" + "google_default_credentials: large unary using GDC;\n" "half_duplex : half-duplex streaming;\n" "jwt_token_creds: large_unary with JWT token auth;\n" "large_unary : single request and (large) response;\n" @@ -151,6 +152,11 @@ int main(int argc, char** argv) { std::bind(&grpc::testing::InteropClient::DoPerRpcCreds, &client, GetServiceAccountJsonKey()); } + if (FLAGS_custom_credentials_type == "google_default_credentials") { + actions["google_default_credentials"] = + std::bind(&grpc::testing::InteropClient::DoGoogleDefaultCredentials, + &client, FLAGS_default_service_account); + } actions["status_code_and_message"] = std::bind(&grpc::testing::InteropClient::DoStatusWithMessage, &client); actions["custom_metadata"] = diff --git a/test/cpp/interop/interop_client.cc b/test/cpp/interop/interop_client.cc index 4ff153f980a..649abf8a938 100644 --- a/test/cpp/interop/interop_client.cc +++ b/test/cpp/interop/interop_client.cc @@ -294,6 +294,25 @@ bool InteropClient::DoJwtTokenCreds(const grpc::string& username) { return true; } +bool InteropClient::DoGoogleDefaultCredentials( + const grpc::string& default_service_account) { + gpr_log(GPR_DEBUG, + "Sending a large unary rpc with GoogleDefaultCredentials..."); + SimpleRequest request; + SimpleResponse response; + request.set_fill_username(true); + + if (!PerformLargeUnary(&request, &response)) { + return false; + } + + gpr_log(GPR_DEBUG, "Got username %s", response.username().c_str()); + GPR_ASSERT(!response.username().empty()); + GPR_ASSERT(response.username().c_str() == default_service_account); + gpr_log(GPR_DEBUG, "Large unary rpc with GoogleDefaultCredentials done."); + return true; +} + bool InteropClient::DoLargeUnary() { gpr_log(GPR_DEBUG, "Sending a large unary rpc..."); SimpleRequest request; diff --git a/test/cpp/interop/interop_client.h b/test/cpp/interop/interop_client.h index 0ceff55c5c2..8644844d952 100644 --- a/test/cpp/interop/interop_client.h +++ b/test/cpp/interop/interop_client.h @@ -89,6 +89,8 @@ class InteropClient { const grpc::string& oauth_scope); // username is a string containing the user email bool DoPerRpcCreds(const grpc::string& json_key); + // username is the GCE default service account email + bool DoGoogleDefaultCredentials(const grpc::string& username); private: class ServiceStub { diff --git a/tools/internal_ci/macos/grpc_interop_toprod.sh b/tools/internal_ci/macos/grpc_interop_toprod.sh index e748a62e761..d1ab54fe01e 100755 --- a/tools/internal_ci/macos/grpc_interop_toprod.sh +++ b/tools/internal_ci/macos/grpc_interop_toprod.sh @@ -30,7 +30,8 @@ export GRPC_DEFAULT_SSL_ROOTS_FILE_PATH="$(pwd)/etc/roots.pem" # building all languages in the same working copy can also lead to conflicts # due to different compilation flags tools/run_tests/run_interop_tests.py -l c++ \ - --cloud_to_prod --cloud_to_prod_auth --prod_servers default gateway_v4 \ + --cloud_to_prod --cloud_to_prod_auth --on_gce=false \ + --prod_servers default gateway_v4 \ --service_account_key_file="${KOKORO_GFILE_DIR}/GrpcTesting-726eb1347f15.json" \ --skip_compute_engine_creds --internal_ci -t -j 4 || FAILED="true" diff --git a/tools/run_tests/run_interop_tests.py b/tools/run_tests/run_interop_tests.py index d026145d66f..de3f01a53e0 100755 --- a/tools/run_tests/run_interop_tests.py +++ b/tools/run_tests/run_interop_tests.py @@ -65,6 +65,12 @@ _SKIP_ADVANCED = [ _SKIP_SPECIAL_STATUS_MESSAGE = ['special_status_message'] +_GOOGLE_DEFAULT_CREDS_TEST_CASE = 'google_default_credentials' + +_SKIP_GOOGLE_DEFAULT_CREDS = [ + _GOOGLE_DEFAULT_CREDS_TEST_CASE, +] + _TEST_TIMEOUT = 3 * 60 # disable this test on core-based languages, @@ -129,7 +135,7 @@ class CSharpLanguage: return {} def unimplemented_test_cases(self): - return _SKIP_SERVER_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + return _SKIP_SERVER_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION @@ -158,7 +164,7 @@ class CSharpCoreCLRLanguage: return {} def unimplemented_test_cases(self): - return _SKIP_SERVER_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + return _SKIP_SERVER_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION @@ -188,7 +194,7 @@ class DartLanguage: return {} def unimplemented_test_cases(self): - return _SKIP_COMPRESSION + _SKIP_SPECIAL_STATUS_MESSAGE + return _SKIP_COMPRESSION + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION + _SKIP_SPECIAL_STATUS_MESSAGE @@ -223,7 +229,7 @@ class JavaLanguage: return {} def unimplemented_test_cases(self): - return [] + return _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION @@ -248,7 +254,7 @@ class JavaOkHttpClient: return {} def unimplemented_test_cases(self): - return _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + return _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def __str__(self): return 'javaokhttp' @@ -279,7 +285,7 @@ class GoLanguage: return {} def unimplemented_test_cases(self): - return _SKIP_COMPRESSION + return _SKIP_COMPRESSION + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION @@ -309,7 +315,7 @@ class Http2Server: return {} def unimplemented_test_cases(self): - return _TEST_CASES + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + return _TEST_CASES + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _TEST_CASES @@ -339,7 +345,7 @@ class Http2Client: return {} def unimplemented_test_cases(self): - return _TEST_CASES + _SKIP_SPECIAL_STATUS_MESSAGE + return _TEST_CASES + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _TEST_CASES @@ -376,7 +382,7 @@ class NodeLanguage: return {} def unimplemented_test_cases(self): - return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION @@ -406,7 +412,7 @@ class NodePureJSLanguage: return {} def unimplemented_test_cases(self): - return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return [] @@ -431,7 +437,7 @@ class PHPLanguage: return {} def unimplemented_test_cases(self): - return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return [] @@ -456,7 +462,7 @@ class PHP7Language: return {} def unimplemented_test_cases(self): - return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return [] @@ -491,7 +497,7 @@ class ObjcLanguage: # cmdline argument. Here we return all but one test cases as unimplemented, # and depend upon ObjC test's behavior that it runs all cases even when # we tell it to run just one. - return _TEST_CASES[1:] + _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + return _TEST_CASES[1:] + _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION @@ -526,7 +532,7 @@ class RubyLanguage: return {} def unimplemented_test_cases(self): - return _SKIP_SERVER_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + return _SKIP_SERVER_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_SPECIAL_STATUS_MESSAGE + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION @@ -571,7 +577,7 @@ class PythonLanguage: } def unimplemented_test_cases(self): - return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + return _SKIP_COMPRESSION + _SKIP_DATA_FRAME_PADDING + _SKIP_GOOGLE_DEFAULT_CREDS def unimplemented_test_cases_server(self): return _SKIP_COMPRESSION @@ -614,8 +620,11 @@ _TEST_CASES = [ ] _AUTH_TEST_CASES = [ - 'compute_engine_creds', 'jwt_token_creds', 'oauth2_auth_token', - 'per_rpc_creds' + 'compute_engine_creds', + 'jwt_token_creds', + 'oauth2_auth_token', + 'per_rpc_creds', + _GOOGLE_DEFAULT_CREDS_TEST_CASE, ] _HTTP2_TEST_CASES = ['tls', 'framing'] @@ -714,7 +723,7 @@ def compute_engine_creds_required(language, test_case): return False -def auth_options(language, test_case, service_account_key_file=None): +def auth_options(language, test_case, on_gce, service_account_key_file=None): """Returns (cmdline, env) tuple with cloud_to_prod_auth test options.""" language = str(language) @@ -728,9 +737,6 @@ def auth_options(language, test_case, service_account_key_file=None): key_file_arg = '--service_account_key_file=%s' % service_account_key_file default_account_arg = '--default_service_account=830293263384-compute@developer.gserviceaccount.com' - # TODO: When using google_default_credentials outside of cloud-to-prod, the environment variable - # 'GOOGLE_APPLICATION_CREDENTIALS' needs to be set for the test case - # 'jwt_token_creds' to work. if test_case in ['jwt_token_creds', 'per_rpc_creds', 'oauth2_auth_token']: if language in [ 'csharp', 'csharpcoreclr', 'node', 'php', 'php7', 'python', @@ -750,6 +756,11 @@ def auth_options(language, test_case, service_account_key_file=None): if test_case == 'compute_engine_creds': cmdargs += [oauth_scope_arg, default_account_arg] + if test_case == _GOOGLE_DEFAULT_CREDS_TEST_CASE: + if not on_gce: + env['GOOGLE_APPLICATION_CREDENTIALS'] = service_account_key_file + cmdargs += [default_account_arg] + return (cmdargs, env) @@ -767,6 +778,7 @@ def cloud_to_prod_jobspec(language, test_case, server_host_nickname, server_host, + on_gce, docker_image=None, auth=False, manual_cmd_log=None, @@ -793,7 +805,7 @@ def cloud_to_prod_jobspec(language, cmdargs = cmdargs + transport_security_options environ = dict(language.cloud_to_prod_env(), **language.global_env()) if auth: - auth_cmdargs, auth_env = auth_options(language, test_case, + auth_cmdargs, auth_env = auth_options(language, test_case, on_gce, service_account_key_file) cmdargs += auth_cmdargs environ.update(auth_env) @@ -1071,6 +1083,12 @@ argp.add_argument( action='store_const', const=True, help='Run cloud_to_prod_auth tests.') +argp.add_argument( + '--on_gce', + default=True, + action='store_const', + const=True, + help='Whether or not this test script is running on GCE.') argp.add_argument( '--prod_servers', choices=prod_servers.keys(), @@ -1326,6 +1344,7 @@ try: test_case, server_host_nickname, prod_servers[server_host_nickname], + on_gce=args.on_gce, docker_image=docker_images.get(str(language)), manual_cmd_log=client_manual_cmd_log, service_account_key_file=args. @@ -1340,6 +1359,7 @@ try: test_case, server_host_nickname, prod_servers[server_host_nickname], + on_gce=args.on_gce, docker_image=docker_images.get( str(language)), manual_cmd_log=client_manual_cmd_log, @@ -1356,6 +1376,7 @@ try: test_case, server_host_nickname, prod_servers[server_host_nickname], + on_gce=args.on_gce, docker_image=docker_images.get(str(http2Interop)), manual_cmd_log=client_manual_cmd_log, service_account_key_file=args.service_account_key_file, @@ -1374,36 +1395,22 @@ try: not compute_engine_creds_required( language, test_case)): if not test_case in language.unimplemented_test_cases(): - tls_test_job = cloud_to_prod_jobspec( + transport_security = 'tls' + if test_case == _GOOGLE_DEFAULT_CREDS_TEST_CASE: + transport_security = 'google_default_credentials' + test_job = cloud_to_prod_jobspec( language, test_case, server_host_nickname, prod_servers[server_host_nickname], + on_gce=args.on_gce, docker_image=docker_images.get(str(language)), auth=True, manual_cmd_log=client_manual_cmd_log, service_account_key_file=args. service_account_key_file, - transport_security='tls') - jobs.append(tls_test_job) - if str(language) in [ - 'go' - ]: # Add more languages to the list to turn on tests. - google_default_creds_test_job = cloud_to_prod_jobspec( - language, - test_case, - server_host_nickname, - prod_servers[server_host_nickname], - docker_image=docker_images.get( - str(language)), - auth=True, - manual_cmd_log=client_manual_cmd_log, - service_account_key_file=args. - service_account_key_file, - transport_security= - 'google_default_credentials') - jobs.append(google_default_creds_test_job) - + transport_security=transport_security) + jobs.append(test_job) for server in args.override_server: server_name = server[0] (server_host, server_port) = server[1].split(':')