Provisioning: correctly use resource clients in controllers (#110737)

* Provisioning: correctly use resource clients in controllers

* better names on fields

* fix struct initialisation

* updating roundtripper tests
This commit is contained in:
Daniele Stefano Ferru
2025-09-06 18:13:39 -06:00
committed by GitHub
parent 6f3d2106c0
commit 76f7836419
4 changed files with 83 additions and 19 deletions
+4 -3
View File
@@ -6,7 +6,6 @@ import (
"net/http"
"github.com/grafana/authlib/authn"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
utilnet "k8s.io/apimachinery/pkg/util/net"
)
@@ -19,20 +18,22 @@ type tokenExchanger interface {
type RoundTripper struct {
client tokenExchanger
transport http.RoundTripper
audience string
}
// NewRoundTripper constructs a RoundTripper that exchanges the provided token per request
// and forwards the request to the provided base transport.
func NewRoundTripper(tokenExchangeClient tokenExchanger, base http.RoundTripper) *RoundTripper {
func NewRoundTripper(tokenExchangeClient tokenExchanger, base http.RoundTripper, audience string) *RoundTripper {
return &RoundTripper{
client: tokenExchangeClient,
transport: base,
audience: audience,
}
}
func (t *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
tokenResponse, err := t.client.Exchange(req.Context(), authn.TokenExchangeRequest{
Audiences: []string{provisioning.GROUP},
Audiences: []string{t.audience},
Namespace: "*",
})
if err != nil {
@@ -34,7 +34,7 @@ func TestRoundTripper_SetsAccessTokenHeader(t *testing.T) {
rr := httptest.NewRecorder()
rr.WriteHeader(http.StatusOK)
return rr.Result(), nil
}))
}), "example-audience")
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example", nil)
resp, err := tr.RoundTrip(req)
@@ -50,7 +50,7 @@ func TestRoundTripper_PropagatesExchangeError(t *testing.T) {
tr := NewRoundTripper(&fakeExchanger{err: io.EOF}, roundTripperFunc(func(_ *http.Request) (*http.Response, error) {
t.Fatal("transport should not be called on exchange error")
return nil, nil
}))
}), "example-audience")
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example", nil)
resp, err := tr.RoundTrip(req)
+10 -6
View File
@@ -111,7 +111,7 @@ func setupFromConfig(cfg *setting.Cfg) (controllerCfg *provisioningControllerCon
APIPath: "/apis",
Host: provisioningServerURL,
WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper {
return authrt.NewRoundTripper(tokenExchangeClient, rt)
return authrt.NewRoundTripper(tokenExchangeClient, rt, provisioning.GROUP)
}),
TLSClientConfig: tlsConfig,
}
@@ -153,19 +153,23 @@ func setupFromConfig(cfg *setting.Cfg) (controllerCfg *provisioningControllerCon
return nil, fmt.Errorf("folders_server_url is required in [operator] section")
}
apiServerURLs := []string{dashboardsServerURL, foldersServerURL, provisioningServerURL}
configProviders := make([]apiserver.RestConfigProvider, len(apiServerURLs))
apiServerURLs := map[string]string{
resources.DashboardResource.Group: dashboardsServerURL,
resources.FolderResource.Group: foldersServerURL,
provisioning.GROUP: provisioningServerURL,
}
configProviders := make(map[string]apiserver.RestConfigProvider)
for i, url := range apiServerURLs {
for group, url := range apiServerURLs {
config := &rest.Config{
APIPath: "/apis",
Host: url,
WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper {
return authrt.NewRoundTripper(tokenExchangeClient, rt)
return authrt.NewRoundTripper(tokenExchangeClient, rt, group)
}),
TLSClientConfig: tlsConfig,
}
configProviders[i] = NewDirectConfigProvider(config)
configProviders[group] = NewDirectConfigProvider(config)
}
clients := resources.NewClientFactoryForMultipleAPIServers(configProviders)
@@ -120,26 +120,39 @@ func NewClientFactory(configProvider apiserver.RestConfigProvider) ClientFactory
}
// NewClientFactoryForMultipleAPIServers creates a ClientFactory for multiple API servers
func NewClientFactoryForMultipleAPIServers(configProviders []apiserver.RestConfigProvider) ClientFactory {
clientFactories := make([]ClientFactory, len(configProviders))
func NewClientFactoryForMultipleAPIServers(configProviders map[string]apiserver.RestConfigProvider) ClientFactory {
clientFactories := make(map[string]ClientFactory)
for i, configProvider := range configProviders {
for api, configProvider := range configProviders {
clientFactory := NewClientFactory(configProvider)
clientFactories[i] = clientFactory
clientFactories[api] = clientFactory
}
return &multiClientFactory{clientFactories: clientFactories}
}
type multiClientFactory struct {
clientFactories []ClientFactory
clientFactories map[string]ClientFactory
}
func (m *multiClientFactory) Clients(ctx context.Context, namespace string) (ResourceClients, error) {
for _, clientFactory := range m.clientFactories {
return clientFactory.Clients(ctx, namespace)
clients := make(map[string]ResourceClients)
for group, clientFactory := range m.clientFactories {
c, err := clientFactory.Clients(ctx, namespace)
if err != nil {
return nil, err
}
clients[group] = c
}
return nil, fmt.Errorf("no client factories available")
if len(clients) == 0 {
return nil, fmt.Errorf("no client factories available")
}
return &multiResourceClients{
namespace: namespace,
resourceClientsByAPIGroup: clients,
}, nil
}
func (f *clientFactory) Clients(ctx context.Context, namespace string) (ResourceClients, error) {
@@ -283,6 +296,52 @@ func (c *resourceClients) User(ctx context.Context) (dynamic.ResourceInterface,
return v, err
}
type multiResourceClients struct {
namespace string
mutex sync.Mutex
resourceClientsByAPIGroup map[string]ResourceClients
}
// ForKind returns a client for a kind.
// If the kind has a version, it will be used.
// If the kind does not have a version, the preferred version will be used.
func (c *multiResourceClients) ForKind(ctx context.Context, gvk schema.GroupVersionKind) (dynamic.ResourceInterface, schema.GroupVersionResource, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
resourceClients, ok := c.resourceClientsByAPIGroup[gvk.Group]
if !ok {
return nil, schema.GroupVersionResource{}, fmt.Errorf("no clients provider for group %s", gvk.Group)
}
return resourceClients.ForKind(ctx, gvk)
}
// 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 *multiResourceClients) ForResource(ctx context.Context, gvr schema.GroupVersionResource) (dynamic.ResourceInterface, schema.GroupVersionKind, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
resourceClients, ok := c.resourceClientsByAPIGroup[gvr.Group]
if !ok {
return nil, schema.GroupVersionKind{}, fmt.Errorf("no clients provider for group %s", gvr.Group)
}
return resourceClients.ForResource(ctx, gvr)
}
func (c *multiResourceClients) Folder(ctx context.Context) (dynamic.ResourceInterface, error) {
client, _, err := c.ForResource(ctx, FolderResource)
return client, err
}
func (c *multiResourceClients) User(ctx context.Context) (dynamic.ResourceInterface, error) {
v, _, err := c.ForResource(ctx, 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