Compare commits
23 Commits
fix/EE-129
...
fix/EE-142
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2a00e7e01 | ||
|
|
0ffbe6a42e | ||
|
|
7e211ef384 | ||
|
|
b4f4ef701a | ||
|
|
e8a6f15210 | ||
|
|
c39c7010be | ||
|
|
78c4530956 | ||
|
|
6ccabb2b88 | ||
|
|
0ac9d15667 | ||
|
|
1830a80a61 | ||
|
|
5ab98f41f1 | ||
|
|
7c02e4b725 | ||
|
|
d6e291db15 | ||
|
|
ab30793c48 | ||
|
|
5fd92d8a3f | ||
|
|
0ff9d49c6f | ||
|
|
80465367a5 | ||
|
|
db1f182670 | ||
|
|
dcb85ad8fe | ||
|
|
bbbc61dca9 | ||
|
|
d2d885359f | ||
|
|
5fe7526de7 | ||
|
|
3b5e15aa42 |
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||
h.PathPrefix("/{id}/kubernetes").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI)))
|
||||
h.PathPrefix("/{id}/agent/docker").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||
h.PathPrefix("/{id}/agent/kubernetes").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI)))
|
||||
h.PathPrefix("/{id}/storidge").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||
return h
|
||||
|
||||
@@ -3,6 +3,7 @@ package endpointproxy
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -65,6 +66,12 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
id := strconv.Itoa(endpointID)
|
||||
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
|
||||
|
||||
prefix := "/" + id + "/agent/docker";
|
||||
if !strings.HasPrefix(r.URL.Path, prefix) {
|
||||
prefix = "/" + id + "/docker";
|
||||
}
|
||||
|
||||
http.StripPrefix(prefix, proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,17 +65,18 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
|
||||
}
|
||||
}
|
||||
|
||||
// For KubernetesLocalEnvironment
|
||||
requestPrefix := fmt.Sprintf("/%d/kubernetes", endpointID)
|
||||
|
||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
if isKubernetesRequest(strings.TrimPrefix(r.URL.String(), requestPrefix)) {
|
||||
requestPrefix = fmt.Sprintf("/%d", endpointID)
|
||||
requestPrefix = fmt.Sprintf("/%d", endpointID)
|
||||
|
||||
agentPrefix := fmt.Sprintf("/%d/agent/kubernetes", endpointID)
|
||||
if strings.HasPrefix(r.URL.Path, agentPrefix) {
|
||||
requestPrefix = agentPrefix
|
||||
}
|
||||
}
|
||||
|
||||
http.StripPrefix(requestPrefix, proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func isKubernetesRequest(requestURL string) bool {
|
||||
return strings.HasPrefix(requestURL, "/api") || strings.HasPrefix(requestURL, "/healthz")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -69,7 +69,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.1.1
|
||||
// @version 2.6.3
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -176,6 +176,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/azure/"):
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/agent/"):
|
||||
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
|
||||
case strings.Contains(r.URL.Path, "/edge/"):
|
||||
http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r)
|
||||
default:
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,20 +34,12 @@ import (
|
||||
// @failure 500 "Server error"
|
||||
// @router /kubernetes/{id}/config [get]
|
||||
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
if r.TLS == nil {
|
||||
return &httperror.HandlerError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Kubernetes config generation only supported on portainer instances running with TLS",
|
||||
Err: errors.New("missing request TLS config"),
|
||||
}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
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 {
|
||||
@@ -64,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}
|
||||
}
|
||||
@@ -76,17 +68,19 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to generate Kubeconfig", err}
|
||||
}
|
||||
|
||||
filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, endpoint.Name)
|
||||
contentAcceptHeader := r.Header.Get("Accept")
|
||||
if contentAcceptHeader == "text/yaml" {
|
||||
yaml, err := kcli.GenerateYAML(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to generate Kubeconfig", err}
|
||||
}
|
||||
w.Header().Set("Content-Disposition", `attachment; filename=config.yaml`)
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.yaml", filenameBase))
|
||||
return YAML(w, yaml)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="config.json"`)
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.json", filenameBase))
|
||||
return response.JSON(w, config)
|
||||
}
|
||||
|
||||
|
||||
65
api/http/handler/kubernetes/namespaces_toggle_system.go
Normal file
65
api/http/handler/kubernetes/namespaces_toggle_system.go
Normal file
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
58
api/http/middlewares/endpoint.go
Normal file
58
api/http/middlewares/endpoint.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
73
api/kubernetes/cli/namespace.go
Normal file
73
api/kubernetes/cli/namespace.go
Normal file
@@ -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
|
||||
|
||||
}
|
||||
185
api/kubernetes/cli/namespace_test.go
Normal file
185
api/kubernetes/cli/namespace_test.go
Normal file
@@ -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)
|
||||
|
||||
})
|
||||
}
|
||||
@@ -1214,8 +1214,8 @@ type (
|
||||
|
||||
// KubeClient represents a service used to query a Kubernetes environment
|
||||
KubeClient interface {
|
||||
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
||||
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
||||
GetServiceAccountBearerToken(userID int) (string, error)
|
||||
CreateUserShellPod(ctx context.Context, serviceAccountName string) (*KubernetesShellPod, error)
|
||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
||||
@@ -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
|
||||
@@ -1409,7 +1410,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.6.2"
|
||||
APIVersion = "2.6.3"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 32
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
|
||||
@@ -4,7 +4,7 @@ angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub);
|
||||
|
||||
function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) {
|
||||
return $resource(
|
||||
`${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub/:registryId`,
|
||||
`${API_ENDPOINT_ENDPOINTS}/:endpointId/agent/:endpointType/v2/dockerhub/:registryId`,
|
||||
{},
|
||||
{
|
||||
limits: { method: 'GET', params: { registryId: '@registryId' } },
|
||||
|
||||
@@ -31,10 +31,6 @@ angular.module('portainer').run([
|
||||
HttpRequestHelper.resetAgentHeaders();
|
||||
});
|
||||
|
||||
$state.defaultErrorHandler(function () {
|
||||
// Do not log transitionTo errors
|
||||
});
|
||||
|
||||
// Keep-alive Edge endpoints by sending a ping request every minute
|
||||
$interval(function () {
|
||||
ping(EndpointProvider, SystemService);
|
||||
|
||||
@@ -376,98 +376,6 @@ a[ng-click] {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ul.sidebar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-title {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-title.endpoint-name {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list a {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list a.active {
|
||||
color: #fff;
|
||||
text-indent: 22px;
|
||||
border-left: 3px solid #fff;
|
||||
background: #2d3e63;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
height: 60px;
|
||||
list-style: none;
|
||||
text-indent: 20px;
|
||||
font-size: 18px;
|
||||
background: #2d3e63;
|
||||
}
|
||||
|
||||
.sidebar-header a {
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar-header a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-header .menu-icon {
|
||||
float: right;
|
||||
padding-right: 28px;
|
||||
line-height: 60px;
|
||||
}
|
||||
|
||||
#page-wrapper:not(.open) .sidebar-footer-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-footer-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-footer-content .logo {
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
height: 100%;
|
||||
max-height: 35px;
|
||||
margin: 2px 0 2px 20px;
|
||||
}
|
||||
|
||||
.sidebar-footer-content .update-notification {
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
border-radius: 2px;
|
||||
background-color: #ff851b;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.sidebar-footer-content .version {
|
||||
font-size: 11px;
|
||||
margin: 11px 20px 0 7px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#sidebar-wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#image-layers .btn {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -481,86 +389,6 @@ ul.sidebar .sidebar-list a.active {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list a.active .menu-icon {
|
||||
text-indent: 25px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list .sidebar-sublist a {
|
||||
text-indent: 35px;
|
||||
font-size: 12px;
|
||||
color: #b2bfdc;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-title {
|
||||
line-height: 36px;
|
||||
}
|
||||
ul.sidebar .sidebar-title .form-control {
|
||||
height: 36px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list a,
|
||||
ul.sidebar .sidebar-list .sidebar-sublist a {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list .menu-icon {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||
color: #fff;
|
||||
border-left: 3px solid #fff;
|
||||
background: #2d3e63;
|
||||
}
|
||||
|
||||
@media (max-height: 785px) {
|
||||
ul.sidebar .sidebar-title {
|
||||
line-height: 26px;
|
||||
}
|
||||
ul.sidebar .sidebar-title .form-control {
|
||||
height: 26px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
ul.sidebar .sidebar-list {
|
||||
height: 26px;
|
||||
}
|
||||
ul.sidebar .sidebar-list a,
|
||||
ul.sidebar .sidebar-list .sidebar-sublist a {
|
||||
font-size: 12px;
|
||||
line-height: 26px;
|
||||
}
|
||||
ul.sidebar .sidebar-list .menu-icon {
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 786px) and (max-height: 924px) {
|
||||
ul.sidebar .sidebar-title {
|
||||
line-height: 30px;
|
||||
}
|
||||
ul.sidebar .sidebar-title .form-control {
|
||||
height: 30px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
ul.sidebar .sidebar-list {
|
||||
height: 30px;
|
||||
}
|
||||
ul.sidebar .sidebar-list a,
|
||||
ul.sidebar .sidebar-list .sidebar-sublist a {
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
}
|
||||
ul.sidebar .sidebar-list .menu-icon {
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.margin-sm-top {
|
||||
margin-top: 5px;
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
padding-left: 70px;
|
||||
}
|
||||
}
|
||||
#page-wrapper.open #sidebar-wrapper {
|
||||
left: 150px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hamburg Menu
|
||||
@@ -254,139 +251,6 @@ div.input-mask {
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
/* #592727 RED */
|
||||
/* #2f5927 GREEN */
|
||||
/* #30426a BLUE (default)*/
|
||||
/* Sidebar background color */
|
||||
/* Sidebar header and footer color */
|
||||
/* Sidebar title text colour */
|
||||
/* Sidebar menu item hover color */
|
||||
/**
|
||||
* Sidebar
|
||||
*/
|
||||
#sidebar-wrapper {
|
||||
background: #30426a;
|
||||
}
|
||||
ul.sidebar .sidebar-main a,
|
||||
.sidebar-footer,
|
||||
ul.sidebar .sidebar-list a:hover,
|
||||
#page-wrapper:not(.open) ul.sidebar .sidebar-title.separator {
|
||||
/* Sidebar header and footer color */
|
||||
background: #2d3e63;
|
||||
}
|
||||
ul.sidebar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
text-indent: 20px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
ul.sidebar li a {
|
||||
color: #fff;
|
||||
display: block;
|
||||
float: left;
|
||||
text-decoration: none;
|
||||
width: 250px;
|
||||
}
|
||||
ul.sidebar .sidebar-main {
|
||||
height: 65px;
|
||||
}
|
||||
ul.sidebar .sidebar-main a {
|
||||
font-size: 18px;
|
||||
line-height: 60px;
|
||||
}
|
||||
ul.sidebar .sidebar-main a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
ul.sidebar .sidebar-main .menu-icon {
|
||||
float: right;
|
||||
font-size: 18px;
|
||||
padding-right: 28px;
|
||||
line-height: 60px;
|
||||
}
|
||||
ul.sidebar .sidebar-title {
|
||||
color: #738bc0;
|
||||
font-size: 12px;
|
||||
height: 35px;
|
||||
line-height: 40px;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.6s ease 0s;
|
||||
}
|
||||
ul.sidebar .sidebar-list {
|
||||
height: 40px;
|
||||
}
|
||||
ul.sidebar .sidebar-list a {
|
||||
text-indent: 25px;
|
||||
font-size: 15px;
|
||||
color: #b2bfdc;
|
||||
line-height: 40px;
|
||||
}
|
||||
ul.sidebar .sidebar-list a:hover {
|
||||
color: #fff;
|
||||
border-left: 3px solid #e99d1a;
|
||||
text-indent: 22px;
|
||||
}
|
||||
ul.sidebar .sidebar-list a:hover .menu-icon {
|
||||
text-indent: 25px;
|
||||
}
|
||||
ul.sidebar .sidebar-list .menu-icon {
|
||||
float: right;
|
||||
padding-right: 29px;
|
||||
line-height: 40px;
|
||||
width: 70px;
|
||||
}
|
||||
#page-wrapper:not(.open) ul.sidebar {
|
||||
bottom: 0;
|
||||
}
|
||||
#page-wrapper:not(.open) ul.sidebar .sidebar-title {
|
||||
display: none;
|
||||
height: 0px;
|
||||
text-indent: -100px;
|
||||
}
|
||||
#page-wrapper:not(.open) ul.sidebar .sidebar-title.separator {
|
||||
display: block;
|
||||
height: 2px;
|
||||
margin: 13px 0;
|
||||
}
|
||||
#page-wrapper:not(.open) ul.sidebar .sidebar-list a:hover span {
|
||||
border-left: 3px solid #e99d1a;
|
||||
text-indent: 22px;
|
||||
}
|
||||
#page-wrapper:not(.open) .sidebar-footer {
|
||||
display: none;
|
||||
}
|
||||
.sidebar-footer {
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
transition: all 0.6s ease 0s;
|
||||
text-align: center;
|
||||
}
|
||||
.sidebar-footer div a {
|
||||
color: #b2bfdc;
|
||||
font-size: 12px;
|
||||
line-height: 43px;
|
||||
}
|
||||
.sidebar-footer div a:hover {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* #592727 RED */
|
||||
/* #2f5927 GREEN */
|
||||
/* #30426a BLUE (default)*/
|
||||
/* Sidebar background color */
|
||||
/* Sidebar header and footer color */
|
||||
/* Sidebar title text colour */
|
||||
/* Sidebar menu item hover color */
|
||||
|
||||
/**
|
||||
* Widgets
|
||||
*/
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.azure').component('azureSidebarContent', {
|
||||
templateUrl: './azureSidebarContent.html',
|
||||
bindings: {
|
||||
endpointId: '<',
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="azure.dashboard({endpointId: $ctrl.endpointId})" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="azure.containerinstances({endpointId: $ctrl.endpointId})" ui-sref-active="active">Container instances <span class="menu-icon fa fa-cubes fa-fw"></span></a>
|
||||
</li>
|
||||
19
app/azure/components/azure-sidebar/azure-sidebar.html
Normal file
19
app/azure/components/azure-sidebar/azure-sidebar.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<sidebar-menu-item
|
||||
path="azure.dashboard"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-tachometer-alt fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="azureSidebar-dashboard"
|
||||
>
|
||||
Dashboard
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
path="azure.containerinstances"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-cubes fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="azureSidebar-containerInstances"
|
||||
>
|
||||
Container instances
|
||||
</sidebar-menu-item>
|
||||
8
app/azure/components/azure-sidebar/index.js
Normal file
8
app/azure/components/azure-sidebar/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.azure').component('azureSidebar', {
|
||||
templateUrl: './azure-sidebar.html',
|
||||
bindings: {
|
||||
endpointId: '<',
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
158
app/docker/components/docker-sidebar/docker-sidebar.html
Normal file
158
app/docker/components/docker-sidebar/docker-sidebar.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<sidebar-menu-item
|
||||
path="docker.dashboard"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-tachometer-alt fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="dockerSidebar-dashboard"
|
||||
>
|
||||
Dashboard
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu
|
||||
ng-if="!$ctrl.offlineMode"
|
||||
label="App Templates"
|
||||
icon-class="fa-rocket fa-fw"
|
||||
path="docker.templates"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
is-sidebar-open="$ctrl.isSidebarOpen"
|
||||
children-paths="[]"
|
||||
>
|
||||
<sidebar-menu-item path="docker.templates.custom" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist" data-cy="dockerSidebar-customTemplates">
|
||||
Custom Templates
|
||||
</sidebar-menu-item>
|
||||
</sidebar-menu>
|
||||
|
||||
<sidebar-menu-item
|
||||
ng-if="$ctrl.showStacks"
|
||||
path="docker.stacks"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-th-list fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="dockerSidebar-stacks"
|
||||
>
|
||||
Stacks
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
ng-if="$ctrl.swarmManagement"
|
||||
path="docker.services"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-list-alt fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="dockerSidebar-services"
|
||||
>
|
||||
Services
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item path="docker.containers" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-cubes fa-fw" class-name="sidebar-list" data-cy="dockerSidebar-containers">
|
||||
Containers
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item path="docker.images" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-clone fa-fw" class-name="sidebar-list" data-cy="dockerSidebar-images">
|
||||
Images
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item path="docker.networks" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-sitemap fa-fw" class-name="sidebar-list" data-cy="dockerSidebar-networks">
|
||||
Networks
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item path="docker.volumes" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-hdd fa-fw" class-name="sidebar-list" data-cy="dockerSidebar-volumes">
|
||||
Volumes
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
ng-if="$ctrl.endpointApiVersion >= 1.3 && $ctrl.swarmManagement"
|
||||
path="docker.configs"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-file-code fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="dockerSidebar-configs"
|
||||
>
|
||||
Configs
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
ng-if="$ctrl.endpointApiVersion >= 1.25 && $ctrl.swarmManagement"
|
||||
path="docker.secrets"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-user-secret fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="dockerSidebar-secrets"
|
||||
>
|
||||
Secrets
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess && !$ctrl.offlineMode"
|
||||
path="docker.events"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-history fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="dockerSidebar-events"
|
||||
>
|
||||
Events
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu
|
||||
ng-if="$ctrl.standaloneManagement"
|
||||
label="Host"
|
||||
icon-class="fa-th fa-fw"
|
||||
path="docker.host"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
is-sidebar-open="$ctrl.isSidebarOpen"
|
||||
children-paths="['docker.registries', 'docker.registries.access', 'docker.featuresConfiguration']"
|
||||
>
|
||||
<div ng-if="$ctrl.adminAccess">
|
||||
<sidebar-menu-item
|
||||
authorization="PortainerEndpointUpdateSettings"
|
||||
path="docker.featuresConfiguration"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
class-name="sidebar-sublist"
|
||||
data-cy="dockerSidebar-setup"
|
||||
>
|
||||
Setup
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
authorization="PortainerRegistryList"
|
||||
path="docker.registries"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
class-name="sidebar-sublist"
|
||||
data-cy="dockerSidebar-registries"
|
||||
>
|
||||
Registries
|
||||
</sidebar-menu-item>
|
||||
</div>
|
||||
</sidebar-menu>
|
||||
|
||||
<sidebar-menu
|
||||
ng-if="$ctrl.swarmManagement"
|
||||
label="Swarm"
|
||||
icon-class="fa-object-group fa-fw"
|
||||
path="docker.swarm"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
is-sidebar-open="$ctrl.isSidebarOpen"
|
||||
children-paths="['docker.registries', 'docker.registries.access', 'docker.featuresConfiguration']"
|
||||
>
|
||||
<div ng-if="$ctrl.adminAccess">
|
||||
<sidebar-menu-item
|
||||
authorization="PortainerEndpointUpdateSettings"
|
||||
path="docker.featuresConfiguration"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
class-name="sidebar-sublist"
|
||||
data-cy="swarmSidebar-setup"
|
||||
>
|
||||
Setup
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
authorization="PortainerRegistryList"
|
||||
path="docker.registries"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
class-name="sidebar-sublist"
|
||||
data-cy="swarmSidebar-registries"
|
||||
>
|
||||
Registries
|
||||
</sidebar-menu-item>
|
||||
</div>
|
||||
</sidebar-menu>
|
||||
@@ -1,12 +1,13 @@
|
||||
angular.module('portainer.docker').component('dockerSidebarContent', {
|
||||
templateUrl: './dockerSidebarContent.html',
|
||||
angular.module('portainer.docker').component('dockerSidebar', {
|
||||
templateUrl: './docker-sidebar.html',
|
||||
bindings: {
|
||||
isSidebarOpen: '<',
|
||||
|
||||
endpointApiVersion: '<',
|
||||
swarmManagement: '<',
|
||||
standaloneManagement: '<',
|
||||
adminAccess: '<',
|
||||
offlineMode: '<',
|
||||
toggle: '<',
|
||||
currentRouteName: '<',
|
||||
endpointId: '<',
|
||||
showStacks: '<',
|
||||
@@ -1,53 +0,0 @@
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="docker.dashboard({endpointId: $ctrl.endpointId})" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="!$ctrl.offlineMode" authorization="DockerContainerCreate, PortainerStackCreate">
|
||||
<a ui-sref="docker.templates({endpointId: $ctrl.endpointId})" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
||||
|
||||
<div class="sidebar-sublist" ng-if="$ctrl.toggle && $ctrl.currentRouteName.includes('docker.templates')">
|
||||
<a ui-sref="docker.templates.custom({endpointId: $ctrl.endpointId})" ui-sref-active="active">Custom Templates</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.showStacks">
|
||||
<a ui-sref="docker.stacks({endpointId: $ctrl.endpointId})" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
|
||||
<a ui-sref="docker.services({endpointId: $ctrl.endpointId})" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="docker.containers({endpointId: $ctrl.endpointId})" ui-sref-active="active">Containers <span class="menu-icon fa fa-cubes fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="docker.images({endpointId: $ctrl.endpointId})" ui-sref-active="active">Images <span class="menu-icon fa fa-clone fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="docker.networks({endpointId: $ctrl.endpointId})" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="docker.volumes({endpointId: $ctrl.endpointId})" ui-sref-active="active">Volumes <span class="menu-icon fa fa-hdd fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.3 && $ctrl.swarmManagement">
|
||||
<a ui-sref="docker.configs({endpointId: $ctrl.endpointId})" ui-sref-active="active">Configs <span class="menu-icon fa fa-file-code fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.25 && $ctrl.swarmManagement">
|
||||
<a ui-sref="docker.secrets({endpointId: $ctrl.endpointId})" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess && !$ctrl.offlineMode">
|
||||
<a ui-sref="docker.events({endpointId: $ctrl.endpointId})" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ng-if="$ctrl.standaloneManagement" ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
|
||||
<a ng-if="$ctrl.swarmManagement" ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
|
||||
|
||||
<div
|
||||
ng-if="$ctrl.adminAccess && ['docker.swarm', 'docker.host', 'docker.registries', 'docker.registries.access', 'docker.featuresConfiguration'].includes($ctrl.currentRouteName)"
|
||||
>
|
||||
<div class="sidebar-sublist">
|
||||
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-sublist">
|
||||
<a ui-sref="docker.registries({endpointId: $ctrl.endpointId})" ui-sref-active="active">Registries</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -26,6 +26,7 @@
|
||||
placeholder="e.g. myImage:myTag"
|
||||
ng-change="$ctrl.onImageChange()"
|
||||
required
|
||||
data-cy="component-imageInput"
|
||||
/>
|
||||
<span ng-if="$ctrl.isDockerHubRegistry()" class="input-group-btn">
|
||||
<a
|
||||
|
||||
@@ -133,7 +133,7 @@ angular.module('portainer.docker').factory('ImageService', [
|
||||
|
||||
Image.create({}, imageConfiguration)
|
||||
.$promise.then(function success(data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('message');
|
||||
var err = data.length > 0 && data[data.length - 1].message;
|
||||
if (err) {
|
||||
var detail = data[data.length - 1];
|
||||
deferred.reject({ msg: detail.message });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ui-select multiple ng-model="$ctrl.model" close-on-select="false">
|
||||
<ui-select multiple ng-model="$ctrl.model" close-on-select="false" data-cy="edgeGroupCreate-edgeGroupsSelector">
|
||||
<ui-select-match placeholder="Select one or multiple group(s)">
|
||||
<span>
|
||||
{{ $item.Name }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Edge Stacks
|
||||
</div>
|
||||
<div class="settings">
|
||||
<div class="settings" data-cy="edgeStack-stackTableSettings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle> <i class="fa fa-cog" aria-hidden="true"></i> Settings </span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
@@ -17,7 +17,13 @@
|
||||
<div class="menuContent">
|
||||
<div>
|
||||
<div class="md-checkbox">
|
||||
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
|
||||
<input
|
||||
id="setting_auto_refresh"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.settings.repeater.autoRefresh"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
data-cy="edgeStack-autoRefreshCheckbox"
|
||||
/>
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
</div>
|
||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||
@@ -48,10 +54,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="edgeStack-removeStackButton"
|
||||
>
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="edge.stacks.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack </button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="edge.stacks.new" data-cy="edgeStack-addStackButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
@@ -66,12 +80,12 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<table class="table table-hover nowrap-cells" data-cy="edgeStack-stackTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="edgeStack-selectAllCheckbox" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
@@ -109,10 +123,10 @@
|
||||
<td><edge-stack-status stack-status="item.Status"></edge-stack-status></td>
|
||||
<td>{{ item.CreationDate | getisodatefromtimestamp }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<tr ng-if="!$ctrl.dataset" data-cy="edgeStack-loadingRow">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0" data-cy="edgeStack-noStackRow">
|
||||
<td colspan="4" class="text-center text-muted">
|
||||
No stack available.
|
||||
</td>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Name
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="group_name" name="group_name" ng-model="$ctrl.model.Name" required auto-focus />
|
||||
<input type="text" class="form-control" id="group_name" name="group_name" ng-model="$ctrl.model.Name" required auto-focus data-cy="edgeGroupCreate-groupNameInput" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="EdgeGroupForm.group_name.$invalid">
|
||||
@@ -138,6 +138,7 @@
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.actionInProgress || !EdgeGroupForm.$valid || (!$ctrl.model.Dynamic && !$ctrl.model.Endpoints.length) || ($ctrl.model.Dynamic && !$ctrl.model.TagIds.length)"
|
||||
button-spinner="$ctrl.actionInProgress"
|
||||
data-cy="edgeGroupCreate-addGroupButton"
|
||||
>
|
||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||
|
||||
@@ -3,10 +3,18 @@
|
||||
<rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="Edge Groups"> </rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="edgeGroup-removeEdgeGroupButton"
|
||||
>
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="edge.groups.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add Edge group </button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="edge.groups.new" data-cy="edgeGroup-addEdgeGroupButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add Edge group
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
@@ -17,15 +25,16 @@
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search..."
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="edgeGroup-searchInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<table class="table table-hover nowrap-cells" data-cy="edgeGroup-edgeGroupTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="edgeGroup-selectAllCheckbox" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Name
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.formValues.Name" id="stack_name" placeholder="e.g. mystack" auto-focus />
|
||||
<input type="text" class="form-control" ng-model="$ctrl.formValues.Name" id="stack_name" placeholder="e.g. mystack" auto-focus data-cy="edgeStackCreate-nameInput" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="method_editor" ng-model="$ctrl.state.Method" value="editor" ng-change="$ctrl.onChangeMethod()" />
|
||||
<label for="method_editor">
|
||||
<label for="method_editor" data-cy="edgeStackCreate-webEditorButton">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Web editor
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="method_upload" ng-model="$ctrl.state.Method" value="upload" ng-change="$ctrl.onChangeMethod()" />
|
||||
<label for="method_upload">
|
||||
<label for="method_upload" data-cy="edgeStackCreate-uploadButton">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Upload
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="method_repository" ng-model="$ctrl.state.Method" value="repository" ng-change="$ctrl.onChangeMethod()" />
|
||||
<label for="method_repository">
|
||||
<label for="method_repository" data-cy="edgeStackCreate-repoButton">
|
||||
<div class="boxselector_header">
|
||||
<i class="fab fa-git" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Repository
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="method_template" ng-model="$ctrl.state.Method" value="template" ng-change="$ctrl.onChangeMethod()" />
|
||||
<label for="method_template">
|
||||
<label for="method_template" data-cy="edgeStackCreate-templateButton">
|
||||
<div class="boxselector_header">
|
||||
<i class="fas fa-rocket" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Template
|
||||
@@ -198,6 +198,7 @@
|
||||
|| !$ctrl.formValues.Name"
|
||||
ng-click="$ctrl.createStack()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
data-cy="edgeStackCreate-createStackButton"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||
|
||||
@@ -16,7 +16,7 @@ angular.module('portainer.integrations.storidge').factory('StoridgeNodeService',
|
||||
var nodes = [];
|
||||
|
||||
for (var key in nodeData) {
|
||||
if (nodeData.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(nodeData, key)) {
|
||||
nodes.push(new StoridgeNodeModel(key, nodeData[key]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ angular.module('portainer.integrations.storidge').factory('StoridgeSnapshotServi
|
||||
var snapshotsData = data.snapshots;
|
||||
let snapshotsArray = [];
|
||||
for (const key in snapshotsData) {
|
||||
if (snapshotsData.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(snapshotsData, key)) {
|
||||
snapshotsArray.push(snapshotsData[key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<table class="table table-hover nowrap-cells" data-cy="k8sAppDetail-containerTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-if="!$ctrl.isPod">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
System resources are hidden, this can be changed in the table settings.
|
||||
</span>
|
||||
<div class="settings">
|
||||
<div class="settings" data-cy="k8sApp-tableSettings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
@@ -22,14 +22,26 @@
|
||||
<label for="applications_setting_show_system">Show system resources</label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
|
||||
<input
|
||||
id="setting_auto_refresh"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.settings.repeater.autoRefresh"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
data-cy="k8sApp-autoRefreshCheckbox"
|
||||
/>
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
</div>
|
||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||
<label for="settings_refresh_rate">
|
||||
Refresh rate
|
||||
</label>
|
||||
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||
<select
|
||||
id="settings_refresh_rate"
|
||||
ng-model="$ctrl.settings.repeater.refreshRate"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
class="small-select"
|
||||
data-cy="k8sApp-refreshRateDropdown"
|
||||
>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">1min</option>
|
||||
@@ -43,7 +55,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,10 +63,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="k8sApp-removeAppButton"
|
||||
>
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.new">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.new" data-cy="k8sApp-addApplicationButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application
|
||||
</button>
|
||||
</div>
|
||||
@@ -68,15 +86,16 @@
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="k8sApp-searchApplicationsInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<table class="table table-hover nowrap-cells" data-cy="k8sApp-appTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="k8sApp-selectAllCheckbox" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })" ng-click="$event.stopPropagation();">{{ item.ResourcePool }}</a>
|
||||
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
||||
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item.ResourcePool)">system</span>
|
||||
</td>
|
||||
<td>{{ item.Applications.length }}</td>
|
||||
<td>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</span>
|
||||
<div class="settings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
|
||||
<span uib-dropdown-toggle data-cy="k8sConfig-configSettingsButton"><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader">
|
||||
@@ -18,7 +18,13 @@
|
||||
<div class="menuContent">
|
||||
<div>
|
||||
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
||||
<input id="setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
||||
<input
|
||||
id="setting_show_system"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.settings.showSystem"
|
||||
ng-change="$ctrl.onSettingsShowSystemChange()"
|
||||
data-cy="k8sConfig-systemResourceCheckbox"
|
||||
/>
|
||||
<label for="setting_show_system">Show system resources</label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
@@ -43,7 +49,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sConfig-closeSettingsButton">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,10 +57,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="k8sConfig-removeConfigButton"
|
||||
>
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new" data-cy="k8sConfig-addConfigButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration
|
||||
</button>
|
||||
</div>
|
||||
@@ -68,10 +80,11 @@
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="k8sConfig-searchInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<table class="table table-hover nowrap-cells" data-cy="k8sConfig-tableSettingsButtonconfigsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
@@ -145,7 +158,7 @@
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="k8sConfig-paginationDropdown">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -52,10 +52,11 @@
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="k8sConfigDetail-eventsTableSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<table class="table table-hover nowrap-cells" data-cy="k8sConfigDetail-eventsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
@@ -117,7 +118,12 @@
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="$ctrl.state.paginatedItemLimit"
|
||||
ng-change="$ctrl.changePaginationLimit()"
|
||||
data-cy="k8sConfigDetail-eventsTablePaginationDropdown"
|
||||
>
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -51,10 +51,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="$ctrl.isAdmin" class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="k8sNamespace-removeNamespaceButton"
|
||||
>
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace
|
||||
</button>
|
||||
</div>
|
||||
@@ -68,6 +74,7 @@
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="k8sNamespace-namespaceSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export default class KubeConfigController {
|
||||
/* @ngInject */
|
||||
constructor($window, KubernetesConfigService) {
|
||||
this.$window = $window;
|
||||
this.KubernetesConfigService = KubernetesConfigService;
|
||||
}
|
||||
|
||||
async downloadKubeconfig() {
|
||||
await this.KubernetesConfigService.downloadConfig();
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.state = { isHTTPS: this.$window.location.protocol === 'https:' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<button
|
||||
ng-if="$ctrl.state.isHTTPS"
|
||||
type="button"
|
||||
class="btn btn-xs btn-primary"
|
||||
ng-click="$ctrl.downloadKubeconfig()"
|
||||
analytics-on
|
||||
analytics-category="kubernetes"
|
||||
analytics-event="kubernetes-kubectl-kubeconfig"
|
||||
>
|
||||
Kubeconfig <i class="fas fa-download space-right"></i>
|
||||
</button>
|
||||
@@ -0,0 +1,7 @@
|
||||
import angular from 'angular';
|
||||
import controller from './kube-config-download-button.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubeConfigDownloadButton', {
|
||||
templateUrl: './kube-config-download-button.html',
|
||||
controller,
|
||||
});
|
||||
@@ -3,13 +3,12 @@ import * as fit from 'xterm/lib/addons/fit/fit';
|
||||
|
||||
export default class KubectlShellController {
|
||||
/* @ngInject */
|
||||
constructor(TerminalWindow, $window, $async, EndpointProvider, LocalStorage, KubernetesConfigService, Notifications) {
|
||||
constructor(TerminalWindow, $window, $async, EndpointProvider, LocalStorage, Notifications) {
|
||||
this.$async = $async;
|
||||
this.$window = $window;
|
||||
this.TerminalWindow = TerminalWindow;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.KubernetesConfigService = KubernetesConfigService;
|
||||
this.Notifications = Notifications;
|
||||
}
|
||||
|
||||
@@ -39,6 +38,7 @@ export default class KubectlShellController {
|
||||
}
|
||||
|
||||
configureSocketAndTerminal(socket, term) {
|
||||
var vm = this;
|
||||
socket.onopen = function () {
|
||||
const terminal_container = document.getElementById('terminal-container');
|
||||
term.open(terminal_container);
|
||||
@@ -55,7 +55,7 @@ export default class KubectlShellController {
|
||||
});
|
||||
|
||||
this.$window.onresize = function () {
|
||||
term.fit();
|
||||
vm.TerminalWindow.terminalresize();
|
||||
};
|
||||
|
||||
socket.onmessage = function (msg) {
|
||||
@@ -82,7 +82,7 @@ export default class KubectlShellController {
|
||||
endpointId: this.EndpointProvider.endpointID(),
|
||||
};
|
||||
|
||||
const wsProtocol = this.state.isHTTPS ? 'wss://' : 'ws://';
|
||||
const wsProtocol = this.$window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const path = '/api/websocket/kubernetes-shell';
|
||||
const queryParams = Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
@@ -96,17 +96,12 @@ export default class KubectlShellController {
|
||||
this.configureSocketAndTerminal(this.state.shell.socket, this.state.shell.term);
|
||||
}
|
||||
|
||||
async downloadKubeconfig() {
|
||||
await this.KubernetesConfigService.downloadConfig();
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.state = {
|
||||
css: 'normal',
|
||||
checked: false,
|
||||
icon: 'fa-window-minimize',
|
||||
isHTTPS: this.$window.location.protocol === 'https:',
|
||||
shell: {
|
||||
connected: false,
|
||||
socket: null,
|
||||
|
||||
@@ -60,7 +60,7 @@ ul.sidebar li .shell-item-center a:hover {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 480px;
|
||||
height: 495px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.connectConsole()" ng-disabled="$ctrl.state.shell.connected">
|
||||
<i class="fa fa-terminal" style="margin-right: 2px;"></i>
|
||||
kubectl shell
|
||||
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.connectConsole()" ng-disabled="$ctrl.state.shell.connected" data-cy="k8sSidebar-shellButton">
|
||||
<i class="fa fa-terminal space-right"></i> kubectl shell
|
||||
</button>
|
||||
|
||||
<kube-config-download-button></kube-config-download-button>
|
||||
|
||||
<div ng-if="$ctrl.state.checked" class="{{ $ctrl.state.css }}-kubectl-shell">
|
||||
<div class="shell-container">
|
||||
<div class="shell-item"><i class="fas fa-terminal" style="margin-right: 5px;"></i>kubectl shell</div>
|
||||
<div ng-if="$ctrl.state.isHTTPS" class="shell-item-center">
|
||||
<a href="" ng-click="$ctrl.downloadKubeconfig()"><i class="fas fa-file-download" style="margin-right: 5px;"></i>Download Kubeconfig</a>
|
||||
</div>
|
||||
<div class="shell-item-right">
|
||||
<i class="fas fa-redo-alt" ng-click="$ctrl.screenClear();"></i>
|
||||
<i class="fas {{ $ctrl.state.icon }}" ng-click="$ctrl.miniRestore();"></i>
|
||||
<i class="fas fa-times" ng-click="$ctrl.disconnect()"></i>
|
||||
<i class="fas fa-redo-alt" ng-click="$ctrl.screenClear();" data-cy="k8sShell-refreshButton"></i>
|
||||
<i
|
||||
class="fas {{ $ctrl.state.icon }}"
|
||||
ng-click="$ctrl.miniRestore();"
|
||||
data-cy="{{ $ctrl.state.icon === '.fa-window-minimize' ? 'k8sShell-restore' : 'k8sShell-minimise' }}"
|
||||
></i>
|
||||
<i class="fas fa-times" ng-click="$ctrl.disconnect()" data-cy="k8sShell-closeButton"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0;" ng-click="$ctrl.addEntry()">
|
||||
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0;" ng-click="$ctrl.addEntry()" data-cy="k8sConfigCreate-createEntryButton">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> Create entry
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0;">
|
||||
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0;" data-cy="k8sConfigCreate-createConfigsFromFileButton">
|
||||
<i class="fa fa-file-upload" aria-hidden="true"></i> Create key/value from file
|
||||
</button>
|
||||
</div>
|
||||
@@ -98,7 +98,14 @@
|
||||
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
|
||||
<div class="col-sm-1"></div>
|
||||
<div class="col-sm-11">
|
||||
<button type="button" class="btn btn-sm btn-danger space-right" style="margin-left: 0;" ng-disabled="entry.Used" ng-click="$ctrl.removeEntry(index, entry)">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger space-right"
|
||||
style="margin-left: 0;"
|
||||
ng-disabled="entry.Used"
|
||||
ng-click="$ctrl.removeEntry(index, entry)"
|
||||
data-cy="k8sConfigDetail-removeEntryButton{{ index }}"
|
||||
>
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove entry
|
||||
</button>
|
||||
<span class="small text-muted" ng-if="entry.Used">
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="kubernetes.dashboard({endpointId: $ctrl.endpointId})" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="kubernetes.resourcePools({endpointId: $ctrl.endpointId})" ui-sref-active="active">Namespaces <span class="menu-icon fa fa-layer-group fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="kubernetes.applications({endpointId: $ctrl.endpointId})" ui-sref-active="active">Applications <span class="menu-icon fa fa-laptop-code fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="kubernetes.configurations({endpointId: $ctrl.endpointId})" ui-sref-active="active">Configurations <span class="menu-icon fa fa-file-code fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="kubernetes.volumes({endpointId: $ctrl.endpointId})" ui-sref-active="active">Volumes <span class="menu-icon fa fa-database fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="kubernetes.cluster({endpointId: $ctrl.endpointId})" ui-sref-active="active">Cluster <span class="menu-icon fa fa-server fa-fw"></span></a>
|
||||
<div
|
||||
ng-if="
|
||||
$ctrl.adminAccess &&
|
||||
['kubernetes.cluster', 'portainer.endpoints.endpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access'].includes($ctrl.currentState)
|
||||
"
|
||||
>
|
||||
<div class="sidebar-sublist">
|
||||
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
|
||||
</div>
|
||||
<div class="sidebar-sublist">
|
||||
<a ui-sref="kubernetes.registries({endpointId: $ctrl.endpointId})" ui-sref-active="active">Registries</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1,8 +1,10 @@
|
||||
angular.module('portainer.kubernetes').component('kubernetesSidebarContent', {
|
||||
templateUrl: './kubernetesSidebarContent.html',
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesSidebar', {
|
||||
templateUrl: './kubernetes-sidebar.html',
|
||||
bindings: {
|
||||
adminAccess: '<',
|
||||
endpointId: '<',
|
||||
currentState: '<',
|
||||
isSidebarOpen: '<',
|
||||
adminAccess: '<',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
<sidebar-menu-item
|
||||
path="kubernetes.dashboard"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-tachometer-alt fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="k8sSidebar-dashboard"
|
||||
>
|
||||
Dashboard
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
path="kubernetes.resourcePools"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-layer-group fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="k8sSidebar-namespaces"
|
||||
>
|
||||
Namespaces
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
path="kubernetes.applications"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-laptop-code fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="k8sSidebar-applications"
|
||||
>
|
||||
Applications
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
path="kubernetes.configurations"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-file-code fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="k8sSidebar-configurations"
|
||||
>
|
||||
Configurations
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item path="kubernetes.volumes" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-database fa-fw" class-name="sidebar-list" data-cy="k8sSidebar-volumes">
|
||||
Volumes
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu
|
||||
icon-class="fa-server fa-fw"
|
||||
label="Cluster"
|
||||
path="kubernetes.cluster"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
is-sidebar-open="$ctrl.isSidebarOpen"
|
||||
children-paths="['kubernetes.cluster', 'portainer.endpoints.endpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access']"
|
||||
>
|
||||
<div ng-if="$ctrl.adminAccess">
|
||||
<sidebar-menu-item
|
||||
authorization="K8sClusterSetupRW"
|
||||
path="portainer.endpoints.endpoint.kubernetesConfig"
|
||||
path-params="{ id: $ctrl.endpointId }"
|
||||
class-name="sidebar-sublist"
|
||||
data-cy="k8sSidebar-setup"
|
||||
>
|
||||
Setup
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
authorization="PortainerRegistryList"
|
||||
path="kubernetes.registries"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
class-name="sidebar-sublist"
|
||||
data-cy="k8sSidebar-registries"
|
||||
>
|
||||
Registries
|
||||
</sidebar-menu-item>
|
||||
</div>
|
||||
</sidebar-menu>
|
||||
@@ -14,7 +14,12 @@
|
||||
Memory reservation
|
||||
</label>
|
||||
<div class="col-sm-9" style="margin-top: 4px;">
|
||||
<uib-progressbar animate="false" value="$ctrl.memoryReservationPercent" type="{{ $ctrl.memoryReservationPercent | kubernetesUsageLevelInfo }}">
|
||||
<uib-progressbar
|
||||
animate="false"
|
||||
value="$ctrl.memoryReservationPercent"
|
||||
type="{{ $ctrl.memoryReservationPercent | kubernetesUsageLevelInfo }}"
|
||||
data-cy="k8sNamespaceDetail-memoryUsage"
|
||||
>
|
||||
<b style="white-space: nowrap;"> {{ $ctrl.memoryReservation }} / {{ $ctrl.memoryLimit }} MB - {{ $ctrl.memoryReservationPercent }}% </b>
|
||||
</uib-progressbar>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<rd-header ng-if="$ctrl.viewReady">
|
||||
<rd-header-title title-text="{{ $ctrl.title }}">
|
||||
<a data-toggle="tooltip" title="refresh the view" ui-sref="{{ $ctrl.state }}" ui-sref-opts="{reload: true}" ng-if="$ctrl.viewReady">
|
||||
<i class="fa fa-sm fa-sync" aria-hidden="true"></i>
|
||||
<i class="fa fa-sm fa-sync" aria-hidden="true" data-cy="component-refreshTableButton"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,10 +19,10 @@ export class KubernetesIngressConverter {
|
||||
: _.map(rule.http.paths, (path) => {
|
||||
const ingRule = new KubernetesIngressRule();
|
||||
ingRule.IngressName = data.metadata.name;
|
||||
ingRule.ServiceName = path.backend.serviceName;
|
||||
ingRule.ServiceName = path.backend.service.name;
|
||||
ingRule.Host = rule.host || '';
|
||||
ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined;
|
||||
ingRule.Port = path.backend.servicePort;
|
||||
ingRule.Port = path.backend.service.port.number;
|
||||
ingRule.Path = path.path;
|
||||
return ingRule;
|
||||
});
|
||||
@@ -151,8 +151,8 @@ export class KubernetesIngressConverter {
|
||||
rule.http.paths = _.map(paths, (p) => {
|
||||
const path = new KubernetesIngressRulePathCreatePayload();
|
||||
path.path = p.Path;
|
||||
path.backend.serviceName = p.ServiceName;
|
||||
path.backend.servicePort = p.Port;
|
||||
path.backend.service.name = p.ServiceName;
|
||||
path.backend.service.port.number = p.Port;
|
||||
return path;
|
||||
});
|
||||
hostsWithRules.push(host);
|
||||
@@ -173,7 +173,7 @@ export class KubernetesIngressConverter {
|
||||
res.spec.rules = [];
|
||||
_.forEach(data.Hosts, (host) => {
|
||||
if (!host.NeedsDeletion) {
|
||||
res.spec.rules.push({ host: host.Host });
|
||||
res.spec.rules.push({ host: host.Host || host });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -20,10 +20,15 @@ export function KubernetesIngressRuleCreatePayload() {
|
||||
|
||||
export function KubernetesIngressRulePathCreatePayload() {
|
||||
return {
|
||||
backend: {
|
||||
serviceName: '',
|
||||
servicePort: 0,
|
||||
},
|
||||
path: '',
|
||||
pathType: 'ImplementationSpecific',
|
||||
backend: {
|
||||
service: {
|
||||
name: '',
|
||||
port: {
|
||||
number: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ angular.module('portainer.kubernetes').factory('KubernetesIngresses', factory);
|
||||
function factory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
'use strict';
|
||||
return function (namespace) {
|
||||
const url = `${API_ENDPOINT_ENDPOINTS}/:endpointId/kubernetes/apis/networking.k8s.io/v1beta1${namespace ? '/namespaces/:namespace' : ''}/ingresses/:id/:action`;
|
||||
const url = `${API_ENDPOINT_ENDPOINTS}/:endpointId/kubernetes/apis/networking.k8s.io/v1${namespace ? '/namespaces/:namespace' : ''}/ingresses/:id/:action`;
|
||||
return $resource(
|
||||
url,
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
|
||||
12
app/kubernetes/rest/portainer-namespace.js
Normal file
12
app/kubernetes/rest/portainer-namespace.js
Normal file
@@ -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' } },
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -89,122 +89,110 @@ class KubernetesApplicationService {
|
||||
|
||||
/* #region GET */
|
||||
async getAsync(namespace, name) {
|
||||
try {
|
||||
const [deployment, daemonSet, statefulSet, pod, pods, autoScalers, ingresses] = await Promise.allSettled([
|
||||
this.KubernetesDeploymentService.get(namespace, name),
|
||||
this.KubernetesDaemonSetService.get(namespace, name),
|
||||
this.KubernetesStatefulSetService.get(namespace, name),
|
||||
this.KubernetesPodService.get(namespace, name),
|
||||
this.KubernetesPodService.get(namespace),
|
||||
this.KubernetesHorizontalPodAutoScalerService.get(namespace),
|
||||
this.KubernetesIngressService.get(namespace),
|
||||
]);
|
||||
const [deployment, daemonSet, statefulSet, pod, pods, autoScalers, ingresses] = await Promise.allSettled([
|
||||
this.KubernetesDeploymentService.get(namespace, name),
|
||||
this.KubernetesDaemonSetService.get(namespace, name),
|
||||
this.KubernetesStatefulSetService.get(namespace, name),
|
||||
this.KubernetesPodService.get(namespace, name),
|
||||
this.KubernetesPodService.get(namespace),
|
||||
this.KubernetesHorizontalPodAutoScalerService.get(namespace),
|
||||
this.KubernetesIngressService.get(namespace),
|
||||
]);
|
||||
|
||||
// const pod = _.find(pods.value, ['metadata.namespace', namespace, 'metadata.name', name]);
|
||||
|
||||
let rootItem;
|
||||
let converterFunc;
|
||||
if (deployment.status === 'fulfilled') {
|
||||
rootItem = deployment;
|
||||
converterFunc = KubernetesApplicationConverter.apiDeploymentToApplication;
|
||||
} else if (daemonSet.status === 'fulfilled') {
|
||||
rootItem = daemonSet;
|
||||
converterFunc = KubernetesApplicationConverter.apiDaemonSetToApplication;
|
||||
} else if (statefulSet.status === 'fulfilled') {
|
||||
rootItem = statefulSet;
|
||||
converterFunc = KubernetesApplicationConverter.apiStatefulSetToapplication;
|
||||
} else if (pod.status === 'fulfilled') {
|
||||
rootItem = pod;
|
||||
converterFunc = KubernetesApplicationConverter.apiPodToApplication;
|
||||
} else {
|
||||
throw new PortainerError('Unable to determine which association to use to convert application');
|
||||
}
|
||||
|
||||
const services = await this.KubernetesServiceService.get(namespace);
|
||||
const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw);
|
||||
const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {};
|
||||
|
||||
const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value);
|
||||
application.Yaml = rootItem.value.Yaml;
|
||||
application.Raw = rootItem.value.Raw;
|
||||
application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item));
|
||||
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
||||
|
||||
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application);
|
||||
const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined;
|
||||
application.AutoScaler = scaler;
|
||||
|
||||
await this.KubernetesHistoryService.get(application);
|
||||
|
||||
if (service.Yaml) {
|
||||
application.Yaml += '---\n' + service.Yaml;
|
||||
}
|
||||
if (scaler && scaler.Yaml) {
|
||||
application.Yaml += '---\n' + scaler.Yaml;
|
||||
}
|
||||
// TODO: refactor @LP
|
||||
// append ingress yaml ?
|
||||
return application;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
let rootItem;
|
||||
let converterFunc;
|
||||
if (deployment.status === 'fulfilled') {
|
||||
rootItem = deployment;
|
||||
converterFunc = KubernetesApplicationConverter.apiDeploymentToApplication;
|
||||
} else if (daemonSet.status === 'fulfilled') {
|
||||
rootItem = daemonSet;
|
||||
converterFunc = KubernetesApplicationConverter.apiDaemonSetToApplication;
|
||||
} else if (statefulSet.status === 'fulfilled') {
|
||||
rootItem = statefulSet;
|
||||
converterFunc = KubernetesApplicationConverter.apiStatefulSetToapplication;
|
||||
} else if (pod.status === 'fulfilled') {
|
||||
rootItem = pod;
|
||||
converterFunc = KubernetesApplicationConverter.apiPodToApplication;
|
||||
} else {
|
||||
throw new PortainerError('Unable to determine which association to use to convert application');
|
||||
}
|
||||
|
||||
const services = await this.KubernetesServiceService.get(namespace);
|
||||
const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw);
|
||||
const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {};
|
||||
|
||||
const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value);
|
||||
application.Yaml = rootItem.value.Yaml;
|
||||
application.Raw = rootItem.value.Raw;
|
||||
application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item));
|
||||
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
||||
|
||||
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application);
|
||||
const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined;
|
||||
application.AutoScaler = scaler;
|
||||
|
||||
await this.KubernetesHistoryService.get(application);
|
||||
|
||||
if (service.Yaml) {
|
||||
application.Yaml += '---\n' + service.Yaml;
|
||||
}
|
||||
if (scaler && scaler.Yaml) {
|
||||
application.Yaml += '---\n' + scaler.Yaml;
|
||||
}
|
||||
// TODO: refactor @LP
|
||||
// append ingress yaml ?
|
||||
return application;
|
||||
}
|
||||
|
||||
async getAllAsync(namespace) {
|
||||
try {
|
||||
const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name');
|
||||
const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name');
|
||||
|
||||
const convertToApplication = (item, converterFunc, services, pods, ingresses) => {
|
||||
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
|
||||
const application = converterFunc(item, pods, service, ingresses);
|
||||
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
||||
return application;
|
||||
};
|
||||
const convertToApplication = (item, converterFunc, services, pods, ingresses) => {
|
||||
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
|
||||
const application = converterFunc(item, pods, service, ingresses);
|
||||
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
||||
return application;
|
||||
};
|
||||
|
||||
const res = await Promise.all(
|
||||
_.map(namespaces, async (ns) => {
|
||||
const [deployments, daemonSets, statefulSets, services, pods, ingresses, autoScalers] = await Promise.all([
|
||||
this.KubernetesDeploymentService.get(ns),
|
||||
this.KubernetesDaemonSetService.get(ns),
|
||||
this.KubernetesStatefulSetService.get(ns),
|
||||
this.KubernetesServiceService.get(ns),
|
||||
this.KubernetesPodService.get(ns),
|
||||
this.KubernetesIngressService.get(ns),
|
||||
this.KubernetesHorizontalPodAutoScalerService.get(ns),
|
||||
]);
|
||||
const res = await Promise.all(
|
||||
_.map(namespaces, async (ns) => {
|
||||
const [deployments, daemonSets, statefulSets, services, pods, ingresses, autoScalers] = await Promise.all([
|
||||
this.KubernetesDeploymentService.get(ns),
|
||||
this.KubernetesDaemonSetService.get(ns),
|
||||
this.KubernetesStatefulSetService.get(ns),
|
||||
this.KubernetesServiceService.get(ns),
|
||||
this.KubernetesPodService.get(ns),
|
||||
this.KubernetesIngressService.get(ns),
|
||||
this.KubernetesHorizontalPodAutoScalerService.get(ns),
|
||||
]);
|
||||
|
||||
const deploymentApplications = _.map(deployments, (item) =>
|
||||
convertToApplication(item, KubernetesApplicationConverter.apiDeploymentToApplication, services, pods, ingresses)
|
||||
);
|
||||
const daemonSetApplications = _.map(daemonSets, (item) =>
|
||||
convertToApplication(item, KubernetesApplicationConverter.apiDaemonSetToApplication, services, pods, ingresses)
|
||||
);
|
||||
const statefulSetApplications = _.map(statefulSets, (item) =>
|
||||
convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses)
|
||||
);
|
||||
const deploymentApplications = _.map(deployments, (item) =>
|
||||
convertToApplication(item, KubernetesApplicationConverter.apiDeploymentToApplication, services, pods, ingresses)
|
||||
);
|
||||
const daemonSetApplications = _.map(daemonSets, (item) => convertToApplication(item, KubernetesApplicationConverter.apiDaemonSetToApplication, services, pods, ingresses));
|
||||
const statefulSetApplications = _.map(statefulSets, (item) =>
|
||||
convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses)
|
||||
);
|
||||
|
||||
const boundPods = _.concat(_.flatMap(deploymentApplications, 'Pods'), _.flatMap(daemonSetApplications, 'Pods'), _.flatMap(statefulSetApplications, 'Pods'));
|
||||
const unboundPods = _.without(pods, ...boundPods);
|
||||
const nakedPodsApplications = _.map(unboundPods, (item) => convertToApplication(item, KubernetesApplicationConverter.apiPodToApplication, services, pods, ingresses));
|
||||
const boundPods = _.concat(_.flatMap(deploymentApplications, 'Pods'), _.flatMap(daemonSetApplications, 'Pods'), _.flatMap(statefulSetApplications, 'Pods'));
|
||||
const unboundPods = _.without(pods, ...boundPods);
|
||||
const nakedPodsApplications = _.map(unboundPods, (item) => convertToApplication(item, KubernetesApplicationConverter.apiPodToApplication, services, pods, ingresses));
|
||||
|
||||
const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications, nakedPodsApplications);
|
||||
_.forEach(applications, (app) => {
|
||||
app.Pods = _.map(app.Pods, (item) => KubernetesPodConverter.apiToModel(item));
|
||||
});
|
||||
await Promise.all(
|
||||
_.forEach(applications, async (application) => {
|
||||
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application);
|
||||
const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined;
|
||||
application.AutoScaler = scaler;
|
||||
})
|
||||
);
|
||||
return applications;
|
||||
})
|
||||
);
|
||||
return _.flatten(res);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications, nakedPodsApplications);
|
||||
_.forEach(applications, (app) => {
|
||||
app.Pods = _.map(app.Pods, (item) => KubernetesPodConverter.apiToModel(item));
|
||||
});
|
||||
await Promise.all(
|
||||
_.forEach(applications, async (application) => {
|
||||
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application);
|
||||
const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined;
|
||||
application.AutoScaler = scaler;
|
||||
})
|
||||
);
|
||||
return applications;
|
||||
})
|
||||
);
|
||||
return _.flatten(res);
|
||||
}
|
||||
|
||||
get(namespace, name) {
|
||||
@@ -226,42 +214,38 @@ class KubernetesApplicationService {
|
||||
* also be displayed in the summary output (getCreatedApplicationResources)
|
||||
*/
|
||||
async createAsync(formValues) {
|
||||
try {
|
||||
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||
|
||||
if (service) {
|
||||
await this.KubernetesServiceService.create(service);
|
||||
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, service.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
if (service) {
|
||||
await this.KubernetesServiceService.create(service);
|
||||
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, service.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
|
||||
const apiService = this._getApplicationApiService(app);
|
||||
|
||||
if (app instanceof KubernetesStatefulSet) {
|
||||
app.VolumeClaims = claims;
|
||||
headlessService = await this.KubernetesServiceService.create(headlessService);
|
||||
app.ServiceName = headlessService.metadata.name;
|
||||
} else {
|
||||
const claimPromises = _.map(claims, (item) => {
|
||||
if (!item.PreviousName && !item.Id) {
|
||||
return this.KubernetesPersistentVolumeClaimService.create(item);
|
||||
}
|
||||
});
|
||||
await Promise.all(_.without(claimPromises, undefined));
|
||||
}
|
||||
|
||||
if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.GLOBAL) {
|
||||
const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app);
|
||||
const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind);
|
||||
await this.KubernetesHorizontalPodAutoScalerService.create(autoScaler);
|
||||
}
|
||||
|
||||
await apiService.create(app);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const apiService = this._getApplicationApiService(app);
|
||||
|
||||
if (app instanceof KubernetesStatefulSet) {
|
||||
app.VolumeClaims = claims;
|
||||
headlessService = await this.KubernetesServiceService.create(headlessService);
|
||||
app.ServiceName = headlessService.metadata.name;
|
||||
} else {
|
||||
const claimPromises = _.map(claims, (item) => {
|
||||
if (!item.PreviousName && !item.Id) {
|
||||
return this.KubernetesPersistentVolumeClaimService.create(item);
|
||||
}
|
||||
});
|
||||
await Promise.all(_.without(claimPromises, undefined));
|
||||
}
|
||||
|
||||
if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.GLOBAL) {
|
||||
const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app);
|
||||
const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind);
|
||||
await this.KubernetesHorizontalPodAutoScalerService.create(autoScaler);
|
||||
}
|
||||
|
||||
await apiService.create(app);
|
||||
}
|
||||
|
||||
create(formValues) {
|
||||
@@ -277,97 +261,89 @@ class KubernetesApplicationService {
|
||||
* in this method should also be displayed in the summary output (getUpdatedApplicationResources)
|
||||
*/
|
||||
async patchAsync(oldFormValues, newFormValues) {
|
||||
try {
|
||||
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||
const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
||||
const oldApiService = this._getApplicationApiService(oldApp);
|
||||
const newApiService = this._getApplicationApiService(newApp);
|
||||
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||
const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
||||
const oldApiService = this._getApplicationApiService(oldApp);
|
||||
const newApiService = this._getApplicationApiService(newApp);
|
||||
|
||||
if (oldApiService !== newApiService) {
|
||||
await this.delete(oldApp);
|
||||
if (oldService) {
|
||||
await this.KubernetesServiceService.delete(oldService);
|
||||
}
|
||||
return await this.create(newFormValues);
|
||||
}
|
||||
|
||||
if (newApp instanceof KubernetesStatefulSet) {
|
||||
await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService);
|
||||
} else {
|
||||
const claimPromises = _.map(newClaims, (newClaim) => {
|
||||
if (!newClaim.PreviousName && !newClaim.Id) {
|
||||
return this.KubernetesPersistentVolumeClaimService.create(newClaim);
|
||||
} else if (!newClaim.Id) {
|
||||
const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName });
|
||||
return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim);
|
||||
}
|
||||
});
|
||||
await Promise.all(claimPromises);
|
||||
}
|
||||
|
||||
await newApiService.patch(oldApp, newApp);
|
||||
|
||||
if (oldService && newService) {
|
||||
await this.KubernetesServiceService.patch(oldService, newService);
|
||||
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name);
|
||||
const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldIngresses, newIngresses));
|
||||
}
|
||||
} else if (!oldService && newService) {
|
||||
await this.KubernetesServiceService.create(newService);
|
||||
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(newFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
} else if (oldService && !newService) {
|
||||
if (oldApiService !== newApiService) {
|
||||
await this.delete(oldApp);
|
||||
if (oldService) {
|
||||
await this.KubernetesServiceService.delete(oldService);
|
||||
if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
}
|
||||
return await this.create(newFormValues);
|
||||
}
|
||||
|
||||
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
|
||||
const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind);
|
||||
if (!oldFormValues.AutoScaler.IsUsed) {
|
||||
if (newFormValues.AutoScaler.IsUsed) {
|
||||
await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler);
|
||||
}
|
||||
} else {
|
||||
const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp);
|
||||
const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind);
|
||||
if (newFormValues.AutoScaler.IsUsed) {
|
||||
await this.KubernetesHorizontalPodAutoScalerService.patch(oldAutoScaler, newAutoScaler);
|
||||
} else {
|
||||
await this.KubernetesHorizontalPodAutoScalerService.delete(oldAutoScaler);
|
||||
if (newApp instanceof KubernetesStatefulSet) {
|
||||
await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService);
|
||||
} else {
|
||||
const claimPromises = _.map(newClaims, (newClaim) => {
|
||||
if (!newClaim.PreviousName && !newClaim.Id) {
|
||||
return this.KubernetesPersistentVolumeClaimService.create(newClaim);
|
||||
} else if (!newClaim.Id) {
|
||||
const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName });
|
||||
return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim);
|
||||
}
|
||||
});
|
||||
await Promise.all(claimPromises);
|
||||
}
|
||||
|
||||
await newApiService.patch(oldApp, newApp);
|
||||
|
||||
if (oldService && newService) {
|
||||
await this.KubernetesServiceService.patch(oldService, newService);
|
||||
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name);
|
||||
const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldIngresses, newIngresses));
|
||||
}
|
||||
} else if (!oldService && newService) {
|
||||
await this.KubernetesServiceService.create(newService);
|
||||
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(newFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
} else if (oldService && !newService) {
|
||||
await this.KubernetesServiceService.delete(oldService);
|
||||
if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
}
|
||||
|
||||
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
|
||||
const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind);
|
||||
if (!oldFormValues.AutoScaler.IsUsed) {
|
||||
if (newFormValues.AutoScaler.IsUsed) {
|
||||
await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler);
|
||||
}
|
||||
} else {
|
||||
const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp);
|
||||
const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind);
|
||||
if (newFormValues.AutoScaler.IsUsed) {
|
||||
await this.KubernetesHorizontalPodAutoScalerService.patch(oldAutoScaler, newAutoScaler);
|
||||
} else {
|
||||
await this.KubernetesHorizontalPodAutoScalerService.delete(oldAutoScaler);
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// this function accepts KubernetesApplication as parameters
|
||||
async patchPartialAsync(oldApp, newApp) {
|
||||
try {
|
||||
const oldAppPayload = {
|
||||
Name: oldApp.Name,
|
||||
Namespace: oldApp.ResourcePool,
|
||||
StackName: oldApp.StackName,
|
||||
Note: oldApp.Note,
|
||||
};
|
||||
const newAppPayload = {
|
||||
Name: newApp.Name,
|
||||
Namespace: newApp.ResourcePool,
|
||||
StackName: newApp.StackName,
|
||||
Note: newApp.Note,
|
||||
};
|
||||
const apiService = this._getApplicationApiService(oldApp);
|
||||
await apiService.patch(oldAppPayload, newAppPayload);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const oldAppPayload = {
|
||||
Name: oldApp.Name,
|
||||
Namespace: oldApp.ResourcePool,
|
||||
StackName: oldApp.StackName,
|
||||
Note: oldApp.Note,
|
||||
};
|
||||
const newAppPayload = {
|
||||
Name: newApp.Name,
|
||||
Namespace: newApp.ResourcePool,
|
||||
StackName: newApp.StackName,
|
||||
Note: newApp.Note,
|
||||
};
|
||||
const apiService = this._getApplicationApiService(oldApp);
|
||||
await apiService.patch(oldAppPayload, newAppPayload);
|
||||
}
|
||||
|
||||
// accept either formValues or applications as parameters
|
||||
@@ -384,42 +360,38 @@ class KubernetesApplicationService {
|
||||
|
||||
/* #region DELETE */
|
||||
async deleteAsync(application) {
|
||||
try {
|
||||
const payload = {
|
||||
Namespace: application.ResourcePool || application.Namespace,
|
||||
Name: application.Name,
|
||||
};
|
||||
const servicePayload = angular.copy(payload);
|
||||
servicePayload.Name = application.Name;
|
||||
const payload = {
|
||||
Namespace: application.ResourcePool || application.Namespace,
|
||||
Name: application.Name,
|
||||
};
|
||||
const servicePayload = angular.copy(payload);
|
||||
servicePayload.Name = application.Name;
|
||||
|
||||
const apiService = this._getApplicationApiService(application);
|
||||
await apiService.delete(payload);
|
||||
const apiService = this._getApplicationApiService(application);
|
||||
await apiService.delete(payload);
|
||||
|
||||
if (apiService === this.KubernetesStatefulSetService) {
|
||||
const headlessServicePayload = angular.copy(payload);
|
||||
headlessServicePayload.Name = application instanceof KubernetesStatefulSet ? application.ServiceName : application.HeadlessServiceName;
|
||||
await this.KubernetesServiceService.delete(headlessServicePayload);
|
||||
}
|
||||
if (apiService === this.KubernetesStatefulSetService) {
|
||||
const headlessServicePayload = angular.copy(payload);
|
||||
headlessServicePayload.Name = application instanceof KubernetesStatefulSet ? application.ServiceName : application.HeadlessServiceName;
|
||||
await this.KubernetesServiceService.delete(headlessServicePayload);
|
||||
}
|
||||
|
||||
if (application.ServiceType) {
|
||||
await this.KubernetesServiceService.delete(servicePayload);
|
||||
const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length;
|
||||
if (isIngress) {
|
||||
const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace);
|
||||
const formValues = {
|
||||
OriginalIngresses: originalIngresses,
|
||||
PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts),
|
||||
};
|
||||
_.forEach(formValues.PublishedPorts, (p) => (p.NeedsDeletion = true));
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, servicePayload.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
if (application.ServiceType) {
|
||||
await this.KubernetesServiceService.delete(servicePayload);
|
||||
const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length;
|
||||
if (isIngress) {
|
||||
const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace);
|
||||
const formValues = {
|
||||
OriginalIngresses: originalIngresses,
|
||||
PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts),
|
||||
};
|
||||
_.forEach(formValues.PublishedPorts, (p) => (p.NeedsDeletion = true));
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, servicePayload.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
if (!_.isEmpty(application.AutoScaler)) {
|
||||
await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler);
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!_.isEmpty(application.AutoScaler)) {
|
||||
await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,13 +402,9 @@ class KubernetesApplicationService {
|
||||
|
||||
/* #region ROLLBACK */
|
||||
async rollbackAsync(application, targetRevision) {
|
||||
try {
|
||||
const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision);
|
||||
const apiService = this._getApplicationApiService(application);
|
||||
await apiService.rollback(application.ResourcePool, application.Name, payload);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision);
|
||||
const apiService = this._getApplicationApiService(application);
|
||||
await apiService.rollback(application.ResourcePool, application.Name, payload);
|
||||
}
|
||||
|
||||
rollback(application, targetRevision) {
|
||||
|
||||
@@ -26,35 +26,27 @@ class KubernetesConfigurationService {
|
||||
* GET
|
||||
*/
|
||||
async getAsync(namespace, name) {
|
||||
try {
|
||||
const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]);
|
||||
let configuration;
|
||||
if (secret.status === 'fulfilled') {
|
||||
configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value);
|
||||
return configuration;
|
||||
}
|
||||
configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value);
|
||||
const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]);
|
||||
let configuration;
|
||||
if (secret.status === 'fulfilled') {
|
||||
configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value);
|
||||
return configuration;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
async getAllAsync(namespace) {
|
||||
try {
|
||||
const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name');
|
||||
const res = await Promise.all(
|
||||
_.map(namespaces, async (ns) => {
|
||||
const [configMaps, secrets] = await Promise.all([this.KubernetesConfigMapService.get(ns), this.KubernetesSecretService.get(ns)]);
|
||||
const secretsConfigurations = _.map(secrets, (secret) => KubernetesConfigurationConverter.secretToConfiguration(secret));
|
||||
const configMapsConfigurations = _.map(configMaps, (configMap) => KubernetesConfigurationConverter.configMapToConfiguration(configMap));
|
||||
return _.concat(configMapsConfigurations, secretsConfigurations);
|
||||
})
|
||||
);
|
||||
return _.flatten(res);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name');
|
||||
const res = await Promise.all(
|
||||
_.map(namespaces, async (ns) => {
|
||||
const [configMaps, secrets] = await Promise.all([this.KubernetesConfigMapService.get(ns), this.KubernetesSecretService.get(ns)]);
|
||||
const secretsConfigurations = _.map(secrets, (secret) => KubernetesConfigurationConverter.secretToConfiguration(secret));
|
||||
const configMapsConfigurations = _.map(configMaps, (configMap) => KubernetesConfigurationConverter.configMapToConfiguration(configMap));
|
||||
return _.concat(configMapsConfigurations, secretsConfigurations);
|
||||
})
|
||||
);
|
||||
return _.flatten(res);
|
||||
}
|
||||
|
||||
get(namespace, name) {
|
||||
@@ -70,16 +62,12 @@ class KubernetesConfigurationService {
|
||||
async createAsync(formValues) {
|
||||
formValues.ConfigurationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ConfigurationOwner);
|
||||
|
||||
try {
|
||||
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
|
||||
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
|
||||
await this.KubernetesConfigMapService.create(configMap);
|
||||
} else {
|
||||
const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues);
|
||||
await this.KubernetesSecretService.create(secret);
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
|
||||
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
|
||||
await this.KubernetesConfigMapService.create(configMap);
|
||||
} else {
|
||||
const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues);
|
||||
await this.KubernetesSecretService.create(secret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,18 +79,14 @@ class KubernetesConfigurationService {
|
||||
* UPDATE
|
||||
*/
|
||||
async updateAsync(formValues, configuration) {
|
||||
try {
|
||||
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
|
||||
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
|
||||
configMap.ConfigurationOwner = configuration.ConfigurationOwner;
|
||||
await this.KubernetesConfigMapService.update(configMap);
|
||||
} else {
|
||||
const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues);
|
||||
secret.ConfigurationOwner = configuration.ConfigurationOwner;
|
||||
await this.KubernetesSecretService.update(secret);
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
|
||||
const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues);
|
||||
configMap.ConfigurationOwner = configuration.ConfigurationOwner;
|
||||
await this.KubernetesConfigMapService.update(configMap);
|
||||
} else {
|
||||
const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues);
|
||||
secret.ConfigurationOwner = configuration.ConfigurationOwner;
|
||||
await this.KubernetesSecretService.update(secret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,14 +98,10 @@ class KubernetesConfigurationService {
|
||||
* DELETE
|
||||
*/
|
||||
async deleteAsync(config) {
|
||||
try {
|
||||
if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) {
|
||||
await this.KubernetesConfigMapService.delete(config);
|
||||
} else {
|
||||
await this.KubernetesSecretService.delete(config);
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) {
|
||||
await this.KubernetesConfigMapService.delete(config);
|
||||
} else {
|
||||
await this.KubernetesSecretService.delete(config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ class KubernetesConfigService {
|
||||
|
||||
async downloadConfig() {
|
||||
const response = await this.KubernetesConfig.get();
|
||||
return this.FileSaver.saveAs(response.data, 'config');
|
||||
const headers = response.headers();
|
||||
const contentDispositionHeader = headers['content-disposition'];
|
||||
const filename = contentDispositionHeader.replace('attachment;', '').trim();
|
||||
return this.FileSaver.saveAs(response.data, filename);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -5,48 +5,48 @@ 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) {
|
||||
try {
|
||||
const namespace = await KubernetesNamespaceService.get(name);
|
||||
const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]);
|
||||
const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace);
|
||||
if (quotaAttempt.status === 'fulfilled') {
|
||||
pool.Quota = quotaAttempt.value;
|
||||
pool.Yaml += '---\n' + quotaAttempt.value.Yaml;
|
||||
}
|
||||
return pool;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
const namespace = await KubernetesNamespaceService.get(name);
|
||||
const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]);
|
||||
const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace);
|
||||
if (quotaAttempt.status === 'fulfilled') {
|
||||
pool.Quota = quotaAttempt.value;
|
||||
pool.Yaml += '---\n' + quotaAttempt.value.Yaml;
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
async function getAll() {
|
||||
try {
|
||||
const namespaces = await KubernetesNamespaceService.get();
|
||||
const pools = await Promise.all(
|
||||
_.map(namespaces, async (namespace) => {
|
||||
const name = namespace.Name;
|
||||
const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]);
|
||||
const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace);
|
||||
if (quotaAttempt.status === 'fulfilled') {
|
||||
pool.Quota = quotaAttempt.value;
|
||||
pool.Yaml += '---\n' + quotaAttempt.value.Yaml;
|
||||
}
|
||||
return pool;
|
||||
})
|
||||
);
|
||||
return pools;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const namespaces = await KubernetesNamespaceService.get();
|
||||
const pools = await Promise.all(
|
||||
_.map(namespaces, async (namespace) => {
|
||||
const name = namespace.Name;
|
||||
const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]);
|
||||
const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace);
|
||||
if (quotaAttempt.status === 'fulfilled') {
|
||||
pool.Quota = quotaAttempt.value;
|
||||
pool.Yaml += '---\n' + quotaAttempt.value.Yaml;
|
||||
}
|
||||
return pool;
|
||||
})
|
||||
);
|
||||
return pools;
|
||||
}
|
||||
|
||||
function get(name) {
|
||||
@@ -58,82 +58,73 @@ export function KubernetesResourcePoolService($async, EndpointService, Kubernete
|
||||
|
||||
function create(formValues) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const [namespace, quota, ingresses, registries] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues);
|
||||
await KubernetesNamespaceService.create(namespace);
|
||||
const [namespace, quota, ingresses, registries] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues);
|
||||
await KubernetesNamespaceService.create(namespace);
|
||||
|
||||
if (quota) {
|
||||
await KubernetesResourceQuotaService.create(quota);
|
||||
}
|
||||
const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i));
|
||||
await Promise.all(ingressPromises);
|
||||
|
||||
const endpointId = formValues.EndpointId;
|
||||
const registriesPromises = _.map(registries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]));
|
||||
await Promise.all(registriesPromises);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
if (quota) {
|
||||
await KubernetesResourceQuotaService.create(quota);
|
||||
}
|
||||
const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i));
|
||||
await Promise.all(ingressPromises);
|
||||
|
||||
const endpointId = formValues.EndpointId;
|
||||
const registriesPromises = _.map(registries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]));
|
||||
await Promise.all(registriesPromises);
|
||||
});
|
||||
}
|
||||
|
||||
function patch(oldFormValues, newFormValues) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
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);
|
||||
} else if (!oldQuota && newQuota) {
|
||||
await KubernetesResourceQuotaService.create(newQuota);
|
||||
} else if (oldQuota && !newQuota) {
|
||||
await KubernetesResourceQuotaService.delete(oldQuota);
|
||||
}
|
||||
|
||||
const create = _.filter(newIngresses, (ing) => !_.find(oldIngresses, { Name: ing.Name }));
|
||||
const del = _.filter(oldIngresses, (ing) => !_.find(newIngresses, { Name: ing.Name }));
|
||||
const patch = _.without(newIngresses, ...create);
|
||||
|
||||
const createPromises = _.map(create, (i) => KubernetesIngressService.create(i));
|
||||
const delPromises = _.map(del, (i) => KubernetesIngressService.delete(i.Namespace, i.Name));
|
||||
const patchPromises = _.map(patch, (ing) => {
|
||||
const old = _.find(oldIngresses, { Name: ing.Name });
|
||||
ing.Paths = angular.copy(old.Paths);
|
||||
ing.PreviousHost = old.Host;
|
||||
return KubernetesIngressService.patch(old, ing);
|
||||
});
|
||||
|
||||
const promises = _.flatten([createPromises, delPromises, patchPromises]);
|
||||
await Promise.all(promises);
|
||||
|
||||
const endpointId = newFormValues.EndpointId;
|
||||
const keptRegistries = _.intersectionBy(oldRegistries, newRegistries, 'Id');
|
||||
const removedRegistries = _.without(oldRegistries, ...keptRegistries);
|
||||
|
||||
const newRegistriesPromises = _.map(newRegistries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]));
|
||||
const removedRegistriesPromises = _.map(removedRegistries, (r) => {
|
||||
_.pull(r.RegistryAccesses[endpointId].Namespaces, newFormValues.Name);
|
||||
return EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]);
|
||||
});
|
||||
|
||||
await Promise.all(_.concat(newRegistriesPromises, removedRegistriesPromises));
|
||||
} catch (err) {
|
||||
throw err;
|
||||
if (oldQuota && newQuota) {
|
||||
await KubernetesResourceQuotaService.patch(oldQuota, newQuota);
|
||||
} else if (!oldQuota && newQuota) {
|
||||
await KubernetesResourceQuotaService.create(newQuota);
|
||||
} else if (oldQuota && !newQuota) {
|
||||
await KubernetesResourceQuotaService.delete(oldQuota);
|
||||
}
|
||||
|
||||
const create = _.filter(newIngresses, (ing) => !_.find(oldIngresses, { Name: ing.Name }));
|
||||
const del = _.filter(oldIngresses, (ing) => !_.find(newIngresses, { Name: ing.Name }));
|
||||
const patch = _.without(newIngresses, ...create);
|
||||
|
||||
const createPromises = _.map(create, (i) => KubernetesIngressService.create(i));
|
||||
const delPromises = _.map(del, (i) => KubernetesIngressService.delete(i.Namespace, i.Name));
|
||||
const patchPromises = _.map(patch, (ing) => {
|
||||
const old = _.find(oldIngresses, { Name: ing.Name });
|
||||
ing.Paths = angular.copy(old.Paths);
|
||||
ing.PreviousHost = old.Host;
|
||||
return KubernetesIngressService.patch(old, ing);
|
||||
});
|
||||
|
||||
const promises = _.flatten([createPromises, delPromises, patchPromises]);
|
||||
await Promise.all(promises);
|
||||
|
||||
const endpointId = newFormValues.EndpointId;
|
||||
const keptRegistries = _.intersectionBy(oldRegistries, newRegistries, 'Id');
|
||||
const removedRegistries = _.without(oldRegistries, ...keptRegistries);
|
||||
|
||||
const newRegistriesPromises = _.map(newRegistries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]));
|
||||
const removedRegistriesPromises = _.map(removedRegistries, (r) => {
|
||||
_.pull(r.RegistryAccesses[endpointId].Namespaces, newFormValues.Name);
|
||||
return EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]);
|
||||
});
|
||||
|
||||
await Promise.all(_.concat(newRegistriesPromises, removedRegistriesPromises));
|
||||
});
|
||||
}
|
||||
|
||||
function _delete(pool) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
await KubernetesNamespaceService.delete(pool.Namespace);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
await KubernetesNamespaceService.delete(pool.Namespace);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSystem(endpointId, namespaceName, system) {
|
||||
return KubernetesPortainerNamespaces.toggleSystem({ namespaceName, endpointId }, { system }).$promise;
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('portainer.kubernetes').service('KubernetesResourcePoolService', KubernetesResourcePoolService);
|
||||
|
||||
@@ -14,13 +14,9 @@ class KubernetesStackService {
|
||||
* GET
|
||||
*/
|
||||
async getAllAsync(namespace) {
|
||||
try {
|
||||
const applications = await this.KubernetesApplicationService.get(namespace);
|
||||
const stacks = _.map(applications, (item) => item.StackName);
|
||||
return _.uniq(_.without(stacks, '-'));
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const applications = await this.KubernetesApplicationService.get(namespace);
|
||||
const stacks = _.map(applications, (item) => item.StackName);
|
||||
return _.uniq(_.without(stacks, '-'));
|
||||
}
|
||||
|
||||
get(namespace) {
|
||||
|
||||
@@ -20,28 +20,20 @@ class KubernetesVolumeService {
|
||||
* GET
|
||||
*/
|
||||
async getAsync(namespace, name) {
|
||||
try {
|
||||
const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]);
|
||||
return KubernetesVolumeConverter.pvcToVolume(pvc, pool);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]);
|
||||
return KubernetesVolumeConverter.pvcToVolume(pvc, pool);
|
||||
}
|
||||
|
||||
async getAllAsync(namespace) {
|
||||
try {
|
||||
const data = await this.KubernetesResourcePoolService.get(namespace);
|
||||
const pools = data instanceof Array ? data : [data];
|
||||
const res = await Promise.all(
|
||||
_.map(pools, async (pool) => {
|
||||
const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name);
|
||||
return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool));
|
||||
})
|
||||
);
|
||||
return _.flatten(res);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
const data = await this.KubernetesResourcePoolService.get(namespace);
|
||||
const pools = data instanceof Array ? data : [data];
|
||||
const res = await Promise.all(
|
||||
_.map(pools, async (pool) => {
|
||||
const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name);
|
||||
return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool));
|
||||
})
|
||||
);
|
||||
return _.flatten(res);
|
||||
}
|
||||
|
||||
get(namespace, name) {
|
||||
@@ -55,11 +47,7 @@ class KubernetesVolumeService {
|
||||
* DELETE
|
||||
*/
|
||||
async deleteAsync(volume) {
|
||||
try {
|
||||
await this.KubernetesPersistentVolumeClaimService.delete(volume.PersistentVolumeClaim);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
await this.KubernetesPersistentVolumeClaimService.delete(volume.PersistentVolumeClaim);
|
||||
}
|
||||
|
||||
delete(volume) {
|
||||
|
||||
21
app/kubernetes/store/namespace.js
Normal file
21
app/kubernetes/store/namespace.js
Normal file
@@ -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));
|
||||
}
|
||||
@@ -5,7 +5,10 @@
|
||||
Advanced deployment allows you to deploy any Kubernetes manifest inside your cluster.
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy"> <i class="fa fa-file-code space-right" aria-hidden="true"></i>Advanced deployment </button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sApp-advancedDeployButton">
|
||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>
|
||||
Advanced deployment
|
||||
</button>
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-12" data-cy="k8sApp-appList">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
auto-focus
|
||||
required
|
||||
ng-disabled="ctrl.state.isEdit"
|
||||
data-cy="k8sAppCreate-applicationName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1556,6 +1557,7 @@
|
||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity"
|
||||
ng-click="ctrl.deployApplication()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
data-cy="k8sAppCreate-deployButton"
|
||||
>
|
||||
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Deploy application</span>
|
||||
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<td data-cy="k8sAppDetail-appName">
|
||||
{{ ctrl.application.Name }}
|
||||
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isExternalApplication()">external</span>
|
||||
</td>
|
||||
@@ -30,9 +30,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Namespace</td>
|
||||
<td>
|
||||
<td data-cy="k8sAppDetail-resourcePoolName">
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a>
|
||||
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>
|
||||
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -46,7 +46,8 @@
|
||||
<td ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD">
|
||||
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
|
||||
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
|
||||
<code>{{ ctrl.application.RunningPodsCount }}</code> / <code>{{ ctrl.application.TotalPodsCount }}</code>
|
||||
<code data-cy="k8sAppDetail-runningPods">{{ ctrl.application.RunningPodsCount }}</code> /
|
||||
<code data-cy="k8sAppDetail-totalPods">{{ ctrl.application.TotalPodsCount }}</code>
|
||||
</td>
|
||||
<td ng-if="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD">
|
||||
{{ ctrl.application.Pods[0].Status }}
|
||||
@@ -317,9 +318,9 @@
|
||||
<td style="width: 50%;">HTTP route</td>
|
||||
</tr>
|
||||
<tr ng-repeat-start="port in ctrl.application.PublishedPorts">
|
||||
<td ng-if="!ctrl.portHasIngressRules(port)">{{ port.TargetPort }}/{{ port.Protocol }}</td>
|
||||
<td ng-if="!ctrl.portHasIngressRules(port)" data-cy="k8sAppDetail-containerPort">{{ port.TargetPort }}/{{ port.Protocol }}</td>
|
||||
<td ng-if="!ctrl.portHasIngressRules(port)">
|
||||
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT">
|
||||
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-nodePort">
|
||||
{{ port.NodePort }}
|
||||
</span>
|
||||
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT">
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
placeholder="my-configuration"
|
||||
auto-focus
|
||||
required
|
||||
data-cy="k8sConfigCreate-nameInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,6 +57,7 @@
|
||||
id="resource-pool-selector"
|
||||
ng-model="ctrl.formValues.ResourcePool"
|
||||
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
|
||||
data-cy="k8sConfigCreate-namespaceDropdown"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +85,7 @@
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" />
|
||||
<label for="type_basic">
|
||||
<label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-file-code" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Non-sensitive
|
||||
@@ -93,7 +95,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" />
|
||||
<label for="type_secret">
|
||||
<label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-user-secret" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Sensitive
|
||||
@@ -141,6 +143,7 @@
|
||||
ng-disabled="!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress"
|
||||
ng-click="ctrl.createConfiguration()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
data-cy="k8sConfigCreate-CreateConfigButton"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Create configuration</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)" data-cy="k8sConfigDetail-configTab">
|
||||
<uib-tab-heading> <i class="fa fa-file-code space-right" aria-hidden="true"></i> Configuration </uib-tab-heading>
|
||||
<div style="padding: 20px;">
|
||||
<table class="table">
|
||||
<table class="table" data-cy="k8sConfigDetail-configTable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
@@ -41,7 +41,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</uib-tab>
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)" data-cy="k8sConfigDetail-eventsTab">
|
||||
<uib-tab-heading>
|
||||
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
|
||||
<div ng-if="ctrl.hasEventWarnings()">
|
||||
@@ -61,7 +61,7 @@
|
||||
>
|
||||
</kubernetes-events-datatable>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()">
|
||||
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
|
||||
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
|
||||
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
|
||||
<kubernetes-yaml-inspector key="configuration-yaml" data="ctrl.configuration.Yaml"> </kubernetes-yaml-inspector>
|
||||
@@ -104,6 +104,7 @@
|
||||
ng-disabled="!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress"
|
||||
ng-click="ctrl.updateConfiguration()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
data-cy="k8sConfigDetail-updateConfig"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Update configuration</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.pools">
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.pools" data-cy="k8sDashboard-namespaces">
|
||||
<a ui-sref="kubernetes.resourcePools">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
@@ -47,7 +47,7 @@
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.applications">
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.applications" data-cy="k8sDashboard-applications">
|
||||
<a ui-sref="kubernetes.applications">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
@@ -60,7 +60,7 @@
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.configurations">
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.configurations" data-cy="k8sDashboard-configurations">
|
||||
<a ui-sref="kubernetes.configurations">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
@@ -73,7 +73,7 @@
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.volumes">
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.volumes" data-cy="k8sDashboard-volumes">
|
||||
<a ui-sref="kubernetes.volumes">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment type
|
||||
</div>
|
||||
<box-selector radio-name="deploy" ng-model="ctrl.state.DeployType" options="ctrl.deployOptions"></box-selector>
|
||||
<box-selector radio-name="deploy" ng-model="ctrl.state.DeployType" options="ctrl.deployOptions" data-cy="k8sAppDeploy-deploymentSelector"></box-selector>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Build method
|
||||
</div>
|
||||
<box-selector radio-name="method" ng-model="ctrl.state.BuildMethod" options="ctrl.methodOptions"></box-selector>
|
||||
<box-selector radio-name="method" ng-model="ctrl.state.BuildMethod" options="ctrl.methodOptions" data-cy="k8sAppDeploy-buildSelector"></box-selector>
|
||||
|
||||
<!-- repository -->
|
||||
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT">
|
||||
@@ -48,7 +48,14 @@
|
||||
<div class="form-group">
|
||||
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Manifest path</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="ctrl.formValues.FilePathInRepository" id="stack_manifest_path" placeholder="deployment.yml" />
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="ctrl.formValues.FilePathInRepository"
|
||||
id="stack_manifest_path"
|
||||
placeholder="deployment.yml"
|
||||
data-cy="k8sAppDeploy-gitManifestPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<git-form-auth-fieldset model="ctrl.formValues" on-change="(ctrl.onChangeFormValues)"></git-form-auth-fieldset>
|
||||
@@ -100,7 +107,14 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="ctrl.disableDeploy()" ng-click="ctrl.deploy()" button-spinner="ctrl.state.actionInProgress">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="ctrl.disableDeploy()"
|
||||
ng-click="ctrl.deploy()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
data-cy="k8sAppDeploy-deployButton"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Deploy</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||
</button>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
ng-pattern="/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/"
|
||||
ng-change="$ctrl.onChangeName()"
|
||||
placeholder="my-project"
|
||||
data-cy="k8sNamespaceCreate-namespaceNameInput"
|
||||
required
|
||||
auto-focus
|
||||
/>
|
||||
@@ -58,7 +59,9 @@
|
||||
<label class="control-label text-left">
|
||||
Resource assignment
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.formValues.HasQuota" /><i></i> </label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="$ctrl.formValues.HasQuota" /><i data-cy="k8sNamespaceCreate-resourceAssignmentToggle"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.formValues.HasQuota && !$ctrl.isQuotaValid()">
|
||||
@@ -84,6 +87,7 @@
|
||||
ceil="$ctrl.state.sliderMaxMemory"
|
||||
step="128"
|
||||
ng-if="$ctrl.state.sliderMaxMemory"
|
||||
data-cy="k8sNamespaceCreate-memoryLimitSlider"
|
||||
>
|
||||
</slider>
|
||||
</div>
|
||||
@@ -96,6 +100,7 @@
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.MemoryLimit"
|
||||
id="memory-limit"
|
||||
data-cy="k8sNamespaceCreate-memoryLimitInput"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -128,6 +133,7 @@
|
||||
step="0.1"
|
||||
precision="2"
|
||||
ng-if="$ctrl.state.sliderMaxCpu"
|
||||
data-cy="k8sNamespaceCreate-cpuLimitSlider"
|
||||
>
|
||||
</slider>
|
||||
</div>
|
||||
@@ -159,7 +165,7 @@
|
||||
<label class="control-label text-left">
|
||||
Load Balancer quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<label class="switch" style="margin-left: 20px;" data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
|
||||
@@ -189,7 +195,7 @@
|
||||
<label class="control-label text-left">
|
||||
Enable quota
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<label class="switch" style="margin-left: 20px;" data-cy="k8sNamespaceCreate-enableQuotaToggle"> <input type="checkbox" disabled /><i></i> </label>
|
||||
<span class="text-muted small" style="margin-left: 15px;">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
This feature is available in
|
||||
@@ -409,7 +415,7 @@
|
||||
ng-click="$ctrl.createResourcePool()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Create namespace</span>
|
||||
<span ng-hide="$ctrl.state.actionInProgress" data-cy="k8sNamespace-createNamespaceButton">Create namespace</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Creation in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -15,9 +15,18 @@
|
||||
<form class="form-horizontal" autocomplete="off" name="resourcePoolEditForm" style="padding: 20px; margin-top: 10px;">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="pool_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" name="pool_name" ng-model="ctrl.pool.Namespace.Name" disabled />
|
||||
<div class="col-sm-12">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ ctrl.pool.Namespace.Name }}
|
||||
<span class="label label-info image-tag label-margins" ng-if="ctrl.isSystem">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
@@ -396,21 +405,26 @@
|
||||
<!-- !summary -->
|
||||
|
||||
<!-- actions -->
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
|
||||
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="form-group">
|
||||
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
ng-if="ctrl.isEditable"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!resourcePoolEditForm.$valid || ctrl.isUpdateButtonDisabled()"
|
||||
ng-click="ctrl.updateResourcePool()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Update namespace</span>
|
||||
<span ng-hide="ctrl.state.actionInProgress" data-cy="k8sNamespaceEdit-updateNamespaceButton">Update namespace</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="ctrl.markUnmarkAsSystem()" button-spinner="ctrl.state.actionInProgress">
|
||||
<span ng-if="ctrl.isSystem">Unmark as system</span>
|
||||
<span ng-if="!ctrl.isSystem">Mark as system</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
|
||||
@@ -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 : ''}<br/><br/>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();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user