package resources import ( "context" "fmt" "sync" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" iam "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/client" ) var ( UserResource = iam.UserResourceInfo.GroupVersionResource() FolderResource = folders.FolderResourceInfo.GroupVersionResource() DashboardResource = dashboardV1.DashboardResourceInfo.GroupVersionResource() DashboardResourceV2 = dashboardV2.DashboardResourceInfo.GroupVersionResource() // SupportedProvisioningResources is the list of resources that can fully managed from the UI SupportedProvisioningResources = []schema.GroupVersionResource{FolderResource, DashboardResource} // SupportsFolderAnnotation is the list of resources that can be saved in a folder SupportsFolderAnnotation = []schema.GroupResource{FolderResource.GroupResource(), DashboardResource.GroupResource()} ) // ClientFactory is a factory for creating clients for a given namespace // //go:generate mockery --name ClientFactory --structname MockClientFactory --inpackage --filename client_factory_mock.go --with-expecter type ClientFactory interface { Clients(ctx context.Context, namespace string) (ResourceClients, error) } type clientFactory struct { configProvider apiserver.RestConfigProvider } // TODO: Rename to NamespacedClients // ResourceClients provides access to clients within a namespace // //go:generate mockery --name ResourceClients --structname MockResourceClients --inpackage --filename clients_mock.go --with-expecter type ResourceClients interface { ForKind(gvk schema.GroupVersionKind) (dynamic.ResourceInterface, schema.GroupVersionResource, error) ForResource(gvr schema.GroupVersionResource) (dynamic.ResourceInterface, schema.GroupVersionKind, error) Folder() (dynamic.ResourceInterface, error) User() (dynamic.ResourceInterface, error) } func NewClientFactory(configProvider apiserver.RestConfigProvider) ClientFactory { return &clientFactory{configProvider} } func (f *clientFactory) Clients(ctx context.Context, namespace string) (ResourceClients, error) { restConfig, err := f.configProvider.GetRestConfig(ctx) if err != nil { return nil, err } if namespace == "" { return nil, fmt.Errorf("missing namespace") } discovery, err := client.NewDiscoveryClient(restConfig) if err != nil { return nil, err } client, err := dynamic.NewForConfig(restConfig) if err != nil { return nil, err } return &resourceClients{ namespace: namespace, discovery: discovery, dynamic: client, byKind: make(map[schema.GroupVersionKind]*clientInfo), byResource: make(map[schema.GroupVersionResource]*clientInfo), }, nil } type resourceClients struct { namespace string dynamic dynamic.Interface discovery client.DiscoveryClient // ResourceInterface cache for this context + namespace mutex sync.Mutex byKind map[schema.GroupVersionKind]*clientInfo byResource map[schema.GroupVersionResource]*clientInfo } type clientInfo struct { gvk schema.GroupVersionKind gvr schema.GroupVersionResource client dynamic.ResourceInterface } func (c *resourceClients) ForKind(gvk schema.GroupVersionKind) (dynamic.ResourceInterface, schema.GroupVersionResource, error) { c.mutex.Lock() defer c.mutex.Unlock() info, ok := c.byKind[gvk] if ok && info.client != nil { return info.client, info.gvr, nil } gvr, err := c.discovery.GetResourceForKind(gvk) if err != nil { return nil, schema.GroupVersionResource{}, err } info = &clientInfo{ gvk: gvk, gvr: gvr, client: c.dynamic.Resource(gvr).Namespace(c.namespace), } c.byKind[gvk] = info c.byResource[gvr] = info return info.client, info.gvr, nil } // ForResource returns a client for a resource. // If the resource has a version, it will be used. // If the resource does not have a version, the preferred version will be used. func (c *resourceClients) ForResource(gvr schema.GroupVersionResource) (dynamic.ResourceInterface, schema.GroupVersionKind, error) { c.mutex.Lock() defer c.mutex.Unlock() info, ok := c.byResource[gvr] if ok && info.client != nil { return info.client, info.gvk, nil } var err error var gvk schema.GroupVersionKind var versionless schema.GroupVersionResource if gvr.Version == "" { versionless = gvr gvr, gvk, err = c.discovery.GetPreferredVesion(schema.GroupResource{ Group: gvr.Group, Resource: gvr.Resource, }) if err != nil { return nil, schema.GroupVersionKind{}, err } info, ok := c.byResource[gvr] if ok && info.client != nil { c.byResource[versionless] = info return info.client, info.gvk, nil } } else { gvk, err = c.discovery.GetKindForResource(gvr) if err != nil { return nil, schema.GroupVersionKind{}, err } } info = &clientInfo{ gvk: gvk, gvr: gvr, client: c.dynamic.Resource(gvr).Namespace(c.namespace), } c.byKind[gvk] = info c.byResource[gvr] = info if versionless.Group != "" { c.byResource[versionless] = info } return info.client, info.gvk, nil } func (c *resourceClients) Folder() (dynamic.ResourceInterface, error) { client, _, err := c.ForResource(FolderResource) return client, err } func (c *resourceClients) User() (dynamic.ResourceInterface, error) { v, _, err := c.ForResource(UserResource) return v, err } // ForEach applies the function to each resource returned from the list operation func ForEach(ctx context.Context, client dynamic.ResourceInterface, fn func(item *unstructured.Unstructured) error) error { var continueToken string for ctx.Err() == nil { list, err := client.List(ctx, metav1.ListOptions{Limit: 100, Continue: continueToken}) if err != nil { return fmt.Errorf("error executing list: %w", err) } for _, item := range list.Items { if ctx.Err() != nil { return ctx.Err() } if err := fn(&item); err != nil { return err } } continueToken = list.GetContinue() if continueToken == "" { break } } return nil }