fix(kubernetes/cli): fix a data-race BE-12259 (#1227)

release/2.33
andres-portainer 2025-09-18 10:22:29 -03:00 committed by GitHub
parent 1e47df6611
commit 68b9fef3f0
29 changed files with 192 additions and 67 deletions

View File

@ -145,21 +145,33 @@ func (kcl *KubeClient) GetNonAdminNamespaces(userID int, teamIDs []int, isRestri
} }
// GetIsKubeAdmin retrieves true if client is admin // GetIsKubeAdmin retrieves true if client is admin
func (client *KubeClient) GetIsKubeAdmin() bool { func (kcl *KubeClient) GetIsKubeAdmin() bool {
return client.IsKubeAdmin kcl.mu.Lock()
defer kcl.mu.Unlock()
return kcl.isKubeAdmin
} }
// UpdateIsKubeAdmin sets whether the kube client is admin // UpdateIsKubeAdmin sets whether the kube client is admin
func (client *KubeClient) SetIsKubeAdmin(isKubeAdmin bool) { func (kcl *KubeClient) SetIsKubeAdmin(isKubeAdmin bool) {
client.IsKubeAdmin = isKubeAdmin kcl.mu.Lock()
defer kcl.mu.Unlock()
kcl.isKubeAdmin = isKubeAdmin
} }
// GetClientNonAdminNamespaces retrieves non-admin namespaces // GetClientNonAdminNamespaces retrieves non-admin namespaces
func (client *KubeClient) GetClientNonAdminNamespaces() []string { func (kcl *KubeClient) GetClientNonAdminNamespaces() []string {
return client.NonAdminNamespaces kcl.mu.Lock()
defer kcl.mu.Unlock()
return kcl.nonAdminNamespaces
} }
// UpdateClientNonAdminNamespaces sets the client non admin namespace list // UpdateClientNonAdminNamespaces sets the client non admin namespace list
func (client *KubeClient) SetClientNonAdminNamespaces(nonAdminNamespaces []string) { func (kcl *KubeClient) SetClientNonAdminNamespaces(nonAdminNamespaces []string) {
client.NonAdminNamespaces = nonAdminNamespaces kcl.mu.Lock()
defer kcl.mu.Unlock()
kcl.nonAdminNamespaces = nonAdminNamespaces
} }

View File

@ -5,7 +5,9 @@ import (
"testing" "testing"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ktypes "k8s.io/api/core/v1" ktypes "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kfake "k8s.io/client-go/kubernetes/fake" kfake "k8s.io/client-go/kubernetes/fake"
@ -65,3 +67,27 @@ func Test_NamespaceAccessPoliciesDeleteNamespace_updatesPortainerConfig_whenConf
}) })
} }
} }
func TestKubeAdmin(t *testing.T) {
kcl := &KubeClient{}
require.False(t, kcl.GetIsKubeAdmin())
kcl.SetIsKubeAdmin(true)
require.True(t, kcl.GetIsKubeAdmin())
kcl.SetIsKubeAdmin(false)
require.False(t, kcl.GetIsKubeAdmin())
}
func TestClientNonAdminNamespaces(t *testing.T) {
kcl := &KubeClient{}
require.Empty(t, kcl.GetClientNonAdminNamespaces())
nss := []string{"ns1", "ns2"}
kcl.SetClientNonAdminNamespaces(nss)
require.Equal(t, nss, kcl.GetClientNonAdminNamespaces())
kcl.SetClientNonAdminNamespaces([]string{})
require.Empty(t, kcl.GetClientNonAdminNamespaces())
}

View File

@ -28,7 +28,7 @@ type PortainerApplicationResources struct {
// if the user is an admin, all namespaces in the current k8s environment(endpoint) are fetched using the fetchApplications function. // if the user is an admin, all namespaces in the current k8s environment(endpoint) are fetched using the fetchApplications function.
// otherwise, namespaces the non-admin user has access to will be used to filter the applications based on the allowed namespaces. // otherwise, namespaces the non-admin user has access to will be used to filter the applications based on the allowed namespaces.
func (kcl *KubeClient) GetApplications(namespace, nodeName string) ([]models.K8sApplication, error) { func (kcl *KubeClient) GetApplications(namespace, nodeName string) ([]models.K8sApplication, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchApplications(namespace, nodeName) return kcl.fetchApplications(namespace, nodeName)
} }
@ -64,9 +64,13 @@ func (kcl *KubeClient) fetchApplications(namespace, nodeName string) ([]models.K
// fetchApplicationsForNonAdmin fetches the applications in the namespaces the user has access to. // fetchApplicationsForNonAdmin fetches the applications in the namespaces the user has access to.
// This function is called when the user is not an admin. // This function is called when the user is not an admin.
func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string) ([]models.K8sApplication, error) { func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string) ([]models.K8sApplication, error) {
log.Debug().Msgf("Fetching applications for non-admin user: %v", kcl.NonAdminNamespaces) nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()
if len(kcl.NonAdminNamespaces) == 0 { log.Debug().
Strs("non_admin_namespaces", nonAdminNamespaces).
Msg("fetching applications for non-admin user")
if len(nonAdminNamespaces) == 0 {
return nil, nil return nil, nil
} }

View File

@ -310,7 +310,7 @@ func TestGetApplications(t *testing.T) {
kubeClient := &KubeClient{ kubeClient := &KubeClient{
cli: fakeClient, cli: fakeClient,
instanceID: "test-instance", instanceID: "test-instance",
IsKubeAdmin: true, isKubeAdmin: true,
} }
// Test cases // Test cases
@ -385,8 +385,8 @@ func TestGetApplications(t *testing.T) {
kubeClient := &KubeClient{ kubeClient := &KubeClient{
cli: fakeClient, cli: fakeClient,
instanceID: "test-instance", instanceID: "test-instance",
IsKubeAdmin: false, isKubeAdmin: false,
NonAdminNamespaces: []string{namespace1}, nonAdminNamespaces: []string{namespace1},
} }
// Test that only resources from allowed namespace are returned // Test that only resources from allowed namespace are returned
@ -445,7 +445,7 @@ func TestGetApplications(t *testing.T) {
kubeClient := &KubeClient{ kubeClient := &KubeClient{
cli: fakeClient, cli: fakeClient,
instanceID: "test-instance", instanceID: "test-instance",
IsKubeAdmin: true, isKubeAdmin: true,
} }
// Test filtering by node name // Test filtering by node name

View File

@ -42,8 +42,8 @@ type (
cli kubernetes.Interface cli kubernetes.Interface
instanceID string instanceID string
mu sync.Mutex mu sync.Mutex
IsKubeAdmin bool isKubeAdmin bool
NonAdminNamespaces []string nonAdminNamespaces []string
} }
) )
@ -147,6 +147,7 @@ func (factory *ClientFactory) GetProxyKubeClient(endpointID, userID string) (*Ku
if ok { if ok {
return client.(*KubeClient), true return client.(*KubeClient), true
} }
return nil, false return nil, false
} }
@ -179,8 +180,8 @@ func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, k
return &KubeClient{ return &KubeClient{
cli: cli, cli: cli,
instanceID: factory.instanceID, instanceID: factory.instanceID,
IsKubeAdmin: IsKubeAdmin, isKubeAdmin: IsKubeAdmin,
NonAdminNamespaces: NonAdminNamespaces, nonAdminNamespaces: NonAdminNamespaces,
}, nil }, nil
} }
@ -193,7 +194,7 @@ func (factory *ClientFactory) createCachedPrivilegedKubeClient(endpoint *portain
return &KubeClient{ return &KubeClient{
cli: cli, cli: cli,
instanceID: factory.instanceID, instanceID: factory.instanceID,
IsKubeAdmin: true, isKubeAdmin: true,
}, nil }, nil
} }
@ -371,6 +372,7 @@ func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint, da
log.Error().Err(err).Msgf("Error getting ingresses in environment %d", environment.ID) log.Error().Err(err).Msgf("Error getting ingresses in environment %d", environment.ID)
return err return err
} }
for _, ingress := range ingresses { for _, ingress := range ingresses {
oldController, ok := ingress.Annotations["ingress.portainer.io/ingress-type"] oldController, ok := ingress.Annotations["ingress.portainer.io/ingress-type"]
if !ok { if !ok {

View File

@ -16,7 +16,7 @@ import (
// GetClusterRoles gets all the clusterRoles for at the cluster level in a k8s endpoint. // GetClusterRoles gets all the clusterRoles for at the cluster level in a k8s endpoint.
// It returns a list of K8sClusterRole objects. // It returns a list of K8sClusterRole objects.
func (kcl *KubeClient) GetClusterRoles() ([]models.K8sClusterRole, error) { func (kcl *KubeClient) GetClusterRoles() ([]models.K8sClusterRole, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchClusterRoles() return kcl.fetchClusterRoles()
} }

View File

@ -16,7 +16,7 @@ import (
// GetClusterRoleBindings gets all the clusterRoleBindings for at the cluster level in a k8s endpoint. // GetClusterRoleBindings gets all the clusterRoleBindings for at the cluster level in a k8s endpoint.
// It returns a list of K8sClusterRoleBinding objects. // It returns a list of K8sClusterRoleBinding objects.
func (kcl *KubeClient) GetClusterRoleBindings() ([]models.K8sClusterRoleBinding, error) { func (kcl *KubeClient) GetClusterRoleBindings() ([]models.K8sClusterRoleBinding, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchClusterRoleBindings() return kcl.fetchClusterRoleBindings()
} }

View File

@ -16,18 +16,23 @@ import (
// if the user is an admin, all configMaps in the current k8s environment(endpoint) are fetched using the fetchConfigMaps function. // if the user is an admin, all configMaps in the current k8s environment(endpoint) are fetched using the fetchConfigMaps function.
// otherwise, namespaces the non-admin user has access to will be used to filter the configMaps based on the allowed namespaces. // otherwise, namespaces the non-admin user has access to will be used to filter the configMaps based on the allowed namespaces.
func (kcl *KubeClient) GetConfigMaps(namespace string) ([]models.K8sConfigMap, error) { func (kcl *KubeClient) GetConfigMaps(namespace string) ([]models.K8sConfigMap, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchConfigMaps(namespace) return kcl.fetchConfigMaps(namespace)
} }
return kcl.fetchConfigMapsForNonAdmin(namespace) return kcl.fetchConfigMapsForNonAdmin(namespace)
} }
// fetchConfigMapsForNonAdmin fetches the configMaps in the namespaces the user has access to. // fetchConfigMapsForNonAdmin fetches the configMaps in the namespaces the user has access to.
// This function is called when the user is not an admin. // This function is called when the user is not an admin.
func (kcl *KubeClient) fetchConfigMapsForNonAdmin(namespace string) ([]models.K8sConfigMap, error) { func (kcl *KubeClient) fetchConfigMapsForNonAdmin(namespace string) ([]models.K8sConfigMap, error) {
log.Debug().Msgf("Fetching configMaps for non-admin user: %v", kcl.NonAdminNamespaces) nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()
if len(kcl.NonAdminNamespaces) == 0 { log.Debug().
Strs("non_admin_namespaces", nonAdminNamespaces).
Msg("fetching configMaps for non-admin user")
if len(nonAdminNamespaces) == 0 {
return nil, nil return nil, nil
} }

View File

@ -15,7 +15,7 @@ import (
// If the user is a kube admin, it returns all cronjobs in the namespace // If the user is a kube admin, it returns all cronjobs in the namespace
// Otherwise, it returns only the cronjobs in the non-admin namespaces // Otherwise, it returns only the cronjobs in the non-admin namespaces
func (kcl *KubeClient) GetCronJobs(namespace string) ([]models.K8sCronJob, error) { func (kcl *KubeClient) GetCronJobs(namespace string) ([]models.K8sCronJob, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchCronJobs(namespace) return kcl.fetchCronJobs(namespace)
} }

View File

@ -18,7 +18,7 @@ func (kcl *KubeClient) TestFetchCronJobs(t *testing.T) {
t.Run("admin client can fetch Cron Jobs from all namespaces", func(t *testing.T) { t.Run("admin client can fetch Cron Jobs from all namespaces", func(t *testing.T) {
kcl.cli = kfake.NewSimpleClientset() kcl.cli = kfake.NewSimpleClientset()
kcl.instanceID = "test" kcl.instanceID = "test"
kcl.IsKubeAdmin = true kcl.isKubeAdmin = true
cronJobs, err := kcl.GetCronJobs("") cronJobs, err := kcl.GetCronJobs("")
if err != nil { if err != nil {
@ -31,8 +31,8 @@ func (kcl *KubeClient) TestFetchCronJobs(t *testing.T) {
t.Run("non-admin client can fetch Cron Jobs from the default namespace only", func(t *testing.T) { t.Run("non-admin client can fetch Cron Jobs from the default namespace only", func(t *testing.T) {
kcl.cli = kfake.NewSimpleClientset() kcl.cli = kfake.NewSimpleClientset()
kcl.instanceID = "test" kcl.instanceID = "test"
kcl.IsKubeAdmin = false kcl.isKubeAdmin = false
kcl.NonAdminNamespaces = []string{"default"} kcl.SetClientNonAdminNamespaces([]string{"default"})
cronJobs, err := kcl.GetCronJobs("") cronJobs, err := kcl.GetCronJobs("")
if err != nil { if err != nil {

View File

@ -12,7 +12,7 @@ import (
// If the user is a kube admin, it returns all events in the namespace // If the user is a kube admin, it returns all events in the namespace
// Otherwise, it returns only the events in the non-admin namespaces // Otherwise, it returns only the events in the non-admin namespaces
func (kcl *KubeClient) GetEvents(namespace string, resourceId string) ([]models.K8sEvent, error) { func (kcl *KubeClient) GetEvents(namespace string, resourceId string) ([]models.K8sEvent, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchAllEvents(namespace, resourceId) return kcl.fetchAllEvents(namespace, resourceId)
} }
@ -22,7 +22,7 @@ func (kcl *KubeClient) GetEvents(namespace string, resourceId string) ([]models.
// fetchEventsForNonAdmin returns all events in the given namespace and resource // fetchEventsForNonAdmin returns all events in the given namespace and resource
// It returns only the events in the non-admin namespaces // It returns only the events in the non-admin namespaces
func (kcl *KubeClient) fetchEventsForNonAdmin(namespace string, resourceId string) ([]models.K8sEvent, error) { func (kcl *KubeClient) fetchEventsForNonAdmin(namespace string, resourceId string) ([]models.K8sEvent, error) {
if len(kcl.NonAdminNamespaces) == 0 { if len(kcl.GetClientNonAdminNamespaces()) == 0 {
return nil, nil return nil, nil
} }

View File

@ -19,7 +19,7 @@ func TestGetEvents(t *testing.T) {
kcl := &KubeClient{ kcl := &KubeClient{
cli: kfake.NewSimpleClientset(), cli: kfake.NewSimpleClientset(),
instanceID: "instance", instanceID: "instance",
IsKubeAdmin: true, isKubeAdmin: true,
} }
event := corev1.Event{ event := corev1.Event{
InvolvedObject: corev1.ObjectReference{UID: "resourceId"}, InvolvedObject: corev1.ObjectReference{UID: "resourceId"},
@ -49,8 +49,8 @@ func TestGetEvents(t *testing.T) {
kcl := &KubeClient{ kcl := &KubeClient{
cli: kfake.NewSimpleClientset(), cli: kfake.NewSimpleClientset(),
instanceID: "instance", instanceID: "instance",
IsKubeAdmin: false, isKubeAdmin: false,
NonAdminNamespaces: []string{"nonAdmin"}, nonAdminNamespaces: []string{"nonAdmin"},
} }
event := corev1.Event{ event := corev1.Event{
InvolvedObject: corev1.ObjectReference{UID: "resourceId"}, InvolvedObject: corev1.ObjectReference{UID: "resourceId"},
@ -81,8 +81,8 @@ func TestGetEvents(t *testing.T) {
kcl := &KubeClient{ kcl := &KubeClient{
cli: kfake.NewSimpleClientset(), cli: kfake.NewSimpleClientset(),
instanceID: "instance", instanceID: "instance",
IsKubeAdmin: false, isKubeAdmin: false,
NonAdminNamespaces: []string{"nonAdmin"}, nonAdminNamespaces: []string{"nonAdmin"},
} }
event := corev1.Event{ event := corev1.Event{
InvolvedObject: corev1.ObjectReference{UID: "resourceId"}, InvolvedObject: corev1.ObjectReference{UID: "resourceId"},

View File

@ -87,17 +87,22 @@ func (kcl *KubeClient) GetIngress(namespace, ingressName string) (models.K8sIngr
// GetIngresses gets all the ingresses for a given namespace in a k8s endpoint. // GetIngresses gets all the ingresses for a given namespace in a k8s endpoint.
func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) { func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchIngresses(namespace) return kcl.fetchIngresses(namespace)
} }
return kcl.fetchIngressesForNonAdmin(namespace) return kcl.fetchIngressesForNonAdmin(namespace)
} }
// fetchIngressesForNonAdmin gets all the ingresses for non-admin users in a k8s endpoint. // fetchIngressesForNonAdmin gets all the ingresses for non-admin users in a k8s endpoint.
func (kcl *KubeClient) fetchIngressesForNonAdmin(namespace string) ([]models.K8sIngressInfo, error) { func (kcl *KubeClient) fetchIngressesForNonAdmin(namespace string) ([]models.K8sIngressInfo, error) {
log.Debug().Msgf("Fetching ingresses for non-admin user: %v", kcl.NonAdminNamespaces) nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()
if len(kcl.NonAdminNamespaces) == 0 { log.Debug().
Strs("non_admin_namespaces", nonAdminNamespaces).
Msg("fetching ingresses for non-admin user")
if len(nonAdminNamespaces) == 0 {
return nil, nil return nil, nil
} }

View File

@ -0,0 +1,15 @@
package cli
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetIngresses(t *testing.T) {
kcl := &KubeClient{}
ingresses, err := kcl.GetIngresses("default")
require.NoError(t, err)
require.Empty(t, ingresses)
}

View File

@ -19,7 +19,7 @@ import (
// If the user is a kube admin, it returns all jobs in the namespace // If the user is a kube admin, it returns all jobs in the namespace
// Otherwise, it returns only the jobs in the non-admin namespaces // Otherwise, it returns only the jobs in the non-admin namespaces
func (kcl *KubeClient) GetJobs(namespace string, includeCronJobChildren bool) ([]models.K8sJob, error) { func (kcl *KubeClient) GetJobs(namespace string, includeCronJobChildren bool) ([]models.K8sJob, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchJobs(namespace, includeCronJobChildren) return kcl.fetchJobs(namespace, includeCronJobChildren)
} }

View File

@ -21,7 +21,7 @@ func (kcl *KubeClient) TestFetchJobs(t *testing.T) {
t.Run("admin client can fetch jobs from all namespaces", func(t *testing.T) { t.Run("admin client can fetch jobs from all namespaces", func(t *testing.T) {
kcl.cli = kfake.NewSimpleClientset() kcl.cli = kfake.NewSimpleClientset()
kcl.instanceID = "test" kcl.instanceID = "test"
kcl.IsKubeAdmin = true kcl.isKubeAdmin = true
jobs, err := kcl.GetJobs("", false) jobs, err := kcl.GetJobs("", false)
if err != nil { if err != nil {
@ -34,8 +34,8 @@ func (kcl *KubeClient) TestFetchJobs(t *testing.T) {
t.Run("non-admin client can fetch jobs from the default namespace only", func(t *testing.T) { t.Run("non-admin client can fetch jobs from the default namespace only", func(t *testing.T) {
kcl.cli = kfake.NewSimpleClientset() kcl.cli = kfake.NewSimpleClientset()
kcl.instanceID = "test" kcl.instanceID = "test"
kcl.IsKubeAdmin = false kcl.isKubeAdmin = false
kcl.NonAdminNamespaces = []string{"default"} kcl.SetClientNonAdminNamespaces([]string{"default"})
jobs, err := kcl.GetJobs("", false) jobs, err := kcl.GetJobs("", false)
if err != nil { if err != nil {

View File

@ -40,9 +40,10 @@ func defaultSystemNamespaces() map[string]struct{} {
// if the user is an admin, all namespaces in the current k8s environment(endpoint) are fetched using the fetchNamespaces function. // if the user is an admin, all namespaces in the current k8s environment(endpoint) are fetched using the fetchNamespaces function.
// otherwise, namespaces the non-admin user has access to will be used to filter the namespaces based on the allowed namespaces. // otherwise, namespaces the non-admin user has access to will be used to filter the namespaces based on the allowed namespaces.
func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, error) { func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchNamespaces() return kcl.fetchNamespaces()
} }
return kcl.fetchNamespacesForNonAdmin() return kcl.fetchNamespacesForNonAdmin()
} }
@ -52,7 +53,7 @@ func (kcl *KubeClient) fetchNamespacesForNonAdmin() (map[string]portainer.K8sNam
Str("context", "fetchNamespacesForNonAdmin"). Str("context", "fetchNamespacesForNonAdmin").
Msg("Fetching namespaces for non-admin user") Msg("Fetching namespaces for non-admin user")
if len(kcl.NonAdminNamespaces) == 0 { if len(kcl.GetClientNonAdminNamespaces()) == 0 {
return nil, nil return nil, nil
} }
@ -142,6 +143,7 @@ func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) (*corev1
Str("context", "CreateNamespace"). Str("context", "CreateNamespace").
Str("Namespace", info.Name). Str("Namespace", info.Name).
Msg("Failed to create the namespace") Msg("Failed to create the namespace")
return nil, err return nil, err
} }
@ -157,7 +159,7 @@ func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) (*corev1
return namespace, nil return namespace, nil
} }
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint. // UpdateNamespace updates a namespace in a k8s endpoint.
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) { func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) {
portainerLabels := map[string]string{ portainerLabels := map[string]string{
namespaceNameLabel: stackutils.SanitizeLabel(info.Name), namespaceNameLabel: stackutils.SanitizeLabel(info.Name),
@ -420,8 +422,10 @@ func (kcl *KubeClient) CombineNamespaceWithResourceQuota(namespace portainer.K8s
// buildNonAdminNamespacesMap builds a map of non-admin namespaces. // buildNonAdminNamespacesMap builds a map of non-admin namespaces.
// the map is used to filter the namespaces based on the allowed namespaces. // the map is used to filter the namespaces based on the allowed namespaces.
func (kcl *KubeClient) buildNonAdminNamespacesMap() map[string]struct{} { func (kcl *KubeClient) buildNonAdminNamespacesMap() map[string]struct{} {
nonAdminNamespaceSet := make(map[string]struct{}, len(kcl.NonAdminNamespaces)) nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()
for _, namespace := range kcl.NonAdminNamespaces { nonAdminNamespaceSet := make(map[string]struct{}, len(nonAdminNamespaces))
for _, namespace := range nonAdminNamespaces {
if !isSystemDefaultNamespace(namespace) { if !isSystemDefaultNamespace(namespace) {
nonAdminNamespaceSet[namespace] = struct{}{} nonAdminNamespaceSet[namespace] = struct{}{}
} }

View File

@ -176,6 +176,7 @@ func Test_ToggleSystemState(t *testing.T) {
expectedPolicies := map[string]portainer.K8sNamespaceAccessPolicy{ expectedPolicies := map[string]portainer.K8sNamespaceAccessPolicy{
"ns2": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}}, "ns2": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}},
} }
actualPolicies, err := kcl.GetNamespaceAccessPolicies() actualPolicies, err := kcl.GetNamespaceAccessPolicies()
assert.NoError(t, err, "failed to fetch policies") assert.NoError(t, err, "failed to fetch policies")
assert.Equal(t, expectedPolicies, actualPolicies) assert.Equal(t, expectedPolicies, actualPolicies)

View File

@ -46,9 +46,9 @@ func (kcl *KubeClient) GetNodesLimits() (portainer.K8sNodesLimits, error) {
// GetMaxResourceLimits gets the maximum CPU and Memory limits(unused resources) of all nodes in the current k8s environment(endpoint) connection, minus the accumulated resourcequotas for all namespaces except the one we're editing (skipNamespace) // GetMaxResourceLimits gets the maximum CPU and Memory limits(unused resources) of all nodes in the current k8s environment(endpoint) connection, minus the accumulated resourcequotas for all namespaces except the one we're editing (skipNamespace)
// if skipNamespace is set to "" then all namespaces are considered // if skipNamespace is set to "" then all namespaces are considered
func (client *KubeClient) GetMaxResourceLimits(skipNamespace string, overCommitEnabled bool, resourceOverCommitPercent int) (portainer.K8sNodeLimits, error) { func (kcl *KubeClient) GetMaxResourceLimits(skipNamespace string, overCommitEnabled bool, resourceOverCommitPercent int) (portainer.K8sNodeLimits, error) {
limits := portainer.K8sNodeLimits{} limits := portainer.K8sNodeLimits{}
nodes, err := client.cli.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) nodes, err := kcl.cli.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
if err != nil { if err != nil {
return limits, err return limits, err
} }
@ -62,7 +62,7 @@ func (client *KubeClient) GetMaxResourceLimits(skipNamespace string, overCommitE
limits.Memory = memory / 1000000 // B to MB limits.Memory = memory / 1000000 // B to MB
if !overCommitEnabled { if !overCommitEnabled {
namespaces, err := client.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil { if err != nil {
return limits, err return limits, err
} }
@ -77,7 +77,7 @@ func (client *KubeClient) GetMaxResourceLimits(skipNamespace string, overCommitE
} }
// minus accumulated resourcequotas for all namespaces except the one we're editing // minus accumulated resourcequotas for all namespaces except the one we're editing
resourceQuota, err := client.cli.CoreV1().ResourceQuotas(namespace.Name).List(context.TODO(), metav1.ListOptions{}) resourceQuota, err := kcl.cli.CoreV1().ResourceQuotas(namespace.Name).List(context.TODO(), metav1.ListOptions{})
if err != nil { if err != nil {
log.Debug().Msgf("error getting resourcequota for namespace %s: %s", namespace.Name, err) log.Debug().Msgf("error getting resourcequota for namespace %s: %s", namespace.Name, err)
continue // skip it continue // skip it

View File

@ -59,6 +59,7 @@ func Test_waitForPodStatus(t *testing.T) {
ctx, cancelFunc := context.WithTimeout(context.TODO(), 0*time.Second) ctx, cancelFunc := context.WithTimeout(context.TODO(), 0*time.Second)
defer cancelFunc() defer cancelFunc()
err = k.waitForPodStatus(ctx, v1.PodRunning, podSpec) err = k.waitForPodStatus(ctx, v1.PodRunning, podSpec)
if !errors.Is(err, context.DeadlineExceeded) { if !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("waitForPodStatus should throw deadline exceeded error; err=%s", err) t.Errorf("waitForPodStatus should throw deadline exceeded error; err=%s", err)

View File

@ -15,18 +15,23 @@ import (
// if the user is an admin, all resource quotas in all namespaces are fetched. // if the user is an admin, all resource quotas in all namespaces are fetched.
// otherwise, namespaces the non-admin user has access to will be used to filter the resource quotas. // otherwise, namespaces the non-admin user has access to will be used to filter the resource quotas.
func (kcl *KubeClient) GetResourceQuotas(namespace string) (*[]corev1.ResourceQuota, error) { func (kcl *KubeClient) GetResourceQuotas(namespace string) (*[]corev1.ResourceQuota, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchResourceQuotas(namespace) return kcl.fetchResourceQuotas(namespace)
} }
return kcl.fetchResourceQuotasForNonAdmin(namespace) return kcl.fetchResourceQuotasForNonAdmin(namespace)
} }
// fetchResourceQuotasForNonAdmin gets the resource quotas in the current k8s environment(endpoint) for a non-admin user. // fetchResourceQuotasForNonAdmin gets the resource quotas in the current k8s environment(endpoint) for a non-admin user.
// the role of the user must have read access to the resource quotas in the defined namespaces. // the role of the user must have read access to the resource quotas in the defined namespaces.
func (kcl *KubeClient) fetchResourceQuotasForNonAdmin(namespace string) (*[]corev1.ResourceQuota, error) { func (kcl *KubeClient) fetchResourceQuotasForNonAdmin(namespace string) (*[]corev1.ResourceQuota, error) {
log.Debug().Msgf("Fetching resource quotas for non-admin user: %v", kcl.NonAdminNamespaces) nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()
if len(kcl.NonAdminNamespaces) == 0 { log.Debug().
Strs("non_admin_namespaces", nonAdminNamespaces).
Msg("fetching resource quotas for non-admin user")
if len(nonAdminNamespaces) == 0 {
return nil, nil return nil, nil
} }

View File

@ -15,7 +15,7 @@ import (
// GetRoles gets all the roles for either at the cluster level or a given namespace in a k8s endpoint. // GetRoles gets all the roles for either at the cluster level or a given namespace in a k8s endpoint.
// It returns a list of K8sRole objects. // It returns a list of K8sRole objects.
func (kcl *KubeClient) GetRoles(namespace string) ([]models.K8sRole, error) { func (kcl *KubeClient) GetRoles(namespace string) ([]models.K8sRole, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchRoles(namespace) return kcl.fetchRoles(namespace)
} }

View File

@ -15,7 +15,7 @@ import (
// GetRoleBindings gets all the roleBindings for either at the cluster level or a given namespace in a k8s endpoint. // GetRoleBindings gets all the roleBindings for either at the cluster level or a given namespace in a k8s endpoint.
// It returns a list of K8sRoleBinding objects. // It returns a list of K8sRoleBinding objects.
func (kcl *KubeClient) GetRoleBindings(namespace string) ([]models.K8sRoleBinding, error) { func (kcl *KubeClient) GetRoleBindings(namespace string) ([]models.K8sRoleBinding, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchRoleBindings(namespace) return kcl.fetchRoleBindings(namespace)
} }

View File

@ -23,18 +23,23 @@ const (
// if the user is an admin, all secrets in the current k8s environment(endpoint) are fetched using the getSecrets function. // if the user is an admin, all secrets in the current k8s environment(endpoint) are fetched using the getSecrets function.
// otherwise, namespaces the non-admin user has access to will be used to filter the secrets based on the allowed namespaces. // otherwise, namespaces the non-admin user has access to will be used to filter the secrets based on the allowed namespaces.
func (kcl *KubeClient) GetSecrets(namespace string) ([]models.K8sSecret, error) { func (kcl *KubeClient) GetSecrets(namespace string) ([]models.K8sSecret, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.getSecrets(namespace) return kcl.getSecrets(namespace)
} }
return kcl.getSecretsForNonAdmin(namespace) return kcl.getSecretsForNonAdmin(namespace)
} }
// getSecretsForNonAdmin fetches the secrets in the namespaces the user has access to. // getSecretsForNonAdmin fetches the secrets in the namespaces the user has access to.
// This function is called when the user is not an admin. // This function is called when the user is not an admin.
func (kcl *KubeClient) getSecretsForNonAdmin(namespace string) ([]models.K8sSecret, error) { func (kcl *KubeClient) getSecretsForNonAdmin(namespace string) ([]models.K8sSecret, error) {
log.Debug().Msgf("Fetching secrets for non-admin user: %v", kcl.NonAdminNamespaces) nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()
if len(kcl.NonAdminNamespaces) == 0 { log.Debug().
Strs("non_admin_namespaces", nonAdminNamespaces).
Msg("fetching secrets for non-admin user")
if len(nonAdminNamespaces) == 0 {
return nil, nil return nil, nil
} }

View File

@ -15,9 +15,10 @@ import (
// GetServices gets all the services for either at the cluster level or a given namespace in a k8s endpoint. // GetServices gets all the services for either at the cluster level or a given namespace in a k8s endpoint.
// It returns a list of K8sServiceInfo objects. // It returns a list of K8sServiceInfo objects.
func (kcl *KubeClient) GetServices(namespace string) ([]models.K8sServiceInfo, error) { func (kcl *KubeClient) GetServices(namespace string) ([]models.K8sServiceInfo, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchServices(namespace) return kcl.fetchServices(namespace)
} }
return kcl.fetchServicesForNonAdmin(namespace) return kcl.fetchServicesForNonAdmin(namespace)
} }
@ -25,9 +26,13 @@ func (kcl *KubeClient) GetServices(namespace string) ([]models.K8sServiceInfo, e
// the namespace will be coming from NonAdminNamespaces as non-admin users are restricted to certain namespaces. // the namespace will be coming from NonAdminNamespaces as non-admin users are restricted to certain namespaces.
// it returns a list of K8sServiceInfo objects. // it returns a list of K8sServiceInfo objects.
func (kcl *KubeClient) fetchServicesForNonAdmin(namespace string) ([]models.K8sServiceInfo, error) { func (kcl *KubeClient) fetchServicesForNonAdmin(namespace string) ([]models.K8sServiceInfo, error) {
log.Debug().Msgf("Fetching services for non-admin user: %v", kcl.NonAdminNamespaces) nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()
if len(kcl.NonAdminNamespaces) == 0 { log.Debug().
Strs("non_admin_namespaces", nonAdminNamespaces).
Msg("fetching services for non-admin user")
if len(nonAdminNamespaces) == 0 {
return nil, nil return nil, nil
} }

View File

@ -16,7 +16,7 @@ import (
// GetServiceAccounts gets all the service accounts for either at the cluster level or a given namespace in a k8s endpoint. // GetServiceAccounts gets all the service accounts for either at the cluster level or a given namespace in a k8s endpoint.
// It returns a list of K8sServiceAccount objects. // It returns a list of K8sServiceAccount objects.
func (kcl *KubeClient) GetServiceAccounts(namespace string) ([]models.K8sServiceAccount, error) { func (kcl *KubeClient) GetServiceAccounts(namespace string) ([]models.K8sServiceAccount, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchServiceAccounts(namespace) return kcl.fetchServiceAccounts(namespace)
} }

View File

@ -0,0 +1,15 @@
package cli
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetServices(t *testing.T) {
kcl := &KubeClient{}
services, err := kcl.GetServices("default")
require.NoError(t, err)
require.Empty(t, services)
}

View File

@ -18,9 +18,10 @@ import (
// If the user is not an admin, it fetches the volumes in the namespaces the user has access to. // If the user is not an admin, it fetches the volumes in the namespaces the user has access to.
// It returns a list of K8sVolumeInfo. // It returns a list of K8sVolumeInfo.
func (kcl *KubeClient) GetVolumes(namespace string) ([]models.K8sVolumeInfo, error) { func (kcl *KubeClient) GetVolumes(namespace string) ([]models.K8sVolumeInfo, error) {
if kcl.IsKubeAdmin { if kcl.GetIsKubeAdmin() {
return kcl.fetchVolumes(namespace) return kcl.fetchVolumes(namespace)
} }
return kcl.fetchVolumesForNonAdmin(namespace) return kcl.fetchVolumesForNonAdmin(namespace)
} }
@ -48,9 +49,13 @@ func (kcl *KubeClient) GetVolume(namespace, volumeName string) (*models.K8sVolum
// This function is called when the user is not an admin. // This function is called when the user is not an admin.
// It fetches all the persistent volume claims, persistent volumes and storage classes in the namespaces the user has access to. // It fetches all the persistent volume claims, persistent volumes and storage classes in the namespaces the user has access to.
func (kcl *KubeClient) fetchVolumesForNonAdmin(namespace string) ([]models.K8sVolumeInfo, error) { func (kcl *KubeClient) fetchVolumesForNonAdmin(namespace string) ([]models.K8sVolumeInfo, error) {
log.Debug().Msgf("Fetching volumes for non-admin user: %v", kcl.NonAdminNamespaces) nonAdminNamespaces := kcl.GetClientNonAdminNamespaces()
if len(kcl.NonAdminNamespaces) == 0 { log.Debug().
Strs("non_admin_namespaces", nonAdminNamespaces).
Msg("fetching volumes for non-admin user")
if len(nonAdminNamespaces) == 0 {
return nil, nil return nil, nil
} }

View File

@ -0,0 +1,15 @@
package cli
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetVolumes(t *testing.T) {
kcl := &KubeClient{}
volumes, err := kcl.GetVolumes("default")
require.NoError(t, err)
require.Empty(t, volumes)
}