diff --git a/api/bolt/migrator/migrate_dbversion31.go b/api/bolt/migrator/migrate_dbversion31.go index b6dcfa7ee..52f6c569b 100644 --- a/api/bolt/migrator/migrate_dbversion31.go +++ b/api/bolt/migrator/migrate_dbversion31.go @@ -5,7 +5,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/errors" - endpointutils "github.com/portainer/portainer/api/internal/endpoint" + "github.com/portainer/portainer/api/internal/endpointutils" snapshotutils "github.com/portainer/portainer/api/internal/snapshot" ) diff --git a/api/http/handler/endpoints/endpoint_registries_list.go b/api/http/handler/endpoints/endpoint_registries_list.go index 93233fada..06309624f 100644 --- a/api/http/handler/endpoints/endpoint_registries_list.go +++ b/api/http/handler/endpoints/endpoint_registries_list.go @@ -10,7 +10,7 @@ import ( portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/security" - endpointutils "github.com/portainer/portainer/api/internal/endpoint" + "github.com/portainer/portainer/api/internal/endpointutils" ) // GET request on /endpoints/{id}/registries?namespace diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index a5c4e1dd5..263aac2ca 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -48,8 +48,8 @@ type Handler struct { EndpointGroupHandler *endpointgroups.Handler EndpointHandler *endpoints.Handler EndpointProxyHandler *endpointproxy.Handler - FileHandler *file.Handler KubernetesHandler *kubernetes.Handler + FileHandler *file.Handler MOTDHandler *motd.Handler RegistryHandler *registries.Handler ResourceControlHandler *resourcecontrols.Handler diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index 686e79883..c317ed346 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -1,28 +1,68 @@ package kubernetes import ( + "errors" "net/http" "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" + "github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/kubernetes/cli" ) // Handler is the HTTP handler which will natively deal with to external endpoints. type Handler struct { *mux.Router - DataStore portainer.DataStore - KubernetesClientFactory *cli.ClientFactory + dataStore portainer.DataStore + kubernetesClientFactory *cli.ClientFactory + authorizationService *authorization.Service } // NewHandler creates a handler to process pre-proxied requests to external APIs. -func NewHandler(bouncer *security.RequestBouncer) *Handler { +func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore portainer.DataStore, kubernetesClientFactory *cli.ClientFactory) *Handler { h := &Handler{ - Router: mux.NewRouter(), + Router: mux.NewRouter(), + dataStore: dataStore, + kubernetesClientFactory: kubernetesClientFactory, + authorizationService: authorizationService, } - h.PathPrefix("/kubernetes/{id}/config").Handler( + + kubeRouter := h.PathPrefix("/kubernetes/{id}").Subrouter() + + kubeRouter.Use(bouncer.AuthenticatedAccess) + kubeRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id")) + kubeRouter.Use(kubeOnlyMiddleware) + + kubeRouter.PathPrefix("/config").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesConfig))).Methods(http.MethodGet) + + // namespaces + // in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?) + // to keep it simple, we've decided to leave it like this. + namespaceRouter := kubeRouter.PathPrefix("/namespaces/{namespace}").Subrouter() + namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut) + return h } + +func kubeOnlyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) { + endpoint, err := middlewares.FetchEndpoint(request) + if err != nil { + httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an endpoint on request context", err) + return + } + + if !endpointutils.IsKubernetesEndpoint(endpoint) { + errMessage := "Endpoint is not a kubernetes endpoint" + httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage)) + return + } + + next.ServeHTTP(rw, request) + }) +} diff --git a/api/http/handler/kubernetes/kubernetes_config.go b/api/http/handler/kubernetes/kubernetes_config.go index f2b1ac5d3..972d34390 100644 --- a/api/http/handler/kubernetes/kubernetes_config.go +++ b/api/http/handler/kubernetes/kubernetes_config.go @@ -39,7 +39,7 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -56,7 +56,7 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) + cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err} } diff --git a/api/http/handler/kubernetes/namespaces_toggle_system.go b/api/http/handler/kubernetes/namespaces_toggle_system.go new file mode 100644 index 000000000..5c71bf748 --- /dev/null +++ b/api/http/handler/kubernetes/namespaces_toggle_system.go @@ -0,0 +1,65 @@ +package kubernetes + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api/http/middlewares" +) + +type namespacesToggleSystemPayload struct { + // Toggle the system state of this namespace to true or false + System bool `example:"true"` +} + +func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error { + return nil +} + +// @id KubernetesNamespacesToggleSystem +// @summary Toggle the system state for a namespace +// @description Toggle the system state for a namespace +// @description **Access policy**: administrator or endpoint admin +// @security jwt +// @tags kubernetes +// @accept json +// @param id path int true "Endpoint identifier" +// @param namespace path string true "Namespace name" +// @param body body namespacesToggleSystemPayload true "Update details" +// @success 200 "Success" +// @failure 400 "Invalid request" +// @failure 404 "Endpoint not found" +// @failure 500 "Server error" +// @router /kubernetes/{id}/namespaces/{namespace}/system [put] +func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpoint, err := middlewares.FetchEndpoint(r) + if err != nil { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err} + } + + namespaceName, err := request.RetrieveRouteVariableValue(r, "namespace") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid namespace identifier route variable", err} + } + + var payload namespacesToggleSystemPayload + err = request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + kubeClient, err := handler.kubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create kubernetes client", err} + } + + err = kubeClient.ToggleSystemState(namespaceName, payload.System) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to toggle system status", err} + } + + return response.Empty(rw) + +} diff --git a/api/http/middlewares/endpoint.go b/api/http/middlewares/endpoint.go new file mode 100644 index 000000000..b23d3ac5a --- /dev/null +++ b/api/http/middlewares/endpoint.go @@ -0,0 +1,58 @@ +package middlewares + +import ( + "context" + "errors" + "net/http" + + "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" + requesthelpers "github.com/portainer/libhttp/request" + portainer "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" +) + +const ( + contextEndpoint = "endpoint" +) + +func WithEndpoint(endpointService portainer.EndpointService, endpointIDParam string) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) { + if endpointIDParam == "" { + endpointIDParam = "id" + } + + endpointID, err := requesthelpers.RetrieveNumericRouteVariableValue(request, endpointIDParam) + if err != nil { + httperror.WriteError(rw, http.StatusBadRequest, "Invalid endpoint identifier route variable", err) + return + } + + endpoint, err := endpointService.Endpoint(portainer.EndpointID(endpointID)) + if err != nil { + statusCode := http.StatusInternalServerError + + if err == bolterrors.ErrObjectNotFound { + statusCode = http.StatusNotFound + } + httperror.WriteError(rw, statusCode, "Unable to find an endpoint with the specified identifier inside the database", err) + return + } + + ctx := context.WithValue(request.Context(), contextEndpoint, endpoint) + + next.ServeHTTP(rw, request.WithContext(ctx)) + + }) + } +} + +func FetchEndpoint(request *http.Request) (*portainer.Endpoint, error) { + contextData := request.Context().Value(contextEndpoint) + if contextData == nil { + return nil, errors.New("Unable to find endpoint data in request context") + } + + return contextData.(*portainer.Endpoint), nil +} diff --git a/api/http/server.go b/api/http/server.go index 951a7e4ae..95f9dfa76 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -26,7 +26,7 @@ import ( "github.com/portainer/portainer/api/http/handler/endpointproxy" "github.com/portainer/portainer/api/http/handler/endpoints" "github.com/portainer/portainer/api/http/handler/file" - kube "github.com/portainer/portainer/api/http/handler/kubernetes" + kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes" "github.com/portainer/portainer/api/http/handler/motd" "github.com/portainer/portainer/api/http/handler/registries" "github.com/portainer/portainer/api/http/handler/resourcecontrols" @@ -160,11 +160,9 @@ func (server *Server) Start() error { endpointProxyHandler.ProxyManager = server.ProxyManager endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService - var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) + var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.KubernetesClientFactory) - var kubernetesHandler = kube.NewHandler(requestBouncer) - kubernetesHandler.DataStore = server.DataStore - kubernetesHandler.KubernetesClientFactory = server.KubernetesClientFactory + var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) var motdHandler = motd.NewHandler(requestBouncer) @@ -244,8 +242,8 @@ func (server *Server) Start() error { EndpointHandler: endpointHandler, EndpointEdgeHandler: endpointEdgeHandler, EndpointProxyHandler: endpointProxyHandler, - FileHandler: fileHandler, KubernetesHandler: kubernetesHandler, + FileHandler: fileHandler, MOTDHandler: motdHandler, RegistryHandler: registryHandler, ResourceControlHandler: resourceControlHandler, diff --git a/api/internal/endpoint/endpoint.go b/api/internal/endpoint/endpoint.go deleted file mode 100644 index 079474d11..000000000 --- a/api/internal/endpoint/endpoint.go +++ /dev/null @@ -1,17 +0,0 @@ -package endpoint - -import portainer "github.com/portainer/portainer/api" - -// IsKubernetesEndpoint returns true if this is a kubernetes endpoint -func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool { - return endpoint.Type == portainer.KubernetesLocalEnvironment || - endpoint.Type == portainer.AgentOnKubernetesEnvironment || - endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment -} - -// IsDockerEndpoint returns true if this is a docker endpoint -func IsDockerEndpoint(endpoint *portainer.Endpoint) bool { - return endpoint.Type == portainer.DockerEnvironment || - endpoint.Type == portainer.AgentOnDockerEnvironment || - endpoint.Type == portainer.EdgeAgentOnDockerEnvironment -} diff --git a/api/internal/endpointutils/endpointutils.go b/api/internal/endpointutils/endpointutils.go index 48c2c5fd1..3929ce4b3 100644 --- a/api/internal/endpointutils/endpointutils.go +++ b/api/internal/endpointutils/endpointutils.go @@ -6,6 +6,7 @@ import ( portainer "github.com/portainer/portainer/api" ) +// IsLocalEndpoint returns true if this is a local endpoint func IsLocalEndpoint(endpoint *portainer.Endpoint) bool { return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5 } diff --git a/api/kubernetes/cli/namespace.go b/api/kubernetes/cli/namespace.go new file mode 100644 index 000000000..7ba20683d --- /dev/null +++ b/api/kubernetes/cli/namespace.go @@ -0,0 +1,73 @@ +package cli + +import ( + "strconv" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + systemNamespaceLabel = "io.portainer.kubernetes.namespace.system" +) + +func defaultSystemNamespaces() map[string]struct{} { + return map[string]struct{}{ + "kube-system": {}, + "kube-public": {}, + "kube-node-lease": {}, + "portainer": {}, + } +} + +func isSystemNamespace(namespace v1.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(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(namespace) + if err != nil { + return errors.Wrap(err, "failed updating namespace object") + } + + if isSystem { + return kcl.NamespaceAccessPoliciesDeleteNamespace(namespaceName) + } + + return nil + +} diff --git a/api/kubernetes/cli/namespace_test.go b/api/kubernetes/cli/namespace_test.go new file mode 100644 index 000000000..207373141 --- /dev/null +++ b/api/kubernetes/cli/namespace_test.go @@ -0,0 +1,185 @@ +package cli + +import ( + "strconv" + "sync" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/stretchr/testify/assert" + + core "k8s.io/api/core/v1" + ktypes "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kfake "k8s.io/client-go/kubernetes/fake" +) + +func Test_ToggleSystemState(t *testing.T) { + t.Run("should skip is default (exit without error)", func(t *testing.T) { + nsName := "default" + kcl := &KubeClient{ + cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName}}), + instanceID: "instance", + lock: &sync.Mutex{}, + } + + err := kcl.ToggleSystemState(nsName, true) + assert.NoError(t, err) + + ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{}) + assert.NoError(t, err) + + _, exists := ns.Labels[systemNamespaceLabel] + assert.False(t, exists, "system label should not exists") + }) + + t.Run("should fail if namespace doesn't exist", func(t *testing.T) { + nsName := "not-exist" + kcl := &KubeClient{ + cli: kfake.NewSimpleClientset(), + instanceID: "instance", + lock: &sync.Mutex{}, + } + + err := kcl.ToggleSystemState(nsName, true) + assert.Error(t, err) + + }) + + t.Run("if called with the same state, should skip (exit without error)", func(t *testing.T) { + nsName := "namespace" + tests := []struct { + isSystem bool + }{ + {isSystem: true}, + {isSystem: false}, + } + + for _, test := range tests { + t.Run(strconv.FormatBool(test.isSystem), func(t *testing.T) { + kcl := &KubeClient{ + cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName, Labels: map[string]string{ + systemNamespaceLabel: strconv.FormatBool(test.isSystem), + }}}), + instanceID: "instance", + lock: &sync.Mutex{}, + } + + err := kcl.ToggleSystemState(nsName, test.isSystem) + assert.NoError(t, err) + + ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{}) + assert.NoError(t, err) + + assert.Equal(t, test.isSystem, isSystemNamespace(*ns)) + }) + } + }) + + t.Run("for regular namespace if isSystem is true and doesn't have a label, should set the label to true", func(t *testing.T) { + nsName := "namespace" + + kcl := &KubeClient{ + cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName}}), + instanceID: "instance", + lock: &sync.Mutex{}, + } + + err := kcl.ToggleSystemState(nsName, true) + assert.NoError(t, err) + + ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{}) + assert.NoError(t, err) + + labelValue, exists := ns.Labels[systemNamespaceLabel] + assert.True(t, exists, "system label should exists") + + assert.Equal(t, "true", labelValue) + }) + + t.Run("for default system namespace if isSystem is false and doesn't have a label, should set the label to false", func(t *testing.T) { + nsName := "portainer" + + kcl := &KubeClient{ + cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName}}), + instanceID: "instance", + lock: &sync.Mutex{}, + } + + err := kcl.ToggleSystemState(nsName, false) + assert.NoError(t, err) + + ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{}) + assert.NoError(t, err) + + labelValue, exists := ns.Labels[systemNamespaceLabel] + assert.True(t, exists, "system label should exists") + + assert.Equal(t, "false", labelValue) + }) + + t.Run("for system namespace (with label), if called with false, should set the label", func(t *testing.T) { + nsName := "namespace" + + kcl := &KubeClient{ + cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName, Labels: map[string]string{ + systemNamespaceLabel: "true", + }}}), + instanceID: "instance", + lock: &sync.Mutex{}, + } + + err := kcl.ToggleSystemState(nsName, false) + assert.NoError(t, err) + + ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{}) + assert.NoError(t, err) + + labelValue, exists := ns.Labels[systemNamespaceLabel] + assert.True(t, exists, "system label should exists") + assert.Equal(t, "false", labelValue) + }) + + t.Run("for non system namespace (with label), if called with true, should set the label, and remove accesses", func(t *testing.T) { + nsName := "ns1" + + namespace := &core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName, Labels: map[string]string{ + systemNamespaceLabel: "false", + }}} + + config := &ktypes.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: portainerConfigMapName, + Namespace: portainerNamespace, + }, + Data: map[string]string{ + "NamespaceAccessPolicies": `{"ns1":{"UserAccessPolicies":{"2":{"RoleId":0}}}, "ns2":{"UserAccessPolicies":{"2":{"RoleId":0}}}}`, + }, + } + + kcl := &KubeClient{ + cli: kfake.NewSimpleClientset(namespace, config), + instanceID: "instance", + lock: &sync.Mutex{}, + } + + err := kcl.ToggleSystemState(nsName, true) + assert.NoError(t, err) + + ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{}) + assert.NoError(t, err) + + labelValue, exists := ns.Labels[systemNamespaceLabel] + assert.True(t, exists, "system label should exists") + assert.Equal(t, "true", labelValue) + + expectedPolicies := map[string]portainer.K8sNamespaceAccessPolicy{ + "ns2": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}}, + } + actualPolicies, err := kcl.GetNamespaceAccessPolicies() + assert.NoError(t, err, "failed to fetch policies") + assert.Equal(t, expectedPolicies, actualPolicies) + + }) +} diff --git a/api/portainer.go b/api/portainer.go index 73e286d6d..ecf845610 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1226,6 +1226,7 @@ type ( CreateRegistrySecret(registry *Registry, namespace string) error IsRegistrySecret(namespace, secretName string) (bool, error) GetKubeConfig(ctx context.Context, apiServerURL string, bearerToken string, tokenData *TokenData) (*clientV1.Config, error) + ToggleSystemState(namespace string, isSystem bool) error } // KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint diff --git a/app/constants.js b/app/constants.js index c6d5dbabb..97f9ca362 100644 --- a/app/constants.js +++ b/app/constants.js @@ -27,8 +27,6 @@ angular .constant('PAGINATION_MAX_ITEMS', 10) .constant('APPLICATION_CACHE_VALIDITY', 3600) .constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.') - .constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']) - .constant('KUBERNETES_DEFAULT_NAMESPACE', 'default') - .constant('KUBERNETES_SYSTEM_NAMESPACES', ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']); + .constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']); export const PORTAINER_FADEOUT = 1500; diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index acdde4334..2d34c1420 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -11,7 +11,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).conf parent: 'endpoint', abstract: true, - onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, KubernetesHealthService, Notifications, StateManager) { + onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, KubernetesHealthService, KubernetesNamespaceService, Notifications, StateManager) { return $async(async () => { if (![5, 6, 7].includes(endpoint.Type)) { $state.go('portainer.home'); @@ -34,6 +34,8 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).conf if (endpoint.Type === 7 && endpoint.Status === 2) { throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.'); } + + await KubernetesNamespaceService.get(); } catch (e) { Notifications.error('Failed loading endpoint', e); $state.go('portainer.home', {}, { reload: true }); diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js index f195c2bc0..d385f38c7 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js @@ -1,13 +1,13 @@ import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [ '$scope', '$controller', - 'KubernetesNamespaceHelper', 'DatatableService', 'Authentication', - function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + function ($scope, $controller, DatatableService, Authentication) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); var ctrl = this; diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js index 848e0ad6b..67fc81cb2 100644 --- a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js @@ -1,15 +1,15 @@ import _ from 'lodash-es'; import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [ '$scope', '$controller', - 'KubernetesNamespaceHelper', 'DatatableService', 'Authentication', - function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + function ($scope, $controller, DatatableService, Authentication) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); this.state = Object.assign(this.state, { expandedItems: [], diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html index d49c972c8..d27aa586a 100644 --- a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html @@ -134,7 +134,7 @@ {{ item.ResourcePool }} - system + system {{ item.Applications.length }} diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js index faeef91c3..ba5c40879 100644 --- a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js @@ -1,14 +1,14 @@ import _ from 'lodash-es'; import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; -angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatatableController', [ +angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksDatatableController', [ '$scope', '$controller', - 'KubernetesNamespaceHelper', 'DatatableService', 'Authentication', - function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + function ($scope, $controller, DatatableService, Authentication) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); this.state = Object.assign(this.state, { expandedItems: [], @@ -33,15 +33,19 @@ angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatat * Do not allow applications in system namespaces to be selected */ this.allowSelection = function (item) { - return !this.isSystemNamespace(item); + return !this.isSystemNamespace(item.ResourcePool); }; - this.isSystemNamespace = function (item) { - return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + /** + * @param {String} namespace Namespace (string name) + * @returns Boolean + */ + this.isSystemNamespace = function (namespace) { + return KubernetesNamespaceHelper.isSystemNamespace(namespace); }; this.isDisplayed = function (item) { - return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem; + return !ctrl.isSystemNamespace(item.ResourcePool) || ctrl.settings.showSystem; }; this.expandItem = function (item, expanded) { diff --git a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js index c9459ffba..f9ebf77d7 100644 --- a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js +++ b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js @@ -1,12 +1,12 @@ import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; angular.module('portainer.docker').controller('KubernetesConfigurationsDatatableController', [ '$scope', '$controller', - 'KubernetesNamespaceHelper', 'DatatableService', 'Authentication', - function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + function ($scope, $controller, DatatableService, Authentication) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); const ctrl = this; diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js index 9bb25c50a..447dcc013 100644 --- a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js +++ b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js @@ -1,12 +1,12 @@ import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; angular.module('portainer.docker').controller('KubernetesNodeApplicationsDatatableController', [ '$scope', '$controller', - 'KubernetesNamespaceHelper', 'DatatableService', - function ($scope, $controller, KubernetesNamespaceHelper, DatatableService) { + function ($scope, $controller, DatatableService) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); this.isSystemNamespace = function (item) { diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js index c81af255d..c7f4e1896 100644 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js @@ -1,10 +1,11 @@ +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; + angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableController', [ '$scope', '$controller', 'Authentication', - 'KubernetesNamespaceHelper', 'DatatableService', - function ($scope, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) { + function ($scope, $controller, Authentication, DatatableService) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); var ctrl = this; @@ -19,14 +20,14 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC this.canManageAccess = function (item) { if (!this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace) { - return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item); + return !KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name) && !this.isSystemNamespace(item); } else { return !this.isSystemNamespace(item); } }; this.disableRemove = function (item) { - return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name) || item.Namespace.Name === 'default'; + return this.isSystemNamespace(item) || KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name); }; this.isSystemNamespace = function (item) { diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js index 2f13b0e56..528fbb6bd 100644 --- a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js @@ -1,14 +1,14 @@ import angular from 'angular'; import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; // TODO: review - refactor to use `extends GenericDatatableController` class KubernetesVolumesDatatableController { /* @ngInject */ - constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) { + constructor($async, $controller, Authentication, DatatableService) { this.$async = $async; this.$controller = $controller; this.Authentication = Authentication; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.DatatableService = DatatableService; this.onInit = this.onInit.bind(this); @@ -29,7 +29,7 @@ class KubernetesVolumesDatatableController { } isSystemNamespace(item) { - return this.KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name); + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name); } isDisplayed(item) { diff --git a/app/kubernetes/converters/namespace.js b/app/kubernetes/converters/namespace.js index 46f2c33e3..6a2b9fb89 100644 --- a/app/kubernetes/converters/namespace.js +++ b/app/kubernetes/converters/namespace.js @@ -1,9 +1,14 @@ import _ from 'lodash-es'; import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads'; -import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models'; +import { + KubernetesPortainerResourcePoolNameLabel, + KubernetesPortainerResourcePoolOwnerLabel, + KubernetesPortainerNamespaceSystemLabel, +} from 'Kubernetes/models/resource-pool/models'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; -class KubernetesNamespaceConverter { +export default class KubernetesNamespaceConverter { static apiToNamespace(data, yaml) { const res = new KubernetesNamespace(); res.Id = data.metadata.uid; @@ -13,6 +18,14 @@ class KubernetesNamespaceConverter { res.Yaml = yaml ? yaml.data : ''; res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : ''; res.ResourcePoolOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] : ''; + + res.IsSystem = KubernetesNamespaceHelper.isDefaultSystemNamespace(data.metadata.name); + if (data.metadata.labels) { + const systemLabel = data.metadata.labels[KubernetesPortainerNamespaceSystemLabel]; + if (!_.isEmpty(systemLabel)) { + res.IsSystem = systemLabel === 'true'; + } + } return res; } @@ -20,6 +33,7 @@ class KubernetesNamespaceConverter { const res = new KubernetesNamespaceCreatePayload(); res.metadata.name = namespace.Name; res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName; + if (namespace.ResourcePoolOwner) { const resourcePoolOwner = _.truncate(namespace.ResourcePoolOwner, { length: 63, omission: '' }); res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = resourcePoolOwner; @@ -27,5 +41,3 @@ class KubernetesNamespaceConverter { return res; } } - -export default KubernetesNamespaceConverter; diff --git a/app/kubernetes/converters/resourcePool.js b/app/kubernetes/converters/resourcePool.js index eb20ce5e4..bec76e174 100644 --- a/app/kubernetes/converters/resourcePool.js +++ b/app/kubernetes/converters/resourcePool.js @@ -18,6 +18,7 @@ class KubernetesResourcePoolConverter { namespace.Name = formValues.Name; namespace.ResourcePoolName = formValues.Name; namespace.ResourcePoolOwner = formValues.Owner; + namespace.IsSystem = formValues.IsSystem; const quota = KubernetesResourceQuotaConverter.resourcePoolFormValuesToResourceQuota(formValues); diff --git a/app/kubernetes/helpers/namespaceHelper.js b/app/kubernetes/helpers/namespaceHelper.js index f41f393c0..dc7d03b20 100644 --- a/app/kubernetes/helpers/namespaceHelper.js +++ b/app/kubernetes/helpers/namespaceHelper.js @@ -1,21 +1,33 @@ import _ from 'lodash-es'; -import angular from 'angular'; -class KubernetesNamespaceHelper { - /* @ngInject */ - constructor(KUBERNETES_SYSTEM_NAMESPACES, KUBERNETES_DEFAULT_NAMESPACE) { - this.KUBERNETES_SYSTEM_NAMESPACES = KUBERNETES_SYSTEM_NAMESPACES; - this.KUBERNETES_DEFAULT_NAMESPACE = KUBERNETES_DEFAULT_NAMESPACE; +import { KUBERNETES_DEFAULT_NAMESPACE, KUBERNETES_DEFAULT_SYSTEM_NAMESPACES } from 'Kubernetes/models/namespace/models'; +import { isSystem } from 'Kubernetes/store/namespace'; + +export default class KubernetesNamespaceHelper { + /** + * Check if namespace is system or not + * @param {String} namespace Namespace (string name) to evaluate + * @returns Boolean + */ + static isSystemNamespace(namespace) { + return isSystem(namespace); } - isSystemNamespace(namespace) { - return _.includes(this.KUBERNETES_SYSTEM_NAMESPACES, namespace); + /** + * Check if namespace is default or not + * @param {String} namespace Namespace (string name) to evaluate + * @returns Boolean + */ + static isDefaultNamespace(namespace) { + return namespace === KUBERNETES_DEFAULT_NAMESPACE; } - isDefaultNamespace(namespace) { - return namespace === this.KUBERNETES_DEFAULT_NAMESPACE; + /** + * Check if namespace is one of the default system namespaces + * @param {String} namespace Namespace (string name) to evaluate + * @returns Boolean + */ + static isDefaultSystemNamespace(namespace) { + return _.includes(KUBERNETES_DEFAULT_SYSTEM_NAMESPACES, namespace); } } - -export default KubernetesNamespaceHelper; -angular.module('portainer.app').service('KubernetesNamespaceHelper', KubernetesNamespaceHelper); diff --git a/app/kubernetes/models/namespace/models.js b/app/kubernetes/models/namespace/models.js index ca19450c8..26af381d5 100644 --- a/app/kubernetes/models/namespace/models.js +++ b/app/kubernetes/models/namespace/models.js @@ -1,11 +1,15 @@ -export function KubernetesNamespace() { - return { - Id: '', - Name: '', - CreationDate: '', - Status: '', - Yaml: '', - ResourcePoolName: '', - ResourcePoolOwner: '', - }; +export class KubernetesNamespace { + constructor() { + this.Id = ''; + this.Name = ''; + this.CreationDate = ''; + this.Status = ''; + this.Yaml = ''; + this.ResourcePoolName = ''; + this.ResourcePoolOwner = ''; + this.IsSystem = false; + } } + +export const KUBERNETES_DEFAULT_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']; +export const KUBERNETES_DEFAULT_NAMESPACE = 'default'; diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js index e4e9f95f1..d174bf2a8 100644 --- a/app/kubernetes/models/resource-pool/formValues.js +++ b/app/kubernetes/models/resource-pool/formValues.js @@ -1,13 +1,12 @@ export function KubernetesResourcePoolFormValues(defaults) { - return { - Name: '', - MemoryLimit: defaults.MemoryLimit, - CpuLimit: defaults.CpuLimit, - HasQuota: false, - IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue - Registries: [], // RegistryViewModel - EndpointId: 0, - }; + this.Name = ''; + this.MemoryLimit = defaults.MemoryLimit; + this.CpuLimit = defaults.CpuLimit; + this.HasQuota = false; + this.IngressClasses = []; // KubernetesResourcePoolIngressClassFormValue + this.Registries = []; // RegistryViewModel + this.EndpointId = 0; + this.IsSystem = false; } /** diff --git a/app/kubernetes/models/resource-pool/models.js b/app/kubernetes/models/resource-pool/models.js index 0e7a67d62..b953cc5d3 100644 --- a/app/kubernetes/models/resource-pool/models.js +++ b/app/kubernetes/models/resource-pool/models.js @@ -2,6 +2,8 @@ export const KubernetesPortainerResourcePoolNameLabel = 'io.portainer.kubernetes export const KubernetesPortainerResourcePoolOwnerLabel = 'io.portainer.kubernetes.resourcepool.owner'; +export const KubernetesPortainerNamespaceSystemLabel = 'io.portainer.kubernetes.namespace.system'; + /** * KubernetesResourcePool Model */ diff --git a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js index e2703bd2c..9367f5aef 100644 --- a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js +++ b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js @@ -1,11 +1,12 @@ +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; + export default class KubernetesRegistryAccessController { /* @ngInject */ - constructor($async, $state, EndpointService, Notifications, KubernetesResourcePoolService, KubernetesNamespaceHelper) { + constructor($async, $state, EndpointService, Notifications, KubernetesResourcePoolService) { this.$async = $async; this.$state = $state; this.Notifications = Notifications; this.KubernetesResourcePoolService = KubernetesResourcePoolService; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.EndpointService = EndpointService; this.state = { @@ -60,7 +61,7 @@ export default class KubernetesRegistryAccessController { const resourcePools = await this.KubernetesResourcePoolService.get(); this.resourcePools = resourcePools - .filter((pool) => !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name) && !this.savedResourcePools.find(({ value }) => value === pool.Namespace.Name)) + .filter((pool) => !KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name) && !this.savedResourcePools.find(({ value }) => value === pool.Namespace.Name)) .map((pool) => ({ name: pool.Namespace.Name, id: pool.Namespace.Id })); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve namespaces'); diff --git a/app/kubernetes/rest/portainer-namespace.js b/app/kubernetes/rest/portainer-namespace.js new file mode 100644 index 000000000..ee54f3fdf --- /dev/null +++ b/app/kubernetes/rest/portainer-namespace.js @@ -0,0 +1,12 @@ +angular.module('portainer.kubernetes').factory('KubernetesPortainerNamespaces', KubernetesPortainerNamespacesFactory); + +function KubernetesPortainerNamespacesFactory($resource) { + const url = '/api/kubernetes/:endpointId/namespaces/:namespaceName/:action'; + return $resource( + url, + {}, + { + toggleSystem: { method: 'PUT', params: { action: 'system' } }, + } + ); +} diff --git a/app/kubernetes/services/namespaceService.js b/app/kubernetes/services/namespaceService.js index 891cbd0b5..fa26d5442 100644 --- a/app/kubernetes/services/namespaceService.js +++ b/app/kubernetes/services/namespaceService.js @@ -4,6 +4,7 @@ import angular from 'angular'; import PortainerError from 'Portainer/error'; import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; import KubernetesNamespaceConverter from 'Kubernetes/converters/namespace'; +import { updateNamespaces } from 'Kubernetes/store/namespace'; import $allSettled from 'Portainer/services/allSettled'; class KubernetesNamespaceService { @@ -27,7 +28,9 @@ class KubernetesNamespaceService { params.id = name; await this.KubernetesNamespaces().status(params).$promise; const [raw, yaml] = await Promise.all([this.KubernetesNamespaces().get(params).$promise, this.KubernetesNamespaces().getYaml(params).$promise]); - return KubernetesNamespaceConverter.apiToNamespace(raw, yaml); + const ns = KubernetesNamespaceConverter.apiToNamespace(raw, yaml); + updateNamespaces([ns]); + return ns; } catch (err) { throw new PortainerError('Unable to retrieve namespace', err); } @@ -43,7 +46,9 @@ class KubernetesNamespaceService { return KubernetesNamespaceConverter.apiToNamespace(item); } }); - return _.without(visibleNamespaces, undefined); + const res = _.without(visibleNamespaces, undefined); + updateNamespaces(res); + return res; } catch (err) { throw new PortainerError('Unable to retrieve namespaces', err); } diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js index a8de30eb4..5922370cb 100644 --- a/app/kubernetes/services/resourcePoolService.js +++ b/app/kubernetes/services/resourcePoolService.js @@ -5,12 +5,20 @@ import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool' import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; /* @ngInject */ -export function KubernetesResourcePoolService($async, EndpointService, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) { +export function KubernetesResourcePoolService( + $async, + EndpointService, + KubernetesNamespaceService, + KubernetesResourceQuotaService, + KubernetesIngressService, + KubernetesPortainerNamespaces +) { return { get, create, patch, delete: _delete, + toggleSystem, }; async function getOne(name) { @@ -67,9 +75,8 @@ export function KubernetesResourcePoolService($async, EndpointService, Kubernete function patch(oldFormValues, newFormValues) { return $async(async () => { - const [oldNamespace, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues); - const [newNamespace, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues); - void oldNamespace, newNamespace; + const [, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues); + const [, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues); if (oldQuota && newQuota) { await KubernetesResourceQuotaService.patch(oldQuota, newQuota); @@ -114,6 +121,10 @@ export function KubernetesResourcePoolService($async, EndpointService, Kubernete await KubernetesNamespaceService.delete(pool.Namespace); }); } + + function toggleSystem(endpointId, namespaceName, system) { + return KubernetesPortainerNamespaces.toggleSystem({ namespaceName, endpointId }, { system }).$promise; + } } angular.module('portainer.kubernetes').service('KubernetesResourcePoolService', KubernetesResourcePoolService); diff --git a/app/kubernetes/store/namespace.js b/app/kubernetes/store/namespace.js new file mode 100644 index 000000000..456b427fa --- /dev/null +++ b/app/kubernetes/store/namespace.js @@ -0,0 +1,21 @@ +// singleton pattern as: +// * we don't want to use AngularJS DI to fetch the single instance +// * we need to use the Store in static functions / non-instanciated classes +const storeNamespaces = {}; + +/** + * Check if a namespace of the store is system or not + * @param {String} name Namespace name + * @returns Boolean + */ +export function isSystem(name) { + return storeNamespaces[name] && storeNamespaces[name].IsSystem; +} + +/** + * Called from KubernetesNamespaceService.get() + * @param {KubernetesNamespace[]} namespaces list of namespaces to update in Store + */ +export function updateNamespaces(namespaces) { + namespaces.forEach((ns) => (storeNamespaces[ns.Name] = ns)); +} diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index aca7d8dcc..6ddd9bdb2 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -28,6 +28,7 @@ import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceRese import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index'; import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; import { KubernetesNodeHelper } from 'Kubernetes/node/helper'; class KubernetesCreateApplicationController { @@ -47,7 +48,6 @@ class KubernetesCreateApplicationController { KubernetesNodeService, KubernetesIngressService, KubernetesPersistentVolumeClaimService, - KubernetesNamespaceHelper, KubernetesVolumeService, RegistryService ) { @@ -64,7 +64,6 @@ class KubernetesCreateApplicationController { this.KubernetesVolumeService = KubernetesVolumeService; this.KubernetesIngressService = KubernetesIngressService; this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.RegistryService = RegistryService; this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; @@ -955,7 +954,7 @@ class KubernetesCreateApplicationController { ]); this.ingresses = ingresses; - this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name)); + this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name)); this.formValues.ResourcePool = this.resourcePools[0]; if (!this.formValues.ResourcePool) { return; diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index a291a7806..a8c67178d 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -32,7 +32,7 @@ Namespace {{ ctrl.application.ResourcePool }} - system + system diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index 1c07e0b44..e47485afc 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -7,6 +7,7 @@ import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models'; import { KubernetesPodContainerTypes } from 'Kubernetes/pod/models/index'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; function computeTolerations(nodes, application) { const pod = application.Pods[0]; @@ -106,7 +107,6 @@ class KubernetesApplicationController { KubernetesStackService, KubernetesPodService, KubernetesNodeService, - KubernetesNamespaceHelper, EndpointProvider ) { this.$async = $async; @@ -122,8 +122,6 @@ class KubernetesApplicationController { this.KubernetesPodService = KubernetesPodService; this.KubernetesNodeService = KubernetesNodeService; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; - this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; this.KubernetesApplicationTypes = KubernetesApplicationTypes; this.EndpointProvider = EndpointProvider; @@ -153,7 +151,7 @@ class KubernetesApplicationController { } isSystemNamespace() { - return this.KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool); + return KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool); } isExternalApplication() { diff --git a/app/kubernetes/views/configurations/create/createConfigurationController.js b/app/kubernetes/views/configurations/create/createConfigurationController.js index d2e4f9d4d..89aeadc90 100644 --- a/app/kubernetes/views/configurations/create/createConfigurationController.js +++ b/app/kubernetes/views/configurations/create/createConfigurationController.js @@ -3,10 +3,11 @@ import _ from 'lodash-es'; import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues'; import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; class KubernetesCreateConfigurationController { /* @ngInject */ - constructor($async, $state, $window, ModalService, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService, KubernetesNamespaceHelper) { + constructor($async, $state, $window, ModalService, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService) { this.$async = $async; this.$state = $state; this.$window = $window; @@ -16,7 +17,6 @@ class KubernetesCreateConfigurationController { this.KubernetesConfigurationService = KubernetesConfigurationService; this.KubernetesResourcePoolService = KubernetesResourcePoolService; this.KubernetesConfigurationTypes = KubernetesConfigurationTypes; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.onInit = this.onInit.bind(this); this.createConfigurationAsync = this.createConfigurationAsync.bind(this); @@ -94,7 +94,7 @@ class KubernetesCreateConfigurationController { try { const resourcePools = await this.KubernetesResourcePoolService.get(); - this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name)); + this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name)); this.formValues.ResourcePool = this.resourcePools[0]; await this.getConfigurations(); diff --git a/app/kubernetes/views/configurations/edit/configurationController.js b/app/kubernetes/views/configurations/edit/configurationController.js index cc71d9b8e..cb2e16f45 100644 --- a/app/kubernetes/views/configurations/edit/configurationController.js +++ b/app/kubernetes/views/configurations/edit/configurationController.js @@ -1,10 +1,12 @@ import angular from 'angular'; +import _ from 'lodash-es'; + import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues'; import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; -import _ from 'lodash-es'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; class KubernetesConfigurationController { /* @ngInject */ @@ -21,8 +23,7 @@ class KubernetesConfigurationController { KubernetesResourcePoolService, ModalService, KubernetesApplicationService, - KubernetesEventService, - KubernetesNamespaceHelper + KubernetesEventService ) { this.$async = $async; this.$state = $state; @@ -36,7 +37,6 @@ class KubernetesConfigurationController { this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesEventService = KubernetesEventService; this.KubernetesConfigurationTypes = KubernetesConfigurationTypes; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.KubernetesConfigMapService = KubernetesConfigMapService; this.KubernetesSecretService = KubernetesSecretService; @@ -52,7 +52,7 @@ class KubernetesConfigurationController { } isSystemNamespace() { - return this.KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace); + return KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace); } isSystemConfig() { diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index 8396caa47..9213ae4e4 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -5,6 +5,7 @@ import { KubernetesFormValidationReferences } from 'Kubernetes/models/applicatio import { KubernetesIngressClass } from 'Kubernetes/ingress/models'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; class KubernetesConfigureController { /* #region CONSTRUCTOR */ @@ -18,7 +19,6 @@ class KubernetesConfigureController { EndpointService, EndpointProvider, ModalService, - KubernetesNamespaceHelper, KubernetesResourcePoolService, KubernetesIngressService, KubernetesMetricsService @@ -30,7 +30,6 @@ class KubernetesConfigureController { this.EndpointService = EndpointService; this.EndpointProvider = EndpointProvider; this.ModalService = ModalService; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.KubernetesResourcePoolService = KubernetesResourcePoolService; this.KubernetesIngressService = KubernetesIngressService; this.KubernetesMetricsService = KubernetesMetricsService; @@ -147,8 +146,7 @@ class KubernetesConfigureController { const allResourcePools = await this.KubernetesResourcePoolService.get(); const resourcePools = _.filter( allResourcePools, - (resourcePool) => - !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !this.KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name) + (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name) ); ingressesToDel.forEach((ingress) => { diff --git a/app/kubernetes/views/dashboard/dashboardController.js b/app/kubernetes/views/dashboard/dashboardController.js index b9fe9f45a..7293e4067 100644 --- a/app/kubernetes/views/dashboard/dashboardController.js +++ b/app/kubernetes/views/dashboard/dashboardController.js @@ -1,6 +1,7 @@ import angular from 'angular'; import _ from 'lodash-es'; import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; class KubernetesDashboardController { /* @ngInject */ @@ -13,7 +14,6 @@ class KubernetesDashboardController { KubernetesApplicationService, KubernetesConfigurationService, KubernetesVolumeService, - KubernetesNamespaceHelper, Authentication, TagService ) { @@ -25,7 +25,6 @@ class KubernetesDashboardController { this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesConfigurationService = KubernetesConfigurationService; this.KubernetesVolumeService = KubernetesVolumeService; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.Authentication = Authentication; this.TagService = TagService; @@ -65,13 +64,8 @@ class KubernetesDashboardController { : '-'; if (!isAdmin) { - this.pools = _.filter(pools, (pool) => { - return !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name); - }); - - this.configurations = _.filter(configurations, (config) => { - return !KubernetesConfigurationHelper.isSystemToken(config); - }); + this.pools = _.filter(pools, (pool) => !KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name)); + this.configurations = _.filter(configurations, (config) => !KubernetesConfigurationHelper.isSystemToken(config)); } else { this.pools = pools; this.configurations = configurations; diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html index deab94041..a4c19c130 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePool.html +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -15,9 +15,18 @@
- -
- +
+ + + + + + + +
Name + {{ ctrl.pool.Namespace.Name }} + system +
@@ -396,13 +405,14 @@ -
+
Actions
-
+
+
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js index d68c64d70..a38e9ac78 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -15,6 +15,7 @@ import { KubernetesFormValidationReferences } from 'Kubernetes/models/applicatio import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants'; import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; class KubernetesResourcePoolController { /* #region CONSTRUCTOR */ @@ -34,7 +35,6 @@ class KubernetesResourcePoolController { KubernetesEventService, KubernetesPodService, KubernetesApplicationService, - KubernetesNamespaceHelper, KubernetesIngressService, KubernetesVolumeService ) { @@ -53,7 +53,6 @@ class KubernetesResourcePoolController { KubernetesEventService, KubernetesPodService, KubernetesApplicationService, - KubernetesNamespaceHelper, KubernetesIngressService, KubernetesVolumeService, }); @@ -171,11 +170,11 @@ class KubernetesResourcePoolController { } /* #region UPDATE NAMESPACE */ - async updateResourcePoolAsync() { + async updateResourcePoolAsync(oldFormValues, newFormValues) { this.state.actionInProgress = true; try { this.checkDefaults(); - await this.KubernetesResourcePoolService.patch(this.savedFormValues, this.formValues); + await this.KubernetesResourcePoolService.patch(oldFormValues, newFormValues); this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name); this.$state.reload(); } catch (err) { @@ -202,13 +201,45 @@ class KubernetesResourcePoolController { ${warnings.ingress ? messages.ingress : ''}

Do you wish to continue?`; this.ModalService.confirmUpdate(displayedMessage, (confirmed) => { if (confirmed) { - return this.$async(this.updateResourcePoolAsync); + return this.$async(this.updateResourcePoolAsync, this.savedFormValues, this.formValues); } }); } else { - return this.$async(this.updateResourcePoolAsync); + return this.$async(this.updateResourcePoolAsync, this.savedFormValues, this.formValues); } } + + async confirmMarkUnmarkAsSystem() { + const message = this.isSystem + ? 'Unmarking this namespace as system will allow non administrator users to manage it and the resources in contains depending on the access control settings. Are you sure?' + : 'Marking this namespace as a system namespace will prevent non administrator users from managing it and the resources it contains. Are you sure?'; + + return new Promise((resolve) => { + this.ModalService.confirmUpdate(message, resolve); + }); + } + + markUnmarkAsSystem() { + return this.$async(async () => { + try { + const namespaceName = this.$state.params.id; + this.state.actionInProgress = true; + + const confirmed = await this.confirmMarkUnmarkAsSystem(); + if (!confirmed) { + return; + } + await this.KubernetesResourcePoolService.toggleSystem(this.endpoint.Id, namespaceName, !this.isSystem); + + this.Notifications.success('Namespace successfully updated', namespaceName); + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to create namespace'); + } finally { + this.state.actionInProgress = false; + } + }); + } /* #endregion */ hasEventWarnings() { @@ -361,6 +392,7 @@ class KubernetesResourcePoolController { this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults); this.formValues.Name = this.pool.Namespace.Name; this.formValues.EndpointId = this.endpoint.Id; + this.formValues.IsSystem = this.pool.Namespace.IsSystem; _.forEach(nodes, (item) => { this.state.sliderMaxMemory += filesizeParser(item.Memory); @@ -377,11 +409,9 @@ class KubernetesResourcePoolController { this.state.resourceReservation.CPU = quota.CpuLimitUsed; this.state.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed); } - - this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name); - if (this.pool.Namespace.Name === 'default') { - this.isEditable = false; - } + this.isSystem = KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name); + this.isDefaultNamespace = KubernetesNamespaceHelper.isDefaultNamespace(this.pool.Namespace.Name); + this.isEditable = !this.isSystem && !this.isDefaultNamespace; await this.getEvents(); await this.getApplications(); diff --git a/app/kubernetes/views/volumes/edit/volume.html b/app/kubernetes/views/volumes/edit/volume.html index 2753a8291..c3bc9331f 100644 --- a/app/kubernetes/views/volumes/edit/volume.html +++ b/app/kubernetes/views/volumes/edit/volume.html @@ -30,7 +30,7 @@ Namespace {{ ctrl.volume.ResourcePool.Namespace.Name }} - system + system diff --git a/app/kubernetes/views/volumes/edit/volumeController.js b/app/kubernetes/views/volumes/edit/volumeController.js index b41408080..332c58c84 100644 --- a/app/kubernetes/views/volumes/edit/volumeController.js +++ b/app/kubernetes/views/volumes/edit/volumeController.js @@ -4,6 +4,7 @@ import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import { KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models'; import filesizeParser from 'filesize-parser'; +import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; class KubernetesVolumeController { /* @ngInject */ @@ -14,7 +15,6 @@ class KubernetesVolumeController { LocalStorage, KubernetesVolumeService, KubernetesEventService, - KubernetesNamespaceHelper, KubernetesApplicationService, KubernetesPersistentVolumeClaimService, ModalService, @@ -27,7 +27,6 @@ class KubernetesVolumeController { this.KubernetesVolumeService = KubernetesVolumeService; this.KubernetesEventService = KubernetesEventService; - this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; this.ModalService = ModalService; @@ -55,7 +54,7 @@ class KubernetesVolumeController { } isSystemNamespace() { - return this.KubernetesNamespaceHelper.isSystemNamespace(this.volume.ResourcePool.Namespace.Name); + return KubernetesNamespaceHelper.isSystemNamespace(this.volume.ResourcePool.Namespace.Name); } isUsed() {