Skip to content

Commit 6a01bd8

Browse files
authored
Use environment variables instead of parameterstore extension (#2501)
* Use environment variables instead of parameterstore extension * Fix tests
1 parent 21884fa commit 6a01bd8

File tree

7 files changed

+123
-137
lines changed

7 files changed

+123
-137
lines changed

src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
using System.Text.Json;
99
using System.Text.Json.Serialization;
1010
using Elastic.Documentation.Api.Core.AskAi;
11-
using Elastic.Documentation.Api.Infrastructure.Aws;
1211
using Microsoft.Extensions.Logging;
1312

1413
namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
1514

16-
public class AgentBuilderAskAiGateway(HttpClient httpClient, IParameterProvider parameterProvider, ILogger<AgentBuilderAskAiGateway> logger) : IAskAiGateway<Stream>
15+
public class AgentBuilderAskAiGateway(HttpClient httpClient, KibanaOptions kibanaOptions, ILogger<AgentBuilderAskAiGateway> logger) : IAskAiGateway<Stream>
1716
{
1817
/// <summary>
1918
/// Model name used by Agent Builder (from AgentId)
@@ -34,14 +33,11 @@ public async Task<Stream> AskAi(AskAiRequest askAiRequest, Cancel ctx = default)
3433

3534
logger.LogInformation("Sending to Agent Builder with conversation_id: \"{ConversationId}\"", askAiRequest.ConversationId?.ToString() ?? "(null - first request)");
3635

37-
var kibanaUrl = await parameterProvider.GetParam("docs-kibana-url", false, ctx);
38-
var kibanaApiKey = await parameterProvider.GetParam("docs-kibana-apikey", true, ctx);
39-
4036
using var request = new HttpRequestMessage(HttpMethod.Post,
41-
$"{kibanaUrl}/api/agent_builder/converse/async");
37+
$"{kibanaOptions.Url}/api/agent_builder/converse/async");
4238
request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
4339
request.Headers.Add("kbn-xsrf", "true");
44-
request.Headers.Authorization = new AuthenticationHeaderValue("ApiKey", kibanaApiKey);
40+
request.Headers.Authorization = new AuthenticationHeaderValue("ApiKey", kibanaOptions.ApiKey);
4541

4642
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctx);
4743

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Microsoft.Extensions.Configuration;
6+
7+
namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
8+
9+
public class KibanaOptions(IConfiguration configuration)
10+
{
11+
public string Url { get; } = configuration["DOCUMENTATION_KIBANA_URL"]
12+
?? throw new InvalidOperationException("DOCUMENTATION_KIBANA_URL not configured");
13+
public string ApiKey { get; } = configuration["DOCUMENTATION_KIBANA_APIKEY"]
14+
?? throw new InvalidOperationException("DOCUMENTATION_KIBANA_APIKEY not configured");
15+
}

src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayOptions.cs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,36 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5-
using Elastic.Documentation.Api.Infrastructure.Aws;
5+
using Microsoft.Extensions.Configuration;
66

77
namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
88

9-
public class LlmGatewayOptions
9+
public class LlmGatewayOptions(IConfiguration configuration)
1010
{
11-
public LlmGatewayOptions(IParameterProvider parameterProvider)
11+
public string ServiceAccount { get; } = ResolveServiceAccount(configuration);
12+
public string FunctionUrl { get; } = configuration["LLM_GATEWAY_FUNCTION_URL"]
13+
?? throw new InvalidOperationException("LLM_GATEWAY_FUNCTION_URL not configured");
14+
public string TargetAudience { get; } = GetTargetAudience(configuration["LLM_GATEWAY_FUNCTION_URL"]
15+
?? throw new InvalidOperationException("LLM_GATEWAY_FUNCTION_URL not configured"));
16+
17+
private static string ResolveServiceAccount(IConfiguration configuration)
1218
{
13-
ServiceAccount = parameterProvider.GetParam("llm-gateway-service-account").GetAwaiter().GetResult();
14-
FunctionUrl = parameterProvider.GetParam("llm-gateway-function-url").GetAwaiter().GetResult();
15-
var uri = new Uri(FunctionUrl);
16-
TargetAudience = $"{uri.Scheme}://{uri.Host}";
19+
// Auto-detect: if value is a file path that exists, read file content
20+
// Otherwise use the value directly (for Lambda with env var containing the JSON)
21+
var serviceAccountValue = configuration["LLM_GATEWAY_SERVICE_ACCOUNT"]
22+
?? configuration["LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH"];
23+
24+
if (string.IsNullOrEmpty(serviceAccountValue))
25+
throw new InvalidOperationException("LLM_GATEWAY_SERVICE_ACCOUNT not configured");
26+
27+
return File.Exists(serviceAccountValue)
28+
? File.ReadAllText(serviceAccountValue)
29+
: serviceAccountValue;
1730
}
1831

19-
public string ServiceAccount { get; }
20-
public string FunctionUrl { get; }
21-
public string TargetAudience { get; }
32+
private static string GetTargetAudience(string functionUrl)
33+
{
34+
var uri = new Uri(functionUrl);
35+
return $"{uri.Scheme}://{uri.Host}";
36+
}
2237
}

src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchOptions.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5-
using Elastic.Documentation.Api.Infrastructure.Aws;
5+
using Microsoft.Extensions.Configuration;
66

77
namespace Elastic.Documentation.Api.Infrastructure.Adapters.Search;
88

9-
public class ElasticsearchOptions(IParameterProvider parameterProvider)
9+
public class ElasticsearchOptions(IConfiguration configuration)
1010
{
11-
public string Url { get; } = parameterProvider.GetParam("docs-elasticsearch-url").GetAwaiter().GetResult();
12-
public string ApiKey { get; } = parameterProvider.GetParam("docs-elasticsearch-apikey").GetAwaiter().GetResult();
13-
public string IndexName { get; } = parameterProvider.GetParam("docs-elasticsearch-index").GetAwaiter().GetResult() ?? "documentation-latest";
11+
// Read from environment variables (set by Terraform from SSM at deploy time)
12+
public string Url { get; } = configuration["DOCUMENTATION_ELASTIC_URL"]
13+
?? throw new InvalidOperationException("DOCUMENTATION_ELASTIC_URL not configured");
14+
public string ApiKey { get; } = configuration["DOCUMENTATION_ELASTIC_APIKEY"]
15+
?? throw new InvalidOperationException("DOCUMENTATION_ELASTIC_APIKEY not configured");
16+
public string IndexName { get; } = configuration["DOCUMENTATION_ELASTIC_INDEX"]
17+
?? "documentation-latest";
1418
}

src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs

Lines changed: 8 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using Elastic.Documentation.Api.Infrastructure.Adapters.Search;
1313
using Elastic.Documentation.Api.Infrastructure.Adapters.Search.Common;
1414
using Elastic.Documentation.Api.Infrastructure.Adapters.Telemetry;
15-
using Elastic.Documentation.Api.Infrastructure.Aws;
1615
using Elastic.Documentation.Api.Infrastructure.Caching;
1716
using Elastic.Documentation.Api.Infrastructure.Gcp;
1817
using Microsoft.Extensions.Configuration;
@@ -75,55 +74,13 @@ private static void AddElasticDocsApiUsecases(this IServiceCollection services,
7574
// Register AppEnvironment as a singleton for dependency injection
7675
_ = services.AddSingleton(new AppEnvironment { Current = appEnv });
7776
AddDistributedCache(services, appEnv);
78-
AddParameterProvider(services, appEnv);
7977
AddAskAiUsecase(services, appEnv);
8078
AddSearchUsecase(services, appEnv);
8179
AddOtlpProxyUsecase(services, appEnv);
8280
}
8381

84-
// https://docs.aws.amazon.com/systems -manager/latest/userguide/ps-integration-lambda-extensions.html
85-
private static void AddParameterProvider(IServiceCollection services, AppEnv appEnv)
86-
{
87-
var logger = GetLogger(services);
88-
89-
switch (appEnv)
90-
{
91-
case AppEnv.Prod:
92-
case AppEnv.Staging:
93-
case AppEnv.Edge:
94-
{
95-
logger?.LogInformation("Configuring LambdaExtensionParameterProvider for environment {AppEnvironment}", appEnv);
96-
try
97-
{
98-
_ = services.AddHttpClient(LambdaExtensionParameterProvider.HttpClientName, client =>
99-
{
100-
client.BaseAddress = new Uri("http://localhost:2773");
101-
client.DefaultRequestHeaders.Add("X-Aws-Parameters-Secrets-Token", Environment.GetEnvironmentVariable("AWS_SESSION_TOKEN"));
102-
});
103-
logger?.LogInformation("Lambda extension HTTP client configured");
104-
105-
_ = services.AddSingleton<IParameterProvider, LambdaExtensionParameterProvider>();
106-
logger?.LogInformation("LambdaExtensionParameterProvider registered successfully");
107-
}
108-
catch (Exception ex)
109-
{
110-
logger?.LogError(ex, "Failed to configure LambdaExtensionParameterProvider for environment {AppEnvironment}", appEnv);
111-
throw;
112-
}
113-
break;
114-
}
115-
case AppEnv.Dev:
116-
{
117-
logger?.LogInformation("Configuring LocalParameterProvider for environment {AppEnvironment}", appEnv);
118-
_ = services.AddSingleton<IParameterProvider, LocalParameterProvider>();
119-
break;
120-
}
121-
default:
122-
{
123-
throw new ArgumentOutOfRangeException(nameof(appEnv), appEnv, "Unsupported environment for parameter provider.");
124-
}
125-
}
126-
}
82+
// Note: IParameterProvider is no longer needed - all options now read from IConfiguration (env vars)
83+
// The LambdaExtensionParameterProvider and LocalParameterProvider can be removed in a future cleanup
12784

12885
private static void AddDistributedCache(IServiceCollection services, AppEnv appEnv)
12986
{
@@ -189,9 +146,13 @@ private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv)
189146
_ = services.AddSingleton<IGcpIdTokenProvider, GcpIdTokenProvider>();
190147
logger?.LogInformation("GcpIdTokenProvider registered with distributed cache support");
191148

192-
_ = services.AddScoped<LlmGatewayOptions>();
149+
// Register options - DI auto-resolves IConfiguration from primary constructor
150+
_ = services.AddSingleton<LlmGatewayOptions>();
193151
logger?.LogInformation("LlmGatewayOptions registered successfully");
194152

153+
_ = services.AddSingleton<KibanaOptions>();
154+
logger?.LogInformation("KibanaOptions registered successfully");
155+
195156
_ = services.AddScoped<AskAiUsecase>();
196157
logger?.LogInformation("AskAiUsecase registered successfully");
197158

@@ -234,7 +195,7 @@ private static void AddSearchUsecase(IServiceCollection services, AppEnv appEnv)
234195
var logger = GetLogger(services);
235196
logger?.LogInformation("Configuring Search use case for environment {AppEnvironment}", appEnv);
236197

237-
// Shared Elasticsearch client accessor (singleton - one client instance)
198+
// Shared Elasticsearch options - DI auto-resolves IConfiguration from primary constructor
238199
_ = services.AddSingleton<ElasticsearchOptions>();
239200
_ = services.AddSingleton<ElasticsearchClientAccessor>();
240201

tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
using System.Text;
77
using Elastic.Documentation.Api.Core.AskAi;
88
using Elastic.Documentation.Api.Infrastructure.Adapters.AskAi;
9-
using Elastic.Documentation.Api.Infrastructure.Aws;
109
using Elastic.Documentation.Api.Infrastructure.Gcp;
1110
using FakeItEasy;
1211
using FluentAssertions;
12+
using Microsoft.Extensions.Configuration;
1313
using Microsoft.Extensions.Logging;
1414
using Xunit;
1515

@@ -22,32 +22,39 @@ namespace Elastic.Documentation.Api.IntegrationTests;
2222
/// </summary>
2323
public class AskAiGatewayStreamingTests
2424
{
25+
private static IConfiguration CreateTestConfiguration(Dictionary<string, string?> values) =>
26+
new ConfigurationBuilder()
27+
.AddInMemoryCollection(values)
28+
.Build();
29+
2530
[Fact]
2631
public async Task AgentBuilderGatewayDoesNotDisposeHttpResponsePrematurely()
2732
{
2833
// Arrange
2934
var mockHandler = new MockHttpMessageHandler();
3035
var sseResponse = """
31-
data: {"type":"conversationStart","id":"conv123","conversation_id":"conv123"}
36+
data: {"type":"conversationStart","id":"test","conversation_id":"test"}
3237
33-
data: {"type":"messageChunk","id":"msg1","content":"Hello World"}
38+
data: {"type":"messageChunk","id":"m1","content":"Hello"}
3439
35-
data: {"type":"conversationEnd","id":"conv123"}
40+
data: {"type":"messageChunk","id":"m1","content":" World"}
41+
42+
data: {"type":"conversationEnd","id":"test"}
3643
3744
3845
""";
3946

4047
mockHandler.SetResponse(sseResponse, "text/event-stream");
4148

4249
using var httpClient = new HttpClient(mockHandler);
43-
var mockParameterProvider = A.Fake<IParameterProvider>();
44-
A.CallTo(() => mockParameterProvider.GetParam("docs-kibana-url", false, A<CancellationToken>._))
45-
.Returns(Task.FromResult("https://test-kibana.example.com"));
46-
A.CallTo(() => mockParameterProvider.GetParam("docs-kibana-apikey", true, A<CancellationToken>._))
47-
.Returns(Task.FromResult("test-api-key"));
50+
var kibanaOptions = new KibanaOptions(CreateTestConfiguration(new Dictionary<string, string?>
51+
{
52+
["DOCUMENTATION_KIBANA_URL"] = "https://test-kibana.example.com",
53+
["DOCUMENTATION_KIBANA_APIKEY"] = "test-api-key"
54+
}));
4855

4956
var mockLogger = A.Fake<ILogger<AgentBuilderAskAiGateway>>();
50-
var gateway = new AgentBuilderAskAiGateway(httpClient, mockParameterProvider, mockLogger);
57+
var gateway = new AgentBuilderAskAiGateway(httpClient, kibanaOptions, mockLogger);
5158

5259
var request = new AskAiRequest("Test message", null);
5360

@@ -95,14 +102,14 @@ public async Task AgentBuilderGatewayAllowsMultipleReadsFromStream()
95102
mockHandler.SetResponse(sseResponse, "text/event-stream");
96103

97104
using var httpClient = new HttpClient(mockHandler);
98-
var mockParameterProvider = A.Fake<IParameterProvider>();
99-
A.CallTo(() => mockParameterProvider.GetParam("docs-kibana-url", A<bool>._, A<CancellationToken>._))
100-
.Returns(Task.FromResult("https://test-kibana.example.com"));
101-
A.CallTo(() => mockParameterProvider.GetParam("docs-kibana-apikey", A<bool>._, A<CancellationToken>._))
102-
.Returns(Task.FromResult("test-api-key"));
105+
var kibanaOptions = new KibanaOptions(CreateTestConfiguration(new Dictionary<string, string?>
106+
{
107+
["DOCUMENTATION_KIBANA_URL"] = "https://test-kibana.example.com",
108+
["DOCUMENTATION_KIBANA_APIKEY"] = "test-api-key"
109+
}));
103110

104111
var mockLogger = A.Fake<ILogger<AgentBuilderAskAiGateway>>();
105-
var gateway = new AgentBuilderAskAiGateway(httpClient, mockParameterProvider, mockLogger);
112+
var gateway = new AgentBuilderAskAiGateway(httpClient, kibanaOptions, mockLogger);
106113

107114
var request = new AskAiRequest("Test", null);
108115

@@ -128,37 +135,34 @@ public async Task AgentBuilderGatewayAllowsMultipleReadsFromStream()
128135
}
129136

130137
[Fact]
131-
public async Task LlmGatewayGatewayDoesNotDisposeHttpResponsePrematurely()
138+
public async Task LlmGatewayDoesNotDisposeHttpResponsePrematurely()
132139
{
133140
// Arrange
134141
var mockHandler = new MockHttpMessageHandler();
135142
var sseResponse = """
136-
data: {"type":"conversationStart","id":"conv456","conversation_id":"conv456"}
143+
data: {"type":"conversationStart","id":"test","conversation_id":"test"}
137144
138-
data: {"type":"reasoning","id":"r1","message":"Analyzing question"}
145+
data: {"type":"reasoning","content":"thinking..."}
139146
140-
data: {"type":"messageChunk","id":"msg2","content":"Answer"}
147+
data: {"type":"messageChunk","id":"m1","content":"Response"}
141148
142-
data: {"type":"conversationEnd","id":"conv456"}
149+
data: {"type":"conversationEnd","id":"test"}
143150
144151
145152
""";
146153

147154
mockHandler.SetResponse(sseResponse, "text/event-stream");
148155

149-
// Create mock token provider
150156
using var httpClient = new HttpClient(mockHandler);
151157
var mockTokenProvider = A.Fake<IGcpIdTokenProvider>();
152158
A.CallTo(() => mockTokenProvider.GenerateIdTokenAsync(A<string>._, A<string>._, A<CancellationToken>._))
153159
.Returns(Task.FromResult("mock-gcp-token"));
154160

155-
var mockParameterProvider = A.Fake<IParameterProvider>();
156-
A.CallTo(() => mockParameterProvider.GetParam("llm-gateway-service-account", A<bool>._, A<CancellationToken>._))
157-
.Returns(Task.FromResult("test@example.com"));
158-
A.CallTo(() => mockParameterProvider.GetParam("llm-gateway-function-url", A<bool>._, A<CancellationToken>._))
159-
.Returns(Task.FromResult("https://test-llm-gateway.example.com"));
160-
161-
var options = new LlmGatewayOptions(mockParameterProvider);
161+
var options = new LlmGatewayOptions(CreateTestConfiguration(new Dictionary<string, string?>
162+
{
163+
["LLM_GATEWAY_FUNCTION_URL"] = "https://test-llm-gateway.example.com",
164+
["LLM_GATEWAY_SERVICE_ACCOUNT"] = "test@example.com"
165+
}));
162166

163167
var gateway = new LlmGatewayAskAiGateway(httpClient, mockTokenProvider, options);
164168

@@ -214,13 +218,11 @@ public async Task LlmGatewayGatewayAllowsMultipleReadsFromStream()
214218
A.CallTo(() => mockTokenProvider.GenerateIdTokenAsync(A<string>._, A<string>._, A<CancellationToken>._))
215219
.Returns(Task.FromResult("mock-token"));
216220

217-
var mockParameterProvider = A.Fake<IParameterProvider>();
218-
A.CallTo(() => mockParameterProvider.GetParam("llm-gateway-service-account", A<bool>._, A<CancellationToken>._))
219-
.Returns(Task.FromResult("test@example.com"));
220-
A.CallTo(() => mockParameterProvider.GetParam("llm-gateway-function-url", A<bool>._, A<CancellationToken>._))
221-
.Returns(Task.FromResult("https://test.example.com"));
222-
223-
var options = new LlmGatewayOptions(mockParameterProvider);
221+
var options = new LlmGatewayOptions(CreateTestConfiguration(new Dictionary<string, string?>
222+
{
223+
["LLM_GATEWAY_FUNCTION_URL"] = "https://test.example.com",
224+
["LLM_GATEWAY_SERVICE_ACCOUNT"] = "test@example.com"
225+
}));
224226

225227
var gateway = new LlmGatewayAskAiGateway(httpClient, mockTokenProvider, options);
226228

@@ -256,14 +258,14 @@ public async Task AgentBuilderGatewayUsesResponseHeadersReadForStreaming()
256258
mockHandler.SetResponse(sseResponse, "text/event-stream");
257259

258260
using var httpClient = new HttpClient(mockHandler);
259-
var mockParameterProvider = A.Fake<IParameterProvider>();
260-
A.CallTo(() => mockParameterProvider.GetParam("docs-kibana-url", A<bool>._, A<CancellationToken>._))
261-
.Returns(Task.FromResult("https://test-kibana.example.com"));
262-
A.CallTo(() => mockParameterProvider.GetParam("docs-kibana-apikey", A<bool>._, A<CancellationToken>._))
263-
.Returns(Task.FromResult("test-api-key"));
261+
var kibanaOptions = new KibanaOptions(CreateTestConfiguration(new Dictionary<string, string?>
262+
{
263+
["DOCUMENTATION_KIBANA_URL"] = "https://test-kibana.example.com",
264+
["DOCUMENTATION_KIBANA_APIKEY"] = "test-api-key"
265+
}));
264266

265267
var mockLogger = A.Fake<ILogger<AgentBuilderAskAiGateway>>();
266-
var gateway = new AgentBuilderAskAiGateway(httpClient, mockParameterProvider, mockLogger);
268+
var gateway = new AgentBuilderAskAiGateway(httpClient, kibanaOptions, mockLogger);
267269

268270
var request = new AskAiRequest("Test", null);
269271

0 commit comments

Comments
 (0)