Skip to content
Draft
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
5 changes: 4 additions & 1 deletion tests/parametric/test_ffe/test_configuration_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ def _assert_no_mock_requests(mock_ffe_agentless_backend: MockFFEAgentlessBackend


def _remote_config_products(test_agent: TestAgentAPI) -> set[str]:
# Not post_only: golang polls /v0.7/config with GET, every other library with
# POST. The verb says nothing about which products the body advertises, and
# the capability assertion next to this one is already unfiltered.
products: set[str] = set()
for request in test_agent.rc_requests(post_only=True):
for request in test_agent.rc_requests():
client = request["body"].get("client", {})
products.update(client.get("products", []))
return products
Expand Down
155 changes: 146 additions & 9 deletions utils/build/docker/golang/parametric/ffe.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,43 @@ package main
import (
"encoding/json"
"net/http"
"sync"

ddof "github.com/DataDog/dd-trace-go/v2/openfeature"
of "github.com/open-feature/go-sdk/openfeature"
)

func (s *apmClientServer) ffeStart(http.ResponseWriter, *http.Request) {
return
var ffeStartOnce sync.Once

func (s *apmClientServer) ffeStart(writer http.ResponseWriter, request *http.Request) {
var startErr error
ffeStartOnce.Do(func() {
provider, err := ddof.NewDatadogProvider(ddof.ProviderConfig{})
if err != nil {
startErr = err
return
}

// AndWait: plain SetProvider returns before Init, so /ffe/start would
// answer 200 with no configuration and the next evaluation gets the
// default. Other SDKs block on initialize inside set_provider.
if err := of.SetProviderAndWait(provider); err != nil {
startErr = err
return
}

s.ddProvider = provider
s.ofClient = of.NewClient("system-tests-weblog-client")
})

if startErr != nil {
writer.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(writer).Encode(map[string]string{"error": startErr.Error()})
return
}

writer.WriteHeader(http.StatusOK)
_ = json.NewEncoder(writer).Encode(map[string]any{})
}

func (s *apmClientServer) ffeEval(writer http.ResponseWriter, request *http.Request) {
Expand All @@ -24,21 +55,127 @@ func (s *apmClientServer) ffeEval(writer http.ResponseWriter, request *http.Requ
return
}

ctx := of.NewEvaluationContext(body.TargetingKey, body.Attributes)
if s.ofClient == nil {
writer.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(writer).Encode(map[string]string{"error": "FFE provider not initialized"})
return
}

if initer, ok := s.ddProvider.(of.StateHandler); ok {
initer.Init(ctx)
switch body.VariationType {
case "BOOLEAN", "STRING", "INTEGER", "NUMERIC", "JSON":
default:
http.Error(writer, "unknown variation type: "+body.VariationType, http.StatusBadRequest)
return
}

val := s.ofClient.Object(request.Context(), body.Flag, body.DefaultValue, ctx)
ctx := of.NewEvaluationContext(body.TargetingKey, body.Attributes)

writer.WriteHeader(http.StatusOK)
value := body.DefaultValue
reason := string(of.DefaultReason)
var errorCode string

evalCtx := request.Context()

func() {
defer func() {
if r := recover(); r != nil {
value = body.DefaultValue
reason = "ERROR"
}
}()

switch body.VariationType {
case "BOOLEAN":
defaultValue, _ := body.DefaultValue.(bool)
details, err := s.ofClient.BooleanValueDetails(evalCtx, body.Flag, defaultValue, ctx)
if err != nil {
value = body.DefaultValue
reason = "ERROR"
return
}
value = details.Value
reason = string(details.Reason)
errorCode = string(details.ErrorCode)
case "STRING":
defaultValue, _ := body.DefaultValue.(string)
details, err := s.ofClient.StringValueDetails(evalCtx, body.Flag, defaultValue, ctx)
if err != nil {
value = body.DefaultValue
reason = "ERROR"
return
}
value = details.Value
reason = string(details.Reason)
errorCode = string(details.ErrorCode)
case "INTEGER":
defaultValue, _ := toInt64(body.DefaultValue)
details, err := s.ofClient.IntValueDetails(evalCtx, body.Flag, defaultValue, ctx)
if err != nil {
value = body.DefaultValue
reason = "ERROR"
return
}
value = details.Value
reason = string(details.Reason)
errorCode = string(details.ErrorCode)
case "NUMERIC":
defaultValue, _ := toFloat64(body.DefaultValue)
details, err := s.ofClient.FloatValueDetails(evalCtx, body.Flag, defaultValue, ctx)
if err != nil {
value = body.DefaultValue
reason = "ERROR"
return
}
value = details.Value
reason = string(details.Reason)
errorCode = string(details.ErrorCode)
case "JSON":
details, err := s.ofClient.ObjectValueDetails(evalCtx, body.Flag, body.DefaultValue, ctx)
if err != nil {
value = body.DefaultValue
reason = "ERROR"
return
}
value = details.Value
reason = string(details.Reason)
errorCode = string(details.ErrorCode)
}
}()

writer.WriteHeader(http.StatusOK)
response := struct {
Value any `json:"value"`
}{val}
Value any `json:"value"`
Reason string `json:"reason"`
ErrorCode string `json:"errorCode"`
}{value, reason, errorCode}

if err := json.NewEncoder(writer).Encode(response); err != nil {
http.Error(writer, "failed to encode response: "+err.Error(), http.StatusInternalServerError)
}
}

func toInt64(v any) (int64, bool) {
switch n := v.(type) {
case int64:
return n, true
case int:
return int64(n), true
case float64:
return int64(n), true
default:
return 0, false
}
}

func toFloat64(v any) (float64, bool) {
switch n := v.(type) {
case float64:
return n, true
case int:
return float64(n), true
case int64:
return float64(n), true
default:
return 0, false
}
}
43 changes: 36 additions & 7 deletions utils/build/docker/golang/parametric/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,48 @@ func newServer() *apmClientServer {
instruments: make(map[string]interface{}),
}

s.ddProvider, err = ddof.NewDatadogProvider(ddof.ProviderConfig{})
if err != nil {
log.Fatalf("failed to create Datadog OpenFeature provider: %v", err)
}
// The configuration-source contract requires lazy activation: no configuration
// delivery may happen before the provider is accessed through /ffe/start. When any
// Feature Flagging configuration variable is set, skip this eager initialization and
// leave provider setup to /ffe/start. Tests that predate that contract keep the
// original eager behavior.
if !ffeConfigurationEnvVarsSet() {
s.ddProvider, err = ddof.NewDatadogProvider(ddof.ProviderConfig{})
if err != nil {
log.Fatalf("failed to create Datadog OpenFeature provider: %v", err)
}

if err := of.SetProvider(s.ddProvider); err != nil {
log.Fatalf("failed to set Datadog OpenFeature provider and wait for initialization: %v", err)
// Async on purpose, unlike /ffe/start: this runs for every parametric
// test that sets no Feature Flagging variable, and waiting would add the
// 10s DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS to each
// container start.
if err := of.SetProvider(s.ddProvider); err != nil {
log.Fatalf("failed to set Datadog OpenFeature provider: %v", err)
}

s.ofClient = of.NewClient("system-tests-weblog-client")
}

s.ofClient = of.NewClient("system-tests-weblog-client")
return s
}

var ffeConfigurationEnvVars = []string{
"DD_FEATURE_FLAGS_ENABLED",
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE",
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL",
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
}

func ffeConfigurationEnvVarsSet() bool {
for _, name := range ffeConfigurationEnvVars {
if _, ok := os.LookupEnv(name); ok {
return true
}
}
return false
}

func main() {
flag.String("Darg1", "", "Argument 1")
flag.Parse()
Expand Down
Loading