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 @@
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() {