Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.executor.spanner;

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.OAuth2Credentials;
import com.google.common.base.Preconditions;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
import org.apache.commons.io.FileUtils;

/**
* {@link OAuth2Credentials} backed by a local file containing a raw OAuth2 bearer access token.
*
* <p>An external host process periodically refreshes and atomically replaces the token file before
* the underlying OAuth2 token expires. Because the file contains only the raw token string without
* expiry metadata, each read assigns a bounded synthetic lifetime so {@link OAuth2Credentials}
* periodically re-reads the file via {@link #refreshAccessToken()}.
*/
final class AccessTokenFileCredentials extends OAuth2Credentials {

private static final long serialVersionUID = 1L;

/**
* Assign a 15-minute synthetic lifetime to each file read. With {@link OAuth2Credentials}'s
* default refresh/expiration margins (~3m45s / 3m), cached tokens are refreshed from disk roughly
* every 11 minutes, well within a typical 1-hour OAuth2 access token lifetime.
*/
static final Duration DEFAULT_SYNTHETIC_LIFETIME = Duration.ofMinutes(15);

private final File tokenFile;
private final Duration syntheticLifetime;

AccessTokenFileCredentials(File tokenFile) {
super(null);
this.tokenFile = Preconditions.checkNotNull(tokenFile, "tokenFile");
this.syntheticLifetime = DEFAULT_SYNTHETIC_LIFETIME;
}

AccessTokenFileCredentials(
File tokenFile,
Duration syntheticLifetime,
Duration refreshMargin,
Duration expirationMargin) {
super(null, refreshMargin, expirationMargin);
this.tokenFile = Preconditions.checkNotNull(tokenFile, "tokenFile");
this.syntheticLifetime = Preconditions.checkNotNull(syntheticLifetime, "syntheticLifetime");
Preconditions.checkArgument(
!syntheticLifetime.isNegative(), "syntheticLifetime cannot be negative");
}

@Override
public AccessToken refreshAccessToken() throws IOException {
if (!tokenFile.isFile()) {
throw new IOException(
"Access token file does not exist or is not a regular file: " + tokenFile);
}
String token = FileUtils.readFileToString(tokenFile, StandardCharsets.UTF_8).trim();
if (token.isEmpty()) {
throw new IOException("Access token file is empty: " + tokenFile);
}
Date expirationTime = new Date(System.currentTimeMillis() + syntheticLifetime.toMillis());
return new AccessToken(token, expirationTime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.api.gax.rpc.UnavailableException;
import com.google.auth.Credentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.ByteArray;
import com.google.cloud.Date;
import com.google.cloud.NoCredentials;
Expand Down Expand Up @@ -165,8 +164,6 @@
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
Expand All @@ -192,7 +189,6 @@
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.io.FileUtils;

/**
* Implementation of the SpannerExecutorProxy gRPC service that proxies action request through the
Expand Down Expand Up @@ -874,18 +870,9 @@ private synchronized Spanner getClient(boolean useMultiplexedSession) throws IOE
private synchronized Spanner initializeClient(long timeoutSeconds, boolean useMultiplexedSession)
throws IOException {
// Create a cloud spanner client
Credentials credentials;
if (WorkerProxy.serviceKeyFile == null
|| WorkerProxy.serviceKeyFile.isEmpty()
|| !new File(WorkerProxy.serviceKeyFile).exists()
|| new File(WorkerProxy.serviceKeyFile).isDirectory()) {
Credentials credentials = WorkerProxy.getCredentialsFromFlags();
if (credentials == null) {
credentials = NoCredentials.getInstance();
} else {
credentials =
ServiceAccountCredentials.fromStream(
new ByteArrayInputStream(
FileUtils.readFileToByteArray(new File(WorkerProxy.serviceKeyFile))),
HTTP_TRANSPORT_FACTORY);
}

TransportChannelProvider channelProvider =
Expand Down Expand Up @@ -959,18 +946,9 @@ private synchronized TraceServiceClient getTraceServiceClient() throws IOExcepti
return traceServiceClient;
}
// Create a trace service client
Credentials credentials;
if (WorkerProxy.serviceKeyFile == null
|| WorkerProxy.serviceKeyFile.isEmpty()
|| !new File(WorkerProxy.serviceKeyFile).exists()
|| new File(WorkerProxy.serviceKeyFile).isDirectory()) {
Credentials credentials = WorkerProxy.getCredentialsFromFlags();
if (credentials == null) {
credentials = NoCredentials.getInstance();
} else {
credentials =
ServiceAccountCredentials.fromStream(
new ByteArrayInputStream(
FileUtils.readFileToByteArray(new File(WorkerProxy.serviceKeyFile))),
HTTP_TRANSPORT_FACTORY);
}

TraceServiceSettings traceServiceSettings =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
Expand All @@ -56,11 +57,13 @@
public class WorkerProxy {

private static final Logger LOGGER = Logger.getLogger(WorkerProxy.class.getName());
private static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = NetHttpTransport::new;

private static final String OPTION_SPANNER_PORT = "spanner_port";
private static final String OPTION_PROXY_PORT = "proxy_port";
private static final String OPTION_CERTIFICATE = "cert";
private static final String OPTION_SERVICE_KEY_FILE = "service_key_file";
static final String OPTION_SPANNER_PORT = "spanner_port";
static final String OPTION_PROXY_PORT = "proxy_port";
static final String OPTION_CERTIFICATE = "cert";
static final String OPTION_SERVICE_KEY_FILE = "service_key_file";
static final String OPTION_ACCESS_TOKEN_FILE = "access_token_file";
private static final String OPTION_USE_PLAIN_TEXT_CHANNEL = "use_plain_text_channel";
private static final String OPTION_ENABLE_GRPC_FAULT_INJECTOR = "enable_grpc_fault_injector";
private static final String OPTION_MULTIPLEXED_SESSION_OPERATIONS_RATIO =
Expand All @@ -70,6 +73,7 @@ public class WorkerProxy {
public static int proxyPort = 0;
public static String cert = "";
public static String serviceKeyFile = "";
public static String accessTokenFile = "";
public static double multiplexedSessionOperationsRatio = 1.0;
public static boolean usePlainTextChannel = false;
public static boolean enableGrpcFaultInjector = false;
Expand All @@ -83,25 +87,41 @@ public class WorkerProxy {
private static final int MIN_PORT = 0, MAX_PORT = 65535;
private static final double MIN_RATIO = 0.0, MAX_RATIO = 1.0, TRACE_SAMPLING_RATE = 0.01;

@Nullable
static Credentials getCredentialsFromFlags() throws IOException {
boolean hasAccessTokenFile = accessTokenFile != null && !accessTokenFile.isEmpty();
boolean hasServiceKeyFile = serviceKeyFile != null && !serviceKeyFile.isEmpty();
if (hasAccessTokenFile && hasServiceKeyFile) {
throw new IllegalArgumentException(
"Cannot specify both --"
+ OPTION_SERVICE_KEY_FILE
+ " and --"
+ OPTION_ACCESS_TOKEN_FILE);
}
if (hasAccessTokenFile) {
return new AccessTokenFileCredentials(new File(accessTokenFile));
}
Comment thread
gyang-google marked this conversation as resolved.
Resolved
if (!hasServiceKeyFile) {
return null;
}
File keyFile = new File(serviceKeyFile);
if (!keyFile.exists() || keyFile.isDirectory()) {
return null;
}
return ServiceAccountCredentials.fromStream(
new ByteArrayInputStream(FileUtils.readFileToByteArray(keyFile)), HTTP_TRANSPORT_FACTORY);
}

public static OpenTelemetrySdk setupOpenTelemetrySdk() throws Exception {
if (serviceKeyFile == null
|| serviceKeyFile.isEmpty()
|| !new File(serviceKeyFile).exists()
|| new File(serviceKeyFile).isDirectory()) {
Credentials credentials = getCredentialsFromFlags();
if (credentials == null) {
LOGGER.log(
Level.WARNING,
"serviceKeyFile is empty or invalid; starting without OpenTelemetry trace export: "
+ serviceKeyFile);
"Credentials are empty or invalid; starting without OpenTelemetry trace export.");
return OpenTelemetrySdk.builder()
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.build();
}
// Read credentials from the serviceKeyFile.
HttpTransportFactory HTTP_TRANSPORT_FACTORY = NetHttpTransport::new;
Credentials credentials =
ServiceAccountCredentials.fromStream(
new ByteArrayInputStream(FileUtils.readFileToByteArray(new File(serviceKeyFile))),
HTTP_TRANSPORT_FACTORY);

// OpenTelemetry configuration.
SpanExporter spanExporter =
Expand Down Expand Up @@ -157,9 +177,14 @@ public static void main(String[] args) throws Exception {
"Certificate need to be assigned in order to start worker proxy.");
}
cert = commandLine.getOptionValue(OPTION_CERTIFICATE);
if (commandLine.hasOption(OPTION_SERVICE_KEY_FILE)) {
serviceKeyFile = commandLine.getOptionValue(OPTION_SERVICE_KEY_FILE);
}
serviceKeyFile =
commandLine.hasOption(OPTION_SERVICE_KEY_FILE)
? commandLine.getOptionValue(OPTION_SERVICE_KEY_FILE)
: "";
accessTokenFile =
commandLine.hasOption(OPTION_ACCESS_TOKEN_FILE)
? commandLine.getOptionValue(OPTION_ACCESS_TOKEN_FILE)
: "";

usePlainTextChannel = commandLine.hasOption(OPTION_USE_PLAIN_TEXT_CHANNEL);
enableGrpcFaultInjector = commandLine.hasOption(OPTION_ENABLE_GRPC_FAULT_INJECTOR);
Expand Down Expand Up @@ -210,7 +235,7 @@ public static void main(String[] args) throws Exception {
server.awaitTermination();
}

private static CommandLine buildOptions(String[] args) {
static CommandLine buildOptions(String[] args) {
Options options = new Options();

options.addOption(
Expand All @@ -220,6 +245,11 @@ private static CommandLine buildOptions(String[] args) {
null, OPTION_CERTIFICATE, true, "Certificate used to connect to Spanner GFE.");
options.addOption(
null, OPTION_SERVICE_KEY_FILE, true, "Service key file used to set authentication.");
options.addOption(
null,
OPTION_ACCESS_TOKEN_FILE,
true,
"File containing an OAuth2 access token used to set authentication.");
options.addOption(
null,
OPTION_USE_PLAIN_TEXT_CHANNEL,
Expand All @@ -238,7 +268,15 @@ private static CommandLine buildOptions(String[] args) {

CommandLineParser parser = new DefaultParser();
try {
return parser.parse(options, args);
CommandLine parsed = parser.parse(options, args);
if (parsed.hasOption(OPTION_SERVICE_KEY_FILE) && parsed.hasOption(OPTION_ACCESS_TOKEN_FILE)) {
throw new IllegalArgumentException(
"Cannot specify both --"
+ OPTION_SERVICE_KEY_FILE
+ " and --"
+ OPTION_ACCESS_TOKEN_FILE);
}
return parsed;
} catch (ParseException e) {
throw new IllegalArgumentException(e.getMessage());
}
Expand Down
Loading
Loading