543 lines
17 KiB
Go
543 lines
17 KiB
Go
package setting
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apiserver/pkg/endpoints/request"
|
|
"k8s.io/client-go/dynamic/fake"
|
|
k8testing "k8s.io/client-go/testing"
|
|
|
|
authlib "github.com/grafana/authlib/authn"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
)
|
|
|
|
func TestRemoteSettingService_ListAsIni(t *testing.T) {
|
|
t.Run("should filter settings by label selector", func(t *testing.T) {
|
|
// Create multiple settings, only some matching the selector
|
|
setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"})
|
|
setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"})
|
|
setting3 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "host", Value: "localhost"})
|
|
|
|
client := newTestClient(500, setting1, setting2, setting3)
|
|
|
|
// Create a selector that should match only database settings
|
|
selector := metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"section": "database",
|
|
},
|
|
}
|
|
|
|
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
|
result, err := client.ListAsIni(ctx, selector)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
// Should only have database settings, not server settings
|
|
assert.True(t, result.HasSection("database"))
|
|
assert.Equal(t, "postgres", result.Section("database").Key("type").String())
|
|
assert.Equal(t, "localhost", result.Section("database").Key("host").String())
|
|
// Should NOT have server settings
|
|
assert.False(t, result.HasSection("server"))
|
|
})
|
|
|
|
t.Run("should return all settings with empty selector", func(t *testing.T) {
|
|
// Create multiple settings across different sections
|
|
setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"})
|
|
setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "mysql"})
|
|
|
|
client := newTestClient(500, setting1, setting2)
|
|
|
|
// Empty selector should select everything
|
|
selector := metav1.LabelSelector{}
|
|
|
|
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
|
result, err := client.ListAsIni(ctx, selector)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
// Should have all settings from all sections
|
|
assert.True(t, result.HasSection("server"))
|
|
assert.Equal(t, "3000", result.Section("server").Key("port").String())
|
|
assert.True(t, result.HasSection("database"))
|
|
assert.Equal(t, "mysql", result.Section("database").Key("type").String())
|
|
})
|
|
}
|
|
|
|
func TestRemoteSettingService_List(t *testing.T) {
|
|
t.Run("should handle single page response", func(t *testing.T) {
|
|
setting := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"})
|
|
|
|
client := newTestClient(500, setting)
|
|
|
|
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
|
result, err := client.List(ctx, metav1.LabelSelector{})
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, result, 1)
|
|
|
|
spec := result[0]
|
|
assert.Equal(t, "server", spec.Section)
|
|
assert.Equal(t, "port", spec.Key)
|
|
assert.Equal(t, "3000", spec.Value)
|
|
})
|
|
|
|
t.Run("should handle multiple pages", func(t *testing.T) {
|
|
totalPages := 3
|
|
pageSize := 5
|
|
|
|
pages := make([][]*unstructured.Unstructured, totalPages)
|
|
for pageNum := 0; pageNum < totalPages; pageNum++ {
|
|
for idx := 0; idx < pageSize; idx++ {
|
|
item := newUnstructuredSetting(
|
|
"test-namespace",
|
|
Setting{
|
|
Section: fmt.Sprintf("section-%d", pageNum),
|
|
Key: fmt.Sprintf("key-%d", idx),
|
|
Value: fmt.Sprintf("val-%d-%d", pageNum, idx),
|
|
},
|
|
)
|
|
pages[pageNum] = append(pages[pageNum], item)
|
|
}
|
|
}
|
|
|
|
scheme := runtime.NewScheme()
|
|
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind)
|
|
listCallCount := 0
|
|
dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) {
|
|
listCallCount++
|
|
|
|
continueToken := fmt.Sprintf("continue-%d", listCallCount)
|
|
if listCallCount == totalPages {
|
|
continueToken = ""
|
|
}
|
|
|
|
if listCallCount <= totalPages {
|
|
list := &unstructured.UnstructuredList{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": ApiGroup + "/" + apiVersion,
|
|
"kind": listKind,
|
|
},
|
|
}
|
|
list.SetContinue(continueToken)
|
|
for _, item := range pages[listCallCount-1] {
|
|
list.Items = append(list.Items, *item)
|
|
}
|
|
return true, list, nil
|
|
}
|
|
|
|
return false, nil, nil
|
|
})
|
|
|
|
client := &remoteSettingService{
|
|
dynamicClient: dynamicClient,
|
|
pageSize: int64(pageSize),
|
|
log: log.NewNopLogger(),
|
|
metrics: initMetrics(),
|
|
}
|
|
|
|
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
|
result, err := client.List(ctx, metav1.LabelSelector{})
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, result, totalPages*pageSize)
|
|
assert.Equal(t, totalPages, listCallCount)
|
|
})
|
|
|
|
t.Run("should pass label selector when provided", func(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind)
|
|
dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) {
|
|
listAction := action.(k8testing.ListActionImpl)
|
|
assert.Equal(t, "app=grafana", listAction.ListOptions.LabelSelector)
|
|
return true, &unstructured.UnstructuredList{}, nil
|
|
})
|
|
|
|
client := &remoteSettingService{
|
|
dynamicClient: dynamicClient,
|
|
pageSize: 500,
|
|
log: log.NewNopLogger(),
|
|
metrics: initMetrics(),
|
|
}
|
|
|
|
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
|
_, err := client.List(ctx, metav1.LabelSelector{MatchLabels: map[string]string{"app": "grafana"}})
|
|
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should stop pagination at 1000 pages", func(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind)
|
|
listCallCount := 0
|
|
dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) {
|
|
listCallCount++
|
|
// Always return a continue token to simulate infinite pagination
|
|
list := &unstructured.UnstructuredList{}
|
|
list.SetContinue("continue-forever")
|
|
return true, list, nil
|
|
})
|
|
|
|
client := &remoteSettingService{
|
|
dynamicClient: dynamicClient,
|
|
pageSize: 10,
|
|
log: log.NewNopLogger(),
|
|
metrics: initMetrics(),
|
|
}
|
|
|
|
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
|
_, err := client.List(ctx, metav1.LabelSelector{})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1000, listCallCount, "Should stop at 1000 pages to prevent infinite loops")
|
|
})
|
|
|
|
t.Run("should return error when parsing setting fails", func(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind)
|
|
dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) {
|
|
// Return a malformed setting without spec
|
|
list := &unstructured.UnstructuredList{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": ApiGroup + "/" + apiVersion,
|
|
"kind": listKind,
|
|
},
|
|
}
|
|
malformedSetting := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": ApiGroup + "/" + apiVersion,
|
|
"kind": kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": "malformed",
|
|
"namespace": "test-namespace",
|
|
},
|
|
// Missing spec
|
|
},
|
|
}
|
|
list.Items = append(list.Items, *malformedSetting)
|
|
return true, list, nil
|
|
})
|
|
|
|
client := &remoteSettingService{
|
|
dynamicClient: dynamicClient,
|
|
pageSize: 500,
|
|
log: log.NewNopLogger(),
|
|
metrics: initMetrics(),
|
|
}
|
|
|
|
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
|
result, err := client.List(ctx, metav1.LabelSelector{})
|
|
|
|
require.Error(t, err)
|
|
assert.Nil(t, result)
|
|
assert.Contains(t, err.Error(), "spec not found")
|
|
})
|
|
}
|
|
|
|
func TestParseSettingResource(t *testing.T) {
|
|
t.Run("should parse valid setting resource", func(t *testing.T) {
|
|
setting := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"})
|
|
|
|
result, err := parseSettingResource(setting)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, "database", result.Section)
|
|
assert.Equal(t, "type", result.Key)
|
|
assert.Equal(t, "postgres", result.Value)
|
|
})
|
|
|
|
t.Run("should return error when spec is missing", func(t *testing.T) {
|
|
setting := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": ApiGroup + "/" + apiVersion,
|
|
"kind": kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": "test-setting",
|
|
"namespace": "test-namespace",
|
|
},
|
|
// No spec
|
|
},
|
|
}
|
|
|
|
result, err := parseSettingResource(setting)
|
|
|
|
require.Error(t, err)
|
|
assert.Nil(t, result)
|
|
assert.Contains(t, err.Error(), "spec not found")
|
|
})
|
|
}
|
|
|
|
func TestRemoteSettingService_ToIni(t *testing.T) {
|
|
t.Run("should convert settings to ini format", func(t *testing.T) {
|
|
settings := []*Setting{
|
|
{Section: "database", Key: "type", Value: "postgres"},
|
|
{Section: "database", Key: "host", Value: "localhost"},
|
|
{Section: "server", Key: "http_port", Value: "3000"},
|
|
}
|
|
|
|
client := &remoteSettingService{
|
|
pageSize: 500,
|
|
log: log.NewNopLogger(),
|
|
}
|
|
|
|
result, err := client.toIni(settings)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.True(t, result.HasSection("database"))
|
|
assert.True(t, result.HasSection("server"))
|
|
assert.Equal(t, "postgres", result.Section("database").Key("type").String())
|
|
assert.Equal(t, "localhost", result.Section("database").Key("host").String())
|
|
assert.Equal(t, "3000", result.Section("server").Key("http_port").String())
|
|
})
|
|
|
|
t.Run("should handle empty settings list", func(t *testing.T) {
|
|
var settings []*Setting
|
|
|
|
client := &remoteSettingService{
|
|
pageSize: 500,
|
|
log: log.NewNopLogger(),
|
|
}
|
|
|
|
result, err := client.toIni(settings)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
sections := result.Sections()
|
|
assert.Len(t, sections, 1) // Only default section
|
|
})
|
|
|
|
t.Run("should create section if it does not exist", func(t *testing.T) {
|
|
settings := []*Setting{
|
|
{Section: "new_section", Key: "new_key", Value: "new_value"},
|
|
}
|
|
|
|
client := &remoteSettingService{
|
|
pageSize: 500,
|
|
log: log.NewNopLogger(),
|
|
}
|
|
|
|
result, err := client.toIni(settings)
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.HasSection("new_section"))
|
|
assert.Equal(t, "new_value", result.Section("new_section").Key("new_key").String())
|
|
})
|
|
|
|
t.Run("should handle multiple keys in same section", func(t *testing.T) {
|
|
settings := []*Setting{
|
|
{Section: "auth", Key: "disable_login_form", Value: "false"},
|
|
{Section: "auth", Key: "disable_signout_menu", Value: "true"},
|
|
}
|
|
|
|
client := &remoteSettingService{
|
|
pageSize: 500,
|
|
log: log.NewNopLogger(),
|
|
}
|
|
|
|
result, err := client.toIni(settings)
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.HasSection("auth"))
|
|
authSection := result.Section("auth")
|
|
assert.Equal(t, "false", authSection.Key("disable_login_form").String())
|
|
assert.Equal(t, "true", authSection.Key("disable_signout_menu").String())
|
|
})
|
|
}
|
|
|
|
func TestNew(t *testing.T) {
|
|
t.Run("should create client with default page size", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "https://example.com",
|
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
|
}
|
|
|
|
client, err := New(config)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
remoteClient := client.(*remoteSettingService)
|
|
assert.Equal(t, DefaultPageSize, remoteClient.pageSize)
|
|
})
|
|
|
|
t.Run("should create client with custom page size", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "https://example.com",
|
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
|
PageSize: 100,
|
|
}
|
|
|
|
client, err := New(config)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
remoteClient := client.(*remoteSettingService)
|
|
assert.Equal(t, int64(100), remoteClient.pageSize)
|
|
})
|
|
|
|
t.Run("should use default page size when zero is provided", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "https://example.com",
|
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
|
PageSize: 0,
|
|
}
|
|
|
|
client, err := New(config)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
remoteClient := client.(*remoteSettingService)
|
|
assert.Equal(t, DefaultPageSize, remoteClient.pageSize)
|
|
})
|
|
|
|
t.Run("should return error when config is invalid", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "", // Invalid: empty URL
|
|
}
|
|
|
|
client, err := New(config)
|
|
|
|
require.Error(t, err)
|
|
assert.Nil(t, client)
|
|
assert.Contains(t, err.Error(), "URL cannot be empty")
|
|
})
|
|
}
|
|
|
|
func TestGetDynamicClient(t *testing.T) {
|
|
logger := log.NewNopLogger()
|
|
|
|
t.Run("should return error when SettingServiceURL is empty", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "",
|
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
|
}
|
|
|
|
client, err := getDynamicClient(config, logger)
|
|
|
|
require.Error(t, err)
|
|
assert.Nil(t, client)
|
|
assert.Contains(t, err.Error(), "URL cannot be empty")
|
|
})
|
|
|
|
t.Run("should return error when both TokenExchangeClient and WrapTransport are nil", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "https://example.com",
|
|
TokenExchangeClient: nil,
|
|
WrapTransport: nil,
|
|
}
|
|
|
|
client, err := getDynamicClient(config, logger)
|
|
|
|
require.Error(t, err)
|
|
assert.Nil(t, client)
|
|
assert.Contains(t, err.Error(), "must set either TokenExchangeClient or WrapTransport")
|
|
})
|
|
|
|
t.Run("should create client with WrapTransport", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "https://example.com",
|
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
|
}
|
|
|
|
client, err := getDynamicClient(config, logger)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
})
|
|
|
|
t.Run("should not fail when QPS and Burst are not provided", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "https://example.com",
|
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
|
}
|
|
|
|
client, err := getDynamicClient(config, logger)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
})
|
|
|
|
t.Run("should not fail when custom QPS and Burst are provided", func(t *testing.T) {
|
|
config := Config{
|
|
URL: "https://example.com",
|
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
|
QPS: 10.0,
|
|
Burst: 20,
|
|
}
|
|
|
|
client, err := getDynamicClient(config, logger)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
})
|
|
|
|
t.Run("should use WrapTransport when both WrapTransport and TokenExchangeClient are provided", func(t *testing.T) {
|
|
wrapTransportCalled := false
|
|
tokenExchangeClient := &authlib.TokenExchangeClient{}
|
|
|
|
config := Config{
|
|
URL: "https://example.com",
|
|
TokenExchangeClient: tokenExchangeClient,
|
|
WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
|
|
wrapTransportCalled = true
|
|
return rt
|
|
},
|
|
}
|
|
|
|
client, err := getDynamicClient(config, logger)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
assert.True(t, wrapTransportCalled, "WrapTransport should be called and take precedence over TokenExchangeClient")
|
|
})
|
|
}
|
|
|
|
// Helper function to create an unstructured Setting object for tests
|
|
func newUnstructuredSetting(namespace string, spec Setting) *unstructured.Unstructured {
|
|
// Generate resource name in the format {section}--{key}
|
|
name := fmt.Sprintf("%s--%s", spec.Section, spec.Key)
|
|
|
|
obj := &unstructured.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"apiVersion": ApiGroup + "/" + apiVersion,
|
|
"kind": kind,
|
|
"metadata": map[string]interface{}{
|
|
"name": name,
|
|
"namespace": namespace,
|
|
},
|
|
"spec": map[string]interface{}{
|
|
"section": spec.Section,
|
|
"key": spec.Key,
|
|
"value": spec.Value,
|
|
},
|
|
},
|
|
}
|
|
// Always set section and key labels
|
|
obj.SetLabels(map[string]string{
|
|
"section": spec.Section,
|
|
"key": spec.Key,
|
|
})
|
|
return obj
|
|
}
|
|
|
|
// Helper function to create a test client with the dynamic fake client
|
|
func newTestClient(pageSize int64, objects ...runtime.Object) *remoteSettingService {
|
|
scheme := runtime.NewScheme()
|
|
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind, objects...)
|
|
|
|
return &remoteSettingService{
|
|
dynamicClient: dynamicClient,
|
|
pageSize: pageSize,
|
|
log: log.NewNopLogger(),
|
|
metrics: initMetrics(),
|
|
}
|
|
}
|