Files
Roberto Jiménez Sánchez f2694ce72f Provisioning: add generic version handling for dashboard export (#114691)
* feat(provisioning): add generic version handling for dashboard export

- Update export job to handle any dashboard version generically (v0, v1, v2, v3, etc.)
- Dynamically construct GroupVersionResource for any stored version
- Cache version-specific clients for efficiency
- Add comprehensive table-driven unit tests for multiple versions
- Add integration test to verify version handling end-to-end
- Remove unnecessary version shim from clean operation (deletion works by name)

* test: add unit test for v2 dashboard version (no suffix)
2025-12-02 16:44:24 +01:00

82 lines
2.6 KiB
Go

package migrate
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
)
//go:generate mockery --name NamespaceCleaner --structname MockNamespaceCleaner --inpackage --filename mock_namespace_cleaner.go --with-expecter
type NamespaceCleaner interface {
Clean(ctx context.Context, namespace string, progress jobs.JobProgressRecorder) error
}
type namespaceCleaner struct {
clients resources.ClientFactory
}
func NewNamespaceCleaner(clients resources.ClientFactory) NamespaceCleaner {
return &namespaceCleaner{clients: clients}
}
func (c *namespaceCleaner) Clean(ctx context.Context, namespace string, progress jobs.JobProgressRecorder) error {
clients, err := c.clients.Clients(ctx, namespace)
if err != nil {
return fmt.Errorf("get clients: %w", err)
}
for _, kind := range resources.SupportedProvisioningResources {
progress.SetMessage(ctx, fmt.Sprintf("remove unprovisioned %s", kind.Resource))
client, _, err := clients.ForResource(ctx, kind)
if err != nil {
return fmt.Errorf("get resource client: %w", err)
}
if err = resources.ForEach(ctx, client, func(item *unstructured.Unstructured) error {
result := jobs.JobResourceResult{
Name: item.GetName(),
Kind: item.GetKind(),
Group: item.GroupVersionKind().Group,
Action: repository.FileActionDeleted,
}
// Skip provisioned resources - only delete unprovisioned (unmanaged) resources
meta, err := utils.MetaAccessor(item)
if err != nil {
result.Error = fmt.Errorf("extracting meta accessor for resource %s: %w", result.Name, err)
progress.Record(ctx, result)
return nil // Continue with next resource
}
manager, _ := meta.GetManagerProperties()
// Skip if resource is managed by any provisioning system
if manager.Identity != "" {
result.Action = repository.FileActionIgnored
progress.Record(ctx, result)
return nil // Skip this resource
}
// Deletion works by name, so we can use any client regardless of version
if err := client.Delete(ctx, item.GetName(), metav1.DeleteOptions{}); err != nil {
result.Error = fmt.Errorf("deleting resource %s/%s %s: %w", result.Group, result.Kind, result.Name, err)
progress.Record(ctx, result)
return fmt.Errorf("delete resource: %w", err)
}
progress.Record(ctx, result)
return nil
}); err != nil {
return err
}
}
return nil
}