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
19 changes: 11 additions & 8 deletions .github/workflows/server-dynamodb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@ on:
- cron: '0 8 * * *'

jobs:
build-dynamodb:
build-test-dynamodb:
runs-on: ubuntu-22.04
services:
dynamodb:
image: amazon/dynamodb-local
ports:
- 8000:8000
steps:
# https://github.com/actions/checkout/releases/tag/v4.3.0
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
- uses: ./.github/actions/ci
with:
cmake_target: launchdarkly-cpp-server-dynamodb-source
# No tests yet; PR 1 is scaffold-only and proves the AWS SDK builds.
run_tests: false
# AWS C++ SDK requires libcurl at link time on Linux/macOS.
install_curl: true
simulate_release: false
simulate_release: true
build-dynamodb-mac:
runs-on: macos-15
steps:
Expand All @@ -36,9 +39,9 @@ jobs:
with:
cmake_target: launchdarkly-cpp-server-dynamodb-source
platform_version: 12
run_tests: false
run_tests: false # TODO: figure out how to run dynamodb-local on Mac
install_curl: true
simulate_release: false
simulate_release: true
build-dynamodb-windows:
runs-on: windows-2022
steps:
Expand All @@ -55,6 +58,6 @@ jobs:
cmake_target: launchdarkly-cpp-server-dynamodb-source
platform_version: 2022
toolset: msvc
run_tests: false
run_tests: false # TODO: figure out how to run dynamodb-local on Windows
install_curl: true
simulate_windows_release: false
simulate_windows_release: true
38 changes: 38 additions & 0 deletions cmake/aws-sdk-cpp.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,42 @@ FetchContent_Declare(aws-sdk-cpp
OVERRIDE_FIND_PACKAGE
)

# Always build the AWS SDK as static archives, even when our own library is
# being built shared (LD_BUILD_SHARED_LIBS=ON). Building aws-sdk-cpp as
# BUILD_SHARED_LIBS=ON produces dylibs on macOS whose dynamodb component
# fails to find AWS Core symbols at link time (the AWS SDK's visibility
# configuration doesn't export them consistently across its FetchContent
# build). Linking aws-sdk-cpp statically into our shared wrapper sidesteps
# the issue.
#
# This needs cache manipulation rather than a function-scoped `set()`:
# aws-sdk-cpp's top-level CMakeLists pins `cmake_policy(SET CMP0077 OLD)`,
# which makes its `option(BUILD_SHARED_LIBS ... ON)` ignore non-cache
# variables and unconditionally write the cache. The only way to stop
# `option()` from setting the cache to ON is to pre-populate the cache
# with OFF before FetchContent_MakeAvailable runs.
#
# The prior cache state is saved before the FORCE-write and restored
# immediately after FetchContent finishes, so other subprojects (e.g.
# libs/server-sdk-otel) see whatever BUILD_SHARED_LIBS value was in the
# cache when this file was first included. An earlier attempt that
# FORCE-wrote OFF without restoring leaked into those subprojects; the
# save/restore here is the fix for that.
if (DEFINED CACHE{BUILD_SHARED_LIBS})
set(_LD_AWS_BSL_PREV "${BUILD_SHARED_LIBS}")
set(_LD_AWS_BSL_WAS_CACHED TRUE)
else ()
set(_LD_AWS_BSL_WAS_CACHED FALSE)
endif ()

set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)

FetchContent_MakeAvailable(aws-sdk-cpp)

if (_LD_AWS_BSL_WAS_CACHED)
set(BUILD_SHARED_LIBS "${_LD_AWS_BSL_PREV}" CACHE BOOL "" FORCE)
else ()
unset(BUILD_SHARED_LIBS CACHE)
endif ()
unset(_LD_AWS_BSL_PREV)
unset(_LD_AWS_BSL_WAS_CACHED)
4 changes: 4 additions & 0 deletions libs/server-sdk-dynamodb-source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ include(FetchContent)
include(${CMAKE_FILES}/aws-sdk-cpp.cmake)

add_subdirectory(src)

if (LD_BUILD_UNIT_TESTS)
add_subdirectory(tests)
endif ()
3 changes: 1 addition & 2 deletions libs/server-sdk-dynamodb-source/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ This component will allow the Server-Side SDK to retrieve feature flag configura
from LaunchDarkly.

> [!NOTE]
> This library currently contains only scaffolding. The functional `DynamoDBDataSource` and Big Segments store
> implementation will land in subsequent releases.
> The Big Segments store implementation will land in a subsequent release.
LaunchDarkly overview
-------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,81 @@
/** @file dynamodb_source.hpp
* @brief Server-Side DynamoDB Source
*/

#pragma once

#include <launchdarkly/server_side/integrations/data_reader/iserialized_data_reader.hpp>
#include <launchdarkly/server_side/integrations/dynamodb/options.hpp>

#include <tl/expected.hpp>

#include <memory>
#include <string>

namespace Aws::DynamoDB {
class DynamoDBClient;
}

namespace launchdarkly::server_side::integrations {

// Scaffold-only entry point. The real DynamoDBDataSource class will replace
// this in a subsequent PR; this declaration exists so the smoke .cpp has
// something to define and the library produces a non-empty archive.
void DynamoDBSourceLinkSmoke();
/**
* @brief DynamoDBDataSource represents a data source for the Server-Side SDK
* backed by Amazon DynamoDB. It is meant to be used in place of the standard
* LaunchDarkly Streaming or Polling data sources.
*
* Call DynamoDBDataSource::Create to obtain a new instance. This instance can
* be passed into the SDK's DataSystem configuration via the LazyLoad builder.
*
* The DynamoDB table must already exist and follow the LaunchDarkly schema:
* a String partition key named `namespace` and a String sort key named `key`.
* The LaunchDarkly Relay Proxy populates the table with this schema; this
* class only reads from it.
*
* This implementation is backed by the AWS SDK for C++.
*/
class DynamoDBDataSource final : public ISerializedDataReader {
public:
/**
* @brief Creates a new DynamoDBDataSource, or returns an error if
* construction failed.
*
* @param table_name Name of the DynamoDB table to read from. The table
* must already exist; this class does not create it.
*
* @param prefix Optional namespace prefix. When non-empty, the source
* reads rows whose partition key is `<prefix>:features`,
* `<prefix>:segments`, etc. This allows multiple LaunchDarkly
* environments to share a single table.
*
* @param options Optional AWS DynamoDB client configuration. See
* @ref DynamoDBClientOptions. When defaulted, the AWS SDK resolves
* region, endpoint, and credentials from the standard provider chain
* (environment variables, shared config files, instance metadata).
*
* @return A DynamoDBDataSource, or an error if construction failed.
*/
static tl::expected<std::unique_ptr<DynamoDBDataSource>, std::string>
Create(std::string table_name,
std::string prefix,
DynamoDBClientOptions options = {});

[[nodiscard]] GetResult Get(ISerializedItemKind const& kind,
std::string const& itemKey) const override;
[[nodiscard]] AllResult All(ISerializedItemKind const& kind) const override;
[[nodiscard]] std::string const& Identity() const override;
[[nodiscard]] bool Initialized() const override;

~DynamoDBDataSource() override;

private:
DynamoDBDataSource(std::unique_ptr<Aws::DynamoDB::DynamoDBClient> client,
std::string table_name,
std::string prefix);

std::unique_ptr<Aws::DynamoDB::DynamoDBClient> client_;
std::string const table_name_;
std::string const prefix_;
std::string const inited_namespace_;
};

} // namespace launchdarkly::server_side::integrations
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/** @file options.hpp
* @brief Options for constructing a DynamoDB-backed integration.
*/

#pragma once

#include <optional>
#include <string>

namespace launchdarkly::server_side::integrations {

/**
* @brief Optional knobs for constructing the AWS DynamoDB client used by
* @ref DynamoDBDataSource (and other DynamoDB-backed integrations).
*
* When unset, fields fall through to the AWS SDK's defaults:
*
* - @ref region resolves via the SDK region provider chain (environment,
* shared config file, instance metadata).
* - @ref endpoint defaults to the standard AWS DynamoDB endpoint for the
* resolved region. Set it to point at DynamoDB Local or LocalStack, e.g.
* `http://localhost:8000`.
* - If none of @ref aws_access_key_id / @ref aws_secret_access_key /
* @ref aws_session_token are set, the SDK's default credential provider
* chain is used (environment variables, shared credentials file, EC2/ECS
* roles).
*/
struct DynamoDBClientOptions {
std::optional<std::string> region;
std::optional<std::string> endpoint;
std::optional<std::string> aws_access_key_id;
std::optional<std::string> aws_secret_access_key;
std::optional<std::string> aws_session_token;
};

} // namespace launchdarkly::server_side::integrations
2 changes: 2 additions & 0 deletions libs/server-sdk-dynamodb-source/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ target_sources(${LIBNAME}
PRIVATE
${HEADER_LIST}
dynamodb_source.cpp
aws_sdk_guard.cpp
client_factory.cpp
)


Expand Down
18 changes: 18 additions & 0 deletions libs/server-sdk-dynamodb-source/src/aws_sdk_guard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "aws_sdk_guard.hpp"

namespace launchdarkly::server_side::integrations::detail {

void AwsSdkGuard::Ensure() {
static AwsSdkGuard instance;
(void)instance;
}

AwsSdkGuard::AwsSdkGuard() {
Aws::InitAPI(options_);
}

AwsSdkGuard::~AwsSdkGuard() {
Aws::ShutdownAPI(options_);
}

} // namespace launchdarkly::server_side::integrations::detail
38 changes: 38 additions & 0 deletions libs/server-sdk-dynamodb-source/src/aws_sdk_guard.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include <aws/core/Aws.h>

namespace launchdarkly::server_side::integrations::detail {

// AwsSdkGuard owns the process-wide Aws::InitAPI / Aws::ShutdownAPI lifecycle
// for this library. Multiple DynamoDB-backed integrations within the same
// process share the single static instance; the API is initialized lazily on
// first use and torn down during normal program termination via C++ static
// destruction.
//
// Static-destruction ordering caveat: if a caller stashes a raw AWS SDK
// pointer in their own static and that static is destroyed AFTER this guard,
// AWS SDK calls during that destructor will be undefined. The standard usage
// pattern (holding the data source / store via a unique_ptr or shared_ptr in
// regular program scope, not in another static) is unaffected because those
// smart pointers destruct before the guard.
class AwsSdkGuard {
public:
// Idempotent. First call constructs the singleton, which runs
// Aws::InitAPI in its constructor. Subsequent calls are no-ops. Safe to
// call from any thread.
static void Ensure();

AwsSdkGuard(AwsSdkGuard const&) = delete;
AwsSdkGuard(AwsSdkGuard&&) = delete;
AwsSdkGuard& operator=(AwsSdkGuard const&) = delete;
AwsSdkGuard& operator=(AwsSdkGuard&&) = delete;

private:
AwsSdkGuard();
~AwsSdkGuard();

Aws::SDKOptions options_;
};

} // namespace launchdarkly::server_side::integrations::detail
83 changes: 83 additions & 0 deletions libs/server-sdk-dynamodb-source/src/client_factory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#include "client_factory.hpp"

#include <aws/core/auth/AWSCredentials.h>
#include <aws/core/client/ClientConfiguration.h>
#include <aws/core/http/Scheme.h>

namespace launchdarkly::server_side::integrations::detail {

namespace {

// Verifies that the credential fields in `options` form a valid combination.
// Returns an empty optional on success, or an error string describing what's
// wrong. The valid combinations are:
//
// - none of the three set (fall through to AWS default credential chain)
// - access_key_id + secret_access_key (long-lived IAM keys)
// - access_key_id + secret_access_key + session_token (STS temporary creds)
//
// All other partial combinations would build a misconfigured AWS client that
// fails opaquely at request time; catching them here surfaces the
// misconfiguration up front.
std::optional<std::string> ValidateCredentials(
DynamoDBClientOptions const& options) {
bool const has_key = options.aws_access_key_id.has_value();
bool const has_secret = options.aws_secret_access_key.has_value();
bool const has_token = options.aws_session_token.has_value();

if (has_key != has_secret) {
return "aws_access_key_id and aws_secret_access_key must both be set "
"or both unset";
}
if (has_token && !has_key) {
return "aws_session_token requires aws_access_key_id and "
"aws_secret_access_key";
}
return std::nullopt;
}

Aws::Client::ClientConfiguration BuildConfig(
DynamoDBClientOptions const& options) {
Aws::Client::ClientConfiguration config;

if (options.region) {
config.region = *options.region;
}

if (options.endpoint) {
config.endpointOverride = *options.endpoint;
// Use HTTP if the endpoint starts with "http://"; otherwise default
// to HTTPS. Endpoint overrides are commonly DynamoDB Local or
// LocalStack on plain HTTP for development.
std::string const& ep = *options.endpoint;
if (ep.rfind("http://", 0) == 0) {
config.scheme = Aws::Http::Scheme::HTTP;
config.verifySSL = false;
}
}

return config;
}

} // namespace

tl::expected<std::unique_ptr<Aws::DynamoDB::DynamoDBClient>, std::string>
BuildDynamoDBClient(DynamoDBClientOptions const& options) {
if (auto err = ValidateCredentials(options)) {
return tl::make_unexpected(std::move(*err));
}

auto const config = BuildConfig(options);

if (options.aws_access_key_id) {
Aws::Auth::AWSCredentials credentials{
*options.aws_access_key_id, *options.aws_secret_access_key,
options.aws_session_token.value_or("")};
return std::make_unique<Aws::DynamoDB::DynamoDBClient>(credentials,
config);
}

return std::make_unique<Aws::DynamoDB::DynamoDBClient>(config);
}

} // namespace launchdarkly::server_side::integrations::detail
Loading
Loading