Skip to content

Commit 59f5ae9

Browse files
committed
feat: Add service-specific endpoints support to FSM config parser
Add support for parsing [services] sections with service-specific endpoint_url configuration following AWS SDKs & Tools specification. Changes: - Extend Profile class with endpoint_url and servicesEndpointUrl map - Add FSM states for [services] and [services serviceId] sections - Add GetServiceEndpointUrl() and GetGlobalEndpointUrl() API methods - Support case-insensitive service ID lookup - Add comprehensive test coverage Supports config format: [profile default] endpoint_url = https://global.example.com [services s3] endpoint_url = http://localhost:9000 Add Profile computed getters using Aws::Crt::Optional for proper null handling: - GetServicesName() returns services definition name or empty Optional - GetEndpointUrl() returns global endpoint URL or empty Optional - Add static Profile::GetServiceEndpointUrl() helper for endpoint resolution - Takes profile, services map, and serviceId as parameters - Maintains Profile as stateless data container - Add AWSConfigFileProfileConfigLoader::GetServices() const accessor Uses Aws::Crt::Optional instead of empty strings to properly represent "no value" state. Static helper pattern keeps Profile ABI-stable while enabling service endpoint resolution without coupling to loader internals. added a test for multiple services definition updated test to use .find before creatng the profile update to use a service object instead of using getvalue added 3 more tests verifying duplication urls, modify the services object with more encapsulation
1 parent 9f07797 commit 59f5ae9

File tree

3 files changed

+516
-5
lines changed

3 files changed

+516
-5
lines changed

src/aws-cpp-sdk-core/include/aws/core/config/AWSProfileConfig.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <aws/core/utils/memory/stl/AWSString.h>
99
#include <aws/core/utils/memory/stl/AWSMap.h>
1010
#include <aws/core/auth/AWSCredentials.h>
11+
#include <aws/crt/Optional.h>
1112

1213
namespace Aws
1314
{
@@ -19,6 +20,24 @@ namespace Aws
1920
class Profile
2021
{
2122
public:
23+
/*
24+
* Data container for service endpoints.
25+
*/
26+
class Services
27+
{
28+
public:
29+
Services() = default;
30+
Services(Aws::Map<Aws::String, Aws::String>&& endpoints, Aws::String name)
31+
: m_endpoints(std::move(endpoints)), m_name(std::move(name)) {}
32+
33+
inline Aws::Map<Aws::String, Aws::String> GetEndpoints() const { return m_endpoints; }
34+
inline Aws::String GetServiceBlockName() const { return m_name; }
35+
inline bool IsSet() const { return !m_name.empty(); }
36+
private:
37+
Aws::Map<Aws::String, Aws::String> m_endpoints;
38+
Aws::String m_name;
39+
};
40+
2241
/*
2342
* Data container for a sso-session config entry.
2443
* This is independent of the general profile configuration and used by a bearer auth token provider.
@@ -93,6 +112,14 @@ namespace Aws
93112
return iter->second;
94113
}
95114

115+
inline const Services& GetServices() const { return m_services; }
116+
inline void SetServices(Services&& services) { m_services = std::move(services); }
117+
118+
inline Aws::Crt::Optional<Aws::String> GetGlobalEndpointUrl() const {
119+
const Aws::String& endpoint = GetValue("endpoint_url");
120+
return endpoint.empty() ? Aws::Crt::Optional<Aws::String>() : Aws::Crt::Optional<Aws::String>(endpoint);
121+
}
122+
96123
inline bool IsSsoSessionSet() const { return m_ssoSessionSet; }
97124
inline const SsoSession& GetSsoSession() const { return m_ssoSession; }
98125
inline void SetSsoSession(const SsoSession& value) { m_ssoSessionSet = true; m_ssoSession = value; }
@@ -112,6 +139,7 @@ namespace Aws
112139
Aws::String m_ssoRoleName;
113140
Aws::String m_defaultsMode;
114141
Aws::Map<Aws::String, Aws::String> m_allKeyValPairs;
142+
Services m_services;
115143

116144
bool m_ssoSessionSet = false;
117145
SsoSession m_ssoSession;

src/aws-cpp-sdk-core/source/config/AWSConfigFileProfileConfigLoader.cpp

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ namespace Aws
4040
static const char PROFILE_SECTION[] = "profile";
4141
static const char DEFAULT[] = "default";
4242
static const char SSO_SESSION_SECTION[] = "sso-session";
43+
static const char SERVICES_SECTION[] = "services";
44+
static const char ENDPOINT_URL_KEY[] = "endpoint_url";
45+
static const char IGNORE_CONFIGURED_ENDPOINT_URLS_KEY[] = "ignore_configured_endpoint_urls";
4346
static const char DEFAULTS_MODE_KEY[] = "defaults_mode";
4447
static const char EQ = '=';
4548
static const char LEFT_BRACKET = '[';
@@ -119,6 +122,7 @@ namespace Aws
119122
static const size_t ASSUME_EMPTY_LEN = 3;
120123
State currentState = START;
121124
Aws::String currentSectionName;
125+
Aws::String activeServiceId;
122126
Aws::Map<Aws::String, Aws::String> currentKeyValues;
123127

124128
Aws::String rawLine;
@@ -142,6 +146,7 @@ namespace Aws
142146
{
143147
FlushSection(currentState, currentSectionName, currentKeyValues);
144148
currentKeyValues.clear();
149+
activeServiceId.clear();
145150
ParseSectionDeclaration(line, currentSectionName, currentState);
146151
continue;
147152
}
@@ -158,6 +163,36 @@ namespace Aws
158163
}
159164
}
160165

166+
if(SERVICES_FOUND == currentState)
167+
{
168+
auto equalsPos = line.find(EQ);
169+
if (equalsPos == std::string::npos) {
170+
continue; // ignore garbage/blank in services section
171+
}
172+
173+
auto left = StringUtils::Trim(line.substr(0, equalsPos).c_str());
174+
auto right = StringUtils::Trim(line.substr(equalsPos + 1).c_str());
175+
176+
// New service block: "s3 =" (right hand side empty)
177+
if (!left.empty() && right.empty()) {
178+
activeServiceId = StringUtils::ToUpper(left.c_str());
179+
StringUtils::Replace(activeServiceId, " ", "_");
180+
continue;
181+
}
182+
183+
// Ignore global endpoint_url in [services name] section
184+
if (activeServiceId.empty() && StringUtils::CaselessCompare(left.c_str(), ENDPOINT_URL_KEY) == 0) {
185+
AWS_LOGSTREAM_DEBUG(PARSER_TAG, "Ignoring global endpoint_url in [services " << currentSectionName << "]");
186+
continue;
187+
}
188+
189+
// Property inside an active block: "endpoint_url = http://..."
190+
if (!activeServiceId.empty() && left == ENDPOINT_URL_KEY) {
191+
m_services[currentSectionName][activeServiceId] = right;
192+
continue;
193+
}
194+
}
195+
161196
if(UNKNOWN_SECTION_FOUND == currentState)
162197
{
163198
// skip any unknown sections
@@ -171,6 +206,22 @@ namespace Aws
171206

172207
FlushSection(currentState, currentSectionName, currentKeyValues);
173208

209+
// Resolve service endpoints
210+
for (auto& profilePair : m_foundProfiles)
211+
{
212+
Profile& profile = profilePair.second;
213+
const Aws::String& servicesRef = profile.GetValue("services");
214+
if (!servicesRef.empty())
215+
{
216+
auto servicesBlk = m_services.find(servicesRef);
217+
Aws::Map<Aws::String, Aws::String> endpoints;
218+
if (servicesBlk != m_services.end()) {
219+
endpoints = servicesBlk->second;
220+
}
221+
profile.SetServices(Profile::Services(std::move(endpoints), servicesRef));
222+
}
223+
}
224+
174225
// Put sso-sessions into profiles
175226
for(auto& profile : m_foundProfiles)
176227
{
@@ -222,6 +273,7 @@ namespace Aws
222273
START = 0,
223274
PROFILE_FOUND,
224275
SSO_SESSION_FOUND,
276+
SERVICES_FOUND,
225277
UNKNOWN_SECTION_FOUND,
226278
FAILURE
227279
};
@@ -271,8 +323,9 @@ namespace Aws
271323

272324
/**
273325
* A helper function to parse config section declaration line
274-
* @param line, an input line, e.g. "[profile default]"
326+
* @param line, an input line, e.g. "[profile default]" or "[services s3]"
275327
* @param ioSectionName, a return argument representing parsed section Identifier, e.g. "default"
328+
* @param ioServiceId, a return argument representing parsed service ID for services sections
276329
* @param ioState, a return argument representing parser state, e.g. PROFILE_FOUND
277330
*/
278331
void ParseSectionDeclaration(const Aws::String& line,
@@ -331,21 +384,21 @@ namespace Aws
331384

332385
if(defaultProfileOrSsoSectionRequired)
333386
{
334-
if (sectionIdentifier != DEFAULT && sectionIdentifier != SSO_SESSION_SECTION)
387+
if (sectionIdentifier != DEFAULT && sectionIdentifier != SSO_SESSION_SECTION && sectionIdentifier != SERVICES_SECTION)
335388
{
336389
AWS_LOGSTREAM_ERROR(PARSER_TAG, "In configuration files, the profile name must start with "
337390
"profile keyword (except default profile): " << line);
338391
break;
339392
}
340-
if (sectionIdentifier != SSO_SESSION_SECTION)
393+
if (sectionIdentifier != SSO_SESSION_SECTION && sectionIdentifier != SERVICES_SECTION)
341394
{
342395
// profile found, still pending check for closing bracket
343396
ioState = PROFILE_FOUND;
344397
ioSectionName = sectionIdentifier;
345398
}
346399
}
347400

348-
if(!m_useProfilePrefix || sectionIdentifier != SSO_SESSION_SECTION)
401+
if(!m_useProfilePrefix || (sectionIdentifier != SSO_SESSION_SECTION && sectionIdentifier != SERVICES_SECTION))
349402
{
350403
// profile found, still pending check for closing bracket
351404
ioState = PROFILE_FOUND;
@@ -374,6 +427,32 @@ namespace Aws
374427
ioSectionName = sectionIdentifier;
375428
}
376429

430+
if(sectionIdentifier == SERVICES_SECTION)
431+
{
432+
// Check if this is [services] or [services name]
433+
pos = line.find_first_not_of(WHITESPACE_CHARACTERS, pos);
434+
if(pos == Aws::String::npos || line[pos] == RIGHT_BRACKET)
435+
{
436+
// This is just [services] section
437+
AWS_LOGSTREAM_ERROR(PARSER_TAG, "[services] section without name is not supported: " << line);
438+
break;
439+
}
440+
else
441+
{
442+
// This is [services name] section
443+
sectionIdentifier = ParseIdentifier(line, pos, errorMsg);
444+
if (!errorMsg.empty())
445+
{
446+
AWS_LOGSTREAM_ERROR(PARSER_TAG, "Failed to parse services definition name: " << errorMsg << " " << line);
447+
break;
448+
}
449+
pos += sectionIdentifier.length();
450+
// services definition found, still pending check for closing bracket
451+
ioState = SERVICES_FOUND;
452+
ioSectionName = sectionIdentifier;
453+
}
454+
}
455+
377456
pos = line.find_first_not_of(WHITESPACE_CHARACTERS, pos);
378457
if(pos == Aws::String::npos)
379458
{
@@ -394,7 +473,7 @@ namespace Aws
394473
break;
395474
}
396475
// the rest is a comment, and we don't care about it.
397-
if ((ioState != SSO_SESSION_FOUND && ioState != PROFILE_FOUND) || ioSectionName.empty())
476+
if ((ioState != SSO_SESSION_FOUND && ioState != PROFILE_FOUND && ioState != SERVICES_FOUND) || ioSectionName.empty())
398477
{
399478
AWS_LOGSTREAM_FATAL(PARSER_TAG, "Unexpected parser state after attempting to parse section " << line);
400479
break;
@@ -412,6 +491,7 @@ namespace Aws
412491
* (i.e. [profile default] and its key1=val1 under).
413492
* @param currentState, a current parser State, e.g. PROFILE_FOUND
414493
* @param currentSectionName, a current section identifier, e.g. "default"
494+
* @param currentServiceId, a current service identifier for services sections
415495
* @param currentKeyValues, a map of parsed key-value properties of a section definition being recorded
416496
*/
417497
void FlushSection(const State currentState, const Aws::String& currentSectionName, Aws::Map<Aws::String, Aws::String>& currentKeyValues)
@@ -529,6 +609,10 @@ namespace Aws
529609
ssoSession.SetName(currentSectionName);
530610
ssoSession.SetAllKeyValPairs(std::move(currentKeyValues));
531611
}
612+
else if (SERVICES_FOUND == currentState) {
613+
// Handle [services name] section - service endpoints are parsed inline during stream processing
614+
AWS_LOGSTREAM_DEBUG(PARSER_TAG, "Processed [services " << currentSectionName << "] section");
615+
}
532616
else
533617
{
534618
AWS_LOGSTREAM_FATAL(PARSER_TAG, "Unknown parser error: unexpected state " << currentState);
@@ -557,6 +641,7 @@ namespace Aws
557641

558642
Aws::Map<String, Profile> m_foundProfiles;
559643
Aws::Map<String, Profile::SsoSession> m_foundSsoSessions;
644+
Aws::Map<String, Aws::Map<String, String>> m_services;
560645
};
561646

562647
static const char* const CONFIG_FILE_LOADER = "Aws::Config::AWSConfigFileProfileConfigLoader";

0 commit comments

Comments
 (0)