2021-08-26 14:00:59 +00:00
package cli
import (
2021-10-12 02:32:14 +00:00
"context"
2022-09-21 04:49:42 +00:00
"fmt"
2024-10-01 01:15:51 +00:00
"net/http"
2021-08-26 14:00:59 +00:00
"strconv"
2024-10-01 01:15:51 +00:00
"time"
2021-08-26 14:00:59 +00:00
"github.com/pkg/errors"
2022-09-21 04:49:42 +00:00
portainer "github.com/portainer/portainer/api"
2022-11-13 19:33:57 +00:00
models "github.com/portainer/portainer/api/http/models/kubernetes"
2024-06-12 23:06:17 +00:00
"github.com/portainer/portainer/api/stacks/stackutils"
2024-10-01 01:15:51 +00:00
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
2023-10-11 19:32:02 +00:00
"github.com/rs/zerolog/log"
2024-10-01 01:15:51 +00:00
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2023-10-11 19:32:02 +00:00
"k8s.io/apimachinery/pkg/api/resource"
2021-08-26 14:00:59 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
systemNamespaceLabel = "io.portainer.kubernetes.namespace.system"
2024-10-01 01:15:51 +00:00
namespaceOwnerLabel = "io.portainer.kubernetes.resourcepool.owner"
namespaceNameLabel = "io.portainer.kubernetes.resourcepool.name"
2021-08-26 14:00:59 +00:00
)
func defaultSystemNamespaces ( ) map [ string ] struct { } {
return map [ string ] struct { } {
"kube-system" : { } ,
"kube-public" : { } ,
"kube-node-lease" : { } ,
"portainer" : { } ,
}
}
2022-09-21 04:49:42 +00:00
// GetNamespaces gets the namespaces in the current k8s environment(endpoint).
2024-10-01 01:15:51 +00:00
// 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.
2022-09-21 04:49:42 +00:00
func ( kcl * KubeClient ) GetNamespaces ( ) ( map [ string ] portainer . K8sNamespaceInfo , error ) {
2024-10-01 01:15:51 +00:00
if kcl . IsKubeAdmin {
return kcl . fetchNamespaces ( )
}
return kcl . fetchNamespacesForNonAdmin ( )
}
// fetchNamespacesForNonAdmin gets the namespaces in the current k8s environment(endpoint) for the non-admin user.
func ( kcl * KubeClient ) fetchNamespacesForNonAdmin ( ) ( map [ string ] portainer . K8sNamespaceInfo , error ) {
log . Debug ( ) . Msgf ( "Fetching namespaces for non-admin user: %v" , kcl . NonAdminNamespaces )
if len ( kcl . NonAdminNamespaces ) == 0 {
return nil , nil
}
namespaces , err := kcl . fetchNamespaces ( )
2022-09-21 04:49:42 +00:00
if err != nil {
2024-10-01 01:15:51 +00:00
return nil , fmt . Errorf ( "an error occurred during the fetchNamespacesForNonAdmin operation, unable to list namespaces for the non-admin user: %w" , err )
2022-09-21 04:49:42 +00:00
}
2024-10-01 01:15:51 +00:00
nonAdminNamespaceSet := kcl . buildNonAdminNamespacesMap ( )
2022-09-21 04:49:42 +00:00
results := make ( map [ string ] portainer . K8sNamespaceInfo )
2024-10-01 01:15:51 +00:00
for _ , namespace := range namespaces {
if _ , exists := nonAdminNamespaceSet [ namespace . Name ] ; exists {
results [ namespace . Name ] = namespace
2022-09-21 04:49:42 +00:00
}
}
return results , nil
}
2024-10-01 01:15:51 +00:00
// fetchNamespaces gets the namespaces in the current k8s environment(endpoint).
// this function is used by both admin and non-admin users.
// the result gets parsed to a map of namespace name to namespace info.
func ( kcl * KubeClient ) fetchNamespaces ( ) ( map [ string ] portainer . K8sNamespaceInfo , error ) {
namespaces , err := kcl . cli . CoreV1 ( ) . Namespaces ( ) . List ( context . TODO ( ) , metav1 . ListOptions { } )
if err != nil {
return nil , fmt . Errorf ( "an error occurred during the fetchNamespacesForAdmin operation, unable to list namespaces for the admin user: %w" , err )
}
results := make ( map [ string ] portainer . K8sNamespaceInfo )
for _ , namespace := range namespaces . Items {
results [ namespace . Name ] = parseNamespace ( & namespace )
}
return results , nil
}
// parseNamespace converts a k8s namespace object to a portainer namespace object.
func parseNamespace ( namespace * corev1 . Namespace ) portainer . K8sNamespaceInfo {
return portainer . K8sNamespaceInfo {
Id : string ( namespace . UID ) ,
Name : namespace . Name ,
Status : namespace . Status ,
CreationDate : namespace . CreationTimestamp . Format ( time . RFC3339 ) ,
NamespaceOwner : namespace . Labels [ namespaceOwnerLabel ] ,
IsSystem : isSystemNamespace ( * namespace ) ,
IsDefault : namespace . Name == defaultNamespace ,
}
}
2022-10-27 03:14:54 +00:00
// GetNamespace gets the namespace in the current k8s environment(endpoint).
func ( kcl * KubeClient ) GetNamespace ( name string ) ( portainer . K8sNamespaceInfo , error ) {
namespace , err := kcl . cli . CoreV1 ( ) . Namespaces ( ) . Get ( context . TODO ( ) , name , metav1 . GetOptions { } )
if err != nil {
return portainer . K8sNamespaceInfo { } , err
}
2024-10-01 01:15:51 +00:00
return parseNamespace ( namespace ) , nil
2022-10-27 03:14:54 +00:00
}
2022-12-20 03:46:51 +00:00
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
2024-10-01 01:15:51 +00:00
func ( kcl * KubeClient ) CreateNamespace ( info models . K8sNamespaceDetails ) ( * corev1 . Namespace , error ) {
2023-10-16 18:15:44 +00:00
portainerLabels := map [ string ] string {
2024-10-01 01:15:51 +00:00
namespaceNameLabel : stackutils . SanitizeLabel ( info . Name ) ,
namespaceOwnerLabel : stackutils . SanitizeLabel ( info . Owner ) ,
2023-10-16 18:15:44 +00:00
}
2022-09-21 04:49:42 +00:00
2024-10-01 01:15:51 +00:00
var ns corev1 . Namespace
2022-09-21 04:49:42 +00:00
ns . Name = info . Name
ns . Annotations = info . Annotations
2023-10-16 18:15:44 +00:00
ns . Labels = portainerLabels
2022-09-21 04:49:42 +00:00
2024-10-01 01:15:51 +00:00
namespace , err := kcl . cli . CoreV1 ( ) . Namespaces ( ) . Create ( context . Background ( ) , & ns , metav1 . CreateOptions { } )
2023-10-11 19:32:02 +00:00
if err != nil {
log . Error ( ) .
Err ( err ) .
Str ( "Namespace" , info . Name ) .
2024-02-15 19:20:24 +00:00
Msg ( "Failed to create the namespace" )
2024-10-01 01:15:51 +00:00
return nil , err
2023-10-11 19:32:02 +00:00
}
2024-02-15 19:20:24 +00:00
if info . ResourceQuota != nil && info . ResourceQuota . Enabled {
2023-10-11 19:32:02 +00:00
log . Info ( ) . Msgf ( "Creating resource quota for namespace %s" , info . Name )
log . Debug ( ) . Msgf ( "Creating resource quota with details: %+v" , info . ResourceQuota )
2024-10-01 01:15:51 +00:00
resourceQuota := & corev1 . ResourceQuota {
2024-02-15 19:20:24 +00:00
ObjectMeta : metav1 . ObjectMeta {
Name : "portainer-rq-" + info . Name ,
Namespace : info . Name ,
Labels : portainerLabels ,
} ,
2024-10-01 01:15:51 +00:00
Spec : corev1 . ResourceQuotaSpec {
Hard : corev1 . ResourceList { } ,
2024-02-15 19:20:24 +00:00
} ,
}
2023-10-11 19:32:02 +00:00
if info . ResourceQuota . Enabled {
memory := resource . MustParse ( info . ResourceQuota . Memory )
cpu := resource . MustParse ( info . ResourceQuota . CPU )
if memory . Value ( ) > 0 {
memQuota := memory
2024-10-01 01:15:51 +00:00
resourceQuota . Spec . Hard [ corev1 . ResourceLimitsMemory ] = memQuota
resourceQuota . Spec . Hard [ corev1 . ResourceRequestsMemory ] = memQuota
2023-10-11 19:32:02 +00:00
}
if cpu . Value ( ) > 0 {
cpuQuota := cpu
2024-10-01 01:15:51 +00:00
resourceQuota . Spec . Hard [ corev1 . ResourceLimitsCPU ] = cpuQuota
resourceQuota . Spec . Hard [ corev1 . ResourceRequestsCPU ] = cpuQuota
2023-10-11 19:32:02 +00:00
}
}
_ , err := kcl . cli . CoreV1 ( ) . ResourceQuotas ( info . Name ) . Create ( context . Background ( ) , resourceQuota , metav1 . CreateOptions { } )
if err != nil {
log . Error ( ) . Msgf ( "Failed to create resource quota for namespace %s: %s" , info . Name , err )
2024-10-01 01:15:51 +00:00
return nil , err
2023-10-11 19:32:02 +00:00
}
}
2024-10-01 01:15:51 +00:00
return namespace , nil
2022-09-21 04:49:42 +00:00
}
2024-10-01 01:15:51 +00:00
func isSystemNamespace ( namespace corev1 . Namespace ) bool {
2021-08-26 14:00:59 +00:00
systemLabelValue , hasSystemLabel := namespace . Labels [ systemNamespaceLabel ]
if hasSystemLabel {
return systemLabelValue == "true"
}
systemNamespaces := defaultSystemNamespaces ( )
_ , isSystem := systemNamespaces [ namespace . Name ]
return isSystem
}
// ToggleSystemState will set a namespace as a system namespace, or remove this state
// if isSystem is true it will set `systemNamespaceLabel` to "true" and false otherwise
// this will skip if namespace is "default" or if the required state is already set
func ( kcl * KubeClient ) ToggleSystemState ( namespaceName string , isSystem bool ) error {
if namespaceName == "default" {
return nil
}
nsService := kcl . cli . CoreV1 ( ) . Namespaces ( )
2021-10-12 02:32:14 +00:00
namespace , err := nsService . Get ( context . TODO ( ) , namespaceName , metav1 . GetOptions { } )
2021-08-26 14:00:59 +00:00
if err != nil {
return errors . Wrap ( err , "failed fetching namespace object" )
}
if isSystemNamespace ( * namespace ) == isSystem {
return nil
}
if namespace . Labels == nil {
namespace . Labels = map [ string ] string { }
}
namespace . Labels [ systemNamespaceLabel ] = strconv . FormatBool ( isSystem )
2021-10-12 02:32:14 +00:00
_ , err = nsService . Update ( context . TODO ( ) , namespace , metav1 . UpdateOptions { } )
2021-08-26 14:00:59 +00:00
if err != nil {
return errors . Wrap ( err , "failed updating namespace object" )
}
if isSystem {
return kcl . NamespaceAccessPoliciesDeleteNamespace ( namespaceName )
}
return nil
}
2022-09-21 04:49:42 +00:00
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint.
2024-10-01 01:15:51 +00:00
func ( kcl * KubeClient ) UpdateNamespace ( info models . K8sNamespaceDetails ) ( * corev1 . Namespace , error ) {
namespace := corev1 . Namespace {
ObjectMeta : metav1 . ObjectMeta {
Name : info . Name ,
Annotations : info . Annotations ,
} ,
}
2022-09-21 04:49:42 +00:00
2024-10-01 01:15:51 +00:00
return kcl . cli . CoreV1 ( ) . Namespaces ( ) . Update ( context . Background ( ) , & namespace , metav1 . UpdateOptions { } )
2022-09-21 04:49:42 +00:00
}
2024-10-01 01:15:51 +00:00
func ( kcl * KubeClient ) DeleteNamespace ( namespaceName string ) ( * corev1 . Namespace , error ) {
namespace , err := kcl . cli . CoreV1 ( ) . Namespaces ( ) . Get ( context . Background ( ) , namespaceName , metav1 . GetOptions { } )
2022-09-21 04:49:42 +00:00
if err != nil {
2024-10-01 01:15:51 +00:00
return nil , err
2022-09-21 04:49:42 +00:00
}
2024-10-01 01:15:51 +00:00
err = kcl . cli . CoreV1 ( ) . Namespaces ( ) . Delete ( context . Background ( ) , namespaceName , metav1 . DeleteOptions { } )
if err != nil {
return nil , err
}
return namespace , nil
}
// CombineNamespacesWithResourceQuotas combines namespaces with resource quotas where matching is based on "portainer-rq-"+namespace.Name
func ( kcl * KubeClient ) CombineNamespacesWithResourceQuotas ( namespaces map [ string ] portainer . K8sNamespaceInfo , w http . ResponseWriter ) * httperror . HandlerError {
resourceQuotas , err := kcl . GetResourceQuotas ( "" )
if err != nil && ! k8serrors . IsNotFound ( err ) {
return httperror . InternalServerError ( "an error occurred during the CombineNamespacesWithResourceQuotas operation, unable to retrieve resource quotas from the Kubernetes for an admin user. Error: " , err )
2022-09-21 04:49:42 +00:00
}
2024-10-01 01:15:51 +00:00
if len ( * resourceQuotas ) > 0 {
return response . JSON ( w , kcl . UpdateNamespacesWithResourceQuotas ( namespaces , * resourceQuotas ) )
}
return response . JSON ( w , kcl . ConvertNamespaceMapToSlice ( namespaces ) )
}
// CombineNamespaceWithResourceQuota combines a namespace with a resource quota prefixed with "portainer-rq-"+namespace.Name
func ( kcl * KubeClient ) CombineNamespaceWithResourceQuota ( namespace portainer . K8sNamespaceInfo , w http . ResponseWriter ) * httperror . HandlerError {
resourceQuota , err := kcl . GetPortainerResourceQuota ( namespace . Name )
if err != nil && ! k8serrors . IsNotFound ( err ) {
return httperror . InternalServerError ( fmt . Sprintf ( "an error occurred during the CombineNamespaceWithResourceQuota operation, unable to retrieve the resource quota associated with the namespace: %s for a non-admin user. Error: " , namespace . Name ) , err )
}
if resourceQuota != nil {
namespace . ResourceQuota = resourceQuota
}
return response . JSON ( w , namespace )
}
// buildNonAdminNamespacesMap builds a map of non-admin namespaces.
// the map is used to filter the namespaces based on the allowed namespaces.
func ( kcl * KubeClient ) buildNonAdminNamespacesMap ( ) map [ string ] struct { } {
nonAdminNamespaceSet := make ( map [ string ] struct { } , len ( kcl . NonAdminNamespaces ) )
for _ , namespace := range kcl . NonAdminNamespaces {
nonAdminNamespaceSet [ namespace ] = struct { } { }
}
return nonAdminNamespaceSet
}
// ConvertNamespaceMapToSlice converts the namespace map to a slice of namespaces.
// this is used to for the API response.
func ( kcl * KubeClient ) ConvertNamespaceMapToSlice ( namespaces map [ string ] portainer . K8sNamespaceInfo ) [ ] portainer . K8sNamespaceInfo {
namespaceSlice := make ( [ ] portainer . K8sNamespaceInfo , 0 , len ( namespaces ) )
for _ , namespace := range namespaces {
namespaceSlice = append ( namespaceSlice , namespace )
}
return namespaceSlice
2022-09-21 04:49:42 +00:00
}