mirror of https://github.com/grafana/grafana
CloudMigrations: Create resource dependency map to validate snapshot request (#102594)
* CloudMigrations: Create resource dependency map to validate snapshot request * CloudMigrations: Validate resource types dependencies in create snapshot request * CloudMigrations: Update service interface to pass parsed resource types for creation * CloudMigrations: Conditionally append resource to snapshot if enabled * CloudMigrations: Add /cloudmigration/resources/dependencies endpoint * CloudMigrations: Properly filter dashboards and folders from snapshotpull/103643/head
parent
662b635ef9
commit
3fad6183aa
@ -0,0 +1,74 @@ |
||||
package cloudmigration |
||||
|
||||
import ( |
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil" |
||||
) |
||||
|
||||
var ( |
||||
ErrDuplicateResourceType = errutil.BadRequest("cloudmigrations.duplicateResourceType") |
||||
ErrUnknownResourceType = errutil.BadRequest("cloudmigrations.unknownResourceType") |
||||
ErrMissingDependency = errutil.BadRequest("cloudmigrations.missingDependency") |
||||
) |
||||
|
||||
// DependencyMap is a map of resource types to their direct dependencies.
|
||||
type DependencyMap map[MigrateDataType][]MigrateDataType |
||||
|
||||
// ResourceDependency is a map of resource types to their direct dependencies.
|
||||
// This is used to determine which resources can be filtered out from the snapshot without breaking the dependency chain.
|
||||
var ResourceDependency = DependencyMap{ |
||||
PluginDataType: nil, |
||||
FolderDataType: nil, |
||||
DatasourceDataType: {PluginDataType}, |
||||
LibraryElementDataType: {FolderDataType}, |
||||
DashboardDataType: {FolderDataType, DatasourceDataType, LibraryElementDataType}, |
||||
MuteTimingType: nil, |
||||
NotificationTemplateType: nil, |
||||
ContactPointType: {NotificationTemplateType}, |
||||
NotificationPolicyType: {ContactPointType}, |
||||
AlertRuleType: {DatasourceDataType, FolderDataType, DashboardDataType, MuteTimingType, ContactPointType, NotificationPolicyType}, |
||||
AlertRuleGroupType: {AlertRuleType}, |
||||
} |
||||
|
||||
// Parse a raw slice of resource types and returns a set of them if it has all correct dependencies.
|
||||
func (depMap DependencyMap) Parse(rawInput []MigrateDataType) (ResourceTypes, error) { |
||||
// Clean up any possible duplicates.
|
||||
input := make(ResourceTypes, len(rawInput)) |
||||
for _, resourceType := range rawInput { |
||||
if _, exists := input[resourceType]; exists { |
||||
return nil, ErrDuplicateResourceType.Errorf("duplicate resource type found: %v", resourceType) |
||||
} |
||||
input[resourceType] = struct{}{} |
||||
} |
||||
|
||||
// Validate that all dependencies are present.
|
||||
for resourceType := range input { |
||||
if err := depMap.validateDependencies(resourceType, input); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return input, nil |
||||
} |
||||
|
||||
// validateDependencies recursively checks if all dependencies for a resource type are present in the input set.
|
||||
func (depMap DependencyMap) validateDependencies(resourceType MigrateDataType, input ResourceTypes) error { |
||||
// Get the direct dependencies for this resource type.
|
||||
dependencies, ok := depMap[resourceType] |
||||
if !ok { |
||||
return ErrUnknownResourceType.Errorf("unknown resource type: %v", resourceType) |
||||
} |
||||
|
||||
// Make sure all direct dependencies are in the input.
|
||||
for _, dep := range dependencies { |
||||
if _, exists := input[dep]; !exists { |
||||
return ErrMissingDependency.Errorf("missing dependency: %v for resource type %v", dep, resourceType) |
||||
} |
||||
|
||||
// Recursively validate dependencies of dependencies
|
||||
if err := depMap.validateDependencies(dep, input); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,267 @@ |
||||
package cloudmigration |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestResourceDependencyParse(t *testing.T) { |
||||
t.Run("empty input returns an empty set", func(t *testing.T) { |
||||
result, err := ResourceDependency.Parse(nil) |
||||
require.NoError(t, err) |
||||
require.Empty(t, result) |
||||
}) |
||||
|
||||
t.Run("input with duplicates returns an error", func(t *testing.T) { |
||||
_, err := ResourceDependency.Parse([]MigrateDataType{FolderDataType, FolderDataType}) |
||||
require.ErrorIs(t, err, ErrDuplicateResourceType) |
||||
}) |
||||
|
||||
t.Run("unknown resource type returns an error", func(t *testing.T) { |
||||
_, err := ResourceDependency.Parse([]MigrateDataType{"does not exist"}) |
||||
require.ErrorIs(t, err, ErrUnknownResourceType) |
||||
}) |
||||
|
||||
t.Run("PluginDataType has no dependencies", func(t *testing.T) { |
||||
result, err := ResourceDependency.Parse([]MigrateDataType{PluginDataType}) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 1) |
||||
require.Contains(t, result, PluginDataType) |
||||
}) |
||||
|
||||
t.Run("FolderDataType has no dependencies", func(t *testing.T) { |
||||
result, err := ResourceDependency.Parse([]MigrateDataType{FolderDataType}) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 1) |
||||
require.Contains(t, result, FolderDataType) |
||||
}) |
||||
|
||||
t.Run("DatasourceDataType requires PluginDataType", func(t *testing.T) { |
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) { |
||||
_, err := ResourceDependency.Parse([]MigrateDataType{DatasourceDataType}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
require.Contains(t, err.Error(), string(PluginDataType)) |
||||
}) |
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) { |
||||
input := []MigrateDataType{DatasourceDataType, PluginDataType} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 2) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("LibraryElementDataType requires FolderDataType", func(t *testing.T) { |
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) { |
||||
_, err := ResourceDependency.Parse([]MigrateDataType{LibraryElementDataType}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
require.Contains(t, err.Error(), string(FolderDataType)) |
||||
}) |
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) { |
||||
input := []MigrateDataType{LibraryElementDataType, FolderDataType} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 2) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("DashboardDataType requires multiple dependencies", func(t *testing.T) { |
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) { |
||||
// Missing: FolderDataType, DatasourceDataType, LibraryElementDataType, PluginDataType
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{DashboardDataType}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
|
||||
// Missing: DatasourceDataType, PluginDataType
|
||||
_, err = ResourceDependency.Parse([]MigrateDataType{ |
||||
DashboardDataType, |
||||
LibraryElementDataType, |
||||
FolderDataType, |
||||
}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
}) |
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) { |
||||
input := []MigrateDataType{ |
||||
DashboardDataType, |
||||
FolderDataType, |
||||
DatasourceDataType, |
||||
LibraryElementDataType, |
||||
PluginDataType, |
||||
} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, len(input)) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("MuteTimingType has no dependencies", func(t *testing.T) { |
||||
result, err := ResourceDependency.Parse([]MigrateDataType{MuteTimingType}) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 1) |
||||
require.Contains(t, result, MuteTimingType) |
||||
}) |
||||
|
||||
t.Run("NotificationTemplateType has no dependencies", func(t *testing.T) { |
||||
result, err := ResourceDependency.Parse([]MigrateDataType{NotificationTemplateType}) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 1) |
||||
require.Contains(t, result, NotificationTemplateType) |
||||
}) |
||||
|
||||
t.Run("ContactPointType requires NotificationTemplateType", func(t *testing.T) { |
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) { |
||||
_, err := ResourceDependency.Parse([]MigrateDataType{ContactPointType}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
require.Contains(t, err.Error(), string(NotificationTemplateType)) |
||||
}) |
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) { |
||||
input := []MigrateDataType{ContactPointType, NotificationTemplateType} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 2) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("NotificationPolicyType requires multiple dependencies", func(t *testing.T) { |
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) { |
||||
// Missing: ContactPointType, NotificationTemplateType
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{NotificationPolicyType}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
|
||||
// Missing: NotificationTemplateType
|
||||
_, err = ResourceDependency.Parse([]MigrateDataType{ |
||||
NotificationPolicyType, |
||||
ContactPointType, |
||||
}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
}) |
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) { |
||||
input := []MigrateDataType{ |
||||
NotificationPolicyType, |
||||
ContactPointType, |
||||
NotificationTemplateType, |
||||
} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 3) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("AlertRuleType requires multiple dependencies", func(t *testing.T) { |
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) { |
||||
// Missing all dependencies
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{AlertRuleType}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
|
||||
// Missing some dependencies
|
||||
_, err = ResourceDependency.Parse([]MigrateDataType{ |
||||
AlertRuleType, |
||||
DatasourceDataType, |
||||
FolderDataType, |
||||
}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
}) |
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) { |
||||
input := []MigrateDataType{ |
||||
AlertRuleType, |
||||
DatasourceDataType, |
||||
FolderDataType, |
||||
DashboardDataType, |
||||
MuteTimingType, |
||||
ContactPointType, |
||||
NotificationPolicyType, |
||||
PluginDataType, |
||||
LibraryElementDataType, |
||||
NotificationTemplateType, |
||||
} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, len(input)) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("AlertRuleGroupType requires AlertRuleType and all its dependencies", func(t *testing.T) { |
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) { |
||||
// Missing all dependencies
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{AlertRuleGroupType}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
|
||||
// With partial dependencies
|
||||
_, err = ResourceDependency.Parse([]MigrateDataType{ |
||||
AlertRuleGroupType, |
||||
AlertRuleType, |
||||
FolderDataType, |
||||
DashboardDataType, |
||||
MuteTimingType, |
||||
}) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
}) |
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) { |
||||
input := []MigrateDataType{ |
||||
AlertRuleGroupType, |
||||
AlertRuleType, |
||||
DatasourceDataType, |
||||
FolderDataType, |
||||
DashboardDataType, |
||||
MuteTimingType, |
||||
ContactPointType, |
||||
NotificationPolicyType, |
||||
PluginDataType, |
||||
LibraryElementDataType, |
||||
NotificationTemplateType, |
||||
} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, len(input)) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("multiple resources with shared dependencies", func(t *testing.T) { |
||||
t.Run("resources with no dependencies", func(t *testing.T) { |
||||
input := []MigrateDataType{ |
||||
FolderDataType, |
||||
PluginDataType, |
||||
MuteTimingType, |
||||
NotificationTemplateType, |
||||
} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 4) |
||||
}) |
||||
|
||||
t.Run("overlapping dependencies", func(t *testing.T) { |
||||
// DashboardDataType -> LibraryElementDataType -> FolderDataType
|
||||
// \-> DatasourceDataType -> PluginDataType
|
||||
// ContactPointType -> NotificationTemplateType
|
||||
input := []MigrateDataType{ |
||||
DashboardDataType, |
||||
LibraryElementDataType, |
||||
FolderDataType, |
||||
DatasourceDataType, |
||||
PluginDataType, |
||||
ContactPointType, |
||||
NotificationTemplateType, |
||||
} |
||||
result, err := ResourceDependency.Parse(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, result, 7) |
||||
}) |
||||
|
||||
t.Run("missing shared dependency fails", func(t *testing.T) { |
||||
// ContactPointType -> NotificationTemplateType
|
||||
// DatasourceDataType -> PluginDataType
|
||||
input := []MigrateDataType{ |
||||
ContactPointType, |
||||
DatasourceDataType, |
||||
} |
||||
_, err := ResourceDependency.Parse(input) |
||||
require.ErrorIs(t, err, ErrMissingDependency) |
||||
}) |
||||
}) |
||||
} |
Loading…
Reference in new issue