Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 131 additions & 12 deletions shared/test/showcase/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,86 @@
require "minitest/focus"
require "fileutils"
require "open3"
require "openssl"
require "tmpdir"

# @private
GAPIC_SHOWCASE_VERSION = "0.44.0"

# @private
SHOWCASE_PORT = 7469

# @private
# File Showcase is asked to write its CA to.
SHOWCASE_CA_FILE = "ca.pem"

# @private
# Blocks until Showcase writes the CA requested via --ca-cert-output-file.
#
# Showcase binds its port before generating TLS material, so the CA lands up to
# 3.2s later (median 1.4s over 40 boots) - too variable for a fixed sleep. The
# file is parsed, not merely tested for existence, since it is briefly visible
# mid-write.
#
# @param ca_path [String]
# @param timeout [Numeric]
# @return [String] ca_path, once it holds a usable certificate.
def wait_for_showcase_ca ca_path, timeout: 30
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
begin
OpenSSL::X509::Certificate.new File.read ca_path
rescue Errno::ENOENT, OpenSSL::X509::CertificateError
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
raise "showcase did not write a CA certificate to #{ca_path} within #{timeout}s"
end
sleep 0.05
retry
end
ca_path
end

# @private
# Starts a Showcase server over TLS and returns once it is usable.
#
# @param binary [String] Path to the gapic-showcase executable.
# @param port [Integer]
# @param ca_path [String] Where Showcase should publish its CA certificate.
# @param log_file [String]
# @param extra_args [Array<String>] Additional flags, e.g. --tls-groups.
# @return [Integer] The server's pid.
def spawn_showcase binary, port:, ca_path:, log_file:, extra_args: []
# A stale CA would satisfy the wait instantly, then fail verification.
FileUtils.rm_f ca_path
# err: [:child, :out] rather than a second redirect to log_file: two redirects
# to one path get independent offsets and would overwrite each other.
pid = Process.spawn(
binary, "run", "--port", ":#{port}",
"--tls", "--ca-cert-output-file", ca_path, *extra_args,
out: [log_file, "w"], err: [:child, :out]
)
begin
wait_for_showcase_ca ca_path
rescue StandardError
# Otherwise it survives holding the port and the next run silently reuses it.
stop_showcase pid
raise
end
pid
end

# @private
# Terminates a Showcase server, tolerating one that has already gone away.
#
# @param pid [Integer, nil]
# @return [void]
def stop_showcase pid
return if pid.nil?
Process.kill "TERM", pid
Process.wait pid
rescue Errno::ESRCH, Errno::ECHILD
nil
end

def generate_library_for_test imports, protos
client_lib = Dir.mktmpdir
FileUtils.mkdir "#{client_lib}/lib"
Expand Down Expand Up @@ -68,48 +143,89 @@ def tar_file_name
class ShowcaseTest < Minitest::Test
def new_echo_client
Google::Showcase::V1beta1::Echo::Client.new do |config|
config.credentials = :this_channel_is_insecure
config.credentials = ShowcaseTest.channel_credentials
end
end

def new_echo_rest_client
Google::Showcase::V1beta1::Echo::Rest::Client.new do |config|
config.endpoint = "http://localhost:7469"
config.endpoint = "https://localhost:#{SHOWCASE_PORT}"
config.credentials = :this_channel_is_insecure
end
end

def new_identity_client
Google::Showcase::V1beta1::Identity::Client.new do |config|
config.credentials = :this_channel_is_insecure
config.credentials = ShowcaseTest.channel_credentials
end
end

def new_echo_operations_client
Google::Showcase::V1beta1::Echo::Operations.new do |config|
config.credentials = :this_channel_is_insecure
config.credentials = ShowcaseTest.channel_credentials
end
end

def new_compliance_rest_client
Google::Showcase::V1beta1::Compliance::Rest::Client.new do |config|
config.endpoint = "http://localhost:7469"
config.endpoint = "https://localhost:#{SHOWCASE_PORT}"
config.credentials = :this_channel_is_insecure
end
end

# Env vars pointing REST at Showcase's CA, saved so after_run can restore them
# rather than leak a test-only root. REST only: Net::HTTP re-reads
# SSL_CERT_FILE per connection, while the gRPC C core resolves
# GRPC_DEFAULT_SSL_ROOTS_FILE_PATH once per process and gets credentials
# explicitly instead.
TLS_ENV_KEYS = ["SSL_CERT_FILE"].freeze

@original_tls_env = TLS_ENV_KEYS.to_h { |key| [key, ENV[key]] }
@showcase_dir = nil
@showcase_ca_path = nil

# Channel credentials trusting the CA of the Showcase server under test.
#
# Each --tls server mints its own CA, so trust cannot be set once per process;
# every channel must be handed its roots.
#
# @param ca_path [String, nil] Defaults to the server this helper started; nil
# falls back to system roots, all that is possible for an external server
# with no SHOWCASE_TLS_CERT.
# @return [GRPC::Core::ChannelCredentials]
def self.channel_credentials ca_path = @showcase_ca_path
return GRPC::Core::ChannelCredentials.new if ca_path.nil?
GRPC::Core::ChannelCredentials.new File.read ca_path
end

@showcase_id = begin
server_id = nil
unless gapic_showcase_running?
tmp_dir = Dir.mktmpdir "gapic-show-case-#{Time.now.to_i}"
log_file = "#{tmp_dir}/gapic-showcase.log"
if gapic_showcase_running?
puts "Existing showcase server is available. Continuing..." if ENV["VERBOSE"]
# Tests speak TLS unconditionally, so an external server must publish its CA.
if ENV["SHOWCASE_TLS_CERT"]
@showcase_ca_path = ENV["SHOWCASE_TLS_CERT"]
TLS_ENV_KEYS.each { |key| ENV[key] = @showcase_ca_path }
else
warn "WARNING: reusing a running showcase server without SHOWCASE_TLS_CERT set; " \
"TLS verification will fail unless it was started with a trusted certificate."
end
else
@showcase_dir = Dir.mktmpdir "gapic-show-case-#{Time.now.to_i}"
log_file = "#{@showcase_dir}/gapic-showcase.log"
url = "https://github.com/googleapis/gapic-showcase/releases/download/v#{GAPIC_SHOWCASE_VERSION}/#{tar_file_name}"
_, status = Open3.capture2 "curl -sSL #{url} | tar -zx --directory #{tmp_dir}/"
_, status = Open3.capture2 "curl -sSL #{url} | tar -zx --directory #{@showcase_dir}/"
raise "failed to start showcase" unless status.exitstatus.zero?
server_id = Process.spawn("#{tmp_dir}/gapic-showcase run", :out => [log_file, "w"])

# Showcase generates its own serving cert under --tls and publishes the
# signing CA, so the harness runs no certificate authority of its own.
@showcase_ca_path = File.join @showcase_dir, SHOWCASE_CA_FILE
server_id = spawn_showcase "#{@showcase_dir}/gapic-showcase",
port: SHOWCASE_PORT,
ca_path: @showcase_ca_path,
log_file: log_file
TLS_ENV_KEYS.each { |key| ENV[key] = @showcase_ca_path }
puts "Started showcase server v#{GAPIC_SHOWCASE_VERSION} (pid: #{server_id}) > #{log_file}." if ENV["VERBOSE"]
else
puts "Existing showcase server is available. Continuing..." if ENV["VERBOSE"]
end

server_id
Expand Down Expand Up @@ -137,5 +253,8 @@ def new_compliance_rest_client
_, status = Open3.capture2 "kill #{@showcase_id}"
raise "failed to kill showcase" unless status.exitstatus.zero?
end

@original_tls_env.each { |key, value| ENV[key] = value }
FileUtils.remove_entry @showcase_dir, true if @showcase_dir
end
end