Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate namespaces retrieval from Kubeops to Kubeapps APIs #5239

Merged
merged 6 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions chart/kubeapps/templates/kubeappsapis/rbac.yaml
Original file line number Diff line number Diff line change
@@ -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 -}}
26 changes: 16 additions & 10 deletions chart/kubeapps/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Expand Down Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @castelblanque, just for the sake of learning, I've never seen this kind of comment/annotation before, and I guess it should be processed by some kind of security linter/checker. Could you tell me what's the tool in charge of this and where is its configuration?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a gosec annotation we use for ignoring false positives and wontfixes. See https://github.com/securego/gosec#annotating-code

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
}
97 changes: 97 additions & 0 deletions cmd/kubeapps-apis/plugins/resources/v1alpha1/common/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2022 the Kubeapps contributors.
// SPDX-License-Identifier: Apache-2.0
package common
castelblanque marked this conversation as resolved.
Show resolved Hide resolved

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)
}
Comment on lines +71 to +73
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't you considered the use of github.com/stretchr/testify library (which I've checked is already included in the project) to ease the writing and reading of tests? This way you could replace these three lines with just a oneliner like require.NotNil(t, err)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, I'll try to remember next time. In many cases, this block of code is just copy-pasted.
Thanks!

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))
}
}
})
}
}
5 changes: 4 additions & 1 deletion cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down
19 changes: 8 additions & 11 deletions cmd/kubeapps-apis/plugins/resources/v1alpha1/namespaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down