From d0fd648c19faa3de8e7b63c85879e62099aed335 Mon Sep 17 00:00:00 2001 From: Rafa Castelblanque Date: Fri, 26 Aug 2022 17:51:52 +0200 Subject: [PATCH 1/4] Migrate namespaces retrieval from Kubeops to Kubeapps APIs Signed-off-by: Rafa Castelblanque --- .../kubeapps/templates/kubeappsapis/rbac.yaml | 42 +++ chart/kubeapps/values.yaml | 26 +- .../resources/v1alpha1/common/plugin.go | 61 ++++ .../resources/v1alpha1/common/plugin_test.go | 95 ++++++ .../plugins/resources/v1alpha1/main.go | 5 +- .../plugins/resources/v1alpha1/namespaces.go | 19 +- .../v1alpha1/namespaces_filtering.go | 182 ++++++++++ .../resources/v1alpha1/namespaces_test.go | 317 ++++++++++++++++-- .../plugins/resources/v1alpha1/server.go | 32 +- cmd/kubeops/cmd/root.go | 2 - cmd/kubeops/cmd/root_test.go | 12 +- .../internal/httphandler/http-handler.go | 82 +---- .../internal/httphandler/http-handler_test.go | 128 ------- cmd/kubeops/server/server.go | 14 +- dashboard/src/actions/auth.test.tsx | 2 +- dashboard/src/actions/namespace.test.tsx | 4 +- dashboard/src/actions/namespace.ts | 12 +- dashboard/src/shared/Namespace.ts | 13 +- pkg/kube/fake.go | 8 - pkg/kube/kube_handler.go | 150 +-------- pkg/kube/kube_handler_test.go | 175 ---------- 21 files changed, 773 insertions(+), 608 deletions(-) create mode 100644 chart/kubeapps/templates/kubeappsapis/rbac.yaml create mode 100644 cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin.go create mode 100644 cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go create mode 100644 cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_filtering.go diff --git a/chart/kubeapps/templates/kubeappsapis/rbac.yaml b/chart/kubeapps/templates/kubeappsapis/rbac.yaml new file mode 100644 index 00000000000..f69e0177f95 --- /dev/null +++ b/chart/kubeapps/templates/kubeappsapis/rbac.yaml @@ -0,0 +1,42 @@ +{{- if .Values.rbac.create -}} +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +kind: ClusterRole +metadata: + name: {{ printf "kubeapps:%s:kubeappsapis-ns-discovery" .Release.Namespace | quote }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: kubeappsapis + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" . ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +rules: + - apiGroups: + - "" + resources: + - namespaces + verbs: + - list +--- +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +kind: ClusterRoleBinding +metadata: + name: {{ printf "kubeapps:%s:kubeappsapis-ns-discovery" .Release.Namespace | quote }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: kubeappsapis + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" . ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ printf "kubeapps:%s:kubeappsapis-ns-discovery" .Release.Namespace | quote }} +subjects: + - kind: ServiceAccount + name: {{ template "kubeapps.kubeappsapis.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end -}} diff --git a/chart/kubeapps/values.yaml b/chart/kubeapps/values.yaml index 670aff84ea2..7f89bb44097 100644 --- a/chart/kubeapps/values.yaml +++ b/chart/kubeapps/values.yaml @@ -1156,16 +1156,6 @@ kubeops: ## - myRegistryKeySecretName ## pullSecrets: [] - ## @param kubeops.namespaceHeaderName Additional header name for trusted namespaces - ## e.g: - ## namespaceHeaderName: X-Consumer-Groups - ## - namespaceHeaderName: "" - ## @param kubeops.namespaceHeaderPattern Additional header pattern for trusted namespaces - ## e.g: - ## namespaceHeaderPattern: namespace:^([\w-]+):\w+$ - ## - namespaceHeaderPattern: "" ## @param kubeops.qps Kubeops QPS (queries per second) rate ## qps: "" @@ -1843,6 +1833,22 @@ kubeappsapis: defaultUpgradePolicy: none ## @param kubeappsapis.pluginConfig.flux.packages.v1alpha1.userManagedSecrets Default policy for handling repository secrets, either managed by the user or by kubeapps-apis userManagedSecrets: false + resources: + packages: + v1alpha1: + ## Trusted namespaces parameters + ## + trustedNamespaces: + ## @param kubeappsapis.pluginConfig.resources.packages.v1alpha1.trustedNamespaces.headerName Optional header name for trusted namespaces + ## e.g: + ## headerName: X-Consumer-Groups + ## + headerName: "" + ## @param kubeappsapis.pluginConfig.resources.packages.v1alpha1.trustedNamespaces.headerPattern Optional header pattern for trusted namespaces + ## e.g: + ## headerPattern: namespace:^([\w-]+):\w+$ + ## + headerPattern: "" ## Bitnami Kubeapps-APIs image ## ref: https://hub.docker.com/r/bitnami/kubeapps-apis/tags/ ## @param kubeappsapis.image.registry Kubeapps-APIs image registry diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin.go new file mode 100644 index 00000000000..132dde4ec0b --- /dev/null +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin.go @@ -0,0 +1,61 @@ +// Copyright 2022 the Kubeapps contributors. +// SPDX-License-Identifier: Apache-2.0 + +package common + +import ( + "encoding/json" + "fmt" + "os" +) + +type ResourcesPluginConfig struct { + TrustedNamespaces TrustedNamespaces +} + +type TrustedNamespaces struct { + HeaderName string + HeaderPattern string +} + +func NewDefaultPluginConfig() *ResourcesPluginConfig { + // If no config is provided, we default to the existing values for backwards compatibility. + return &ResourcesPluginConfig{} +} + +// ParsePluginConfig parses the input plugin configuration json file and returns the configuration options. +func ParsePluginConfig(pluginConfigPath string) (*ResourcesPluginConfig, error) { + + // Resources plugin config defines the following struct and json config + type resourcesConfig struct { + Resources struct { + Packages struct { + V1alpha1 struct { + TrustedNamespaces struct { + HeaderName string `json:"headerName"` + HeaderPattern string `json:"headerPattern"` + } `json:"trustedNamespaces"` + } `json:"v1alpha1"` + } `json:"packages"` + } `json:"resources"` + } + var config resourcesConfig + + // #nosec G304 + pluginConfig, err := os.ReadFile(pluginConfigPath) + if err != nil { + return nil, fmt.Errorf("unable to open plugin config at %q: %w", pluginConfigPath, err) + } + err = json.Unmarshal(pluginConfig, &config) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal pluginconfig: %q error: %w", string(pluginConfig), err) + } + + // return configured value + return &ResourcesPluginConfig{ + TrustedNamespaces: TrustedNamespaces{ + HeaderName: config.Resources.Packages.V1alpha1.TrustedNamespaces.HeaderName, + HeaderPattern: config.Resources.Packages.V1alpha1.TrustedNamespaces.HeaderPattern, + }, + }, nil +} diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go new file mode 100644 index 00000000000..0707177f05e --- /dev/null +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go @@ -0,0 +1,95 @@ +package common + +import ( + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/plugins/pkg/pkgutils" + log "k8s.io/klog/v2" + "os" + "runtime" + "sigs.k8s.io/yaml" + "strings" + "testing" +) + +func TestParsePluginConfig(t *testing.T) { + testCases := []struct { + name string + pluginYAMLConf []byte + expectedConfig *ResourcesPluginConfig + expectedError string + }{ + { + name: "non existing plugin-config file", + pluginYAMLConf: nil, + expectedConfig: &ResourcesPluginConfig{}, + expectedError: "", + }, + { + name: "invalid plugin config", + pluginYAMLConf: []byte(` +resources: + packages: + v1alpha1: + trustedNamespaces: + headerName: true + `), + expectedConfig: nil, + expectedError: "json: cannot unmarshal", + }, + { + name: "non-default, valid plugin config", + pluginYAMLConf: []byte(` +resources: + packages: + v1alpha1: + trustedNamespaces: + headerName: "X-Consumer-Groups" + headerPattern: "^namespace:([\\w-]+)$" + `), + expectedConfig: &ResourcesPluginConfig{ + TrustedNamespaces: TrustedNamespaces{ + HeaderName: "X-Consumer-Groups", + HeaderPattern: "^namespace:([\\w-]+)$", + }, + }, + expectedError: "", + }, + } + opts := cmpopts.IgnoreUnexported(pkgutils.VersionsInSummary{}) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // TODO(agamez): env vars and file paths should be handled properly for Windows operating system + if runtime.GOOS == "windows" { + t.Skip("Skipping in a Windows OS") + } + filename := "" + if tc.pluginYAMLConf != nil { + pluginJSONConf, err := yaml.YAMLToJSON(tc.pluginYAMLConf) + if err != nil { + log.Fatalf("%s", err) + } + f, err := os.CreateTemp(".", "plugin_json_conf") + if err != nil { + log.Fatalf("%s", err) + } + defer os.Remove(f.Name()) // clean up + if _, err := f.Write(pluginJSONConf); err != nil { + log.Fatalf("%s", err) + } + if err := f.Close(); err != nil { + log.Fatalf("%s", err) + } + filename = f.Name() + } + pluginConfig, err := ParsePluginConfig(filename) + if err != nil && !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("err got %q, want to find %q", err.Error(), tc.expectedError) + } else if pluginConfig != nil { + if got, want := pluginConfig, tc.expectedConfig; !cmp.Equal(want, got, opts) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts)) + } + } + }) + } +} diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go index 6c251302aaf..ff8973ee8d4 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go @@ -27,9 +27,10 @@ func init() { // RegisterWithGRPCServer enables a plugin to register with a gRPC server // returning the server implementation. +// //nolint:deadcode func RegisterWithGRPCServer(opts pluginsv1alpha1.GRPCPluginRegistrationOptions) (interface{}, error) { - svr, err := NewServer(opts.ConfigGetter, opts.ClientQPS, opts.ClientBurst) + svr, err := NewServer(opts.ConfigGetter, opts.ClientQPS, opts.ClientBurst, opts.PluginConfigPath) if err != nil { return nil, err } @@ -39,12 +40,14 @@ func RegisterWithGRPCServer(opts pluginsv1alpha1.GRPCPluginRegistrationOptions) // RegisterHTTPHandlerFromEndpoint enables a plugin to register an http // handler to translate to the gRPC request. +// //nolint:deadcode func RegisterHTTPHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error { return v1alpha1.RegisterResourcesServiceHandlerFromEndpoint(ctx, mux, endpoint, opts) } // GetPluginDetail returns a core.plugins.Plugin describing itself. +// //nolint:deadcode func GetPluginDetail() *pluginsgrpcv1alpha1.Plugin { return &pluginDetail diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces.go index 5f2746b5d13..437ab11fa15 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces.go @@ -72,28 +72,25 @@ func (s *Server) CreateNamespace(ctx context.Context, r *v1alpha1.CreateNamespac return &v1alpha1.CreateNamespaceResponse{}, nil } -// GetNamespaceNames returns the list of namespace names for a cluster if the -// user has the required RBAC. -// -// Note that we can't yet use this from the dashboard to replace the similar endpoint -// in kubeops until we update to ensure a configured service account can also be -// passed in (resources plugin config) and used if the user does not have RBAC. +// GetNamespaceNames returns the list of namespace names from either the cluster or the incoming trusted namespaces. +// In any case, only if the user has the required RBAC. func (s *Server) GetNamespaceNames(ctx context.Context, r *v1alpha1.GetNamespaceNamesRequest) (*v1alpha1.GetNamespaceNamesResponse, error) { cluster := r.GetCluster() log.InfoS("+resources GetNamespaceNames ", "cluster", cluster) - typedClient, _, err := s.clientGetter(ctx, cluster) + // Check if there are trusted namespaces in the request + trustedNamespaces, err := getTrustedNamespacesFromHeader(ctx, s.pluginConfig.TrustedNamespaces.HeaderName, s.pluginConfig.TrustedNamespaces.HeaderPattern) if err != nil { - return nil, status.Errorf(codes.Internal, "unable to get the k8s client: '%v'", err) + return nil, statuserror.FromK8sError("get", "Namespaces", "", err) } - namespaceList, err := typedClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + namespaceList, err := s.GetAccessibleNamespaces(ctx, cluster, trustedNamespaces) if err != nil { return nil, statuserror.FromK8sError("list", "Namespaces", "", err) } - namespaces := make([]string, len(namespaceList.Items)) - for i, ns := range namespaceList.Items { + namespaces := make([]string, len(namespaceList)) + for i, ns := range namespaceList { namespaces[i] = ns.Name } diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_filtering.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_filtering.go new file mode 100644 index 00000000000..1729875d449 --- /dev/null +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_filtering.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + authorizationapi "k8s.io/api/authorization/v1" + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + log "k8s.io/klog/v2" + "math" + "regexp" + "strings" + "sync" +) + +type checkNSJob struct { + ns corev1.Namespace +} + +type checkNSResult struct { + checkNSJob + allowed bool + Error error +} + +func (s *Server) MaxWorkers() int { + return int(s.clientQPS) +} + +// GetAccessibleNamespaces return the list of namespaces that the user has permission to access +func (s *Server) GetAccessibleNamespaces(ctx context.Context, cluster string, trustedNamespaces []corev1.Namespace) ([]corev1.Namespace, error) { + var namespaceList []corev1.Namespace + + if len(trustedNamespaces) > 0 { + namespaceList = append(namespaceList, trustedNamespaces...) + } else { + + typedClient, _, err := s.clientGetter(ctx, cluster) + if err != nil { + return nil, status.Errorf(codes.Internal, "unable to get the k8s client: '%v'", err) + } + + // Try to list namespaces with the user token, for backward compatibility + namespaces, err := typedClient.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + if k8sErrors.IsForbidden(err) { + backgroundCtx := context.Background() + // The user doesn't have permissions to list namespaces, use the current pod's service account + userClient, err := s.serviceAccountClientGetter.Typed(backgroundCtx) + if err != nil { + return nil, err + } + namespaces, err = userClient.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) + if err != nil && k8sErrors.IsForbidden(err) { + // Not even the configured kubeapps-apis service account has permission + return nil, err + } + } else { + return nil, err + } + + // Filter namespaces in which the user has permissions to write (secrets) only + namespaceList, err = filterAllowedNamespaces(typedClient, s.MaxWorkers(), namespaces.Items) + if err != nil { + return nil, err + } + } else { + // If the user can list namespaces, do not filter them + namespaceList = namespaces.Items + } + } + + // Filter out namespaces in terminating state + return filterActiveNamespaces(namespaceList), nil +} + +// getTrustedNamespacesFromHeader returns a list of namespaces from the header request +// The name and the value of the header field is specified by 2 variables: +// - headerName is a name of the expected header field, e.g. X-Consumer-Groups +// - headerPattern is a regular expression, and it matches only single regex group, e.g. ^namespace:([\w-]+)$ +func getTrustedNamespacesFromHeader(ctx context.Context, headerName, headerPattern string) ([]corev1.Namespace, error) { + var namespaces []corev1.Namespace + if headerName == "" || headerPattern == "" { + return []corev1.Namespace{}, nil + } + r, err := regexp.Compile(headerPattern) + if err != nil { + log.Errorf("unable to compile regular expression: %v", err) + return namespaces, err + } + + // Get trusted namespaces from the request header + md, _ := metadata.FromIncomingContext(ctx) + if md != nil && len(md[strings.ToLower(headerName)]) > 0 { // metadata is always lowercase + headerValue := md[strings.ToLower(headerName)][0] + trustedNamespaces := strings.Split(headerValue, ",") + for _, n := range trustedNamespaces { + // Check if the namespace matches the regex + rns := r.FindStringSubmatch(strings.TrimSpace(n)) + if rns == nil || len(rns) < 2 { + continue + } + ns := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: rns[1]}, + Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}, + } + namespaces = append(namespaces, ns) + } + } + return namespaces, nil +} + +func nsCheckerWorker(client kubernetes.Interface, nsJobs <-chan checkNSJob, resultChan chan checkNSResult) { + for j := range nsJobs { + res, err := client.AuthorizationV1().SelfSubjectAccessReviews().Create(context.TODO(), &authorizationapi.SelfSubjectAccessReview{ + Spec: authorizationapi.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationapi.ResourceAttributes{ + Group: "", + Resource: "secrets", + Verb: "get", + Namespace: j.ns.Name, + }, + }, + }, metav1.CreateOptions{}) + resultChan <- checkNSResult{j, res.Status.Allowed, err} + } +} + +func filterAllowedNamespaces(userClient kubernetes.Interface, maxWorkers int, namespaces []corev1.Namespace) ([]corev1.Namespace, error) { + var allowedNamespaces []corev1.Namespace + + var wg sync.WaitGroup + workers := int(math.Min(float64(len(namespaces)), float64(maxWorkers))) + checkNSJobs := make(chan checkNSJob, workers) + nsCheckRes := make(chan checkNSResult, workers) + + // Process maxReq ns at a time + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + nsCheckerWorker(userClient, checkNSJobs, nsCheckRes) + wg.Done() + }() + } + go func() { + wg.Wait() + close(nsCheckRes) + }() + + go func() { + for _, ns := range namespaces { + checkNSJobs <- checkNSJob{ns} + } + close(checkNSJobs) + }() + + // Start receiving results + for res := range nsCheckRes { + if res.Error == nil { + if res.allowed { + allowedNamespaces = append(allowedNamespaces, res.ns) + } + } else { + log.Errorf("failed to check namespace permissions. Got %v", res.Error) + } + } + return allowedNamespaces, nil +} + +func filterActiveNamespaces(namespaces []corev1.Namespace) []corev1.Namespace { + var readyNamespaces []corev1.Namespace + for _, namespace := range namespaces { + if namespace.Status.Phase == corev1.NamespaceActive { + readyNamespaces = append(readyNamespaces, namespace) + } + } + return readyNamespaces +} diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_test.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_test.go index ad137d62d72..96a4ea8d476 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_test.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_test.go @@ -7,6 +7,11 @@ import ( "context" "errors" "github.com/stretchr/testify/assert" + "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/plugins/pkg/clientgetter" + "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/plugins/resources/v1alpha1/common" + "google.golang.org/grpc/metadata" + "net/http" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -263,19 +268,23 @@ func TestGetNamespaceNames(t *testing.T) { v1alpha1.GetNamespaceNamesResponse{}, ) + defaultRequest := &v1alpha1.GetNamespaceNamesRequest{ + Cluster: "default", + } + testCases := []struct { - name string - request *v1alpha1.GetNamespaceNamesRequest - k8sError error - expectedResponse *v1alpha1.GetNamespaceNamesResponse - expectedErrorCode codes.Code - existingObjects []runtime.Object + name string + request *v1alpha1.GetNamespaceNamesRequest + trustedNamespacesConfig common.TrustedNamespaces + k8sError error + requestHeaders http.Header + expectedResponse *v1alpha1.GetNamespaceNamesResponse + expectedErrorCode codes.Code + existingObjects []runtime.Object }{ { - name: "returns existing namespaces if user has RBAC", - request: &v1alpha1.GetNamespaceNamesRequest{ - Cluster: "default", - }, + name: "returns existing namespaces if user has RBAC", + request: defaultRequest, existingObjects: []runtime.Object{ &core.Namespace{ TypeMeta: metav1.TypeMeta{ @@ -285,6 +294,9 @@ func TestGetNamespaceNames(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "default", }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, }, &core.Namespace{ TypeMeta: metav1.TypeMeta{ @@ -294,6 +306,9 @@ func TestGetNamespaceNames(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "kubeapps", }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, }, }, expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ @@ -304,10 +319,8 @@ func TestGetNamespaceNames(t *testing.T) { }, }, { - name: "returns permission denied if k8s returns a forbidden error", - request: &v1alpha1.GetNamespaceNamesRequest{ - Cluster: "default", - }, + name: "returns permission denied if k8s returns a forbidden error", + request: defaultRequest, k8sError: k8serrors.NewForbidden(schema.GroupResource{ Group: "v1", Resource: "namespaces", @@ -315,13 +328,258 @@ func TestGetNamespaceNames(t *testing.T) { expectedErrorCode: codes.PermissionDenied, }, { - name: "returns an internal error if k8s returns an unexpected error", - request: &v1alpha1.GetNamespaceNamesRequest{ - Cluster: "default", - }, + name: "returns an internal error if k8s returns an unexpected error", + request: defaultRequest, k8sError: k8serrors.NewInternalError(errors.New("Bang")), expectedErrorCode: codes.Internal, }, + { + name: "it should return the list of only active namespaces if accessible", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "terminating-ns", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceTerminating, + }, + }, + }, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "foo", + }, + }, + }, + { + name: "it should return the list of namespaces matching the trusted namespaces header", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + }, + trustedNamespacesConfig: common.TrustedNamespaces{ + HeaderName: "X-Consumer-Groups", + HeaderPattern: "^namespace:(\\w+)$", + }, + requestHeaders: http.Header{"X-Consumer-Groups": []string{"namespace:ns1", "namespace:ns2"}}, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "ns1", + "ns2", + }, + }, + }, + { + name: "it should return the existing list of namespaces when trusted namespaces header does not match pattern", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + }, + trustedNamespacesConfig: common.TrustedNamespaces{ + HeaderName: "X-Consumer-Groups", + HeaderPattern: "^namespace:(\\w+)$", + }, + requestHeaders: http.Header{"X-Consumer-Groups": []string{"nspace:ns1", "nspace:ns2"}}, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "foo", + }, + }, + }, + { + name: "it should return the existing list of namespaces when trusted namespaces header does not match name", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + }, + trustedNamespacesConfig: common.TrustedNamespaces{ + HeaderName: "X-Consumer-Groups", + HeaderPattern: "^namespace:(\\w+)$", + }, + requestHeaders: http.Header{"Y-Consumer-Groups": []string{"namespace:ns1", "namespace:ns2"}}, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "foo", + }, + }, + }, + { + name: "it should return the existing list of namespaces when trusted namespaces header name is empty", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + }, + trustedNamespacesConfig: common.TrustedNamespaces{ + HeaderName: "", + HeaderPattern: "^namespace:(\\w+)$", + }, + requestHeaders: http.Header{"X-Consumer-Groups": []string{"namespace:ns1", "namespace:ns2"}}, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "foo", + }, + }, + }, + { + name: "it should return the existing list of namespaces when trusted namespaces pattern is empty", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + }, + trustedNamespacesConfig: common.TrustedNamespaces{ + HeaderName: "X-Consumer-Groups", + HeaderPattern: "", + }, + requestHeaders: http.Header{"X-Consumer-Groups": []string{"namespace:ns1", "namespace:ns2"}}, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "foo", + }, + }, + }, + { + name: "it should return some of the namespaces from trusted namespaces header when not all match the pattern", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + }, + trustedNamespacesConfig: common.TrustedNamespaces{ + HeaderName: "X-Consumer-Groups", + HeaderPattern: "^namespace:(\\w+)$", + }, + requestHeaders: http.Header{"X-Consumer-Groups": []string{"namespace:ns1:read", "namespace:ns2", "ns3", "namespace:ns4", "ns:ns5:write"}}, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "ns2", + "ns4", + }, + }, + }, + { + name: "it should return existing namespaces if no trusted ns header but trusted configuration is in place", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + }, + trustedNamespacesConfig: common.TrustedNamespaces{ + HeaderName: "X-Consumer-Groups", + HeaderPattern: "^namespace:(\\w+)$", + }, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "foo", + }, + }, + }, + { + name: "it should ignore incoming trusted namespaces header when no trusted configuration is in place", + existingObjects: []runtime.Object{ + &core.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: v1.NamespaceStatus{ + Phase: v1.NamespaceActive, + }, + }, + }, + requestHeaders: http.Header{"X-Consumer-Groups": []string{"namespace:ns1:read", "namespace:ns2", "ns3", "namespace:ns4", "ns:ns5:write"}}, + expectedResponse: &v1alpha1.GetNamespaceNamesResponse{ + NamespaceNames: []string{ + "foo", + }, + }, + }, } for _, tc := range testCases { @@ -333,13 +591,34 @@ func TestGetNamespaceNames(t *testing.T) { return true, &v1.NamespaceList{}, tc.k8sError }) } + + backgroundClientGetter := func(ctx context.Context) (clientgetter.ClientInterfaces, error) { + return clientgetter. + NewBuilder(). + WithTyped(fakeClient). + Build(), nil + } + + pluginConfig := &common.ResourcesPluginConfig{} + if (tc.trustedNamespacesConfig != common.TrustedNamespaces{}) { + pluginConfig.TrustedNamespaces = tc.trustedNamespacesConfig + } + s := Server{ clientGetter: func(context.Context, string) (kubernetes.Interface, dynamic.Interface, error) { return fakeClient, nil, nil }, + serviceAccountClientGetter: backgroundClientGetter, + clientQPS: 5, + pluginConfig: pluginConfig, + } + + ctx := context.Background() + for headerName, headerValue := range tc.requestHeaders { + ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(headerName, strings.Join(headerValue, ","))) } - response, err := s.GetNamespaceNames(context.Background(), tc.request) + response, err := s.GetNamespaceNames(ctx, tc.request) if got, want := status.Code(err), tc.expectedErrorCode; got != want { t.Fatalf("got: %d, want: %d, err: %+v", got, want, err) diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/server.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/server.go index 6601f42ef1d..2c5dfbed073 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/server.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/server.go @@ -7,6 +7,8 @@ import ( "context" "encoding/json" "fmt" + "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/plugins/pkg/clientgetter" + "github.com/vmware-tanzu/kubeapps/cmd/kubeapps-apis/plugins/resources/v1alpha1/common" "os" "sync" @@ -44,6 +46,10 @@ type Server struct { // non-test implementation. clientGetter clientGetter + // for interactions with k8s API server in the context of + // kubeapps-internal-kubeappsapis service account + serviceAccountClientGetter clientgetter.BackgroundClientGetterFunc + // corePackagesClientGetter holds a function to obtain the core.packages.v1alpha1 // client. It is similarly initialised in NewServer() below. corePackagesClientGetter func() (pkgsGRPCv1alpha1.PackagesServiceClient, error) @@ -56,6 +62,11 @@ type Server struct { // stub version using the unsafe helpers while the real implementation // queries the k8s API for a REST mapper. kindToResource func(meta.RESTMapper, schema.GroupVersionKind) (schema.GroupVersionResource, meta.RESTScopeName, error) + + // pluginConfig Resources plugin configuration values + pluginConfig *common.ResourcesPluginConfig + + clientQPS float32 } // createRESTMapper returns a rest mapper configured with the APIs of the @@ -90,11 +101,27 @@ func createRESTMapper(clientQPS float32, clientBurst int) (meta.RESTMapper, erro return restmapper.NewDiscoveryRESTMapper(groupResources), nil } -func NewServer(configGetter core.KubernetesConfigGetter, clientQPS float32, clientBurst int) (*Server, error) { +func NewServer(configGetter core.KubernetesConfigGetter, clientQPS float32, clientBurst int, pluginConfigPath string) (*Server, error) { mapper, err := createRESTMapper(clientQPS, clientBurst) if err != nil { return nil, err } + + // If no config is provided, we default to the existing values for backwards compatibility. + pluginConfig := common.NewDefaultPluginConfig() + if pluginConfigPath != "" { + pluginConfig, err = common.ParsePluginConfig(pluginConfigPath) + if err != nil { + log.Fatalf("%s", err) + } + log.Infof("+resources using custom config: [%v]", *pluginConfig) + } else { + log.Info("+resources using default config since pluginConfigPath is empty") + } + + // Get the "in-cluster" client getter + backgroundClientGetter := clientgetter.NewBackgroundClientGetter(configGetter, clientgetter.Options{}) + return &Server{ clientGetter: func(ctx context.Context, cluster string) (kubernetes.Interface, dynamic.Interface, error) { if configGetter == nil { @@ -114,6 +141,7 @@ func NewServer(configGetter core.KubernetesConfigGetter, clientQPS float32, clie } return typedClient, dynamicClient, nil }, + serviceAccountClientGetter: backgroundClientGetter, corePackagesClientGetter: func() (pkgsGRPCv1alpha1.PackagesServiceClient, error) { port := os.Getenv("PORT") conn, err := grpc.Dial("localhost:"+port, grpc.WithTransportCredentials(insecure.NewCredentials())) @@ -130,6 +158,8 @@ func NewServer(configGetter core.KubernetesConfigGetter, clientQPS float32, clie } return mapping.Resource, mapping.Scope.Name(), nil }, + clientQPS: clientQPS, + pluginConfig: pluginConfig, }, nil } diff --git a/cmd/kubeops/cmd/root.go b/cmd/kubeops/cmd/root.go index 450fa62a1c5..516adf0eb54 100644 --- a/cmd/kubeops/cmd/root.go +++ b/cmd/kubeops/cmd/root.go @@ -52,8 +52,6 @@ func setFlags(c *cobra.Command) { c.Flags().StringVar(&serveOpts.PinnipedProxyCACert, "pinniped-proxy-ca-cert", "", "Path to certificate authority to use with requests to pinniped-proxy service") c.Flags().IntVar(&serveOpts.Burst, "burst", 15, "internal burst capacity") c.Flags().Float32Var(&serveOpts.Qps, "qps", 10, "internal QPS rate") - c.Flags().StringVar(&serveOpts.NamespaceHeaderName, "namespace-header-name", "", "name of the header field, e.g. namespace-header-name=X-Consumer-Groups") - c.Flags().StringVar(&serveOpts.NamespaceHeaderPattern, "namespace-header-pattern", "", "regular expression that matches only single group, e.g. namespace-header-pattern=^namespace:([\\w]+):\\w+$, to match namespace:ns:read") // TODO(agamez): remove once a new version of the chart is released var deprecated string diff --git a/cmd/kubeops/cmd/root_test.go b/cmd/kubeops/cmd/root_test.go index 36fd2e54814..6af5cc6aa7d 100644 --- a/cmd/kubeops/cmd/root_test.go +++ b/cmd/kubeops/cmd/root_test.go @@ -30,13 +30,11 @@ func TestParseFlagsCorrect(t *testing.T) { "--namespace-header-pattern", "foo07", }, server.ServeOptions{ - ClustersConfigPath: "foo04", - PinnipedProxyURL: "foo05", - PinnipedProxyCACert: "/etc/foo/my-ca.crt", - Burst: 903, - Qps: 904, - NamespaceHeaderName: "foo06", - NamespaceHeaderPattern: "foo07", + ClustersConfigPath: "foo04", + PinnipedProxyURL: "foo05", + PinnipedProxyCACert: "/etc/foo/my-ca.crt", + Burst: 903, + Qps: 904, }, true, }, diff --git a/cmd/kubeops/internal/httphandler/http-handler.go b/cmd/kubeops/internal/httphandler/http-handler.go index ae7250b1bcb..364d1f8dc3f 100644 --- a/cmd/kubeops/internal/httphandler/http-handler.go +++ b/cmd/kubeops/internal/httphandler/http-handler.go @@ -7,22 +7,14 @@ import ( "encoding/json" "net/http" "os" - "regexp" "strings" "github.com/gorilla/mux" "github.com/vmware-tanzu/kubeapps/pkg/kube" - corev1 "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" log "k8s.io/klog/v2" ) -// namespacesResponse is used to marshal the JSON response -type namespacesResponse struct { - Namespaces []corev1.Namespace `json:"namespaces"` -} - type allowedResponse struct { Allowed bool `json:"allowed"` } @@ -67,75 +59,6 @@ func getNamespaceAndCluster(req *http.Request) (string, string) { return requestNamespace, requestCluster } -// getHeaderNamespaces returns a list of namespaces from the header request -// The name and the value of the header field is specified by 2 variables: -// - headerName is a name of the expected header field, e.g. X-Consumer-Groups -// - headerPattern is a regular expression and it matches only single regex group, e.g. ^namespace:([\w-]+)$ -func getHeaderNamespaces(req *http.Request, headerName, headerPattern string) ([]corev1.Namespace, error) { - var namespaces = []corev1.Namespace{} - if headerName == "" || headerPattern == "" { - return []corev1.Namespace{}, nil - } - r, err := regexp.Compile(headerPattern) - if err != nil { - log.Errorf("unable to compile regular expression: %v", err) - return namespaces, err - } - headerNamespacesOrigin := strings.Split(req.Header.Get(headerName), ",") - for _, n := range headerNamespacesOrigin { - rns := r.FindStringSubmatch(strings.TrimSpace(n)) - if rns == nil || len(rns) < 2 { - continue - } - ns := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{Name: rns[1]}, - Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}, - } - namespaces = append(namespaces, ns) - } - return namespaces, nil -} - -// GetNamespaces return the list of namespaces -func GetNamespaces(kubeHandler kube.AuthHandler) func(w http.ResponseWriter, req *http.Request) { - return func(w http.ResponseWriter, req *http.Request) { - token := extractToken(req.Header.Get("Authorization")) - _, requestCluster := getNamespaceAndCluster(req) - - options := kubeHandler.GetOptions() - - clientset, err := kubeHandler.AsUser(token, requestCluster) - if err != nil { - returnK8sError(err, "get", "Namespaces", w) - return - } - - headerNamespaces, err := getHeaderNamespaces(req, options.NamespaceHeaderName, options.NamespaceHeaderPattern) - if err != nil { - returnK8sError(err, "get", "Namespaces", w) - } - - namespaces, err := clientset.GetNamespaces(headerNamespaces) - if err != nil { - returnK8sError(err, "get", "Namespaces", w) - } - - response := namespacesResponse{ - Namespaces: namespaces, - } - responseBody, err := json.Marshal(response) - if err != nil { - JSONError(w, err.Error(), http.StatusInternalServerError) - return - } - _, err = w.Write(responseBody) - if err != nil { - return - } - } -} - -// GetOperatorLogo return the list of namespaces func GetOperatorLogo(kubeHandler kube.AuthHandler) func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) { name := mux.Vars(req)["name"] @@ -205,14 +128,13 @@ func CanI(kubeHandler kube.AuthHandler) func(w http.ResponseWriter, req *http.Re } // SetupDefaultRoutes enables call-sites to use the backend api's default routes with minimal setup. -func SetupDefaultRoutes(r *mux.Router, namespaceHeaderName, namespaceHeaderPattern string, burst int, qps float32, clustersConfig kube.ClustersConfig) error { - backendHandler, err := kube.NewHandler(os.Getenv("POD_NAMESPACE"), namespaceHeaderName, namespaceHeaderPattern, burst, qps, clustersConfig) +func SetupDefaultRoutes(r *mux.Router, burst int, qps float32, clustersConfig kube.ClustersConfig) error { + backendHandler, err := kube.NewHandler(os.Getenv("POD_NAMESPACE"), burst, qps, clustersConfig) if err != nil { return err } //TODO(agamez): move these endpoints to a separate plugin when possible r.Methods("POST").Path("/clusters/{cluster}/can-i").Handler(http.HandlerFunc(CanI(backendHandler))) - r.Methods("GET").Path("/clusters/{cluster}/namespaces").Handler(http.HandlerFunc(GetNamespaces(backendHandler))) r.Methods("GET").Path("/clusters/{cluster}/namespaces/{namespace}/operator/{name}/logo").Handler(http.HandlerFunc(GetOperatorLogo(backendHandler))) return nil } diff --git a/cmd/kubeops/internal/httphandler/http-handler_test.go b/cmd/kubeops/internal/httphandler/http-handler_test.go index dbb104ab1f1..f7ce484b3ab 100644 --- a/cmd/kubeops/internal/httphandler/http-handler_test.go +++ b/cmd/kubeops/internal/httphandler/http-handler_test.go @@ -7,7 +7,6 @@ import ( "bytes" "encoding/json" "fmt" - "net/http" "net/http/httptest" "strings" "testing" @@ -15,10 +14,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/gorilla/mux" "github.com/vmware-tanzu/kubeapps/pkg/kube" - corev1 "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" ) func checkError(t *testing.T, response *httptest.ResponseRecorder, expectedError error) { @@ -71,131 +68,6 @@ func TestExtractToken(t *testing.T) { } } -func TestGetNamespaces(t *testing.T) { - testCases := []struct { - name string - existingNamespaces []corev1.Namespace - expectedNamespaces []corev1.Namespace - err error - expectedCode int - additionalHeader http.Header - namespaceHeaderOptions kube.KubeOptions - }{ - { - name: "it should return the list of namespaces and a 200 if the repo is created", - existingNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedCode: 200, - }, - { - name: "it should return a 403 when forbidden", - err: k8sErrors.NewForbidden(schema.GroupResource{}, "foo", fmt.Errorf("nope")), - expectedCode: 403, - }, - { - name: "it should return the list of namespaces from the header and a 200 if the repo is created", - existingNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedNamespaces: []corev1.Namespace{ - {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}, Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}}, - {ObjectMeta: metav1.ObjectMeta{Name: "ns2"}, Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}}, - }, - expectedCode: 200, - additionalHeader: http.Header{"X-Consumer-Groups": []string{"namespace:ns1", "namespace:ns2"}}, - namespaceHeaderOptions: kube.KubeOptions{ - NamespaceHeaderName: "X-Consumer-Groups", - NamespaceHeaderPattern: "^namespace:(\\w+)$", - }, - }, - { - name: "it should return the existing list of namespaces and a 200 when header does not match kubeops arg namespace-header-name", - existingNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedCode: 200, - additionalHeader: http.Header{"X-Consumer-Groups": []string{"nspace:ns1", "nspace:ns2"}}, - namespaceHeaderOptions: kube.KubeOptions{ - NamespaceHeaderName: "X-Consumer-Groups", - NamespaceHeaderPattern: "^namespace:(\\w+)$", - }, - }, - { - name: "it should return the existing list of namespaces and a 200 when header does not match kubeops arg namespace-header-pattern", - existingNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedCode: 200, - additionalHeader: http.Header{"Y-Consumer-Groups": []string{"namespace:ns1", "namespace:ns2"}}, - namespaceHeaderOptions: kube.KubeOptions{ - NamespaceHeaderName: "X-Consumer-Groups", - NamespaceHeaderPattern: "^namespace:(\\w+)$", - }, - }, - { - name: "it should return the existing list of namespaces and a 200 when kubeops arg namespace-header-name is empty", - existingNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedCode: 200, - additionalHeader: http.Header{"Y-Consumer-Groups": []string{"namespace:ns1", "namespace:ns2"}}, - namespaceHeaderOptions: kube.KubeOptions{ - NamespaceHeaderName: "", - NamespaceHeaderPattern: "^namespace:(\\w+)$", - }, - }, - { - name: "it should return the existing list of namespaces and a 200 when kubeops arg namespace-header-pattern is empty", - existingNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedCode: 200, - additionalHeader: http.Header{"Y-Consumer-Groups": []string{"namespace:ns1", "namespace:ns2"}}, - namespaceHeaderOptions: kube.KubeOptions{ - NamespaceHeaderName: "X-Consumer-Groups", - NamespaceHeaderPattern: "", - }, - }, - { - name: "it should return some of the namespaces from header and a 200 when not all match namespace-header-pattern", - existingNamespaces: []corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, - expectedNamespaces: []corev1.Namespace{ - {ObjectMeta: metav1.ObjectMeta{Name: "ns2"}, Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}}, - {ObjectMeta: metav1.ObjectMeta{Name: "ns4"}, Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}}, - }, - expectedCode: 200, - additionalHeader: http.Header{"X-Consumer-Groups": []string{"namespace:ns1:read", "namespace:ns2", "ns3", "namespace:ns4", "ns:ns5:write"}}, - namespaceHeaderOptions: kube.KubeOptions{ - NamespaceHeaderName: "X-Consumer-Groups", - NamespaceHeaderPattern: "^namespace:(\\w+)$", - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - getNSFunc := GetNamespaces(&kube.FakeHandler{Namespaces: tc.existingNamespaces, Err: tc.err, Options: tc.namespaceHeaderOptions}) - req := httptest.NewRequest("GET", "https://foo.bar/backend/v1/namespaces", nil) - - for headerName, headerValue := range tc.additionalHeader { - req.Header.Set(headerName, strings.Join(headerValue, ",")) - } - - response := httptest.NewRecorder() - getNSFunc(response, req) - - if got, want := response.Code, tc.expectedCode; got != want { - t.Errorf("got: %d, want: %d\nBody: %s", got, want, response.Body) - } - - if response.Code == 200 { - var nsResponse namespacesResponse - err := json.NewDecoder(response.Body).Decode(&nsResponse) - if err != nil { - t.Fatalf("%+v", err) - } - expectedResponse := namespacesResponse{Namespaces: tc.expectedNamespaces} - if got, want := nsResponse, expectedResponse; !cmp.Equal(want, got) { - t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got)) - } - } - }) - } -} - func TestGetOperatorLogo(t *testing.T) { testCases := []struct { name string diff --git a/cmd/kubeops/server/server.go b/cmd/kubeops/server/server.go index d9bf76f5d60..188acafbf10 100644 --- a/cmd/kubeops/server/server.go +++ b/cmd/kubeops/server/server.go @@ -22,13 +22,11 @@ import ( ) type ServeOptions struct { - ClustersConfigPath string - PinnipedProxyURL string - PinnipedProxyCACert string - Burst int - Qps float32 - NamespaceHeaderName string - NamespaceHeaderPattern string + ClustersConfigPath string + PinnipedProxyURL string + PinnipedProxyCACert string + Burst int + Qps float32 } const clustersCAFilesPrefix = "/etc/additional-clusters-cafiles" @@ -62,7 +60,7 @@ func Serve(serveOpts ServeOptions) error { r.Handle("/live", health) r.Handle("/ready", health) - err := httphandler.SetupDefaultRoutes(r.PathPrefix("/backend/v1").Subrouter(), serveOpts.NamespaceHeaderName, serveOpts.NamespaceHeaderPattern, serveOpts.Burst, serveOpts.Qps, clustersConfig) + err := httphandler.SetupDefaultRoutes(r.PathPrefix("/backend/v1").Subrouter(), serveOpts.Burst, serveOpts.Qps, clustersConfig) if err != nil { return fmt.Errorf("Unable to setup backend routes: %+v", err) } diff --git a/dashboard/src/actions/auth.test.tsx b/dashboard/src/actions/auth.test.tsx index ce304304822..0cbefdba2d3 100644 --- a/dashboard/src/actions/auth.test.tsx +++ b/dashboard/src/actions/auth.test.tsx @@ -31,7 +31,7 @@ beforeEach(() => { Auth.setAuthToken = jest.fn(); Auth.unsetAuthToken = jest.fn(); Namespace.list = jest.fn(async () => { - return { namespaces: [] }; + return { namespaceNames: [] }; }); jest.spyOn(NS, "unsetStoredNamespace"); diff --git a/dashboard/src/actions/namespace.test.tsx b/dashboard/src/actions/namespace.test.tsx index 8a711ae3ab4..64183287b5d 100644 --- a/dashboard/src/actions/namespace.test.tsx +++ b/dashboard/src/actions/namespace.test.tsx @@ -72,7 +72,7 @@ describe("fetchNamespaces", () => { it("dispatches the list of namespace names if no error", async () => { Namespace.list = jest.fn().mockImplementationOnce(() => { return { - namespaces: [{ metadata: { name: "overlook-hotel" } }, { metadata: { name: "room-217" } }], + namespaceNames: ["overlook-hotel", "room-217"], }; }); const expectedActions = [ @@ -123,7 +123,7 @@ describe("createNamespace", () => { Namespace.create = jest.fn(); Namespace.list = jest.fn().mockImplementationOnce(() => { return { - namespaces: [{ metadata: { name: "overlook-hotel" } }, { metadata: { name: "room-217" } }], + namespaceNames: ["overlook-hotel", "room-217"], }; }); const expectedActions = [ diff --git a/dashboard/src/actions/namespace.ts b/dashboard/src/actions/namespace.ts index 1f7f8efce04..e99b31fa145 100644 --- a/dashboard/src/actions/namespace.ts +++ b/dashboard/src/actions/namespace.ts @@ -1,11 +1,10 @@ // Copyright 2018-2022 the Kubeapps contributors. // SPDX-License-Identifier: Apache-2.0 -import { get } from "lodash"; import { ThunkAction } from "redux-thunk"; import { Kube } from "shared/Kube"; import Namespace, { setStoredNamespace } from "shared/Namespace"; -import { IResource, IStoreState } from "shared/types"; +import { IStoreState } from "shared/types"; import { ActionType, deprecated } from "typesafe-actions"; const { createAction } = deprecated; @@ -57,10 +56,7 @@ export function fetchNamespaces( return async dispatch => { try { const namespaceList = await Namespace.list(cluster); - const namespaceStrings = get(namespaceList, "namespaces", []).map( - (n: IResource) => n.metadata.name, - ); - if (namespaceStrings.length === 0) { + if (!namespaceList.namespaceNames || namespaceList.namespaceNames.length === 0) { dispatch( errorNamespaces( cluster, @@ -70,8 +66,8 @@ export function fetchNamespaces( ); return []; } - dispatch(receiveNamespaces(cluster, namespaceStrings)); - return namespaceStrings; + dispatch(receiveNamespaces(cluster, namespaceList.namespaceNames)); + return namespaceList.namespaceNames; } catch (e: any) { dispatch(errorNamespaces(cluster, e, "list")); return []; diff --git a/dashboard/src/shared/Namespace.ts b/dashboard/src/shared/Namespace.ts index c1d506f56f5..939083663fa 100644 --- a/dashboard/src/shared/Namespace.ts +++ b/dashboard/src/shared/Namespace.ts @@ -3,23 +3,14 @@ import { get } from "lodash"; import { Auth } from "./Auth"; -import { axiosWithAuth } from "./AxiosInstance"; -import { ForbiddenError, IResource, NotFoundError } from "./types"; -import * as url from "./url"; +import { ForbiddenError, NotFoundError } from "./types"; import { KubeappsGrpcClient } from "./KubeappsGrpcClient"; export default class Namespace { private static resourcesClient = () => new KubeappsGrpcClient().getResourcesServiceClientImpl(); - // TODO(agamez): Migrate API call, see #4785 public static async list(cluster: string) { - // This call is hitting an actual backend endpoint (see cmd\kubeops\internal\http-handler) - // while the other two calls (create, get) have been updated to use the - // resources client rather than the k8s API server. - const { data } = await axiosWithAuth.get<{ namespaces: IResource[] }>( - url.backend.namespaces.list(cluster), - ); - return data; + return this.resourcesClient().GetNamespaceNames({ cluster: cluster }); } public static async create( diff --git a/pkg/kube/fake.go b/pkg/kube/fake.go index 8e0eb9d8e6b..e094a7703cd 100644 --- a/pkg/kube/fake.go +++ b/pkg/kube/fake.go @@ -84,14 +84,6 @@ func (c *FakeHandler) GetAppRepository(name, namespace string) (*v1alpha1.AppRep return nil, k8sErrors.NewNotFound(schema.GroupResource{}, "foo") } -// GetNamespaces fake -func (c *FakeHandler) GetNamespaces(precheckedNamespaces []corev1.Namespace) ([]corev1.Namespace, error) { - if len(precheckedNamespaces) > 0 { - return precheckedNamespaces, c.Err - } - return c.Namespaces, c.Err -} - // GetSecret fake func (c *FakeHandler) GetSecret(name, namespace string) (*corev1.Secret, error) { for _, r := range c.Secrets { diff --git a/pkg/kube/kube_handler.go b/pkg/kube/kube_handler.go index 78f5bf450da..56dd177e985 100644 --- a/pkg/kube/kube_handler.go +++ b/pkg/kube/kube_handler.go @@ -8,21 +8,12 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io" - "io/ioutil" - "math" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "strings" - "sync" - "github.com/vmware-tanzu/kubeapps/cmd/apprepository-controller/pkg/apis/apprepository/v1alpha1" apprepoclientset "github.com/vmware-tanzu/kubeapps/cmd/apprepository-controller/pkg/client/clientset/versioned" v1alpha1typed "github.com/vmware-tanzu/kubeapps/cmd/apprepository-controller/pkg/client/clientset/versioned/typed/apprepository/v1alpha1" httpclient "github.com/vmware-tanzu/kubeapps/pkg/http-client" + "io" + "io/ioutil" authorizationapi "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" @@ -34,6 +25,12 @@ import ( "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" log "k8s.io/klog/v2" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" ) const OCIImageManifestMediaType = "application/vnd.oci.image.manifest.v1+json" @@ -249,8 +246,6 @@ func (c *combinedClientset) MaxWorkers() int { } type KubeOptions struct { - NamespaceHeaderName string - NamespaceHeaderPattern string } // kubeHandler handles http requests for operating on app repositories and k8s resources @@ -312,7 +307,6 @@ type handler interface { UpdateAppRepository(appRepoBody io.ReadCloser, requestNamespace string) (*v1alpha1.AppRepository, error) RefreshAppRepository(repoName string, requestNamespace string) (*v1alpha1.AppRepository, error) DeleteAppRepository(name, namespace string) error - GetNamespaces(precheckedNamespaces []corev1.Namespace) ([]corev1.Namespace, error) GetSecret(name, namespace string) (*corev1.Secret, error) GetAppRepository(repoName, repoNamespace string) (*v1alpha1.AppRepository, error) ValidateAppRepository(appRepoBody io.ReadCloser, requestNamespace string) (*ValidationResponse, error) @@ -436,7 +430,7 @@ var ErrEmptyOCIRegistry = fmt.Errorf("You need to specify at least one repositor // NewHandler returns a handler configured with a service account client set and a config // with a blank token to be copied when creating user client sets with specific tokens. -func NewHandler(kubeappsNamespace, namespaceHeaderName, namespaceHeaderPattern string, burst int, qps float32, clustersConfig ClustersConfig) (AuthHandler, error) { +func NewHandler(kubeappsNamespace string, burst int, qps float32, clustersConfig ClustersConfig) (AuthHandler, error) { clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{ @@ -461,10 +455,7 @@ func NewHandler(kubeappsNamespace, namespaceHeaderName, namespaceHeaderPattern s config.Burst = burst config.QPS = qps - options := KubeOptions{ - NamespaceHeaderName: namespaceHeaderName, - NamespaceHeaderPattern: namespaceHeaderPattern, - } + options := KubeOptions{} svcRestConfig, err := rest.InClusterConfig() if err != nil { @@ -737,7 +728,8 @@ type repoManifest struct { } // Deprecated: Remove when the new Package Repository API implementation is completed -// getOCIAppRepositoryTag get a tag for the given repoURL & repoName +// +// getOCIAppRepositoryTag get a tag for the given repoURL & repoName func getOCIAppRepositoryTag(cli httpclient.Client, repoURL string, repoName string) (string, error) { // This function is the implementation of below curl command // curl -XGET -H "Authorization: Basic $harborauthz" @@ -797,7 +789,8 @@ func getOCIAppRepositoryTag(cli httpclient.Client, repoURL string, repoName stri } // Deprecated: Remove when the new Package Repository API implementation is completed -// getOCIAppRepositoryMediaType get manifests config.MediaType for the given repoURL & repoName +// +// getOCIAppRepositoryMediaType get manifests config.MediaType for the given repoURL & repoName func getOCIAppRepositoryMediaType(cli httpclient.Client, repoURL string, repoName string, tagVersion string) (string, error) { // This function is the implementation of below curl command // curl -XGET -H "Authorization: Basic $harborauthz" @@ -1086,121 +1079,6 @@ func KubeappsSecretNameForRepo(repoName, namespace string) string { return fmt.Sprintf("%s-%s", namespace, secretNameForRepo(repoName)) } -type checkNSJob struct { - ns corev1.Namespace -} - -type checkNSResult struct { - checkNSJob - allowed bool - Error error -} - -func nsCheckerWorker(userClientset combinedClientsetInterface, nsJobs <-chan checkNSJob, resultChan chan checkNSResult) { - for j := range nsJobs { - res, err := userClientset.AuthorizationV1().SelfSubjectAccessReviews().Create(context.TODO(), &authorizationapi.SelfSubjectAccessReview{ - Spec: authorizationapi.SelfSubjectAccessReviewSpec{ - ResourceAttributes: &authorizationapi.ResourceAttributes{ - Group: "", - Resource: "secrets", - Verb: "get", - Namespace: j.ns.Name, - }, - }, - }, metav1.CreateOptions{}) - resultChan <- checkNSResult{j, res.Status.Allowed, err} - } -} - -func filterAllowedNamespaces(userClientset combinedClientsetInterface, namespaces []corev1.Namespace) ([]corev1.Namespace, error) { - allowedNamespaces := []corev1.Namespace{} - - var wg sync.WaitGroup - workers := int(math.Min(float64(len(namespaces)), float64(userClientset.MaxWorkers()))) - checkNSJobs := make(chan checkNSJob, workers) - nsCheckRes := make(chan checkNSResult, workers) - - // Process maxReq ns at a time - for i := 0; i < workers; i++ { - wg.Add(1) - go func() { - nsCheckerWorker(userClientset, checkNSJobs, nsCheckRes) - wg.Done() - }() - } - go func() { - wg.Wait() - close(nsCheckRes) - }() - - go func() { - for _, ns := range namespaces { - checkNSJobs <- checkNSJob{ns} - } - close(checkNSJobs) - }() - - // Start receiving results - for res := range nsCheckRes { - if res.Error == nil { - if res.allowed { - allowedNamespaces = append(allowedNamespaces, res.ns) - } - } else { - log.Errorf("failed to check namespace permissions. Got %v", res.Error) - } - } - return allowedNamespaces, nil -} - -func filterActiveNamespaces(namespaces []corev1.Namespace) []corev1.Namespace { - readyNamespaces := []corev1.Namespace{} - for _, namespace := range namespaces { - if namespace.Status.Phase == corev1.NamespaceActive { - readyNamespaces = append(readyNamespaces, namespace) - } - } - return readyNamespaces -} - -// GetNamespaces return the list of namespaces that the user has permission to access -func (a *userHandler) GetNamespaces(precheckedNamespaces []corev1.Namespace) ([]corev1.Namespace, error) { - var namespaceList []corev1.Namespace - - if len(precheckedNamespaces) > 0 { - namespaceList = append(namespaceList, precheckedNamespaces...) - } else { - // Try to list namespaces with the user token, for backward compatibility - namespaces, err := a.clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) - if err != nil { - if k8sErrors.IsForbidden(err) { - // The user doesn't have permissions to list namespaces, use the current serviceaccount - namespaces, err = a.svcClientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) - if err != nil && k8sErrors.IsForbidden(err) { - // If the configured svcclient doesn't have permission, just return an empty list. - return []corev1.Namespace{}, nil - } - } else { - return nil, err - } - - // Filter namespaces in which the user has permissions to write (secrets) only - namespaceList, err = filterAllowedNamespaces(a.clientset, namespaces.Items) - if err != nil { - return nil, err - } - } else { - // If the user can list namespaces, do not filter them - namespaceList = namespaces.Items - } - } - - // Filter namespaces that are in terminating state - namespaceList = filterActiveNamespaces(namespaceList) - - return namespaceList, nil -} - // GetSecret return the a secret from a namespace using a token if given func (a *userHandler) GetSecret(name, namespace string) (*corev1.Secret, error) { return a.clientset.CoreV1().Secrets(namespace).Get(context.TODO(), name, metav1.GetOptions{}) diff --git a/pkg/kube/kube_handler_test.go b/pkg/kube/kube_handler_test.go index 6d25c32822e..47364b61a7b 100644 --- a/pkg/kube/kube_handler_test.go +++ b/pkg/kube/kube_handler_test.go @@ -30,7 +30,6 @@ import ( k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" fakecoreclientset "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/rest" @@ -891,180 +890,6 @@ func TestSecretForRequest(t *testing.T) { } } -type existingNs struct { - name string - phase corev1.NamespacePhase -} - -func TestGetNamespaces(t *testing.T) { - - testCases := []struct { - name string - existingNamespaces []existingNs - allowed bool - userClientErr error - svcClientErr error - expectedNamespaces []string - precheckedNamespaces []corev1.Namespace - }{ - { - name: "it lists namespaces if the user client returns the namespaces", - existingNamespaces: []existingNs{ - {"foo", corev1.NamespaceActive}, - {"bar", corev1.NamespaceActive}, - {"zed", corev1.NamespaceActive}, - }, - expectedNamespaces: []string{"foo", "bar", "zed"}, - allowed: true, - }, - { - name: "it lists namespaces if the userclient fails but the service client succeeds", - existingNamespaces: []existingNs{ - {"foo", corev1.NamespaceActive}, - }, - userClientErr: k8sErrors.NewForbidden(schema.GroupResource{}, "bang", fmt.Errorf("Bang")), - expectedNamespaces: []string{"foo"}, - allowed: true, - }, - { - name: "it filters the namespaces if the userclient fails but the service client succeeds", - existingNamespaces: []existingNs{ - {"foo", corev1.NamespaceActive}, - }, - userClientErr: k8sErrors.NewForbidden(schema.GroupResource{}, "bang", fmt.Errorf("Bang")), - expectedNamespaces: []string{}, - allowed: false, - }, - { - name: "it returns an empty list if both the user and service account forbidden", - existingNamespaces: []existingNs{ - {"foo", corev1.NamespaceActive}, - }, - userClientErr: k8sErrors.NewForbidden(schema.GroupResource{}, "bang", fmt.Errorf("Bang")), - svcClientErr: k8sErrors.NewForbidden(schema.GroupResource{}, "bang", fmt.Errorf("Bang")), - expectedNamespaces: []string{}, - allowed: true, - }, - { - name: "it filters namespaces in terminating status", - existingNamespaces: []existingNs{ - {"foo", corev1.NamespaceTerminating}, - {"bar", corev1.NamespaceActive}, - }, - expectedNamespaces: []string{"bar"}, - allowed: true, - }, - { - name: "it lists namespaces if the user client sends the namespaces", - existingNamespaces: []existingNs{ - {"foo", corev1.NamespaceActive}, - }, - precheckedNamespaces: []corev1.Namespace{{ - ObjectMeta: metav1.ObjectMeta{Name: "bar"}, - Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}, - }}, - expectedNamespaces: []string{"bar"}, - allowed: true, - }, - { - name: "it lists existing namespaces if the user client sends empty list of the namespaces", - existingNamespaces: []existingNs{ - {"foo", corev1.NamespaceActive}, - }, - precheckedNamespaces: []corev1.Namespace{}, - expectedNamespaces: []string{"foo"}, - allowed: true, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - userClientSet := fakeCombinedClientset{ - fakeapprepoclientset.NewSimpleClientset(), - fakecoreclientset.NewSimpleClientset(), - &fakeRest.RESTClient{}, - } - - svcClientSet := fakeCombinedClientset{ - fakeapprepoclientset.NewSimpleClientset(), - fakecoreclientset.NewSimpleClientset(), - &fakeRest.RESTClient{}, - } - - setClientsetData(userClientSet, tc.existingNamespaces, tc.userClientErr) - setClientsetData(svcClientSet, tc.existingNamespaces, tc.svcClientErr) - - // Set whether the userClientSet is allowed to create self subject access reviews. - // The handler for the reactor has only the action as input, which does not convey - // enough info to be able to decide whether an individual namespace is allowed - // (as action.GetNamespace() is always empty as self subject access reviews are - // *not* themselves namespaced - the spec includes the namespace but is not contained - // in the action). As a result, we can only filter everything or nothing in tests. - userClientSet.Clientset.Fake.PrependReactor( - "create", - "selfsubjectaccessreviews", - func(action k8stesting.Action) (handled bool, ret k8sruntime.Object, err error) { - mysar := &authorizationv1.SelfSubjectAccessReview{ - Status: authorizationv1.SubjectAccessReviewStatus{ - Allowed: tc.allowed, - Reason: "I want to test it", - }, - } - return true, mysar, nil - }, - ) - - handler := kubeHandler{ - clientsetForConfig: func(*rest.Config) (combinedClientsetInterface, error) { return userClientSet, nil }, - kubeappsNamespace: "kubeapps", - kubeappsSvcClientset: svcClientSet, - clustersConfig: ClustersConfig{ - KubeappsClusterName: "default", - Clusters: map[string]ClusterConfig{ - "default": {}, - }, - }, - } - - userHandler, err := handler.AsUser("token", "default") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - namespaces, err := userHandler.GetNamespaces(tc.precheckedNamespaces) - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - namespaceNames := []string{} - for _, ns := range namespaces { - namespaceNames = append(namespaceNames, ns.ObjectMeta.Name) - } - if !cmp.Equal(namespaceNames, tc.expectedNamespaces) { - t.Errorf("Unexpected response: %s", cmp.Diff(namespaceNames, tc.expectedNamespaces)) - } - }) - } -} - -// setClientsetData configures the fake clientset with the return and error. -func setClientsetData(cs fakeCombinedClientset, namespaceNames []existingNs, err error) { - namespaces := []corev1.Namespace{} - for _, ns := range namespaceNames { - namespaces = append(namespaces, corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{Name: ns.name}, - Status: corev1.NamespaceStatus{ - Phase: ns.phase, - }, - }) - } - cs.Clientset.Fake.PrependReactor( - "list", - "namespaces", - func(action k8stesting.Action) (bool, k8sruntime.Object, error) { - return true, &corev1.NamespaceList{Items: namespaces}, err - }, - ) -} - func TestGetValidator(t *testing.T) { testCases := []struct { name string From ad13c98d1e84c6968296cdc2fde173b8caf6cd19 Mon Sep 17 00:00:00 2001 From: Rafa Castelblanque Date: Mon, 29 Aug 2022 08:16:15 +0200 Subject: [PATCH 2/4] Added missing copyright headers Signed-off-by: Rafa Castelblanque --- .../plugins/resources/v1alpha1/common/plugin_test.go | 2 ++ .../plugins/resources/v1alpha1/namespaces_filtering.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go index 0707177f05e..0f081560d1f 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go @@ -1,3 +1,5 @@ +// Copyright 2022 the Kubeapps contributors. +// SPDX-License-Identifier: Apache-2.0 package common import ( diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_filtering.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_filtering.go index 1729875d449..4d3b114f822 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_filtering.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces_filtering.go @@ -1,3 +1,5 @@ +// Copyright 2022 the Kubeapps contributors. +// SPDX-License-Identifier: Apache-2.0 package main import ( From f8a486a927505f4d35a9dbacde473d876c10d741 Mon Sep 17 00:00:00 2001 From: Rafa Castelblanque Date: Wed, 31 Aug 2022 13:15:51 +0200 Subject: [PATCH 3/4] Leave the two CLI params as deprecated Signed-off-by: Rafa Castelblanque --- cmd/kubeops/cmd/root.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/kubeops/cmd/root.go b/cmd/kubeops/cmd/root.go index 516adf0eb54..0057690f848 100644 --- a/cmd/kubeops/cmd/root.go +++ b/cmd/kubeops/cmd/root.go @@ -53,8 +53,11 @@ func setFlags(c *cobra.Command) { c.Flags().IntVar(&serveOpts.Burst, "burst", 15, "internal burst capacity") c.Flags().Float32Var(&serveOpts.Qps, "qps", 10, "internal QPS rate") - // TODO(agamez): remove once a new version of the chart is released var deprecated string + // TODO(rcastelblanq) Remove together with the whole Kubeops + c.Flags().StringVar(&deprecated, "namespace-header-name", "", "(deprecated) name of the header field, e.g. namespace-header-name=X-Consumer-Groups") + c.Flags().StringVar(&deprecated, "namespace-header-pattern", "", "(deprecated) regular expression that matches only single group, e.g. namespace-header-pattern=^namespace:([\\w]+):\\w+$, to match namespace:ns:read") + // TODO(agamez): remove once a new version of the chart is released c.Flags().StringVar(&deprecated, "user-agent-comment", "", "(deprecated) UserAgent comment used during outbound requests") } From fbf252b18ae277340f160d786f8d327040b20ed8 Mon Sep 17 00:00:00 2001 From: Rafa Castelblanque Date: Wed, 31 Aug 2022 14:21:06 +0200 Subject: [PATCH 4/4] Updated README.md Signed-off-by: Rafa Castelblanque --- chart/kubeapps/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chart/kubeapps/README.md b/chart/kubeapps/README.md index 1dbafd04545..e5c95ff0b48 100644 --- a/chart/kubeapps/README.md +++ b/chart/kubeapps/README.md @@ -356,8 +356,6 @@ Once you have installed Kubeapps follow the [Getting Started Guide](https://gith | `kubeops.image.digest` | Kubeops image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag | `""` | | `kubeops.image.pullPolicy` | Kubeops image pull policy | `IfNotPresent` | | `kubeops.image.pullSecrets` | Kubeops image pull secrets | `[]` | -| `kubeops.namespaceHeaderName` | Additional header name for trusted namespaces | `""` | -| `kubeops.namespaceHeaderPattern` | Additional header pattern for trusted namespaces | `""` | | `kubeops.qps` | Kubeops QPS (queries per second) rate | `""` | | `kubeops.burst` | Kubeops burst rate | `""` | | `kubeops.extraFlags` | Additional command line flags for Kubeops | `[]` | @@ -549,6 +547,8 @@ Once you have installed Kubeapps follow the [Getting Started Guide](https://gith | `kubeappsapis.pluginConfig.kappController.packages.v1alpha1.defaultAllowDowngrades` | Default policy for allowing applications to be downgraded to previous versions | `false` | | `kubeappsapis.pluginConfig.flux.packages.v1alpha1.defaultUpgradePolicy` | Default upgrade policy generating version constraints | `none` | | `kubeappsapis.pluginConfig.flux.packages.v1alpha1.userManagedSecrets` | Default policy for handling repository secrets, either managed by the user or by kubeapps-apis | `false` | +| `kubeappsapis.pluginConfig.resources.packages.v1alpha1.trustedNamespaces.headerName` | Optional header name for trusted namespaces | `""` | +| `kubeappsapis.pluginConfig.resources.packages.v1alpha1.trustedNamespaces.headerPattern` | Optional header pattern for trusted namespaces | `""` | | `kubeappsapis.image.registry` | Kubeapps-APIs image registry | `docker.io` | | `kubeappsapis.image.repository` | Kubeapps-APIs image repository | `kubeapps/kubeapps-apis` | | `kubeappsapis.image.tag` | Kubeapps-APIs image tag (immutable tags are recommended) | `latest` |