Compare commits
3 Commits
feat/EE-10
...
fix/releas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dd40d6d01 | ||
|
|
d4c3612334 | ||
|
|
eae70e6be0 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -14,10 +17,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
)
|
||||
@@ -81,7 +80,7 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po
|
||||
// Otherwise it will use kubectl to deploy the manifest.
|
||||
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
||||
if endpoint.Type == portainer.KubernetesLocalEnvironment {
|
||||
token, err := deployer.getToken(request, endpoint, true)
|
||||
token, err := deployer.getToken(request, endpoint, true);
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -180,7 +179,7 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := deployer.getToken(request, endpoint, false)
|
||||
token, err := deployer.getToken(request, endpoint, false);
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -230,7 +229,7 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port
|
||||
}
|
||||
|
||||
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
|
||||
func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error) {
|
||||
func (deployer *KubernetesDeployer) ConvertCompose(data string) ([]byte, error) {
|
||||
command := path.Join(deployer.binaryPath, "kompose")
|
||||
if runtime.GOOS == "windows" {
|
||||
command = path.Join(deployer.binaryPath, "kompose.exe")
|
||||
@@ -242,7 +241,7 @@ func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error)
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = bytes.NewReader(data)
|
||||
cmd.Stdin = strings.NewReader(data)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
||||
@@ -37,7 +37,7 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
|
||||
k8s.io/api v0.17.2
|
||||
k8s.io/apimachinery v0.17.2
|
||||
k8s.io/client-go v0.17.2
|
||||
|
||||
@@ -387,9 +387,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -20,7 +20,6 @@ type Handler struct {
|
||||
dataStore portainer.DataStore
|
||||
kubernetesClientFactory *cli.ClientFactory
|
||||
authorizationService *authorization.Service
|
||||
JwtService portainer.JWTService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to process pre-proxied requests to external APIs.
|
||||
@@ -40,8 +39,6 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
|
||||
|
||||
kubeRouter.PathPrefix("/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?)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -43,16 +46,16 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque
|
||||
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)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
|
||||
@@ -81,6 +84,20 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
@@ -8,15 +9,12 @@ import (
|
||||
"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"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
)
|
||||
|
||||
const defaultReferenceName = "refs/heads/master"
|
||||
@@ -38,12 +36,6 @@ type kubernetesGitDeploymentPayload struct {
|
||||
FilePathInRepository string
|
||||
}
|
||||
|
||||
type kubernetesManifestURLDeploymentPayload struct {
|
||||
Namespace string
|
||||
ComposeFormat bool
|
||||
ManifestURL string
|
||||
}
|
||||
|
||||
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return errors.New("Invalid stack file content")
|
||||
@@ -73,13 +65,6 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
|
||||
return errors.New("Invalid manifest URL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type createKubernetesStackResponse struct {
|
||||
Output string `json:"Output"`
|
||||
}
|
||||
@@ -110,12 +95,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
output, err := handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
|
||||
StackID: stackID,
|
||||
Name: stack.Name,
|
||||
Owner: stack.CreatedBy,
|
||||
Kind: "content",
|
||||
})
|
||||
output, err := handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||
}
|
||||
@@ -129,8 +109,6 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||
Output: output,
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
|
||||
@@ -161,12 +139,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
|
||||
}
|
||||
|
||||
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
|
||||
StackID: stackID,
|
||||
Name: stack.Name,
|
||||
Owner: stack.CreatedBy,
|
||||
Kind: "git",
|
||||
})
|
||||
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||
}
|
||||
@@ -179,86 +152,23 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
resp := &createKubernetesStackResponse{
|
||||
Output: output,
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
|
||||
|
||||
func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
var payload kubernetesManifestURLDeploymentPayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", 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(),
|
||||
}
|
||||
|
||||
var manifestContent []byte
|
||||
manifestContent, err := client.Get(payload.ManifestURL, 30)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve manifest from URL", err}
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, manifestContent)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
doCleanUp := true
|
||||
defer handler.cleanUp(stack, &doCleanUp)
|
||||
|
||||
output, err := handler.deployKubernetesStack(r, endpoint, string(manifestContent), payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
|
||||
StackID: stackID,
|
||||
Name: stack.Name,
|
||||
Owner: stack.CreatedBy,
|
||||
Kind: "url",
|
||||
})
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
|
||||
}
|
||||
|
||||
resp := &createKubernetesStackResponse{
|
||||
Output: output,
|
||||
}
|
||||
|
||||
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) (string, error) {
|
||||
handler.stackCreationMutex.Lock()
|
||||
defer handler.stackCreationMutex.Unlock()
|
||||
|
||||
manifest := []byte(stackConfig)
|
||||
if composeFormat {
|
||||
convertedConfig, err := handler.KubernetesDeployer.ConvertCompose(manifest)
|
||||
convertedConfig, err := handler.KubernetesDeployer.ConvertCompose(stackConfig)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
|
||||
return "", err
|
||||
}
|
||||
manifest = convertedConfig
|
||||
stackConfig = string(convertedConfig)
|
||||
}
|
||||
|
||||
manifest, err := k.AddAppLabels(manifest, appLabels)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to add application labels")
|
||||
}
|
||||
return handler.KubernetesDeployer.Deploy(request, endpoint, stackConfig, namespace)
|
||||
|
||||
return handler.KubernetesDeployer.Deploy(r, endpoint, string(manifest), namespace)
|
||||
}
|
||||
|
||||
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
|
||||
|
||||
@@ -149,8 +149,6 @@ func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Req
|
||||
return handler.createKubernetesStackFromFileContent(w, r, endpoint)
|
||||
case "repository":
|
||||
return handler.createKubernetesStackFromGitRepository(w, r, endpoint)
|
||||
case "url":
|
||||
return handler.createKubernetesStackFromManifestURL(w, r, endpoint)
|
||||
}
|
||||
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)}
|
||||
}
|
||||
|
||||
@@ -135,25 +135,21 @@ func (handler *Handler) hijackPodExecStartOperation(
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer stdoutWriter.Close()
|
||||
|
||||
// errorChan is used to propagate errors from the go routines to the caller.
|
||||
errorChan := make(chan error, 1)
|
||||
go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
|
||||
go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
|
||||
|
||||
// StartExecProcess is a blocking operation which streams IO to/from pod;
|
||||
// this must execute in asynchronously, since the websocketConn could return errors (e.g. client disconnects) before
|
||||
// the blocking operation is completed.
|
||||
go cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter, errorChan)
|
||||
|
||||
err = <-errorChan
|
||||
|
||||
// websocket client successfully disconnected
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
log.Printf("websocket error: %s \n", err.Error())
|
||||
return nil
|
||||
err = cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
|
||||
err = <-errorChan
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
log.Printf("websocket error: %s \n", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, bool, error) {
|
||||
|
||||
@@ -161,7 +161,6 @@ func (server *Server) Start() error {
|
||||
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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
@@ -15,12 +15,10 @@ import (
|
||||
// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write
|
||||
// to the stdout parameter.
|
||||
// This function only works against a local endpoint using an in-cluster config with the user's SA token.
|
||||
// This is a blocking operation.
|
||||
func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error) {
|
||||
func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if !useAdminToken {
|
||||
@@ -46,8 +44,7 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
err = exec.Stream(remotecommand.StreamOptions{
|
||||
@@ -57,7 +54,9 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
|
||||
})
|
||||
if err != nil {
|
||||
if _, ok := err.(utilexec.ExitError); !ok {
|
||||
errChan <- errors.New("unable to start exec process")
|
||||
return errors.New("unable to start exec process")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type KubeAppLabels struct {
|
||||
StackID int
|
||||
Name string
|
||||
Owner string
|
||||
Kind string
|
||||
}
|
||||
|
||||
// AddAppLabels adds required labels to "Resource"->metadata->labels.
|
||||
// It'll add those labels to all Resource (nodes with a kind property exluding a list) it can find in provided yaml.
|
||||
// Items in the yaml file could either be organised as a list or broken into multi documents.
|
||||
func AddAppLabels(manifestYaml []byte, appLabels KubeAppLabels) ([]byte, error) {
|
||||
if bytes.Equal(manifestYaml, []byte("")) {
|
||||
return manifestYaml, nil
|
||||
}
|
||||
|
||||
docs := make([][]byte, 0)
|
||||
yamlDecoder := yaml.NewDecoder(bytes.NewReader(manifestYaml))
|
||||
|
||||
for {
|
||||
m := make(map[string]interface{})
|
||||
err := yamlDecoder.Decode(&m)
|
||||
|
||||
// if decoded document is empty
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// if there are no more documents in the file
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
addResourceLabels(m, appLabels)
|
||||
|
||||
var out bytes.Buffer
|
||||
yamlEncoder := yaml.NewEncoder(&out)
|
||||
yamlEncoder.SetIndent(2)
|
||||
if err := yamlEncoder.Encode(m); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal yaml manifest")
|
||||
}
|
||||
|
||||
docs = append(docs, out.Bytes())
|
||||
}
|
||||
|
||||
return bytes.Join(docs, []byte("---\n")), nil
|
||||
}
|
||||
|
||||
func addResourceLabels(yamlDoc interface{}, appLabels KubeAppLabels) {
|
||||
m, ok := yamlDoc.(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
kind, ok := m["kind"]
|
||||
if ok && !strings.EqualFold(kind.(string), "list") {
|
||||
addLabels(m, appLabels)
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range m {
|
||||
switch v.(type) {
|
||||
case map[string]interface{}:
|
||||
addResourceLabels(v, appLabels)
|
||||
case []interface{}:
|
||||
for _, item := range v.([]interface{}) {
|
||||
addResourceLabels(item, appLabels)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addLabels(obj map[string]interface{}, appLabels KubeAppLabels) {
|
||||
metadata := make(map[string]interface{})
|
||||
if m, ok := obj["metadata"]; ok {
|
||||
metadata = m.(map[string]interface{})
|
||||
}
|
||||
|
||||
labels := make(map[string]string)
|
||||
if l, ok := metadata["labels"]; ok {
|
||||
for k, v := range l.(map[string]interface{}) {
|
||||
labels[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
name := appLabels.Name
|
||||
if appLabels.Name == "" {
|
||||
if n, ok := metadata["name"]; ok {
|
||||
name = n.(string)
|
||||
}
|
||||
}
|
||||
|
||||
labels["io.portainer.kubernetes.application.stackid"] = strconv.Itoa(appLabels.StackID)
|
||||
labels["io.portainer.kubernetes.application.name"] = name
|
||||
labels["io.portainer.kubernetes.application.owner"] = appLabels.Owner
|
||||
labels["io.portainer.kubernetes.application.kind"] = appLabels.Kind
|
||||
|
||||
metadata["labels"] = labels
|
||||
obj["metadata"] = metadata
|
||||
}
|
||||
@@ -1,493 +0,0 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_AddAppLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantOutput string
|
||||
}{
|
||||
{
|
||||
name: "single deployment without labels",
|
||||
input: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: busybox
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: busybox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: busybox
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: busybox
|
||||
`,
|
||||
wantOutput: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: busybox
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: busybox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: busybox
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: busybox
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "single deployment with existing labels",
|
||||
input: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
foo: bar
|
||||
name: busybox
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: busybox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: busybox
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: busybox
|
||||
`,
|
||||
wantOutput: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
foo: bar
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: busybox
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: busybox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: busybox
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: busybox
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "complex kompose output",
|
||||
input: `apiVersion: v1
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: web
|
||||
name: web
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
selector:
|
||||
io.kompose.service: web
|
||||
status:
|
||||
loadBalancer: {}
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: redis
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
io.kompose.service: redis
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: redis
|
||||
status: {}
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
io.kompose.service: web
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: web
|
||||
status: {}
|
||||
kind: List
|
||||
metadata: {}
|
||||
`,
|
||||
wantOutput: `apiVersion: v1
|
||||
items:
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: web
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: web
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
selector:
|
||||
io.kompose.service: web
|
||||
status:
|
||||
loadBalancer: {}
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: redis
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
io.kompose.service: redis
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: redis
|
||||
status: {}
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
io.kompose.service: web
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: web
|
||||
status: {}
|
||||
kind: List
|
||||
metadata: {}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "multiple items separated by ---",
|
||||
input: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: busybox
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: busybox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: busybox
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: busybox
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: web
|
||||
name: web
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
selector:
|
||||
io.kompose.service: web
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
foo: bar
|
||||
name: busybox
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: busybox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: busybox
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: busybox
|
||||
`,
|
||||
wantOutput: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: busybox
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: busybox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: busybox
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: busybox
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: web
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: web
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
selector:
|
||||
io.kompose.service: web
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
foo: bar
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: busybox
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: busybox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: busybox
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: busybox
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
wantOutput: "",
|
||||
},
|
||||
{
|
||||
name: "no only deployments",
|
||||
input: `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: web
|
||||
name: web
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
selector:
|
||||
io.kompose.service: web
|
||||
`,
|
||||
wantOutput: `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
io.kompose.service: web
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: best-name
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: web
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
selector:
|
||||
io.kompose.service: web
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
labels := KubeAppLabels{
|
||||
StackID: 123,
|
||||
Name: "best-name",
|
||||
Owner: "best-owner",
|
||||
Kind: "git",
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := AddAppLabels([]byte(tt.input), labels)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantOutput, string(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_AddAppLabels_PickingName_WhenLabelNameIsEmpty(t *testing.T) {
|
||||
labels := KubeAppLabels{
|
||||
StackID: 123,
|
||||
Owner: "best-owner",
|
||||
Kind: "git",
|
||||
}
|
||||
|
||||
input := `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: web
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
`
|
||||
|
||||
expected := `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: web
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
name: web
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
`
|
||||
|
||||
result, err := AddAppLabels([]byte(input), labels)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
func Test_AddAppLabels_PickingName_WhenLabelAndMetadataNameAreEmpty(t *testing.T) {
|
||||
labels := KubeAppLabels{
|
||||
StackID: 123,
|
||||
Owner: "best-owner",
|
||||
Kind: "git",
|
||||
}
|
||||
|
||||
input := `apiVersion: v1
|
||||
kind: Service
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
`
|
||||
|
||||
expected := `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
io.portainer.kubernetes.application.kind: git
|
||||
io.portainer.kubernetes.application.name: ""
|
||||
io.portainer.kubernetes.application.owner: best-owner
|
||||
io.portainer.kubernetes.application.stackid: "123"
|
||||
spec:
|
||||
ports:
|
||||
- name: "5000"
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
`
|
||||
|
||||
result, err := AddAppLabels([]byte(input), labels)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, string(result))
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -1217,7 +1208,6 @@ 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)
|
||||
}
|
||||
@@ -1228,9 +1218,8 @@ type (
|
||||
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
||||
GetServiceAccountBearerToken(userID int) (string, error)
|
||||
CreateUserShellPod(ctx context.Context, serviceAccountName string) (*KubernetesShellPod, error)
|
||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan 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
|
||||
@@ -1243,7 +1232,7 @@ type (
|
||||
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
|
||||
KubernetesDeployer interface {
|
||||
Deploy(request *http.Request, endpoint *Endpoint, data string, namespace string) (string, error)
|
||||
ConvertCompose(data []byte) ([]byte, error)
|
||||
ConvertCompose(data string) ([]byte, error)
|
||||
}
|
||||
|
||||
// KubernetesSnapshotter represents a service used to create Kubernetes endpoint snapshots
|
||||
@@ -1455,8 +1444,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 (
|
||||
|
||||
@@ -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';
|
||||
@@ -209,15 +208,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 = {
|
||||
|
||||
@@ -73,10 +73,7 @@
|
||||
<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">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application with form
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sApp-deployFromManifestButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
@@ -165,7 +162,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>
|
||||
|
||||
@@ -66,11 +66,8 @@
|
||||
>
|
||||
<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-addConfigWithFormButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration with form
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sConfig-deployFromManifestButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new" data-cy="k8sConfig-addConfigButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -60,11 +60,8 @@
|
||||
>
|
||||
<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-addNamespaceWithFormButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace with form
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sNamespace-deployFromManifestButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
@@ -95,13 +92,6 @@
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Namespace.Status')">
|
||||
Status
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Status' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Status' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Quota')">
|
||||
Quota
|
||||
@@ -134,9 +124,6 @@
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace.Name })">{{ item.Namespace.Name }}</a>
|
||||
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ $ctrl.namespaceStatusColor(item.Namespace.Status) }}">{{ item.Namespace.Status }}</span>
|
||||
</td>
|
||||
<td> <i class="fa {{ item.Quota ? 'fa-toggle-on' : 'fa-toggle-off' }}" aria-hidden="true" style="margin-right: 2px;"></i> {{ item.Quota ? 'Yes' : 'No' }} </td>
|
||||
<td>{{ item.Namespace.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
|
||||
<td ng-if="$ctrl.isAdmin">
|
||||
|
||||
@@ -38,17 +38,6 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
|
||||
return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin);
|
||||
};
|
||||
|
||||
this.namespaceStatusColor = function(status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'active':
|
||||
return 'success';
|
||||
case 'terminating':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'primary';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Do not allow system namespaces to be selected
|
||||
*/
|
||||
|
||||
@@ -54,9 +54,6 @@
|
||||
<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.deploy" data-cy="k8sVolume-deployFromManifestButton">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
|
||||
@@ -19,7 +19,6 @@ export default class KubectlShellController {
|
||||
this.state.shell.term.dispose();
|
||||
this.state.shell.connected = false;
|
||||
this.TerminalWindow.terminalclose();
|
||||
this.$window.onresize = null;
|
||||
}
|
||||
|
||||
screenClear() {
|
||||
|
||||
@@ -8,16 +8,6 @@
|
||||
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 }"
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
KubernetesPortainerApplicationNote,
|
||||
KubernetesPortainerApplicationOwnerLabel,
|
||||
KubernetesPortainerApplicationStackNameLabel,
|
||||
KubernetesPortainerApplicationStackIdLabel,
|
||||
} from 'Kubernetes/models/application/models';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
@@ -55,16 +54,10 @@ 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.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,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,
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
export default class KubeCustomTemplatesViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications) {
|
||||
Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, ModalService, Notifications });
|
||||
|
||||
this.state = {
|
||||
selectedTemplate: null,
|
||||
formValidationError: '',
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
this.currentUser = {
|
||||
isAdmin: false,
|
||||
id: null,
|
||||
};
|
||||
|
||||
this.isEditAllowed = this.isEditAllowed.bind(this);
|
||||
this.getTemplates = this.getTemplates.bind(this);
|
||||
this.validateForm = this.validateForm.bind(this);
|
||||
this.confirmDelete = this.confirmDelete.bind(this);
|
||||
this.selectTemplate = this.selectTemplate.bind(this);
|
||||
}
|
||||
|
||||
selectTemplate(template) {
|
||||
this.$state.go('kubernetes.deploy', { templateId: template.Id });
|
||||
}
|
||||
|
||||
isEditAllowed(template) {
|
||||
// todo - check if current user is admin/endpointadmin/owner
|
||||
return this.currentUser.isAdmin || this.currentUser.id === template.CreatedByUserId;
|
||||
}
|
||||
|
||||
getTemplates() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.templates = await this.CustomTemplateService.customTemplates(3);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failed loading templates', err, 'Unable to load custom templates');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validateForm(accessControlData, isAdmin) {
|
||||
this.state.formValidationError = '';
|
||||
const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
||||
if (error) {
|
||||
this.state.formValidationError = error;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
confirmDelete(templateId) {
|
||||
return this.$async(async () => {
|
||||
const confirmed = await this.ModalService.confirmDeletionAsync('Are you sure that you want to delete this template?');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.CustomTemplateService.remove(templateId);
|
||||
_.remove(this.templates, { Id: templateId });
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed to delete template');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.getTemplates();
|
||||
|
||||
this.currentUser.isAdmin = this.Authentication.isAdmin();
|
||||
const user = this.Authentication.getUserDetails();
|
||||
this.currentUser.id = user.ID;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<rd-header id="view-top">
|
||||
<rd-header-title title-text="Custom Templates">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="kubernetes.templates.custom" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Custom Templates</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<custom-templates-list
|
||||
ng-if="$ctrl.templates"
|
||||
title-text="Templates"
|
||||
title-icon="fa-rocket"
|
||||
templates="$ctrl.templates"
|
||||
table-key="customTemplates"
|
||||
is-edit-allowed="$ctrl.isEditAllowed"
|
||||
on-select-click="($ctrl.selectTemplate)"
|
||||
on-delete-click="($ctrl.confirmDelete)"
|
||||
create-path="kubernetes.templates.custom.new"
|
||||
edit-path="kubernetes.templates.custom.edit"
|
||||
></custom-templates-list>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
import controller from './kube-edit-custom-template-view.controller.js';
|
||||
|
||||
export const kubeEditCustomTemplateView = {
|
||||
templateUrl: './kube-edit-custom-template-view.html',
|
||||
controller,
|
||||
};
|
||||
@@ -1,143 +0,0 @@
|
||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { ResourceControlViewModel } from '@/portainer/models/resourceControl/resourceControl';
|
||||
|
||||
class KubeEditCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
|
||||
Object.assign(this, { $async, $state, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
|
||||
|
||||
this.formValues = null;
|
||||
this.state = {
|
||||
formValidationError: '',
|
||||
isEditorDirty: false,
|
||||
};
|
||||
this.templates = [];
|
||||
|
||||
this.getTemplate = this.getTemplate.bind(this);
|
||||
this.submitAction = this.submitAction.bind(this);
|
||||
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
||||
this.onBeforeUnload = this.onBeforeUnload.bind(this);
|
||||
}
|
||||
|
||||
getTemplate() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const { id } = this.$state.params;
|
||||
|
||||
const [template, file] = await Promise.all([this.CustomTemplateService.customTemplate(id), this.CustomTemplateService.customTemplateFile(id)]);
|
||||
template.FileContent = file;
|
||||
this.formValues = template;
|
||||
this.oldFileContent = this.formValues.FileContent;
|
||||
|
||||
this.formValues.ResourceControl = new ResourceControlViewModel(template.ResourceControl);
|
||||
this.formValues.AccessControlData = new AccessControlFormData();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve custom template data');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
if (!this.formValues.FileContent) {
|
||||
this.state.formValidationError = 'Template file content must not be empty';
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = this.formValues.Title;
|
||||
const id = this.$state.params.id;
|
||||
|
||||
const isNotUnique = this.templates.some((template) => template.Title === title && template.Id != id);
|
||||
if (isNotUnique) {
|
||||
this.state.formValidationError = `A template with the name ${title} 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;
|
||||
}
|
||||
|
||||
submitAction() {
|
||||
return this.$async(async () => {
|
||||
if (!this.validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionInProgress = true;
|
||||
try {
|
||||
await this.CustomTemplateService.updateCustomTemplate(this.formValues.Id, this.formValues);
|
||||
|
||||
const userDetails = this.Authentication.getUserDetails();
|
||||
const userId = userDetails.ID;
|
||||
await this.ResourceControlService.applyResourceControl(userId, this.formValues.AccessControlData, this.formValues.ResourceControl);
|
||||
|
||||
this.Notifications.success('Custom template successfully updated');
|
||||
this.state.isEditorDirty = false;
|
||||
this.$state.go('kubernetes.templates.custom');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update custom template');
|
||||
} finally {
|
||||
this.actionInProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeFileContent(value) {
|
||||
if (stripSpaces(this.formValues.FileContent) !== stripSpaces(value)) {
|
||||
this.formValues.FileContent = value;
|
||||
this.state.isEditorDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
this.$async(async () => {
|
||||
this.getTemplate();
|
||||
|
||||
try {
|
||||
this.templates = await this.CustomTemplateService.customTemplates();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.onBeforeUnload);
|
||||
});
|
||||
}
|
||||
|
||||
isEditorDirty() {
|
||||
return this.formValues.FileContent !== this.oldFileContent && this.state.isEditorDirty;
|
||||
}
|
||||
|
||||
uiCanExit() {
|
||||
if (this.isEditorDirty()) {
|
||||
return this.ModalService.confirmWebEditorDiscard();
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnload(event) {
|
||||
if (this.formValues.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
window.removeEventListener('beforeunload', this.onBeforeUnload);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubeEditCustomTemplateViewController;
|
||||
|
||||
function stripSpaces(str = '') {
|
||||
return str.replace(/(\r\n|\n|\r)/gm, '');
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<rd-header>
|
||||
<rd-header-title title-text="Edit Custom Template">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="kubernetes.templates.custom.edit({id:$ctrl.formValues.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content> <a ui-sref="kubernetes.templates.custom">Custom templates</a> > {{ $ctrl.formValues.Title }} </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="$ctrl.formValues">
|
||||
<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>
|
||||
|
||||
<web-editor-form
|
||||
identifier="template-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>
|
||||
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData" resource-control="$ctrl.formValues.ResourceControl"></por-access-control-form>
|
||||
|
||||
<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.actionInProgress || $ctrl.form.$invalid || !$ctrl.formValues.Title || !$ctrl.formValues.FileContent"
|
||||
ng-click="$ctrl.submitAction()"
|
||||
button-spinner="$ctrl.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.actionInProgress">Update the template</span>
|
||||
<span ng-show="$ctrl.actionInProgress">Update in progress...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">
|
||||
{{ $ctrl.state.formValidationError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,7 +6,7 @@ class KubernetesStackHelper {
|
||||
const res = _.reduce(
|
||||
applications,
|
||||
(acc, app) => {
|
||||
if (app.StackName) {
|
||||
if (app.StackName !== '-') {
|
||||
let stack = _.find(acc, { Name: app.StackName, ResourcePool: app.ResourcePool });
|
||||
if (!stack) {
|
||||
stack = new KubernetesStack();
|
||||
|
||||
@@ -40,7 +40,6 @@ export const KubernetesApplicationQuotaDefaults = {
|
||||
};
|
||||
|
||||
export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack';
|
||||
export const KubernetesPortainerApplicationStackIdLabel = 'io.portainer.kubernetes.application.stackid';
|
||||
|
||||
export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name';
|
||||
|
||||
|
||||
@@ -6,12 +6,9 @@ export const KubernetesDeployManifestTypes = Object.freeze({
|
||||
export const KubernetesDeployBuildMethods = Object.freeze({
|
||||
GIT: 1,
|
||||
WEB_EDITOR: 2,
|
||||
CUSTOM_TEMPLATE: 3,
|
||||
URL: 4
|
||||
});
|
||||
|
||||
export const KubernetesDeployRequestMethods = Object.freeze({
|
||||
REPOSITORY: 'repository',
|
||||
STRING: 'string',
|
||||
URL: 'url'
|
||||
});
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
/**
|
||||
* NodesLimits Model
|
||||
*/
|
||||
export class KubernetesNodesLimits {
|
||||
constructor(nodesLimits) {
|
||||
this.MaxCPU = 0;
|
||||
this.MaxMemory = 0;
|
||||
this.nodesLimits = this.convertCPU(nodesLimits);
|
||||
|
||||
this.calculateMaxCPUMemory();
|
||||
}
|
||||
|
||||
convertCPU(nodesLimits) {
|
||||
_.forEach(nodesLimits, (value) => {
|
||||
if (value.CPU) {
|
||||
value.CPU /= 1000.0;
|
||||
}
|
||||
});
|
||||
return nodesLimits;
|
||||
}
|
||||
|
||||
calculateMaxCPUMemory() {
|
||||
const nodesLimitsArray = Object.values(this.nodesLimits);
|
||||
this.MaxCPU = _.maxBy(nodesLimitsArray, 'CPU').CPU;
|
||||
this.MaxMemory = _.maxBy(nodesLimitsArray, 'Memory').Memory;
|
||||
}
|
||||
|
||||
// check if there is enough cpu and memory to allocate containers in replica mode
|
||||
overflowForReplica(cpu, memory, instances) {
|
||||
_.forEach(this.nodesLimits, (value) => {
|
||||
instances -= Math.min(Math.floor(value.CPU / cpu), Math.floor(value.Memory / memory));
|
||||
});
|
||||
|
||||
return instances > 0;
|
||||
}
|
||||
|
||||
// check if there is enough cpu and memory to allocate containers in global mode
|
||||
overflowForGlobal(cpu, memory) {
|
||||
let overflow = false;
|
||||
|
||||
_.forEach(this.nodesLimits, (value) => {
|
||||
if (cpu > value.CPU || memory > value.Memory) {
|
||||
overflow = true;
|
||||
}
|
||||
});
|
||||
|
||||
return overflow;
|
||||
}
|
||||
|
||||
excludesPods(pods, cpuLimit, memoryLimit) {
|
||||
const nodesLimits = this.nodesLimits;
|
||||
|
||||
_.forEach(pods, (value) => {
|
||||
const node = value.Node;
|
||||
if (node && nodesLimits[node]) {
|
||||
nodesLimits[node].CPU += cpuLimit;
|
||||
nodesLimits[node].Memory += memoryLimit;
|
||||
}
|
||||
});
|
||||
|
||||
this.calculateMaxCPUMemory();
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,9 @@ import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
|
||||
export default class KubernetesRegistryAccessController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, ModalService, EndpointService, Notifications, KubernetesResourcePoolService) {
|
||||
constructor($async, $state, EndpointService, Notifications, KubernetesResourcePoolService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.ModalService = ModalService;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.EndpointService = EndpointService;
|
||||
@@ -27,15 +26,8 @@ export default class KubernetesRegistryAccessController {
|
||||
|
||||
handleRemove(namespaces) {
|
||||
const removeNamespaces = namespaces.map(({ value }) => value);
|
||||
const nsToUpdate = this.savedResourcePools.map(({ value }) => value).filter((value) => !removeNamespaces.includes(value));
|
||||
|
||||
const displayedMessage =
|
||||
'This registry might be used by one or more applications inside this environment. Removing the registry access could lead to a service interruption for these applications.<br/><br/>Do you wish to continue?';
|
||||
this.ModalService.confirmDeletion(displayedMessage, (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.updateNamespaces(nsToUpdate);
|
||||
}
|
||||
});
|
||||
return this.updateNamespaces(this.savedResourcePools.map(({ value }) => value).filter((value) => !removeNamespaces.includes(value)));
|
||||
}
|
||||
|
||||
updateNamespaces(namespaces) {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.kubernetes').factory('KubernetesNodesLimits', KubernetesNodesLimitsFactory);
|
||||
|
||||
/* @ngInject */
|
||||
function KubernetesNodesLimitsFactory($resource, API_ENDPOINT_KUBERNETES, EndpointProvider) {
|
||||
const url = API_ENDPOINT_KUBERNETES + '/:endpointId/nodes_limits';
|
||||
return $resource(
|
||||
url,
|
||||
{
|
||||
endpointId: EndpointProvider.endpointID,
|
||||
},
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: (data) => ({ data: JSON.parse(data) }),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -41,11 +41,14 @@ class KubernetesNamespaceService {
|
||||
const data = await this.KubernetesNamespaces().get().$promise;
|
||||
const promises = _.map(data.items, (item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise);
|
||||
const namespaces = await $allSettled(promises);
|
||||
const allNamespaces = _.map(namespaces.fulfilled, (item) => {
|
||||
return KubernetesNamespaceConverter.apiToNamespace(item);
|
||||
const visibleNamespaces = _.map(namespaces.fulfilled, (item) => {
|
||||
if (item.status.phase !== 'Terminating') {
|
||||
return KubernetesNamespaceConverter.apiToNamespace(item);
|
||||
}
|
||||
});
|
||||
updateNamespaces(allNamespaces);
|
||||
return allNamespaces;
|
||||
const res = _.without(visibleNamespaces, undefined);
|
||||
updateNamespaces(res);
|
||||
return res;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve namespaces', err);
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import PortainerError from 'Portainer/error';
|
||||
import { KubernetesNodesLimits } from 'Kubernetes/models/nodes-limits/models';
|
||||
|
||||
class KubernetesNodesLimitsService {
|
||||
/* @ngInject */
|
||||
constructor(KubernetesNodesLimits) {
|
||||
this.KubernetesNodesLimits = KubernetesNodesLimits;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
async get() {
|
||||
try {
|
||||
const nodesLimits = await this.KubernetesNodesLimits.get().$promise;
|
||||
return new KubernetesNodesLimits(nodesLimits.data);
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve nodes limits', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesNodesLimitsService;
|
||||
angular.module('portainer.kubernetes').service('KubernetesNodesLimitsService', KubernetesNodesLimitsService);
|
||||
14
app/kubernetes/templates/advancedDeploymentPanel.html
Normal file
14
app/kubernetes/templates/advancedDeploymentPanel.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<information-panel title-text="Advanced deployment">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Advanced deployment allows you to deploy any Kubernetes manifest inside your cluster.
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sApp-advancedDeployButton">
|
||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>
|
||||
Advanced deployment
|
||||
</button>
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
@@ -5,6 +5,8 @@
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12" data-cy="k8sApp-appList">
|
||||
<rd-widget>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require('../../templates/advancedDeploymentPanel.html');
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
||||
|
||||
@@ -722,13 +722,6 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.nodeLimitsOverflow()">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
These reservations would exceed the resources currently available in the cluster.
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cpu-limit-input -->
|
||||
<!-- #endregion -->
|
||||
|
||||
@@ -1347,7 +1340,7 @@
|
||||
class="form-control"
|
||||
name="ingress_class_{{ $index }}"
|
||||
ng-model="publishedPort.IngressName"
|
||||
ng-options="ingress.Name as ingress.Name for ingress in ctrl.ingresses"
|
||||
ng-options="ingress.Name as ingress.Name for ingress in ctrl.filteredIngresses"
|
||||
ng-required="!publishedPort.NeedsDeletion"
|
||||
ng-change="ctrl.onChangePortMappingIngress($index)"
|
||||
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||
|
||||
@@ -49,8 +49,7 @@ class KubernetesCreateApplicationController {
|
||||
KubernetesIngressService,
|
||||
KubernetesPersistentVolumeClaimService,
|
||||
KubernetesVolumeService,
|
||||
RegistryService,
|
||||
KubernetesNodesLimitsService
|
||||
RegistryService
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
@@ -66,7 +65,6 @@ class KubernetesCreateApplicationController {
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
||||
this.RegistryService = RegistryService;
|
||||
this.KubernetesNodesLimitsService = KubernetesNodesLimitsService;
|
||||
|
||||
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||
@@ -94,10 +92,6 @@ class KubernetesCreateApplicationController {
|
||||
memory: 0,
|
||||
cpu: 0,
|
||||
},
|
||||
namespaceLimits: {
|
||||
memory: 0,
|
||||
cpu: 0,
|
||||
},
|
||||
resourcePoolHasQuota: false,
|
||||
viewReady: false,
|
||||
availableSizeUnits: ['MB', 'GB', 'TB'],
|
||||
@@ -371,7 +365,7 @@ class KubernetesCreateApplicationController {
|
||||
/* #region PUBLISHED PORTS UI MANAGEMENT */
|
||||
addPublishedPort() {
|
||||
const p = new KubernetesApplicationPublishedPortFormValue();
|
||||
const ingresses = this.ingresses;
|
||||
const ingresses = this.filteredIngresses;
|
||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined;
|
||||
p.IngressHosts = ingresses && ingresses.length ? ingresses[0].Hosts : undefined;
|
||||
@@ -382,7 +376,7 @@ class KubernetesCreateApplicationController {
|
||||
}
|
||||
|
||||
resetPublishedPorts() {
|
||||
const ingresses = this.ingresses;
|
||||
const ingresses = this.filteredIngresses;
|
||||
_.forEach(this.formValues.PublishedPorts, (p) => {
|
||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined;
|
||||
@@ -441,7 +435,7 @@ class KubernetesCreateApplicationController {
|
||||
|
||||
onChangePortMappingIngress(index) {
|
||||
const publishedPort = this.formValues.PublishedPorts[index];
|
||||
const ingress = _.find(this.ingresses, { Name: publishedPort.IngressName });
|
||||
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
||||
publishedPort.IngressHosts = ingress.Hosts;
|
||||
this.ingressHostnames = ingress.Hosts;
|
||||
publishedPort.IngressHost = this.ingressHostnames.length ? this.ingressHostnames[0] : [];
|
||||
@@ -589,28 +583,14 @@ class KubernetesCreateApplicationController {
|
||||
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
|
||||
}
|
||||
|
||||
nodeLimitsOverflow() {
|
||||
const cpu = this.formValues.CpuLimit;
|
||||
const memory = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit);
|
||||
|
||||
const overflow = this.nodesLimits.overflowForReplica(cpu, memory, 1);
|
||||
|
||||
return overflow;
|
||||
}
|
||||
|
||||
effectiveInstances() {
|
||||
return this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL ? this.nodeNumber : this.formValues.ReplicaCount;
|
||||
}
|
||||
|
||||
resourceReservationsOverflow() {
|
||||
const instances = this.effectiveInstances();
|
||||
const instances = this.formValues.ReplicaCount;
|
||||
const cpu = this.formValues.CpuLimit;
|
||||
const maxCpu = this.state.namespaceLimits.cpu;
|
||||
const memory = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit);
|
||||
const maxMemory = this.state.namespaceLimits.memory;
|
||||
const maxCpu = this.state.sliders.cpu.max;
|
||||
const memory = this.formValues.MemoryLimit;
|
||||
const maxMemory = this.state.sliders.memory.max;
|
||||
|
||||
// multiply 1000 can avoid 0.1 * 3 > 0.3
|
||||
if (cpu * 1000 * instances > maxCpu * 1000) {
|
||||
if (cpu * instances > maxCpu) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -618,23 +598,17 @@ class KubernetesCreateApplicationController {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.REPLICATED) {
|
||||
return this.nodesLimits.overflowForReplica(cpu, memory, instances);
|
||||
}
|
||||
|
||||
// DeploymentType == GLOBAL
|
||||
return this.nodesLimits.overflowForGlobal(cpu, memory);
|
||||
return false;
|
||||
}
|
||||
|
||||
autoScalerOverflow() {
|
||||
const instances = this.formValues.AutoScaler.MaxReplicas;
|
||||
const cpu = this.formValues.CpuLimit;
|
||||
const maxCpu = this.state.namespaceLimits.cpu;
|
||||
const memory = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit);
|
||||
const maxMemory = this.state.namespaceLimits.memory;
|
||||
const maxCpu = this.state.sliders.cpu.max;
|
||||
const memory = this.formValues.MemoryLimit;
|
||||
const maxMemory = this.state.sliders.memory.max;
|
||||
|
||||
// multiply 1000 can avoid 0.1 * 3 > 0.3
|
||||
if (cpu * 1000 * instances > maxCpu * 1000) {
|
||||
if (cpu * instances > maxCpu) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -642,7 +616,7 @@ class KubernetesCreateApplicationController {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.nodesLimits.overflowForReplica(cpu, memory, instances);
|
||||
return false;
|
||||
}
|
||||
|
||||
publishViaLoadBalancerEnabled() {
|
||||
@@ -650,7 +624,7 @@ class KubernetesCreateApplicationController {
|
||||
}
|
||||
|
||||
publishViaIngressEnabled() {
|
||||
return this.ingresses.length;
|
||||
return this.filteredIngresses.length;
|
||||
}
|
||||
|
||||
isEditAndNoChangesMade() {
|
||||
@@ -758,66 +732,50 @@ class KubernetesCreateApplicationController {
|
||||
|
||||
/* #region DATA AUTO REFRESH */
|
||||
updateSliders() {
|
||||
const quota = this.formValues.ResourcePool.Quota;
|
||||
let minCpu = 0,
|
||||
minMemory = 0,
|
||||
maxCpu = this.state.namespaceLimits.cpu,
|
||||
maxMemory = this.state.namespaceLimits.memory;
|
||||
|
||||
if (quota) {
|
||||
if (quota.CpuLimit) {
|
||||
minCpu = KubernetesApplicationQuotaDefaults.CpuLimit;
|
||||
}
|
||||
if (quota.MemoryLimit) {
|
||||
minMemory = KubernetesResourceReservationHelper.bytesValue(KubernetesApplicationQuotaDefaults.MemoryLimit);
|
||||
}
|
||||
}
|
||||
|
||||
maxCpu = Math.min(maxCpu, this.nodesLimits.MaxCPU);
|
||||
maxMemory = Math.min(maxMemory, this.nodesLimits.MaxMemory);
|
||||
|
||||
if (maxMemory < minMemory) {
|
||||
minMemory = 0;
|
||||
maxMemory = 0;
|
||||
}
|
||||
|
||||
this.state.sliders.memory.min = KubernetesResourceReservationHelper.megaBytesValue(minMemory);
|
||||
this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory);
|
||||
this.state.sliders.cpu.min = minCpu;
|
||||
this.state.sliders.cpu.max = _.floor(maxCpu, 2);
|
||||
if (!this.state.isEdit) {
|
||||
this.formValues.CpuLimit = minCpu;
|
||||
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(minMemory);
|
||||
}
|
||||
}
|
||||
|
||||
updateNamespaceLimits() {
|
||||
let maxCpu = this.state.nodes.cpu;
|
||||
let maxMemory = this.state.nodes.memory;
|
||||
const quota = this.formValues.ResourcePool.Quota;
|
||||
|
||||
this.state.resourcePoolHasQuota = false;
|
||||
|
||||
const quota = this.formValues.ResourcePool.Quota;
|
||||
let minCpu,
|
||||
maxCpu,
|
||||
minMemory,
|
||||
maxMemory = 0;
|
||||
if (quota) {
|
||||
if (quota.CpuLimit) {
|
||||
this.state.resourcePoolHasQuota = true;
|
||||
minCpu = KubernetesApplicationQuotaDefaults.CpuLimit;
|
||||
maxCpu = quota.CpuLimit - quota.CpuLimitUsed;
|
||||
if (this.state.isEdit && this.savedFormValues.CpuLimit) {
|
||||
maxCpu += this.savedFormValues.CpuLimit * this.effectiveInstances();
|
||||
maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount;
|
||||
}
|
||||
} else {
|
||||
minCpu = 0;
|
||||
maxCpu = this.state.nodes.cpu;
|
||||
}
|
||||
|
||||
if (quota.MemoryLimit) {
|
||||
this.state.resourcePoolHasQuota = true;
|
||||
minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit;
|
||||
maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed;
|
||||
if (this.state.isEdit && this.savedFormValues.MemoryLimit) {
|
||||
maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.effectiveInstances();
|
||||
maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount;
|
||||
}
|
||||
} else {
|
||||
minMemory = 0;
|
||||
maxMemory = this.state.nodes.memory;
|
||||
}
|
||||
} else {
|
||||
minCpu = 0;
|
||||
maxCpu = this.state.nodes.cpu;
|
||||
minMemory = 0;
|
||||
maxMemory = this.state.nodes.memory;
|
||||
}
|
||||
this.state.sliders.memory.min = minMemory;
|
||||
this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory);
|
||||
this.state.sliders.cpu.min = minCpu;
|
||||
this.state.sliders.cpu.max = _.round(maxCpu, 2);
|
||||
if (!this.state.isEdit) {
|
||||
this.formValues.CpuLimit = minCpu;
|
||||
this.formValues.MemoryLimit = minMemory;
|
||||
}
|
||||
|
||||
this.state.namespaceLimits.cpu = maxCpu;
|
||||
this.state.namespaceLimits.memory = maxMemory;
|
||||
}
|
||||
|
||||
refreshStacks(namespace) {
|
||||
@@ -871,22 +829,16 @@ class KubernetesCreateApplicationController {
|
||||
}
|
||||
|
||||
refreshIngresses(namespace) {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
this.ingresses = await this.KubernetesIngressService.get(namespace);
|
||||
this.ingressHostnames = this.ingresses.length ? this.ingresses[0].Hosts : [];
|
||||
if (!this.publishViaIngressEnabled()) {
|
||||
if (this.savedFormValues) {
|
||||
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
||||
} else {
|
||||
this.formValues.PublishingType = this.ApplicationPublishingTypes.INTERNAL;
|
||||
}
|
||||
}
|
||||
this.formValues.OriginalIngresses = this.ingresses;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses');
|
||||
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
|
||||
this.ingressHostnames = this.filteredIngresses.length ? this.filteredIngresses[0].Hosts : [];
|
||||
if (!this.publishViaIngressEnabled()) {
|
||||
if (this.savedFormValues) {
|
||||
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
||||
} else {
|
||||
this.formValues.PublishingType = this.ApplicationPublishingTypes.INTERNAL;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.formValues.OriginalIngresses = this.filteredIngresses;
|
||||
}
|
||||
|
||||
refreshNamespaceData(namespace) {
|
||||
@@ -911,7 +863,6 @@ class KubernetesCreateApplicationController {
|
||||
onResourcePoolSelectionChange() {
|
||||
return this.$async(async () => {
|
||||
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
||||
this.updateNamespaceLimits();
|
||||
this.updateSliders();
|
||||
await this.refreshNamespaceData(namespace);
|
||||
this.resetFormValues();
|
||||
@@ -996,12 +947,12 @@ class KubernetesCreateApplicationController {
|
||||
this.state.useLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
|
||||
const [resourcePools, nodes, nodesLimits] = await Promise.all([
|
||||
const [resourcePools, nodes, ingresses] = await Promise.all([
|
||||
this.KubernetesResourcePoolService.get(),
|
||||
this.KubernetesNodeService.get(),
|
||||
this.KubernetesNodesLimitsService.get(),
|
||||
this.KubernetesIngressService.get(),
|
||||
]);
|
||||
this.nodesLimits = nodesLimits;
|
||||
this.ingresses = ingresses;
|
||||
|
||||
this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
@@ -1014,7 +965,6 @@ class KubernetesCreateApplicationController {
|
||||
this.state.nodes.cpu += item.CPU;
|
||||
});
|
||||
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
|
||||
this.nodeNumber = nodes.length;
|
||||
|
||||
const namespace = this.state.isEdit ? this.$state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
|
||||
await this.refreshNamespaceData(namespace);
|
||||
@@ -1027,9 +977,9 @@ class KubernetesCreateApplicationController {
|
||||
this.configurations,
|
||||
this.persistentVolumeClaims,
|
||||
this.nodesLabels,
|
||||
this.ingresses
|
||||
this.filteredIngresses
|
||||
);
|
||||
this.formValues.OriginalIngresses = this.ingresses;
|
||||
this.formValues.OriginalIngresses = this.filteredIngresses;
|
||||
this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel);
|
||||
this.savedFormValues = angular.copy(this.formValues);
|
||||
delete this.formValues.ApplicationType;
|
||||
@@ -1046,13 +996,8 @@ class KubernetesCreateApplicationController {
|
||||
await this.refreshNamespaceData(namespace);
|
||||
} else {
|
||||
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
|
||||
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
|
||||
}
|
||||
|
||||
if (this.state.isEdit) {
|
||||
this.nodesLimits.excludesPods(this.application.Pods, this.formValues.CpuLimit, KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit));
|
||||
}
|
||||
|
||||
this.updateNamespaceLimits();
|
||||
this.updateSliders();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stack</td>
|
||||
<td>{{ ctrl.application.StackName || '-' }}</td>
|
||||
<td>{{ ctrl.application.StackName }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Namespace</td>
|
||||
@@ -191,15 +191,21 @@
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div ng-if="!ctrl.isSystemNamespace()" style="margin-bottom: 15px;">
|
||||
<button ng-if="!ctrl.isExternalApplication()" type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.application.edit" style="margin-left: 0;">
|
||||
<div ng-if="!ctrl.isSystemNamespace()">
|
||||
<button
|
||||
ng-if="!ctrl.isExternalApplication()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ui-sref="kubernetes.applications.application.edit"
|
||||
style="margin-left: 0; margin-bottom: 15px;"
|
||||
>
|
||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit this application
|
||||
</button>
|
||||
<button
|
||||
ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
style="margin-left: 0;"
|
||||
style="margin-left: 0; margin-bottom: 15px;"
|
||||
ng-click="ctrl.redeployApplication()"
|
||||
>
|
||||
<i class="fa fa-redo space-right" aria-hidden="true"></i>Redeploy
|
||||
@@ -208,19 +214,12 @@
|
||||
ng-if="!ctrl.isExternalApplication()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
style="margin-left: 0;"
|
||||
style="margin-left: 0; margin-bottom: 15px;"
|
||||
ng-click="ctrl.rollbackApplication()"
|
||||
ng-disabled="ctrl.application.Revisions.length < 2"
|
||||
>
|
||||
<i class="fas fa-history space-right" aria-hidden="true"></i>Rollback to previous configuration
|
||||
</button>
|
||||
<a
|
||||
ng-if="ctrl.isStack() && ctrl.stackFileContent"
|
||||
class="btn btn-sm btn-primary space-left"
|
||||
ui-sref="kubernetes.templates.custom.new({fileContent: ctrl.stackFileContent})"
|
||||
>
|
||||
<i class="fas fa-plus space-right" aria-hidden="true"></i>Create template from application
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- ACCESSING APPLICATION -->
|
||||
|
||||
@@ -107,8 +107,7 @@ class KubernetesApplicationController {
|
||||
KubernetesStackService,
|
||||
KubernetesPodService,
|
||||
KubernetesNodeService,
|
||||
EndpointProvider,
|
||||
StackService
|
||||
EndpointProvider
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
@@ -116,7 +115,6 @@ class KubernetesApplicationController {
|
||||
this.Notifications = Notifications;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.ModalService = ModalService;
|
||||
this.StackService = StackService;
|
||||
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesEventService = KubernetesEventService;
|
||||
@@ -195,10 +193,6 @@ class KubernetesApplicationController {
|
||||
return !rule.Host && !rule.IP ? false : true;
|
||||
}
|
||||
|
||||
isStack() {
|
||||
return this.application.StackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* ROLLBACK
|
||||
*/
|
||||
@@ -314,11 +308,6 @@ class KubernetesApplicationController {
|
||||
|
||||
this.placements = computePlacements(nodes, this.application);
|
||||
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
|
||||
|
||||
if (application.StackId) {
|
||||
const file = await this.StackService.getStackFile(application.StackId);
|
||||
this.stackFileContent = file;
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||
} finally {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-configurations-datatable
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require('../../templates/advancedDeploymentPanel.html');
|
||||
|
||||
import angular from 'angular';
|
||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||
|
||||
|
||||
@@ -23,18 +23,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment type
|
||||
</div>
|
||||
<box-selector radio-name="deploy" ng-model="ctrl.state.DeployType" options="ctrl.deployOptions" data-cy="k8sAppDeploy-deploymentSelector"></box-selector>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Build method
|
||||
</div>
|
||||
<box-selector radio-name="method" ng-model="ctrl.state.BuildMethod" options="ctrl.methodOptions" data-cy="k8sAppDeploy-buildSelector"></box-selector>
|
||||
|
||||
<div ng-if="ctrl.state.BuildMethod !== ctrl.BuildMethods.CUSTOM_TEMPLATE">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment type
|
||||
</div>
|
||||
<box-selector radio-name="deploy" ng-model="ctrl.state.DeployType" options="ctrl.deployOptions" data-cy="k8sAppDeploy-deploymentSelector"></box-selector>
|
||||
</div>
|
||||
|
||||
<!-- repository -->
|
||||
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
@@ -64,17 +62,9 @@
|
||||
</div>
|
||||
<!-- !repository -->
|
||||
|
||||
<custom-template-selector
|
||||
ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.CUSTOM_TEMPLATE"
|
||||
new-template-path="kubernetes.templates.custom.new"
|
||||
stack-type="3"
|
||||
on-change="(ctrl.onChangeTemplateId)"
|
||||
value="ctrl.state.templateId"
|
||||
></custom-template-selector>
|
||||
|
||||
<!-- editor -->
|
||||
<web-editor-form
|
||||
ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR || (ctrl.state.BuildMethod === ctrl.BuildMethods.CUSTOM_TEMPLATE && ctrl.state.templateId)"
|
||||
ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR"
|
||||
identifier="kubernetes-deploy-editor"
|
||||
value="ctrl.formValues.EditorContent"
|
||||
on-change="(ctrl.onChangeFileContent)"
|
||||
@@ -111,33 +101,6 @@
|
||||
</web-editor-form>
|
||||
|
||||
<!-- !editor -->
|
||||
|
||||
<!-- url -->
|
||||
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.URL">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
URL
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Indicate the URL to the manifest.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manifest_url" class="col-sm-1 control-label text-left">URL</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="ctrl.formValues.ManifestURL"
|
||||
id="manifest_url"
|
||||
placeholder="https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/controllers/nginx-deployment.yaml"
|
||||
data-cy="k8sAppDeploy-urlFileUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !url -->
|
||||
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import PortainerError from 'Portainer/error';
|
||||
|
||||
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods } from 'Kubernetes/models/deploy';
|
||||
import { buildOption } from '@/portainer/components/box-selector';
|
||||
class KubernetesDeployController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $window, CustomTemplateService, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) {
|
||||
constructor($async, $state, $window, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$window = $window;
|
||||
this.CustomTemplateService = CustomTemplateService;
|
||||
this.ModalService = ModalService;
|
||||
this.Notifications = Notifications;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
@@ -20,14 +18,12 @@ class KubernetesDeployController {
|
||||
|
||||
this.deployOptions = [
|
||||
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
|
||||
buildOption('method_compose', 'fab fa-docker', 'Compose', 'docker-compose format', KubernetesDeployManifestTypes.COMPOSE),
|
||||
buildOption('method_compose', 'fa fa-docker', 'Compose', 'docker-compose format', KubernetesDeployManifestTypes.COMPOSE),
|
||||
];
|
||||
|
||||
this.methodOptions = [
|
||||
buildOption('method_repo', 'fab fa-github', 'Git Repository', 'Use a git repository', KubernetesDeployBuildMethods.GIT),
|
||||
buildOption('method_editor', 'fa fa-edit', 'Web editor', 'Use our Web editor', KubernetesDeployBuildMethods.WEB_EDITOR),
|
||||
buildOption('method_url', 'fa fa-globe', 'URL', 'Specify a URL to a file', KubernetesDeployBuildMethods.URL),
|
||||
buildOption('method_template', 'fa fa-rocket', 'Custom Template', 'Use a custom template', KubernetesDeployBuildMethods.CUSTOM_TEMPLATE),
|
||||
];
|
||||
|
||||
this.state = {
|
||||
@@ -37,7 +33,6 @@ class KubernetesDeployController {
|
||||
activeTab: 0,
|
||||
viewReady: false,
|
||||
isEditorDirty: false,
|
||||
templateId: null,
|
||||
};
|
||||
|
||||
this.formValues = {};
|
||||
@@ -45,7 +40,7 @@ class KubernetesDeployController {
|
||||
this.BuildMethods = KubernetesDeployBuildMethods;
|
||||
this.endpointId = this.EndpointProvider.endpointID();
|
||||
|
||||
this.onChangeTemplateId = this.onChangeTemplateId.bind(this);
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.deployAsync = this.deployAsync.bind(this);
|
||||
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
||||
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
|
||||
@@ -59,11 +54,10 @@ class KubernetesDeployController {
|
||||
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
||||
(!this.formValues.RepositoryURL ||
|
||||
!this.formValues.FilePathInRepository ||
|
||||
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) && _.isEmpty(this.formValues.Namespace);
|
||||
const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent) && _.isEmpty(this.formValues.Namespace);
|
||||
const isURLFormInvalid = this.state.BuildMethod == KubernetesDeployBuildMethods.WEB_EDITOR.URL && _.isEmpty(this.formValues.ManifestURL);
|
||||
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword)));
|
||||
const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent);
|
||||
|
||||
return isGitFormInvalid || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress;
|
||||
return isGitFormInvalid || isWebEditorInvalid || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress;
|
||||
}
|
||||
|
||||
onChangeFormValues(values) {
|
||||
@@ -81,23 +75,6 @@ class KubernetesDeployController {
|
||||
this.onChangeFormValues({ RepositoryReferenceName: value });
|
||||
}
|
||||
|
||||
onChangeTemplateId(templateId) {
|
||||
return this.$async(async () => {
|
||||
if (this.state.templateId === templateId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.templateId = templateId;
|
||||
|
||||
try {
|
||||
const fileContent = await this.CustomTemplateService.customTemplateFile(templateId);
|
||||
this.onChangeFileContent(fileContent);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load template file');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeFileContent(value) {
|
||||
this.formValues.EditorContent = value;
|
||||
this.state.isEditorDirty = true;
|
||||
@@ -114,29 +91,10 @@ class KubernetesDeployController {
|
||||
this.state.actionInProgress = true;
|
||||
|
||||
try {
|
||||
let method;
|
||||
let composeFormat = this.state.DeployType === this.ManifestDeployTypes.COMPOSE;
|
||||
|
||||
switch (this.state.BuildMethod) {
|
||||
case this.BuildMethods.GIT:
|
||||
method = KubernetesDeployRequestMethods.REPOSITORY;
|
||||
break;
|
||||
case this.BuildMethods.WEB_EDITOR:
|
||||
method = KubernetesDeployRequestMethods.STRING;
|
||||
break;
|
||||
case KubernetesDeployBuildMethods.CUSTOM_TEMPLATE:
|
||||
method = KubernetesDeployRequestMethods.STRING;
|
||||
composeFormat = false;
|
||||
break;
|
||||
case this.BuildMethods.URL:
|
||||
method = KubernetesDeployRequestMethods.URL;
|
||||
break;
|
||||
default:
|
||||
throw new PortainerError('Unable to determine build method');
|
||||
}
|
||||
const method = this.state.BuildMethod === this.BuildMethods.GIT ? KubernetesDeployRequestMethods.REPOSITORY : KubernetesDeployRequestMethods.STRING;
|
||||
|
||||
const payload = {
|
||||
ComposeFormat: composeFormat,
|
||||
ComposeFormat: this.state.DeployType === this.ManifestDeployTypes.COMPOSE,
|
||||
Namespace: this.formValues.Namespace,
|
||||
};
|
||||
|
||||
@@ -149,10 +107,8 @@ class KubernetesDeployController {
|
||||
payload.RepositoryPassword = this.formValues.RepositoryPassword;
|
||||
}
|
||||
payload.FilePathInRepository = this.formValues.FilePathInRepository;
|
||||
} else if (method === KubernetesDeployRequestMethods.STRING) {
|
||||
payload.StackFileContent = this.formValues.EditorContent;
|
||||
} else {
|
||||
payload.ManifestURL = this.formValues.ManifestURL;
|
||||
payload.StackFileContent = this.formValues.EditorContent;
|
||||
}
|
||||
|
||||
await this.StackService.kubernetesDeploy(this.endpointId, method, payload);
|
||||
@@ -201,27 +157,20 @@ class KubernetesDeployController {
|
||||
return this.ModalService.confirmWebEditorDiscard();
|
||||
}
|
||||
}
|
||||
async onInit() {
|
||||
await this.getNamespaces();
|
||||
|
||||
this.state.viewReady = true;
|
||||
|
||||
this.$window.onbeforeunload = () => {
|
||||
if (this.formValues.EditorContent && this.state.isEditorDirty) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
await this.getNamespaces();
|
||||
|
||||
if (this.$state.params.templateId) {
|
||||
const templateId = parseInt(this.$state.params.templateId, 10);
|
||||
if (templateId && !Number.isNaN(templateId)) {
|
||||
this.state.BuildMethod = KubernetesDeployBuildMethods.CUSTOM_TEMPLATE;
|
||||
this.onChangeTemplateId(templateId);
|
||||
}
|
||||
}
|
||||
|
||||
this.state.viewReady = true;
|
||||
|
||||
this.$window.onbeforeunload = () => {
|
||||
if (this.formValues.EditorContent && this.state.isEditorDirty) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
|
||||
@@ -317,16 +317,16 @@
|
||||
Registries
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="!ctrl.isAdmin || ctrl.isSystem">
|
||||
<div class="form-group" ng-if="!ctrl.isAdmin">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
|
||||
Selected registries
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-4">
|
||||
{{ ctrl.selectedRegistries ? ctrl.selectedRegistries : 'None' }}
|
||||
{{ ctrl.selectedRegistries }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.isAdmin && !ctrl.isSystem">
|
||||
<div ng-if="ctrl.isAdmin">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<p>
|
||||
@@ -405,14 +405,14 @@
|
||||
<!-- !summary -->
|
||||
|
||||
<!-- actions -->
|
||||
<div ng-if="ctrl.isAdmin" class="col-sm-12 form-section-title">
|
||||
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div ng-if="ctrl.isAdmin" class="form-group">
|
||||
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
ng-if="!ctrl.isSystem"
|
||||
ng-if="ctrl.isEditable"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!resourcePoolEditForm.$valid || ctrl.isUpdateButtonDisabled()"
|
||||
ng-click="ctrl.updateResourcePool()"
|
||||
@@ -421,13 +421,7 @@
|
||||
<span ng-hide="ctrl.state.actionInProgress" data-cy="k8sNamespaceEdit-updateNamespaceButton">Update namespace</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
|
||||
</button>
|
||||
<button
|
||||
ng-if="!ctrl.isDefaultNamespace"
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="ctrl.markUnmarkAsSystem()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="ctrl.markUnmarkAsSystem()" button-spinner="ctrl.state.actionInProgress">
|
||||
<span ng-if="ctrl.isSystem">Unmark as system</span>
|
||||
<span ng-if="!ctrl.isSystem">Mark as system</span>
|
||||
</button>
|
||||
|
||||
@@ -185,26 +185,20 @@ class KubernetesResourcePoolController {
|
||||
}
|
||||
|
||||
updateResourcePool() {
|
||||
const ingressesToDelete = _.filter(this.formValues.IngressClasses, { WasSelected: true, Selected: false });
|
||||
const registriesToDelete = _.filter(this.registries, { WasChecked: true, Checked: false });
|
||||
const willBeDeleted = _.filter(this.formValues.IngressClasses, { WasSelected: true, Selected: false });
|
||||
const warnings = {
|
||||
quota: this.hasResourceQuotaBeenReduced(),
|
||||
ingress: ingressesToDelete.length !== 0,
|
||||
registries: registriesToDelete.length !== 0,
|
||||
ingress: willBeDeleted.length !== 0,
|
||||
};
|
||||
|
||||
if (warnings.quota || warnings.ingress || warnings.registries) {
|
||||
if (warnings.quota || warnings.ingress) {
|
||||
const messages = {
|
||||
quota:
|
||||
'Reducing the quota assigned to an "in-use" namespace may have unintended consequences, including preventing running applications from functioning correctly and potentially even blocking them from running at all.',
|
||||
ingress: 'Deactivating ingresses may cause applications to be unaccessible. All ingress configurations from affected applications will be removed.',
|
||||
registries:
|
||||
'Some registries you removed might be used by one or more applications inside this environment. Removing the registries access could lead to a service interruption for these applications.',
|
||||
};
|
||||
const displayedMessage = `${warnings.quota ? messages.quota + '<br/><br/>' : ''}
|
||||
${warnings.ingress ? messages.ingress + '<br/><br/>' : ''}
|
||||
${warnings.registries ? messages.registries + '<br/><br/>' : ''}
|
||||
Do you wish to continue?`;
|
||||
const displayedMessage = `${warnings.quota ? messages.quota : ''}${warnings.quota && warnings.ingress ? '<br/><br/>' : ''}
|
||||
${warnings.ingress ? messages.ingress : ''}<br/><br/>Do you wish to continue?`;
|
||||
this.ModalService.confirmUpdate(displayedMessage, (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.updateResourcePoolAsync, this.savedFormValues, this.formValues);
|
||||
@@ -299,7 +293,7 @@ class KubernetesResourcePoolController {
|
||||
this.state.ingressesLoading = true;
|
||||
try {
|
||||
const namespace = this.pool.Namespace.Name;
|
||||
this.allIngresses = await this.KubernetesIngressService.get(this.state.hasWriteAuthorization ? '' : namespace);
|
||||
this.allIngresses = await this.KubernetesIngressService.get();
|
||||
this.ingresses = _.filter(this.allIngresses, { Namespace: namespace });
|
||||
_.forEach(this.ingresses, (ing) => {
|
||||
ing.Namespace = namespace;
|
||||
@@ -328,11 +322,10 @@ class KubernetesResourcePoolController {
|
||||
this.registries.forEach((reg) => {
|
||||
if (reg.RegistryAccesses && reg.RegistryAccesses[this.endpoint.Id] && reg.RegistryAccesses[this.endpoint.Id].Namespaces.includes(namespace)) {
|
||||
reg.Checked = true;
|
||||
reg.WasChecked = true;
|
||||
this.formValues.Registries.push(reg);
|
||||
}
|
||||
});
|
||||
this.selectedRegistries = this.formValues.Registries.map((r) => r.Name).join(', ');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require('../../templates/advancedDeploymentPanel.html');
|
||||
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import angular from 'angular';
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<!-- !icon-url-input -->
|
||||
|
||||
<!-- platform-input -->
|
||||
<div ng-if="$ctrl.showPlatformField" class="form-group">
|
||||
<div class="form-group">
|
||||
<label for="platform" class="col-sm-3 col-lg-2 control-label text-left">Platform</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select class="form-control" ng-model="$ctrl.formValues.Platform" ng-options="+(opt.value) as opt.label for opt in $ctrl.platformTypes"> </select>
|
||||
@@ -61,7 +61,7 @@
|
||||
<!-- !platform-input -->
|
||||
|
||||
<!-- platform-input -->
|
||||
<div ng-if="$ctrl.showTypeField" class="form-group">
|
||||
<div class="form-group">
|
||||
<label for="platform" class="col-sm-3 col-lg-2 control-label text-left">Type</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select class="form-control" ng-model="$ctrl.formValues.Type" ng-options="+(opt.value) as opt.label for opt in $ctrl.templateTypes"> </select>
|
||||
|
||||
@@ -5,7 +5,5 @@ angular.module('portainer.app').component('customTemplateCommonFields', {
|
||||
controller: CustomTemplateCommonFieldsController,
|
||||
bindings: {
|
||||
formValues: '=',
|
||||
showPlatformField: '<',
|
||||
showTypeField: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-state="$ctrl.createPath"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add Custom Template </button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.templates.custom.new">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add Custom Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="searchBar" style="border-top: 2px solid #f6f6f6;">
|
||||
@@ -25,12 +27,12 @@
|
||||
<template-item
|
||||
ng-repeat="template in $ctrl.templates | filter:$ctrl.state.textFilter"
|
||||
model="template"
|
||||
type-label="{{ $ctrl.typeLabel(template.Type) }}"
|
||||
type-label="{{ template.Type === 1 ? 'swarm' : 'standalone' }}"
|
||||
on-select="($ctrl.onSelectClick)"
|
||||
>
|
||||
<template-item-actions>
|
||||
<div ng-if="$ctrl.isEditAllowed(template)" style="display: flex;">
|
||||
<a ui-state="$ctrl.editPath" ui-state-params="({id: template.Id})" ng-click="$event.stopPropagation();" class="btn btn-primary btn-xs" style="margin-right: 10px;">
|
||||
<a ui-sref="docker.templates.custom.edit({id: template.Id})" ng-click="$event.stopPropagation();" class="btn btn-primary btn-xs" style="margin-right: 10px;">
|
||||
Edit
|
||||
</a>
|
||||
<button class="btn btn-danger btn-xs" ng-click="$ctrl.onDeleteClick(template.Id); $event.stopPropagation();">Delete</button>
|
||||
|
||||
@@ -1,28 +1,7 @@
|
||||
const CUSTOM_TEMPLATES_TYPES = {
|
||||
STANDALONE: 1,
|
||||
SWARM: 2,
|
||||
KUBERNETES: 3,
|
||||
};
|
||||
|
||||
angular.module('portainer.docker').controller('CustomTemplatesListController', function ($scope, $controller, DatatableService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
this.typeLabel = typeLabel;
|
||||
this.$onInit = $onInit;
|
||||
|
||||
function typeLabel(type) {
|
||||
switch (type) {
|
||||
case CUSTOM_TEMPLATES_TYPES.SWARM:
|
||||
return 'swarm';
|
||||
case CUSTOM_TEMPLATES_TYPES.KUBERNETES:
|
||||
return 'manifest';
|
||||
case CUSTOM_TEMPLATES_TYPES.STANDALONE:
|
||||
default:
|
||||
return 'standalone';
|
||||
}
|
||||
}
|
||||
|
||||
function $onInit() {
|
||||
this.$onInit = function () {
|
||||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
@@ -53,5 +32,5 @@ angular.module('portainer.docker').controller('CustomTemplatesListController', f
|
||||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -12,7 +12,5 @@ angular.module('portainer.app').component('customTemplatesList', {
|
||||
showSwarmStacks: '<',
|
||||
onDeleteClick: '<',
|
||||
isEditAllowed: '<',
|
||||
createPath: '@',
|
||||
editPath: '@',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<ng-form class="file-upload-form" name="$ctrl.fileUploadForm">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Upload
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small" ng-transclude="description"> </span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-sm btn-primary" ngf-select="$ctrl.onChange($file)" ng-model="$ctrl.file" ng-required="$ctrl.ngRequired" name="file">
|
||||
Select file
|
||||
</button>
|
||||
<span class="space-left">
|
||||
{{ $ctrl.file.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.file" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
@@ -1,13 +0,0 @@
|
||||
export const fileUploadForm = {
|
||||
templateUrl: './file-upload-form.html',
|
||||
|
||||
bindings: {
|
||||
file: '<',
|
||||
ngRequired: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
|
||||
transclude: {
|
||||
description: '?fileUploadDescription',
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { webEditorForm } from './web-editor-form';
|
||||
import { fileUploadForm } from './file-upload-form';
|
||||
|
||||
export default angular.module('portainer.app.components.form', []).component('webEditorForm', webEditorForm).component('fileUploadForm', fileUploadForm).name;
|
||||
export default angular.module('portainer.app.components.form', []).component('webEditorForm', webEditorForm).name;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import uuidv4 from 'uuid/v4';
|
||||
|
||||
class StackRedeployGitFormController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
|
||||
@@ -16,7 +15,6 @@ class StackRedeployGitFormController {
|
||||
redeployInProgress: false,
|
||||
showConfig: false,
|
||||
isEdit: false,
|
||||
hasUnsavedChanges: false,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
@@ -36,8 +34,11 @@ class StackRedeployGitFormController {
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onChangeRef = this.onChangeRef.bind(this);
|
||||
this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this);
|
||||
this.onChangeEnvVar = this.onChangeEnvVar.bind(this);
|
||||
this.handleEnvVarChange = this.handleEnvVarChange.bind(this);
|
||||
}
|
||||
|
||||
onChangeRef(value) {
|
||||
this.onChange({ RefName: value });
|
||||
}
|
||||
|
||||
onChange(values) {
|
||||
@@ -45,25 +46,6 @@ class StackRedeployGitFormController {
|
||||
...this.formValues,
|
||||
...values,
|
||||
};
|
||||
|
||||
this.state.hasUnsavedChanges = angular.toJson(this.savedFormValues) !== angular.toJson(this.formValues);
|
||||
}
|
||||
|
||||
onChangeRef(value) {
|
||||
this.onChange({ RefName: value });
|
||||
}
|
||||
|
||||
onChangeAutoUpdate(values) {
|
||||
this.onChange({
|
||||
AutoUpdate: {
|
||||
...this.formValues.AutoUpdate,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onChangeEnvVar(value) {
|
||||
this.onChange({ Env: value });
|
||||
}
|
||||
|
||||
async submit() {
|
||||
@@ -101,8 +83,6 @@ class StackRedeployGitFormController {
|
||||
try {
|
||||
this.state.inProgress = true;
|
||||
await this.StackService.updateGitStackSettings(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), this.formValues);
|
||||
this.savedFormValues = angular.copy(this.formValues);
|
||||
this.state.hasUnsavedChanges = false;
|
||||
this.Notifications.success('Save stack settings successfully');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to save stack settings');
|
||||
@@ -116,6 +96,10 @@ class StackRedeployGitFormController {
|
||||
return this.state.inProgress || this.state.redeployInProgress;
|
||||
}
|
||||
|
||||
handleEnvVarChange(value) {
|
||||
this.formValues.Env = value;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.formValues.RefName = this.model.ReferenceName;
|
||||
this.formValues.Env = this.stack.Env;
|
||||
@@ -141,8 +125,6 @@ class StackRedeployGitFormController {
|
||||
this.formValues.RepositoryAuthentication = true;
|
||||
this.state.isEdit = true;
|
||||
}
|
||||
|
||||
this.savedFormValues = angular.copy(this.formValues);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<git-form-auto-update-fieldset model="$ctrl.formValues.AutoUpdate" on-change="($ctrl.onChangeAutoUpdate)"></git-form-auto-update-fieldset>
|
||||
<git-form-auto-update-fieldset model="$ctrl.formValues.AutoUpdate" on-change="($ctrl.onChange)"></git-form-auto-update-fieldset>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<p>
|
||||
@@ -40,14 +40,14 @@
|
||||
<environment-variables-panel
|
||||
ng-model="$ctrl.formValues.Env"
|
||||
explanation="These values will be used as substitutions in the stack file"
|
||||
on-change="($ctrl.onChangeEnvVar)"
|
||||
on-change="($ctrl.handleEnvVarChange)"
|
||||
></environment-variables-panel>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.submit()"
|
||||
ng-if="!$ctrl.formValues.AutoUpdate.RepositoryAutomaticUpdates"
|
||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.redeployInProgress"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
@@ -60,7 +60,7 @@
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="$ctrl.saveGitSettings()"
|
||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
|
||||
style="margin-top: 7px; margin-left: 0;"
|
||||
button-spinner="$ctrl.state.inProgress"
|
||||
>
|
||||
|
||||
@@ -18,14 +18,11 @@
|
||||
{{ $ctrl.model.Title }}
|
||||
</span>
|
||||
<span class="space-left blocklist-item-subtitle">
|
||||
<span ng-if="$ctrl.model.Type != 3">
|
||||
<span>
|
||||
<i class="fab fa-linux" aria-hidden="true" ng-if="$ctrl.model.Platform === 1 || $ctrl.model.Platform === 'linux' || !$ctrl.model.Platform"></i>
|
||||
<span ng-if="!$ctrl.model.Platform"> & </span>
|
||||
<i class="fab fa-windows" aria-hidden="true" ng-if="$ctrl.model.Platform === 2 || $ctrl.model.Platform === 'windows' || !$ctrl.model.Platform"></i>
|
||||
</span>
|
||||
<span ng-if="$ctrl.model.Type === 3">
|
||||
<i class="fa fa-dharmachakra" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>
|
||||
{{ $ctrl.typeLabel }}
|
||||
</span>
|
||||
|
||||
@@ -10,7 +10,6 @@ export function SettingsViewModel(data) {
|
||||
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
|
||||
this.UserSessionTimeout = data.UserSessionTimeout;
|
||||
this.EnableTelemetry = data.EnableTelemetry;
|
||||
this.KubeconfigExpiry = data.KubeconfigExpiry;
|
||||
}
|
||||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="customTemplateForm">
|
||||
<custom-template-common-fields form-values="$ctrl.formValues" show-platform-field="true" show-type-field="true"></custom-template-common-fields>
|
||||
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
|
||||
|
||||
<!-- build-method -->
|
||||
<div ng-if="!$ctrl.state.fromStack">
|
||||
|
||||
@@ -175,7 +175,7 @@ class CreateCustomTemplateViewController {
|
||||
}
|
||||
|
||||
try {
|
||||
this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
|
||||
this.templates = await this.CustomTemplateService.customTemplates();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
title-icon="fa-rocket"
|
||||
templates="$ctrl.templates"
|
||||
table-key="customTemplates"
|
||||
create-path="docker.templates.custom.new"
|
||||
is-edit-allowed="$ctrl.isEditAllowed"
|
||||
on-select-click="($ctrl.selectTemplate)"
|
||||
on-delete-click="($ctrl.confirmDelete)"
|
||||
|
||||
@@ -81,12 +81,16 @@ class CustomTemplatesViewController {
|
||||
return this.currentUser.isAdmin || this.currentUser.id === template.CreatedByUserId;
|
||||
}
|
||||
|
||||
getTemplates() {
|
||||
return this.$async(this.getTemplatesAsync);
|
||||
getTemplates(endpointMode) {
|
||||
return this.$async(this.getTemplatesAsync, endpointMode);
|
||||
}
|
||||
async getTemplatesAsync() {
|
||||
async getTemplatesAsync({ provider, role }) {
|
||||
try {
|
||||
this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
|
||||
let stackType = 2;
|
||||
if (provider === 'DOCKER_SWARM_MODE' && role === 'MANAGER') {
|
||||
stackType = 1;
|
||||
}
|
||||
this.templates = await this.CustomTemplateService.customTemplates(stackType);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failed loading templates', err, 'Unable to load custom templates');
|
||||
}
|
||||
@@ -233,6 +237,7 @@ class CustomTemplatesViewController {
|
||||
case 2:
|
||||
deployable = endpoint.mode.provider === this.DOCKER_STANDALONE;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return deployable;
|
||||
@@ -246,7 +251,7 @@ class CustomTemplatesViewController {
|
||||
apiVersion,
|
||||
} = applicationState;
|
||||
|
||||
this.getTemplates();
|
||||
this.getTemplates(endpointMode);
|
||||
this.getNetworks(endpointMode.provider, apiVersion);
|
||||
|
||||
this.currentUser.isAdmin = this.Authentication.isAdmin();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="customTemplateForm">
|
||||
<custom-template-common-fields form-values="$ctrl.formValues" show-platform-field="true" show-type-field="true"></custom-template-common-fields>
|
||||
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
|
||||
|
||||
<!-- web-editor -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
||||
@@ -112,7 +112,7 @@ class EditCustomTemplateViewController {
|
||||
this.getTemplate();
|
||||
|
||||
try {
|
||||
this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
|
||||
this.templates = await this.CustomTemplateService.customTemplates();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
||||
}
|
||||
|
||||
@@ -21,11 +21,7 @@ angular.module('portainer.app').controller('RegistriesController', [
|
||||
};
|
||||
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
const regAttrMsg = selectedItems.length > 1 ? 'hese' : 'his';
|
||||
const registriesMsg = selectedItems.length > 1 ? 'registries' : 'registry';
|
||||
const msg = `T${regAttrMsg} ${registriesMsg} might be used by applications inside one or more endpoints. Removing the ${registriesMsg} could lead to a service interruption for the applications using t${regAttrMsg} ${registriesMsg}. Do you want to remove the selected ${registriesMsg}?`;
|
||||
|
||||
ModalService.confirmDeletion(msg, function onConfirm(confirmed) {
|
||||
ModalService.confirmDeletion('Do you want to remove the selected registries?', function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,24 +118,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !edge -->
|
||||
<!-- kube -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Kubernetes
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edge_checkin" class="col-sm-2 control-label text-left">
|
||||
Kubeconfig expiry
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select
|
||||
id="kubeconfig_expiry"
|
||||
class="form-control"
|
||||
ng-model="settings.KubeconfigExpiry"
|
||||
ng-options="opt.value as opt.key for opt in state.availableKubeconfigExpiryOptions"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! kube -->
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
||||
@@ -24,28 +24,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
value: 30,
|
||||
},
|
||||
],
|
||||
availableKubeconfigExpiryOptions: [
|
||||
{
|
||||
key: '1 day',
|
||||
value: '24h',
|
||||
},
|
||||
{
|
||||
key: '7 days',
|
||||
value: `${24 * 7}h`,
|
||||
},
|
||||
{
|
||||
key: '30 days',
|
||||
value: `${24 * 30}h`,
|
||||
},
|
||||
{
|
||||
key: '1 year',
|
||||
value: `${24 * 30 * 12}h`,
|
||||
},
|
||||
{
|
||||
key: 'No expiry',
|
||||
value: '0',
|
||||
},
|
||||
],
|
||||
|
||||
backupInProgress: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -175,9 +175,7 @@ function shell_build_binary_azuredevops(p, a) {
|
||||
function shell_run_container() {
|
||||
return [
|
||||
'docker rm -f portainer',
|
||||
'docker run -d -p 8000:8000 -p 9000:9000 -p 9443:9443 -v ' +
|
||||
portainer_root +
|
||||
'/dist:/app -v ' +
|
||||
'docker run -d -p 8000:8000 -p 9000:9000 -v ' + portainer_root + '/dist:/app -v ' +
|
||||
portainer_data +
|
||||
':/data -v /var/run/docker.sock:/var/run/docker.sock:z -v /var/run/docker.sock:/var/run/alternative.sock:z -v /tmp:/tmp --name portainer portainer/base /app/portainer',
|
||||
].join(';');
|
||||
|
||||
Reference in New Issue
Block a user