diff --git a/.github/workflows/test-sdk-packages.yml b/.github/workflows/test-sdk-packages.yml index ebf4704c..a9e50ca6 100644 --- a/.github/workflows/test-sdk-packages.yml +++ b/.github/workflows/test-sdk-packages.yml @@ -43,3 +43,15 @@ jobs: sdkName: 'eppo/node-server-sdk' sdkRelayDir: 'node-sdk-relay' secrets: inherit + + test-ruby-sdk: + strategy: + fail-fast: false + matrix: + platform: ['linux'] + uses: ./.github/workflows/test-server-sdk.yml + with: + platform: ${{ matrix.platform }} + sdkName: 'eppo/ruby-sdk' + sdkRelayDir: 'ruby-sdk-relay' + secrets: inherit diff --git a/package-testing/python-sdk-relay/Dockerfile b/package-testing/python-sdk-relay/Dockerfile new file mode 100644 index 00000000..183d3ab7 --- /dev/null +++ b/package-testing/python-sdk-relay/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt + +# Copy the source code +COPY src/ ./src/ + +CMD ["python", "/app/src/server.py"] diff --git a/package-testing/python-sdk-relay/docker-run.sh b/package-testing/python-sdk-relay/docker-run.sh new file mode 100755 index 00000000..5a14d353 --- /dev/null +++ b/package-testing/python-sdk-relay/docker-run.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# Default is to use the latest build +VERSION="${1:-latest}" + +echo "Starting deployment with version: $VERSION" + +if [ -e .env ]; then + echo "Loading environment variables from .env file" + source .env +fi + +echo "Stopping existing container..." +docker stop python-relay +echo "Removing existing container..." +docker remove python-relay + +echo "Building new image..." +docker build . -t Eppo-exp/python-sdk-relay:$VERSION + +echo "Starting new container..." +docker run -p $SDK_RELAY_PORT:$SDK_RELAY_PORT \ + --add-host host.docker.internal:host-gateway \ + -e SDK_REF \ + -e EPPO_BASE_URL \ + -e SDK_RELAY_PORT \ + --name python-relay \ + --rm \ + -t Eppo-exp/python-sdk-relay:$VERSION; diff --git a/package-testing/ruby-sdk-relay/.gitignore b/package-testing/ruby-sdk-relay/.gitignore new file mode 100644 index 00000000..db8aa031 --- /dev/null +++ b/package-testing/ruby-sdk-relay/.gitignore @@ -0,0 +1,3 @@ +.env +vendor/ +.bundle/ \ No newline at end of file diff --git a/package-testing/ruby-sdk-relay/Gemfile b/package-testing/ruby-sdk-relay/Gemfile new file mode 100644 index 00000000..9abb9bfb --- /dev/null +++ b/package-testing/ruby-sdk-relay/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem 'sinatra', '~> 4.1' +gem 'eppo-server-sdk', '~> 3.3' + +gem "rackup", "~> 2.2" +gem "puma", "~> 6.5" diff --git a/package-testing/ruby-sdk-relay/Gemfile.lock b/package-testing/ruby-sdk-relay/Gemfile.lock new file mode 100644 index 00000000..e1c7616e --- /dev/null +++ b/package-testing/ruby-sdk-relay/Gemfile.lock @@ -0,0 +1,51 @@ +GEM + remote: https://rubygems.org/ + specs: + base64 (0.2.0) + eppo-server-sdk (3.3.0-aarch64-linux) + eppo-server-sdk (3.3.0-aarch64-linux-musl) + eppo-server-sdk (3.3.0-arm-linux) + eppo-server-sdk (3.3.0-arm64-darwin) + eppo-server-sdk (3.3.0-x86_64-linux) + eppo-server-sdk (3.3.0-x86_64-linux-musl) + logger (1.6.3) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) + nio4r (2.7.4) + puma (6.5.0) + nio4r (~> 2.0) + rack (3.1.8) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.0.0) + rack (>= 3.0.0) + rackup (2.2.1) + rack (>= 3) + ruby2_keywords (0.0.5) + sinatra (4.1.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.1.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + tilt (2.4.0) + +PLATFORMS + aarch64-linux + aarch64-linux-musl + arm-linux + arm64-darwin + x86_64-linux + x86_64-linux-musl + +DEPENDENCIES + eppo-server-sdk (~> 3.3) + puma (~> 6.5) + rackup (~> 2.2) + sinatra (~> 4.1) + +BUNDLED WITH + 2.5.9 diff --git a/package-testing/ruby-sdk-relay/README.md b/package-testing/ruby-sdk-relay/README.md new file mode 100644 index 00000000..50a04f0b --- /dev/null +++ b/package-testing/ruby-sdk-relay/README.md @@ -0,0 +1,29 @@ +# Ruby Testing Server + +Post test case files to this server and check the results against what's expected. + +## Running locally with Docker + +Build the docker image: + +```shell +docker build -t Eppo-exp/ruby-sdk-relay . +``` + +Run the docker container: + +```shell +./docker-run.sh +``` + +## Development + +1. Install dependencies: +```shell +bundle install +``` + +2. Run the server: +```shell +bundle exec ruby src/server.rb +``` diff --git a/package-testing/ruby-sdk-relay/build-and-run.sh b/package-testing/ruby-sdk-relay/build-and-run.sh new file mode 100755 index 00000000..f7c84f14 --- /dev/null +++ b/package-testing/ruby-sdk-relay/build-and-run.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +# Set default values for vars +: "${SDK_REF:=main}" +: "${SDK_RELAY_HOST:=localhost}" +: "${SDK_RELAY_PORT:=4000}" +SDK="https://github.com/Eppo-exp/eppo-multiplatform.git" + +# Check if Ruby is installed +if ! command -v ruby &> /dev/null; then + echo "Ruby is not installed. Please install Ruby before running this script." + echo "You can install Ruby using your package manager or a version manager like rbenv or rvm." + exit 1 +fi + +# Check if Bundler is installed, install if not +if ! command -v bundle &> /dev/null; then + echo "Bundler is not installed. Installing Bundler..." + gem install bundler || { echo "Failed to install Bundler"; exit 1; } +fi + +# Install Ruby dependencies +echo "Installing Ruby dependencies..." +bundle install || { echo "Failed to install Ruby dependencies"; exit 1; } + +# checkout the specified ref of the SDK repo, build it, and then insert it into vendors here. +rm -rf tmp +mkdir -p tmp + +echo "Cloning ${SDK}@${SDK_REF}" +git clone -b ${SDK_REF} --depth 1 --single-branch ${SDK} tmp || { echo "Cloning repo failed"; exit 1; } + +# Build and install the Ruby gem with Rust components +echo "Building and installing the Ruby SDK with Rust components..." +if [ -d "tmp/ruby-sdk" ]; then + cd tmp/ruby-sdk + + # Install dependencies + bundle install || { echo "Failed to install Ruby SDK dependencies"; exit 1; } + + # Build the gem with Rust components + bundle exec rake build || { echo "Failed to build the Ruby gem"; exit 1; } + + # Install the built gem + GEM_FILE=$(ls pkg/*.gem 2>/dev/null | head -n 1) + if [ -n "$GEM_FILE" ]; then + gem install $GEM_FILE || { echo "Failed to install the gem"; exit 1; } + else + echo "Error: Gem file not found" + exit 1 + fi + cd ../.. +else + echo "Error: Ruby SDK directory not found at expected location (tmp/ruby-sdk)" + exit 1 +fi + +# cleanup +rm -rf tmp + +# start the relay server +echo "Listening on ${SDK_RELAY_HOST}:${SDK_RELAY_PORT}" +SDK_RELAY_HOST=${SDK_RELAY_HOST} SDK_RELAY_PORT=${SDK_RELAY_PORT} bundle exec ruby src/server.rb diff --git a/package-testing/ruby-sdk-relay/src/server.rb b/package-testing/ruby-sdk-relay/src/server.rb new file mode 100644 index 00000000..632e96d1 --- /dev/null +++ b/package-testing/ruby-sdk-relay/src/server.rb @@ -0,0 +1,133 @@ +require 'bundler/setup' +require 'sinatra' +require 'json' +require 'eppo_client' + +class LocalAssignmentLogger < EppoClient::AssignmentLogger + def log_assignment(assignment) + puts "Assignment: #{assignment}" + end +end + +def initialize_client_and_wait + puts "Initializing client" + api_key = ENV['EPPO_API_KEY'] || 'NOKEYSPECIFIED' + base_url = ENV['EPPO_BASE_URL'] || 'http://localhost:5000/api' + + client_config = EppoClient::Config.new( + api_key, + base_url: base_url, + assignment_logger: LocalAssignmentLogger.new + ) + + EppoClient::init(client_config) + client = EppoClient::Client.instance + sleep(3) + puts "Client initialized" +end + +# Health check endpoint +get '/' do + "OK" +end + +# SDK reset endpoint +post '/sdk/reset' do + initialize_client_and_wait + "Reset complete" +end + +# SDK details endpoint +get '/sdk/details' do + content_type :json + { + sdkName: "ruby-sdk", + sdkVersion: "3.3.0", + supportsBandits: false, + supportsDynamicTyping: false + }.to_json +end + +# Assignment endpoint +post '/flags/v1/assignment' do + content_type :json + data = JSON.parse(request.body.read) + + client = EppoClient::Client.instance + + begin + result = case data['assignmentType'] + when 'BOOLEAN' + client.get_boolean_assignment( + data['flag'], + data['subjectKey'], + data['subjectAttributes'], + data['defaultValue'] + ) + when 'INTEGER' + client.get_integer_assignment( + data['flag'], + data['subjectKey'], + data['subjectAttributes'], + data['defaultValue'].to_i + ) + when 'STRING' + client.get_string_assignment( + data['flag'], + data['subjectKey'], + data['subjectAttributes'], + data['defaultValue'] + ) + when 'NUMERIC' + client.get_numeric_assignment( + data['flag'], + data['subjectKey'], + data['subjectAttributes'], + data['defaultValue'].to_f + ) + when 'JSON' + client.get_json_assignment( + data['flag'], + data['subjectKey'], + data['subjectAttributes'], + data['defaultValue'] + ) + end + + { + result: result, + assignmentLog: [], + banditLog: [], + error: nil + }.to_json + rescue => e + puts "Error processing assignment: #{e}" + { + result: nil, + assignmentLog: [], + banditLog: [], + error: e.message + }.to_json + end +end + +# Bandit endpoint +post '/bandits/v1/action' do + content_type :json + data = JSON.parse(request.body.read) + + # TODO: Implement bandit logic + { + result: "action", + assignmentLog: [], + banditLog: [], + error: nil + }.to_json +end + +initialize_client_and_wait + +host = ENV['SDK_RELAY_HOST'] || '0.0.0.0' +port = ENV['SDK_RELAY_PORT'] || 7001 +set :port, port +set :bind, host