mirror of https://github.com/portainer/portainer
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
300 lines
10 KiB
300 lines
10 KiB
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
portainer "github.com/portainer/portainer/api"
|
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
|
"github.com/rs/zerolog/log"
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
const (
|
|
systemNamespaceLabel = "io.portainer.kubernetes.namespace.system"
|
|
namespaceOwnerLabel = "io.portainer.kubernetes.resourcepool.owner"
|
|
namespaceNameLabel = "io.portainer.kubernetes.resourcepool.name"
|
|
)
|
|
|
|
func defaultSystemNamespaces() map[string]struct{} {
|
|
return map[string]struct{}{
|
|
"kube-system": {},
|
|
"kube-public": {},
|
|
"kube-node-lease": {},
|
|
"portainer": {},
|
|
}
|
|
}
|
|
|
|
// GetNamespaces gets the namespaces in the current k8s environment(endpoint).
|
|
// 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.
|
|
func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, error) {
|
|
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()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("an error occurred during the fetchNamespacesForNonAdmin operation, unable to list namespaces for the non-admin user: %w", err)
|
|
}
|
|
|
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
|
results := make(map[string]portainer.K8sNamespaceInfo)
|
|
for _, namespace := range namespaces {
|
|
if _, exists := nonAdminNamespaceSet[namespace.Name]; exists {
|
|
results[namespace.Name] = namespace
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return parseNamespace(namespace), nil
|
|
}
|
|
|
|
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
|
|
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) {
|
|
portainerLabels := map[string]string{
|
|
namespaceNameLabel: stackutils.SanitizeLabel(info.Name),
|
|
namespaceOwnerLabel: stackutils.SanitizeLabel(info.Owner),
|
|
}
|
|
|
|
var ns corev1.Namespace
|
|
ns.Name = info.Name
|
|
ns.Annotations = info.Annotations
|
|
ns.Labels = portainerLabels
|
|
|
|
namespace, err := kcl.cli.CoreV1().Namespaces().Create(context.Background(), &ns, metav1.CreateOptions{})
|
|
if err != nil {
|
|
log.Error().
|
|
Err(err).
|
|
Str("Namespace", info.Name).
|
|
Msg("Failed to create the namespace")
|
|
return nil, err
|
|
}
|
|
|
|
if info.ResourceQuota != nil && info.ResourceQuota.Enabled {
|
|
log.Info().Msgf("Creating resource quota for namespace %s", info.Name)
|
|
log.Debug().Msgf("Creating resource quota with details: %+v", info.ResourceQuota)
|
|
|
|
resourceQuota := &corev1.ResourceQuota{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "portainer-rq-" + info.Name,
|
|
Namespace: info.Name,
|
|
Labels: portainerLabels,
|
|
},
|
|
Spec: corev1.ResourceQuotaSpec{
|
|
Hard: corev1.ResourceList{},
|
|
},
|
|
}
|
|
|
|
if info.ResourceQuota.Enabled {
|
|
memory := resource.MustParse(info.ResourceQuota.Memory)
|
|
cpu := resource.MustParse(info.ResourceQuota.CPU)
|
|
if memory.Value() > 0 {
|
|
memQuota := memory
|
|
resourceQuota.Spec.Hard[corev1.ResourceLimitsMemory] = memQuota
|
|
resourceQuota.Spec.Hard[corev1.ResourceRequestsMemory] = memQuota
|
|
}
|
|
|
|
if cpu.Value() > 0 {
|
|
cpuQuota := cpu
|
|
resourceQuota.Spec.Hard[corev1.ResourceLimitsCPU] = cpuQuota
|
|
resourceQuota.Spec.Hard[corev1.ResourceRequestsCPU] = cpuQuota
|
|
}
|
|
}
|
|
|
|
_, 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)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return namespace, nil
|
|
}
|
|
|
|
func isSystemNamespace(namespace corev1.Namespace) bool {
|
|
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()
|
|
|
|
namespace, err := nsService.Get(context.TODO(), namespaceName, metav1.GetOptions{})
|
|
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)
|
|
|
|
_, err = nsService.Update(context.TODO(), namespace, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed updating namespace object")
|
|
}
|
|
|
|
if isSystem {
|
|
return kcl.NamespaceAccessPoliciesDeleteNamespace(namespaceName)
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint.
|
|
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) {
|
|
namespace := corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: info.Name,
|
|
Annotations: info.Annotations,
|
|
},
|
|
}
|
|
|
|
return kcl.cli.CoreV1().Namespaces().Update(context.Background(), &namespace, metav1.UpdateOptions{})
|
|
}
|
|
|
|
func (kcl *KubeClient) DeleteNamespace(namespaceName string) (*corev1.Namespace, error) {
|
|
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.Background(), namespaceName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|