1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 00:09:40 +02:00

refactor(namespace): migrate namespace edit to react [r8s-125] (#38)

This commit is contained in:
Ali 2024-12-11 10:15:46 +13:00 committed by GitHub
parent 40c7742e46
commit ce7e0d8d60
108 changed files with 3183 additions and 2194 deletions

View file

@ -0,0 +1,51 @@
package kubernetes
import (
"bytes"
"io"
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
)
// @id UpdateKubernetesNamespaceDeprecated
// @summary Update a namespace
// @description Update a namespace within the given environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace"
// @param body body models.K8sNamespaceDetails true "Namespace details"
// @success 200 {object} portainer.K8sNamespaceInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific namespace."
// @failure 500 "Server error occurred while attempting to update the namespace."
// @router /kubernetes/{id}/namespaces [put]
func deprecatedNamespaceParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) {
environmentId, err := request.RetrieveRouteVariableValue(r, "id")
if err != nil {
return "", httperror.BadRequest("Invalid query parameter: id", err)
}
// Restore the original body for further use
bodyBytes, err := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
payload := models.K8sNamespaceDetails{}
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return "", httperror.BadRequest("Invalid request. Unable to parse namespace payload", err)
}
namespaceName := payload.Name
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
return "/kubernetes/" + environmentId + "/namespaces/" + namespaceName, nil
}

View file

@ -81,11 +81,11 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
endpointRouter.Handle("/services/delete", httperror.LoggerHandler(h.deleteKubernetesServices)).Methods(http.MethodPost)
endpointRouter.Handle("/rbac_enabled", httperror.LoggerHandler(h.getKubernetesRBACStatus)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.createKubernetesNamespace)).Methods(http.MethodPost)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.deleteKubernetesNamespace)).Methods(http.MethodDelete)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.getKubernetesNamespaces)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces/count", httperror.LoggerHandler(h.getKubernetesNamespacesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces/{namespace}", httperror.LoggerHandler(h.getKubernetesNamespace)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces/{namespace}", httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
endpointRouter.Handle("/volumes", httperror.LoggerHandler(h.GetAllKubernetesVolumes)).Methods(http.MethodGet)
endpointRouter.Handle("/volumes/count", httperror.LoggerHandler(h.getAllKubernetesVolumesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/service_accounts", httperror.LoggerHandler(h.getAllKubernetesServiceAccounts)).Methods(http.MethodGet)
@ -115,8 +115,12 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.createKubernetesService)).Methods(http.MethodPost)
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.updateKubernetesService)).Methods(http.MethodPut)
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.getKubernetesServicesByNamespace)).Methods(http.MethodGet)
namespaceRouter.Handle("/volumes", httperror.LoggerHandler(h.GetKubernetesVolumesInNamespace)).Methods(http.MethodGet)
namespaceRouter.Handle("/volumes/{volume}", httperror.LoggerHandler(h.getKubernetesVolume)).Methods(http.MethodGet)
// Deprecated
endpointRouter.Handle("/namespaces", middlewares.Deprecated(endpointRouter, deprecatedNamespaceParser)).Methods(http.MethodPut)
return h
}

View file

@ -27,7 +27,7 @@ import (
// @failure 500 "Server error occurred while attempting to retrieve kubernetes volumes."
// @router /kubernetes/{id}/volumes [get]
func (handler *Handler) GetAllKubernetesVolumes(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
volumes, err := handler.getKubernetesVolumes(r)
volumes, err := handler.getKubernetesVolumes(r, "")
if err != nil {
return err
}
@ -49,7 +49,7 @@ func (handler *Handler) GetAllKubernetesVolumes(w http.ResponseWriter, r *http.R
// @failure 500 "Server error occurred while attempting to retrieve kubernetes volumes count."
// @router /kubernetes/{id}/volumes/count [get]
func (handler *Handler) getAllKubernetesVolumesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
volumes, err := handler.getKubernetesVolumes(r)
volumes, err := handler.getKubernetesVolumes(r, "")
if err != nil {
return err
}
@ -57,6 +57,36 @@ func (handler *Handler) getAllKubernetesVolumesCount(w http.ResponseWriter, r *h
return response.JSON(w, len(volumes))
}
// @id GetKubernetesVolumesInNamespace
// @summary Get Kubernetes volumes within a namespace in the given Portainer environment
// @description Get a list of kubernetes volumes within the specified namespace in the given environment (Endpoint). The Endpoint ID must be a valid Portainer environment identifier.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace identifier"
// @param withApplications query boolean false "When set to True, include the applications that are using the volumes. It is set to false by default"
// @success 200 {object} map[string]kubernetes.K8sVolumeInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 403 "Unauthorized access or operation not allowed."
// @failure 500 "Server error occurred while attempting to retrieve kubernetes volumes in the namespace."
// @router /kubernetes/{id}/namespaces/{namespace}/volumes [get]
func (handler *Handler) GetKubernetesVolumesInNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesVolumesInNamespace").Msg("Unable to retrieve namespace identifier")
return httperror.BadRequest("Invalid namespace identifier", err)
}
volumes, httpErr := handler.getKubernetesVolumes(r, namespace)
if httpErr != nil {
return httpErr
}
return response.JSON(w, volumes)
}
// @id GetKubernetesVolume
// @summary Get a Kubernetes volume within the given Portainer environment
// @description Get a Kubernetes volume within the given environment (Endpoint). The Endpoint ID must be a valid Portainer environment identifier.
@ -109,7 +139,7 @@ func (handler *Handler) getKubernetesVolume(w http.ResponseWriter, r *http.Reque
return response.JSON(w, volume)
}
func (handler *Handler) getKubernetesVolumes(r *http.Request) ([]models.K8sVolumeInfo, *httperror.HandlerError) {
func (handler *Handler) getKubernetesVolumes(r *http.Request, namespace string) ([]models.K8sVolumeInfo, *httperror.HandlerError) {
withApplications, err := request.RetrieveBooleanQueryParameter(r, "withApplications", true)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Bool("withApplications", withApplications).Msg("Unable to parse query parameter")
@ -122,7 +152,7 @@ func (handler *Handler) getKubernetesVolumes(r *http.Request) ([]models.K8sVolum
return nil, httperror.InternalServerError("Failed to prepare Kubernetes client", httpErr)
}
volumes, err := cli.GetVolumes("")
volumes, err := cli.GetVolumes(namespace)
if err != nil {
if k8serrors.IsUnauthorized(err) {
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Msg("Unauthorized access")

View file

@ -47,7 +47,9 @@ func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, e
// 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)
log.Debug().
Str("context", "fetchNamespacesForNonAdmin").
Msg("Fetching namespaces for non-admin user")
if len(kcl.NonAdminNamespaces) == 0 {
return nil, nil
@ -75,6 +77,11 @@ func (kcl *KubeClient) fetchNamespacesForNonAdmin() (map[string]portainer.K8sNam
func (kcl *KubeClient) fetchNamespaces() (map[string]portainer.K8sNamespaceInfo, error) {
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
log.Error().
Str("context", "fetchNamespaces").
Err(err).
Msg("Failed to list namespaces")
return nil, fmt.Errorf("an error occurred during the fetchNamespacesForAdmin operation, unable to list namespaces for the admin user: %w", err)
}
@ -92,6 +99,7 @@ func parseNamespace(namespace *corev1.Namespace) portainer.K8sNamespaceInfo {
Id: string(namespace.UID),
Name: namespace.Name,
Status: namespace.Status,
Annotations: namespace.Annotations,
CreationDate: namespace.CreationTimestamp.Format(time.RFC3339),
NamespaceOwner: namespace.Labels[namespaceOwnerLabel],
IsSystem: isSystemNamespace(namespace),
@ -103,13 +111,18 @@ func parseNamespace(namespace *corev1.Namespace) portainer.K8sNamespaceInfo {
func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, error) {
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
log.Error().
Str("context", "GetNamespace").
Str("namespace", name).
Err(err).
Msg("Failed to get namespace")
return portainer.K8sNamespaceInfo{}, err
}
return parseNamespace(namespace), nil
}
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
// CreateNamespace creates a new namespace in a k8s endpoint.
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) {
portainerLabels := map[string]string{
namespaceNameLabel: stackutils.SanitizeLabel(info.Name),
@ -125,52 +138,127 @@ func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) (*corev1
if err != nil {
log.Error().
Err(err).
Str("context", "CreateNamespace").
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
}
if err := kcl.createOrUpdateNamespaceResourceQuota(info, portainerLabels); err != nil {
log.Error().
Err(err).
Str("context", "CreateNamespace").
Str("name", info.Name).
Msg("failed to create or update resource quota for namespace")
return nil, err
}
return namespace, nil
}
// UpdateIngress updates an ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) UpdateNamespace(info models.K8sNamespaceDetails) (*corev1.Namespace, error) {
portainerLabels := map[string]string{
namespaceNameLabel: stackutils.SanitizeLabel(info.Name),
namespaceOwnerLabel: stackutils.SanitizeLabel(info.Owner),
}
namespace := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: info.Name,
Annotations: info.Annotations,
},
}
updatedNamespace, err := kcl.cli.CoreV1().Namespaces().Update(context.Background(), &namespace, metav1.UpdateOptions{})
if err != nil {
log.Error().
Str("context", "UpdateNamespace").
Str("namespace", info.Name).
Err(err).
Msg("Failed to update namespace")
return nil, err
}
if err := kcl.createOrUpdateNamespaceResourceQuota(info, portainerLabels); err != nil {
log.Error().
Err(err).
Str("context", "UpdateNamespace").
Str("name", info.Name).
Msg("failed to create or update resource quota for namespace")
return nil, err
}
return updatedNamespace, nil
}
func (kcl *KubeClient) createOrUpdateNamespaceResourceQuota(info models.K8sNamespaceDetails, portainerLabels map[string]string) error {
if !info.ResourceQuota.Enabled {
if err := kcl.deleteNamespaceResourceQuota(info.Name); err != nil {
log.Debug().Err(err).Str("context", "createOrUpdateNamespaceResourceQuota").Str("name", info.Name).Msg("failed to delete resource quota for namespace")
}
return nil
}
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).Update(context.Background(), resourceQuota, metav1.UpdateOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
log.Warn().
Str("context", "createOrUpdateNamespaceResourceQuota").
Str("name", info.Name).
Msg("resource quota not found, creating")
_, err = kcl.cli.CoreV1().ResourceQuotas(info.Name).Create(context.Background(), resourceQuota, metav1.CreateOptions{})
}
}
return err
}
func (kcl *KubeClient) deleteNamespaceResourceQuota(namespaceName string) error {
err := kcl.cli.CoreV1().ResourceQuotas(namespaceName).Delete(context.Background(), "portainer-rq-"+namespaceName, metav1.DeleteOptions{})
if err != nil && !k8serrors.IsNotFound(err) {
log.Error().
Str("context", "deleteNamespaceResourceQuota").
Str("name", namespaceName).
Err(err).
Msg("failed to delete resource quota for namespace")
return err
}
log.Warn().
Str("context", "deleteNamespaceResourceQuota").
Str("name", namespaceName).
Msg("resource quota to delete not found")
return nil
}
func isSystemNamespace(namespace *corev1.Namespace) bool {
systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel]
if hasSystemLabel {
@ -180,7 +268,6 @@ func isSystemNamespace(namespace *corev1.Namespace) bool {
systemNamespaces := defaultSystemNamespaces()
_, isSystem := systemNamespaces[namespace.Name]
return isSystem
}
@ -201,10 +288,13 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er
return nil
}
nsService := kcl.cli.CoreV1().Namespaces()
namespace, err := nsService.Get(context.TODO(), namespaceName, metav1.GetOptions{})
namespace, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), namespaceName, metav1.GetOptions{})
if err != nil {
log.Error().
Str("context", "ToggleSystemState").
Str("namespace", namespaceName).
Err(err).
Msg("failed to get namespace")
return errors.Wrap(err, "failed fetching namespace object")
}
@ -218,8 +308,12 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er
namespace.Labels[systemNamespaceLabel] = strconv.FormatBool(isSystem)
_, err = nsService.Update(context.TODO(), namespace, metav1.UpdateOptions{})
if err != nil {
if _, err := kcl.cli.CoreV1().Namespaces().Update(context.TODO(), namespace, metav1.UpdateOptions{}); err != nil {
log.Error().
Str("context", "ToggleSystemState").
Str("namespace", namespaceName).
Err(err).
Msg("failed updating namespace object")
return errors.Wrap(err, "failed updating namespace object")
}
@ -228,29 +322,26 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er
}
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 {
log.Error().
Str("context", "DeleteNamespace").
Str("namespace", namespaceName).
Err(err).
Msg("failed fetching namespace object")
return nil, err
}
err = kcl.cli.CoreV1().Namespaces().Delete(context.Background(), namespaceName, metav1.DeleteOptions{})
if err != nil {
log.Error().
Str("context", "DeleteNamespace").
Str("namespace", namespaceName).
Err(err).
Msg("failed deleting namespace object")
return nil, err
}
@ -261,6 +352,10 @@ func (kcl *KubeClient) DeleteNamespace(namespaceName string) (*corev1.Namespace,
func (kcl *KubeClient) CombineNamespacesWithResourceQuotas(namespaces map[string]portainer.K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError {
resourceQuotas, err := kcl.GetResourceQuotas("")
if err != nil && !k8serrors.IsNotFound(err) {
log.Error().
Str("context", "CombineNamespacesWithResourceQuotas").
Err(err).
Msg("unable to retrieve resource quotas from the Kubernetes for an admin user")
return httperror.InternalServerError("an error occurred during the CombineNamespacesWithResourceQuotas operation, unable to retrieve resource quotas from the Kubernetes for an admin user. Error: ", err)
}
@ -275,6 +370,11 @@ func (kcl *KubeClient) CombineNamespacesWithResourceQuotas(namespaces map[string
func (kcl *KubeClient) CombineNamespaceWithResourceQuota(namespace portainer.K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError {
resourceQuota, err := kcl.GetPortainerResourceQuota(namespace.Name)
if err != nil && !k8serrors.IsNotFound(err) {
log.Error().
Str("context", "CombineNamespaceWithResourceQuota").
Str("namespace", namespace.Name).
Err(err).
Msg("unable to retrieve the resource quota associated with the namespace")
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)
}

View file

@ -611,6 +611,7 @@ type (
Id string `json:"Id"`
Name string `json:"Name"`
Status corev1.NamespaceStatus `json:"Status"`
Annotations map[string]string `json:"Annotations"`
CreationDate string `json:"CreationDate"`
NamespaceOwner string `json:"NamespaceOwner"`
IsSystem bool `json:"IsSystem"`