Compare commits
5 Commits
default-na
...
feat/EE-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
719f7c09d3 | ||
|
|
98298d1e57 | ||
|
|
7899538245 | ||
|
|
553a1da235 | ||
|
|
9fae031390 |
@@ -45,7 +45,6 @@ func (store *Store) Init() error {
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
endpointutils "github.com/portainer/portainer/api/internal/endpoint"
|
||||
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
|
||||
)
|
||||
|
||||
@@ -24,10 +24,6 @@ func (m *Migrator) migrateDBVersionToDB32() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.kubeconfigExpiryToDB32(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -215,12 +211,3 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrator) kubeconfigExpiryToDB32() error {
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.KubeconfigExpiry = portainer.DefaultKubeconfigExpiry
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
|
||||
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
dataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
jwtService, err := jwt.NewService(settings.UserSessionTimeout, dataStore)
|
||||
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -95,10 +95,7 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port
|
||||
args = append(args, "--server", endpoint.URL)
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
args = append(args, "--token", token)
|
||||
fmt.Printf("Namespace: %s\n", namespace)
|
||||
if namespace != "" {
|
||||
args = append(args, "--namespace", namespace)
|
||||
}
|
||||
args = append(args, "--namespace", namespace)
|
||||
args = append(args, "apply", "-f", "-")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -105,10 +105,9 @@ type customTemplateFromFileContentPayload struct {
|
||||
Note string `example:"This is my <b>custom</b> template"`
|
||||
// Platform associated to the template.
|
||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||
// Required for Docker stacks
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
|
||||
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
|
||||
// Type of created stack (1 - swarm, 2 - compose)
|
||||
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
||||
// Content of stack file
|
||||
FileContent string `validate:"required"`
|
||||
}
|
||||
@@ -123,10 +122,10 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
return errors.New("Invalid file content")
|
||||
}
|
||||
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
return nil
|
||||
@@ -172,8 +171,7 @@ type customTemplateFromGitRepositoryPayload struct {
|
||||
Note string `example:"This is my <b>custom</b> template"`
|
||||
// Platform associated to the template.
|
||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||
// Required for Docker stacks
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
|
||||
// Type of created stack (1 - swarm, 2 - compose)
|
||||
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
||||
|
||||
@@ -207,11 +205,6 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
|
||||
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
if payload.Type == portainer.KubernetesStack {
|
||||
return errors.New("Creating a Kubernetes custom template from git is not supported")
|
||||
}
|
||||
|
||||
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
@@ -285,21 +278,20 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
|
||||
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
|
||||
payload.Note = note
|
||||
|
||||
platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true)
|
||||
templatePlatform := portainer.CustomTemplatePlatform(platform)
|
||||
if templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
payload.Platform = templatePlatform
|
||||
|
||||
typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true)
|
||||
templateType := portainer.StackType(typeNumeral)
|
||||
if templateType != portainer.KubernetesStack && templateType != portainer.DockerSwarmStack && templateType != portainer.DockerComposeStack {
|
||||
if templateType != portainer.DockerComposeStack && templateType != portainer.DockerSwarmStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
payload.Type = templateType
|
||||
|
||||
platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true)
|
||||
templatePlatform := portainer.CustomTemplatePlatform(platform)
|
||||
if templateType != portainer.KubernetesStack && templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
|
||||
payload.Platform = templatePlatform
|
||||
|
||||
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "File")
|
||||
if err != nil {
|
||||
return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
|
||||
|
||||
@@ -2,9 +2,7 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -19,16 +17,10 @@ import (
|
||||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param type query []int true "Template types" Enums(1,2,3)
|
||||
// @success 200 {array} portainer.CustomTemplate "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /custom_templates [get]
|
||||
func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
templateTypes, err := parseTemplateTypes(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template type", err}
|
||||
}
|
||||
|
||||
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
|
||||
@@ -60,52 +52,5 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
|
||||
customTemplates = authorization.FilterAuthorizedCustomTemplates(customTemplates, user, userTeamIDs)
|
||||
}
|
||||
|
||||
customTemplates = filterByType(customTemplates, templateTypes)
|
||||
|
||||
return response.JSON(w, customTemplates)
|
||||
}
|
||||
|
||||
func parseTemplateTypes(r *http.Request) ([]portainer.StackType, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to parse request params")
|
||||
}
|
||||
|
||||
types, exist := r.Form["type"]
|
||||
if !exist {
|
||||
return []portainer.StackType{}, nil
|
||||
}
|
||||
|
||||
res := []portainer.StackType{}
|
||||
for _, templateTypeStr := range types {
|
||||
templateType, err := strconv.Atoi(templateTypeStr)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed parsing template type")
|
||||
}
|
||||
|
||||
res = append(res, portainer.StackType(templateType))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func filterByType(customTemplates []portainer.CustomTemplate, templateTypes []portainer.StackType) []portainer.CustomTemplate {
|
||||
if len(templateTypes) == 0 {
|
||||
return customTemplates
|
||||
}
|
||||
|
||||
typeSet := map[portainer.StackType]bool{}
|
||||
for _, templateType := range templateTypes {
|
||||
typeSet[templateType] = true
|
||||
}
|
||||
|
||||
filtered := []portainer.CustomTemplate{}
|
||||
|
||||
for _, template := range customTemplates {
|
||||
if typeSet[template.Type] {
|
||||
filtered = append(filtered, template)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
@@ -27,10 +27,9 @@ type customTemplateUpdatePayload struct {
|
||||
Note string `example:"This is my <b>custom</b> template"`
|
||||
// Platform associated to the template.
|
||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||
// Required for Docker stacks
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
|
||||
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
|
||||
// Type of created stack (1 - swarm, 2 - compose)
|
||||
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
||||
// Content of stack file
|
||||
FileContent string `validate:"required"`
|
||||
}
|
||||
@@ -42,10 +41,10 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
return errors.New("Invalid file content")
|
||||
}
|
||||
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
if payload.Type != portainer.DockerComposeStack && payload.Type != portainer.DockerSwarmStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
if govalidator.IsNull(payload.Description) {
|
||||
|
||||
@@ -29,10 +29,6 @@ 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,7 +3,6 @@ package endpointproxy
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -66,12 +65,6 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
id := strconv.Itoa(endpointID)
|
||||
|
||||
prefix := "/" + id + "/agent/docker";
|
||||
if !strings.HasPrefix(r.URL.Path, prefix) {
|
||||
prefix = "/" + id + "/docker";
|
||||
}
|
||||
|
||||
http.StripPrefix(prefix, proxy).ServeHTTP(w, r)
|
||||
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,18 +65,17 @@ 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 {
|
||||
requestPrefix = fmt.Sprintf("/%d", endpointID)
|
||||
|
||||
agentPrefix := fmt.Sprintf("/%d/agent/kubernetes", endpointID)
|
||||
if strings.HasPrefix(r.URL.Path, agentPrefix) {
|
||||
requestPrefix = agentPrefix
|
||||
if isKubernetesRequest(strings.TrimPrefix(r.URL.String(), requestPrefix)) {
|
||||
requestPrefix = fmt.Sprintf("/%d", endpointID)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
endpointutils "github.com/portainer/portainer/api/internal/endpoint"
|
||||
)
|
||||
|
||||
// GET request on /endpoints/{id}/registries?namespace
|
||||
|
||||
@@ -48,8 +48,8 @@ type Handler struct {
|
||||
EndpointGroupHandler *endpointgroups.Handler
|
||||
EndpointHandler *endpoints.Handler
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
FileHandler *file.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
@@ -69,7 +69,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.6.3
|
||||
// @version 2.1.1
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -176,8 +176,6 @@ 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,71 +1,28 @@
|
||||
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
|
||||
authorizationService *authorization.Service
|
||||
JwtService portainer.JWTService
|
||||
DataStore portainer.DataStore
|
||||
KubernetesClientFactory *cli.ClientFactory
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to process pre-proxied requests to external APIs.
|
||||
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore portainer.DataStore, kubernetesClientFactory *cli.ClientFactory) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
dataStore: dataStore,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
authorizationService: authorizationService,
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
kubeRouter := h.PathPrefix("/kubernetes/{id}").Subrouter()
|
||||
|
||||
kubeRouter.Use(bouncer.AuthenticatedAccess)
|
||||
kubeRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
kubeRouter.Use(kubeOnlyMiddleware)
|
||||
|
||||
kubeRouter.PathPrefix("/config").Handler(
|
||||
h.PathPrefix("/kubernetes/{id}/config").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesConfig))).Methods(http.MethodGet)
|
||||
kubeRouter.PathPrefix("/nodes_limits").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesNodesLimits))).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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ package kubernetes
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
kcli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
@@ -31,29 +34,37 @@ 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 {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
bearerToken, err := extractBearerToken(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusUnauthorized, "Unauthorized", err}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", 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}
|
||||
}
|
||||
@@ -65,22 +76,34 @@ 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", fmt.Sprintf("attachment; %s.yaml", filenameBase))
|
||||
w.Header().Set("Content-Disposition", `attachment; filename=config.yaml`)
|
||||
return YAML(w, yaml)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.json", filenameBase))
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="config.json"`)
|
||||
return response.JSON(w, config)
|
||||
}
|
||||
|
||||
// extractBearerToken extracts user's portainer bearer token from request auth header
|
||||
func extractBearerToken(r *http.Request) (string, error) {
|
||||
token := ""
|
||||
tokens := r.Header["Authorization"]
|
||||
if len(tokens) >= 1 {
|
||||
token = tokens[0]
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
if token == "" {
|
||||
return "", httperrors.ErrUnauthorized
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// getProxyUrl generates portainer proxy url which acts as proxy to k8s api server
|
||||
func getProxyUrl(r *http.Request, endpointID int) string {
|
||||
return fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpointID)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// @id getKubernetesNodesLimits
|
||||
// @summary Get CPU and memory limits of all nodes within k8s cluster
|
||||
// @description Get CPU and memory limits of all nodes within k8s cluster
|
||||
// @description **Access policy**: authorized
|
||||
// @tags kubernetes
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Endpoint identifier"
|
||||
// @success 200 {object} K8sNodesLimits "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Endpoint not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /kubernetes/{id}/nodes_limits [get]
|
||||
func (handler *Handler) getKubernetesNodesLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
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))
|
||||
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 {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
|
||||
}
|
||||
|
||||
nodesLimits, err := cli.GetNodesLimits()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve nodes limits", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, nodesLimits)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -32,8 +32,6 @@ type settingsUpdatePayload struct {
|
||||
EnableEdgeComputeFeatures *bool `example:"true"`
|
||||
// The duration of a user session
|
||||
UserSessionTimeout *string `example:"5m"`
|
||||
// The expiry of a Kubeconfig
|
||||
KubeconfigExpiry *string `example:"24h" default:"0"`
|
||||
// Whether telemetry is enabled
|
||||
EnableTelemetry *bool `example:"false"`
|
||||
}
|
||||
@@ -54,12 +52,6 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
return errors.New("Invalid user session timeout")
|
||||
}
|
||||
}
|
||||
if payload.KubeconfigExpiry != nil {
|
||||
_, err := time.ParseDuration(*payload.KubeconfigExpiry)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Kubeconfig Expiry")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -143,10 +135,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
if payload.KubeconfigExpiry != nil {
|
||||
settings.KubeconfigExpiry = *payload.KubeconfigExpiry
|
||||
}
|
||||
|
||||
if payload.UserSessionTimeout != nil {
|
||||
settings.UserSessionTimeout = *payload.UserSessionTimeout
|
||||
|
||||
|
||||
@@ -404,15 +404,5 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
||||
}
|
||||
}
|
||||
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
handler.SwarmStackManager.Login(config.registries, config.endpoint)
|
||||
|
||||
err = handler.ComposeStackManager.Up(config.stack, config.endpoint)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start up the stack")
|
||||
}
|
||||
|
||||
return handler.SwarmStackManager.Logout(config.endpoint)
|
||||
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries)
|
||||
}
|
||||
|
||||
@@ -7,19 +7,19 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
)
|
||||
|
||||
const defaultReferenceName = "refs/heads/master"
|
||||
|
||||
type kubernetesStringDeploymentPayload struct {
|
||||
ComposeFormat bool
|
||||
Namespace string
|
||||
@@ -61,7 +61,7 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
||||
return errors.New("Invalid file path in repository")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultReferenceName
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -70,20 +70,28 @@ type createKubernetesStackResponse struct {
|
||||
Output string `json:"Output"`
|
||||
}
|
||||
|
||||
func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
var payload kubernetesStringDeploymentPayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Type: portainer.KubernetesStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ManifestFileDefaultName,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ID: portainer.StackID(stackID),
|
||||
Type: portainer.KubernetesStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ManifestFileDefaultName,
|
||||
Namespace: payload.Namespace,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
CreatedBy: user.Username,
|
||||
IsComposeFormat: payload.ComposeFormat,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -102,6 +110,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||
Owner: stack.CreatedBy,
|
||||
Kind: "content",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||
}
|
||||
@@ -111,29 +120,42 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
|
||||
resp := &createKubernetesStackResponse{
|
||||
Output: output,
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
|
||||
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
var payload kubernetesGitDeploymentPayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
||||
}
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Type: portainer.KubernetesStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.FilePathInRepository,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ID: portainer.StackID(stackID),
|
||||
Type: portainer.KubernetesStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.FilePathInRepository,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: payload.RepositoryURL,
|
||||
ReferenceName: payload.RepositoryReferenceName,
|
||||
ConfigFilePath: payload.FilePathInRepository,
|
||||
},
|
||||
Namespace: payload.Namespace,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
CreatedBy: user.Username,
|
||||
IsComposeFormat: payload.ComposeFormat,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -142,6 +164,12 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
commitId, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
|
||||
}
|
||||
stack.GitConfig.ConfigHash = commitId
|
||||
|
||||
stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
|
||||
@@ -153,6 +181,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
Owner: stack.CreatedBy,
|
||||
Kind: "git",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||
}
|
||||
@@ -162,16 +191,15 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
|
||||
resp := &createKubernetesStackResponse{
|
||||
Output: output,
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
|
||||
func (handler *Handler) deployKubernetesStack(r *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string, appLabels k.KubeAppLabels) (string, error) {
|
||||
func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string, appLabels k.KubeAppLabels) (string, error) {
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
@@ -189,11 +217,8 @@ func (handler *Handler) deployKubernetesStack(r *http.Request, endpoint *portain
|
||||
return "", errors.Wrap(err, "failed to add application labels")
|
||||
}
|
||||
|
||||
if !composeFormat && namespace == "default" {
|
||||
namespace = ""
|
||||
}
|
||||
return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace)
|
||||
|
||||
return handler.KubernetesDeployer.Deploy(r, endpoint, string(manifest), namespace)
|
||||
}
|
||||
|
||||
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -391,7 +392,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
||||
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
|
||||
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to validate user admin privileges")
|
||||
}
|
||||
|
||||
settings := &config.endpoint.SecuritySettings
|
||||
@@ -401,30 +402,15 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
||||
path := path.Join(config.stack.ProjectPath, file)
|
||||
stackContent, err := handler.FileService.GetFileContent(path)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.WithMessage(err, "failed to get stack file content")
|
||||
}
|
||||
|
||||
err = handler.isValidStackFile(stackContent, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.WithMessage(err, "swarm stack file content validation failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
handler.SwarmStackManager.Login(config.registries, config.endpoint)
|
||||
|
||||
err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.SwarmStackManager.Logout(config.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return handler.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||
case portainer.DockerComposeStack:
|
||||
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
|
||||
case portainer.KubernetesStack:
|
||||
return handler.createKubernetesStack(w, r, method, endpoint)
|
||||
return handler.createKubernetesStack(w, r, method, endpoint, tokenData.ID)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
|
||||
@@ -143,12 +143,12 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string, repository or file", Err: errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
switch method {
|
||||
case "string":
|
||||
return handler.createKubernetesStackFromFileContent(w, r, endpoint)
|
||||
return handler.createKubernetesStackFromFileContent(w, r, endpoint, userID)
|
||||
case "repository":
|
||||
return handler.createKubernetesStackFromGitRepository(w, r, endpoint)
|
||||
return handler.createKubernetesStackFromGitRepository(w, r, endpoint, userID)
|
||||
}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string or repository", Err: errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -72,9 +73,9 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||
|
||||
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||
@@ -82,7 +83,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
|
||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
|
||||
}
|
||||
if endpointID != int(stack.EndpointID) {
|
||||
stack.EndpointID = portainer.EndpointID(endpointID)
|
||||
@@ -90,32 +91,36 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
//only check resource control when it is a DockerSwarmStack or a DockerComposeStack
|
||||
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
|
||||
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
||||
@@ -123,9 +128,17 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return updateError
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot find context user", Err: errors.Wrap(err, "failed to fetch the user")}
|
||||
}
|
||||
stack.UpdatedBy = user.Username
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.Status = portainer.StackStatusActive
|
||||
|
||||
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
|
||||
}
|
||||
|
||||
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
|
||||
@@ -139,15 +152,20 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||
func (handler *Handler) updateAndDeployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
if stack.Type == portainer.DockerSwarmStack {
|
||||
return handler.updateSwarmStack(r, stack, endpoint)
|
||||
} else if stack.Type == portainer.DockerComposeStack {
|
||||
return handler.updateComposeStack(r, stack, endpoint)
|
||||
} else if stack.Type == portainer.KubernetesStack {
|
||||
return handler.updateKubernetesStack(r, stack, endpoint)
|
||||
} else {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unsupported stack", Err: errors.Errorf("unsupported stack type: %v", stack.Type)}
|
||||
}
|
||||
return handler.updateComposeStack(r, stack, endpoint)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
var payload updateComposeStackPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
stack.Env = payload.Env
|
||||
@@ -155,7 +173,7 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
|
||||
}
|
||||
|
||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
@@ -163,13 +181,9 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
|
||||
return configErr
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
stack.Status = portainer.StackStatusActive
|
||||
|
||||
err = handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -179,7 +193,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
||||
var payload updateSwarmStackPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
stack.Env = payload.Env
|
||||
@@ -187,7 +201,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
|
||||
}
|
||||
|
||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune)
|
||||
@@ -195,13 +209,9 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
||||
return configErr
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
stack.Status = portainer.StackStatusActive
|
||||
|
||||
err = handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -37,8 +37,8 @@ func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
|
||||
// @id StackUpdateGit
|
||||
// @summary Redeploy a stack
|
||||
// @description Pull and redeploy a stack via Git
|
||||
// @summary Update a stack's Git configs
|
||||
// @description Update the Git settings in a stack, e.g., RepositoryReferenceName and AutoUpdate
|
||||
// @description **Access policy**: restricted
|
||||
// @tags stacks
|
||||
// @security jwt
|
||||
@@ -46,7 +46,7 @@ func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||
// @produce json
|
||||
// @param id path int true "Stack identifier"
|
||||
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
|
||||
// @param body body updateStackGitPayload true "Git configs for pull and redeploy a stack"
|
||||
// @param body body stackGitUpdatePayload true "Git configs for pull and redeploy a stack"
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
|
||||
@@ -2,11 +2,14 @@ package stacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
@@ -16,6 +19,7 @@ import (
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
)
|
||||
|
||||
type stackGitRedployPayload struct {
|
||||
@@ -30,11 +34,26 @@ func (payload *stackGitRedployPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/stacks/:id/git?endpointId=<endpointId>
|
||||
// @id StackGitRedeploy
|
||||
// @summary Redeploy a stack
|
||||
// @description Pull and redeploy a stack via Git
|
||||
// @description **Access policy**: restricted
|
||||
// @tags stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Stack identifier"
|
||||
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
|
||||
// @param body body stackGitRedployPayload true "Git configs for pull and redeploy a stack"
|
||||
// @success 200 {object} portainer.Stack "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /stacks/:id/git/redeploy [put]
|
||||
func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
@@ -75,22 +94,26 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||
//only check resource control when it is a DockerSwarmStack or a DockerComposeStack
|
||||
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
|
||||
var payload stackGitRedployPayload
|
||||
@@ -140,9 +163,23 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
return httpErr
|
||||
}
|
||||
|
||||
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable get latest commit id", Err: errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID)}
|
||||
}
|
||||
stack.GitConfig.ConfigHash = newHash
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot find context user", Err: errors.Wrap(err, "failed to fetch the user")}
|
||||
}
|
||||
stack.UpdatedBy = user.Username
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.Status = portainer.StackStatusActive
|
||||
|
||||
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: errors.Wrap(err, "failed to update the stack")}
|
||||
}
|
||||
|
||||
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
|
||||
@@ -154,37 +191,48 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
if stack.Type == portainer.DockerSwarmStack {
|
||||
switch stack.Type {
|
||||
case portainer.DockerSwarmStack:
|
||||
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
err := handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
if err := handler.deploySwarmStack(config); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
stack.Status = portainer.StackStatusActive
|
||||
case portainer.DockerComposeStack:
|
||||
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return nil
|
||||
if err := handler.deployComposeStack(config); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||
}
|
||||
|
||||
case portainer.KubernetesStack:
|
||||
if stack.Namespace == "" {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Invalid namespace", Err: errors.New("Namespace must not be empty when redeploying kubernetes stacks")}
|
||||
}
|
||||
content, err := ioutil.ReadFile(filepath.Join(stack.ProjectPath, stack.GitConfig.ConfigFilePath))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to read deployment.yml manifest file", Err: errors.Wrap(err, "failed to read manifest file")}
|
||||
}
|
||||
_, err = handler.deployKubernetesStack(r, endpoint, string(content), false, stack.Namespace, k.KubeAppLabels{
|
||||
StackID: int(stack.ID),
|
||||
Name: stack.Name,
|
||||
Owner: stack.CreatedBy,
|
||||
Kind: "git",
|
||||
})
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to redeploy Kubernetes stack", Err: errors.WithMessage(err, "failed to deploy kube application")}
|
||||
}
|
||||
|
||||
default:
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unsupported stack", Err: errors.Errorf("unsupported stack type: %v", stack.Type)}
|
||||
}
|
||||
|
||||
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
err := handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
stack.Status = portainer.StackStatusActive
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
92
api/http/handler/stacks/update_kubernetes_stack.go
Normal file
92
api/http/handler/stacks/update_kubernetes_stack.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
)
|
||||
|
||||
type kubernetesFileStackUpdatePayload struct {
|
||||
StackFileContent string
|
||||
}
|
||||
|
||||
type kubernetesGitStackUpdatePayload struct {
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
}
|
||||
|
||||
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return errors.New("Invalid stack file content")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
|
||||
if stack.GitConfig != nil {
|
||||
var payload kubernetesGitStackUpdatePayload
|
||||
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||
if payload.RepositoryAuthentication {
|
||||
password := payload.RepositoryPassword
|
||||
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
|
||||
password = stack.GitConfig.Authentication.Password
|
||||
}
|
||||
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
} else {
|
||||
stack.GitConfig.Authentication = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload kubernetesFileStackUpdatePayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
_, err = handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, stack.IsComposeFormat, stack.Namespace, k.KubeAppLabels{
|
||||
StackID: int(stack.ID),
|
||||
Name: stack.Name,
|
||||
Owner: stack.CreatedBy,
|
||||
Kind: "content",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack via file content", Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -86,12 +86,17 @@ 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,
|
||||
"",
|
||||
true,
|
||||
serviceAccountToken,
|
||||
isAdminToken,
|
||||
endpoint,
|
||||
shellPod.Namespace,
|
||||
shellPod.PodName,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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"
|
||||
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||
kube "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,12 @@ func (server *Server) Start() error {
|
||||
endpointProxyHandler.ProxyManager = server.ProxyManager
|
||||
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.KubernetesClientFactory)
|
||||
kubernetesHandler.JwtService = server.JWTService
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
|
||||
var kubernetesHandler = kube.NewHandler(requestBouncer)
|
||||
kubernetesHandler.DataStore = server.DataStore
|
||||
kubernetesHandler.KubernetesClientFactory = server.KubernetesClientFactory
|
||||
|
||||
var motdHandler = motd.NewHandler(requestBouncer)
|
||||
|
||||
var registryHandler = registries.NewHandler(requestBouncer)
|
||||
@@ -243,8 +244,8 @@ func (server *Server) Start() error {
|
||||
EndpointHandler: endpointHandler,
|
||||
EndpointEdgeHandler: endpointEdgeHandler,
|
||||
EndpointProxyHandler: endpointProxyHandler,
|
||||
KubernetesHandler: kubernetesHandler,
|
||||
FileHandler: fileHandler,
|
||||
KubernetesHandler: kubernetesHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
|
||||
17
api/internal/endpoint/endpoint.go
Normal file
17
api/internal/endpoint/endpoint.go
Normal file
@@ -0,0 +1,17 @@
|
||||
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,7 +6,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -70,21 +70,6 @@ func NewDatastore(options ...datastoreOption) *datastore {
|
||||
return &d
|
||||
}
|
||||
|
||||
|
||||
type stubSettingsService struct {
|
||||
settings *portainer.Settings
|
||||
}
|
||||
|
||||
func (s *stubSettingsService) Settings() (*portainer.Settings, error) { return s.settings, nil }
|
||||
func (s *stubSettingsService) UpdateSettings(settings *portainer.Settings) error { return nil }
|
||||
|
||||
func WithSettings(settings *portainer.Settings) datastoreOption {
|
||||
return func(d *datastore) {
|
||||
d.settings = &stubSettingsService{settings: settings}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type stubUserService struct {
|
||||
users []portainer.User
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
type Service struct {
|
||||
secret []byte
|
||||
userSessionTimeout time.Duration
|
||||
dataStore portainer.DataStore
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
@@ -32,7 +31,7 @@ var (
|
||||
)
|
||||
|
||||
// NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens.
|
||||
func NewService(userSessionDuration string, dataStore portainer.DataStore) (*Service, error) {
|
||||
func NewService(userSessionDuration string) (*Service, error) {
|
||||
userSessionTimeout, err := time.ParseDuration(userSessionDuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -46,28 +45,19 @@ func NewService(userSessionDuration string, dataStore portainer.DataStore) (*Ser
|
||||
service := &Service{
|
||||
secret,
|
||||
userSessionTimeout,
|
||||
dataStore,
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (service *Service) defaultExpireAt() (int64) {
|
||||
return time.Now().Add(service.userSessionTimeout).Unix()
|
||||
}
|
||||
|
||||
// GenerateToken generates a new JWT token.
|
||||
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
|
||||
return service.generateSignedToken(data, service.defaultExpireAt())
|
||||
return service.generateSignedToken(data, nil)
|
||||
}
|
||||
|
||||
// GenerateTokenForOAuth generates a new JWT for OAuth login
|
||||
// token expiry time from the OAuth provider is considered
|
||||
func (service *Service) GenerateTokenForOAuth(data *portainer.TokenData, expiryTime *time.Time) (string, error) {
|
||||
expireAt := service.defaultExpireAt()
|
||||
if expiryTime != nil && !expiryTime.IsZero() {
|
||||
expireAt = expiryTime.Unix()
|
||||
}
|
||||
return service.generateSignedToken(data, expireAt)
|
||||
return service.generateSignedToken(data, expiryTime)
|
||||
}
|
||||
|
||||
// ParseAndVerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
|
||||
@@ -98,13 +88,17 @@ func (service *Service) SetUserSessionDuration(userSessionDuration time.Duration
|
||||
service.userSessionTimeout = userSessionDuration
|
||||
}
|
||||
|
||||
func (service *Service) generateSignedToken(data *portainer.TokenData, expiresAt int64) (string, error) {
|
||||
func (service *Service) generateSignedToken(data *portainer.TokenData, expiryTime *time.Time) (string, error) {
|
||||
expireToken := time.Now().Add(service.userSessionTimeout).Unix()
|
||||
if expiryTime != nil && !expiryTime.IsZero() {
|
||||
expireToken = expiryTime.Unix()
|
||||
}
|
||||
cl := claims{
|
||||
UserID: int(data.ID),
|
||||
Username: data.Username,
|
||||
Role: int(data.Role),
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expiresAt,
|
||||
ExpiresAt: expireToken,
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateTokenForKubeconfig generates a new JWT token for Kubeconfig
|
||||
func (service *Service) GenerateTokenForKubeconfig(data *portainer.TokenData) (string, error) {
|
||||
settings, err := service.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expiryDuration, err := time.ParseDuration(settings.KubeconfigExpiry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expiryAt := time.Now().Add(expiryDuration).Unix()
|
||||
if expiryDuration == time.Duration(0) {
|
||||
expiryAt = 0
|
||||
}
|
||||
|
||||
return service.generateSignedToken(data, expiryAt)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestService_GenerateTokenForKubeconfig(t *testing.T) {
|
||||
type fields struct {
|
||||
userSessionTimeout string
|
||||
dataStore portainer.DataStore
|
||||
}
|
||||
|
||||
type args struct {
|
||||
data *portainer.TokenData
|
||||
}
|
||||
|
||||
mySettings := &portainer.Settings{
|
||||
KubeconfigExpiry: "0",
|
||||
}
|
||||
|
||||
myFields := fields{
|
||||
userSessionTimeout: "24h",
|
||||
dataStore: i.NewDatastore(i.WithSettings(mySettings)),
|
||||
}
|
||||
|
||||
myTokenData := &portainer.TokenData{
|
||||
Username: "Joe",
|
||||
ID: 1,
|
||||
Role: 1,
|
||||
}
|
||||
|
||||
myArgs := args{
|
||||
data: myTokenData,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantExpiresAt int64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "kubeconfig no expiry",
|
||||
fields: myFields,
|
||||
args: myArgs,
|
||||
wantExpiresAt: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
service, err := NewService(tt.fields.userSessionTimeout, tt.fields.dataStore)
|
||||
assert.NoError(t, err, "failed to create a copy of service")
|
||||
|
||||
got, err := service.GenerateTokenForKubeconfig(tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GenerateTokenForKubeconfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
parsedToken, err := jwt.ParseWithClaims(got, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return service.secret, nil
|
||||
})
|
||||
assert.NoError(t, err, "failed to parse generated token")
|
||||
|
||||
tokenClaims, ok := parsedToken.Claims.(*claims)
|
||||
assert.Equal(t, true, ok, "failed to claims out of generated ticket")
|
||||
|
||||
assert.Equal(t, myTokenData.Username, tokenClaims.Username)
|
||||
assert.Equal(t, int(myTokenData.ID), tokenClaims.UserID)
|
||||
assert.Equal(t, int(myTokenData.Role), tokenClaims.Role)
|
||||
assert.Equal(t, tt.wantExpiresAt, tokenClaims.ExpiresAt)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGenerateSignedToken(t *testing.T) {
|
||||
svc, err := NewService("24h", nil)
|
||||
svc, err := NewService("24h")
|
||||
assert.NoError(t, err, "failed to create a copy of service")
|
||||
|
||||
token := &portainer.TokenData{
|
||||
@@ -18,9 +18,9 @@ func TestGenerateSignedToken(t *testing.T) {
|
||||
ID: 1,
|
||||
Role: 1,
|
||||
}
|
||||
expiresAt := time.Now().Add(1 * time.Hour).Unix()
|
||||
expirtationTime := time.Now().Add(1 * time.Hour)
|
||||
|
||||
generatedToken, err := svc.generateSignedToken(token, expiresAt)
|
||||
generatedToken, err := svc.generateSignedToken(token, &expirtationTime)
|
||||
assert.NoError(t, err, "failed to generate a signed token")
|
||||
|
||||
parsedToken, err := jwt.ParseWithClaims(generatedToken, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
@@ -34,5 +34,5 @@ func TestGenerateSignedToken(t *testing.T) {
|
||||
assert.Equal(t, token.Username, tokenClaims.Username)
|
||||
assert.Equal(t, int(token.ID), tokenClaims.UserID)
|
||||
assert.Equal(t, int(token.Role), tokenClaims.Role)
|
||||
assert.Equal(t, expiresAt, tokenClaims.ExpiresAt)
|
||||
assert.Equal(t, expirtationTime.Unix(), tokenClaims.ExpiresAt)
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
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
|
||||
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
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)
|
||||
|
||||
})
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GetNodesLimits gets the CPU and Memory limits(unused resources) of all nodes in the current k8s endpoint connection
|
||||
func (kcl *KubeClient) GetNodesLimits() (portainer.K8sNodesLimits, error) {
|
||||
nodesLimits := make(portainer.K8sNodesLimits)
|
||||
|
||||
nodes, err := kcl.cli.CoreV1().Nodes().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pods, err := kcl.cli.CoreV1().Pods("").List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range nodes.Items {
|
||||
cpu := item.Status.Allocatable.Cpu().MilliValue()
|
||||
memory := item.Status.Allocatable.Memory().Value()
|
||||
|
||||
nodesLimits[item.ObjectMeta.Name] = &portainer.K8sNodeLimits{
|
||||
CPU: cpu,
|
||||
Memory: memory,
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range pods.Items {
|
||||
if nodeLimits, ok := nodesLimits[item.Spec.NodeName]; ok {
|
||||
for _, container := range item.Spec.Containers {
|
||||
nodeLimits.CPU -= container.Resources.Requests.Cpu().MilliValue()
|
||||
nodeLimits.Memory -= container.Resources.Requests.Memory().Value()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodesLimits, nil
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kfake "k8s.io/client-go/kubernetes/fake"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newNodes() *v1.NodeList {
|
||||
return &v1.NodeList{
|
||||
Items: []v1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-node-0",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Allocatable: v1.ResourceList{
|
||||
v1.ResourceName(v1.ResourceCPU): resource.MustParse("2"),
|
||||
v1.ResourceName(v1.ResourceMemory): resource.MustParse("4M"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-node-1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Allocatable: v1.ResourceList{
|
||||
v1.ResourceName(v1.ResourceCPU): resource.MustParse("3"),
|
||||
v1.ResourceName(v1.ResourceMemory): resource.MustParse("6M"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newPods() *v1.PodList {
|
||||
return &v1.PodList{
|
||||
Items: []v1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-container-0",
|
||||
Namespace: "test-namespace-0",
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
NodeName: "test-node-0",
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "test-container-0",
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName(v1.ResourceCPU): resource.MustParse("1"),
|
||||
v1.ResourceName(v1.ResourceMemory): resource.MustParse("2M"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-container-1",
|
||||
Namespace: "test-namespace-1",
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
NodeName: "test-node-1",
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "test-container-1",
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceName(v1.ResourceCPU): resource.MustParse("2"),
|
||||
v1.ResourceName(v1.ResourceMemory): resource.MustParse("3M"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubeClient_GetNodesLimits(t *testing.T) {
|
||||
type fields struct {
|
||||
cli kubernetes.Interface
|
||||
}
|
||||
|
||||
fieldsInstance := fields{
|
||||
cli: kfake.NewSimpleClientset(newNodes(), newPods()),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want portainer.K8sNodesLimits
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "2 nodes 2 pods",
|
||||
fields: fieldsInstance,
|
||||
want: portainer.K8sNodesLimits{
|
||||
"test-node-0": &portainer.K8sNodeLimits{
|
||||
CPU: 1000,
|
||||
Memory: 2000000,
|
||||
},
|
||||
"test-node-1": &portainer.K8sNodeLimits{
|
||||
CPU: 1000,
|
||||
Memory: 3000000,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
kcl := &KubeClient{
|
||||
cli: tt.fields.cli,
|
||||
}
|
||||
got, err := kcl.GetNodesLimits()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetNodesLimits() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetNodesLimits() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,15 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
|
||||
Resources: []string{"storageclasses"},
|
||||
APIGroups: []string{"storage.k8s.io"},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"list"},
|
||||
Resources: []string{"ingresses"},
|
||||
APIGroups: []string{"networking.k8s.io"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) upsertPortainerK8sClusterRoles() error {
|
||||
func (kcl *KubeClient) createPortainerUserClusterRole() error {
|
||||
clusterRole := &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: portainerUserCRName,
|
||||
@@ -30,13 +35,8 @@ func (kcl *KubeClient) upsertPortainerK8sClusterRoles() error {
|
||||
}
|
||||
|
||||
_, err := kcl.cli.RbacV1().ClusterRoles().Create(clusterRole)
|
||||
if err != nil {
|
||||
if k8serrors.IsAlreadyExists(err) {
|
||||
_, err = kcl.cli.RbacV1().ClusterRoles().Update(clusterRole)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil && !k8serrors.IsAlreadyExists(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -63,7 +63,7 @@ func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int, restri
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) ensureRequiredResourcesExist() error {
|
||||
return kcl.upsertPortainerK8sClusterRoles()
|
||||
return kcl.createPortainerUserClusterRole()
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error {
|
||||
|
||||
@@ -398,13 +398,6 @@ type (
|
||||
// JobType represents a job type
|
||||
JobType int
|
||||
|
||||
K8sNodeLimits struct {
|
||||
CPU int64 `json:"CPU"`
|
||||
Memory int64 `json:"Memory"`
|
||||
}
|
||||
|
||||
K8sNodesLimits map[string]*K8sNodeLimits
|
||||
|
||||
K8sNamespaceAccessPolicy struct {
|
||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||
@@ -689,8 +682,6 @@ type (
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:""`
|
||||
// The duration of a user session
|
||||
UserSessionTimeout string `json:"UserSessionTimeout" example:"5m"`
|
||||
// The expiry of a Kubeconfig
|
||||
KubeconfigExpiry string `json:"KubeconfigExpiry" example:"24h"`
|
||||
// Whether telemetry is enabled
|
||||
EnableTelemetry bool `json:"EnableTelemetry" example:"false"`
|
||||
|
||||
@@ -759,6 +750,10 @@ type (
|
||||
AutoUpdate *StackAutoUpdate `json:"AutoUpdate"`
|
||||
// The git config of this stack
|
||||
GitConfig *gittypes.RepoConfig
|
||||
// Kubernetes namespace if stack is a kube application
|
||||
Namespace string `example:"default"`
|
||||
// IsComposeFormat indicates if the Kubernetes stack is created from a Docker Compose file
|
||||
IsComposeFormat bool `example:"false"`
|
||||
}
|
||||
|
||||
//StackAutoUpdate represents the git auto sync config for stack deployment
|
||||
@@ -1217,27 +1212,24 @@ type (
|
||||
JWTService interface {
|
||||
GenerateToken(data *TokenData) (string, error)
|
||||
GenerateTokenForOAuth(data *TokenData, expiryTime *time.Time) (string, error)
|
||||
GenerateTokenForKubeconfig(data *TokenData) (string, error)
|
||||
ParseAndVerifyToken(token string) (*TokenData, error)
|
||||
SetUserSessionDuration(userSessionDuration time.Duration)
|
||||
}
|
||||
|
||||
// KubeClient represents a service used to query a Kubernetes environment
|
||||
KubeClient interface {
|
||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
||||
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) 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
|
||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||
GetNodesLimits() (K8sNodesLimits, error)
|
||||
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
||||
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
|
||||
DeleteRegistrySecret(registry *Registry, namespace string) error
|
||||
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
|
||||
@@ -1421,7 +1413,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.6.3"
|
||||
APIVersion = "2.6.2"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 32
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
@@ -1455,8 +1447,6 @@ const (
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json"
|
||||
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
|
||||
DefaultUserSessionTimeout = "8h"
|
||||
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
|
||||
DefaultKubeconfigExpiry = "0"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -4,7 +4,7 @@ angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub);
|
||||
|
||||
function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) {
|
||||
return $resource(
|
||||
`${API_ENDPOINT_ENDPOINTS}/:endpointId/agent/:endpointType/v2/dockerhub/:registryId`,
|
||||
`${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub/:registryId`,
|
||||
{},
|
||||
{
|
||||
limits: { method: 'GET', params: { registryId: '@registryId' } },
|
||||
|
||||
@@ -31,6 +31,10 @@ 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,6 +376,98 @@ 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;
|
||||
}
|
||||
@@ -389,6 +481,86 @@ a[ng-click] {
|
||||
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,6 +14,9 @@
|
||||
padding-left: 70px;
|
||||
}
|
||||
}
|
||||
#page-wrapper.open #sidebar-wrapper {
|
||||
left: 150px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hamburg Menu
|
||||
@@ -251,6 +254,139 @@ 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
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.azure').component('azureSidebarContent', {
|
||||
templateUrl: './azureSidebarContent.html',
|
||||
bindings: {
|
||||
endpointId: '<',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
<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>
|
||||
@@ -1,19 +0,0 @@
|
||||
<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>
|
||||
@@ -1,8 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.azure').component('azureSidebar', {
|
||||
templateUrl: './azure-sidebar.html',
|
||||
bindings: {
|
||||
endpointId: '<',
|
||||
},
|
||||
});
|
||||
@@ -27,6 +27,8 @@ 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('PREDEFINED_NETWORKS', ['host', 'bridge', 'none'])
|
||||
.constant('KUBERNETES_DEFAULT_NAMESPACE', 'default')
|
||||
.constant('KUBERNETES_SYSTEM_NAMESPACES', ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']);
|
||||
|
||||
export const PORTAINER_FADEOUT = 1500;
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
<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,13 +1,12 @@
|
||||
angular.module('portainer.docker').component('dockerSidebar', {
|
||||
templateUrl: './docker-sidebar.html',
|
||||
angular.module('portainer.docker').component('dockerSidebarContent', {
|
||||
templateUrl: './dockerSidebarContent.html',
|
||||
bindings: {
|
||||
isSidebarOpen: '<',
|
||||
|
||||
endpointApiVersion: '<',
|
||||
swarmManagement: '<',
|
||||
standaloneManagement: '<',
|
||||
adminAccess: '<',
|
||||
offlineMode: '<',
|
||||
toggle: '<',
|
||||
currentRouteName: '<',
|
||||
endpointId: '<',
|
||||
showStacks: '<',
|
||||
@@ -0,0 +1,53 @@
|
||||
<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,7 +26,6 @@
|
||||
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].message;
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('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" data-cy="edgeGroupCreate-edgeGroupsSelector">
|
||||
<ui-select multiple ng-model="$ctrl.model" close-on-select="false">
|
||||
<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" data-cy="edgeStack-stackTableSettings">
|
||||
<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> Settings </span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
@@ -17,13 +17,7 @@
|
||||
<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()"
|
||||
data-cy="edgeStack-autoRefreshCheckbox"
|
||||
/>
|
||||
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
</div>
|
||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||
@@ -54,18 +48,10 @@
|
||||
</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)"
|
||||
data-cy="edgeStack-removeStackButton"
|
||||
>
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<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" data-cy="edgeStack-addStackButton">
|
||||
<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"> <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>
|
||||
@@ -80,12 +66,12 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells" data-cy="edgeStack-stackTable">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<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()" data-cy="edgeStack-selectAllCheckbox" />
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
@@ -123,10 +109,10 @@
|
||||
<td><edge-stack-status stack-status="item.Status"></edge-stack-status></td>
|
||||
<td>{{ item.CreationDate | getisodatefromtimestamp }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset" data-cy="edgeStack-loadingRow">
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0" data-cy="edgeStack-noStackRow">
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<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 data-cy="edgeGroupCreate-groupNameInput" />
|
||||
<input type="text" class="form-control" id="group_name" name="group_name" ng-model="$ctrl.model.Name" required auto-focus />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="EdgeGroupForm.group_name.$invalid">
|
||||
@@ -138,7 +138,6 @@
|
||||
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,18 +3,10 @@
|
||||
<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)"
|
||||
data-cy="edgeGroup-removeEdgeGroupButton"
|
||||
>
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<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" data-cy="edgeGroup-addEdgeGroupButton">
|
||||
<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"> <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>
|
||||
@@ -25,16 +17,15 @@
|
||||
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" data-cy="edgeGroup-edgeGroupTable">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="edgeGroup-selectAllCheckbox" />
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<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 data-cy="edgeStackCreate-nameInput" />
|
||||
<input type="text" class="form-control" ng-model="$ctrl.formValues.Name" id="stack_name" placeholder="e.g. mystack" auto-focus />
|
||||
</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" data-cy="edgeStackCreate-webEditorButton">
|
||||
<label for="method_editor">
|
||||
<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" data-cy="edgeStackCreate-uploadButton">
|
||||
<label for="method_upload">
|
||||
<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" data-cy="edgeStackCreate-repoButton">
|
||||
<label for="method_repository">
|
||||
<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" data-cy="edgeStackCreate-templateButton">
|
||||
<label for="method_template">
|
||||
<div class="boxselector_header">
|
||||
<i class="fas fa-rocket" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Template
|
||||
@@ -198,7 +198,6 @@
|
||||
|| !$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 (Object.prototype.hasOwnProperty.call(nodeData, key)) {
|
||||
if (nodeData.hasOwnProperty(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 (Object.prototype.hasOwnProperty.call(snapshotsData, key)) {
|
||||
if (snapshotsData.hasOwnProperty(key)) {
|
||||
snapshotsArray.push(snapshotsData[key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import registriesModule from './registries';
|
||||
import customTemplateModule from './custom-templates';
|
||||
|
||||
angular.module('portainer.kubernetes', ['portainer.app', registriesModule, customTemplateModule]).config([
|
||||
angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).config([
|
||||
'$stateRegistryProvider',
|
||||
function ($stateRegistryProvider) {
|
||||
'use strict';
|
||||
@@ -12,7 +11,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
parent: 'endpoint',
|
||||
abstract: true,
|
||||
|
||||
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, KubernetesHealthService, KubernetesNamespaceService, Notifications, StateManager) {
|
||||
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, KubernetesHealthService, Notifications, StateManager) {
|
||||
return $async(async () => {
|
||||
if (![5, 6, 7].includes(endpoint.Type)) {
|
||||
$state.go('portainer.home');
|
||||
@@ -35,8 +34,6 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
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 });
|
||||
@@ -209,15 +206,12 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
|
||||
const deploy = {
|
||||
name: 'kubernetes.deploy',
|
||||
url: '/deploy?templateId',
|
||||
url: '/deploy',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubernetesDeployView',
|
||||
},
|
||||
},
|
||||
params: {
|
||||
templateId: '',
|
||||
},
|
||||
};
|
||||
|
||||
const resourcePools = {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells" data-cy="k8sAppDetail-containerTable">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<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" data-cy="k8sApp-tableSettings">
|
||||
<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>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
@@ -22,26 +22,14 @@
|
||||
<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()"
|
||||
data-cy="k8sApp-autoRefreshCheckbox"
|
||||
/>
|
||||
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
|
||||
<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"
|
||||
data-cy="k8sApp-refreshRateDropdown"
|
||||
>
|
||||
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">1min</option>
|
||||
@@ -55,7 +43,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,16 +51,10 @@
|
||||
</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)"
|
||||
data-cy="k8sApp-removeAppButton"
|
||||
>
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<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" data-cy="k8sApp-addApplicationButton">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.new">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application
|
||||
</button>
|
||||
</div>
|
||||
@@ -86,16 +68,15 @@
|
||||
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" data-cy="k8sApp-appTable">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="k8sApp-selectAllCheckbox" />
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
@@ -162,7 +143,7 @@
|
||||
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
||||
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
|
||||
</td>
|
||||
<td>{{ item.StackName || '-' }}</td>
|
||||
<td>{{ item.StackName }}</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
|
||||
</td>
|
||||
|
||||
@@ -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, DatatableService, Authentication) {
|
||||
function ($scope, $controller, KubernetesNamespaceHelper, 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, DatatableService, Authentication) {
|
||||
function ($scope, $controller, KubernetesNamespaceHelper, 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.ResourcePool)">system</span>
|
||||
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">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.kubernetes').controller('KubernetesApplicationsStacksDatatableController', [
|
||||
angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatatableController', [
|
||||
'$scope',
|
||||
'$controller',
|
||||
'KubernetesNamespaceHelper',
|
||||
'DatatableService',
|
||||
'Authentication',
|
||||
function ($scope, $controller, DatatableService, Authentication) {
|
||||
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
this.state = Object.assign(this.state, {
|
||||
expandedItems: [],
|
||||
@@ -33,19 +33,15 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksD
|
||||
* Do not allow applications in system namespaces to be selected
|
||||
*/
|
||||
this.allowSelection = function (item) {
|
||||
return !this.isSystemNamespace(item.ResourcePool);
|
||||
return !this.isSystemNamespace(item);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} namespace Namespace (string name)
|
||||
* @returns Boolean
|
||||
*/
|
||||
this.isSystemNamespace = function (namespace) {
|
||||
return KubernetesNamespaceHelper.isSystemNamespace(namespace);
|
||||
this.isSystemNamespace = function (item) {
|
||||
return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool);
|
||||
};
|
||||
|
||||
this.isDisplayed = function (item) {
|
||||
return !ctrl.isSystemNamespace(item.ResourcePool) || ctrl.settings.showSystem;
|
||||
return !ctrl.isSystemNamespace(item) || 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 data-cy="k8sConfig-configSettingsButton"><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
|
||||
<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>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader">
|
||||
@@ -18,13 +18,7 @@
|
||||
<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()"
|
||||
data-cy="k8sConfig-systemResourceCheckbox"
|
||||
/>
|
||||
<input id="setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
||||
<label for="setting_show_system">Show system resources</label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
@@ -49,7 +43,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sConfig-closeSettingsButton">Close</a>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,16 +51,10 @@
|
||||
</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)"
|
||||
data-cy="k8sConfig-removeConfigButton"
|
||||
>
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<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" data-cy="k8sConfig-addConfigButton">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration
|
||||
</button>
|
||||
</div>
|
||||
@@ -80,11 +68,10 @@
|
||||
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" data-cy="k8sConfig-tableSettingsButtonconfigsTable">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
@@ -158,7 +145,7 @@
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="k8sConfig-paginationDropdown">
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<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, DatatableService, Authentication) {
|
||||
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
const ctrl = this;
|
||||
|
||||
@@ -52,11 +52,10 @@
|
||||
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" data-cy="k8sConfigDetail-eventsTable">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
@@ -118,12 +117,7 @@
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="$ctrl.state.paginatedItemLimit"
|
||||
ng-change="$ctrl.changePaginationLimit()"
|
||||
data-cy="k8sConfigDetail-eventsTablePaginationDropdown"
|
||||
>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<td
|
||||
><a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a></td
|
||||
>
|
||||
<td>{{ item.StackName || '-' }}</td>
|
||||
<td>{{ item.StackName }}</td>
|
||||
<td title="{{ item.Image }}">{{ item.Image | truncate: 64 }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<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-primary image-tag" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
|
||||
</td>
|
||||
<td>{{ item.StackName || '-' }}</td>
|
||||
<td>{{ item.StackName }}</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
|
||||
</td>
|
||||
|
||||
@@ -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, DatatableService) {
|
||||
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
this.isSystemNamespace = function (item) {
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
|
||||
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="$ctrl.isExternalApplication(item)">external</span>
|
||||
</td>
|
||||
<td>{{ item.StackName || '-' }}</td>
|
||||
<td>{{ item.StackName }}</td>
|
||||
<td title="{{ item.Image }}"
|
||||
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
|
||||
>
|
||||
|
||||
@@ -51,16 +51,10 @@
|
||||
</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)"
|
||||
data-cy="k8sNamespace-removeNamespaceButton"
|
||||
>
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<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" data-cy="k8sNamespace-addNamespaceButton">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace
|
||||
</button>
|
||||
</div>
|
||||
@@ -74,7 +68,6 @@
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="k8sNamespace-namespaceSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
|
||||
angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableController', [
|
||||
'$scope',
|
||||
'$controller',
|
||||
'Authentication',
|
||||
'KubernetesNamespaceHelper',
|
||||
'DatatableService',
|
||||
function ($scope, $controller, Authentication, DatatableService) {
|
||||
function ($scope, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
var ctrl = this;
|
||||
@@ -20,14 +19,14 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
|
||||
|
||||
this.canManageAccess = function (item) {
|
||||
if (!this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace) {
|
||||
return !KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name) && !this.isSystemNamespace(item);
|
||||
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item);
|
||||
} else {
|
||||
return !this.isSystemNamespace(item);
|
||||
}
|
||||
};
|
||||
|
||||
this.disableRemove = function (item) {
|
||||
return this.isSystemNamespace(item) || KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name);
|
||||
return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name) || item.Namespace.Name === 'default';
|
||||
};
|
||||
|
||||
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, DatatableService) {
|
||||
constructor($async, $controller, Authentication, KubernetesNamespaceHelper, 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 KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name);
|
||||
return this.KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name);
|
||||
}
|
||||
|
||||
isDisplayed(item) {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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:' };
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@@ -1,7 +0,0 @@
|
||||
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,12 +3,13 @@ import * as fit from 'xterm/lib/addons/fit/fit';
|
||||
|
||||
export default class KubectlShellController {
|
||||
/* @ngInject */
|
||||
constructor(TerminalWindow, $window, $async, EndpointProvider, LocalStorage, Notifications) {
|
||||
constructor(TerminalWindow, $window, $async, EndpointProvider, LocalStorage, KubernetesConfigService, Notifications) {
|
||||
this.$async = $async;
|
||||
this.$window = $window;
|
||||
this.TerminalWindow = TerminalWindow;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.KubernetesConfigService = KubernetesConfigService;
|
||||
this.Notifications = Notifications;
|
||||
}
|
||||
|
||||
@@ -19,7 +20,6 @@ export default class KubectlShellController {
|
||||
this.state.shell.term.dispose();
|
||||
this.state.shell.connected = false;
|
||||
this.TerminalWindow.terminalclose();
|
||||
this.$window.onresize = null;
|
||||
}
|
||||
|
||||
screenClear() {
|
||||
@@ -39,7 +39,6 @@ export default class KubectlShellController {
|
||||
}
|
||||
|
||||
configureSocketAndTerminal(socket, term) {
|
||||
var vm = this;
|
||||
socket.onopen = function () {
|
||||
const terminal_container = document.getElementById('terminal-container');
|
||||
term.open(terminal_container);
|
||||
@@ -56,7 +55,7 @@ export default class KubectlShellController {
|
||||
});
|
||||
|
||||
this.$window.onresize = function () {
|
||||
vm.TerminalWindow.terminalresize();
|
||||
term.fit();
|
||||
};
|
||||
|
||||
socket.onmessage = function (msg) {
|
||||
@@ -83,7 +82,7 @@ export default class KubectlShellController {
|
||||
endpointId: this.EndpointProvider.endpointID(),
|
||||
};
|
||||
|
||||
const wsProtocol = this.$window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const wsProtocol = this.state.isHTTPS ? 'wss://' : 'ws://';
|
||||
const path = '/api/websocket/kubernetes-shell';
|
||||
const queryParams = Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
@@ -97,12 +96,17 @@ 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: 495px;
|
||||
height: 480px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<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 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>
|
||||
|
||||
<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();" 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>
|
||||
<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>
|
||||
</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()" data-cy="k8sConfigCreate-createEntryButton">
|
||||
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0;" ng-click="$ctrl.addEntry()">
|
||||
<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;" data-cy="k8sConfigCreate-createConfigsFromFileButton">
|
||||
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0;">
|
||||
<i class="fa fa-file-upload" aria-hidden="true"></i> Create key/value from file
|
||||
</button>
|
||||
</div>
|
||||
@@ -98,14 +98,7 @@
|
||||
<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)"
|
||||
data-cy="k8sConfigDetail-removeEntryButton{{ index }}"
|
||||
>
|
||||
<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)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove entry
|
||||
</button>
|
||||
<span class="small text-muted" ng-if="entry.Used">
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<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,10 +1,8 @@
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesSidebar', {
|
||||
templateUrl: './kubernetes-sidebar.html',
|
||||
angular.module('portainer.kubernetes').component('kubernetesSidebarContent', {
|
||||
templateUrl: './kubernetesSidebarContent.html',
|
||||
bindings: {
|
||||
endpointId: '<',
|
||||
isSidebarOpen: '<',
|
||||
adminAccess: '<',
|
||||
endpointId: '<',
|
||||
currentState: '<',
|
||||
},
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
<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.templates.custom"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
icon-class="fa-rocket fa-fw"
|
||||
class-name="sidebar-list"
|
||||
data-cy="k8sSidebar-customTemplates"
|
||||
>
|
||||
Custom Templates
|
||||
</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,12 +14,7 @@
|
||||
Memory reservation
|
||||
</label>
|
||||
<div class="col-sm-9" style="margin-top: 4px;">
|
||||
<uib-progressbar
|
||||
animate="false"
|
||||
value="$ctrl.memoryReservationPercent"
|
||||
type="{{ $ctrl.memoryReservationPercent | kubernetesUsageLevelInfo }}"
|
||||
data-cy="k8sNamespaceDetail-memoryUsage"
|
||||
>
|
||||
<uib-progressbar animate="false" value="$ctrl.memoryReservationPercent" type="{{ $ctrl.memoryReservationPercent | kubernetesUsageLevelInfo }}">
|
||||
<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" data-cy="component-refreshTableButton"></i>
|
||||
<i class="fa fa-sm fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
KubernetesPortainerApplicationOwnerLabel,
|
||||
KubernetesPortainerApplicationStackNameLabel,
|
||||
KubernetesPortainerApplicationStackIdLabel,
|
||||
KubernetesPortainerApplicationKindLabel,
|
||||
} from 'Kubernetes/models/application/models';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
@@ -55,16 +56,12 @@ class KubernetesApplicationConverter {
|
||||
const containers = data.spec.template ? _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined) : data.spec.containers;
|
||||
res.Id = data.metadata.uid;
|
||||
res.Name = data.metadata.name;
|
||||
|
||||
if (data.metadata.labels) {
|
||||
const { labels } = data.metadata;
|
||||
res.StackId = labels[KubernetesPortainerApplicationStackIdLabel] ? parseInt(labels[KubernetesPortainerApplicationStackIdLabel], 10) : null;
|
||||
res.StackName = labels[KubernetesPortainerApplicationStackNameLabel] || '';
|
||||
res.ApplicationOwner = labels[KubernetesPortainerApplicationOwnerLabel] || '';
|
||||
res.ApplicationName = labels[KubernetesPortainerApplicationNameLabel] || res.Name;
|
||||
}
|
||||
|
||||
res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-';
|
||||
res.StackId = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackIdLabel] || '' : '';
|
||||
res.ApplicationKind = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationKindLabel] || '' : '';
|
||||
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] || '' : '';
|
||||
res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : '';
|
||||
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name;
|
||||
res.ResourcePool = data.metadata.namespace;
|
||||
if (containers.length) {
|
||||
res.Image = containers[0].image;
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
||||
import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads';
|
||||
import {
|
||||
KubernetesPortainerResourcePoolNameLabel,
|
||||
KubernetesPortainerResourcePoolOwnerLabel,
|
||||
KubernetesPortainerNamespaceSystemLabel,
|
||||
} from 'Kubernetes/models/resource-pool/models';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
|
||||
|
||||
export default class KubernetesNamespaceConverter {
|
||||
class KubernetesNamespaceConverter {
|
||||
static apiToNamespace(data, yaml) {
|
||||
const res = new KubernetesNamespace();
|
||||
res.Id = data.metadata.uid;
|
||||
@@ -18,14 +13,6 @@ export default 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;
|
||||
}
|
||||
|
||||
@@ -33,7 +20,6 @@ export default 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;
|
||||
@@ -41,3 +27,5 @@ export default class KubernetesNamespaceConverter {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesNamespaceConverter;
|
||||
|
||||
@@ -18,7 +18,6 @@ class KubernetesResourcePoolConverter {
|
||||
namespace.Name = formValues.Name;
|
||||
namespace.ResourcePoolName = formValues.Name;
|
||||
namespace.ResourcePoolOwner = formValues.Owner;
|
||||
namespace.IsSystem = formValues.IsSystem;
|
||||
|
||||
const quota = KubernetesResourceQuotaConverter.resourcePoolFormValuesToResourceQuota(formValues);
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { kubeCustomTemplatesView } from './kube-custom-templates-view';
|
||||
import { kubeEditCustomTemplateView } from './kube-edit-custom-template-view';
|
||||
import { kubeCreateCustomTemplateView } from './kube-create-custom-template-view';
|
||||
|
||||
export default angular
|
||||
.module('portainer.kubernetes.custom-templates', [])
|
||||
.config(config)
|
||||
.component('kubeCustomTemplatesView', kubeCustomTemplatesView)
|
||||
.component('kubeEditCustomTemplateView', kubeEditCustomTemplateView)
|
||||
.component('kubeCreateCustomTemplateView', kubeCreateCustomTemplateView).name;
|
||||
|
||||
function config($stateRegistryProvider) {
|
||||
const templates = {
|
||||
name: 'kubernetes.templates',
|
||||
url: '/templates',
|
||||
abstract: true,
|
||||
};
|
||||
|
||||
const customTemplates = {
|
||||
name: 'kubernetes.templates.custom',
|
||||
url: '/custom',
|
||||
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubeCustomTemplatesView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const customTemplatesNew = {
|
||||
name: 'kubernetes.templates.custom.new',
|
||||
url: '/new?fileContent',
|
||||
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubeCreateCustomTemplateView',
|
||||
},
|
||||
},
|
||||
params: {
|
||||
fileContent: '',
|
||||
},
|
||||
};
|
||||
|
||||
const customTemplatesEdit = {
|
||||
name: 'kubernetes.templates.custom.edit',
|
||||
url: '/:id',
|
||||
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubeEditCustomTemplateView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(templates);
|
||||
$stateRegistryProvider.register(customTemplates);
|
||||
$stateRegistryProvider.register(customTemplatesNew);
|
||||
$stateRegistryProvider.register(customTemplatesEdit);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import controller from './kube-create-custom-template-view.controller.js';
|
||||
|
||||
export const kubeCreateCustomTemplateView = {
|
||||
templateUrl: './kube-create-custom-template-view.html',
|
||||
controller,
|
||||
};
|
||||
@@ -1,169 +0,0 @@
|
||||
import { buildOption } from '@/portainer/components/box-selector';
|
||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
|
||||
class KubeCreateCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications, ResourceControlService) {
|
||||
Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications, ResourceControlService });
|
||||
|
||||
this.methodOptions = [
|
||||
buildOption('method_editor', 'fa fa-edit', 'Web editor', 'Use our Web editor', 'editor'),
|
||||
buildOption('method_upload', 'fa fa-upload', 'Upload', 'Upload from your computer', 'upload'),
|
||||
];
|
||||
|
||||
this.templates = null;
|
||||
|
||||
this.state = {
|
||||
method: 'editor',
|
||||
actionInProgress: false,
|
||||
formValidationError: '',
|
||||
isEditorDirty: false,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
FileContent: '',
|
||||
File: null,
|
||||
Title: '',
|
||||
Description: '',
|
||||
Note: '',
|
||||
Logo: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
};
|
||||
|
||||
this.onChangeFile = this.onChangeFile.bind(this);
|
||||
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
||||
this.onChangeMethod = this.onChangeMethod.bind(this);
|
||||
this.onBeforeOnload = this.onBeforeOnload.bind(this);
|
||||
}
|
||||
|
||||
onChangeMethod(method) {
|
||||
this.state.method = method;
|
||||
}
|
||||
|
||||
onChangeFileContent(content) {
|
||||
this.formValues.FileContent = content;
|
||||
this.state.isEditorDirty = true;
|
||||
}
|
||||
|
||||
onChangeFile(file) {
|
||||
this.formValues.File = file;
|
||||
}
|
||||
|
||||
async createCustomTemplate() {
|
||||
return this.$async(async () => {
|
||||
const { method } = this.state;
|
||||
|
||||
if (!this.validateForm(method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
const customTemplate = await this.createCustomTemplateByMethod(method, this.formValues);
|
||||
|
||||
const accessControlData = this.formValues.AccessControlData;
|
||||
const userDetails = this.Authentication.getUserDetails();
|
||||
const userId = userDetails.ID;
|
||||
await this.ResourceControlService.applyResourceControl(userId, accessControlData, customTemplate.ResourceControl);
|
||||
|
||||
this.Notifications.success('Custom template successfully created');
|
||||
this.state.isEditorDirty = false;
|
||||
this.$state.go('kubernetes.templates.custom');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed creating custom template');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createCustomTemplateByMethod(method, template) {
|
||||
template.Type = 3;
|
||||
|
||||
switch (method) {
|
||||
case 'editor':
|
||||
return this.createCustomTemplateFromFileContent(template);
|
||||
case 'upload':
|
||||
return this.createCustomTemplateFromFileUpload(template);
|
||||
}
|
||||
}
|
||||
|
||||
createCustomTemplateFromFileContent(template) {
|
||||
return this.CustomTemplateService.createCustomTemplateFromFileContent(template);
|
||||
}
|
||||
|
||||
createCustomTemplateFromFileUpload(template) {
|
||||
return this.CustomTemplateService.createCustomTemplateFromFileUpload(template);
|
||||
}
|
||||
|
||||
validateForm(method) {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
if (method === 'editor' && this.formValues.FileContent === '') {
|
||||
this.state.formValidationError = 'Template file content must not be empty';
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = this.formValues.Title;
|
||||
const isNotUnique = this.templates.some((template) => template.Title === title);
|
||||
if (isNotUnique) {
|
||||
this.state.formValidationError = 'A template with the same name already exists';
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAdmin = this.Authentication.isAdmin();
|
||||
const accessControlData = this.formValues.AccessControlData;
|
||||
const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
this.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
return this.$async(async () => {
|
||||
const { fileContent, type } = this.$state.params;
|
||||
|
||||
this.formValues.FileContent = fileContent;
|
||||
if (type) {
|
||||
this.formValues.Type = +type;
|
||||
}
|
||||
|
||||
try {
|
||||
this.templates = await this.CustomTemplateService.customTemplates(3);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
||||
}
|
||||
|
||||
this.state.loading = false;
|
||||
|
||||
window.addEventListener('beforeunload', this.onBeforeOnload);
|
||||
});
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
window.removeEventListener('beforeunload', this.onBeforeOnload);
|
||||
}
|
||||
|
||||
isEditorDirty() {
|
||||
return this.state.method === 'editor' && this.formValues.FileContent && this.state.isEditorDirty;
|
||||
}
|
||||
|
||||
onBeforeOnload(event) {
|
||||
if (this.isEditorDirty()) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
}
|
||||
|
||||
uiCanExit() {
|
||||
if (this.isEditorDirty()) {
|
||||
return this.ModalService.confirmWebEditorDiscard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubeCreateCustomTemplateViewController;
|
||||
@@ -1,71 +0,0 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Create Custom template"></rd-header-title>
|
||||
<rd-header-content> <a ui-sref="kubernetes.templates.custom">Custom Templates</a> > Create Custom template </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="$ctrl.form">
|
||||
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
|
||||
|
||||
<!-- build-method -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Build method
|
||||
</div>
|
||||
<box-selector radio-name="method" ng-model="$ctrl.state.method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
|
||||
|
||||
<web-editor-form
|
||||
ng-if="$ctrl.state.method === 'editor'"
|
||||
identifier="template-creation-editor"
|
||||
value="$ctrl.formValues.FileContent"
|
||||
on-change="($ctrl.onChangeFileContent)"
|
||||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="# Define or paste the content of your manifest file here"
|
||||
>
|
||||
<editor-description>
|
||||
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>
|
||||
<p>
|
||||
You can get more information about Kubernetes file format in the
|
||||
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
|
||||
</p>
|
||||
</editor-description>
|
||||
</web-editor-form>
|
||||
|
||||
<file-upload-form ng-if="$ctrl.state.method === 'upload'" file="$ctrl.formValues.File" on-change="($ctrl.onChangeFile)" ng-required="true">
|
||||
<file-upload-description>
|
||||
You can upload a Manifest file from your computer.
|
||||
</file-upload-description>
|
||||
</file-upload-form>
|
||||
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData"></por-access-control-form>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.state.actionInProgress || $ctrl.form.$invalid || ($ctrl.state.method === 'editor' && !$ctrl.formValues.FileContent)"
|
||||
ng-click="$ctrl.createCustomTemplate()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Create custom template</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Creation in progress...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">
|
||||
{{ $ctrl.state.formValidationError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
import controller from './kube-custom-templates-view.controller.js';
|
||||
|
||||
export const kubeCustomTemplatesView = {
|
||||
templateUrl: './kube-custom-templates-view.html',
|
||||
controller,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user