From 15b4cb3c919a8cbd89e8fc227765256b938a1346 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Fri, 3 May 2024 14:02:44 +0300 Subject: [PATCH 01/22] wip --- backend/cmd/server/config.go | 10 +++++----- backend/cmd/server/main.go | 2 +- backend/graph/resolver.go | 19 ++++++++++++------- backend/graph/schema.resolvers.go | 8 ++++++-- backend/hack/server.yaml | 3 +++ backend/internal/ginapp/config.go | 6 +++--- backend/internal/ginapp/ginapp.go | 2 +- backend/internal/ginapp/graphql.go | 4 ++-- hack/config.yaml | 8 ++++++++ 9 files changed, 41 insertions(+), 21 deletions(-) diff --git a/backend/cmd/server/config.go b/backend/cmd/server/config.go index b95bcab5..771dd189 100644 --- a/backend/cmd/server/config.go +++ b/backend/cmd/server/config.go @@ -23,10 +23,10 @@ import ( ) type Config struct { - AuthMode ginapp.AuthMode `mapstructure:"auth-mode" validate:"oneof=cluster token local"` - KubeConfig string `mapstructure:"kube-config"` - BasePath string `mapstructure:"base-path"` - Namespace string + AuthMode ginapp.AuthMode `mapstructure:"auth-mode" validate:"oneof=cluster token local"` + KubeConfig string `mapstructure:"kube-config"` + BasePath string `mapstructure:"base-path"` + AllowedNamespaces []string `mapstructure:"allowed-namespaces"` // session options Session struct { @@ -110,7 +110,7 @@ func DefaultConfig() Config { cfg.AuthMode = appDefault.AuthMode cfg.KubeConfig = filepath.Join(home, ".kube", "config") cfg.BasePath = appDefault.BasePath - cfg.Namespace = appDefault.Namespace + cfg.AllowedNamespaces = appDefault.AllowedNamespaces cfg.Session.Secret = appDefault.Session.Secret cfg.Session.Cookie.Name = appDefault.Session.Cookie.Name diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 70bcdaf9..da6338db 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -192,7 +192,7 @@ func main() { appCfg.AuthMode = ginapp.AuthMode(cfg.AuthMode) appCfg.KubeConfig = cfg.KubeConfig appCfg.BasePath = cfg.BasePath - appCfg.Namespace = cfg.Namespace + appCfg.AllowedNamespaces = cfg.AllowedNamespaces appCfg.AccessLog.Enabled = cfg.Logging.AccessLog.Enabled appCfg.AccessLog.HideHealthChecks = cfg.Logging.AccessLog.HideHealthChecks appCfg.Session.Secret = cfg.Session.Secret diff --git a/backend/graph/resolver.go b/backend/graph/resolver.go index 1fe2cdc3..ab10da89 100644 --- a/backend/graph/resolver.go +++ b/backend/graph/resolver.go @@ -18,6 +18,7 @@ import ( //"os" "context" + "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -32,9 +33,9 @@ import ( //go:generate go run github.com/99designs/gqlgen generate type Resolver struct { - k8sCfg *rest.Config - namespace string - TestClientset *fake.Clientset + k8sCfg *rest.Config + allowedNamespaces []string + TestClientset *fake.Clientset } func (r *Resolver) K8SClientset(ctx context.Context) kubernetes.Interface { @@ -62,8 +63,12 @@ func (r *Resolver) K8SClientset(ctx context.Context) kubernetes.Interface { func (r *Resolver) ToNamespace(namespace *string) string { // check configured namespace - if r.namespace != "" { - return r.namespace + if len(r.allowedNamespaces) > 0 { + if slices.Contains(r.allowedNamespaces, *namespace) { + return *namespace + } else { + panic("xxx") + } } // use default behavior @@ -74,7 +79,7 @@ func (r *Resolver) ToNamespace(namespace *string) string { return ns } -func NewResolver(cfg *rest.Config, namespace string) (*Resolver, error) { +func NewResolver(cfg *rest.Config, allowedNamespaces []string) (*Resolver, error) { // try in-cluster config - return &Resolver{k8sCfg: cfg, namespace: namespace}, nil + return &Resolver{k8sCfg: cfg, allowedNamespaces: allowedNamespaces}, nil } diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index d640b54c..16f0f6fb 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "io" + "slices" "strings" "github.com/kubetail-org/kubetail/graph/model" @@ -127,12 +128,15 @@ func (r *queryResolver) BatchV1JobsList(ctx context.Context, namespace *string, // CoreV1NamespacesList is the resolver for the coreV1NamespacesList field. func (r *queryResolver) CoreV1NamespacesList(ctx context.Context, options *metav1.ListOptions) (*corev1.NamespaceList, error) { response, err := r.K8SClientset(ctx).CoreV1().Namespaces().List(ctx, toListOptions(options)) + if err != nil { + return response, nil + } // apply app namespace filter - if response != nil && r.namespace != "" { + if len(r.allowedNamespaces) > 0 { items := []corev1.Namespace{} for _, item := range response.Items { - if item.Name == r.namespace { + if slices.Contains(r.allowedNamespaces, item.Name) { items = append(items, item) } } diff --git a/backend/hack/server.yaml b/backend/hack/server.yaml index 0a09e139..f7c4dd11 100644 --- a/backend/hack/server.yaml +++ b/backend/hack/server.yaml @@ -22,6 +22,9 @@ gin-mode: debug auth-mode: local kube-config: ${HOME}/.kube/config base-path: / +allowed-namespaces: + - default + - kube-system session: secret: REPLACEME diff --git a/backend/internal/ginapp/config.go b/backend/internal/ginapp/config.go index 988a794e..0eead060 100644 --- a/backend/internal/ginapp/config.go +++ b/backend/internal/ginapp/config.go @@ -30,8 +30,8 @@ type Config struct { // Base path BasePath string - // namespace filter - Namespace string + // Allowed namespaces + AllowedNamespaces []string // access log options AccessLog struct { @@ -79,7 +79,7 @@ func DefaultConfig() Config { cfg.AuthMode = AuthModeToken cfg.BasePath = "/" - cfg.Namespace = "" + cfg.AllowedNamespaces = []string{} cfg.AccessLog.Enabled = true cfg.AccessLog.HideHealthChecks = false diff --git a/backend/internal/ginapp/ginapp.go b/backend/internal/ginapp/ginapp.go index f3c1a148..14e198d7 100644 --- a/backend/internal/ginapp/ginapp.go +++ b/backend/internal/ginapp/ginapp.go @@ -182,7 +182,7 @@ func NewGinApp(config Config) (*GinApp, error) { // graphql handler h := &GraphQLHandlers{app} - endpointHandler := h.EndpointHandler(k8sCfg, config.Namespace, csrfProtect) + endpointHandler := h.EndpointHandler(k8sCfg, config.AllowedNamespaces, csrfProtect) graphql.GET("", endpointHandler) graphql.POST("", endpointHandler) } diff --git a/backend/internal/ginapp/graphql.go b/backend/internal/ginapp/graphql.go index ab3d434d..339c9bf3 100644 --- a/backend/internal/ginapp/graphql.go +++ b/backend/internal/ginapp/graphql.go @@ -36,9 +36,9 @@ type GraphQLHandlers struct { } // GET|POST "/graphql": GraphQL query endpoint -func (app *GraphQLHandlers) EndpointHandler(cfg *rest.Config, namespace string, csrfProtect func(http.Handler) http.Handler) gin.HandlerFunc { +func (app *GraphQLHandlers) EndpointHandler(cfg *rest.Config, allowedNamespaces []string, csrfProtect func(http.Handler) http.Handler) gin.HandlerFunc { // init resolver - r, err := graph.NewResolver(cfg, namespace) + r, err := graph.NewResolver(cfg, allowedNamespaces) if err != nil { panic(err) } diff --git a/hack/config.yaml b/hack/config.yaml index 9e8ebdf6..01c70ee6 100644 --- a/hack/config.yaml +++ b/hack/config.yaml @@ -50,6 +50,14 @@ kube-config: ${HOME}/.kube/config # base-path: / +## allowed-namespaces ## +# +# If non-empty, restricts queries to allowed namespaces +# +# Default value: [] +# +allowed-namespaces: [] + ## csrf ## # csrf: From 388aceae94e28a4819eef5f7dcc4b96ca33fec0a Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Fri, 3 May 2024 17:25:05 +0300 Subject: [PATCH 02/22] wip --- backend/graph/helpers.go | 34 ++++++++++++ backend/graph/resolver.go | 20 +++++++ backend/graph/schema.resolvers.go | 19 ++++++- hack/minikube.yaml | 87 ++++++++++++++++++++++++++++--- 4 files changed, 151 insertions(+), 9 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 9abfefbd..55ee16f8 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -30,7 +30,9 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/utils/ptr" @@ -83,6 +85,38 @@ type FollowArgs struct { Since string } +// gvr +func initGVR[T runtime.Object]() (schema.GroupVersionResource, error) { + switch any((*T)(nil)).(type) { + case *corev1.PodList: + return schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, nil + default: + return schema.GroupVersionResource{}, fmt.Errorf("not implemented") + } +} + +// listResource +func listResource[T runtime.Object](ctx context.Context, dynamicClient dynamic.Interface, namespace string, options *metav1.ListOptions) (T, error) { + var output T + + gvr, err := initGVR[T]() + if err != nil { + return output, err + } + + list, err := dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, toListOptions(options)) + if err != nil { + return output, err + } + + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) + if err != nil { + return output, err + } + + return output, nil +} + // watchEventProxyChannel func watchEventProxyChannel(ctx context.Context, watchAPI watch.Interface) <-chan *watch.Event { evCh := watchAPI.ResultChan() diff --git a/backend/graph/resolver.go b/backend/graph/resolver.go index ab10da89..9c8feeee 100644 --- a/backend/graph/resolver.go +++ b/backend/graph/resolver.go @@ -21,6 +21,7 @@ import ( "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/rest" @@ -61,6 +62,25 @@ func (r *Resolver) K8SClientset(ctx context.Context) kubernetes.Interface { return clientset } +func (r *Resolver) K8SDynamicClient(ctx context.Context) dynamic.Interface { + // copy config + cfg := rest.CopyConfig(r.k8sCfg) + + // get token from context + token, ok := ctx.Value(K8STokenCtxKey).(string) + if ok { + cfg.BearerToken = token + cfg.BearerTokenFile = "" + } + + dynamicClient, err := dynamic.NewForConfig(cfg) + if err != nil { + panic(err) + } + + return dynamicClient +} + func (r *Resolver) ToNamespace(namespace *string) string { // check configured namespace if len(r.allowedNamespaces) > 0 { diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index 16f0f6fb..ff152340 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -158,7 +158,24 @@ func (r *queryResolver) CoreV1PodsGet(ctx context.Context, namespace *string, na // CoreV1PodsList is the resolver for the coreV1PodsList field. func (r *queryResolver) CoreV1PodsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*corev1.PodList, error) { - return r.K8SClientset(ctx).CoreV1().Pods(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + /* + podGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + + list, err := r.K8SDynamicClient(ctx).Resource(podGVR).Namespace(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + if err != nil { + return nil, err + } + + podList := &corev1.PodList{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), podList) + if err != nil { + return nil, err + } + + return podList, err + */ + //return r.K8SClientset(ctx).CoreV1().Pods(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + return listResource[*corev1.PodList](ctx, r.K8SDynamicClient(ctx), r.ToNamespace(namespace), options) } // CoreV1PodsGetLogs is the resolver for the coreV1PodsGetLogs field. diff --git a/hack/minikube.yaml b/hack/minikube.yaml index fc3978b3..989a5658 100644 --- a/hack/minikube.yaml +++ b/hack/minikube.yaml @@ -21,30 +21,101 @@ data: secret: REPLACEME csrf: secret: REPLACEME +#--- +#kind: ClusterRole +#apiVersion: rbac.authorization.k8s.io/v1 +#metadata: +# name: kubetail +# annotations: +#rules: +#- apiGroups: ["", apps, batch] +# resources: [cronjobs, daemonsets, deployments, jobs, namespaces, nodes, pods, pods/log, replicasets, statefulsets] +# verbs: [get, list, watch] +#--- +#kind: ClusterRoleBinding +#apiVersion: rbac.authorization.k8s.io/v1 +#metadata: +# name: kubetail +# annotations: +#roleRef: +# apiGroup: rbac.authorization.k8s.io +# kind: ClusterRole +# name: kubetail +#subjects: +#- kind: ServiceAccount +# name: kubetail +# namespace: kubetail +#--- --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: kubetail - annotations: rules: -- apiGroups: ["", apps, batch] - resources: [cronjobs, daemonsets, deployments, jobs, namespaces, nodes, pods, pods/log, replicasets, statefulsets] - verbs: [get, list, watch] +- apiGroups: [""] + resources: ["nodes", "namespaces"] + verbs: ["get", "list", "watch"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: kubetail - annotations: +subjects: +- kind: ServiceAccount + namespace: kubetail + name: kubetail roleRef: - apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: kubetail + apiGroup: rbac.authorization.k8s.io +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: kubernetes-dashboard + name: kubetail +rules: +- apiGroups: ["", "apps", "batch"] + resources: ["cronjobs", "daemonsets", "deployments", "jobs", "pods", "pods/log", "replicasets", "statefulsets"] + verbs: ["get", "list", "watch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: kubernetes-dashboard + name: kubetail +subjects: +- kind: ServiceAccount + name: kubetail + namespace: kubetail +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kubetail +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: kube-system + name: kubetail +rules: +- apiGroups: ["", "apps", "batch"] + resources: ["cronjobs", "daemonsets", "deployments", "jobs", "pods", "pods/log", "replicasets", "statefulsets"] + verbs: ["get", "list", "watch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: kube-system + name: kubetail subjects: - kind: ServiceAccount name: kubetail namespace: kubetail +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kubetail --- kind: Service apiVersion: v1 @@ -98,8 +169,8 @@ spec: type: RuntimeDefault containers: - name: kubetail - #image: docker.io/kubetail/kubetail:0.4.5 - image: kubetail:latest + image: docker.io/kubetail/kubetail:0.4.6 + #image: kubetail:latest securityContext: allowPrivilegeEscalation: false capabilities: From 8ce93ec61e08ef472dac7e71d9dbbe3c7393968e Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Sat, 4 May 2024 00:24:27 +0300 Subject: [PATCH 03/22] wip --- backend/graph/errors.go | 1 + backend/graph/helpers.go | 28 ++++++++++++++++++++-------- backend/graph/resolver.go | 14 ++++++++++++++ backend/graph/schema.resolvers.go | 18 +----------------- backend/hack/server.yaml | 2 +- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/backend/graph/errors.go b/backend/graph/errors.go index 4b0e405b..ab0ed40f 100644 --- a/backend/graph/errors.go +++ b/backend/graph/errors.go @@ -22,6 +22,7 @@ import ( // custom errors var ( ErrUnauthenticated = NewError("KUBETAIL_UNAUTHENTICATED", "Authentication required") + ErrForbidden = NewError("KUBETAIL_FORBIDDEN", "Access forbidden") ErrWatchError = NewError("KUBETAIL_WATCH_ERROR", "Watch error") ErrInternalServerError = NewError("INTERNAL_SERVER_ERROR", "Internal server error") ) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 55ee16f8..55495831 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -29,10 +29,10 @@ import ( "github.com/sosodev/duration" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/utils/ptr" @@ -85,30 +85,42 @@ type FollowArgs struct { Since string } -// gvr -func initGVR[T runtime.Object]() (schema.GroupVersionResource, error) { - switch any((*T)(nil)).(type) { +// getGVR +func getGVR(obj runtime.Object) (schema.GroupVersionResource, error) { + switch (obj).(type) { case *corev1.PodList: return schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, nil default: - return schema.GroupVersionResource{}, fmt.Errorf("not implemented") + return schema.GroupVersionResource{}, fmt.Errorf("not implemented: %T", obj) } } // listResource -func listResource[T runtime.Object](ctx context.Context, dynamicClient dynamic.Interface, namespace string, options *metav1.ListOptions) (T, error) { +func listResource[T runtime.Object](r *queryResolver, ctx context.Context, namespace *string, options *metav1.ListOptions) (T, error) { var output T - gvr, err := initGVR[T]() + gvr, err := getGVR(output) if err != nil { return output, err } - list, err := dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, toListOptions(options)) + ns, err := r.ToNamespace2(namespace) if err != nil { return output, err } + var list *unstructured.UnstructuredList + + if ns == "" && len(r.allowedNamespaces) > 0 { + // implement list across namespaces + + } else { + list, err = r.K8SDynamicClient(ctx).Resource(gvr).Namespace(ns).List(ctx, toListOptions(options)) + if err != nil { + return output, err + } + } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) if err != nil { return output, err diff --git a/backend/graph/resolver.go b/backend/graph/resolver.go index 9c8feeee..5a2ec6e3 100644 --- a/backend/graph/resolver.go +++ b/backend/graph/resolver.go @@ -99,6 +99,20 @@ func (r *Resolver) ToNamespace(namespace *string) string { return ns } +func (r *Resolver) ToNamespace2(namespace *string) (string, error) { + ns := metav1.NamespaceDefault + if namespace != nil { + ns = *namespace + } + + // perform auth + if ns != "" && len(r.allowedNamespaces) > 0 && !slices.Contains(r.allowedNamespaces, ns) { + return "", ErrForbidden + } + + return ns, nil +} + func NewResolver(cfg *rest.Config, allowedNamespaces []string) (*Resolver, error) { // try in-cluster config return &Resolver{k8sCfg: cfg, allowedNamespaces: allowedNamespaces}, nil diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index ff152340..43c7d8b9 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -158,24 +158,8 @@ func (r *queryResolver) CoreV1PodsGet(ctx context.Context, namespace *string, na // CoreV1PodsList is the resolver for the coreV1PodsList field. func (r *queryResolver) CoreV1PodsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*corev1.PodList, error) { - /* - podGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} - - list, err := r.K8SDynamicClient(ctx).Resource(podGVR).Namespace(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) - if err != nil { - return nil, err - } - - podList := &corev1.PodList{} - err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), podList) - if err != nil { - return nil, err - } - - return podList, err - */ //return r.K8SClientset(ctx).CoreV1().Pods(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) - return listResource[*corev1.PodList](ctx, r.K8SDynamicClient(ctx), r.ToNamespace(namespace), options) + return listResource[*corev1.PodList](r, ctx, namespace, options) } // CoreV1PodsGetLogs is the resolver for the coreV1PodsGetLogs field. diff --git a/backend/hack/server.yaml b/backend/hack/server.yaml index f7c4dd11..72a5ac19 100644 --- a/backend/hack/server.yaml +++ b/backend/hack/server.yaml @@ -23,7 +23,7 @@ auth-mode: local kube-config: ${HOME}/.kube/config base-path: / allowed-namespaces: - - default + - kubernetes-dashboard - kube-system session: From 1f622f2527df6c462108b0010d0e95b0740b25e4 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Thu, 9 May 2024 10:30:16 +0300 Subject: [PATCH 04/22] wip --- backend/graph/helpers.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 55495831..1ec793dd 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "strings" + "sync" "time" "github.com/99designs/gqlgen/graphql/handler/transport" @@ -33,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/utils/ptr" @@ -95,6 +97,11 @@ func getGVR(obj runtime.Object) (schema.GroupVersionResource, error) { } } +// fetchListResource +func fetchListResource(ctx context.Context, dynamicClient dynamic.NamespaceableResourceInterface, namespace *string, options *metav1.ListOptions, wg *sync.WaitGroup, results chan<- []string) { + +} + // listResource func listResource[T runtime.Object](r *queryResolver, ctx context.Context, namespace *string, options *metav1.ListOptions) (T, error) { var output T From eb059d202638708d3135b3e2806b7e816e727118 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Thu, 9 May 2024 10:31:07 +0300 Subject: [PATCH 05/22] wip --- backend/graph/helpers.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 1ec793dd..ea42ade4 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -98,8 +98,13 @@ func getGVR(obj runtime.Object) (schema.GroupVersionResource, error) { } // fetchListResource -func fetchListResource(ctx context.Context, dynamicClient dynamic.NamespaceableResourceInterface, namespace *string, options *metav1.ListOptions, wg *sync.WaitGroup, results chan<- []string) { - +func fetchListResource(ctx context.Context, dynamicClient dynamic.NamespaceableResourceInterface, namespace string, options *metav1.ListOptions, wg *sync.WaitGroup, results chan<- *unstructured.UnstructuredList) { + defer wg.Done() + list, err := dynamicClient.Namespace(namespace).List(ctx, toListOptions(options)) + if err != nil { + // log error here + } + results <- list } // listResource @@ -116,6 +121,16 @@ func listResource[T runtime.Object](r *queryResolver, ctx context.Context, names return output, err } + namespaces := []string{} + if ns == "" && len(r.allowedNamespaces) > 0 { + // implement list across namespaces + namespaces = r.allowedNamespaces + } else { + namespaces = []string{ns} + } + + var wg sync.WaitGroup + results := make(chan *unstructured.UnstructuredList, len(namespaces)) var list *unstructured.UnstructuredList if ns == "" && len(r.allowedNamespaces) > 0 { From 3b949b154fe238ea9c7fc09ff74ad0a3421f15a0 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Thu, 9 May 2024 17:09:37 +0300 Subject: [PATCH 06/22] wip --- backend/graph/helpers.go | 100 ++++++++++++++++++++++++++++---------- backend/graph/resolver.go | 15 ++++-- 2 files changed, 87 insertions(+), 28 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index ea42ade4..17dfcf79 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "io" + "sort" "strings" "sync" "time" @@ -30,7 +31,7 @@ import ( "github.com/sosodev/duration" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" @@ -97,56 +98,105 @@ func getGVR(obj runtime.Object) (schema.GroupVersionResource, error) { } } +// Represents response from fetchListResource() +type FetchResponse[T runtime.Object] struct { + Namespace string + Result T + Error error +} + +// mergeResults +func mergeResults(results []*unstructured.UnstructuredList, options metav1.ListOptions) *unstructured.UnstructuredList { + // loop through results + items := []unstructured.Unstructured{} + remainingItemCount := int64(0) + resourceVersionMap := map[string]string{} + continueMap := map[string]string{} + + for _, result := range results { + remainingItemCount += *result.GetRemainingItemCount() + for _, item := range result.Items { + items = append(items, item) + } + } + + // sort items + sort.Slice(items, func(i, j int) bool { + return items[i].GetName() < items[j].GetName() + }) + + // init merged object + output := new(unstructured.UnstructuredList) + output.SetRemainingItemCount(&remainingItemCount) + output.Items = items[:options.Limit] + + return output +} + // fetchListResource -func fetchListResource(ctx context.Context, dynamicClient dynamic.NamespaceableResourceInterface, namespace string, options *metav1.ListOptions, wg *sync.WaitGroup, results chan<- *unstructured.UnstructuredList) { +func fetchListResource[T runtime.Object](ctx context.Context, client dynamic.NamespaceableResourceInterface, namespace string, options metav1.ListOptions, wg *sync.WaitGroup, ch chan<- FetchResponse[T]) { defer wg.Done() - list, err := dynamicClient.Namespace(namespace).List(ctx, toListOptions(options)) + + list, err := client.Namespace(namespace).List(ctx, options) + if err != nil { + ch <- FetchResponse[T]{Error: err} + return + } + + var result T + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &result) if err != nil { - // log error here + ch <- FetchResponse[T]{Error: err} + return } - results <- list + + ch <- FetchResponse[T]{Result: result} } // listResource func listResource[T runtime.Object](r *queryResolver, ctx context.Context, namespace *string, options *metav1.ListOptions) (T, error) { var output T + // init client gvr, err := getGVR(output) if err != nil { return output, err } + client := r.K8SDynamicClient(ctx).Resource(gvr) - ns, err := r.ToNamespace2(namespace) + // init namespaces + namespaces, err := r.ToNamespaces(namespace) if err != nil { return output, err } - namespaces := []string{} - if ns == "" && len(r.allowedNamespaces) > 0 { - // implement list across namespaces - namespaces = r.allowedNamespaces - } else { - namespaces = []string{ns} - } + // init list options + opts := toListOptions(options) + // execute requests in parallel var wg sync.WaitGroup - results := make(chan *unstructured.UnstructuredList, len(namespaces)) - var list *unstructured.UnstructuredList + ch := make(chan FetchResponse[T], len(namespaces)) - if ns == "" && len(r.allowedNamespaces) > 0 { - // implement list across namespaces + for _, namespace := range namespaces { + wg.Add(1) + go fetchListResource[T](ctx, client, namespace, opts, &wg, ch) + } - } else { - list, err = r.K8SDynamicClient(ctx).Resource(gvr).Namespace(ns).List(ctx, toListOptions(options)) - if err != nil { - return output, err + wg.Wait() + close(ch) + + results := make([]T, len(namespaces)) + + i := 0 + for resp := range ch { + if resp.Error != nil { + return output, resp.Error } + results[i] = resp.Result + i += 1 } - err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) - if err != nil { - return output, err - } + result := mergeResults(results) return output, nil } diff --git a/backend/graph/resolver.go b/backend/graph/resolver.go index 5a2ec6e3..82d1dfc9 100644 --- a/backend/graph/resolver.go +++ b/backend/graph/resolver.go @@ -99,7 +99,9 @@ func (r *Resolver) ToNamespace(namespace *string) string { return ns } -func (r *Resolver) ToNamespace2(namespace *string) (string, error) { +func (r *Resolver) ToNamespaces(namespace *string) ([]string, error) { + var namespaces []string + ns := metav1.NamespaceDefault if namespace != nil { ns = *namespace @@ -107,10 +109,17 @@ func (r *Resolver) ToNamespace2(namespace *string) (string, error) { // perform auth if ns != "" && len(r.allowedNamespaces) > 0 && !slices.Contains(r.allowedNamespaces, ns) { - return "", ErrForbidden + return nil, ErrForbidden + } + + // listify + if ns == "" && len(r.allowedNamespaces) > 0 { + namespaces = r.allowedNamespaces + } else { + namespaces = []string{ns} } - return ns, nil + return namespaces, nil } func NewResolver(cfg *rest.Config, allowedNamespaces []string) (*Resolver, error) { From de9dcdd970aadfe33bbf2011ef0d59435015c892 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Thu, 9 May 2024 17:59:40 +0300 Subject: [PATCH 07/22] wip --- backend/graph/helpers.go | 58 ++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 17dfcf79..8e090db2 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -99,22 +99,34 @@ func getGVR(obj runtime.Object) (schema.GroupVersionResource, error) { } // Represents response from fetchListResource() -type FetchResponse[T runtime.Object] struct { +type FetchResponse struct { Namespace string - Result T + Result *unstructured.UnstructuredList Error error } // mergeResults -func mergeResults(results []*unstructured.UnstructuredList, options metav1.ListOptions) *unstructured.UnstructuredList { +func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstructured.UnstructuredList, error) { // loop through results items := []unstructured.Unstructured{} remainingItemCount := int64(0) resourceVersionMap := map[string]string{} continueMap := map[string]string{} - for _, result := range results { + for _, resp := range responses { + // exit if any query resulted in error + if resp.Error != nil { + return nil, resp.Error + } + + result := resp.Result + + // metadata remainingItemCount += *result.GetRemainingItemCount() + resourceVersionMap[resp.Namespace] = result.GetResourceVersion() + continueMap[resp.Namespace] = result.GetContinue() + + // loop through items for _, item := range result.Items { items = append(items, item) } @@ -130,27 +142,20 @@ func mergeResults(results []*unstructured.UnstructuredList, options metav1.ListO output.SetRemainingItemCount(&remainingItemCount) output.Items = items[:options.Limit] - return output + return output, nil } // fetchListResource -func fetchListResource[T runtime.Object](ctx context.Context, client dynamic.NamespaceableResourceInterface, namespace string, options metav1.ListOptions, wg *sync.WaitGroup, ch chan<- FetchResponse[T]) { +func fetchListResource[T runtime.Object](ctx context.Context, client dynamic.NamespaceableResourceInterface, namespace string, options metav1.ListOptions, wg *sync.WaitGroup, ch chan<- FetchResponse) { defer wg.Done() list, err := client.Namespace(namespace).List(ctx, options) if err != nil { - ch <- FetchResponse[T]{Error: err} - return - } - - var result T - err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &result) - if err != nil { - ch <- FetchResponse[T]{Error: err} + ch <- FetchResponse{Error: err} return } - ch <- FetchResponse[T]{Result: result} + ch <- FetchResponse{Namespace: namespace, Result: list} } // listResource @@ -175,7 +180,7 @@ func listResource[T runtime.Object](r *queryResolver, ctx context.Context, names // execute requests in parallel var wg sync.WaitGroup - ch := make(chan FetchResponse[T], len(namespaces)) + ch := make(chan FetchResponse, len(namespaces)) for _, namespace := range namespaces { wg.Add(1) @@ -185,18 +190,25 @@ func listResource[T runtime.Object](r *queryResolver, ctx context.Context, names wg.Wait() close(ch) - results := make([]T, len(namespaces)) - + // gather responses + responses := make([]FetchResponse, len(namespaces)) i := 0 for resp := range ch { - if resp.Error != nil { - return output, resp.Error - } - results[i] = resp.Result + responses[i] = resp i += 1 } - result := mergeResults(results) + // merge results + list, err := mergeResults(responses, opts) + if err != nil { + return output, err + } + + // de-serialize + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) + if err != nil { + return output, err + } return output, nil } From 1f2647d80c29f64017bcc369017a380030bd13f6 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Fri, 10 May 2024 00:03:10 +0300 Subject: [PATCH 08/22] wip --- backend/go.mod | 7 ++++--- backend/go.sum | 10 ++++++++++ backend/graph/helpers.go | 9 +++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 4d28a932..bb3cc29f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -21,9 +21,9 @@ require ( github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 github.com/vektah/gqlparser/v2 v2.5.11 - k8s.io/api v0.29.3 - k8s.io/apimachinery v0.29.3 - k8s.io/client-go v0.29.3 + k8s.io/api v0.30.0 + k8s.io/apimachinery v0.30.0 + k8s.io/client-go v0.30.0 k8s.io/utils v0.0.0-20240310230437-4693a0247e57 ) @@ -106,6 +106,7 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiserver v0.30.0 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 // indirect nhooyr.io/websocket v1.8.7 // indirect diff --git a/backend/go.sum b/backend/go.sum index cd06d14d..33c68145 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -171,8 +171,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -333,10 +335,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/apiserver v0.30.0 h1:QCec+U72tMQ+9tR6A0sMBB5Vh6ImCEkoKkTDRABWq6M= +k8s.io/apiserver v0.30.0/go.mod h1:smOIBq8t0MbKZi7O7SyIpjPsiKJ8qa+llcFCluKyqiY= k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 h1:qVoMaQV5t62UUvHe16Q3eb2c5HPzLHYzsi0Tu/xLndo= diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 8e090db2..2acdd024 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -35,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/storage" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/utils/ptr" @@ -126,10 +127,10 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr resourceVersionMap[resp.Namespace] = result.GetResourceVersion() continueMap[resp.Namespace] = result.GetContinue() - // loop through items - for _, item := range result.Items { - items = append(items, item) - } + fmt.Println(storage.DecodeContinue(result.GetContinue(), "")) + + // items + items = append(items, result.Items...) } // sort items From 8fe3e59b2366f01379ebb5952e1f059f7bf9b842 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Fri, 10 May 2024 09:01:55 +0300 Subject: [PATCH 09/22] wip --- backend/go.mod | 2 +- backend/go.sum | 111 +++++++++++++++++++++++++++++++++++---- backend/graph/helpers.go | 70 ++++++++++++++++++++---- 3 files changed, 162 insertions(+), 21 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index bb3cc29f..25ffdfe2 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -23,6 +23,7 @@ require ( github.com/vektah/gqlparser/v2 v2.5.11 k8s.io/api v0.30.0 k8s.io/apimachinery v0.30.0 + k8s.io/apiserver v0.30.0 k8s.io/client-go v0.30.0 k8s.io/utils v0.0.0-20240310230437-4693a0247e57 ) @@ -106,7 +107,6 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiserver v0.30.0 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 // indirect nhooyr.io/websocket v1.8.7 // indirect diff --git a/backend/go.sum b/backend/go.sum index 33c68145..c1b14c24 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -6,14 +6,25 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= @@ -24,10 +35,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -49,6 +64,8 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= @@ -80,12 +97,16 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -115,6 +136,14 @@ github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMN github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo= github.com/graph-gophers/graphql-transport-ws v0.0.2/go.mod h1:5BVKvFzOd2BalVIBFfnfmHjpJi/MZ5rOj8G55mXvZ8g= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/gwatts/gin-adapter v1.0.0 h1:TsmmhYTR79/RMTsfYJ2IQvI1F5KZ3ZFJxuQSYEOpyIA= github.com/gwatts/gin-adapter v1.0.0/go.mod h1:44AEV+938HsS0mjfXtBDCUZS9vONlF2gwvh8wu4sRYc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -127,6 +156,8 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -159,6 +190,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -169,12 +202,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= -github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= -github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= -github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= +github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -182,6 +213,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -195,6 +234,10 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sosodev/duration v1.3.0 h1:g3E6mto+hFdA2uZXeNDYff8LYeg7v5D4YKP/Ng/NUkE= github.com/sosodev/duration v1.3.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -225,6 +268,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -235,13 +280,51 @@ github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4= +go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= +go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.etcd.io/etcd/pkg/v3 v3.5.10 h1:WPR8K0e9kWl1gAhB5A7gEa5ZBTNkT9NdNWrR8Qpo1CM= +go.etcd.io/etcd/pkg/v3 v3.5.10/go.mod h1:TKTuCKKcF1zxmfKWDkfz5qqYaE3JncKKZPFf8c1nFUs= +go.etcd.io/etcd/raft/v3 v3.5.10 h1:cgNAYe7xrsrn/5kXMSaH8kM/Ky8mAdMqGOxyYwpP0LA= +go.etcd.io/etcd/raft/v3 v3.5.10/go.mod h1:odD6kr8XQXTy9oQnyMPBOr0TVe+gT0neQhElQ6jbGRc= +go.etcd.io/etcd/server/v3 v3.5.10 h1:4NOGyOwD5sUZ22PiWYKmfxqoeh72z6EhYjNosKGLmZg= +go.etcd.io/etcd/server/v3 v3.5.10/go.mod h1:gBplPHfs6YI0L+RpGkTQO7buDbHv5HJGG/Bst0/zIPo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0 h1:ZOLJc06r4CB42laIXg/7udr0pbZyuAihN10A/XuiQRY= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0/go.mod h1:5z+/ZWJQKXa9YT34fQNx5K8Hd1EoIhvtUygUQPqEOgQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= @@ -315,6 +398,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= @@ -326,6 +417,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -333,20 +426,16 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= -k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= -k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= -k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/apiserver v0.30.0 h1:QCec+U72tMQ+9tR6A0sMBB5Vh6ImCEkoKkTDRABWq6M= k8s.io/apiserver v0.30.0/go.mod h1:smOIBq8t0MbKZi7O7SyIpjPsiKJ8qa+llcFCluKyqiY= -k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= -k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/component-base v0.30.0 h1:cj6bp38g0ainlfYtaOQuRELh5KSYjhKxM+io7AUIk4o= +k8s.io/component-base v0.30.0/go.mod h1:V9x/0ePFNaKeKYA3bOvIbrNoluTSG+fSJKjLdjOoeXQ= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 h1:qVoMaQV5t62UUvHe16Q3eb2c5HPzLHYzsi0Tu/xLndo= @@ -357,6 +446,8 @@ nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 h1:/U5vjBbQn3RChhv7P11uhYvCSm5G2GaIi5AIGBS6r4c= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmGM2dfIiLRfrC6jb5kV2Mq/sK1ZP303cxzkV5Y4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 2acdd024..9c7d745e 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -35,7 +35,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/storage" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/utils/ptr" @@ -106,13 +105,32 @@ type FetchResponse struct { Error error } +// represents multi-namespace continue token +type continueToken struct { + ResourceVersions map[string]string `json:"rv"` + StartKey string `json:"start"` +} + +// encode continue token +func encodeContinue(resourceVersions map[string]string, startKey string) (string, error) { + token := continueToken{ResourceVersions: resourceVersions, StartKey: startKey} + + // json-encoding + tokenBytes, err := json.Marshal(token) + if err != nil { + return "", err + } + + // base64-encoding + return base64.StdEncoding.EncodeToString(tokenBytes), nil +} + // mergeResults func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstructured.UnstructuredList, error) { // loop through results items := []unstructured.Unstructured{} remainingItemCount := int64(0) resourceVersionMap := map[string]string{} - continueMap := map[string]string{} for _, resp := range responses { // exit if any query resulted in error @@ -123,11 +141,10 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr result := resp.Result // metadata - remainingItemCount += *result.GetRemainingItemCount() + remainingItemCount += ptr.Deref(result.GetRemainingItemCount(), 0) resourceVersionMap[resp.Namespace] = result.GetResourceVersion() - continueMap[resp.Namespace] = result.GetContinue() - fmt.Println(storage.DecodeContinue(result.GetContinue(), "")) + //fmt.Println(storage.DecodeContinue(result.GetContinue(), "")) // items items = append(items, result.Items...) @@ -138,10 +155,35 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr return items[i].GetName() < items[j].GetName() }) + // slice items + ignoreCount := int64(len(items) - int(options.Limit)) + if ignoreCount > 0 { + remainingItemCount += ignoreCount + items = items[:options.Limit] + } + + // encode resourceVersionMap + resourceVersionBytes, err := json.Marshal(resourceVersionMap) + if err != nil { + return nil, err + } + resourceVersion := base64.StdEncoding.EncodeToString(resourceVersionBytes) + + // generate continue token + var continueToken string + if len(items) > 0 { + continueToken, err = encodeContinue(resourceVersionMap, items[0].GetName()) + if err != nil { + return nil, err + } + } + // init merged object output := new(unstructured.UnstructuredList) output.SetRemainingItemCount(&remainingItemCount) - output.Items = items[:options.Limit] + output.SetResourceVersion(resourceVersion) + output.SetContinue(continueToken) + output.Items = items return output, nil } @@ -199,10 +241,18 @@ func listResource[T runtime.Object](r *queryResolver, ctx context.Context, names i += 1 } - // merge results - list, err := mergeResults(responses, opts) - if err != nil { - return output, err + // if multiple queries, merge results + var list *unstructured.UnstructuredList + if len(namespaces) > 1 { + list, err = mergeResults(responses, opts) + if err != nil { + return output, err + } + } else { + if responses[0].Error != nil { + return output, responses[0].Error + } + list = responses[0].Result } // de-serialize From ecb66892a59ea579206d26a2bf2f84bc427d8094 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Sat, 11 May 2024 01:48:51 +0300 Subject: [PATCH 10/22] wip --- backend/graph/helpers.go | 149 +++++++++++++++++++++++++++++---------- 1 file changed, 111 insertions(+), 38 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 9c7d745e..9b1a6b8e 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "sort" + "strconv" "strings" "sync" "time" @@ -35,6 +36,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/storage" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/utils/ptr" @@ -121,10 +123,49 @@ func encodeContinue(resourceVersions map[string]string, startKey string) (string return "", err } - // base64-encoding + // base64-encode return base64.StdEncoding.EncodeToString(tokenBytes), nil } +// decode continue token +func decodeContinue(tokenStr string) (map[string]string, error) { + if tokenStr == "" { + return map[string]string{}, nil + } + + // base64-decode + tokenBytes, err := base64.StdEncoding.DecodeString(tokenStr) + if err != nil { + return nil, err + } + + // json-decode + token := &continueToken{} + err = json.Unmarshal(tokenBytes, token) + if err != nil { + return nil, err + } + + // generate continue tokens + continueMap := map[string]string{} + for namespace, rvStr := range token.ResourceVersions { + rvInt64, err := strconv.ParseInt(rvStr, 10, 64) + if err != nil { + return nil, err + } + + continueStr, err := storage.EncodeContinue("/"+token.StartKey, "/", rvInt64) + if err != nil { + return nil, err + } + continueMap[namespace] = continueStr + } + + fmt.Println(token.ResourceVersions) + fmt.Println(continueMap) + return continueMap, nil +} + // mergeResults func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstructured.UnstructuredList, error) { // loop through results @@ -172,7 +213,7 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr // generate continue token var continueToken string if len(items) > 0 { - continueToken, err = encodeContinue(resourceVersionMap, items[0].GetName()) + continueToken, err = encodeContinue(resourceVersionMap, items[len(items)-1].GetName()) if err != nil { return nil, err } @@ -188,46 +229,59 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr return output, nil } -// fetchListResource -func fetchListResource[T runtime.Object](ctx context.Context, client dynamic.NamespaceableResourceInterface, namespace string, options metav1.ListOptions, wg *sync.WaitGroup, ch chan<- FetchResponse) { - defer wg.Done() - - list, err := client.Namespace(namespace).List(ctx, options) - if err != nil { - ch <- FetchResponse{Error: err} - return - } - - ch <- FetchResponse{Namespace: namespace, Result: list} -} - -// listResource -func listResource[T runtime.Object](r *queryResolver, ctx context.Context, namespace *string, options *metav1.ListOptions) (T, error) { +// listResourceSingle +func listResourceSingle[T runtime.Object](ctx context.Context, client dynamic.NamespaceableResourceInterface, namespace string, options metav1.ListOptions) (T, error) { var output T - // init client - gvr, err := getGVR(output) + list, err := client.Namespace(namespace).List(ctx, options) if err != nil { return output, err } - client := r.K8SDynamicClient(ctx).Resource(gvr) - // init namespaces - namespaces, err := r.ToNamespaces(namespace) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) if err != nil { return output, err } - // init list options - opts := toListOptions(options) + return output, nil +} - // execute requests in parallel +// listResourceMulti +func listResourceMulti[T runtime.Object](ctx context.Context, client dynamic.NamespaceableResourceInterface, namespaces []string, options metav1.ListOptions) (T, error) { + var output T var wg sync.WaitGroup ch := make(chan FetchResponse, len(namespaces)) + // decode continue token + continueMap, err := decodeContinue(options.Continue) + if err != nil { + return output, err + } + + // execute queries for _, namespace := range namespaces { wg.Add(1) - go fetchListResource[T](ctx, client, namespace, opts, &wg, ch) + go func(namespace string) { + defer wg.Done() + + thisOpts := options + thisContinue, exists := continueMap[namespace] + if exists { + thisOpts.Continue = thisContinue + } else { + thisOpts.Continue = "" + } + + fmt.Println(thisOpts) + + list, err := client.Namespace(namespace).List(ctx, thisOpts) + if err != nil { + ch <- FetchResponse{Error: err} + return + } + + ch <- FetchResponse{Namespace: namespace, Result: list} + }(namespace) } wg.Wait() @@ -241,18 +295,10 @@ func listResource[T runtime.Object](r *queryResolver, ctx context.Context, names i += 1 } - // if multiple queries, merge results - var list *unstructured.UnstructuredList - if len(namespaces) > 1 { - list, err = mergeResults(responses, opts) - if err != nil { - return output, err - } - } else { - if responses[0].Error != nil { - return output, responses[0].Error - } - list = responses[0].Result + // merge results + list, err := mergeResults(responses, options) + if err != nil { + return output, err } // de-serialize @@ -264,6 +310,33 @@ func listResource[T runtime.Object](r *queryResolver, ctx context.Context, names return output, nil } +// listResource +func listResource[T runtime.Object](r *queryResolver, ctx context.Context, namespace *string, options *metav1.ListOptions) (T, error) { + var output T + + // init client + gvr, err := getGVR(output) + if err != nil { + return output, err + } + client := r.K8SDynamicClient(ctx).Resource(gvr) + + // init namespaces + namespaces, err := r.ToNamespaces(namespace) + if err != nil { + return output, err + } + + // init list options + opts := toListOptions(options) + + if len(namespaces) == 1 { + return listResourceSingle[T](ctx, client, namespaces[0], opts) + } else { + return listResourceMulti[T](ctx, client, namespaces, opts) + } +} + // watchEventProxyChannel func watchEventProxyChannel(ctx context.Context, watchAPI watch.Interface) <-chan *watch.Event { evCh := watchAPI.ResultChan() From ad7b82a2d5421f947d67e874c42ad90a90f0083c Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Sun, 12 May 2024 02:39:01 +0300 Subject: [PATCH 11/22] wip --- backend/graph/helpers.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 9b1a6b8e..d6b90615 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -114,7 +114,7 @@ type continueToken struct { } // encode continue token -func encodeContinue(resourceVersions map[string]string, startKey string) (string, error) { +func encodeContinueMulti(resourceVersions map[string]string, startKey string) (string, error) { token := continueToken{ResourceVersions: resourceVersions, StartKey: startKey} // json-encoding @@ -128,7 +128,7 @@ func encodeContinue(resourceVersions map[string]string, startKey string) (string } // decode continue token -func decodeContinue(tokenStr string) (map[string]string, error) { +func decodeContinueMulti(tokenStr string) (map[string]string, error) { if tokenStr == "" { return map[string]string{}, nil } @@ -154,15 +154,13 @@ func decodeContinue(tokenStr string) (map[string]string, error) { return nil, err } - continueStr, err := storage.EncodeContinue("/"+token.StartKey, "/", rvInt64) + continueStr, err := storage.EncodeContinue("/"+token.StartKey+"\u0000", "/", rvInt64) if err != nil { return nil, err } continueMap[namespace] = continueStr } - fmt.Println(token.ResourceVersions) - fmt.Println(continueMap) return continueMap, nil } @@ -185,8 +183,6 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr remainingItemCount += ptr.Deref(result.GetRemainingItemCount(), 0) resourceVersionMap[resp.Namespace] = result.GetResourceVersion() - //fmt.Println(storage.DecodeContinue(result.GetContinue(), "")) - // items items = append(items, result.Items...) } @@ -213,7 +209,7 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr // generate continue token var continueToken string if len(items) > 0 { - continueToken, err = encodeContinue(resourceVersionMap, items[len(items)-1].GetName()) + continueToken, err = encodeContinueMulti(resourceVersionMap, items[len(items)-1].GetName()) if err != nil { return nil, err } @@ -253,7 +249,7 @@ func listResourceMulti[T runtime.Object](ctx context.Context, client dynamic.Nam ch := make(chan FetchResponse, len(namespaces)) // decode continue token - continueMap, err := decodeContinue(options.Continue) + continueMap, err := decodeContinueMulti(options.Continue) if err != nil { return output, err } @@ -265,6 +261,7 @@ func listResourceMulti[T runtime.Object](ctx context.Context, client dynamic.Nam defer wg.Done() thisOpts := options + thisContinue, exists := continueMap[namespace] if exists { thisOpts.Continue = thisContinue @@ -272,8 +269,6 @@ func listResourceMulti[T runtime.Object](ctx context.Context, client dynamic.Nam thisOpts.Continue = "" } - fmt.Println(thisOpts) - list, err := client.Namespace(namespace).List(ctx, thisOpts) if err != nil { ch <- FetchResponse{Error: err} From 264980b3dd6d193710f66dc5707a845306c5c5ea Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Sun, 12 May 2024 03:01:50 +0300 Subject: [PATCH 12/22] wip --- backend/graph/helpers.go | 53 +++++++++++++--------------------------- 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index d6b90615..4ce072d0 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -225,33 +225,15 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr return output, nil } -// listResourceSingle -func listResourceSingle[T runtime.Object](ctx context.Context, client dynamic.NamespaceableResourceInterface, namespace string, options metav1.ListOptions) (T, error) { - var output T - - list, err := client.Namespace(namespace).List(ctx, options) - if err != nil { - return output, err - } - - err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) - if err != nil { - return output, err - } - - return output, nil -} - // listResourceMulti -func listResourceMulti[T runtime.Object](ctx context.Context, client dynamic.NamespaceableResourceInterface, namespaces []string, options metav1.ListOptions) (T, error) { - var output T +func listResourceMulti(ctx context.Context, client dynamic.NamespaceableResourceInterface, namespaces []string, options metav1.ListOptions) (*unstructured.UnstructuredList, error) { var wg sync.WaitGroup ch := make(chan FetchResponse, len(namespaces)) // decode continue token continueMap, err := decodeContinueMulti(options.Continue) if err != nil { - return output, err + return nil, err } // execute queries @@ -291,18 +273,7 @@ func listResourceMulti[T runtime.Object](ctx context.Context, client dynamic.Nam } // merge results - list, err := mergeResults(responses, options) - if err != nil { - return output, err - } - - // de-serialize - err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) - if err != nil { - return output, err - } - - return output, nil + return mergeResults(responses, options) } // listResource @@ -325,11 +296,21 @@ func listResource[T runtime.Object](r *queryResolver, ctx context.Context, names // init list options opts := toListOptions(options) - if len(namespaces) == 1 { - return listResourceSingle[T](ctx, client, namespaces[0], opts) - } else { - return listResourceMulti[T](ctx, client, namespaces, opts) + // execute requests + list, err := func() (*unstructured.UnstructuredList, error) { + if len(namespaces) == 1 { + return client.Namespace(namespaces[0]).List(ctx, opts) + } else { + return listResourceMulti(ctx, client, namespaces, opts) + } + }() + if err != nil { + return output, err } + + // return de-serialized object + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) + return output, err } // watchEventProxyChannel From e87cdfa98617b9bab2f1622e23f763e6932e3e0d Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Mon, 13 May 2024 16:59:15 +0300 Subject: [PATCH 13/22] wip --- backend/graph/helpers.go | 28 +++++++++--------- backend/graph/resolver.go | 11 +++++++ backend/graph/schema.resolvers.go | 7 +++-- backend/graph_test/query_resolver_test.go | 10 +++---- backend/graph_test/testutils_test.go | 35 +++++++++++++++++++++++ 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 4ce072d0..e5d14ebb 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -90,9 +90,11 @@ type FollowArgs struct { Since string } -// getGVR -func getGVR(obj runtime.Object) (schema.GroupVersionResource, error) { +// GetGVR +func GetGVR(obj runtime.Object) (schema.GroupVersionResource, error) { switch (obj).(type) { + case *corev1.Pod: + return schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, nil case *corev1.PodList: return schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, nil default: @@ -108,14 +110,14 @@ type FetchResponse struct { } // represents multi-namespace continue token -type continueToken struct { +type continueMultiToken struct { ResourceVersions map[string]string `json:"rv"` StartKey string `json:"start"` } // encode continue token func encodeContinueMulti(resourceVersions map[string]string, startKey string) (string, error) { - token := continueToken{ResourceVersions: resourceVersions, StartKey: startKey} + token := continueMultiToken{ResourceVersions: resourceVersions, StartKey: startKey} // json-encoding tokenBytes, err := json.Marshal(token) @@ -140,7 +142,7 @@ func decodeContinueMulti(tokenStr string) (map[string]string, error) { } // json-decode - token := &continueToken{} + token := &continueMultiToken{} err = json.Unmarshal(tokenBytes, token) if err != nil { return nil, err @@ -277,20 +279,19 @@ func listResourceMulti(ctx context.Context, client dynamic.NamespaceableResource } // listResource -func listResource[T runtime.Object](r *queryResolver, ctx context.Context, namespace *string, options *metav1.ListOptions) (T, error) { - var output T - +func listResource(r *queryResolver, ctx context.Context, namespace *string, options *metav1.ListOptions, modelPtr runtime.Object) error { // init client - gvr, err := getGVR(output) + gvr, err := GetGVR(modelPtr) if err != nil { - return output, err + return err } + client := r.K8SDynamicClient(ctx).Resource(gvr) // init namespaces namespaces, err := r.ToNamespaces(namespace) if err != nil { - return output, err + return err } // init list options @@ -305,12 +306,11 @@ func listResource[T runtime.Object](r *queryResolver, ctx context.Context, names } }() if err != nil { - return output, err + return err } // return de-serialized object - err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), &output) - return output, err + return runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), modelPtr) } // watchEventProxyChannel diff --git a/backend/graph/resolver.go b/backend/graph/resolver.go index 82d1dfc9..c0787a26 100644 --- a/backend/graph/resolver.go +++ b/backend/graph/resolver.go @@ -18,10 +18,12 @@ import ( //"os" "context" + "fmt" "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/dynamic" + dynamicFake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/rest" @@ -37,6 +39,7 @@ type Resolver struct { k8sCfg *rest.Config allowedNamespaces []string TestClientset *fake.Clientset + TestDynamicClient *dynamicFake.FakeDynamicClient } func (r *Resolver) K8SClientset(ctx context.Context) kubernetes.Interface { @@ -63,9 +66,15 @@ func (r *Resolver) K8SClientset(ctx context.Context) kubernetes.Interface { } func (r *Resolver) K8SDynamicClient(ctx context.Context) dynamic.Interface { + if r.TestDynamicClient != nil { + return r.TestDynamicClient + } + // copy config cfg := rest.CopyConfig(r.k8sCfg) + fmt.Println("111") + // get token from context token, ok := ctx.Value(K8STokenCtxKey).(string) if ok { @@ -73,6 +82,8 @@ func (r *Resolver) K8SDynamicClient(ctx context.Context) dynamic.Interface { cfg.BearerTokenFile = "" } + fmt.Println("222") + dynamicClient, err := dynamic.NewForConfig(cfg) if err != nil { panic(err) diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index 43c7d8b9..02491546 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -158,8 +158,11 @@ func (r *queryResolver) CoreV1PodsGet(ctx context.Context, namespace *string, na // CoreV1PodsList is the resolver for the coreV1PodsList field. func (r *queryResolver) CoreV1PodsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*corev1.PodList, error) { - //return r.K8SClientset(ctx).CoreV1().Pods(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) - return listResource[*corev1.PodList](r, ctx, namespace, options) + podList := &corev1.PodList{} + if err := listResource(r, ctx, namespace, options, podList); err != nil { + return nil, err + } + return podList, nil } // CoreV1PodsGetLogs is the resolver for the coreV1PodsGetLogs field. diff --git a/backend/graph_test/query_resolver_test.go b/backend/graph_test/query_resolver_test.go index 84759eb7..07f9593d 100644 --- a/backend/graph_test/query_resolver_test.go +++ b/backend/graph_test/query_resolver_test.go @@ -777,11 +777,11 @@ func (suite *QueryResolverTestSuite) TestCoreV1PodsList() { } // add data - obj1 := corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "x1"}} - suite.resolver.TestClientset.CoreV1().Pods("ns").Create(context.Background(), &obj1, metav1.CreateOptions{}) - - obj2 := corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "x2"}} - suite.resolver.TestClientset.CoreV1().Pods("ns").Create(context.Background(), &obj2, metav1.CreateOptions{}) + suite.PopulateDynamicClient( + "ns", + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "x1"}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "x2"}}, + ) // check not empty { diff --git a/backend/graph_test/testutils_test.go b/backend/graph_test/testutils_test.go index c57b1f25..68ec6993 100644 --- a/backend/graph_test/testutils_test.go +++ b/backend/graph_test/testutils_test.go @@ -33,6 +33,11 @@ import ( "github.com/rs/zerolog/log" "github.com/stretchr/testify/suite" "github.com/vektah/gqlparser/v2/gqlerror" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + dynamicFake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/fake" "github.com/kubetail-org/kubetail/graph" @@ -82,6 +87,36 @@ func (suite *GraphTestSuite) TearDownSuite() { func (suite *GraphTestSuite) SetupTest() { // init fake clientset suite.resolver.TestClientset = fake.NewSimpleClientset() + + // init fake dynamic client + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + panic(err) + } + suite.resolver.TestDynamicClient = dynamicFake.NewSimpleDynamicClient(scheme) +} + +func (suite *GraphTestSuite) PopulateDynamicClient(ns string, objects ...runtime.Object) { + for _, obj := range objects { + // get gvr + gvr, err := graph.GetGVR(obj) + if err != nil { + panic(err) + } + + // initialize unstructured object + unstrObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + panic(err) + } + x := unstructured.Unstructured{Object: unstrObj} + + // create + _, err = suite.resolver.TestDynamicClient.Resource(gvr).Namespace(ns).Create(context.Background(), &x, metav1.CreateOptions{}) + if err != nil { + panic(err) + } + } } func (suite *GraphTestSuite) Post(request GraphQLRequest, prepareContext PrepareContextFunc) (*http.Response, error) { From 2274eea7eee036d2ac9d6c5610673e4f59bd83ef Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Mon, 13 May 2024 17:45:17 +0300 Subject: [PATCH 14/22] wip --- backend/graph/helpers.go | 18 +++++-- backend/graph/schema.resolvers.go | 36 +++++++++++--- backend/graph_test/query_resolver_test.go | 60 +++++++++++------------ backend/graph_test/testutils_test.go | 8 +++ 4 files changed, 83 insertions(+), 39 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index e5d14ebb..c15eb704 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -30,6 +30,8 @@ import ( "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/sosodev/duration" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -93,9 +95,19 @@ type FollowArgs struct { // GetGVR func GetGVR(obj runtime.Object) (schema.GroupVersionResource, error) { switch (obj).(type) { - case *corev1.Pod: - return schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, nil - case *corev1.PodList: + case *appsv1.DaemonSet, *appsv1.DaemonSetList: + return schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"}, nil + case *appsv1.Deployment, *appsv1.DeploymentList: + return schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, nil + case *appsv1.ReplicaSet, *appsv1.ReplicaSetList: + return schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"}, nil + case *appsv1.StatefulSet, *appsv1.StatefulSetList: + return schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"}, nil + case *batchv1.Job, *batchv1.JobList: + return schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"}, nil + case *batchv1.CronJob, *batchv1.CronJobList: + return schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"}, nil + case *corev1.Pod, *corev1.PodList: return schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, nil default: return schema.GroupVersionResource{}, fmt.Errorf("not implemented: %T", obj) diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index 02491546..2bd7632c 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -72,7 +72,11 @@ func (r *queryResolver) AppsV1DaemonSetsGet(ctx context.Context, name string, na // AppsV1DaemonSetsList is the resolver for the appsV1DaemonSetsList field. func (r *queryResolver) AppsV1DaemonSetsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*appsv1.DaemonSetList, error) { - return r.K8SClientset(ctx).AppsV1().DaemonSets(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + daemonSetList := &appsv1.DaemonSetList{} + if err := listResource(r, ctx, namespace, options, daemonSetList); err != nil { + return nil, err + } + return daemonSetList, nil } // AppsV1DeploymentsGet is the resolver for the appsV1DeploymentsGet field. @@ -82,7 +86,11 @@ func (r *queryResolver) AppsV1DeploymentsGet(ctx context.Context, name string, n // AppsV1DeploymentsList is the resolver for the appsV1DeploymentsList field. func (r *queryResolver) AppsV1DeploymentsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*appsv1.DeploymentList, error) { - return r.K8SClientset(ctx).AppsV1().Deployments(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + deploymentList := &appsv1.DeploymentList{} + if err := listResource(r, ctx, namespace, options, deploymentList); err != nil { + return nil, err + } + return deploymentList, nil } // AppsV1ReplicaSetsGet is the resolver for the appsV1ReplicaSetsGet field. @@ -92,7 +100,11 @@ func (r *queryResolver) AppsV1ReplicaSetsGet(ctx context.Context, name string, n // AppsV1ReplicaSetsList is the resolver for the appsV1ReplicaSetsList field. func (r *queryResolver) AppsV1ReplicaSetsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*appsv1.ReplicaSetList, error) { - return r.K8SClientset(ctx).AppsV1().ReplicaSets(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + replicaSetList := &appsv1.ReplicaSetList{} + if err := listResource(r, ctx, namespace, options, replicaSetList); err != nil { + return nil, err + } + return replicaSetList, nil } // AppsV1StatefulSetsGet is the resolver for the appsV1StatefulSetsGet field. @@ -102,7 +114,11 @@ func (r *queryResolver) AppsV1StatefulSetsGet(ctx context.Context, name string, // AppsV1StatefulSetsList is the resolver for the appsV1StatefulSetsList field. func (r *queryResolver) AppsV1StatefulSetsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*appsv1.StatefulSetList, error) { - return r.K8SClientset(ctx).AppsV1().StatefulSets(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + statefulSetList := &appsv1.StatefulSetList{} + if err := listResource(r, ctx, namespace, options, statefulSetList); err != nil { + return nil, err + } + return statefulSetList, nil } // BatchV1CronJobsGet is the resolver for the batchV1CronJobsGet field. @@ -112,7 +128,11 @@ func (r *queryResolver) BatchV1CronJobsGet(ctx context.Context, name string, nam // BatchV1CronJobsList is the resolver for the batchV1CronJobsList field. func (r *queryResolver) BatchV1CronJobsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*batchv1.CronJobList, error) { - return r.K8SClientset(ctx).BatchV1().CronJobs(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + cronJobList := &batchv1.CronJobList{} + if err := listResource(r, ctx, namespace, options, cronJobList); err != nil { + return nil, err + } + return cronJobList, nil } // BatchV1JobsGet is the resolver for the batchV1JobsGet field. @@ -122,7 +142,11 @@ func (r *queryResolver) BatchV1JobsGet(ctx context.Context, name string, namespa // BatchV1JobsList is the resolver for the batchV1JobsList field. func (r *queryResolver) BatchV1JobsList(ctx context.Context, namespace *string, options *metav1.ListOptions) (*batchv1.JobList, error) { - return r.K8SClientset(ctx).BatchV1().Jobs(r.ToNamespace(namespace)).List(ctx, toListOptions(options)) + jobList := &batchv1.JobList{} + if err := listResource(r, ctx, namespace, options, jobList); err != nil { + return nil, err + } + return jobList, nil } // CoreV1NamespacesList is the resolver for the coreV1NamespacesList field. diff --git a/backend/graph_test/query_resolver_test.go b/backend/graph_test/query_resolver_test.go index 07f9593d..e09913ae 100644 --- a/backend/graph_test/query_resolver_test.go +++ b/backend/graph_test/query_resolver_test.go @@ -105,11 +105,11 @@ func (suite *QueryResolverTestSuite) TestAppsV1DaemonSetsList() { } // add data - obj1 := appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: "x1"}} - suite.resolver.TestClientset.AppsV1().DaemonSets("ns").Create(context.Background(), &obj1, metav1.CreateOptions{}) - - obj2 := appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: "x2"}} - suite.resolver.TestClientset.AppsV1().DaemonSets("ns").Create(context.Background(), &obj2, metav1.CreateOptions{}) + suite.PopulateDynamicClient( + "ns", + &appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: "x1"}}, + &appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: "x2"}}, + ) // check not empty { @@ -199,11 +199,11 @@ func (suite *QueryResolverTestSuite) TestAppsV1DeploymentsList() { } // add data - obj1 := appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "x1"}} - suite.resolver.TestClientset.AppsV1().Deployments("ns").Create(context.Background(), &obj1, metav1.CreateOptions{}) - - obj2 := appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "x2"}} - suite.resolver.TestClientset.AppsV1().Deployments("ns").Create(context.Background(), &obj2, metav1.CreateOptions{}) + suite.PopulateDynamicClient( + "ns", + &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "x1"}}, + &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "x2"}}, + ) // check not empty { @@ -293,11 +293,11 @@ func (suite *QueryResolverTestSuite) TestAppsV1ReplicaSetsList() { } // add data - obj1 := appsv1.ReplicaSet{ObjectMeta: metav1.ObjectMeta{Name: "x1"}} - suite.resolver.TestClientset.AppsV1().ReplicaSets("ns").Create(context.Background(), &obj1, metav1.CreateOptions{}) - - obj2 := appsv1.ReplicaSet{ObjectMeta: metav1.ObjectMeta{Name: "x2"}} - suite.resolver.TestClientset.AppsV1().ReplicaSets("ns").Create(context.Background(), &obj2, metav1.CreateOptions{}) + suite.PopulateDynamicClient( + "ns", + &appsv1.ReplicaSet{ObjectMeta: metav1.ObjectMeta{Name: "x1"}}, + &appsv1.ReplicaSet{ObjectMeta: metav1.ObjectMeta{Name: "x2"}}, + ) // check not empty { @@ -387,11 +387,11 @@ func (suite *QueryResolverTestSuite) TestAppsV1StatefulSetsList() { } // add data - obj1 := appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "x1"}} - suite.resolver.TestClientset.AppsV1().StatefulSets("ns").Create(context.Background(), &obj1, metav1.CreateOptions{}) - - obj2 := appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "x2"}} - suite.resolver.TestClientset.AppsV1().StatefulSets("ns").Create(context.Background(), &obj2, metav1.CreateOptions{}) + suite.PopulateDynamicClient( + "ns", + &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "x1"}}, + &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "x2"}}, + ) // check not empty { @@ -481,11 +481,11 @@ func (suite *QueryResolverTestSuite) TestBatchV1CronJobsList() { } // add data - obj1 := batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Name: "x1"}} - suite.resolver.TestClientset.BatchV1().CronJobs("ns").Create(context.Background(), &obj1, metav1.CreateOptions{}) - - obj2 := batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Name: "x2"}} - suite.resolver.TestClientset.BatchV1().CronJobs("ns").Create(context.Background(), &obj2, metav1.CreateOptions{}) + suite.PopulateDynamicClient( + "ns", + &batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Name: "x1"}}, + &batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{Name: "x2"}}, + ) // check not empty { @@ -575,11 +575,11 @@ func (suite *QueryResolverTestSuite) TestBatchV1JobsList() { } // add data - obj1 := batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "x1"}} - suite.resolver.TestClientset.BatchV1().Jobs("ns").Create(context.Background(), &obj1, metav1.CreateOptions{}) - - obj2 := batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "x2"}} - suite.resolver.TestClientset.BatchV1().Jobs("ns").Create(context.Background(), &obj2, metav1.CreateOptions{}) + suite.PopulateDynamicClient( + "ns", + &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "x1"}}, + &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "x2"}}, + ) // check not empty { diff --git a/backend/graph_test/testutils_test.go b/backend/graph_test/testutils_test.go index 68ec6993..5c04a6be 100644 --- a/backend/graph_test/testutils_test.go +++ b/backend/graph_test/testutils_test.go @@ -33,6 +33,8 @@ import ( "github.com/rs/zerolog/log" "github.com/stretchr/testify/suite" "github.com/vektah/gqlparser/v2/gqlerror" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -90,6 +92,12 @@ func (suite *GraphTestSuite) SetupTest() { // init fake dynamic client scheme := runtime.NewScheme() + if err := appsv1.AddToScheme(scheme); err != nil { + panic(err) + } + if err := batchv1.AddToScheme(scheme); err != nil { + panic(err) + } if err := corev1.AddToScheme(scheme); err != nil { panic(err) } From f6e2e4ecee18a04c40593f65147f98eacb55d7b8 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Tue, 14 May 2024 07:59:39 +0300 Subject: [PATCH 15/22] wip --- backend/graph/helpers_test.go | 77 +++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 backend/graph/helpers_test.go diff --git a/backend/graph/helpers_test.go b/backend/graph/helpers_test.go new file mode 100644 index 00000000..f1e7d97e --- /dev/null +++ b/backend/graph/helpers_test.go @@ -0,0 +1,77 @@ +package graph + +import ( + "testing" + + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/storage" +) + +func TestGetGVRSuccess(t *testing.T) { + newGVR := func(group, version, resource string) schema.GroupVersionResource { + return schema.GroupVersionResource{Group: group, Version: version, Resource: resource} + } + + tests := []struct { + name string + object runtime.Object + wantGVR schema.GroupVersionResource + }{ + {"CronJob", &batchv1.CronJob{}, newGVR("batch", "v1", "cronjobs")}, + {"CronJobList", &batchv1.CronJobList{}, newGVR("batch", "v1", "cronjobs")}, + {"DaemonSet", &appsv1.DaemonSet{}, newGVR("apps", "v1", "daemonsets")}, + {"DaemonSetList", &appsv1.DaemonSetList{}, newGVR("apps", "v1", "daemonsets")}, + {"Deployment", &appsv1.Deployment{}, newGVR("apps", "v1", "deployments")}, + {"DeploymentList", &appsv1.DeploymentList{}, newGVR("apps", "v1", "deployments")}, + {"Job", &batchv1.Job{}, newGVR("batch", "v1", "jobs")}, + {"JobList", &batchv1.JobList{}, newGVR("batch", "v1", "jobs")}, + {"Pod", &corev1.Pod{}, newGVR("", "v1", "pods")}, + {"PodList", &corev1.PodList{}, newGVR("", "v1", "pods")}, + {"ReplicaSet", &appsv1.ReplicaSet{}, newGVR("apps", "v1", "replicasets")}, + {"ReplicaSetList", &appsv1.ReplicaSetList{}, newGVR("apps", "v1", "replicasets")}, + {"StatefulSet", &appsv1.StatefulSet{}, newGVR("apps", "v1", "statefulsets")}, + {"StatefulSetList", &appsv1.StatefulSetList{}, newGVR("apps", "v1", "statefulsets")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gvr, err := GetGVR(tt.object) + assert.Nil(t, err) + assert.Equal(t, gvr, tt.wantGVR) + }) + } +} + +func TestContinueMulti(t *testing.T) { + rv := map[string]string{ + "ns1": "1000", + "ns2": "2000", + } + + startKey := "xxx" + + // create continue-multi token + continueMultiToken, err := encodeContinueMulti(rv, startKey) + assert.Nil(t, err) + + // decode continue-multi token into map of k8s continue tokens + continueMap, err := decodeContinueMulti(continueMultiToken) + assert.Nil(t, err) + + // check token 1 + c1, _ := storage.EncodeContinue("/"+startKey+"\u0000", "/", 1000) + assert.Equal(t, continueMap["ns1"], c1) + + // check token 2 + c2, _ := storage.EncodeContinue("/"+startKey+"\u0000", "/", 2000) + assert.Equal(t, continueMap["ns2"], c2) +} + +func TestMergeResults(t *testing.T) { + fetchResponses := +} \ No newline at end of file From e8b18e99fef96e3d065bc375e71b75c1cccdaedb Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Tue, 14 May 2024 12:50:24 +0300 Subject: [PATCH 16/22] wip --- backend/graph/helpers.go | 2 +- backend/graph/helpers_test.go | 213 ++++++++++++++++++++++++++++++- backend/graph/resolver.go | 7 - backend/graph/resolver_test.go | 225 +++++++++++++++++++++++++++++++++ 4 files changed, 436 insertions(+), 11 deletions(-) create mode 100644 backend/graph/resolver_test.go diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index c15eb704..5cd25b3b 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -222,7 +222,7 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr // generate continue token var continueToken string - if len(items) > 0 { + if len(items) > 0 && remainingItemCount > 0 { continueToken, err = encodeContinueMulti(resourceVersionMap, items[len(items)-1].GetName()) if err != nil { return nil, err diff --git a/backend/graph/helpers_test.go b/backend/graph/helpers_test.go index f1e7d97e..26b00338 100644 --- a/backend/graph/helpers_test.go +++ b/backend/graph/helpers_test.go @@ -1,15 +1,33 @@ +// Copyright 2024 Andres Morey +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package graph import ( + "strconv" "testing" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/storage" + "k8s.io/utils/ptr" ) func TestGetGVRSuccess(t *testing.T) { @@ -72,6 +90,195 @@ func TestContinueMulti(t *testing.T) { assert.Equal(t, continueMap["ns2"], c2) } -func TestMergeResults(t *testing.T) { - fetchResponses := -} \ No newline at end of file +func TestMergeResultsSuccess(t *testing.T) { + tests := []struct { + name string + results []corev1.PodList + listOptions metav1.ListOptions + wantMerged corev1.PodList + }{ + { + "no items", + []corev1.PodList{ + { + ListMeta: metav1.ListMeta{ResourceVersion: "1", RemainingItemCount: ptr.To[int64](0)}, + Items: []corev1.Pod{}, + }, + { + ListMeta: metav1.ListMeta{ResourceVersion: "2", RemainingItemCount: ptr.To[int64](0)}, + Items: []corev1.Pod{}, + }, + }, + metav1.ListOptions{Limit: 10}, + corev1.PodList{ + ListMeta: metav1.ListMeta{ + ResourceVersion: "eyJuczAiOiIxIiwibnMxIjoiMiJ9", + RemainingItemCount: ptr.To[int64](0), + Continue: "", + }, + Items: []corev1.Pod{}, + }, + }, + { + "num items less than limit", + []corev1.PodList{ + { + ListMeta: metav1.ListMeta{ResourceVersion: "1", RemainingItemCount: ptr.To[int64](0)}, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-1-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-1-2"}}, + }, + }, + { + ListMeta: metav1.ListMeta{ResourceVersion: "2", RemainingItemCount: ptr.To[int64](0)}, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-2-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-2-2"}}, + }, + }, + }, + metav1.ListOptions{Limit: 10}, + corev1.PodList{ + ListMeta: metav1.ListMeta{ + ResourceVersion: "eyJuczAiOiIxIiwibnMxIjoiMiJ9", + RemainingItemCount: ptr.To[int64](0), + Continue: "", + }, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-1-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-1-2"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-2-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-2-2"}}, + }, + }, + }, + { + "num items more than limit", + []corev1.PodList{ + { + ListMeta: metav1.ListMeta{ResourceVersion: "1", RemainingItemCount: ptr.To[int64](0)}, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-1-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-1-2"}}, + }, + }, + { + ListMeta: metav1.ListMeta{ResourceVersion: "2", RemainingItemCount: ptr.To[int64](0)}, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-2-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-2-2"}}, + }, + }, + }, + metav1.ListOptions{Limit: 3}, + corev1.PodList{ + ListMeta: metav1.ListMeta{ + ResourceVersion: "eyJuczAiOiIxIiwibnMxIjoiMiJ9", + RemainingItemCount: ptr.To[int64](1), + Continue: "eyJydiI6eyJuczAiOiIxIiwibnMxIjoiMiJ9LCJzdGFydCI6Iml0ZW0tMi0xIn0=", + }, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-1-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-1-2"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-2-1"}}, + }, + }, + }, + { + "items with mixed sorting", + []corev1.PodList{ + { + ListMeta: metav1.ListMeta{ResourceVersion: "1", RemainingItemCount: ptr.To[int64](0)}, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-A"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-C"}}, + }, + }, + { + ListMeta: metav1.ListMeta{ResourceVersion: "2", RemainingItemCount: ptr.To[int64](0)}, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-B"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-D"}}, + }, + }, + }, + metav1.ListOptions{Limit: 3}, + corev1.PodList{ + ListMeta: metav1.ListMeta{ + ResourceVersion: "eyJuczAiOiIxIiwibnMxIjoiMiJ9", + RemainingItemCount: ptr.To[int64](1), + Continue: "eyJydiI6eyJuczAiOiIxIiwibnMxIjoiMiJ9LCJzdGFydCI6Iml0ZW0tQyJ9", + }, + Items: []corev1.Pod{ + {ObjectMeta: metav1.ObjectMeta{Name: "item-A"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-B"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "item-C"}}, + }, + }, + }, + } + + for _, tt := range tests { + fetchResponses := []FetchResponse{} + + // build response objects + for i, result := range tt.results { + // convert object + rObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&result) + assert.Nil(t, err) + + // convert items + items := []unstructured.Unstructured{} + for _, item := range result.Items { + itemObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&item) + assert.Nil(t, err) + items = append(items, unstructured.Unstructured{Object: itemObj}) + } + + resp := FetchResponse{ + Namespace: "ns" + strconv.Itoa(i), + Result: &unstructured.UnstructuredList{Object: rObj, Items: items}, + } + + fetchResponses = append(fetchResponses, resp) + } + + // merge results + mergedResultObj, err := mergeResults(fetchResponses, tt.listOptions) + assert.Nil(t, err) + + // check result + mergedResult := corev1.PodList{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(mergedResultObj.UnstructuredContent(), &mergedResult) + assert.Nil(t, err) + + // check metadata + assert.Equal(t, tt.wantMerged.ResourceVersion, mergedResult.ResourceVersion) + assert.Equal(t, tt.wantMerged.RemainingItemCount, mergedResult.RemainingItemCount) + assert.Equal(t, tt.wantMerged.Continue, mergedResult.Continue) + + // check number of items returned + assert.Equal(t, len(tt.wantMerged.Items), len(mergedResult.Items)) + + // check order + for i, wantItem := range tt.wantMerged.Items { + assert.Equal(t, wantItem.Name, mergedResult.Items[i].Name) + } + } +} + +func TestMergeResultsError(t *testing.T) { + // build first result + r1 := corev1.PodList{} + r1Obj, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(&r1) + + // build fetch responses + fetchResponses := []FetchResponse{ + {Namespace: "ns1", Result: &unstructured.UnstructuredList{Object: r1Obj}}, + {Namespace: "ns2", Error: ErrForbidden}, + } + + // merge results + _, err := mergeResults(fetchResponses, metav1.ListOptions{}) + assert.NotNil(t, err) +} diff --git a/backend/graph/resolver.go b/backend/graph/resolver.go index c0787a26..9a646e47 100644 --- a/backend/graph/resolver.go +++ b/backend/graph/resolver.go @@ -15,10 +15,7 @@ package graph import ( - //"os" - "context" - "fmt" "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -73,8 +70,6 @@ func (r *Resolver) K8SDynamicClient(ctx context.Context) dynamic.Interface { // copy config cfg := rest.CopyConfig(r.k8sCfg) - fmt.Println("111") - // get token from context token, ok := ctx.Value(K8STokenCtxKey).(string) if ok { @@ -82,8 +77,6 @@ func (r *Resolver) K8SDynamicClient(ctx context.Context) dynamic.Interface { cfg.BearerTokenFile = "" } - fmt.Println("222") - dynamicClient, err := dynamic.NewForConfig(cfg) if err != nil { panic(err) diff --git a/backend/graph/resolver_test.go b/backend/graph/resolver_test.go new file mode 100644 index 00000000..2c54b699 --- /dev/null +++ b/backend/graph/resolver_test.go @@ -0,0 +1,225 @@ +// Copyright 2024 Andres Morey +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package graph + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/utils/ptr" +) + +func TestToNamespaceSuccess(t *testing.T) { + tests := []struct { + name string + setAllowedNamespaces []string + setNamespace *string + wantNamespace string + }{ + { + "any namespace allowed: ", + []string{}, + nil, + "default", + }, + { + "any namespace allowed: ", + []string{}, + ptr.To[string](""), + "", + }, + { + "any namespace allowed: default", + []string{}, + ptr.To[string]("default"), + "default", + }, + { + "any namespace allowed: testns", + []string{}, + ptr.To[string]("testns"), + "testns", + }, + { + "single namespace allowed: testns", + []string{"testns"}, + ptr.To[string]("testns"), + "testns", + }, + { + "multiple namespaces allowed: ", + []string{"testns1", "testns2"}, + ptr.To[string]("testns1"), + "testns1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Resolver{allowedNamespaces: tt.setAllowedNamespaces} + actualNamespace := r.ToNamespace(tt.setNamespace) + assert.Equal(t, tt.wantNamespace, actualNamespace) + }) + } +} + +func TestToNamespaceError(t *testing.T) { + tests := []struct { + name string + setAllowedNamespaces []string + setNamespace *string + }{ + { + "single namespace allowed: ", + []string{"testns"}, + nil, + }, + { + "single namespace allowed: not-testns", + []string{"testns"}, + ptr.To[string]("not-testns"), + }, + { + "multiple namespaces allowed: ", + []string{"testns1", "testns2"}, + nil, + }, + { + "multiple namespaces allowed: not-testns1", + []string{"testns1", "testns2"}, + ptr.To[string]("not-testns1"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Panics(t, func() { + r := Resolver{allowedNamespaces: tt.setAllowedNamespaces} + r.ToNamespace(tt.setNamespace) + }) + }) + } +} + +func TestToNamespacesSuccess(t *testing.T) { + tests := []struct { + name string + setAllowedNamespaces []string + setNamespace *string + wantNamespaces []string + }{ + { + "any namespace allowed: ", + []string{}, + nil, + []string{"default"}, + }, + { + "any namespace allowed: ", + []string{}, + ptr.To[string](""), + []string{""}, + }, + { + "any namespace allowed: default", + []string{}, + ptr.To[string]("default"), + []string{"default"}, + }, + { + "any namespace allowed: testns", + []string{}, + ptr.To[string]("testns"), + []string{"testns"}, + }, + { + "single namespace allowed: ", + []string{"testns"}, + ptr.To[string](""), + []string{"testns"}, + }, + { + "single namespace allowed: testns", + []string{"testns"}, + ptr.To[string]("testns"), + []string{"testns"}, + }, + { + "multiple namespaces allowed: ", + []string{"testns1", "testns2"}, + ptr.To[string](""), + []string{"testns1", "testns2"}, + }, + { + "multiple namespaces allowed: testns1", + []string{"testns1", "testns2"}, + ptr.To[string]("testns1"), + []string{"testns1"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Resolver{allowedNamespaces: tt.setAllowedNamespaces} + actualNamespaces, err := r.ToNamespaces(tt.setNamespace) + assert.Nil(t, err) + assert.Equal(t, tt.wantNamespaces, actualNamespaces) + }) + } +} + +func TestToNamespacesError(t *testing.T) { + tests := []struct { + name string + setAllowedNamespaces []string + setNamespace *string + wantError error + }{ + { + "single namespace allowed: ", + []string{"testns"}, + nil, + ErrForbidden, + }, + { + "single namespace allowed: not-testns", + []string{"testns"}, + ptr.To[string]("not-testns"), + ErrForbidden, + }, + { + "multiple namespaces allowed: ", + []string{"testns1", "testns2"}, + nil, + ErrForbidden, + }, + { + "multiple namespaces allowed: not-testns1", + []string{"testns1", "testns2"}, + ptr.To[string]("not-testns1"), + ErrForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Resolver{allowedNamespaces: tt.setAllowedNamespaces} + actualNamespaces, err := r.ToNamespaces(tt.setNamespace) + assert.NotNil(t, err) + assert.Equal(t, tt.wantError, err) + assert.Equal(t, []string(nil), actualNamespaces) + }) + } +} From 90e570f9e3a30e0772d5167381ed438acdc1329f Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Tue, 14 May 2024 12:52:48 +0300 Subject: [PATCH 17/22] wip --- backend/graph/resolver_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/graph/resolver_test.go b/backend/graph/resolver_test.go index 2c54b699..8bd20e61 100644 --- a/backend/graph/resolver_test.go +++ b/backend/graph/resolver_test.go @@ -86,6 +86,11 @@ func TestToNamespaceError(t *testing.T) { []string{"testns"}, nil, }, + { + "single namespace allowed: ", + []string{"testns"}, + ptr.To[string](""), + }, { "single namespace allowed: not-testns", []string{"testns"}, @@ -96,6 +101,11 @@ func TestToNamespaceError(t *testing.T) { []string{"testns1", "testns2"}, nil, }, + { + "multiple namespaces allowed: ", + []string{"testns1", "testns2"}, + ptr.To[string](""), + }, { "multiple namespaces allowed: not-testns1", []string{"testns1", "testns2"}, From db46a6e65433b47a051832b7c2e2dfd1c5bb00dc Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Tue, 14 May 2024 16:22:07 +0300 Subject: [PATCH 18/22] wip --- backend/graph/resolver.go | 20 ++-- backend/graph/resolver_test.go | 12 +- backend/graph/schema.resolvers.go | 152 +++++++++++++++++++++---- backend/graph/schema.resolvers_test.go | 135 ++++++++++++++++++++++ 4 files changed, 283 insertions(+), 36 deletions(-) create mode 100644 backend/graph/schema.resolvers_test.go diff --git a/backend/graph/resolver.go b/backend/graph/resolver.go index 9a646e47..7ae08a4f 100644 --- a/backend/graph/resolver.go +++ b/backend/graph/resolver.go @@ -85,22 +85,18 @@ func (r *Resolver) K8SDynamicClient(ctx context.Context) dynamic.Interface { return dynamicClient } -func (r *Resolver) ToNamespace(namespace *string) string { - // check configured namespace - if len(r.allowedNamespaces) > 0 { - if slices.Contains(r.allowedNamespaces, *namespace) { - return *namespace - } else { - panic("xxx") - } - } - - // use default behavior +func (r *Resolver) ToNamespace(namespace *string) (string, error) { ns := metav1.NamespaceDefault if namespace != nil { ns = *namespace } - return ns + + // perform auth + if len(r.allowedNamespaces) > 0 && !slices.Contains(r.allowedNamespaces, ns) { + return "", ErrForbidden + } + + return ns, nil } func (r *Resolver) ToNamespaces(namespace *string) ([]string, error) { diff --git a/backend/graph/resolver_test.go b/backend/graph/resolver_test.go index 8bd20e61..cb42c26a 100644 --- a/backend/graph/resolver_test.go +++ b/backend/graph/resolver_test.go @@ -69,7 +69,8 @@ func TestToNamespaceSuccess(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := Resolver{allowedNamespaces: tt.setAllowedNamespaces} - actualNamespace := r.ToNamespace(tt.setNamespace) + actualNamespace, err := r.ToNamespace(tt.setNamespace) + assert.Nil(t, err) assert.Equal(t, tt.wantNamespace, actualNamespace) }) } @@ -115,10 +116,11 @@ func TestToNamespaceError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Panics(t, func() { - r := Resolver{allowedNamespaces: tt.setAllowedNamespaces} - r.ToNamespace(tt.setNamespace) - }) + r := Resolver{allowedNamespaces: tt.setAllowedNamespaces} + ns, err := r.ToNamespace(tt.setNamespace) + assert.Equal(t, ns, "") + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) }) } } diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index 2bd7632c..b1f001a7 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -1,3 +1,17 @@ +// Copyright 2024 Andres Morey +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package graph // This file will be automatically regenerated based on the schema, any resolver implementations @@ -67,7 +81,11 @@ func (r *coreV1PodsWatchEventResolver) Object(ctx context.Context, obj *watch.Ev // AppsV1DaemonSetsGet is the resolver for the appsV1DaemonSetsGet field. func (r *queryResolver) AppsV1DaemonSetsGet(ctx context.Context, name string, namespace *string, options *metav1.GetOptions) (*appsv1.DaemonSet, error) { - return r.K8SClientset(ctx).AppsV1().DaemonSets(r.ToNamespace(namespace)).Get(ctx, name, toGetOptions(options)) + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + return r.K8SClientset(ctx).AppsV1().DaemonSets(ns).Get(ctx, name, toGetOptions(options)) } // AppsV1DaemonSetsList is the resolver for the appsV1DaemonSetsList field. @@ -81,7 +99,11 @@ func (r *queryResolver) AppsV1DaemonSetsList(ctx context.Context, namespace *str // AppsV1DeploymentsGet is the resolver for the appsV1DeploymentsGet field. func (r *queryResolver) AppsV1DeploymentsGet(ctx context.Context, name string, namespace *string, options *metav1.GetOptions) (*appsv1.Deployment, error) { - return r.K8SClientset(ctx).AppsV1().Deployments(r.ToNamespace(namespace)).Get(ctx, name, toGetOptions(options)) + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + return r.K8SClientset(ctx).AppsV1().Deployments(ns).Get(ctx, name, toGetOptions(options)) } // AppsV1DeploymentsList is the resolver for the appsV1DeploymentsList field. @@ -95,7 +117,11 @@ func (r *queryResolver) AppsV1DeploymentsList(ctx context.Context, namespace *st // AppsV1ReplicaSetsGet is the resolver for the appsV1ReplicaSetsGet field. func (r *queryResolver) AppsV1ReplicaSetsGet(ctx context.Context, name string, namespace *string, options *metav1.GetOptions) (*appsv1.ReplicaSet, error) { - return r.K8SClientset(ctx).AppsV1().ReplicaSets(r.ToNamespace(namespace)).Get(ctx, name, toGetOptions(options)) + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + return r.K8SClientset(ctx).AppsV1().ReplicaSets(ns).Get(ctx, name, toGetOptions(options)) } // AppsV1ReplicaSetsList is the resolver for the appsV1ReplicaSetsList field. @@ -109,7 +135,11 @@ func (r *queryResolver) AppsV1ReplicaSetsList(ctx context.Context, namespace *st // AppsV1StatefulSetsGet is the resolver for the appsV1StatefulSetsGet field. func (r *queryResolver) AppsV1StatefulSetsGet(ctx context.Context, name string, namespace *string, options *metav1.GetOptions) (*appsv1.StatefulSet, error) { - return r.K8SClientset(ctx).AppsV1().StatefulSets(r.ToNamespace(namespace)).Get(ctx, name, toGetOptions(options)) + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + return r.K8SClientset(ctx).AppsV1().StatefulSets(ns).Get(ctx, name, toGetOptions(options)) } // AppsV1StatefulSetsList is the resolver for the appsV1StatefulSetsList field. @@ -123,7 +153,11 @@ func (r *queryResolver) AppsV1StatefulSetsList(ctx context.Context, namespace *s // BatchV1CronJobsGet is the resolver for the batchV1CronJobsGet field. func (r *queryResolver) BatchV1CronJobsGet(ctx context.Context, name string, namespace *string, options *metav1.GetOptions) (*batchv1.CronJob, error) { - return r.K8SClientset(ctx).BatchV1().CronJobs(r.ToNamespace(namespace)).Get(ctx, name, toGetOptions(options)) + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + return r.K8SClientset(ctx).BatchV1().CronJobs(ns).Get(ctx, name, toGetOptions(options)) } // BatchV1CronJobsList is the resolver for the batchV1CronJobsList field. @@ -137,7 +171,11 @@ func (r *queryResolver) BatchV1CronJobsList(ctx context.Context, namespace *stri // BatchV1JobsGet is the resolver for the batchV1JobsGet field. func (r *queryResolver) BatchV1JobsGet(ctx context.Context, name string, namespace *string, options *metav1.GetOptions) (*batchv1.Job, error) { - return r.K8SClientset(ctx).BatchV1().Jobs(r.ToNamespace(namespace)).Get(ctx, name, toGetOptions(options)) + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + return r.K8SClientset(ctx).BatchV1().Jobs(ns).Get(ctx, name, toGetOptions(options)) } // BatchV1JobsList is the resolver for the batchV1JobsList field. @@ -177,7 +215,11 @@ func (r *queryResolver) CoreV1NodesList(ctx context.Context, options *metav1.Lis // CoreV1PodsGet is the resolver for the coreV1PodsGet field. func (r *queryResolver) CoreV1PodsGet(ctx context.Context, namespace *string, name string, options *metav1.GetOptions) (*corev1.Pod, error) { - return r.K8SClientset(ctx).CoreV1().Pods(r.ToNamespace(namespace)).Get(ctx, name, toGetOptions(options)) + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + return r.K8SClientset(ctx).CoreV1().Pods(ns).Get(ctx, name, toGetOptions(options)) } // CoreV1PodsList is the resolver for the coreV1PodsList field. @@ -196,8 +238,14 @@ func (r *queryResolver) CoreV1PodsGetLogs(ctx context.Context, namespace *string opts.Follow = false opts.Timestamps = true + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + // execute query - req := r.K8SClientset(ctx).CoreV1().Pods(r.ToNamespace(namespace)).GetLogs(name, &opts) + req := r.K8SClientset(ctx).CoreV1().Pods(ns).GetLogs(name, &opts) podLogs, err := req.Stream(ctx) if err != nil { return nil, err @@ -223,6 +271,12 @@ func (r *queryResolver) CoreV1PodsGetLogs(ctx context.Context, namespace *string // PodLogHead is the resolver for the podLogHead field. func (r *queryResolver) PodLogHead(ctx context.Context, namespace *string, name string, container *string, after *string, since *string, first *int) (*model.PodLogQueryResponse, error) { + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + // build query args args := HeadArgs{} @@ -238,11 +292,17 @@ func (r *queryResolver) PodLogHead(ctx context.Context, namespace *string, name args.First = uint(*first) } - return headPodLog(ctx, r.K8SClientset(ctx), r.ToNamespace(namespace), name, container, args) + return headPodLog(ctx, r.K8SClientset(ctx), ns, name, container, args) } // PodLogTail is the resolver for the podLogTail field. func (r *queryResolver) PodLogTail(ctx context.Context, namespace *string, name string, container *string, before *string, last *int) (*model.PodLogQueryResponse, error) { + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + // build query args args := TailArgs{} @@ -254,7 +314,7 @@ func (r *queryResolver) PodLogTail(ctx context.Context, namespace *string, name args.Last = uint(*last) } - return tailPodLog(ctx, r.K8SClientset(ctx), r.ToNamespace(namespace), name, container, args) + return tailPodLog(ctx, r.K8SClientset(ctx), ns, name, container, args) } // LivezGet is the resolver for the livezGet field. @@ -269,7 +329,13 @@ func (r *queryResolver) ReadyzGet(ctx context.Context) (model.HealthCheckRespons // AppsV1DaemonSetsWatch is the resolver for the appsV1DaemonSetsWatch field. func (r *subscriptionResolver) AppsV1DaemonSetsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - watchAPI, err := r.K8SClientset(ctx).AppsV1().DaemonSets(r.ToNamespace(namespace)).Watch(ctx, toListOptions(options)) + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + + watchAPI, err := r.K8SClientset(ctx).AppsV1().DaemonSets(ns).Watch(ctx, toListOptions(options)) if err != nil { return nil, err } @@ -278,7 +344,13 @@ func (r *subscriptionResolver) AppsV1DaemonSetsWatch(ctx context.Context, namesp // AppsV1DeploymentsWatch is the resolver for the appsV1DeploymentsWatch field. func (r *subscriptionResolver) AppsV1DeploymentsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - watchAPI, err := r.K8SClientset(ctx).AppsV1().Deployments(r.ToNamespace(namespace)).Watch(ctx, toListOptions(options)) + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + + watchAPI, err := r.K8SClientset(ctx).AppsV1().Deployments(ns).Watch(ctx, toListOptions(options)) if err != nil { return nil, err } @@ -287,7 +359,13 @@ func (r *subscriptionResolver) AppsV1DeploymentsWatch(ctx context.Context, names // AppsV1ReplicaSetsWatch is the resolver for the appsV1ReplicaSetsWatch field. func (r *subscriptionResolver) AppsV1ReplicaSetsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - watchAPI, err := r.K8SClientset(ctx).AppsV1().ReplicaSets(r.ToNamespace(namespace)).Watch(ctx, toListOptions(options)) + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + + watchAPI, err := r.K8SClientset(ctx).AppsV1().ReplicaSets(ns).Watch(ctx, toListOptions(options)) if err != nil { return nil, err } @@ -296,7 +374,13 @@ func (r *subscriptionResolver) AppsV1ReplicaSetsWatch(ctx context.Context, names // AppsV1StatefulSetsWatch is the resolver for the appsV1StatefulSetsWatch field. func (r *subscriptionResolver) AppsV1StatefulSetsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - watchAPI, err := r.K8SClientset(ctx).AppsV1().StatefulSets(r.ToNamespace(namespace)).Watch(ctx, toListOptions(options)) + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + + watchAPI, err := r.K8SClientset(ctx).AppsV1().StatefulSets(ns).Watch(ctx, toListOptions(options)) if err != nil { return nil, err } @@ -305,7 +389,13 @@ func (r *subscriptionResolver) AppsV1StatefulSetsWatch(ctx context.Context, name // BatchV1CronJobsWatch is the resolver for the batchV1CronJobsWatch field. func (r *subscriptionResolver) BatchV1CronJobsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - watchAPI, err := r.K8SClientset(ctx).BatchV1().CronJobs(r.ToNamespace(namespace)).Watch(ctx, toListOptions(options)) + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + + watchAPI, err := r.K8SClientset(ctx).BatchV1().CronJobs(ns).Watch(ctx, toListOptions(options)) if err != nil { return nil, err } @@ -314,7 +404,13 @@ func (r *subscriptionResolver) BatchV1CronJobsWatch(ctx context.Context, namespa // BatchV1JobsWatch is the resolver for the batchV1JobsWatch field. func (r *subscriptionResolver) BatchV1JobsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - watchAPI, err := r.K8SClientset(ctx).BatchV1().Jobs(r.ToNamespace(namespace)).Watch(ctx, toListOptions(options)) + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + + watchAPI, err := r.K8SClientset(ctx).BatchV1().Jobs(ns).Watch(ctx, toListOptions(options)) if err != nil { return nil, err } @@ -341,7 +437,13 @@ func (r *subscriptionResolver) CoreV1NodesWatch(ctx context.Context, options *me // CoreV1PodsWatch is the resolver for the coreV1PodsWatch field. func (r *subscriptionResolver) CoreV1PodsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - watchAPI, err := r.K8SClientset(ctx).CoreV1().Pods(r.ToNamespace(namespace)).Watch(ctx, toListOptions(options)) + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + + watchAPI, err := r.K8SClientset(ctx).CoreV1().Pods(ns).Watch(ctx, toListOptions(options)) if err != nil { return nil, err } @@ -355,8 +457,14 @@ func (r *subscriptionResolver) CoreV1PodLogTail(ctx context.Context, namespace * opts.Follow = true opts.Timestamps = true + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + // execute query - req := r.K8SClientset(ctx).CoreV1().Pods(r.ToNamespace(namespace)).GetLogs(name, &opts) + req := r.K8SClientset(ctx).CoreV1().Pods(ns).GetLogs(name, &opts) podLogs, err := req.Stream(ctx) if err != nil { return nil, err @@ -380,6 +488,12 @@ func (r *subscriptionResolver) CoreV1PodLogTail(ctx context.Context, namespace * // PodLogFollow is the resolver for the podLogFollow field. func (r *subscriptionResolver) PodLogFollow(ctx context.Context, namespace *string, name string, container *string, after *string, since *string) (<-chan *model.LogRecord, error) { + // init namespace + ns, err := r.ToNamespace(namespace) + if err != nil { + return nil, err + } + // build follow args args := FollowArgs{} @@ -392,7 +506,7 @@ func (r *subscriptionResolver) PodLogFollow(ctx context.Context, namespace *stri } // init follow - inCh, err := followPodLog(ctx, r.K8SClientset(ctx), r.ToNamespace(namespace), name, container, args) + inCh, err := followPodLog(ctx, r.K8SClientset(ctx), ns, name, container, args) if err != nil { return nil, err } diff --git a/backend/graph/schema.resolvers_test.go b/backend/graph/schema.resolvers_test.go new file mode 100644 index 00000000..f11e1c89 --- /dev/null +++ b/backend/graph/schema.resolvers_test.go @@ -0,0 +1,135 @@ +// Copyright 2024 Andres Morey +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package graph + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + dynamicFake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/utils/ptr" +) + +func TestAllowedNamespacesGetQueries(t *testing.T) { + // init resolver + r := queryResolver{&Resolver{ + allowedNamespaces: []string{"ns1", "ns2"}, + TestClientset: fake.NewSimpleClientset(), + }} + + // table-driven tests + tests := []struct { + name string + setNamespace *string + }{ + {"namespace not specified", nil}, + {"namespace specified but not allowed", ptr.To[string]("nsforbidden")}, + {"namespace specified as wildcard", ptr.To[string]("")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := r.AppsV1DaemonSetsGet(context.Background(), "", tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.AppsV1DeploymentsGet(context.Background(), "", tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.AppsV1ReplicaSetsGet(context.Background(), "", tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.AppsV1StatefulSetsGet(context.Background(), "", tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.BatchV1CronJobsGet(context.Background(), "", tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.BatchV1JobsGet(context.Background(), "", tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.CoreV1PodsGet(context.Background(), tt.setNamespace, "", nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + }) + } +} + +func TestAllowedNamespacesListQueries(t *testing.T) { + // init dynamic client + scheme := runtime.NewScheme() + appsv1.AddToScheme(scheme) + batchv1.AddToScheme(scheme) + corev1.AddToScheme(scheme) + dynamicClient := dynamicFake.NewSimpleDynamicClient(scheme) + + // init resolver + r := queryResolver{&Resolver{ + allowedNamespaces: []string{"ns1", "ns2"}, + TestDynamicClient: dynamicClient, + }} + + // table-driven tests + tests := []struct { + name string + setNamespace *string + }{ + {"namespace not specified", nil}, + {"namespace specified but not allowed", ptr.To[string]("nsforbidden")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := r.AppsV1DaemonSetsList(context.Background(), tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.AppsV1DeploymentsList(context.Background(), tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.AppsV1ReplicaSetsList(context.Background(), tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.AppsV1StatefulSetsList(context.Background(), tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.BatchV1CronJobsList(context.Background(), tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.BatchV1JobsList(context.Background(), tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + + _, err = r.CoreV1PodsList(context.Background(), tt.setNamespace, nil) + assert.NotNil(t, err) + assert.Equal(t, err, ErrForbidden) + }) + } +} From da9cb116fa33526ff3d321aa4741455b4c20d44e Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Wed, 15 May 2024 00:26:54 +0300 Subject: [PATCH 19/22] wip --- backend/graph/helpers.go | 88 +++++++++++++++++++++++++++++++ backend/graph/schema.resolvers.go | 14 ++--- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 5cd25b3b..0c6c668c 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -367,6 +367,91 @@ func watchEventProxyChannel(ctx context.Context, watchAPI watch.Interface) <-cha return outCh } +// watchResource +func watchResource(r *subscriptionResolver, ctx context.Context, gvr schema.GroupVersionResource, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { + client := r.K8SDynamicClient(ctx).Resource(gvr) + + // init namespaces + namespaces, err := r.ToNamespaces(namespace) + if err != nil { + return nil, err + } + + // init list options + opts := toListOptions(options) + + // init watch api's + watchAPIs := []watch.Interface{} + for _, ns := range namespaces { + watchAPI, err := client.Namespace(ns).Watch(ctx, opts) + if err != nil { + return nil, err + } + watchAPIs = append(watchAPIs, watchAPI) + } + + // start watches + outCh := make(chan *watch.Event) + var wg sync.WaitGroup + + for _, watchAPI := range watchAPIs { + wg.Add(1) + go func(watchAPI watch.Interface) { + defer wg.Done() + + for ev := range watchAPI.ResultChan() { + // just-in-case (maybe this is unnecessary) + if ev.Type == "" || ev.Object == nil { + break + } + + // exit if error + if ev.Type == watch.Error { + status, ok := ev.Object.(*metav1.Status) + if ok { + transport.AddSubscriptionError(ctx, NewWatchError(status)) + } else { + transport.AddSubscriptionError(ctx, ErrInternalServerError) + } + break + } + + //runtime.DefaultUnstructuredConverter.FromUnstructured(ev.UnstructuredContent(), &ev.Object) + + // write to output channel + outCh <- &ev + } + + // cleanup + watchAPI.Stop() + }(watchAPI) + } + + // monitor watch api's and ctx + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + go func() { + select { + case <-done: + // do nothing + case <-ctx.Done(): + // do nothing + } + + // stop watchers + for _, watchAPI := range watchAPIs { + watchAPI.Stop() + } + close(outCh) + }() + + return outCh, nil +} + // getHealth func getHealth(ctx context.Context, clientset kubernetes.Interface, endpoint string) model.HealthCheckResponse { resp := model.HealthCheckResponse{ @@ -454,6 +539,9 @@ func typeassertRuntimeObject[T any](object runtime.Object) (T, error) { switch o := object.(type) { case T: return o, nil + case *unstructured.Unstructured: + err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.UnstructuredContent(), &zeroVal) + return zeroVal, err default: return zeroVal, fmt.Errorf("not expecting type %T", o) } diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index b1f001a7..0c1fe734 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -31,6 +31,7 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" ) @@ -329,17 +330,8 @@ func (r *queryResolver) ReadyzGet(ctx context.Context) (model.HealthCheckRespons // AppsV1DaemonSetsWatch is the resolver for the appsV1DaemonSetsWatch field. func (r *subscriptionResolver) AppsV1DaemonSetsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - // init namespace - ns, err := r.ToNamespace(namespace) - if err != nil { - return nil, err - } - - watchAPI, err := r.K8SClientset(ctx).AppsV1().DaemonSets(ns).Watch(ctx, toListOptions(options)) - if err != nil { - return nil, err - } - return watchEventProxyChannel(ctx, watchAPI), nil + gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"} + return watchResource(r, ctx, gvr, namespace, options) } // AppsV1DeploymentsWatch is the resolver for the appsV1DeploymentsWatch field. From 54fa11ac6f467970dcc8b609d1f35bca2835d84d Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Wed, 15 May 2024 16:28:14 +0300 Subject: [PATCH 20/22] wip --- backend/graph/helpers.go | 155 ++++++++++++++++++++---------- backend/graph/schema.resolvers.go | 102 +++++++------------- 2 files changed, 138 insertions(+), 119 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index 0c6c668c..f833da63 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -127,6 +127,41 @@ type continueMultiToken struct { StartKey string `json:"start"` } +// encode resource version +func encodeResourceVersionMulti(resourceVersionMap map[string]string) (string, error) { + // json encode + resourceVersionBytes, err := json.Marshal(resourceVersionMap) + if err != nil { + return "", err + } + + // base64 encode + return base64.StdEncoding.EncodeToString(resourceVersionBytes), nil +} + +// decode resource version +func decodeResourceVersionMulti(resourceVersionToken string) (map[string]string, error) { + resourceVersionMap := map[string]string{} + + if resourceVersionToken == "" { + return resourceVersionMap, nil + } + + // base64 decode + resourceVersionBytes, err := base64.StdEncoding.DecodeString(resourceVersionToken) + if err != nil { + return nil, err + } + + // json decode + err = json.Unmarshal(resourceVersionBytes, &resourceVersionMap) + if err != nil { + return nil, err + } + + return resourceVersionMap, nil +} + // encode continue token func encodeContinueMulti(resourceVersions map[string]string, startKey string) (string, error) { token := continueMultiToken{ResourceVersions: resourceVersions, StartKey: startKey} @@ -214,11 +249,10 @@ func mergeResults(responses []FetchResponse, options metav1.ListOptions) (*unstr } // encode resourceVersionMap - resourceVersionBytes, err := json.Marshal(resourceVersionMap) + resourceVersion, err := encodeResourceVersionMulti(resourceVersionMap) if err != nil { return nil, err } - resourceVersion := base64.StdEncoding.EncodeToString(resourceVersionBytes) // generate continue token var continueToken string @@ -368,7 +402,51 @@ func watchEventProxyChannel(ctx context.Context, watchAPI watch.Interface) <-cha } // watchResource -func watchResource(r *subscriptionResolver, ctx context.Context, gvr schema.GroupVersionResource, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { +func watchResource(ctx context.Context, watchAPI watch.Interface, outCh chan<- *watch.Event, cancel context.CancelFunc, wg *sync.WaitGroup) { + defer wg.Done() + evCh := watchAPI.ResultChan() + +Loop: + for { + select { + case <-ctx.Done(): + // listener closed connection or another goroutine encountered an error + break Loop + case ev := <-evCh: + if ev.Type == "MODIFIED" { + fmt.Println(ev) + } + + // just-in-case (maybe this is unnecessary) + if ev.Type == "" || ev.Object == nil { + // stop all + cancel() + } + + // exit if error + if ev.Type == watch.Error { + status, ok := ev.Object.(*metav1.Status) + if ok { + transport.AddSubscriptionError(ctx, NewWatchError(status)) + } else { + transport.AddSubscriptionError(ctx, ErrInternalServerError) + } + + // stop all + cancel() + } + + // write to output channel + outCh <- &ev + } + } + + // cleanup + watchAPI.Stop() +} + +// watchResourceMulti +func watchResourceMulti(r *subscriptionResolver, ctx context.Context, gvr schema.GroupVersionResource, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { client := r.K8SDynamicClient(ctx).Resource(gvr) // init namespaces @@ -380,72 +458,47 @@ func watchResource(r *subscriptionResolver, ctx context.Context, gvr schema.Grou // init list options opts := toListOptions(options) + // decode resource version + resourceVersionMap, err := decodeResourceVersionMulti(opts.ResourceVersion) + if err != nil { + return nil, err + } + // init watch api's watchAPIs := []watch.Interface{} for _, ns := range namespaces { - watchAPI, err := client.Namespace(ns).Watch(ctx, opts) + // init options + thisOpts := opts + + thisResourceVersion, exists := resourceVersionMap[ns] + if exists { + thisOpts.ResourceVersion = thisResourceVersion + } else { + thisOpts.ResourceVersion = "" + } + + // init watch api + watchAPI, err := client.Namespace(ns).Watch(ctx, thisOpts) if err != nil { return nil, err } watchAPIs = append(watchAPIs, watchAPI) } - // start watches + // start watchers outCh := make(chan *watch.Event) + ctx, cancel := context.WithCancel(ctx) var wg sync.WaitGroup for _, watchAPI := range watchAPIs { wg.Add(1) - go func(watchAPI watch.Interface) { - defer wg.Done() - - for ev := range watchAPI.ResultChan() { - // just-in-case (maybe this is unnecessary) - if ev.Type == "" || ev.Object == nil { - break - } - - // exit if error - if ev.Type == watch.Error { - status, ok := ev.Object.(*metav1.Status) - if ok { - transport.AddSubscriptionError(ctx, NewWatchError(status)) - } else { - transport.AddSubscriptionError(ctx, ErrInternalServerError) - } - break - } - - //runtime.DefaultUnstructuredConverter.FromUnstructured(ev.UnstructuredContent(), &ev.Object) - - // write to output channel - outCh <- &ev - } - - // cleanup - watchAPI.Stop() - }(watchAPI) + go watchResource(ctx, watchAPI, outCh, cancel, &wg) } - // monitor watch api's and ctx - done := make(chan struct{}) + // cleanup go func() { wg.Wait() - close(done) - }() - - go func() { - select { - case <-done: - // do nothing - case <-ctx.Done(): - // do nothing - } - - // stop watchers - for _, watchAPI := range watchAPIs { - watchAPI.Stop() - } + cancel() close(outCh) }() diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index 0c1fe734..26f77e5f 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -26,6 +26,7 @@ import ( "slices" "strings" + "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/kubetail-org/kubetail/graph/model" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -331,82 +332,37 @@ func (r *queryResolver) ReadyzGet(ctx context.Context) (model.HealthCheckRespons // AppsV1DaemonSetsWatch is the resolver for the appsV1DaemonSetsWatch field. func (r *subscriptionResolver) AppsV1DaemonSetsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"} - return watchResource(r, ctx, gvr, namespace, options) + return watchResourceMulti(r, ctx, gvr, namespace, options) } // AppsV1DeploymentsWatch is the resolver for the appsV1DeploymentsWatch field. func (r *subscriptionResolver) AppsV1DeploymentsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - // init namespace - ns, err := r.ToNamespace(namespace) - if err != nil { - return nil, err - } - - watchAPI, err := r.K8SClientset(ctx).AppsV1().Deployments(ns).Watch(ctx, toListOptions(options)) - if err != nil { - return nil, err - } - return watchEventProxyChannel(ctx, watchAPI), nil + gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} + return watchResourceMulti(r, ctx, gvr, namespace, options) } // AppsV1ReplicaSetsWatch is the resolver for the appsV1ReplicaSetsWatch field. func (r *subscriptionResolver) AppsV1ReplicaSetsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - // init namespace - ns, err := r.ToNamespace(namespace) - if err != nil { - return nil, err - } - - watchAPI, err := r.K8SClientset(ctx).AppsV1().ReplicaSets(ns).Watch(ctx, toListOptions(options)) - if err != nil { - return nil, err - } - return watchEventProxyChannel(ctx, watchAPI), nil + gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"} + return watchResourceMulti(r, ctx, gvr, namespace, options) } // AppsV1StatefulSetsWatch is the resolver for the appsV1StatefulSetsWatch field. func (r *subscriptionResolver) AppsV1StatefulSetsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - // init namespace - ns, err := r.ToNamespace(namespace) - if err != nil { - return nil, err - } - - watchAPI, err := r.K8SClientset(ctx).AppsV1().StatefulSets(ns).Watch(ctx, toListOptions(options)) - if err != nil { - return nil, err - } - return watchEventProxyChannel(ctx, watchAPI), nil + gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"} + return watchResourceMulti(r, ctx, gvr, namespace, options) } // BatchV1CronJobsWatch is the resolver for the batchV1CronJobsWatch field. func (r *subscriptionResolver) BatchV1CronJobsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - // init namespace - ns, err := r.ToNamespace(namespace) - if err != nil { - return nil, err - } - - watchAPI, err := r.K8SClientset(ctx).BatchV1().CronJobs(ns).Watch(ctx, toListOptions(options)) - if err != nil { - return nil, err - } - return watchEventProxyChannel(ctx, watchAPI), nil + gvr := schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"} + return watchResourceMulti(r, ctx, gvr, namespace, options) } // BatchV1JobsWatch is the resolver for the batchV1JobsWatch field. func (r *subscriptionResolver) BatchV1JobsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - // init namespace - ns, err := r.ToNamespace(namespace) - if err != nil { - return nil, err - } - - watchAPI, err := r.K8SClientset(ctx).BatchV1().Jobs(ns).Watch(ctx, toListOptions(options)) - if err != nil { - return nil, err - } - return watchEventProxyChannel(ctx, watchAPI), nil + gvr := schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"} + return watchResourceMulti(r, ctx, gvr, namespace, options) } // CoreV1NamespacesWatch is the resolver for the coreV1NamespacesWatch field. @@ -415,7 +371,26 @@ func (r *subscriptionResolver) CoreV1NamespacesWatch(ctx context.Context, option if err != nil { return nil, err } - return watchEventProxyChannel(ctx, watchAPI), nil + + outCh := make(chan *watch.Event) + go func() { + for ev := range watchEventProxyChannel(ctx, watchAPI) { + ns, err := typeassertRuntimeObject[*corev1.Namespace](ev.Object) + if err != nil { + transport.AddSubscriptionError(ctx, ErrInternalServerError) + break + } + + // perform auth and write to channel + if len(r.allowedNamespaces) == 0 || (len(r.allowedNamespaces) > 0 && slices.Contains(r.allowedNamespaces, ns.Name)) { + outCh <- ev + } + } + close(outCh) + }() + + return outCh, nil + } // CoreV1NodesWatch is the resolver for the coreV1NodesWatch field. @@ -429,17 +404,8 @@ func (r *subscriptionResolver) CoreV1NodesWatch(ctx context.Context, options *me // CoreV1PodsWatch is the resolver for the coreV1PodsWatch field. func (r *subscriptionResolver) CoreV1PodsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - // init namespace - ns, err := r.ToNamespace(namespace) - if err != nil { - return nil, err - } - - watchAPI, err := r.K8SClientset(ctx).CoreV1().Pods(ns).Watch(ctx, toListOptions(options)) - if err != nil { - return nil, err - } - return watchEventProxyChannel(ctx, watchAPI), nil + gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + return watchResourceMulti(r, ctx, gvr, namespace, options) } // CoreV1PodLogTail is the resolver for the coreV1PodLogTail field. From 4a36a1662299431d6133b49c81f29195d0600985 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Wed, 15 May 2024 17:56:58 +0300 Subject: [PATCH 21/22] wip --- backend/graph/helpers.go | 6 +----- backend/graph/schema.resolvers.go | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/graph/helpers.go b/backend/graph/helpers.go index f833da63..558053ef 100644 --- a/backend/graph/helpers.go +++ b/backend/graph/helpers.go @@ -413,10 +413,6 @@ Loop: // listener closed connection or another goroutine encountered an error break Loop case ev := <-evCh: - if ev.Type == "MODIFIED" { - fmt.Println(ev) - } - // just-in-case (maybe this is unnecessary) if ev.Type == "" || ev.Object == nil { // stop all @@ -593,7 +589,7 @@ func typeassertRuntimeObject[T any](object runtime.Object) (T, error) { case T: return o, nil case *unstructured.Unstructured: - err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.UnstructuredContent(), &zeroVal) + err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.Object, &zeroVal) return zeroVal, err default: return zeroVal, fmt.Errorf("not expecting type %T", o) diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index 26f77e5f..bed44032 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -361,7 +361,7 @@ func (r *subscriptionResolver) BatchV1CronJobsWatch(ctx context.Context, namespa // BatchV1JobsWatch is the resolver for the batchV1JobsWatch field. func (r *subscriptionResolver) BatchV1JobsWatch(ctx context.Context, namespace *string, options *metav1.ListOptions) (<-chan *watch.Event, error) { - gvr := schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"} + gvr := schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"} return watchResourceMulti(r, ctx, gvr, namespace, options) } From 86ef3ece54ef442772630f970a3a32d57e863cd2 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Wed, 15 May 2024 18:54:07 +0300 Subject: [PATCH 22/22] wip --- backend/graph/schema.resolvers.go | 22 +++++++++---------- .../graph_test/subscription_resolver_test.go | 14 ++++++------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index bed44032..2a1ea89e 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -235,17 +235,17 @@ func (r *queryResolver) CoreV1PodsList(ctx context.Context, namespace *string, o // CoreV1PodsGetLogs is the resolver for the coreV1PodsGetLogs field. func (r *queryResolver) CoreV1PodsGetLogs(ctx context.Context, namespace *string, name string, options *corev1.PodLogOptions) ([]model.LogRecord, error) { - // init options - opts := toPodLogOptions(options) - opts.Follow = false - opts.Timestamps = true - // init namespace ns, err := r.ToNamespace(namespace) if err != nil { return nil, err } + // init options + opts := toPodLogOptions(options) + opts.Follow = false + opts.Timestamps = true + // execute query req := r.K8SClientset(ctx).CoreV1().Pods(ns).GetLogs(name, &opts) podLogs, err := req.Stream(ctx) @@ -381,7 +381,7 @@ func (r *subscriptionResolver) CoreV1NamespacesWatch(ctx context.Context, option break } - // perform auth and write to channel + // filter out non-authorized namespaces if len(r.allowedNamespaces) == 0 || (len(r.allowedNamespaces) > 0 && slices.Contains(r.allowedNamespaces, ns.Name)) { outCh <- ev } @@ -410,17 +410,17 @@ func (r *subscriptionResolver) CoreV1PodsWatch(ctx context.Context, namespace *s // CoreV1PodLogTail is the resolver for the coreV1PodLogTail field. func (r *subscriptionResolver) CoreV1PodLogTail(ctx context.Context, namespace *string, name string, options *corev1.PodLogOptions) (<-chan *model.LogRecord, error) { - // init options - opts := toPodLogOptions(options) - opts.Follow = true - opts.Timestamps = true - // init namespace ns, err := r.ToNamespace(namespace) if err != nil { return nil, err } + // init options + opts := toPodLogOptions(options) + opts.Follow = true + opts.Timestamps = true + // execute query req := r.K8SClientset(ctx).CoreV1().Pods(ns).GetLogs(name, &opts) podLogs, err := req.Stream(ctx) diff --git a/backend/graph_test/subscription_resolver_test.go b/backend/graph_test/subscription_resolver_test.go index 105bccdb..a00682fb 100644 --- a/backend/graph_test/subscription_resolver_test.go +++ b/backend/graph_test/subscription_resolver_test.go @@ -49,7 +49,7 @@ func (suite *SubscriptionResolverTestSuite) TestAppsV1DaemonSetsWatch() { // init reactor watcher := watch.NewFake() defer watcher.Stop() - suite.resolver.TestClientset.PrependWatchReactor("daemonsets", k8stesting.DefaultWatchReactor(watcher, nil)) + suite.resolver.TestDynamicClient.PrependWatchReactor("daemonsets", k8stesting.DefaultWatchReactor(watcher, nil)) // init subscription sub := suite.MustSubscribe(GraphQLRequest{Query: query}, nil) @@ -93,7 +93,7 @@ func (suite *SubscriptionResolverTestSuite) TestAppsV1DeploymentsWatch() { // init reactor watcher := watch.NewFake() defer watcher.Stop() - suite.resolver.TestClientset.PrependWatchReactor("deployments", k8stesting.DefaultWatchReactor(watcher, nil)) + suite.resolver.TestDynamicClient.PrependWatchReactor("deployments", k8stesting.DefaultWatchReactor(watcher, nil)) // init subscription sub := suite.MustSubscribe(GraphQLRequest{Query: query}, nil) @@ -137,7 +137,7 @@ func (suite *SubscriptionResolverTestSuite) TestAppsV1ReplicaSetsWatch() { // init reactor watcher := watch.NewFake() defer watcher.Stop() - suite.resolver.TestClientset.PrependWatchReactor("replicasets", k8stesting.DefaultWatchReactor(watcher, nil)) + suite.resolver.TestDynamicClient.PrependWatchReactor("replicasets", k8stesting.DefaultWatchReactor(watcher, nil)) // init subscription sub := suite.MustSubscribe(GraphQLRequest{Query: query}, nil) @@ -181,7 +181,7 @@ func (suite *SubscriptionResolverTestSuite) TestAppsV1StatefulSetsWatch() { // init reactor watcher := watch.NewFake() defer watcher.Stop() - suite.resolver.TestClientset.PrependWatchReactor("statefulsets", k8stesting.DefaultWatchReactor(watcher, nil)) + suite.resolver.TestDynamicClient.PrependWatchReactor("statefulsets", k8stesting.DefaultWatchReactor(watcher, nil)) // init subscription sub := suite.MustSubscribe(GraphQLRequest{Query: query}, nil) @@ -225,7 +225,7 @@ func (suite *SubscriptionResolverTestSuite) TestBatchV1CronJobsWatch() { // init reactor watcher := watch.NewFake() defer watcher.Stop() - suite.resolver.TestClientset.PrependWatchReactor("cronjobs", k8stesting.DefaultWatchReactor(watcher, nil)) + suite.resolver.TestDynamicClient.PrependWatchReactor("cronjobs", k8stesting.DefaultWatchReactor(watcher, nil)) // init subscription sub := suite.MustSubscribe(GraphQLRequest{Query: query}, nil) @@ -269,7 +269,7 @@ func (suite *SubscriptionResolverTestSuite) TestBatchV1JobsWatch() { // init reactor watcher := watch.NewFake() defer watcher.Stop() - suite.resolver.TestClientset.PrependWatchReactor("jobs", k8stesting.DefaultWatchReactor(watcher, nil)) + suite.resolver.TestDynamicClient.PrependWatchReactor("jobs", k8stesting.DefaultWatchReactor(watcher, nil)) // init subscription sub := suite.MustSubscribe(GraphQLRequest{Query: query}, nil) @@ -401,7 +401,7 @@ func (suite *SubscriptionResolverTestSuite) TestCoreV1PodsWatch() { // init reactor watcher := watch.NewFake() defer watcher.Stop() - suite.resolver.TestClientset.PrependWatchReactor("pods", k8stesting.DefaultWatchReactor(watcher, nil)) + suite.resolver.TestDynamicClient.PrependWatchReactor("pods", k8stesting.DefaultWatchReactor(watcher, nil)) // init subscription sub := suite.MustSubscribe(GraphQLRequest{Query: query}, nil)