Skip to content

Commit 832a92e

Browse files
committed
feat: add AdkConfiguration abstraction layer for programmatic config injection
Replace hardcoded System.getenv() calls with a three-tier fallback chain: 1. Programmatic overrides via AdkConfiguration.set() (highest priority) 2. JVM system properties via System.getProperty() 3. OS environment variables via System.getenv() (backward compatible) Files changed: - core/.../config/AdkConfiguration.java (new): central config provider with ConcurrentHashMap-backed programmatic overrides, set/get/clear/ clearAll API, and Optional-returning get() with full fallback chain. - core/.../sessions/ApiClient.java: replace 3x getenv with AdkConfiguration (GOOGLE_API_KEY, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION) - core/.../tools/retrieval/VertexAiRagRetrieval.java: replace getenv with AdkConfiguration (GOOGLE_GENAI_USE_VERTEXAI) - core/.../models/ApigeeLlm.java: replace 2x getenv with AdkConfiguration (APIGEE_PROXY_URL, isEnvEnabled helper) - core/.../config/AdkConfigurationTest.java (new): 8 unit tests covering fallback priority, clear/clearAll, null safety, and defaults. Fixes #1022
1 parent d837ef0 commit 832a92e

5 files changed

Lines changed: 204 additions & 6 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.adk.config;
17+
18+
import java.util.Optional;
19+
import java.util.concurrent.ConcurrentHashMap;
20+
import java.util.concurrent.ConcurrentMap;
21+
22+
/**
23+
* Central configuration provider for the ADK SDK.
24+
*
25+
* <p>Resolves configuration values using the following fallback chain (highest priority first):
26+
*
27+
* <ol>
28+
* <li>Programmatic values set via {@link #set(String, String)}.
29+
* <li>JVM system properties ({@link System#getProperty(String)}).
30+
* <li>Operating system environment variables ({@link System#getenv(String)}) — preserved for
31+
* backward compatibility.
32+
* </ol>
33+
*
34+
* <p>This abstraction allows applications (e.g. Spring Boot) to inject configuration that
35+
* originates from {@code application.yaml}, secret managers, or any other source, without relying
36+
* on the immutable OS environment.
37+
*/
38+
public final class AdkConfiguration {
39+
40+
private static final ConcurrentMap<String, String> OVERRIDES = new ConcurrentHashMap<>();
41+
42+
private AdkConfiguration() {}
43+
44+
/**
45+
* Programmatically sets a configuration value. Takes precedence over system properties and
46+
* environment variables.
47+
*
48+
* @param key the configuration key (typically the same name as the legacy environment variable)
49+
* @param value the value to associate with the key; must not be {@code null}. Use {@link
50+
* #clear(String)} to remove an entry.
51+
*/
52+
public static void set(String key, String value) {
53+
if (key == null) {
54+
throw new IllegalArgumentException("key must not be null");
55+
}
56+
if (value == null) {
57+
throw new IllegalArgumentException("value must not be null; use clear(key) to remove");
58+
}
59+
OVERRIDES.put(key, value);
60+
}
61+
62+
/** Removes a programmatic override for the given key, if any. */
63+
public static void clear(String key) {
64+
if (key != null) {
65+
OVERRIDES.remove(key);
66+
}
67+
}
68+
69+
/** Removes all programmatic overrides. Primarily intended for tests. */
70+
public static void clearAll() {
71+
OVERRIDES.clear();
72+
}
73+
74+
/**
75+
* Resolves a configuration value using the fallback chain described in the class javadoc.
76+
*
77+
* @param key the configuration key
78+
* @return an {@link Optional} containing the resolved value, or empty if no source provides one
79+
*/
80+
public static Optional<String> get(String key) {
81+
if (key == null) {
82+
return Optional.empty();
83+
}
84+
String override = OVERRIDES.get(key);
85+
if (override != null) {
86+
return Optional.of(override);
87+
}
88+
String systemProperty = System.getProperty(key);
89+
if (systemProperty != null) {
90+
return Optional.of(systemProperty);
91+
}
92+
return Optional.ofNullable(System.getenv(key));
93+
}
94+
95+
/**
96+
* Resolves a configuration value, returning the provided default if no source provides one.
97+
*
98+
* @param key the configuration key
99+
* @param defaultValue value to return when the key cannot be resolved
100+
*/
101+
public static String getOrDefault(String key, String defaultValue) {
102+
return get(key).orElse(defaultValue);
103+
}
104+
}

core/src/main/java/com/google/adk/models/ApigeeLlm.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.common.base.Strings.isNullOrEmpty;
2020

2121
import com.google.adk.Version;
22+
import com.google.adk.config.AdkConfiguration;
2223
import com.google.common.annotations.VisibleForTesting;
2324
import com.google.common.collect.ImmutableMap;
2425
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -73,7 +74,7 @@ private ApigeeLlm(String modelName, String proxyUrl, Map<String, String> customH
7374

7475
String effectiveProxyUrl = proxyUrl;
7576
if (isNullOrEmpty(effectiveProxyUrl)) {
76-
effectiveProxyUrl = System.getenv(APIGEE_PROXY_URL_ENV_VARIABLE_NAME);
77+
effectiveProxyUrl = AdkConfiguration.get(APIGEE_PROXY_URL_ENV_VARIABLE_NAME).orElse(null);
7778
}
7879
if (isNullOrEmpty(effectiveProxyUrl)) {
7980
throw new IllegalArgumentException(
@@ -306,7 +307,7 @@ private static boolean validateModelString(String model) {
306307
}
307308

308309
private static boolean isEnvEnabled(String envVarName) {
309-
String value = System.getenv(envVarName);
310+
String value = AdkConfiguration.get(envVarName).orElse(null);
310311
return Boolean.parseBoolean(value) || Objects.equals(value, "1");
311312
}
312313

core/src/main/java/com/google/adk/sessions/ApiClient.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.base.StandardSystemProperty.JAVA_VERSION;
2020

21+
import com.google.adk.config.AdkConfiguration;
2122
import com.google.auth.oauth2.GoogleCredentials;
2223
import com.google.common.base.Ascii;
2324
import com.google.common.base.Strings;
@@ -46,7 +47,7 @@ abstract class ApiClient {
4647
/** Constructs an ApiClient for Google AI APIs. */
4748
ApiClient(@Nullable String apiKey, @Nullable HttpOptions customHttpOptions) {
4849

49-
this.apiKey = apiKey != null ? apiKey : System.getenv("GOOGLE_API_KEY");
50+
this.apiKey = apiKey != null ? apiKey : AdkConfiguration.get("GOOGLE_API_KEY").orElse(null);
5051

5152
if (Strings.isNullOrEmpty(this.apiKey)) {
5253
throw new IllegalArgumentException(
@@ -74,15 +75,17 @@ abstract class ApiClient {
7475
@Nullable GoogleCredentials credentials,
7576
@Nullable HttpOptions customHttpOptions) {
7677

77-
this.project = project != null ? project : System.getenv("GOOGLE_CLOUD_PROJECT");
78+
this.project =
79+
project != null ? project : AdkConfiguration.get("GOOGLE_CLOUD_PROJECT").orElse(null);
7880

7981
if (Strings.isNullOrEmpty(this.project)) {
8082
throw new IllegalArgumentException(
8183
"Project must either be provided or set in the environment variable"
8284
+ " GOOGLE_CLOUD_PROJECT.");
8385
}
8486

85-
this.location = location != null ? location : System.getenv("GOOGLE_CLOUD_LOCATION");
87+
this.location =
88+
location != null ? location : AdkConfiguration.get("GOOGLE_CLOUD_LOCATION").orElse(null);
8689

8790
if (Strings.isNullOrEmpty(this.location)) {
8891
throw new IllegalArgumentException(

core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.collect.ImmutableList.toImmutableList;
2020

21+
import com.google.adk.config.AdkConfiguration;
2122
import com.google.adk.models.LlmRequest;
2223
import com.google.adk.tools.ToolContext;
2324
import com.google.adk.utils.ModelNameUtils;
@@ -106,7 +107,8 @@ public Completable processLlmRequest(
106107
LlmRequest.Builder llmRequestBuilder, ToolContext toolContext) {
107108
LlmRequest llmRequest = llmRequestBuilder.build();
108109
// Use Gemini built-in Vertex AI RAG tool for Gemini models when using Vertex AI API Model
109-
boolean useVertexAi = Boolean.parseBoolean(System.getenv("GOOGLE_GENAI_USE_VERTEXAI"));
110+
boolean useVertexAi =
111+
Boolean.parseBoolean(AdkConfiguration.getOrDefault("GOOGLE_GENAI_USE_VERTEXAI", "false"));
110112
if (useVertexAi && llmRequest.model().filter(ModelNameUtils::isGeminiModel).isPresent()) {
111113
GenerateContentConfig config =
112114
llmRequest.config().orElseGet(() -> GenerateContentConfig.builder().build());
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.adk.config;
17+
18+
import static com.google.common.truth.Truth.assertThat;
19+
import static org.junit.jupiter.api.Assertions.assertThrows;
20+
21+
import org.junit.jupiter.api.AfterEach;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
25+
public class AdkConfigurationTest {
26+
27+
private static final String KEY = "ADK_CONFIG_TEST_KEY";
28+
29+
@BeforeEach
30+
void setUp() {
31+
AdkConfiguration.clearAll();
32+
System.clearProperty(KEY);
33+
}
34+
35+
@AfterEach
36+
void tearDown() {
37+
AdkConfiguration.clearAll();
38+
System.clearProperty(KEY);
39+
}
40+
41+
@Test
42+
void get_returnsEmpty_whenNoSourceProvidesValue() {
43+
assertThat(AdkConfiguration.get(KEY)).isEmpty();
44+
}
45+
46+
@Test
47+
void get_returnsSystemProperty_whenSet() {
48+
System.setProperty(KEY, "from-sysprop");
49+
assertThat(AdkConfiguration.get(KEY)).hasValue("from-sysprop");
50+
}
51+
52+
@Test
53+
void set_takesPrecedenceOverSystemProperty() {
54+
System.setProperty(KEY, "from-sysprop");
55+
AdkConfiguration.set(KEY, "from-programmatic");
56+
assertThat(AdkConfiguration.get(KEY)).hasValue("from-programmatic");
57+
}
58+
59+
@Test
60+
void clear_removesProgrammaticOverride_andFallsBackToSystemProperty() {
61+
System.setProperty(KEY, "from-sysprop");
62+
AdkConfiguration.set(KEY, "from-programmatic");
63+
AdkConfiguration.clear(KEY);
64+
assertThat(AdkConfiguration.get(KEY)).hasValue("from-sysprop");
65+
}
66+
67+
@Test
68+
void getOrDefault_returnsDefault_whenUnset() {
69+
assertThat(AdkConfiguration.getOrDefault(KEY, "fallback")).isEqualTo("fallback");
70+
}
71+
72+
@Test
73+
void getOrDefault_returnsResolvedValue_whenSet() {
74+
AdkConfiguration.set(KEY, "resolved");
75+
assertThat(AdkConfiguration.getOrDefault(KEY, "fallback")).isEqualTo("resolved");
76+
}
77+
78+
@Test
79+
void set_throws_onNullKeyOrValue() {
80+
assertThrows(IllegalArgumentException.class, () -> AdkConfiguration.set(null, "v"));
81+
assertThrows(IllegalArgumentException.class, () -> AdkConfiguration.set(KEY, null));
82+
}
83+
84+
@Test
85+
void get_returnsEmpty_forNullKey() {
86+
assertThat(AdkConfiguration.get(null)).isEmpty();
87+
}
88+
}

0 commit comments

Comments
 (0)