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/handler/websocket/shell_pod.go b/api/http/handler/websocket/shell_pod.go index c47b311bc..e7e5861b3 100644 --- a/api/http/handler/websocket/shell_pod.go +++ b/api/http/handler/websocket/shell_pod.go @@ -86,17 +86,12 @@ func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Req return nil } - serviceAccountToken, isAdminToken, err := handler.getToken(r, endpoint, false) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get user service account token", err} - } - handlerErr := handler.hijackPodExecStartOperation( w, r, cli, - serviceAccountToken, - isAdminToken, + "", + true, endpoint, shellPod.Namespace, shellPod.PodName, 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 6cd1e11cc..eb2e8ac72 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1230,6 +1230,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/azure/components/azure-sidebar/azure-sidebar.html b/app/azure/components/azure-sidebar/azure-sidebar.html index 7f6c5345a..08fcec28e 100644 --- a/app/azure/components/azure-sidebar/azure-sidebar.html +++ b/app/azure/components/azure-sidebar/azure-sidebar.html @@ -1,7 +1,19 @@ - + Dashboard - + Container instances 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/docker/components/docker-sidebar/docker-sidebar.html b/app/docker/components/docker-sidebar/docker-sidebar.html index 59866f008..7992dd60c 100644 --- a/app/docker/components/docker-sidebar/docker-sidebar.html +++ b/app/docker/components/docker-sidebar/docker-sidebar.html @@ -1,4 +1,10 @@ - + Dashboard @@ -11,32 +17,46 @@ is-sidebar-open="$ctrl.isSidebarOpen" children-paths="[]" > - + Custom Templates - + Stacks - + Services - + Containers - + Images - + Networks - + Volumes @@ -46,6 +66,7 @@ path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-file-code fa-fw" class-name="sidebar-list" + data-cy="dockerSidebar-configs" > Configs @@ -56,6 +77,7 @@ path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-user-secret fa-fw" class-name="sidebar-list" + data-cy="dockerSidebar-secrets" > Secrets @@ -66,6 +88,7 @@ path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-history fa-fw" class-name="sidebar-list" + data-cy="dockerSidebar-events" > Events @@ -85,11 +108,18 @@ path="docker.featuresConfiguration" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist" + data-cy="dockerSidebar-setup" > Setup - + Registries @@ -110,11 +140,18 @@ path="docker.featuresConfiguration" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist" + data-cy="swarmSidebar-setup" > Setup - + Registries diff --git a/app/docker/components/imageRegistry/por-image-registry.html b/app/docker/components/imageRegistry/por-image-registry.html index e332a9f4d..e3197f678 100644 --- a/app/docker/components/imageRegistry/por-image-registry.html +++ b/app/docker/components/imageRegistry/por-image-registry.html @@ -26,6 +26,7 @@ placeholder="e.g. myImage:myTag" ng-change="$ctrl.onImageChange()" required + data-cy="component-imageInput" /> + {{ $item.Name }} diff --git a/app/edge/components/edge-stacks-datatable/edgeStacksDatatable.html b/app/edge/components/edge-stacks-datatable/edgeStacksDatatable.html index a5e1dbc66..76c74c4e8 100644 --- a/app/edge/components/edge-stacks-datatable/edgeStacksDatatable.html +++ b/app/edge/components/edge-stacks-datatable/edgeStacksDatatable.html @@ -6,7 +6,7 @@ Edge Stacks -
+
Settings