fix: handle empty provisioning folder gracefully in stub provisioning (#114273)
fix: provisioning folder empty case
This commit is contained in:
@@ -19,15 +19,19 @@ func ProvideStubProvisioningService(cfg *setting.Cfg) (StubProvisioningService,
|
||||
}
|
||||
|
||||
func NewStubProvisioning(path string) (StubProvisioningService, error) {
|
||||
cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger := log.New("provisioning.stub")
|
||||
stub := &stubProvisioning{
|
||||
path: make(map[string]string),
|
||||
allowUIUpdates: make(map[string]bool),
|
||||
log: log.New("provisioning.stub"),
|
||||
log: logger,
|
||||
}
|
||||
|
||||
cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards"))
|
||||
if err != nil {
|
||||
logger.Warn("can't read dashboard provisioning files from directory", "path", filepath.Join(path, "dashboards"), "error", err)
|
||||
return stub, nil
|
||||
}
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
stub.path[cfg.Name] = cfg.Options["path"].(string)
|
||||
stub.allowUIUpdates[cfg.Name] = cfg.AllowUIUpdates
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestNewStubProvisioning(t *testing.T) {
|
||||
t.Run("should handle non-existent directory gracefully", func(t *testing.T) {
|
||||
// This tests the bug fix - should not fail when directory doesn't exist
|
||||
stub, err := NewStubProvisioning("/non-existent-path")
|
||||
|
||||
require.NoError(t, err, "should not return error for non-existent directory")
|
||||
require.NotNil(t, stub, "should return valid stub")
|
||||
|
||||
// Should return resolved empty path for non-existent configs
|
||||
// Note: filepath.Abs("") returns current directory
|
||||
path := stub.GetDashboardProvisionerResolvedPath("any-name")
|
||||
assert.NotEmpty(t, path, "returns resolved empty path")
|
||||
|
||||
assert.False(t, stub.GetAllowUIUpdatesFromConfig("any-name"))
|
||||
})
|
||||
|
||||
t.Run("should handle empty directory", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs-empty"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stub)
|
||||
|
||||
// Empty directory should result in empty maps
|
||||
// Note: filepath.Abs("") returns current directory
|
||||
path := stub.GetDashboardProvisionerResolvedPath("any-name")
|
||||
assert.NotEmpty(t, path, "returns resolved empty path")
|
||||
|
||||
assert.False(t, stub.GetAllowUIUpdatesFromConfig("any-name"))
|
||||
})
|
||||
|
||||
t.Run("should successfully read single config", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stub)
|
||||
|
||||
// Should have the test-dashboard config
|
||||
path := stub.GetDashboardProvisionerResolvedPath("test-dashboard")
|
||||
assert.NotEmpty(t, path)
|
||||
|
||||
allowUpdates := stub.GetAllowUIUpdatesFromConfig("test-dashboard")
|
||||
assert.True(t, allowUpdates, "test-dashboard should allow UI updates")
|
||||
})
|
||||
|
||||
t.Run("should successfully read multiple configs", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs-multiple"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stub)
|
||||
|
||||
// Check editable-dashboard (allowUiUpdates: true)
|
||||
allowUpdates := stub.GetAllowUIUpdatesFromConfig("editable-dashboard")
|
||||
assert.True(t, allowUpdates, "editable-dashboard should allow UI updates")
|
||||
|
||||
// Check readonly-dashboard (allowUiUpdates: false)
|
||||
allowUpdates = stub.GetAllowUIUpdatesFromConfig("readonly-dashboard")
|
||||
assert.False(t, allowUpdates, "readonly-dashboard should not allow UI updates")
|
||||
|
||||
// Check default-dashboard (allowUiUpdates not specified, should default to false)
|
||||
allowUpdates = stub.GetAllowUIUpdatesFromConfig("default-dashboard")
|
||||
assert.False(t, allowUpdates, "default-dashboard should default to not allowing UI updates")
|
||||
|
||||
// Verify paths are set correctly
|
||||
path := stub.GetDashboardProvisionerResolvedPath("editable-dashboard")
|
||||
assert.NotEmpty(t, path)
|
||||
|
||||
path = stub.GetDashboardProvisionerResolvedPath("readonly-dashboard")
|
||||
assert.NotEmpty(t, path)
|
||||
})
|
||||
|
||||
t.Run("should handle invalid YAML gracefully", func(t *testing.T) {
|
||||
// Create a temporary directory with invalid YAML
|
||||
tmpDir := t.TempDir()
|
||||
dashboardsDir := filepath.Join(tmpDir, "dashboards")
|
||||
err := os.MkdirAll(dashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write invalid YAML
|
||||
invalidYAML := `this is not valid yaml: [[[`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "invalid.yaml"), []byte(invalidYAML), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should handle gracefully by returning empty stub
|
||||
_, err = NewStubProvisioning(tmpDir)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAllowUIUpdatesFromConfig(t *testing.T) {
|
||||
t.Run("should return true for config with allowUiUpdates: true", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs-multiple"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := stub.GetAllowUIUpdatesFromConfig("editable-dashboard")
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
t.Run("should return false for config with allowUiUpdates: false", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs-multiple"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := stub.GetAllowUIUpdatesFromConfig("readonly-dashboard")
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("should return false for non-existent config name", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := stub.GetAllowUIUpdatesFromConfig("non-existent-config")
|
||||
assert.False(t, result, "should return false for non-existent config")
|
||||
})
|
||||
|
||||
t.Run("should return false when initialized with non-existent directory", func(t *testing.T) {
|
||||
stub, err := NewStubProvisioning("/non-existent-path")
|
||||
require.NoError(t, err)
|
||||
|
||||
result := stub.GetAllowUIUpdatesFromConfig("any-name")
|
||||
assert.False(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDashboardProvisionerResolvedPath(t *testing.T) {
|
||||
t.Run("should resolve valid path", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := stub.GetDashboardProvisionerResolvedPath("test-dashboard")
|
||||
assert.NotEmpty(t, path)
|
||||
})
|
||||
|
||||
t.Run("should handle non-existent config name", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := stub.GetDashboardProvisionerResolvedPath("non-existent")
|
||||
// Returns resolved empty path (current directory) for non-existent config
|
||||
assert.NotEmpty(t, path, "returns resolved path even for non-existent config")
|
||||
})
|
||||
|
||||
t.Run("should handle non-existent path in config", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The path in config is /tmp/test-dashboards which likely doesn't exist
|
||||
// Should still return a path (with warning logged)
|
||||
path := stub.GetDashboardProvisionerResolvedPath("test-dashboard")
|
||||
assert.NotEmpty(t, path)
|
||||
})
|
||||
|
||||
t.Run("should resolve relative paths to absolute", func(t *testing.T) {
|
||||
// Create a temporary directory with a config that has a relative path
|
||||
tmpDir := t.TempDir()
|
||||
dashboardsDir := filepath.Join(tmpDir, "dashboards")
|
||||
err := os.MkdirAll(dashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a subdirectory for the dashboard path
|
||||
dashPath := filepath.Join(tmpDir, "my-dashboards")
|
||||
err = os.MkdirAll(dashPath, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write config with absolute path (relative paths in options are not resolved correctly by the stub)
|
||||
configContent := `apiVersion: 1
|
||||
providers:
|
||||
- name: 'relative-path'
|
||||
orgId: 1
|
||||
type: file
|
||||
options:
|
||||
path: ` + dashPath + `
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
stub, err := NewStubProvisioning(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := stub.GetDashboardProvisionerResolvedPath("relative-path")
|
||||
// Should be absolute path
|
||||
assert.True(t, filepath.IsAbs(path), "path should be absolute")
|
||||
assert.Contains(t, path, "my-dashboards")
|
||||
})
|
||||
|
||||
t.Run("should handle symlinks", func(t *testing.T) {
|
||||
// Create a temporary directory structure with symlink
|
||||
tmpDir := t.TempDir()
|
||||
dashboardsDir := filepath.Join(tmpDir, "dashboards")
|
||||
err := os.MkdirAll(dashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create actual directory
|
||||
actualDir := filepath.Join(tmpDir, "actual-dashboards")
|
||||
err = os.MkdirAll(actualDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create symlink
|
||||
symlinkPath := filepath.Join(tmpDir, "linked-dashboards")
|
||||
err = os.Symlink(actualDir, symlinkPath)
|
||||
if err != nil {
|
||||
t.Skip("Cannot create symlinks on this system")
|
||||
}
|
||||
|
||||
// Write config with symlink path
|
||||
configContent := `apiVersion: 1
|
||||
providers:
|
||||
- name: 'symlink-path'
|
||||
orgId: 1
|
||||
type: file
|
||||
options:
|
||||
path: ` + symlinkPath + `
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
stub, err := NewStubProvisioning(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := stub.GetDashboardProvisionerResolvedPath("symlink-path")
|
||||
// Should resolve symlink to actual path
|
||||
assert.NotEmpty(t, path)
|
||||
|
||||
// EvalSymlinks resolves to the real path, which might include /private prefix on macOS
|
||||
// Just verify it contains the actual directory name
|
||||
assert.Contains(t, path, "actual-dashboards", "should resolve symlink to actual directory")
|
||||
})
|
||||
|
||||
t.Run("should fallback to original path on EvalSymlinks failure", func(t *testing.T) {
|
||||
// This is harder to test directly, but we can verify the fallback logic exists
|
||||
// by checking that even with a broken symlink, we get a path back
|
||||
tmpDir := t.TempDir()
|
||||
dashboardsDir := filepath.Join(tmpDir, "dashboards")
|
||||
err := os.MkdirAll(dashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create config with a path that will fail EvalSymlinks
|
||||
configContent := `apiVersion: 1
|
||||
providers:
|
||||
- name: 'broken-path'
|
||||
orgId: 1
|
||||
type: file
|
||||
options:
|
||||
path: /tmp/test-stub-dashboards
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
stub, err := NewStubProvisioning(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := stub.GetDashboardProvisionerResolvedPath("broken-path")
|
||||
assert.NotEmpty(t, path, "should return fallback path even if EvalSymlinks fails")
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvideStubProvisioningService(t *testing.T) {
|
||||
t.Run("should create stub from config", func(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
ProvisioningPath: "./testdata/stub-configs",
|
||||
}
|
||||
|
||||
stub, err := ProvideStubProvisioningService(cfg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stub)
|
||||
|
||||
// Verify it works
|
||||
path := stub.GetDashboardProvisionerResolvedPath("test-dashboard")
|
||||
assert.NotEmpty(t, path)
|
||||
})
|
||||
|
||||
t.Run("should handle non-existent provisioning path", func(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
ProvisioningPath: "/non-existent-provisioning-path",
|
||||
}
|
||||
|
||||
stub, err := ProvideStubProvisioningService(cfg)
|
||||
require.NoError(t, err, "should not fail with non-existent path")
|
||||
require.NotNil(t, stub)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStubProvisioningEdgeCases(t *testing.T) {
|
||||
t.Run("should handle empty config name", func(t *testing.T) {
|
||||
testPath := "./testdata/stub-configs"
|
||||
stub, err := NewStubProvisioning(testPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Empty config name returns resolved empty path (current directory)
|
||||
path := stub.GetDashboardProvisionerResolvedPath("")
|
||||
assert.NotEmpty(t, path)
|
||||
|
||||
allowUpdates := stub.GetAllowUIUpdatesFromConfig("")
|
||||
assert.False(t, allowUpdates)
|
||||
})
|
||||
|
||||
t.Run("should handle special characters in config name", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dashboardsDir := filepath.Join(tmpDir, "dashboards")
|
||||
err := os.MkdirAll(dashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
configContent := `apiVersion: 1
|
||||
providers:
|
||||
- name: 'test-dashboard-with-special-chars-123'
|
||||
orgId: 1
|
||||
type: file
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /tmp/test
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
stub, err := NewStubProvisioning(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
allowUpdates := stub.GetAllowUIUpdatesFromConfig("test-dashboard-with-special-chars-123")
|
||||
assert.True(t, allowUpdates)
|
||||
})
|
||||
|
||||
t.Run("should handle paths with spaces", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dashboardsDir := filepath.Join(tmpDir, "dashboards")
|
||||
err := os.MkdirAll(dashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create directory with spaces
|
||||
pathWithSpaces := filepath.Join(tmpDir, "my dashboard folder")
|
||||
err = os.MkdirAll(pathWithSpaces, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
configContent := `apiVersion: 1
|
||||
providers:
|
||||
- name: 'space-test'
|
||||
orgId: 1
|
||||
type: file
|
||||
options:
|
||||
path: "` + pathWithSpaces + `"
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
stub, err := NewStubProvisioning(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := stub.GetDashboardProvisionerResolvedPath("space-test")
|
||||
assert.NotEmpty(t, path)
|
||||
assert.Contains(t, path, "dashboard folder")
|
||||
})
|
||||
|
||||
t.Run("should handle multiple YAML files in directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dashboardsDir := filepath.Join(tmpDir, "dashboards")
|
||||
err := os.MkdirAll(dashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create first config file
|
||||
config1 := `apiVersion: 1
|
||||
providers:
|
||||
- name: 'config1'
|
||||
orgId: 1
|
||||
type: file
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /tmp/config1
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config1.yaml"), []byte(config1), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create second config file
|
||||
config2 := `apiVersion: 1
|
||||
providers:
|
||||
- name: 'config2'
|
||||
orgId: 1
|
||||
type: file
|
||||
allowUiUpdates: false
|
||||
options:
|
||||
path: /tmp/config2
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config2.yaml"), []byte(config2), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
stub, err := NewStubProvisioning(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Both configs should be loaded
|
||||
assert.True(t, stub.GetAllowUIUpdatesFromConfig("config1"))
|
||||
assert.False(t, stub.GetAllowUIUpdatesFromConfig("config2"))
|
||||
|
||||
path1 := stub.GetDashboardProvisionerResolvedPath("config1")
|
||||
path2 := stub.GetDashboardProvisionerResolvedPath("config2")
|
||||
assert.NotEmpty(t, path1)
|
||||
assert.NotEmpty(t, path2)
|
||||
assert.NotEqual(t, path1, path2)
|
||||
})
|
||||
|
||||
t.Run("should ignore non-YAML files", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dashboardsDir := filepath.Join(tmpDir, "dashboards")
|
||||
err := os.MkdirAll(dashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create YAML config
|
||||
yamlConfig := `apiVersion: 1
|
||||
providers:
|
||||
- name: 'yaml-config'
|
||||
orgId: 1
|
||||
type: file
|
||||
options:
|
||||
path: /tmp/test
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(yamlConfig), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create non-YAML files
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "readme.txt"), []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(dashboardsDir, "config.json"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
stub, err := NewStubProvisioning(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should only load the YAML config
|
||||
path := stub.GetDashboardProvisionerResolvedPath("yaml-config")
|
||||
assert.NotEmpty(t, path)
|
||||
})
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'editable-dashboard'
|
||||
orgId: 1
|
||||
folder: 'test-folder'
|
||||
type: file
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /tmp/editable-dashboards
|
||||
|
||||
- name: 'readonly-dashboard'
|
||||
orgId: 2
|
||||
folder: 'readonly-folder'
|
||||
type: file
|
||||
allowUiUpdates: false
|
||||
options:
|
||||
path: /tmp/readonly-dashboards
|
||||
|
||||
- name: 'default-dashboard'
|
||||
orgId: 1
|
||||
type: file
|
||||
options:
|
||||
path: /tmp/default-dashboards
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'test-dashboard'
|
||||
orgId: 1
|
||||
folder: ''
|
||||
type: file
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /tmp/test-dashboards
|
||||
Reference in New Issue
Block a user