Files

217 lines
6.3 KiB
Go

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
}