diff --git a/.github/workflows/test-sdk-packages.yml b/.github/workflows/test-sdk-packages.yml index 4086ee1..fd96c96 100644 --- a/.github/workflows/test-sdk-packages.yml +++ b/.github/workflows/test-sdk-packages.yml @@ -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 diff --git a/package-testing/go-sdk-relay/.gitignore b/package-testing/go-sdk-relay/.gitignore new file mode 100644 index 0000000..51a41ee --- /dev/null +++ b/package-testing/go-sdk-relay/.gitignore @@ -0,0 +1,2 @@ +.env +bin/ diff --git a/package-testing/go-sdk-relay/Dockerfile b/package-testing/go-sdk-relay/Dockerfile new file mode 100644 index 0000000..42bf96b --- /dev/null +++ b/package-testing/go-sdk-relay/Dockerfile @@ -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"] diff --git a/package-testing/go-sdk-relay/README.md b/package-testing/go-sdk-relay/README.md new file mode 100644 index 0000000..f157196 --- /dev/null +++ b/package-testing/go-sdk-relay/README.md @@ -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 +``` diff --git a/package-testing/go-sdk-relay/docker-run.sh b/package-testing/go-sdk-relay/docker-run.sh new file mode 100755 index 0000000..163b7a1 --- /dev/null +++ b/package-testing/go-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 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 diff --git a/package-testing/go-sdk-relay/go.mod b/package-testing/go-sdk-relay/go.mod new file mode 100644 index 0000000..324fe2f --- /dev/null +++ b/package-testing/go-sdk-relay/go.mod @@ -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 +) diff --git a/package-testing/go-sdk-relay/go.sum b/package-testing/go-sdk-relay/go.sum new file mode 100644 index 0000000..954c4ec --- /dev/null +++ b/package-testing/go-sdk-relay/go.sum @@ -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= diff --git a/package-testing/go-sdk-relay/main.go b/package-testing/go-sdk-relay/main.go new file mode 100644 index 0000000..9f116ca --- /dev/null +++ b/package-testing/go-sdk-relay/main.go @@ -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)) +}