Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add golang to relay server (FF-3495) #97

Open
wants to merge 4 commits into
base: lr/ff-3496/ruby-relay-really
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions .github/workflows/test-sdk-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,15 @@ jobs:
sdkName: 'eppo/ruby-sdk'
sdkRelayDir: 'ruby-sdk-relay'
secrets: inherit

test-go-sdk:
strategy:
fail-fast: false
matrix:
platform: ['linux']
uses: ./.github/workflows/test-server-sdk.yml
with:
platform: ${{ matrix.platform }}
sdkName: 'eppo/go-sdk'
sdkRelayDir: 'go-sdk-relay'
secrets: inherit
2 changes: 2 additions & 0 deletions package-testing/go-sdk-relay/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
bin/
18 changes: 18 additions & 0 deletions package-testing/go-sdk-relay/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM golang:1.23

WORKDIR /app

# Copy go mod and sum files
COPY . .

# Download all dependencies
RUN go mod download

# Copy the source code
COPY . .

# Build the application
RUN go build -o main .

# Run the application
CMD ["./main"]
29 changes: 29 additions & 0 deletions package-testing/go-sdk-relay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Go 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/go-sdk-relay .
```

Run the docker container:

```shell
./docker-run.sh
```

## Development

1. Install dependencies:
```shell
go mod download
```

2. Run the server:
```shell
go run main.go
```
29 changes: 29 additions & 0 deletions package-testing/go-sdk-relay/docker-run.sh
Original file line number Diff line number Diff line change
@@ -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 go-relay
echo "Removing existing container..."
docker remove go-relay

echo "Building new image..."
docker build . -t Eppo-exp/go-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 go-relay \
--rm \
-t Eppo-exp/go-sdk-relay:$VERSION
12 changes: 12 additions & 0 deletions package-testing/go-sdk-relay/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/eppo/go-sdk-relay

go 1.23.1

require github.com/Eppo-exp/golang-sdk/v6 v6.1.0

require (
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
)
10 changes: 10 additions & 0 deletions package-testing/go-sdk-relay/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/Eppo-exp/golang-sdk/v6 v6.1.0 h1:a9nEXYnc/r4cNRMeV8CHc1c/VXbcXS5KiFPDpsTDTH8=
github.com/Eppo-exp/golang-sdk/v6 v6.1.0/go.mod h1:UZ385Go97q/BmPG4wsnaSGbdQE4LAiIo25oQVbJrm20=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
234 changes: 234 additions & 0 deletions package-testing/go-sdk-relay/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"

"github.com/Eppo-exp/golang-sdk/v6/eppoclient"
)

type AssignmentRequest struct {
Flag string `json:"flag"`
SubjectKey string `json:"subjectKey"`
SubjectAttributes map[string]interface{} `json:"subjectAttributes"`
AssignmentType string `json:"assignmentType"`
DefaultValue interface{} `json:"defaultValue"`
}

type AssignmentResponse struct {
Result interface{} `json:"result"`
AssignmentLog []string `json:"assignmentLog"`
BanditLog []string `json:"banditLog"`
Error *string `json:"error"`
}

type SDKDetails struct {
SDKName string `json:"sdkName"`
SDKVersion string `json:"sdkVersion"`
SupportsBandits bool `json:"supportsBandits"`
SupportsDynamicTyping bool `json:"supportsDynamicTyping"`
}

var eppoClient *eppoclient.EppoClient

func initializeClient() error {
apiKey := os.Getenv("EPPO_API_KEY")
if apiKey == "" {
apiKey = "NOKEYSPECIFIED"
}

baseURL := os.Getenv("EPPO_BASE_URL")
if baseURL == "" {
baseURL = "http://localhost:5000/api"
}

// Create configuration
config := eppoclient.Config{
SdkKey: apiKey,
BaseUrl: baseURL,
}

// Initialize client
client, err := eppoclient.InitClient(config)
if err != nil {
return fmt.Errorf("failed to create client: %v", err)
}

// Wait for initialization with timeout
initChan := make(chan struct{})
go func() {
<-client.Initialized()
close(initChan)
}()

select {
case <-initChan:
log.Printf("Eppo client initialized successfully")
case <-time.After(5 * time.Second):
log.Printf("Warning: Timed out waiting for Eppo SDK to initialize")
}

eppoClient = client
return nil
}

func handleHealthCheck(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "OK")
}

func handleSDKDetails(w http.ResponseWriter, r *http.Request) {
details := SDKDetails{
SDKName: "go-sdk",
SDKVersion: "6.1.0",
SupportsBandits: false,
SupportsDynamicTyping: false,
}
json.NewEncoder(w).Encode(details)
}

func handleReset(w http.ResponseWriter, r *http.Request) {
err := initializeClient()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, "Reset complete")
}

func handleAssignment(w http.ResponseWriter, r *http.Request) {
var req AssignmentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// Always prepare a response with default values
response := AssignmentResponse{
Result: nil,
AssignmentLog: []string{},
BanditLog: []string{},
}

if eppoClient == nil {
errStr := "client not initialized"
response.Error = &errStr
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return
}

var err error
defer func() {
if r := recover(); r != nil {
errStr := fmt.Sprintf("panic recovered: %v", r)
response.Error = &errStr
response.Result = req.DefaultValue
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
}()

switch req.AssignmentType {
case "BOOLEAN":
defaultVal := false
if v, ok := req.DefaultValue.(bool); ok {
defaultVal = v
} else if v, ok := req.DefaultValue.(float64); ok {
defaultVal = v != 0
}
result, _ := eppoClient.GetBoolAssignment(req.Flag, req.SubjectKey, req.SubjectAttributes, defaultVal)
response.Result = result

case "STRING":
defaultVal := ""
if v, ok := req.DefaultValue.(string); ok {
defaultVal = v
}
result, _ := eppoClient.GetStringAssignment(req.Flag, req.SubjectKey, req.SubjectAttributes, defaultVal)
response.Result = result

case "NUMERIC":
defaultVal := 0.0
switch v := req.DefaultValue.(type) {
case float64:
defaultVal = v
case int:
defaultVal = float64(v)
case string:
defaultVal, _ = strconv.ParseFloat(v, 64)
}
result, _ := eppoClient.GetNumericAssignment(req.Flag, req.SubjectKey, req.SubjectAttributes, defaultVal)
response.Result = result

case "INTEGER":
defaultVal := int64(0)
switch v := req.DefaultValue.(type) {
case float64:
defaultVal = int64(v)
case int:
defaultVal = int64(v)
case int64:
defaultVal = v
}
result, _ := eppoClient.GetIntegerAssignment(req.Flag, req.SubjectKey, req.SubjectAttributes, defaultVal)
response.Result = float64(result)

case "JSON":
result, _ := eppoClient.GetJSONAssignment(req.Flag, req.SubjectKey, req.SubjectAttributes, req.DefaultValue)
response.Result = result

default:
errStr := fmt.Sprintf("unsupported assignment type: %s", req.AssignmentType)
response.Error = &errStr
}

if err != nil {
errStr := err.Error()
response.Error = &errStr
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}

func handleBanditAction(w http.ResponseWriter, r *http.Request) {
// TODO: Implement bandit logic
response := AssignmentResponse{
Result: "action",
AssignmentLog: []string{},
BanditLog: []string{},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}

func main() {
if err := initializeClient(); err != nil {
log.Fatalf("Failed to initialize client: %v", err)
}

http.HandleFunc("/", handleHealthCheck)
http.HandleFunc("/sdk/details", handleSDKDetails)
http.HandleFunc("/sdk/reset", handleReset)
http.HandleFunc("/flags/v1/assignment", handleAssignment)
http.HandleFunc("/bandits/v1/action", handleBanditAction)

port := os.Getenv("SDK_RELAY_PORT")
if port == "" {
port = "7001"
}

host := os.Getenv("SDK_RELAY_HOST")
if host == "" {
host = "0.0.0.0"
}

addr := fmt.Sprintf("%s:%s", host, port)
log.Printf("Starting server on %s", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
Loading