mirror of
https://github.com/grafana/grafana.git
synced 2026-02-18 18:20:52 -05:00
FS: Add OpenFeature scaffolding and local setup (#117904)
* add openfeature handling + local env to frontend-service * remove react18 manifest that was accidentally added * fix and add some more unit tests * review changes * remove comment from frontend_service * translations...
This commit is contained in:
parent
234760f61e
commit
aaaeb13d32
12 changed files with 221 additions and 157 deletions
|
|
@ -71,6 +71,7 @@ dc_resource("tempo", labels=["observability"])
|
|||
|
||||
dc_resource("postgres", labels=["misc"])
|
||||
dc_resource("tempo-init", labels=["misc"])
|
||||
dc_resource("goff", labels=["misc"])
|
||||
|
||||
# paths in tilt files are confusing....
|
||||
# - if tilt is dealing with the path, it is relative to the Tiltfile
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ server {
|
|||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header baggage "namespace=stacks-11";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,10 +78,23 @@ services:
|
|||
OTEL_SERVICE_NAME: frontend-service
|
||||
GF_TRACING_OPENTELEMETRY_OTLP_ADDRESS: 'alloy:4317'
|
||||
GF_TRACING_OPENTELEMETRY_OTLP_PROPAGATION: jaeger,w3c
|
||||
GF_FEATURE_TOGGLES_OPENFEATURE_PROVIDER: ofrep
|
||||
GF_FEATURE_TOGGLES_OPENFEATURE_URL: http://goff:1031
|
||||
# Faro (Grafana JavaScript Agent) configuration - sends frontend observability data to Loki via Alloy
|
||||
GF_LOG_FRONTEND_ENABLED: true
|
||||
GF_LOG_FRONTEND_CUSTOM_ENDPOINT: http://localhost:12347/collect
|
||||
|
||||
goff:
|
||||
image: gofeatureflag/go-feature-flag:latest
|
||||
command: /go-feature-flag --config /config/config.yaml
|
||||
ports:
|
||||
- 1031:1031/tcp
|
||||
configs:
|
||||
- source: goff_config
|
||||
target: /config/config.yaml
|
||||
- source: goff_flags
|
||||
target: /flags/flags.yaml
|
||||
|
||||
postgres:
|
||||
image: postgres:16.1-alpine3.19@sha256:17eb369d9330fe7fbdb2f705418c18823d66322584c77c2b43cc0e1851d01de7
|
||||
environment:
|
||||
|
|
@ -155,6 +168,20 @@ services:
|
|||
tempo-init:
|
||||
condition: service_completed_successfully
|
||||
|
||||
configs:
|
||||
goff_config:
|
||||
content: |
|
||||
logLevel: debug
|
||||
logFormat: logfmt
|
||||
listen: 1031
|
||||
evaluationContextEnrichment:
|
||||
cluster: localdev
|
||||
retrievers:
|
||||
- kind: "file"
|
||||
path: "/flags/flags.yaml"
|
||||
goff_flags:
|
||||
file: ./goff-flags.yaml
|
||||
|
||||
volumes:
|
||||
backend-data:
|
||||
postgres-data:
|
||||
|
|
|
|||
6
devenv/frontend-service/goff-flags.yaml
Normal file
6
devenv/frontend-service/goff-flags.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
flagName:
|
||||
variations:
|
||||
enabled: true
|
||||
disabled: false
|
||||
defaultRule:
|
||||
variation: disabled
|
||||
|
|
@ -6,7 +6,10 @@ import (
|
|||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
|
||||
"go.opentelemetry.io/otel/baggage"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
|
|
@ -37,8 +40,9 @@ func setRequestContext(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||
ctx, span := tracing.Start(ctx, "setRequestContext")
|
||||
defer span.End()
|
||||
|
||||
webCtx := web.FromContext(ctx)
|
||||
reqContext := &contextmodel.ReqContext{
|
||||
Context: web.FromContext(ctx),
|
||||
Context: webCtx,
|
||||
Logger: log.New("context"),
|
||||
SignedInUser: &user.SignedInUser{},
|
||||
}
|
||||
|
|
@ -48,7 +52,9 @@ func setRequestContext(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||
|
||||
// Set the context for the http.Request.Context
|
||||
// This modifies both r and reqContext.Req since they point to the same value
|
||||
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||
if webCtx != nil {
|
||||
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||
}
|
||||
|
||||
// add traceID to logger context
|
||||
traceID := tracing.TraceIDFromContext(ctx, false)
|
||||
|
|
@ -64,5 +70,27 @@ func setRequestContext(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||
reqContext.Logger = reqContext.Logger.New("hostname", hostname)
|
||||
}
|
||||
|
||||
// Parse namespace from W3C baggage header
|
||||
var namespace string
|
||||
if baggageHeader := r.Header.Get("baggage"); baggageHeader != "" {
|
||||
if bag, err := baggage.Parse(baggageHeader); err == nil {
|
||||
if member := bag.Member("namespace"); member.Value() != "" {
|
||||
namespace = member.Value()
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx = request.WithNamespace(ctx, namespace)
|
||||
|
||||
// Note: OpenFeature is already initialized by target.go before this service starts.
|
||||
// The frontend service only needs to set evaluation context per request
|
||||
openFeatureNamespace := "default"
|
||||
if namespace != "" {
|
||||
openFeatureNamespace = namespace
|
||||
}
|
||||
evalCtx := openfeature.NewEvaluationContext(openFeatureNamespace, map[string]any{
|
||||
"namespace": openFeatureNamespace,
|
||||
})
|
||||
ctx = openfeature.MergeTransactionContext(ctx, evalCtx)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
|
|
|||
96
pkg/services/frontend/context_middleware_test.go
Normal file
96
pkg/services/frontend/context_middleware_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package frontend
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
func TestContextMiddleware(t *testing.T) {
|
||||
t.Run("calls next handler with context", func(t *testing.T) {
|
||||
service := &frontendService{}
|
||||
nextCalled := false
|
||||
var capturedRequest *http.Request
|
||||
|
||||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextCalled = true
|
||||
capturedRequest = r
|
||||
})
|
||||
|
||||
middleware := service.contextMiddleware()
|
||||
handler := middleware(nextHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
assert.True(t, nextCalled, "next handler should be called")
|
||||
assert.NotNil(t, capturedRequest, "request should be passed to next handler")
|
||||
assert.NotNil(t, capturedRequest.Context(), "request context should be set")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetRequestContext(t *testing.T) {
|
||||
t.Run("parses namespace from baggage header", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("baggage", "namespace=stacks-123")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx := setRequestContext(req.Context(), rec, req)
|
||||
|
||||
namespace, ok := request.NamespaceFrom(ctx)
|
||||
assert.True(t, ok, "Namespace should be present in context")
|
||||
assert.Equal(t, "stacks-123", namespace, "Namespace should match baggage value")
|
||||
})
|
||||
|
||||
t.Run("handles baggage header with multiple members", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("baggage", "trace-id=abc123,namespace=tenant-456,user-id=xyz")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx := setRequestContext(req.Context(), rec, req)
|
||||
|
||||
namespace, ok := request.NamespaceFrom(ctx)
|
||||
assert.True(t, ok, "Namespace should be present in context")
|
||||
assert.Equal(t, "tenant-456", namespace, "Namespace should match baggage value")
|
||||
})
|
||||
|
||||
t.Run("defaults to empty namespace when baggage header is missing", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx := setRequestContext(req.Context(), rec, req)
|
||||
|
||||
namespace, ok := request.NamespaceFrom(ctx)
|
||||
assert.True(t, ok, "Namespace should be present in context")
|
||||
assert.Empty(t, namespace, "Namespace should be empty when not provided")
|
||||
})
|
||||
|
||||
t.Run("handles invalid baggage header gracefully", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("baggage", "invalid-baggage-format;;;")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx := setRequestContext(req.Context(), rec, req)
|
||||
|
||||
namespace, ok := request.NamespaceFrom(ctx)
|
||||
assert.True(t, ok, "Namespace should be present in context")
|
||||
assert.Empty(t, namespace, "Namespace should be empty when baggage is invalid")
|
||||
})
|
||||
|
||||
t.Run("handles baggage header without namespace member", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("baggage", "other-key=other-value")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx := setRequestContext(req.Context(), rec, req)
|
||||
|
||||
namespace, ok := request.NamespaceFrom(ctx)
|
||||
assert.True(t, ok, "Namespace should be present in context")
|
||||
assert.Empty(t, namespace, "Namespace should be empty when namespace member not in baggage")
|
||||
})
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/grafana/grafana/pkg/middleware/loggermw"
|
||||
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
fswebassets "github.com/grafana/grafana/pkg/services/frontend/webassets"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
||||
|
|
@ -56,12 +55,8 @@ type frontendService struct {
|
|||
|
||||
func ProvideFrontendService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, promGatherer prometheus.Gatherer, promRegister prometheus.Registerer, license licensing.Licensing, hooksService *hooks.HooksService) (*frontendService, error) {
|
||||
logger := log.New("frontend-service")
|
||||
assetsManifest, err := fswebassets.GetWebAssets(cfg, license)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index, err := NewIndexProvider(cfg, assetsManifest, license, hooksService)
|
||||
index, err := NewIndexProvider(cfg, license, hooksService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
fswebassets "github.com/grafana/grafana/pkg/services/frontend/webassets"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
|
@ -22,7 +23,7 @@ type IndexProvider struct {
|
|||
index *template.Template
|
||||
hooksService *hooks.HooksService
|
||||
config *setting.Cfg
|
||||
assets dtos.EntryPointAssets // Includes CDN info
|
||||
license licensing.Licensing
|
||||
}
|
||||
|
||||
type IndexViewData struct {
|
||||
|
|
@ -53,7 +54,7 @@ var (
|
|||
htmlTemplates = template.Must(template.New("html").Delims("[[", "]]").ParseFS(templatesFS, `*.html`))
|
||||
)
|
||||
|
||||
func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, license licensing.Licensing, hooksService *hooks.HooksService) (*IndexProvider, error) {
|
||||
func NewIndexProvider(cfg *setting.Cfg, license licensing.Licensing, hooksService *hooks.HooksService) (*IndexProvider, error) {
|
||||
t := htmlTemplates.Lookup("index.html")
|
||||
if t == nil {
|
||||
return nil, fmt.Errorf("missing index template")
|
||||
|
|
@ -69,7 +70,7 @@ func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, li
|
|||
index: t,
|
||||
hooksService: hooksService,
|
||||
config: cfg,
|
||||
assets: assetsManifest,
|
||||
license: license,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +90,13 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.
|
|||
return
|
||||
}
|
||||
|
||||
assetsManifest, err := fswebassets.GetWebAssets(ctx, p.config, p.license)
|
||||
if err != nil {
|
||||
p.log.Error("unable to get web assets", "err", err)
|
||||
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
reqCtx := contexthandler.FromContext(ctx)
|
||||
|
||||
// make a copy of the settings
|
||||
|
|
@ -98,7 +106,7 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.
|
|||
AppTitle: "Grafana",
|
||||
AppSubUrl: p.config.AppSubURL,
|
||||
IsDevelopmentEnv: p.config.Env == setting.Dev,
|
||||
Assets: p.assets,
|
||||
Assets: assetsManifest,
|
||||
DefaultUser: dtos.CurrentUser{},
|
||||
Nonce: reqCtx.RequestNonce,
|
||||
PublicDashboardAccessToken: reqCtx.PublicDashboardAccessToken,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package frontend
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel/baggage"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
|
||||
|
|
@ -29,17 +28,6 @@ func RequestConfigMiddleware(cfg *setting.Cfg, license licensing.Licensing, sett
|
|||
ctx, span := tracing.Start(r.Context(), "frontend.RequestConfigMiddleware")
|
||||
defer span.End()
|
||||
|
||||
// Parse namespace from W3C baggage header
|
||||
var namespace string
|
||||
if baggageHeader := r.Header.Get("baggage"); baggageHeader != "" {
|
||||
if bag, err := baggage.Parse(baggageHeader); err == nil {
|
||||
if member := bag.Member("namespace"); member.Value() != "" {
|
||||
namespace = member.Value()
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx = request.WithNamespace(ctx, namespace)
|
||||
|
||||
reqCtx := contexthandler.FromContext(ctx)
|
||||
logger := reqCtx.Logger
|
||||
|
||||
|
|
@ -48,28 +36,30 @@ func RequestConfigMiddleware(cfg *setting.Cfg, license licensing.Licensing, sett
|
|||
requestConfig := NewFSRequestConfig(cfg, license)
|
||||
|
||||
// Fetch tenant-specific configuration if namespace is present
|
||||
if namespace != "" && settingsService != nil {
|
||||
// Fetch tenant overrides for relevant sections only
|
||||
selector := metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||
Key: "section",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"security"}, // TODO: get correct list
|
||||
}, {
|
||||
// don't return values from defaults.ini as they conflict with the services's own defaults
|
||||
Key: "source",
|
||||
Operator: metav1.LabelSelectorOpNotIn,
|
||||
Values: []string{"defaults"},
|
||||
}},
|
||||
}
|
||||
if namespace, ok := request.NamespaceFrom(ctx); ok {
|
||||
if settingsService != nil {
|
||||
// Fetch tenant overrides for relevant sections only
|
||||
selector := metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||
Key: "section",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"security"}, // TODO: get correct list
|
||||
}, {
|
||||
// don't return values from defaults.ini as they conflict with the services's own defaults
|
||||
Key: "source",
|
||||
Operator: metav1.LabelSelectorOpNotIn,
|
||||
Values: []string{"defaults"},
|
||||
}},
|
||||
}
|
||||
|
||||
settings, err := settingsService.ListAsIni(ctx, selector)
|
||||
if err != nil {
|
||||
logger.Error("failed to fetch tenant settings", "namespace", namespace, "err", err)
|
||||
// Fall back to base config
|
||||
} else {
|
||||
// Merge tenant overrides with base config
|
||||
requestConfig.ApplyOverrides(settings, logger)
|
||||
settings, err := settingsService.ListAsIni(ctx, selector)
|
||||
if err != nil {
|
||||
logger.Error("failed to fetch tenant settings", "namespace", namespace, "err", err)
|
||||
// Fall back to base config
|
||||
} else {
|
||||
// Merge tenant overrides with base config
|
||||
requestConfig.ApplyOverrides(settings, logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"gopkg.in/ini.v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
|
|
@ -22,13 +23,16 @@ import (
|
|||
)
|
||||
|
||||
// setupTestContext creates a request with a proper context that includes a logger
|
||||
func setupTestContext(r *http.Request) *http.Request {
|
||||
func setupTestContext(r *http.Request, namespace string) *http.Request {
|
||||
logger := log.NewNopLogger()
|
||||
reqCtx := &contextmodel.ReqContext{
|
||||
Context: &web.Context{Req: r},
|
||||
Logger: logger,
|
||||
}
|
||||
ctx := ctxkey.Set(r.Context(), reqCtx)
|
||||
if namespace != "" {
|
||||
ctx = request.WithNamespace(ctx, namespace)
|
||||
}
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +60,7 @@ func TestRequestConfigMiddleware(t *testing.T) {
|
|||
handler := middleware(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req = setupTestContext(req)
|
||||
req = setupTestContext(req, "stacks-123")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
|
@ -88,7 +92,7 @@ func TestRequestConfigMiddleware(t *testing.T) {
|
|||
handler := middleware(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req = setupTestContext(req)
|
||||
req = setupTestContext(req, "stacks-123")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
|
@ -128,8 +132,7 @@ func TestRequestConfigMiddleware(t *testing.T) {
|
|||
handler := middleware(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("baggage", "namespace=stacks-123")
|
||||
req = setupTestContext(req)
|
||||
req = setupTestContext(req, "stacks-123")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
|
@ -175,8 +178,7 @@ func TestRequestConfigMiddleware(t *testing.T) {
|
|||
handler := middleware(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("baggage", "namespace=stacks-123")
|
||||
req = setupTestContext(req)
|
||||
req = setupTestContext(req, "stacks-123")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
|
@ -192,7 +194,7 @@ func TestRequestConfigMiddleware(t *testing.T) {
|
|||
assert.True(t, mockSettingsService.called)
|
||||
})
|
||||
|
||||
t.Run("should not call settings service without namespace header", func(t *testing.T) {
|
||||
t.Run("should not call settings service when no namespace is present", func(t *testing.T) {
|
||||
mockSettingsService := &mockSettingsService{}
|
||||
|
||||
license := &licensing.OSSLicensingService{}
|
||||
|
|
@ -211,7 +213,7 @@ func TestRequestConfigMiddleware(t *testing.T) {
|
|||
handler := middleware(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req = setupTestContext(req)
|
||||
req = setupTestContext(req, "")
|
||||
// No baggage header
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
|
|
@ -220,103 +222,6 @@ func TestRequestConfigMiddleware(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.False(t, mockSettingsService.called)
|
||||
})
|
||||
|
||||
t.Run("should parse namespace from baggage header with multiple values", func(t *testing.T) {
|
||||
mockSettingsService := &mockSettingsService{
|
||||
settings: []*settingservice.Setting{
|
||||
{Section: "security", Key: "content_security_policy", Value: "true"},
|
||||
},
|
||||
}
|
||||
|
||||
license := &licensing.OSSLicensingService{}
|
||||
cfg := &setting.Cfg{
|
||||
Raw: ini.Empty(),
|
||||
HTTPPort: "1234",
|
||||
CSPEnabled: false, // Base config has CSP disabled
|
||||
}
|
||||
|
||||
middleware := RequestConfigMiddleware(cfg, license, mockSettingsService)
|
||||
|
||||
var capturedConfig FSRequestConfig
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
capturedConfig, err = FSRequestConfigFromContext(r.Context())
|
||||
require.NoError(t, err)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
handler := middleware(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
// Baggage header with multiple key-value pairs
|
||||
req.Header.Set("baggage", "trace-id=abc123,namespace=tenant-456,user-id=xyz")
|
||||
req = setupTestContext(req)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.True(t, capturedConfig.CSPEnabled, "Should apply tenant overrides when namespace is present")
|
||||
assert.True(t, mockSettingsService.called, "Should call settings service when namespace is in baggage")
|
||||
})
|
||||
|
||||
t.Run("should not call settings service with malformed baggage header", func(t *testing.T) {
|
||||
mockSettingsService := &mockSettingsService{}
|
||||
|
||||
license := &licensing.OSSLicensingService{}
|
||||
cfg := &setting.Cfg{
|
||||
Raw: ini.Empty(),
|
||||
HTTPPort: "1234",
|
||||
}
|
||||
|
||||
middleware := RequestConfigMiddleware(cfg, license, mockSettingsService)
|
||||
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
handler := middleware(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
// Malformed baggage header
|
||||
req.Header.Set("baggage", "invalid baggage format;;;")
|
||||
req = setupTestContext(req)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.False(t, mockSettingsService.called, "Should not call settings service with malformed baggage")
|
||||
})
|
||||
|
||||
t.Run("should not call settings service when baggage has no namespace", func(t *testing.T) {
|
||||
mockSettingsService := &mockSettingsService{}
|
||||
|
||||
license := &licensing.OSSLicensingService{}
|
||||
cfg := &setting.Cfg{
|
||||
Raw: ini.Empty(),
|
||||
HTTPPort: "1234",
|
||||
}
|
||||
|
||||
middleware := RequestConfigMiddleware(cfg, license, mockSettingsService)
|
||||
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
handler := middleware(testHandler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
// Baggage header without namespace key
|
||||
req.Header.Set("baggage", "trace-id=abc123,user-id=xyz")
|
||||
req = setupTestContext(req)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.False(t, mockSettingsService.called, "Should not call settings service when namespace is not in baggage")
|
||||
})
|
||||
}
|
||||
|
||||
// mockSettingsService is a simple mock for testing
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fswebassets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
|
|
@ -34,8 +35,9 @@ func getCDNRoot(cfg *setting.Cfg, license licensing.Licensing) string {
|
|||
}
|
||||
|
||||
// New codepath for retrieving web assets URLs for the frontend-service
|
||||
func GetWebAssets(cfg *setting.Cfg, license licensing.Licensing) (dtos.EntryPointAssets, error) {
|
||||
assetsManifest, err := webassets.ReadWebAssetsFromFile(filepath.Join(cfg.StaticRootPath, "build", "assets-manifest.json"))
|
||||
func GetWebAssets(ctx context.Context, cfg *setting.Cfg, license licensing.Licensing) (dtos.EntryPointAssets, error) {
|
||||
assetsFilename := "assets-manifest.json"
|
||||
assetsManifest, err := webassets.ReadWebAssetsFromFile(filepath.Join(cfg.StaticRootPath, "build", assetsFilename))
|
||||
if err != nil {
|
||||
return dtos.EntryPointAssets{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fswebassets_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
|
|
@ -16,8 +17,9 @@ func TestGetWebAssets_WithoutCDNConfigured(t *testing.T) {
|
|||
}
|
||||
license := licensingtest.NewFakeLicensing()
|
||||
license.On("ContentDeliveryPrefix").Return("grafana")
|
||||
ctx := context.Background()
|
||||
|
||||
assets, err := fswebassets.GetWebAssets(cfg, license)
|
||||
assets, err := fswebassets.GetWebAssets(ctx, cfg, license)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, assets)
|
||||
|
||||
|
|
@ -33,8 +35,9 @@ func TestGetWebAssets_PrefixFromLicense(t *testing.T) {
|
|||
}
|
||||
license := licensingtest.NewFakeLicensing()
|
||||
license.On("ContentDeliveryPrefix").Return("grafana-pro-max")
|
||||
ctx := context.Background()
|
||||
|
||||
assets, err := fswebassets.GetWebAssets(cfg, license)
|
||||
assets, err := fswebassets.GetWebAssets(ctx, cfg, license)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, assets)
|
||||
|
||||
|
|
@ -49,8 +52,9 @@ func TestGetWebAssets_PrefixFromConfig(t *testing.T) {
|
|||
}
|
||||
license := licensingtest.NewFakeLicensing()
|
||||
license.On("ContentDeliveryPrefix").Return("should-not-be-used")
|
||||
ctx := context.Background()
|
||||
|
||||
assets, err := fswebassets.GetWebAssets(cfg, license)
|
||||
assets, err := fswebassets.GetWebAssets(ctx, cfg, license)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, assets)
|
||||
|
||||
|
|
@ -66,8 +70,9 @@ func TestGetWebAssets_PrefixFromConfigTrailingSlash(t *testing.T) {
|
|||
}
|
||||
license := licensingtest.NewFakeLicensing()
|
||||
license.On("ContentDeliveryPrefix").Return("should-not-be-used")
|
||||
ctx := context.Background()
|
||||
|
||||
assets, err := fswebassets.GetWebAssets(cfg, license)
|
||||
assets, err := fswebassets.GetWebAssets(ctx, cfg, license)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, assets)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue