diff --git a/core/clustersmngr/clustersmngr.go b/core/clustersmngr/clustersmngr.go new file mode 100644 index 0000000000..2299a0df81 --- /dev/null +++ b/core/clustersmngr/clustersmngr.go @@ -0,0 +1,92 @@ +package clustersmngr + +import ( + "context" + "fmt" + + "github.com/weaveworks/weave-gitops/pkg/kube" + "github.com/weaveworks/weave-gitops/pkg/server/auth" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate + +type key int + +const ( + // Clusters Clients Pool context key + ClustersClientsPoolCtxKey key = iota +) + +var ( + scheme = kube.CreateScheme() +) + +// Cluster defines a leaf cluster +type Cluster struct { + // Name defines the cluster name + Name string `yaml:"name"` + // Server defines cluster api address + Server string `yaml:"server"` + + // SecretRef defines secret name that holds the cluster Bearer Token + SecretRef string `yaml:"secretRef"` + // BearerToken cluster access token read from SecretRef + BearerToken string + + // TLSConfig holds configuration for TLS connection with the cluster values read from SecretRef + TLSConfig rest.TLSClientConfig +} + +//ClusterFetcher fetches all leaf clusters +//counterfeiter:generate . ClusterFetcher +type ClusterFetcher interface { + Fetch(ctx context.Context) ([]Cluster, error) +} + +// ClientsPool stores all clients to the leaf clusters +type ClientsPool interface { + Add(user *auth.UserPrincipal, cluster Cluster) error + Clients() map[string]client.Client +} + +type clientsPool struct { + clients map[string]client.Client +} + +// NewClustersClientsPool initializes a new ClientsPool +func NewClustersClientsPool() ClientsPool { + return &clientsPool{ + clients: map[string]client.Client{}, + } +} + +// Add adds a cluster client to the clients pool with the given user impersonation +func (cp *clientsPool) Add(user *auth.UserPrincipal, cluster Cluster) error { + config := &rest.Config{ + Host: cluster.Server, + BearerToken: cluster.BearerToken, + TLSClientConfig: cluster.TLSConfig, + Impersonate: rest.ImpersonationConfig{ + UserName: user.ID, + Groups: user.Groups, + }, + } + + leafClient, err := client.New(config, client.Options{ + Scheme: scheme, + }) + if err != nil { + return fmt.Errorf("failed to create leaf client: %w", err) + } + + cp.clients[cluster.Name] = leafClient + + return nil +} + +// Clients returns the clusters clients +func (cp *clientsPool) Clients() map[string]client.Client { + return cp.clients +} diff --git a/core/clustersmngr/clustersmngrfakes/fake_cluster_fetcher.go b/core/clustersmngr/clustersmngrfakes/fake_cluster_fetcher.go new file mode 100644 index 0000000000..735d769f50 --- /dev/null +++ b/core/clustersmngr/clustersmngrfakes/fake_cluster_fetcher.go @@ -0,0 +1,117 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package clustersmngrfakes + +import ( + "context" + "sync" + + "github.com/weaveworks/weave-gitops/core/clustersmngr" +) + +type FakeClusterFetcher struct { + FetchStub func(context.Context) ([]clustersmngr.Cluster, error) + fetchMutex sync.RWMutex + fetchArgsForCall []struct { + arg1 context.Context + } + fetchReturns struct { + result1 []clustersmngr.Cluster + result2 error + } + fetchReturnsOnCall map[int]struct { + result1 []clustersmngr.Cluster + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeClusterFetcher) Fetch(arg1 context.Context) ([]clustersmngr.Cluster, error) { + fake.fetchMutex.Lock() + ret, specificReturn := fake.fetchReturnsOnCall[len(fake.fetchArgsForCall)] + fake.fetchArgsForCall = append(fake.fetchArgsForCall, struct { + arg1 context.Context + }{arg1}) + stub := fake.FetchStub + fakeReturns := fake.fetchReturns + fake.recordInvocation("Fetch", []interface{}{arg1}) + fake.fetchMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeClusterFetcher) FetchCallCount() int { + fake.fetchMutex.RLock() + defer fake.fetchMutex.RUnlock() + return len(fake.fetchArgsForCall) +} + +func (fake *FakeClusterFetcher) FetchCalls(stub func(context.Context) ([]clustersmngr.Cluster, error)) { + fake.fetchMutex.Lock() + defer fake.fetchMutex.Unlock() + fake.FetchStub = stub +} + +func (fake *FakeClusterFetcher) FetchArgsForCall(i int) context.Context { + fake.fetchMutex.RLock() + defer fake.fetchMutex.RUnlock() + argsForCall := fake.fetchArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeClusterFetcher) FetchReturns(result1 []clustersmngr.Cluster, result2 error) { + fake.fetchMutex.Lock() + defer fake.fetchMutex.Unlock() + fake.FetchStub = nil + fake.fetchReturns = struct { + result1 []clustersmngr.Cluster + result2 error + }{result1, result2} +} + +func (fake *FakeClusterFetcher) FetchReturnsOnCall(i int, result1 []clustersmngr.Cluster, result2 error) { + fake.fetchMutex.Lock() + defer fake.fetchMutex.Unlock() + fake.FetchStub = nil + if fake.fetchReturnsOnCall == nil { + fake.fetchReturnsOnCall = make(map[int]struct { + result1 []clustersmngr.Cluster + result2 error + }) + } + fake.fetchReturnsOnCall[i] = struct { + result1 []clustersmngr.Cluster + result2 error + }{result1, result2} +} + +func (fake *FakeClusterFetcher) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.fetchMutex.RLock() + defer fake.fetchMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeClusterFetcher) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ clustersmngr.ClusterFetcher = new(FakeClusterFetcher) diff --git a/core/clustersmngr/middleware.go b/core/clustersmngr/middleware.go new file mode 100644 index 0000000000..b68986ca67 --- /dev/null +++ b/core/clustersmngr/middleware.go @@ -0,0 +1,50 @@ +package clustersmngr + +import ( + "context" + "fmt" + "net/http" + + "github.com/weaveworks/weave-gitops/pkg/server/auth" +) + +// WithClustersClients creates clusters client for provided user in the context +func WithClustersClients(clustersFetcher ClusterFetcher, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := auth.Principal(r.Context()) + if user == nil { + next.ServeHTTP(w, r) + return + } + + clusters, err := clustersFetcher.Fetch(r.Context()) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "failed fetching clusters list: %w", err) + return + } + + clientsPool := NewClustersClientsPool() + for _, c := range clusters { + if err := clientsPool.Add(user, c); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "failed adding cluster client to the pool: %s", err) + return + } + } + + ctx := context.WithValue(r.Context(), ClustersClientsPoolCtxKey, clientsPool) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// ClientsPoolFromCtx returns the ClusterClients pool stored in the context +func ClientsPoolFromCtx(ctx context.Context) ClientsPool { + pool, ok := ctx.Value(ClustersClientsPoolCtxKey).(*clientsPool) + if ok { + return pool + } + + return nil +} diff --git a/core/clustersmngr/middleware_test.go b/core/clustersmngr/middleware_test.go new file mode 100644 index 0000000000..7f91054a3b --- /dev/null +++ b/core/clustersmngr/middleware_test.go @@ -0,0 +1,66 @@ +package clustersmngr_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + . "github.com/onsi/gomega" + + "github.com/weaveworks/weave-gitops/core/clustersmngr" + "github.com/weaveworks/weave-gitops/core/clustersmngr/clustersmngrfakes" + "github.com/weaveworks/weave-gitops/pkg/server/auth" +) + +func TestWithClustersClientsMiddleware(t *testing.T) { + cluster := makeLeafCluster(t) + clustersFetcher := &clustersmngrfakes.FakeClusterFetcher{} + clustersFetcher.FetchReturns([]clustersmngr.Cluster{cluster}, nil) + + g := NewGomegaWithT(t) + + defaultHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + middleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + clientsPool := clustersmngr.ClientsPoolFromCtx(r.Context()) + + g.Expect(clientsPool.Clients()).To(HaveKey(cluster.Name)) + + next.ServeHTTP(w, r) + }) + }(defaultHandler) + + middleware = clustersmngr.WithClustersClients(clustersFetcher, middleware) + middleware = authMiddleware(middleware) + + req := httptest.NewRequest(http.MethodGet, "http://www.foo.com/", nil) + res := httptest.NewRecorder() + middleware.ServeHTTP(res, req) + + g.Expect(res).To(HaveHTTPStatus(http.StatusOK)) +} + +func TestWithClustersClientsMiddlewareFailsToFetchCluster(t *testing.T) { + defaultHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + clustersFetcher := &clustersmngrfakes.FakeClusterFetcher{} + clustersFetcher.FetchReturns(nil, errors.New("error")) + + middleware := clustersmngr.WithClustersClients(clustersFetcher, defaultHandler) + middleware = authMiddleware(middleware) + + req := httptest.NewRequest(http.MethodGet, "http://www.foo.com/", nil) + res := httptest.NewRecorder() + middleware.ServeHTTP(res, req) + + g := NewGomegaWithT(t) + + g.Expect(res).To(HaveHTTPStatus(http.StatusInternalServerError)) +} + +func authMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r.WithContext(auth.WithPrincipal(r.Context(), &auth.UserPrincipal{ID: "user@weave.gitops", Groups: []string{"developers"}}))) + }) +} diff --git a/core/clustersmngr/single_fetcher.go b/core/clustersmngr/single_fetcher.go new file mode 100644 index 0000000000..598dcadcd3 --- /dev/null +++ b/core/clustersmngr/single_fetcher.go @@ -0,0 +1,28 @@ +package clustersmngr + +import ( + "context" + + "k8s.io/client-go/rest" +) + +type singleClusterFetcher struct { + restConfig *rest.Config +} + +func NewSingleClusterFetcher(config *rest.Config) (ClusterFetcher, error) { + return singleClusterFetcher{ + restConfig: config, + }, nil +} + +func (cf singleClusterFetcher) Fetch(ctx context.Context) ([]Cluster, error) { + return []Cluster{ + { + Name: "Default", + Server: cf.restConfig.Host, + BearerToken: cf.restConfig.BearerToken, + TLSConfig: cf.restConfig.TLSClientConfig, + }, + }, nil +} diff --git a/core/clustersmngr/single_fetcher_test.go b/core/clustersmngr/single_fetcher_test.go new file mode 100644 index 0000000000..07e66812fb --- /dev/null +++ b/core/clustersmngr/single_fetcher_test.go @@ -0,0 +1,29 @@ +package clustersmngr_test + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + "github.com/weaveworks/weave-gitops/core/clustersmngr" + "k8s.io/client-go/rest" +) + +func TestSingleFetcher(t *testing.T) { + config := &rest.Config{ + Host: "my-host", + BearerToken: "my-token", + } + + g := NewGomegaWithT(t) + + fetcher, err := clustersmngr.NewSingleClusterFetcher(config) + g.Expect(err).To(BeNil()) + + clusters, err := fetcher.Fetch(context.TODO()) + g.Expect(err).To(BeNil()) + + g.Expect(clusters[0].Name).To(Equal("Default")) + g.Expect(clusters[0].Server).To(Equal(config.Host)) + g.Expect(clusters[0].BearerToken).To(Equal(config.BearerToken)) +} diff --git a/core/clustersmngr/suite_test.go b/core/clustersmngr/suite_test.go new file mode 100644 index 0000000000..88b099f3bf --- /dev/null +++ b/core/clustersmngr/suite_test.go @@ -0,0 +1,40 @@ +package clustersmngr_test + +import ( + "os" + "testing" + + "github.com/weaveworks/weave-gitops/core/clustersmngr" + "github.com/weaveworks/weave-gitops/pkg/testutils" +) + +var k8sEnv *testutils.K8sTestEnv + +func TestMain(m *testing.M) { + os.Setenv("KUBEBUILDER_ASSETS", "../../tools/bin/envtest") + + var err error + k8sEnv, err = testutils.StartK8sTestEnvironment([]string{ + "../../manifests/crds", + "../../tools/testcrds", + }) + + if err != nil { + panic(err) + } + + code := m.Run() + + k8sEnv.Stop() + + os.Exit(code) +} + +func makeLeafCluster(t *testing.T) clustersmngr.Cluster { + return clustersmngr.Cluster{ + Name: "leaf-cluster", + Server: k8sEnv.Rest.Host, + BearerToken: k8sEnv.Rest.BearerToken, + TLSConfig: k8sEnv.Rest.TLSClientConfig, + } +} diff --git a/core/server/kustomization.go b/core/server/kustomization.go index 750268c9f7..6923060d62 100644 --- a/core/server/kustomization.go +++ b/core/server/kustomization.go @@ -2,34 +2,40 @@ package server import ( "context" + "errors" "fmt" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "github.com/weaveworks/weave-gitops/core/clustersmngr" "github.com/weaveworks/weave-gitops/core/server/types" pb "github.com/weaveworks/weave-gitops/pkg/api/core" ) func (cs *coreServer) ListKustomizations(ctx context.Context, msg *pb.ListKustomizationsRequest) (*pb.ListKustomizationsResponse, error) { - k8s, err := cs.k8s.Client(ctx) - if err != nil { - return nil, doClientError(err) - } - - l := &kustomizev1.KustomizationList{} - - if err := list(ctx, k8s, temporarilyEmptyAppName, msg.Namespace, l); err != nil { - return nil, err + clientsPool := clustersmngr.ClientsPoolFromCtx(ctx) + if clientsPool == nil { + return &pb.ListKustomizationsResponse{ + Kustomizations: []*pb.Kustomization{}, + }, errors.New("no clients pool present in context") } var results []*pb.Kustomization - for _, kustomization := range l.Items { - k, err := types.KustomizationToProto(&kustomization) - if err != nil { - return nil, fmt.Errorf("converting items: %w", err) + //TODO: handle failures and parallelize + for _, c := range clientsPool.Clients() { + l := &kustomizev1.KustomizationList{} + if err := list(ctx, c, temporarilyEmptyAppName, msg.Namespace, l); err != nil { + return nil, err } - results = append(results, k) + for _, kustomization := range l.Items { + k, err := types.KustomizationToProto(&kustomization) + if err != nil { + return nil, fmt.Errorf("converting items: %w", err) + } + + results = append(results, k) + } } return &pb.ListKustomizationsResponse{ diff --git a/core/server/server.go b/core/server/server.go index 0d9aa0ca20..f4e918ffa5 100644 --- a/core/server/server.go +++ b/core/server/server.go @@ -36,19 +36,19 @@ type coreServer struct { type CoreServerConfig struct { Logger logr.Logger - restCfg *rest.Config + RestCfg *rest.Config clusterName string } func NewCoreConfig(cfg *rest.Config, clusterName string) CoreServerConfig { return CoreServerConfig{ - restCfg: cfg, + RestCfg: cfg, clusterName: clusterName, } } func NewCoreServer(cfg CoreServerConfig) pb.CoreServer { - cfgGetter := kube.NewImpersonatingConfigGetter(cfg.restCfg, false) + cfgGetter := kube.NewImpersonatingConfigGetter(cfg.RestCfg, false) return &coreServer{ k8s: kube.NewDefaultClientGetter(cfgGetter, cfg.clusterName), diff --git a/core/server/suite_test.go b/core/server/suite_test.go index 002e0e7fa8..da5ec0b6d0 100644 --- a/core/server/suite_test.go +++ b/core/server/suite_test.go @@ -6,8 +6,10 @@ import ( "os" "testing" + "github.com/weaveworks/weave-gitops/core/clustersmngr" "github.com/weaveworks/weave-gitops/core/server" pb "github.com/weaveworks/weave-gitops/pkg/api/core" + "github.com/weaveworks/weave-gitops/pkg/server/auth" "github.com/weaveworks/weave-gitops/pkg/testutils" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -38,7 +40,7 @@ func TestMain(m *testing.M) { } func makeGRPCServer(cfg *rest.Config, t *testing.T) pb.CoreClient { - s := grpc.NewServer() + s := grpc.NewServer(withClientsPoolInterceptor(cfg, &auth.UserPrincipal{})) coreCfg := server.NewCoreConfig(cfg, "foobar") core := server.NewCoreServer(coreCfg) @@ -74,3 +76,23 @@ func makeGRPCServer(cfg *rest.Config, t *testing.T) pb.CoreClient { return pb.NewCoreClient(conn) } + +func withClientsPoolInterceptor(config *rest.Config, user *auth.UserPrincipal) grpc.ServerOption { + return grpc.UnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + cluster := clustersmngr.Cluster{ + Name: "Default", + Server: config.Host, + BearerToken: config.BearerToken, + TLSConfig: config.TLSClientConfig, + } + + clientsPool := clustersmngr.NewClustersClientsPool() + if err := clientsPool.Add(user, cluster); err != nil { + return nil, err + } + + ctx = context.WithValue(ctx, clustersmngr.ClustersClientsPoolCtxKey, clientsPool) + + return handler(ctx, req) + }) +} diff --git a/pkg/server/handler.go b/pkg/server/handler.go index 4d9314a560..844264ba4a 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -7,6 +7,7 @@ import ( "os" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/weaveworks/weave-gitops/core/clustersmngr" core "github.com/weaveworks/weave-gitops/core/server" pbapp "github.com/weaveworks/weave-gitops/pkg/api/applications" pbprofiles "github.com/weaveworks/weave-gitops/pkg/api/profiles" @@ -42,6 +43,12 @@ func NewHandlers(ctx context.Context, cfg *Config) (http.Handler, error) { httpHandler = middleware.WithProviderToken(cfg.AppConfig.JwtClient, httpHandler, cfg.AppConfig.Logger) if AuthEnabled() { + clustersFetcher, err := clustersmngr.NewSingleClusterFetcher(cfg.CoreServerConfig.RestCfg) + if err != nil { + return nil, fmt.Errorf("failed fetching clusters: %w", err) + } + + httpHandler = clustersmngr.WithClustersClients(clustersFetcher, httpHandler) httpHandler = auth.WithAPIAuth(httpHandler, cfg.AuthServer, PublicRoutes) } diff --git a/ui/components/SearchField.tsx b/ui/components/SearchField.tsx index c6b972aa45..c2f6e09805 100644 --- a/ui/components/SearchField.tsx +++ b/ui/components/SearchField.tsx @@ -7,7 +7,6 @@ import Input from "./Input"; type Props = { className?: string; - onSubmit: (val: string) => void; }; diff --git a/ui/components/SearchInput.tsx b/ui/components/SearchInput.tsx deleted file mode 100644 index 5b39eb61fb..0000000000 --- a/ui/components/SearchInput.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from "react"; -import styled from "styled-components"; -import Button from "./Button"; -import Flex from "./Flex"; -import Icon, { IconType } from "./Icon"; -import Input from "./Input"; -import Spacer from "./Spacer"; - -export interface Props { - className?: string; - /** function to store input in parent component state */ - setSearch: (value: string) => void; - /** customizable text for input placeholder */ - placeholder?: string; -} - -const CollapsibleInput = styled(Input)` - max-width: 0px; - overflow: hidden; - transition: max-width 0.5s ease; - &.show { - max-width: 250px; - } -`; - -function SearchInput({ className, setSearch, placeholder = "SEARCH" }: Props) { - const [show, setShow] = React.useState(false); - return ( - - ) => - setSearch(e.target.value) - } - placeholder={placeholder} - /> - - - - ); -} - -export default styled(SearchInput).attrs({ className: SearchInput.name })``; diff --git a/ui/stories/SearchInput.stories.tsx b/ui/stories/SearchInput.stories.tsx deleted file mode 100644 index 7d470fe19d..0000000000 --- a/ui/stories/SearchInput.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Meta, Story } from "@storybook/react"; -import React from "react"; -import SearchInput, { Props } from "../components/SearchInput"; - -export default { - title: "SearchInput", - component: SearchInput, - parameters: { - docs: { - description: { - component: - "Series of deletable MUI Chip components: https://mui.com/components/chips/", - }, - }, - }, -} as Meta; - -const Template: Story = (args) => { - return ; -}; - -export const Default = Template.bind({}); -Default.args = {};