diff --git a/gamelift-server-sdk-tests/source/aws/gamelift/internal/GameLiftServerStateTest.cpp b/gamelift-server-sdk-tests/source/aws/gamelift/internal/GameLiftServerStateTest.cpp index 0156e18..a288491 100644 --- a/gamelift-server-sdk-tests/source/aws/gamelift/internal/GameLiftServerStateTest.cpp +++ b/gamelift-server-sdk-tests/source/aws/gamelift/internal/GameLiftServerStateTest.cpp @@ -922,6 +922,97 @@ TEST_F(GameLiftServerStateTest, GIVEN_retriable_failure_WHEN_sendMessage_THEN_re // THEN ASSERT_TRUE(outcome.IsSuccess()); } + +TEST_F(GameLiftServerStateTest, GIVEN_nonContainerComputeType_WHEN_listContainersNetworkInfo_THEN_returnsUnsupportedError) { + // GIVEN +#ifdef _WIN32 + _putenv_s("GAMELIFT_COMPUTE_TYPE", "EC2"); +#else + setenv("GAMELIFT_COMPUTE_TYPE", "EC2", 1); +#endif + + // WHEN + auto outcome = serverState->ListContainersNetworkInfo(); + + // THEN + EXPECT_FALSE(outcome.IsSuccess()); + EXPECT_EQ(outcome.GetError().GetErrorType(), GAMELIFT_ERROR_TYPE::UNSUPPORTED_COMPUTE_TYPE_EXCEPTION); + +#ifdef _WIN32 + _putenv_s("GAMELIFT_COMPUTE_TYPE", ""); +#else + unsetenv("GAMELIFT_COMPUTE_TYPE"); +#endif +} + +TEST_F(GameLiftServerStateTest, GIVEN_noComputeType_WHEN_listContainersNetworkInfo_THEN_returnsUnsupportedError) { + // GIVEN +#ifdef _WIN32 + _putenv_s("GAMELIFT_COMPUTE_TYPE", ""); +#else + unsetenv("GAMELIFT_COMPUTE_TYPE"); +#endif + + // WHEN + auto outcome = serverState->ListContainersNetworkInfo(); + + // THEN + EXPECT_FALSE(outcome.IsSuccess()); + EXPECT_EQ(outcome.GetError().GetErrorType(), GAMELIFT_ERROR_TYPE::UNSUPPORTED_COMPUTE_TYPE_EXCEPTION); +} + +TEST_F(GameLiftServerStateTest, GIVEN_containerTypeNoEndpointNoMetadata_WHEN_listContainersNetworkInfo_THEN_returnsServiceError) { + // GIVEN +#ifdef _WIN32 + _putenv_s("GAMELIFT_COMPUTE_TYPE", "CONTAINER"); + _putenv_s("GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT", ""); + _putenv_s("ECS_CONTAINER_METADATA_URI_V4", ""); +#else + setenv("GAMELIFT_COMPUTE_TYPE", "CONTAINER", 1); + unsetenv("GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT"); + unsetenv("ECS_CONTAINER_METADATA_URI_V4"); +#endif + + // WHEN + auto outcome = serverState->ListContainersNetworkInfo(); + + // THEN - cannot resolve endpoint from env var or metadata + EXPECT_FALSE(outcome.IsSuccess()); + EXPECT_EQ(outcome.GetError().GetErrorType(), GAMELIFT_ERROR_TYPE::INTERNAL_SERVICE_EXCEPTION); + +#ifdef _WIN32 + _putenv_s("GAMELIFT_COMPUTE_TYPE", ""); +#else + unsetenv("GAMELIFT_COMPUTE_TYPE"); +#endif +} + +TEST_F(GameLiftServerStateTest, GIVEN_containerTypeWithUnreachableEndpoint_WHEN_listContainersNetworkInfo_THEN_returnsServiceError) { + // GIVEN +#ifdef _WIN32 + _putenv_s("GAMELIFT_COMPUTE_TYPE", "CONTAINER"); + _putenv_s("GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT", "http://192.0.2.1:9999"); +#else + setenv("GAMELIFT_COMPUTE_TYPE", "CONTAINER", 1); + setenv("GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT", "http://192.0.2.1:9999", 1); +#endif + + // WHEN + auto outcome = serverState->ListContainersNetworkInfo(); + + // THEN - connection fails to unreachable address + EXPECT_FALSE(outcome.IsSuccess()); + EXPECT_EQ(outcome.GetError().GetErrorType(), GAMELIFT_ERROR_TYPE::INTERNAL_SERVICE_EXCEPTION); + +#ifdef _WIN32 + _putenv_s("GAMELIFT_COMPUTE_TYPE", ""); + _putenv_s("GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT", ""); +#else + unsetenv("GAMELIFT_COMPUTE_TYPE"); + unsetenv("GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT"); +#endif +} + } // namespace Test } // namespace Internal } // namespace GameLift diff --git a/gamelift-server-sdk-tests/source/aws/gamelift/internal/network/GameLiftWebSocketClientManagerTest.cpp b/gamelift-server-sdk-tests/source/aws/gamelift/internal/network/GameLiftWebSocketClientManagerTest.cpp index 818dcd1..bead86a 100644 --- a/gamelift-server-sdk-tests/source/aws/gamelift/internal/network/GameLiftWebSocketClientManagerTest.cpp +++ b/gamelift-server-sdk-tests/source/aws/gamelift/internal/network/GameLiftWebSocketClientManagerTest.cpp @@ -19,7 +19,7 @@ namespace GameLift { namespace Internal { namespace Test { -static const std::string sdkVersion = "5.4.1"; +static const std::string sdkVersion = "5.5.0"; // private constants copied over for testing purposes static constexpr const char *PID_KEY = "pID"; static constexpr const char *SDK_VERSION_KEY = "sdkVersion"; diff --git a/gamelift-server-sdk-tests/source/aws/gamelift/server/GameLiftServerAPITest.cpp b/gamelift-server-sdk-tests/source/aws/gamelift/server/GameLiftServerAPITest.cpp index f33c5c7..f8a3b3d 100644 --- a/gamelift-server-sdk-tests/source/aws/gamelift/server/GameLiftServerAPITest.cpp +++ b/gamelift-server-sdk-tests/source/aws/gamelift/server/GameLiftServerAPITest.cpp @@ -20,7 +20,7 @@ namespace GameLift { namespace Server { namespace Test { -static const std::string sdkVersion = "5.4.1"; +static const std::string sdkVersion = "5.5.0"; TEST(GameLiftServerAPITest, GIVEN_SdkVersion_WHEN_GetSdkVersion_THEN_success) { // GIVEN diff --git a/gamelift-server-sdk-tests/source/aws/gamelift/server/model/ListContainersNetworkInfoTest.cpp b/gamelift-server-sdk-tests/source/aws/gamelift/server/model/ListContainersNetworkInfoTest.cpp new file mode 100644 index 0000000..d454977 --- /dev/null +++ b/gamelift-server-sdk-tests/source/aws/gamelift/server/model/ListContainersNetworkInfoTest.cpp @@ -0,0 +1,126 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include +#include +#include + +using namespace Aws::GameLift; +using namespace Aws::GameLift::Server::Model; + +namespace Aws { +namespace GameLift { +namespace Internal { +namespace Test { + +class ListContainersNetworkInfoTest : public ::testing::Test { +protected: + void SetUp() override { + } + + void TearDown() override { +#ifdef _WIN32 + _putenv_s("GAMELIFT_COMPUTE_TYPE", ""); + _putenv_s("GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT", ""); + _putenv_s("ECS_CONTAINER_METADATA_URI_V4", ""); +#else + unsetenv("GAMELIFT_COMPUTE_TYPE"); + unsetenv("GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT"); + unsetenv("ECS_CONTAINER_METADATA_URI_V4"); +#endif + } +}; + +TEST_F(ListContainersNetworkInfoTest, GIVEN_sdkNotInitialized_WHEN_listContainersNetworkInfo_THEN_returnsError) { + // GIVEN - SDK not initialized + + // WHEN + auto outcome = Server::ListContainersNetworkInfo(); + + // THEN + EXPECT_FALSE(outcome.IsSuccess()); + EXPECT_EQ(outcome.GetError().GetErrorType(), GAMELIFT_ERROR_TYPE::NOT_INITIALIZED); +} + +// Model tests +TEST(ContainerNetworkInfoModelTest, GIVEN_defaultConstructor_WHEN_created_THEN_hasDefaultValues) { + ContainerNetworkInfo info; + EXPECT_EQ(info.GetContainerGroupType(), ContainerGroupType::GAME_SERVER); +} + +TEST(ContainerNetworkInfoModelTest, GIVEN_values_WHEN_setAndGet_THEN_returnsCorrectValues) { + ContainerNetworkInfo info; + info.SetContainerName("otel-collector"); + info.SetContainerId("abc123def456"); + info.SetIpAddress("172.17.0.2"); + info.SetContainerGroupType(ContainerGroupType::PER_INSTANCE); + +#ifdef GAMELIFT_USE_STD + EXPECT_EQ(info.GetContainerName(), "otel-collector"); + EXPECT_EQ(info.GetContainerId(), "abc123def456"); + EXPECT_EQ(info.GetIpAddress(), "172.17.0.2"); +#else + EXPECT_STREQ(info.GetContainerName(), "otel-collector"); + EXPECT_STREQ(info.GetContainerId(), "abc123def456"); + EXPECT_STREQ(info.GetIpAddress(), "172.17.0.2"); +#endif + EXPECT_EQ(info.GetContainerGroupType(), ContainerGroupType::PER_INSTANCE); +} + +// The discovery server returns the full 64-char Docker SHA-256 container ID rather than the +// truncated 12-char short form. Verify the buffer holds it without truncation. +TEST(ContainerNetworkInfoModelTest, GIVEN_fullDockerContainerId_WHEN_setAndGet_THEN_preservesAll64Chars) { + const char *fullContainerId = + "9d97c0b58d18a3f2e4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7"; + ASSERT_EQ(strlen(fullContainerId), 64u); + + ContainerNetworkInfo info; + info.SetContainerId(fullContainerId); + +#ifdef GAMELIFT_USE_STD + EXPECT_EQ(info.GetContainerId(), fullContainerId); + EXPECT_EQ(info.GetContainerId().size(), 64u); +#else + EXPECT_STREQ(info.GetContainerId(), fullContainerId); + EXPECT_EQ(strlen(info.GetContainerId()), 64u); +#endif +} + +TEST(ContainerNetworkInfoModelTest, GIVEN_result_WHEN_addContainerNetworkInfo_THEN_containsEntry) { + ListContainersNetworkInfoResult result; + + ContainerNetworkInfo info; + info.SetContainerName("game-server"); + info.SetContainerId("9d97c0b58d18"); + info.SetIpAddress("172.17.0.4"); + info.SetContainerGroupType(ContainerGroupType::GAME_SERVER); + + result.AddContainerNetworkInfo(info); + +#ifdef GAMELIFT_USE_STD + EXPECT_EQ(result.GetContainersNetworkInfo().size(), 1); + EXPECT_EQ(result.GetContainersNetworkInfo()[0].GetContainerName(), "game-server"); + EXPECT_EQ(result.GetContainersNetworkInfo()[0].GetIpAddress(), "172.17.0.4"); +#else + EXPECT_EQ(result.GetContainersNetworkInfoCount(), 1); + EXPECT_STREQ(result.GetContainersNetworkInfo()[0].GetContainerName(), "game-server"); + EXPECT_STREQ(result.GetContainersNetworkInfo()[0].GetIpAddress(), "172.17.0.4"); +#endif +} + +} // namespace Test +} // namespace Internal +} // namespace GameLift +} // namespace Aws diff --git a/gamelift-server-sdk/include/aws/gamelift/common/GameLiftErrors.h b/gamelift-server-sdk/include/aws/gamelift/common/GameLiftErrors.h index 39c613b..1914835 100644 --- a/gamelift-server-sdk/include/aws/gamelift/common/GameLiftErrors.h +++ b/gamelift-server-sdk/include/aws/gamelift/common/GameLiftErrors.h @@ -66,7 +66,8 @@ enum class AWS_GAMELIFT_API GAMELIFT_ERROR_TYPE { WEBSOCKET_RETRIABLE_SEND_MESSAGE_FAILURE, // Retriable failure to send message to the Amazon GameLift Servers // Service WebSocket WEBSOCKET_SEND_MESSAGE_FAILURE, // Failure to send message to the Amazon GameLift Servers WebSocket - VALIDATION_EXCEPTION // Client-side error when invalid parameters are passed. + VALIDATION_EXCEPTION, // Client-side error when invalid parameters are passed. + UNSUPPORTED_COMPUTE_TYPE_EXCEPTION // API called on an unsupported compute type. }; class AWS_GAMELIFT_API GameLiftError { @@ -260,6 +261,8 @@ class AWS_GAMELIFT_API GameLiftError { return "WebSocket Send Message Failed."; case GAMELIFT_ERROR_TYPE::VALIDATION_EXCEPTION: return "Validation exception."; + case GAMELIFT_ERROR_TYPE::UNSUPPORTED_COMPUTE_TYPE_EXCEPTION: + return "Unsupported compute type."; default: return "Unknown Error"; } @@ -350,6 +353,8 @@ class AWS_GAMELIFT_API GameLiftError { return "Sending Message to the Amazon GameLift Servers WebSocket has failed."; case GAMELIFT_ERROR_TYPE::VALIDATION_EXCEPTION: return "The input is invalid."; + case GAMELIFT_ERROR_TYPE::UNSUPPORTED_COMPUTE_TYPE_EXCEPTION: + return "This API is not supported on the current compute type."; default: return "An unexpected error has occurred."; } diff --git a/gamelift-server-sdk/include/aws/gamelift/common/Outcome.h b/gamelift-server-sdk/include/aws/gamelift/common/Outcome.h index a9522aa..efd4e10 100644 --- a/gamelift-server-sdk/include/aws/gamelift/common/Outcome.h +++ b/gamelift-server-sdk/include/aws/gamelift/common/Outcome.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -124,6 +125,7 @@ typedef Outcome AwsLongOutcome; typedef Outcome DescribePlayerSessionsOutcome; typedef Outcome StartMatchBackfillOutcome; typedef Outcome GetComputeCertificateOutcome; +typedef Outcome ListContainersNetworkInfoOutcome; typedef Outcome GetFleetRoleCredentialsOutcome; } // namespace GameLift } // namespace Aws diff --git a/gamelift-server-sdk/include/aws/gamelift/internal/GameLiftServerState.h b/gamelift-server-sdk/include/aws/gamelift/internal/GameLiftServerState.h index d28d1a3..1cd96a6 100644 --- a/gamelift-server-sdk/include/aws/gamelift/internal/GameLiftServerState.h +++ b/gamelift-server-sdk/include/aws/gamelift/internal/GameLiftServerState.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,9 @@ class GameLiftServerState : public GameLiftCommonState, public IGameLiftMessageH static constexpr const char *ENV_VAR_SDK_TOOL_VERSION = "GAMELIFT_SDK_TOOL_VERSION"; static constexpr const char *COMPUTE_TYPE_CONTAINER = "CONTAINER"; static constexpr const char *AGENTLESS_CONTAINER_PROCESS_ID = "ManagedResource"; + static constexpr const char *ENV_VAR_CONTAINER_DISCOVERY_SERVER_ENDPOINT = "GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT"; + static constexpr const char *ENV_VAR_CONTAINER_METADATA_URI = "ECS_CONTAINER_METADATA_URI_V4"; + static constexpr const int DISCOVERY_SERVER_PORT = 4092; static constexpr const int HEALTHCHECK_INTERVAL_MILLIS = 60 * 1000; static constexpr const int HEALTHCHECK_MAX_JITTER_MILLIS = 10 * 1000; @@ -208,6 +212,8 @@ class GameLiftServerState : public GameLiftCommonState, public IGameLiftMessageH public: GetFleetRoleCredentialsOutcome GetFleetRoleCredentials(const Aws::GameLift::Server::Model::GetFleetRoleCredentialsRequest &request); + ListContainersNetworkInfoOutcome ListContainersNetworkInfo(); + void SetGlobalProcessor(Aws::GameLift::Metrics::IMetricsProcessor* processor); // When within 15 minutes of expiration we retrieve new instance role credentials @@ -216,6 +222,9 @@ class GameLiftServerState : public GameLiftCommonState, public IGameLiftMessageH private: bool AssertNetworkInitialized(); void SetUpCallbacks(); + std::string ResolveDiscoveryEndpointFromMetadata(HttpClient &httpClient); + Outcome FetchDiscoveryServerResponse(HttpClient &httpClient); + ListContainersNetworkInfoOutcome ParseDiscoveryServerResponse(HttpResponse &response); static void DetectGameLiftTools(); bool m_processReady; diff --git a/gamelift-server-sdk/include/aws/gamelift/server/GameLiftServerAPI.h b/gamelift-server-sdk/include/aws/gamelift/server/GameLiftServerAPI.h index f254f44..3940591 100644 --- a/gamelift-server-sdk/include/aws/gamelift/server/GameLiftServerAPI.h +++ b/gamelift-server-sdk/include/aws/gamelift/server/GameLiftServerAPI.h @@ -331,6 +331,13 @@ AWS_GAMELIFT_API GetComputeCertificateOutcome GetComputeCertificate(); */ AWS_GAMELIFT_API GetFleetRoleCredentialsOutcome GetFleetRoleCredentials(const Aws::GameLift::Server::Model::GetFleetRoleCredentialsRequest &request); +/** +Queries the container discovery server and returns network information for all containers +running on the same instance. Only supported on container fleets. +@return ListContainersNetworkInfoOutcome containing container names, IPs, IDs, and group types. +*/ +AWS_GAMELIFT_API ListContainersNetworkInfoOutcome ListContainersNetworkInfo(); + } // namespace Server } // namespace GameLift } // namespace Aws diff --git a/gamelift-server-sdk/include/aws/gamelift/server/model/ListContainersNetworkInfoResult.h b/gamelift-server-sdk/include/aws/gamelift/server/model/ListContainersNetworkInfoResult.h new file mode 100644 index 0000000..0e6eb0b --- /dev/null +++ b/gamelift-server-sdk/include/aws/gamelift/server/model/ListContainersNetworkInfoResult.h @@ -0,0 +1,260 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ +#pragma once + +#if defined(_MSC_VER) && !defined(GAMELIFT_USE_STD) +#pragma warning(push) // Save warning settings. +#pragma warning(disable : 4996) // Disable deprecated warning for strncpy +#endif + +#include + +#ifndef GAMELIFT_USE_STD +#include +#include +#endif + +#ifdef GAMELIFT_USE_STD +#include +#include +#endif + +namespace Aws { +namespace GameLift { +namespace Server { +namespace Model { + +enum class ContainerGroupType { GAME_SERVER, PER_INSTANCE }; + +#ifdef GAMELIFT_USE_STD + +class AWS_GAMELIFT_API ContainerNetworkInfo { +public: + ContainerNetworkInfo() : m_containerGroupType(ContainerGroupType::GAME_SERVER) {} + ~ContainerNetworkInfo() {} + + ContainerNetworkInfo(const ContainerNetworkInfo &other) + : m_containerName(other.m_containerName), m_containerId(other.m_containerId), + m_ipAddress(other.m_ipAddress), m_containerGroupType(other.m_containerGroupType) {} + + ContainerNetworkInfo(ContainerNetworkInfo &&other) { *this = std::move(other); } + + ContainerNetworkInfo &operator=(const ContainerNetworkInfo &other) { + m_containerName = other.m_containerName; + m_containerId = other.m_containerId; + m_ipAddress = other.m_ipAddress; + m_containerGroupType = other.m_containerGroupType; + return *this; + } + + ContainerNetworkInfo &operator=(ContainerNetworkInfo &&other) { + m_containerName = std::move(other.m_containerName); + m_containerId = std::move(other.m_containerId); + m_ipAddress = std::move(other.m_ipAddress); + m_containerGroupType = other.m_containerGroupType; + return *this; + } + + inline const std::string &GetContainerName() const { return m_containerName; } + inline void SetContainerName(const std::string &value) { m_containerName = value; } + inline ContainerNetworkInfo &WithContainerName(const std::string &value) { + SetContainerName(value); + return *this; + } + + inline const std::string &GetContainerId() const { return m_containerId; } + inline void SetContainerId(const std::string &value) { m_containerId = value; } + inline ContainerNetworkInfo &WithContainerId(const std::string &value) { + SetContainerId(value); + return *this; + } + + inline const std::string &GetIpAddress() const { return m_ipAddress; } + inline void SetIpAddress(const std::string &value) { m_ipAddress = value; } + inline ContainerNetworkInfo &WithIpAddress(const std::string &value) { + SetIpAddress(value); + return *this; + } + + inline ContainerGroupType GetContainerGroupType() const { return m_containerGroupType; } + inline void SetContainerGroupType(ContainerGroupType value) { m_containerGroupType = value; } + inline ContainerNetworkInfo &WithContainerGroupType(ContainerGroupType value) { + SetContainerGroupType(value); + return *this; + } + +private: + std::string m_containerName; + std::string m_containerId; + std::string m_ipAddress; + ContainerGroupType m_containerGroupType; +}; + +class AWS_GAMELIFT_API ListContainersNetworkInfoResult { +public: + ListContainersNetworkInfoResult() {} + ~ListContainersNetworkInfoResult() {} + + ListContainersNetworkInfoResult(const ListContainersNetworkInfoResult &other) + : m_containersNetworkInfo(other.m_containersNetworkInfo) {} + + ListContainersNetworkInfoResult(ListContainersNetworkInfoResult &&other) { *this = std::move(other); } + + ListContainersNetworkInfoResult &operator=(const ListContainersNetworkInfoResult &other) { + m_containersNetworkInfo = other.m_containersNetworkInfo; + return *this; + } + + ListContainersNetworkInfoResult &operator=(ListContainersNetworkInfoResult &&other) { + m_containersNetworkInfo = std::move(other.m_containersNetworkInfo); + return *this; + } + + inline const std::vector &GetContainersNetworkInfo() const { return m_containersNetworkInfo; } + inline void SetContainersNetworkInfo(const std::vector &value) { m_containersNetworkInfo = value; } + inline void AddContainerNetworkInfo(const ContainerNetworkInfo &value) { m_containersNetworkInfo.push_back(value); } + inline ListContainersNetworkInfoResult &WithContainersNetworkInfo(const std::vector &value) { + SetContainersNetworkInfo(value); + return *this; + } + +private: + std::vector m_containersNetworkInfo; +}; + +#else + +#ifndef MAX_CONTAINER_NAME_LENGTH +#define MAX_CONTAINER_NAME_LENGTH 128 +#endif +#ifndef MAX_CONTAINER_ID_LENGTH +// Docker container IDs are SHA-256 digests rendered as 64 hex chars; +1 for null terminator. +#define MAX_CONTAINER_ID_LENGTH 65 +#endif +#ifndef MAX_IP_ADDRESS_LENGTH +#define MAX_IP_ADDRESS_LENGTH 46 +#endif +#ifndef MAX_CONTAINER_NETWORK_INFOS +#define MAX_CONTAINER_NETWORK_INFOS 1527 +#endif + +class AWS_GAMELIFT_API ContainerNetworkInfo { +public: + ContainerNetworkInfo() : m_containerGroupType(ContainerGroupType::GAME_SERVER) { + memset(m_containerName, 0, sizeof(m_containerName)); + memset(m_containerId, 0, sizeof(m_containerId)); + memset(m_ipAddress, 0, sizeof(m_ipAddress)); + } + + ~ContainerNetworkInfo() {} + + ContainerNetworkInfo(const ContainerNetworkInfo &other) : m_containerGroupType(other.m_containerGroupType) { + strncpy(m_containerName, other.m_containerName, sizeof(m_containerName)); + strncpy(m_containerId, other.m_containerId, sizeof(m_containerId)); + strncpy(m_ipAddress, other.m_ipAddress, sizeof(m_ipAddress)); + } + + ContainerNetworkInfo &operator=(const ContainerNetworkInfo &other) { + strncpy(m_containerName, other.m_containerName, sizeof(m_containerName)); + strncpy(m_containerId, other.m_containerId, sizeof(m_containerId)); + strncpy(m_ipAddress, other.m_ipAddress, sizeof(m_ipAddress)); + m_containerGroupType = other.m_containerGroupType; + return *this; + } + + inline const char *GetContainerName() const { return m_containerName; } + inline void SetContainerName(const char *value) { + strncpy(m_containerName, value, sizeof(m_containerName)); + m_containerName[sizeof(m_containerName) - 1] = '\0'; + } + inline ContainerNetworkInfo &WithContainerName(const char *value) { + SetContainerName(value); + return *this; + } + + inline const char *GetContainerId() const { return m_containerId; } + inline void SetContainerId(const char *value) { + strncpy(m_containerId, value, sizeof(m_containerId)); + m_containerId[sizeof(m_containerId) - 1] = '\0'; + } + inline ContainerNetworkInfo &WithContainerId(const char *value) { + SetContainerId(value); + return *this; + } + + inline const char *GetIpAddress() const { return m_ipAddress; } + inline void SetIpAddress(const char *value) { + strncpy(m_ipAddress, value, sizeof(m_ipAddress)); + m_ipAddress[sizeof(m_ipAddress) - 1] = '\0'; + } + inline ContainerNetworkInfo &WithIpAddress(const char *value) { + SetIpAddress(value); + return *this; + } + + inline ContainerGroupType GetContainerGroupType() const { return m_containerGroupType; } + inline void SetContainerGroupType(ContainerGroupType value) { m_containerGroupType = value; } + inline ContainerNetworkInfo &WithContainerGroupType(ContainerGroupType value) { + SetContainerGroupType(value); + return *this; + } + +private: + char m_containerName[MAX_CONTAINER_NAME_LENGTH]; + char m_containerId[MAX_CONTAINER_ID_LENGTH]; + char m_ipAddress[MAX_IP_ADDRESS_LENGTH]; + ContainerGroupType m_containerGroupType; +}; + +class AWS_GAMELIFT_API ListContainersNetworkInfoResult { +public: + ListContainersNetworkInfoResult() : m_containersNetworkInfo_count(0) {} + ~ListContainersNetworkInfoResult() {} + + ListContainersNetworkInfoResult(const ListContainersNetworkInfoResult &other) + : m_containersNetworkInfo_count(other.m_containersNetworkInfo_count) { + std::copy(std::begin(other.m_containersNetworkInfo), + std::begin(other.m_containersNetworkInfo) + other.m_containersNetworkInfo_count, + std::begin(m_containersNetworkInfo)); + } + + ListContainersNetworkInfoResult &operator=(const ListContainersNetworkInfoResult &other) { + m_containersNetworkInfo_count = other.m_containersNetworkInfo_count; + std::copy(std::begin(other.m_containersNetworkInfo), + std::begin(other.m_containersNetworkInfo) + other.m_containersNetworkInfo_count, + std::begin(m_containersNetworkInfo)); + return *this; + } + + inline const ContainerNetworkInfo *GetContainersNetworkInfo() const { return m_containersNetworkInfo; } + inline int GetContainersNetworkInfoCount() const { return m_containersNetworkInfo_count; } + inline void AddContainerNetworkInfo(const ContainerNetworkInfo &value) { + if (m_containersNetworkInfo_count < MAX_CONTAINER_NETWORK_INFOS) { + m_containersNetworkInfo[m_containersNetworkInfo_count++] = value; + } + } + +private: + ContainerNetworkInfo m_containersNetworkInfo[MAX_CONTAINER_NETWORK_INFOS]; + int m_containersNetworkInfo_count; +}; + +#endif + +} // namespace Model +} // namespace Server +} // namespace GameLift +} // namespace Aws + +#if defined(_MSC_VER) && !defined(GAMELIFT_USE_STD) +#pragma warning(pop) // Restore warnings to previous state. +#endif diff --git a/gamelift-server-sdk/source/aws/gamelift/internal/GameLiftServerState.cpp b/gamelift-server-sdk/source/aws/gamelift/internal/GameLiftServerState.cpp index ffca185..c691f40 100644 --- a/gamelift-server-sdk/source/aws/gamelift/internal/GameLiftServerState.cpp +++ b/gamelift-server-sdk/source/aws/gamelift/internal/GameLiftServerState.cpp @@ -53,6 +53,7 @@ #include #include #include +#include "rapidjson/document.h" using namespace Aws::GameLift; @@ -1065,6 +1066,151 @@ Aws::GameLift::Internal::GameLiftServerState::GetFleetRoleCredentials(const Aws: return GetFleetRoleCredentialsOutcome(result); } +ListContainersNetworkInfoOutcome Aws::GameLift::Internal::GameLiftServerState::ListContainersNetworkInfo() { + spdlog::info("ListContainersNetworkInfo called"); + const char *computeType = std::getenv(ENV_VAR_COMPUTE_TYPE); + if (!computeType || std::strcmp(computeType, COMPUTE_TYPE_CONTAINER) != 0) { + spdlog::error("ListContainersNetworkInfo is only supported on container fleets"); + return ListContainersNetworkInfoOutcome(GameLiftError(GAMELIFT_ERROR_TYPE::UNSUPPORTED_COMPUTE_TYPE_EXCEPTION, + "ListContainersNetworkInfo is only supported on container fleets.")); + } + + HttpClient httpClient; + auto fetchOutcome = FetchDiscoveryServerResponse(httpClient); + if (!fetchOutcome.IsSuccess()) { + spdlog::error("{}", fetchOutcome.GetError()); + return ListContainersNetworkInfoOutcome(GameLiftError(GAMELIFT_ERROR_TYPE::INTERNAL_SERVICE_EXCEPTION, + fetchOutcome.GetError().c_str())); + } + + return ParseDiscoveryServerResponse(fetchOutcome.GetResult()); +} + +// Resolves the discovery server endpoint and fetches the /v1/ response. +// Strategy: +// 1. Use GAMELIFT_CONTAINER_DISCOVERY_SERVER_ENDPOINT env var set by GameLift on container fleets +// 2. Fallback: query ECS container metadata to get this container's IP, derive the bridge gateway +// (replace last octet with .1), and use that as the discovery server address on the known port. +// 3. If the env var endpoint fails to connect, retry with the metadata-derived endpoint in case +// the env var is stale (e.g., Docker bridge IP changed). +Outcome Aws::GameLift::Internal::GameLiftServerState::FetchDiscoveryServerResponse(HttpClient &httpClient) { + const char *endpointEnv = std::getenv(ENV_VAR_CONTAINER_DISCOVERY_SERVER_ENDPOINT); + std::string endpoint = endpointEnv ? endpointEnv : ""; + + if (endpoint.empty()) { + endpoint = ResolveDiscoveryEndpointFromMetadata(httpClient); + if (endpoint.empty()) { + return std::string("Could not resolve discovery server endpoint."); + } + spdlog::info("Resolved discovery server endpoint from ECS metadata: {}", endpoint); + } else { + spdlog::info("Using discovery server endpoint from env var: {}", endpoint); + } + + std::string url = endpoint + "/v1/"; + + try { + return httpClient.SendGetRequest(url); + } catch (const std::runtime_error &e) { + if (endpointEnv) { + spdlog::warn("Failed to connect at {}: {}. Attempting fallback.", url, e.what()); + std::string fallback = ResolveDiscoveryEndpointFromMetadata(httpClient); + if (!fallback.empty() && fallback != endpoint) { + spdlog::info("Fallback: trying {}", fallback); + try { + return httpClient.SendGetRequest(fallback + "/v1/"); + } catch (const std::runtime_error &fe) { + return std::string("Failed to connect to discovery server: " + std::string(fe.what())); + } + } + } + return std::string("Failed to connect to discovery server: " + std::string(e.what())); + } +} + +std::string Aws::GameLift::Internal::GameLiftServerState::ResolveDiscoveryEndpointFromMetadata(HttpClient &httpClient) { + const char *metadataUri = std::getenv(ENV_VAR_CONTAINER_METADATA_URI); + if (!metadataUri) { + return ""; + } + + try { + HttpResponse metadataResponse = httpClient.SendGetRequest(std::string(metadataUri)); + if (!metadataResponse.IsSuccessfulStatusCode()) { + return ""; + } + + rapidjson::Document metaDoc; + if (metaDoc.Parse(metadataResponse.body.c_str()).HasParseError()) { + return ""; + } + + // ECS container metadata returns: {"Networks":[{"NetworkMode":"bridge","IPv4Addresses":["172.17.0.5"]}]} + // The bridge gateway is always .1 on the container's subnet (e.g., 172.17.0.5 → 172.17.0.1) + if (metaDoc.HasMember("Networks") && metaDoc["Networks"].IsArray() && metaDoc["Networks"].Size() > 0) { + const auto &network = metaDoc["Networks"][0]; + if (network.HasMember("IPv4Addresses") && network["IPv4Addresses"].IsArray() + && network["IPv4Addresses"].Size() > 0) { + std::string containerIp = network["IPv4Addresses"][0].GetString(); + std::string gatewayIp = containerIp.substr(0, containerIp.find_last_of('.')) + ".1"; + return "http://" + gatewayIp + ":" + std::to_string(DISCOVERY_SERVER_PORT); + } + } + } catch (const std::runtime_error &) { + } + + return ""; +} + +ListContainersNetworkInfoOutcome Aws::GameLift::Internal::GameLiftServerState::ParseDiscoveryServerResponse(HttpResponse &response) { + if (!response.IsSuccessfulStatusCode()) { + spdlog::error("Discovery server returned HTTP {}: {}", response.statusCode, response.body); + return ListContainersNetworkInfoOutcome(GameLiftError(GAMELIFT_ERROR_TYPE::INTERNAL_SERVICE_EXCEPTION, + ("Discovery server returned HTTP " + std::to_string(response.statusCode)).c_str())); + } + + rapidjson::Document document; + if (document.Parse(response.body.c_str()).HasParseError() || !document.IsArray()) { + spdlog::error("Failed to parse discovery server response: {}", response.body); + return ListContainersNetworkInfoOutcome(GameLiftError(GAMELIFT_ERROR_TYPE::INTERNAL_SERVICE_EXCEPTION, + "Invalid response from discovery server")); + } + + if (document.Size() == 0) { + spdlog::warn("Discovery server returned empty container list"); + } + + spdlog::info("ListContainersNetworkInfo response: {}", response.body); + + Aws::GameLift::Server::Model::ListContainersNetworkInfoResult result; + for (rapidjson::SizeType i = 0; i < document.Size(); i++) { + const auto &entry = document[i]; + if (!entry.IsObject()) continue; + + Aws::GameLift::Server::Model::ContainerNetworkInfo info; + if (entry.HasMember("containerName") && entry["containerName"].IsString()) { + info.SetContainerName(entry["containerName"].GetString()); + } + if (entry.HasMember("containerId") && entry["containerId"].IsString()) { + info.SetContainerId(entry["containerId"].GetString()); + } + if (entry.HasMember("ipAddress") && entry["ipAddress"].IsString()) { + info.SetIpAddress(entry["ipAddress"].GetString()); + } + if (entry.HasMember("containerGroupType") && entry["containerGroupType"].IsString()) { + std::string groupType = entry["containerGroupType"].GetString(); + if (groupType == "PER_INSTANCE") { + info.SetContainerGroupType(Aws::GameLift::Server::Model::ContainerGroupType::PER_INSTANCE); + } else { + info.SetContainerGroupType(Aws::GameLift::Server::Model::ContainerGroupType::GAME_SERVER); + } + } + result.AddContainerNetworkInfo(info); + } + + return ListContainersNetworkInfoOutcome(result); +} + void Aws::GameLift::Internal::GameLiftServerState::GetOverrideParams( char **webSocketUrl, char **authToken, diff --git a/gamelift-server-sdk/source/aws/gamelift/server/GameLiftServerAPI.cpp b/gamelift-server-sdk/source/aws/gamelift/server/GameLiftServerAPI.cpp index 8620be4..769d2d9 100644 --- a/gamelift-server-sdk/source/aws/gamelift/server/GameLiftServerAPI.cpp +++ b/gamelift-server-sdk/source/aws/gamelift/server/GameLiftServerAPI.cpp @@ -24,7 +24,7 @@ using namespace Aws::GameLift; -static const std::string sdkVersion = "5.4.1"; +static const std::string sdkVersion = "5.5.0"; #ifdef GAMELIFT_USE_STD Aws::GameLift::AwsStringOutcome Server::GetSdkVersion() { return AwsStringOutcome(sdkVersion); } @@ -396,6 +396,21 @@ GetFleetRoleCredentialsOutcome Server::GetFleetRoleCredentials(const Aws::GameLi return GetFleetRoleCredentialsOutcome(GameLiftError(GAMELIFT_ERROR_TYPE::NOT_INITIALIZED)); } +ListContainersNetworkInfoOutcome Server::ListContainersNetworkInfo() { + Internal::GetInstanceOutcome giOutcome = Internal::GameLiftCommonState::GetInstance(Internal::GAMELIFT_INTERNAL_STATE_TYPE::SERVER); + + if (!giOutcome.IsSuccess()) { + return ListContainersNetworkInfoOutcome(giOutcome.GetError()); + } + + auto *serverState = dynamic_cast(giOutcome.GetResult()); + if (serverState != nullptr) { + return serverState->ListContainersNetworkInfo(); + } + + return ListContainersNetworkInfoOutcome(GameLiftError(GAMELIFT_ERROR_TYPE::NOT_INITIALIZED)); +} + GenericOutcome Server::InitMetrics() { return InitMetrics(Aws::GameLift::Metrics::CreateMetricsParametersFromEnvironmentOrDefault()); } diff --git a/release-notes/5.5.0-release-notes.txt b/release-notes/5.5.0-release-notes.txt new file mode 100644 index 0000000..ef8f3c3 --- /dev/null +++ b/release-notes/5.5.0-release-notes.txt @@ -0,0 +1 @@ +- Adds the ListContainersNetworkInfo() server SDK action to retrieve network information for all containers running on the same instance of a managed container fleet, enabling container-to-container discovery.