mirror of
https://github.com/portainer/portainer.git
synced 2025-07-21 14:29:40 +02:00
resolve conflicts
This commit is contained in:
commit
0ee42f76c3
84 changed files with 1475 additions and 205 deletions
|
@ -163,5 +163,19 @@
|
||||||
"// @failure 500 \"Server error\"",
|
"// @failure 500 \"Server error\"",
|
||||||
"// @router /{id} [get]"
|
"// @router /{id} [get]"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"analytics": {
|
||||||
|
"prefix": "nlt",
|
||||||
|
"body": ["analytics-on", "analytics-category=\"$1\"", "analytics-event=\"$2\""],
|
||||||
|
"description": "analytics"
|
||||||
|
},
|
||||||
|
"analytics-if": {
|
||||||
|
"prefix": "nltf",
|
||||||
|
"body": ["analytics-if=\"$1\""],
|
||||||
|
"description": "analytics"
|
||||||
|
},
|
||||||
|
"analytics-metadata": {
|
||||||
|
"prefix": "nltm",
|
||||||
|
"body": "analytics-properties=\"{ metadata: { $1 } }\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,9 +105,10 @@ type customTemplateFromFileContentPayload struct {
|
||||||
Note string `example:"This is my <b>custom</b> template"`
|
Note string `example:"This is my <b>custom</b> template"`
|
||||||
// Platform associated to the template.
|
// Platform associated to the template.
|
||||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
|
// Required for Docker stacks
|
||||||
// Type of created stack (1 - swarm, 2 - compose)
|
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||||
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
|
||||||
|
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||||
// Content of stack file
|
// Content of stack file
|
||||||
FileContent string `validate:"required"`
|
FileContent string `validate:"required"`
|
||||||
}
|
}
|
||||||
|
@ -122,10 +123,10 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
|
||||||
if govalidator.IsNull(payload.FileContent) {
|
if govalidator.IsNull(payload.FileContent) {
|
||||||
return errors.New("Invalid file content")
|
return errors.New("Invalid file content")
|
||||||
}
|
}
|
||||||
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||||
return errors.New("Invalid custom template platform")
|
return errors.New("Invalid custom template platform")
|
||||||
}
|
}
|
||||||
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||||
return errors.New("Invalid custom template type")
|
return errors.New("Invalid custom template type")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -171,7 +172,8 @@ type customTemplateFromGitRepositoryPayload struct {
|
||||||
Note string `example:"This is my <b>custom</b> template"`
|
Note string `example:"This is my <b>custom</b> template"`
|
||||||
// Platform associated to the template.
|
// Platform associated to the template.
|
||||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
|
// Required for Docker stacks
|
||||||
|
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||||
// Type of created stack (1 - swarm, 2 - compose)
|
// Type of created stack (1 - swarm, 2 - compose)
|
||||||
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
||||||
|
|
||||||
|
@ -205,6 +207,11 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
|
||||||
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
||||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
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 {
|
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||||
return errors.New("Invalid custom template platform")
|
return errors.New("Invalid custom template platform")
|
||||||
}
|
}
|
||||||
|
@ -278,20 +285,21 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
|
||||||
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
|
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
|
||||||
payload.Note = note
|
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)
|
typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true)
|
||||||
templateType := portainer.StackType(typeNumeral)
|
templateType := portainer.StackType(typeNumeral)
|
||||||
if templateType != portainer.DockerComposeStack && templateType != portainer.DockerSwarmStack {
|
if templateType != portainer.KubernetesStack && templateType != portainer.DockerSwarmStack && templateType != portainer.DockerComposeStack {
|
||||||
return errors.New("Invalid custom template type")
|
return errors.New("Invalid custom template type")
|
||||||
}
|
}
|
||||||
payload.Type = templateType
|
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")
|
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "File")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
|
return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
|
||||||
|
|
|
@ -2,7 +2,9 @@ package customtemplates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
@ -17,10 +19,16 @@ import (
|
||||||
// @tags custom_templates
|
// @tags custom_templates
|
||||||
// @security jwt
|
// @security jwt
|
||||||
// @produce json
|
// @produce json
|
||||||
|
// @param type query []int true "Template types" Enums(1,2,3)
|
||||||
// @success 200 {array} portainer.CustomTemplate "Success"
|
// @success 200 {array} portainer.CustomTemplate "Success"
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error"
|
||||||
// @router /custom_templates [get]
|
// @router /custom_templates [get]
|
||||||
func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
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()
|
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
|
||||||
|
@ -52,5 +60,52 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
|
||||||
customTemplates = authorization.FilterAuthorizedCustomTemplates(customTemplates, user, userTeamIDs)
|
customTemplates = authorization.FilterAuthorizedCustomTemplates(customTemplates, user, userTeamIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customTemplates = filterByType(customTemplates, templateTypes)
|
||||||
|
|
||||||
return response.JSON(w, customTemplates)
|
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,9 +27,10 @@ type customTemplateUpdatePayload struct {
|
||||||
Note string `example:"This is my <b>custom</b> template"`
|
Note string `example:"This is my <b>custom</b> template"`
|
||||||
// Platform associated to the template.
|
// Platform associated to the template.
|
||||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
|
// Required for Docker stacks
|
||||||
// Type of created stack (1 - swarm, 2 - compose)
|
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||||
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
|
||||||
|
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||||
// Content of stack file
|
// Content of stack file
|
||||||
FileContent string `validate:"required"`
|
FileContent string `validate:"required"`
|
||||||
}
|
}
|
||||||
|
@ -41,10 +42,10 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||||
if govalidator.IsNull(payload.FileContent) {
|
if govalidator.IsNull(payload.FileContent) {
|
||||||
return errors.New("Invalid file content")
|
return errors.New("Invalid file content")
|
||||||
}
|
}
|
||||||
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||||
return errors.New("Invalid custom template platform")
|
return errors.New("Invalid custom template platform")
|
||||||
}
|
}
|
||||||
if payload.Type != portainer.DockerComposeStack && payload.Type != portainer.DockerSwarmStack {
|
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||||
return errors.New("Invalid custom template type")
|
return errors.New("Invalid custom template type")
|
||||||
}
|
}
|
||||||
if govalidator.IsNull(payload.Description) {
|
if govalidator.IsNull(payload.Description) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/portainer/portainer/api/http/client"
|
||||||
k "github.com/portainer/portainer/api/kubernetes"
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,6 +39,12 @@ type kubernetesGitDeploymentPayload struct {
|
||||||
FilePathInRepository string
|
FilePathInRepository string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type kubernetesManifestURLDeploymentPayload struct {
|
||||||
|
Namespace string
|
||||||
|
ComposeFormat bool
|
||||||
|
ManifestURL string
|
||||||
|
}
|
||||||
|
|
||||||
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
|
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
|
||||||
if govalidator.IsNull(payload.StackFileContent) {
|
if govalidator.IsNull(payload.StackFileContent) {
|
||||||
return errors.New("Invalid stack file content")
|
return errors.New("Invalid stack file content")
|
||||||
|
@ -67,6 +74,13 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
||||||
return nil
|
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 {
|
type createKubernetesStackResponse struct {
|
||||||
Output string `json:"Output"`
|
Output string `json:"Output"`
|
||||||
}
|
}
|
||||||
|
@ -132,6 +146,8 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||||
Output: output,
|
Output: output,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doCleanUp = false
|
||||||
|
|
||||||
return response.JSON(w, resp)
|
return response.JSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,6 +225,70 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||||
resp := &createKubernetesStackResponse{
|
resp := &createKubernetesStackResponse{
|
||||||
Output: output,
|
Output: output,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doCleanUp = false
|
||||||
|
|
||||||
|
return response.JSON(w, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *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}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := handler.DataStore.User().User(userID)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
|
stack := &portainer.Stack{
|
||||||
|
ID: portainer.StackID(stackID),
|
||||||
|
Type: portainer.KubernetesStack,
|
||||||
|
EndpointID: endpoint.ID,
|
||||||
|
EntryPoint: filesystem.ManifestFileDefaultName,
|
||||||
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
|
CreatedBy: user.Username,
|
||||||
|
IsComposeFormat: payload.ComposeFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifestContent []byte
|
||||||
|
manifestContent, err = client.Get(payload.ManifestURL, 30)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve manifest from URL", Err: 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)
|
return response.JSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +311,6 @@ func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *p
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace)
|
return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
|
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
|
||||||
|
|
|
@ -149,6 +149,8 @@ func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Req
|
||||||
return handler.createKubernetesStackFromFileContent(w, r, endpoint, userID)
|
return handler.createKubernetesStackFromFileContent(w, r, endpoint, userID)
|
||||||
case "repository":
|
case "repository":
|
||||||
return handler.createKubernetesStackFromGitRepository(w, r, endpoint, userID)
|
return handler.createKubernetesStackFromGitRepository(w, r, endpoint, userID)
|
||||||
|
case "url":
|
||||||
|
return handler.createKubernetesStackFromManifestURL(w, r, endpoint, userID)
|
||||||
}
|
}
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string or repository", Err: errors.New(request.ErrInvalidQueryParameter)}
|
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,21 +135,25 @@ func (handler *Handler) hijackPodExecStartOperation(
|
||||||
stdoutReader, stdoutWriter := io.Pipe()
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
defer stdoutWriter.Close()
|
defer stdoutWriter.Close()
|
||||||
|
|
||||||
|
// errorChan is used to propagate errors from the go routines to the caller.
|
||||||
errorChan := make(chan error, 1)
|
errorChan := make(chan error, 1)
|
||||||
go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
|
go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
|
||||||
go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
|
go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
|
||||||
|
|
||||||
err = cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
|
// StartExecProcess is a blocking operation which streams IO to/from pod;
|
||||||
if err != nil {
|
// this must execute in asynchronously, since the websocketConn could return errors (e.g. client disconnects) before
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
|
// the blocking operation is completed.
|
||||||
}
|
go cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter, errorChan)
|
||||||
|
|
||||||
err = <-errorChan
|
err = <-errorChan
|
||||||
|
|
||||||
|
// websocket client successfully disconnected
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||||
log.Printf("websocket error: %s \n", err.Error())
|
log.Printf("websocket error: %s \n", err.Error())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, bool, error) {
|
func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, bool, error) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/remotecommand"
|
"k8s.io/client-go/tools/remotecommand"
|
||||||
|
@ -15,10 +15,12 @@ import (
|
||||||
// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write
|
// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write
|
||||||
// to the stdout parameter.
|
// to the stdout parameter.
|
||||||
// This function only works against a local endpoint using an in-cluster config with the user's SA token.
|
// This function only works against a local endpoint using an in-cluster config with the user's SA token.
|
||||||
func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error {
|
// 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) {
|
||||||
config, err := rest.InClusterConfig()
|
config, err := rest.InClusterConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
errChan <- err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !useAdminToken {
|
if !useAdminToken {
|
||||||
|
@ -44,7 +46,8 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
|
||||||
|
|
||||||
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
errChan <- err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = exec.Stream(remotecommand.StreamOptions{
|
err = exec.Stream(remotecommand.StreamOptions{
|
||||||
|
@ -54,9 +57,7 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(utilexec.ExitError); !ok {
|
if _, ok := err.(utilexec.ExitError); !ok {
|
||||||
return errors.New("unable to start exec process")
|
errChan <- errors.New("unable to start exec process")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1232,7 +1232,7 @@ type (
|
||||||
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
|
||||||
GetServiceAccountBearerToken(userID int) (string, error)
|
GetServiceAccountBearerToken(userID int) (string, error)
|
||||||
CreateUserShellPod(ctx context.Context, serviceAccountName string) (*KubernetesShellPod, error)
|
CreateUserShellPod(ctx context.Context, serviceAccountName string) (*KubernetesShellPod, error)
|
||||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
||||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||||
GetNodesLimits() (K8sNodesLimits, error)
|
GetNodesLimits() (K8sNodesLimits, error)
|
||||||
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
const basePath = 'http://portainer-ce.app';
|
const basePath = 'http://portainer-ce.app';
|
||||||
|
|
||||||
|
@ -131,7 +132,8 @@ function config($analyticsProvider, $windowProvider) {
|
||||||
|
|
||||||
let metadataString = '';
|
let metadataString = '';
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
metadataString = JSON.stringify(metadata).toLowerCase();
|
const kebabCasedMetadata = Object.fromEntries(Object.entries(metadata).map(([key, value]) => [_.kebabCase(key), value]));
|
||||||
|
metadataString = JSON.stringify(kebabCasedMetadata).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
push([
|
push([
|
||||||
|
|
|
@ -199,6 +199,10 @@
|
||||||
ng-click="$ctrl.createStack()"
|
ng-click="$ctrl.createStack()"
|
||||||
button-spinner="$ctrl.state.actionInProgress"
|
button-spinner="$ctrl.state.actionInProgress"
|
||||||
data-cy="edgeStackCreate-createStackButton"
|
data-cy="edgeStackCreate-createStackButton"
|
||||||
|
analytics-on
|
||||||
|
analytics-event="edge-stack-creation"
|
||||||
|
analytics-category="edge"
|
||||||
|
analytics-properties="$ctrl.buildAnalyticsProperties()"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
|
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
|
||||||
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
|
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||||
|
|
|
@ -43,6 +43,30 @@ export class CreateEdgeStackViewController {
|
||||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildAnalyticsProperties() {
|
||||||
|
const format = 'compose';
|
||||||
|
const metadata = { type: methodLabel(this.state.Method), format };
|
||||||
|
|
||||||
|
if (metadata.type === 'template') {
|
||||||
|
metadata.templateName = this.selectedTemplate.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { metadata };
|
||||||
|
|
||||||
|
function methodLabel(method) {
|
||||||
|
switch (method) {
|
||||||
|
case 'editor':
|
||||||
|
return 'web-editor';
|
||||||
|
case 'repository':
|
||||||
|
return 'git';
|
||||||
|
case 'upload':
|
||||||
|
return 'file-upload';
|
||||||
|
case 'template':
|
||||||
|
return 'template';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async uiCanExit() {
|
async uiCanExit() {
|
||||||
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
|
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
|
||||||
return this.ModalService.confirmWebEditorDiscard();
|
return this.ModalService.confirmWebEditorDiscard();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import registriesModule from './registries';
|
import registriesModule from './registries';
|
||||||
|
import customTemplateModule from './custom-templates';
|
||||||
|
|
||||||
angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).config([
|
angular.module('portainer.kubernetes', ['portainer.app', registriesModule, customTemplateModule]).config([
|
||||||
'$stateRegistryProvider',
|
'$stateRegistryProvider',
|
||||||
function ($stateRegistryProvider) {
|
function ($stateRegistryProvider) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
@ -208,12 +209,15 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).conf
|
||||||
|
|
||||||
const deploy = {
|
const deploy = {
|
||||||
name: 'kubernetes.deploy',
|
name: 'kubernetes.deploy',
|
||||||
url: '/deploy',
|
url: '/deploy?templateId',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'kubernetesDeployView',
|
component: 'kubernetesDeployView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
params: {
|
||||||
|
templateId: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const resourcePools = {
|
const resourcePools = {
|
||||||
|
|
|
@ -73,7 +73,10 @@
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.new" data-cy="k8sApp-addApplicationButton">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.new" data-cy="k8sApp-addApplicationButton">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application
|
<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
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar">
|
<div class="searchBar">
|
||||||
|
@ -162,7 +165,7 @@
|
||||||
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
||||||
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
|
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.StackName }}</td>
|
<td>{{ item.StackName || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
|
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -66,8 +66,11 @@
|
||||||
>
|
>
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new" data-cy="k8sConfig-addConfigButton">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new" data-cy="k8sConfig-addConfigWithFormButton">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar">
|
<div class="searchBar">
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
<td
|
<td
|
||||||
><a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a></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>
|
<td title="{{ item.Image }}">{{ item.Image | truncate: 64 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<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-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>
|
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.StackName }}</td>
|
<td>{{ item.StackName || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
|
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -106,7 +106,7 @@
|
||||||
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
|
<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>
|
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="$ctrl.isExternalApplication(item)">external</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.StackName }}</td>
|
<td>{{ item.StackName || '-' }}</td>
|
||||||
<td title="{{ item.Image }}"
|
<td title="{{ item.Image }}"
|
||||||
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
|
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
|
||||||
>
|
>
|
||||||
|
|
|
@ -60,8 +60,11 @@
|
||||||
>
|
>
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceButton">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceWithFormButton">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar">
|
<div class="searchBar">
|
||||||
|
@ -92,6 +95,13 @@
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Name' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Name' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</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>
|
<th>
|
||||||
<a ng-click="$ctrl.changeOrderBy('Quota')">
|
<a ng-click="$ctrl.changeOrderBy('Quota')">
|
||||||
Quota
|
Quota
|
||||||
|
@ -124,6 +134,9 @@
|
||||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace.Name })">{{ item.Namespace.Name }}</a>
|
<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>
|
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
||||||
</td>
|
</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> <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>{{ item.Namespace.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
|
||||||
<td ng-if="$ctrl.isAdmin">
|
<td ng-if="$ctrl.isAdmin">
|
||||||
|
|
|
@ -38,6 +38,17 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
|
||||||
return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin);
|
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
|
* Do not allow system namespaces to be selected
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -54,6 +54,9 @@
|
||||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</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>
|
||||||
<div class="searchBar">
|
<div class="searchBar">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default class KubectlShellController {
|
||||||
this.state.shell.term.dispose();
|
this.state.shell.term.dispose();
|
||||||
this.state.shell.connected = false;
|
this.state.shell.connected = false;
|
||||||
this.TerminalWindow.terminalclose();
|
this.TerminalWindow.terminalclose();
|
||||||
|
this.$window.onresize = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
screenClear() {
|
screenClear() {
|
||||||
|
|
|
@ -1,4 +1,13 @@
|
||||||
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.connectConsole()" ng-disabled="$ctrl.state.shell.connected" data-cy="k8sSidebar-shellButton">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-xs btn-primary"
|
||||||
|
ng-click="$ctrl.connectConsole()"
|
||||||
|
ng-disabled="$ctrl.state.shell.connected"
|
||||||
|
data-cy="k8sSidebar-shellButton"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="kubernetes"
|
||||||
|
analytics-event="kubernetes-kubectl-shell"
|
||||||
|
>
|
||||||
<i class="fa fa-terminal space-right"></i> kubectl shell
|
<i class="fa fa-terminal space-right"></i> kubectl shell
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,16 @@
|
||||||
Dashboard
|
Dashboard
|
||||||
</sidebar-menu-item>
|
</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
|
<sidebar-menu-item
|
||||||
path="kubernetes.resourcePools"
|
path="kubernetes.resourcePools"
|
||||||
path-params="{ endpointId: $ctrl.endpointId }"
|
path-params="{ endpointId: $ctrl.endpointId }"
|
||||||
|
|
|
@ -56,12 +56,17 @@ class KubernetesApplicationConverter {
|
||||||
const containers = data.spec.template ? _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined) : data.spec.containers;
|
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.Id = data.metadata.uid;
|
||||||
res.Name = data.metadata.name;
|
res.Name = data.metadata.name;
|
||||||
res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-';
|
|
||||||
res.StackId = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackIdLabel] || '' : '';
|
if (data.metadata.labels) {
|
||||||
res.ApplicationKind = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationKindLabel] || '' : '';
|
const { labels } = data.metadata;
|
||||||
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] || '' : '';
|
res.StackId = labels[KubernetesPortainerApplicationStackIdLabel] ? parseInt(labels[KubernetesPortainerApplicationStackIdLabel], 10) : null;
|
||||||
|
res.StackName = labels[KubernetesPortainerApplicationStackNameLabel] || '';
|
||||||
|
res.ApplicationKind = labels[KubernetesPortainerApplicationKindLabel] || '';
|
||||||
|
res.ApplicationOwner = labels[KubernetesPortainerApplicationOwnerLabel] || '';
|
||||||
|
res.ApplicationName = labels[KubernetesPortainerApplicationNameLabel] || res.Name;
|
||||||
|
}
|
||||||
|
|
||||||
res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : '';
|
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;
|
res.ResourcePool = data.metadata.namespace;
|
||||||
if (containers.length) {
|
if (containers.length) {
|
||||||
res.Image = containers[0].image;
|
res.Image = containers[0].image;
|
||||||
|
|
61
app/kubernetes/custom-templates/index.js
Normal file
61
app/kubernetes/custom-templates/index.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import controller from './kube-create-custom-template-view.controller.js';
|
||||||
|
|
||||||
|
export const kubeCreateCustomTemplateView = {
|
||||||
|
templateUrl: './kube-create-custom-template-view.html',
|
||||||
|
controller,
|
||||||
|
};
|
|
@ -0,0 +1,169 @@
|
||||||
|
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;
|
|
@ -0,0 +1,71 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
import controller from './kube-custom-templates-view.controller.js';
|
||||||
|
|
||||||
|
export const kubeCustomTemplatesView = {
|
||||||
|
templateUrl: './kube-custom-templates-view.html',
|
||||||
|
controller,
|
||||||
|
};
|
|
@ -0,0 +1,79 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
import controller from './kube-edit-custom-template-view.controller.js';
|
||||||
|
|
||||||
|
export const kubeEditCustomTemplateView = {
|
||||||
|
templateUrl: './kube-edit-custom-template-view.html',
|
||||||
|
controller,
|
||||||
|
};
|
|
@ -0,0 +1,143 @@
|
||||||
|
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, '');
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
<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(
|
const res = _.reduce(
|
||||||
applications,
|
applications,
|
||||||
(acc, app) => {
|
(acc, app) => {
|
||||||
if (app.StackName !== '-') {
|
if (app.StackName) {
|
||||||
let stack = _.find(acc, { Name: app.StackName, ResourcePool: app.ResourcePool });
|
let stack = _.find(acc, { Name: app.StackName, ResourcePool: app.ResourcePool });
|
||||||
if (!stack) {
|
if (!stack) {
|
||||||
stack = new KubernetesStack();
|
stack = new KubernetesStack();
|
||||||
|
|
|
@ -6,9 +6,12 @@ export const KubernetesDeployManifestTypes = Object.freeze({
|
||||||
export const KubernetesDeployBuildMethods = Object.freeze({
|
export const KubernetesDeployBuildMethods = Object.freeze({
|
||||||
GIT: 1,
|
GIT: 1,
|
||||||
WEB_EDITOR: 2,
|
WEB_EDITOR: 2,
|
||||||
|
CUSTOM_TEMPLATE: 3,
|
||||||
|
URL: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const KubernetesDeployRequestMethods = Object.freeze({
|
export const KubernetesDeployRequestMethods = Object.freeze({
|
||||||
REPOSITORY: 'repository',
|
REPOSITORY: 'repository',
|
||||||
STRING: 'string',
|
STRING: 'string',
|
||||||
|
URL: 'url',
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,14 +41,11 @@ class KubernetesNamespaceService {
|
||||||
const data = await this.KubernetesNamespaces().get().$promise;
|
const data = await this.KubernetesNamespaces().get().$promise;
|
||||||
const promises = _.map(data.items, (item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise);
|
const promises = _.map(data.items, (item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise);
|
||||||
const namespaces = await $allSettled(promises);
|
const namespaces = await $allSettled(promises);
|
||||||
const visibleNamespaces = _.map(namespaces.fulfilled, (item) => {
|
const allNamespaces = _.map(namespaces.fulfilled, (item) => {
|
||||||
if (item.status.phase !== 'Terminating') {
|
return KubernetesNamespaceConverter.apiToNamespace(item);
|
||||||
return KubernetesNamespaceConverter.apiToNamespace(item);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
const res = _.without(visibleNamespaces, undefined);
|
updateNamespaces(allNamespaces);
|
||||||
updateNamespaces(res);
|
return allNamespaces;
|
||||||
return res;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new PortainerError('Unable to retrieve namespaces', err);
|
throw new PortainerError('Unable to retrieve namespaces', err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
<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,8 +5,6 @@
|
||||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||||
|
|
||||||
<div ng-if="ctrl.state.viewReady">
|
<div ng-if="ctrl.state.viewReady">
|
||||||
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12" data-cy="k8sApp-appList">
|
<div class="col-sm-12" data-cy="k8sApp-appList">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
require('../../templates/advancedDeploymentPanel.html');
|
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Stack</td>
|
<td>Stack</td>
|
||||||
<td>{{ ctrl.application.StackName }}</td>
|
<td>{{ ctrl.application.StackName || '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Namespace</td>
|
<td>Namespace</td>
|
||||||
|
@ -194,21 +194,15 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<div ng-if="!ctrl.isSystemNamespace()">
|
<div ng-if="!ctrl.isSystemNamespace()" style="margin-bottom: 15px;">
|
||||||
<button
|
<button ng-if="!ctrl.isExternalApplication()" type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.application.edit" style="margin-left: 0;">
|
||||||
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
|
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit this application
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD"
|
ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
style="margin-left: 0; margin-bottom: 15px;"
|
style="margin-left: 0;"
|
||||||
ng-click="ctrl.redeployApplication()"
|
ng-click="ctrl.redeployApplication()"
|
||||||
>
|
>
|
||||||
<i class="fa fa-redo space-right" aria-hidden="true"></i>Redeploy
|
<i class="fa fa-redo space-right" aria-hidden="true"></i>Redeploy
|
||||||
|
@ -217,12 +211,19 @@
|
||||||
ng-if="!ctrl.isExternalApplication()"
|
ng-if="!ctrl.isExternalApplication()"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
style="margin-left: 0; margin-bottom: 15px;"
|
style="margin-left: 0;"
|
||||||
ng-click="ctrl.rollbackApplication()"
|
ng-click="ctrl.rollbackApplication()"
|
||||||
ng-disabled="ctrl.application.Revisions.length < 2 || ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
|
ng-disabled="ctrl.application.Revisions.length < 2 || ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
|
||||||
>
|
>
|
||||||
<i class="fas fa-history space-right" aria-hidden="true"></i>Rollback to previous configuration
|
<i class="fas fa-history space-right" aria-hidden="true"></i>Rollback to previous configuration
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- ACCESSING APPLICATION -->
|
<!-- ACCESSING APPLICATION -->
|
||||||
|
|
|
@ -112,7 +112,8 @@ class KubernetesApplicationController {
|
||||||
KubernetesStackService,
|
KubernetesStackService,
|
||||||
KubernetesPodService,
|
KubernetesPodService,
|
||||||
KubernetesNodeService,
|
KubernetesNodeService,
|
||||||
EndpointProvider
|
EndpointProvider,
|
||||||
|
StackService
|
||||||
) {
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
|
@ -120,6 +121,7 @@ class KubernetesApplicationController {
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.LocalStorage = LocalStorage;
|
this.LocalStorage = LocalStorage;
|
||||||
this.ModalService = ModalService;
|
this.ModalService = ModalService;
|
||||||
|
this.StackService = StackService;
|
||||||
|
|
||||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||||
this.KubernetesEventService = KubernetesEventService;
|
this.KubernetesEventService = KubernetesEventService;
|
||||||
|
@ -200,6 +202,10 @@ class KubernetesApplicationController {
|
||||||
return !rule.Host && !rule.IP ? false : true;
|
return !rule.Host && !rule.IP ? false : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isStack() {
|
||||||
|
return this.application.StackId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ROLLBACK
|
* ROLLBACK
|
||||||
*/
|
*/
|
||||||
|
@ -323,6 +329,11 @@ class KubernetesApplicationController {
|
||||||
|
|
||||||
this.placements = computePlacements(nodes, this.application);
|
this.placements = computePlacements(nodes, this.application);
|
||||||
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
|
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) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||||
|
|
||||||
<div ng-if="ctrl.state.viewReady">
|
<div ng-if="ctrl.state.viewReady">
|
||||||
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<kubernetes-configurations-datatable
|
<kubernetes-configurations-datatable
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
require('../../templates/advancedDeploymentPanel.html');
|
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||||
|
|
||||||
|
|
|
@ -299,6 +299,11 @@
|
||||||
ng-click="ctrl.configure()"
|
ng-click="ctrl.configure()"
|
||||||
ng-disabled="ctrl.state.actionInProgress || !kubernetesClusterSetupForm.$valid || !ctrl.hasValidStorageConfiguration()"
|
ng-disabled="ctrl.state.actionInProgress || !kubernetesClusterSetupForm.$valid || !ctrl.hasValidStorageConfiguration()"
|
||||||
button-spinner="ctrl.state.actionInProgress"
|
button-spinner="ctrl.state.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-if="ctrl.restrictDefaultToggledOn()"
|
||||||
|
analytics-category="kubernetes"
|
||||||
|
analytics-event="kubernetes-configure"
|
||||||
|
analytics-properties="{ metadata: { restrictAccessToDefaultNamespace: ctrl.formValues.RestrictDefaultNamespace } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="ctrl.state.actionInProgress">Save configuration</span>
|
<span ng-hide="ctrl.state.actionInProgress">Save configuration</span>
|
||||||
<span ng-show="ctrl.state.actionInProgress">Saving configuration...</span>
|
<span ng-show="ctrl.state.actionInProgress">Saving configuration...</span>
|
||||||
|
|
|
@ -237,6 +237,10 @@ class KubernetesConfigureController {
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
restrictDefaultToggledOn() {
|
||||||
|
return this.formValues.RestrictDefaultNamespace && !this.oldFormValues.RestrictDefaultNamespace;
|
||||||
|
}
|
||||||
|
|
||||||
/* #region ON INIT */
|
/* #region ON INIT */
|
||||||
async onInit() {
|
async onInit() {
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -287,6 +291,8 @@ class KubernetesConfigureController {
|
||||||
ic.NeedsDeletion = false;
|
ic.NeedsDeletion = false;
|
||||||
return ic;
|
return ic;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.oldFormValues = Object.assign({}, this.formValues);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration');
|
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -23,16 +23,18 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="col-sm-12 form-section-title">
|
||||||
Build method
|
Build method
|
||||||
</div>
|
</div>
|
||||||
<box-selector radio-name="method" ng-model="ctrl.state.BuildMethod" options="ctrl.methodOptions" data-cy="k8sAppDeploy-buildSelector"></box-selector>
|
<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 -->
|
<!-- repository -->
|
||||||
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT">
|
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
@ -62,9 +64,17 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !repository -->
|
<!-- !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 -->
|
<!-- editor -->
|
||||||
<web-editor-form
|
<web-editor-form
|
||||||
ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR"
|
ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR || (ctrl.state.BuildMethod === ctrl.BuildMethods.CUSTOM_TEMPLATE && ctrl.state.templateId)"
|
||||||
identifier="kubernetes-deploy-editor"
|
identifier="kubernetes-deploy-editor"
|
||||||
value="ctrl.formValues.EditorContent"
|
value="ctrl.formValues.EditorContent"
|
||||||
on-change="(ctrl.onChangeFileContent)"
|
on-change="(ctrl.onChangeFileContent)"
|
||||||
|
@ -101,6 +111,33 @@
|
||||||
</web-editor-form>
|
</web-editor-form>
|
||||||
|
|
||||||
<!-- !editor -->
|
<!-- !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 -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
|
@ -114,6 +151,10 @@
|
||||||
ng-click="ctrl.deploy()"
|
ng-click="ctrl.deploy()"
|
||||||
button-spinner="ctrl.state.actionInProgress"
|
button-spinner="ctrl.state.actionInProgress"
|
||||||
data-cy="k8sAppDeploy-deployButton"
|
data-cy="k8sAppDeploy-deployButton"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="kubernetes"
|
||||||
|
analytics-event="kubernetes-application-advanced-deployment"
|
||||||
|
analytics-properties="ctrl.buildAnalyticsProperties()"
|
||||||
>
|
>
|
||||||
<span ng-hide="ctrl.state.actionInProgress">Deploy</span>
|
<span ng-hide="ctrl.state.actionInProgress">Deploy</span>
|
||||||
<span ng-show="ctrl.state.actionInProgress">Deployment in progress...</span>
|
<span ng-show="ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
|
import PortainerError from 'Portainer/error';
|
||||||
|
|
||||||
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods } from 'Kubernetes/models/deploy';
|
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods } from 'Kubernetes/models/deploy';
|
||||||
import { buildOption } from '@/portainer/components/box-selector';
|
import { buildOption } from '@/portainer/components/box-selector';
|
||||||
class KubernetesDeployController {
|
class KubernetesDeployController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, $window, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) {
|
constructor($async, $state, $window, Authentication, CustomTemplateService, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.$window = $window;
|
this.$window = $window;
|
||||||
|
this.Authentication = Authentication;
|
||||||
|
this.CustomTemplateService = CustomTemplateService;
|
||||||
this.ModalService = ModalService;
|
this.ModalService = ModalService;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.EndpointProvider = EndpointProvider;
|
this.EndpointProvider = EndpointProvider;
|
||||||
|
@ -18,12 +21,14 @@ class KubernetesDeployController {
|
||||||
|
|
||||||
this.deployOptions = [
|
this.deployOptions = [
|
||||||
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
|
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
|
||||||
buildOption('method_compose', 'fa fa-docker', 'Compose', 'docker-compose format', KubernetesDeployManifestTypes.COMPOSE),
|
buildOption('method_compose', 'fab fa-docker', 'Compose', 'docker-compose format', KubernetesDeployManifestTypes.COMPOSE),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.methodOptions = [
|
this.methodOptions = [
|
||||||
buildOption('method_repo', 'fab fa-github', 'Git Repository', 'Use a git repository', KubernetesDeployBuildMethods.GIT),
|
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_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 = {
|
this.state = {
|
||||||
|
@ -33,6 +38,7 @@ class KubernetesDeployController {
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
viewReady: false,
|
viewReady: false,
|
||||||
isEditorDirty: false,
|
isEditorDirty: false,
|
||||||
|
templateId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.formValues = {};
|
this.formValues = {};
|
||||||
|
@ -40,13 +46,54 @@ class KubernetesDeployController {
|
||||||
this.BuildMethods = KubernetesDeployBuildMethods;
|
this.BuildMethods = KubernetesDeployBuildMethods;
|
||||||
this.endpointId = this.EndpointProvider.endpointID();
|
this.endpointId = this.EndpointProvider.endpointID();
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onChangeTemplateId = this.onChangeTemplateId.bind(this);
|
||||||
this.deployAsync = this.deployAsync.bind(this);
|
this.deployAsync = this.deployAsync.bind(this);
|
||||||
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
||||||
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
|
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
|
||||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||||
this.onRepoUrlChange = this.onRepoUrlChange.bind(this);
|
this.onRepoUrlChange = this.onRepoUrlChange.bind(this);
|
||||||
this.onRepoRefChange = this.onRepoRefChange.bind(this);
|
this.onRepoRefChange = this.onRepoRefChange.bind(this);
|
||||||
|
this.buildAnalyticsProperties = this.buildAnalyticsProperties.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAnalyticsProperties() {
|
||||||
|
const metadata = {
|
||||||
|
type: buildLabel(this.state.BuildMethod),
|
||||||
|
format: formatLabel(this.state.DeployType),
|
||||||
|
role: roleLabel(this.Authentication.isAdmin()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state.BuildMethod === KubernetesDeployBuildMethods.GIT) {
|
||||||
|
metadata.auth = this.formValues.RepositoryAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { metadata };
|
||||||
|
|
||||||
|
function roleLabel(isAdmin) {
|
||||||
|
if (isAdmin) {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'standard';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLabel(buildMethod) {
|
||||||
|
switch (buildMethod) {
|
||||||
|
case KubernetesDeployBuildMethods.GIT:
|
||||||
|
return 'git';
|
||||||
|
case KubernetesDeployBuildMethods.WEB_EDITOR:
|
||||||
|
return 'web-editor';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLabel(format) {
|
||||||
|
switch (format) {
|
||||||
|
case KubernetesDeployManifestTypes.COMPOSE:
|
||||||
|
return 'compose';
|
||||||
|
case KubernetesDeployManifestTypes.KUBERNETES:
|
||||||
|
return 'manifest';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disableDeploy() {
|
disableDeploy() {
|
||||||
|
@ -54,10 +101,13 @@ class KubernetesDeployController {
|
||||||
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
||||||
(!this.formValues.RepositoryURL ||
|
(!this.formValues.RepositoryURL ||
|
||||||
!this.formValues.FilePathInRepository ||
|
!this.formValues.FilePathInRepository ||
|
||||||
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword)));
|
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) &&
|
||||||
const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent);
|
_.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);
|
||||||
|
|
||||||
return isGitFormInvalid || isWebEditorInvalid || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress;
|
return isGitFormInvalid || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeFormValues(values) {
|
onChangeFormValues(values) {
|
||||||
|
@ -75,6 +125,23 @@ class KubernetesDeployController {
|
||||||
this.onChangeFormValues({ RepositoryReferenceName: value });
|
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) {
|
onChangeFileContent(value) {
|
||||||
this.formValues.EditorContent = value;
|
this.formValues.EditorContent = value;
|
||||||
this.state.isEditorDirty = true;
|
this.state.isEditorDirty = true;
|
||||||
|
@ -91,10 +158,29 @@ class KubernetesDeployController {
|
||||||
this.state.actionInProgress = true;
|
this.state.actionInProgress = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const method = this.state.BuildMethod === this.BuildMethods.GIT ? KubernetesDeployRequestMethods.REPOSITORY : KubernetesDeployRequestMethods.STRING;
|
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 payload = {
|
const payload = {
|
||||||
ComposeFormat: this.state.DeployType === this.ManifestDeployTypes.COMPOSE,
|
ComposeFormat: composeFormat,
|
||||||
Namespace: this.formValues.Namespace,
|
Namespace: this.formValues.Namespace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -107,8 +193,10 @@ class KubernetesDeployController {
|
||||||
payload.RepositoryPassword = this.formValues.RepositoryPassword;
|
payload.RepositoryPassword = this.formValues.RepositoryPassword;
|
||||||
}
|
}
|
||||||
payload.FilePathInRepository = this.formValues.FilePathInRepository;
|
payload.FilePathInRepository = this.formValues.FilePathInRepository;
|
||||||
} else {
|
} else if (method === KubernetesDeployRequestMethods.STRING) {
|
||||||
payload.StackFileContent = this.formValues.EditorContent;
|
payload.StackFileContent = this.formValues.EditorContent;
|
||||||
|
} else {
|
||||||
|
payload.ManifestURL = this.formValues.ManifestURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.StackService.kubernetesDeploy(this.endpointId, method, payload);
|
await this.StackService.kubernetesDeploy(this.endpointId, method, payload);
|
||||||
|
@ -157,20 +245,27 @@ class KubernetesDeployController {
|
||||||
return this.ModalService.confirmWebEditorDiscard();
|
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() {
|
$onInit() {
|
||||||
return this.$async(this.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 '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$onDestroy() {
|
$onDestroy() {
|
||||||
|
|
|
@ -317,16 +317,16 @@
|
||||||
Registries
|
Registries
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" ng-if="!ctrl.isAdmin">
|
<div class="form-group" ng-if="!ctrl.isAdmin || ctrl.isSystem">
|
||||||
<label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
|
<label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
|
||||||
Selected registries
|
Selected registries
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-9 col-lg-4">
|
<div class="col-sm-9 col-lg-4">
|
||||||
{{ ctrl.selectedRegistries }}
|
{{ ctrl.selectedRegistries ? ctrl.selectedRegistries : 'None' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="ctrl.isAdmin">
|
<div ng-if="ctrl.isAdmin && !ctrl.isSystem">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
<p>
|
<p>
|
||||||
|
@ -405,14 +405,14 @@
|
||||||
<!-- !summary -->
|
<!-- !summary -->
|
||||||
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="col-sm-12 form-section-title">
|
<div ng-if="ctrl.isAdmin" class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="form-group">
|
<div ng-if="ctrl.isAdmin" class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
ng-if="ctrl.isEditable"
|
ng-if="!ctrl.isSystem"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
ng-disabled="!resourcePoolEditForm.$valid || ctrl.isUpdateButtonDisabled()"
|
ng-disabled="!resourcePoolEditForm.$valid || ctrl.isUpdateButtonDisabled()"
|
||||||
ng-click="ctrl.updateResourcePool()"
|
ng-click="ctrl.updateResourcePool()"
|
||||||
|
@ -421,7 +421,13 @@
|
||||||
<span ng-hide="ctrl.state.actionInProgress" data-cy="k8sNamespaceEdit-updateNamespaceButton">Update namespace</span>
|
<span ng-hide="ctrl.state.actionInProgress" data-cy="k8sNamespaceEdit-updateNamespaceButton">Update namespace</span>
|
||||||
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
|
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-click="ctrl.markUnmarkAsSystem()" button-spinner="ctrl.state.actionInProgress">
|
<button
|
||||||
|
ng-if="!ctrl.isDefaultNamespace"
|
||||||
|
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">Unmark as system</span>
|
||||||
<span ng-if="!ctrl.isSystem">Mark as system</span>
|
<span ng-if="!ctrl.isSystem">Mark as system</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -325,7 +325,7 @@ class KubernetesResourcePoolController {
|
||||||
this.formValues.Registries.push(reg);
|
this.formValues.Registries.push(reg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.selectedRegistries = this.formValues.Registries.map((r) => r.Name).join(', ');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||||
|
|
||||||
<div ng-if="ctrl.state.viewReady">
|
<div ng-if="ctrl.state.viewReady">
|
||||||
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
require('../../templates/advancedDeploymentPanel.html');
|
|
||||||
|
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
<!-- !icon-url-input -->
|
<!-- !icon-url-input -->
|
||||||
|
|
||||||
<!-- platform-input -->
|
<!-- platform-input -->
|
||||||
<div class="form-group">
|
<div ng-if="$ctrl.showPlatformField" class="form-group">
|
||||||
<label for="platform" class="col-sm-3 col-lg-2 control-label text-left">Platform</label>
|
<label for="platform" class="col-sm-3 col-lg-2 control-label text-left">Platform</label>
|
||||||
<div class="col-sm-9 col-lg-10">
|
<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>
|
<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 -->
|
||||||
|
|
||||||
<!-- platform-input -->
|
<!-- platform-input -->
|
||||||
<div class="form-group">
|
<div ng-if="$ctrl.showTypeField" class="form-group">
|
||||||
<label for="platform" class="col-sm-3 col-lg-2 control-label text-left">Type</label>
|
<label for="platform" class="col-sm-3 col-lg-2 control-label text-left">Type</label>
|
||||||
<div class="col-sm-9 col-lg-10">
|
<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>
|
<select class="form-control" ng-model="$ctrl.formValues.Type" ng-options="+(opt.value) as opt.label for opt in $ctrl.templateTypes"> </select>
|
||||||
|
|
|
@ -5,5 +5,7 @@ angular.module('portainer.app').component('customTemplateCommonFields', {
|
||||||
controller: CustomTemplateCommonFieldsController,
|
controller: CustomTemplateCommonFieldsController,
|
||||||
bindings: {
|
bindings: {
|
||||||
formValues: '=',
|
formValues: '=',
|
||||||
|
showPlatformField: '<',
|
||||||
|
showTypeField: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,8 +8,8 @@ class CustomTemplateSelectorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleChangeTemplate(templateId) {
|
async handleChangeTemplate(templateId) {
|
||||||
this.selectedTemplate = this.templates.find((t) => t.id === templateId);
|
this.selectedTemplate = this.templates.find((t) => t.Id === templateId);
|
||||||
this.onChange(templateId);
|
this.onChange(templateId, this.selectedTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
$onChanges({ value }) {
|
$onChanges({ value }) {
|
||||||
|
|
|
@ -5,9 +5,7 @@
|
||||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar">
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.templates.custom.new">
|
<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>
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add Custom Template
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="searchBar" style="border-top: 2px solid #f6f6f6;">
|
<div class="searchBar" style="border-top: 2px solid #f6f6f6;">
|
||||||
|
@ -27,12 +25,12 @@
|
||||||
<template-item
|
<template-item
|
||||||
ng-repeat="template in $ctrl.templates | filter:$ctrl.state.textFilter"
|
ng-repeat="template in $ctrl.templates | filter:$ctrl.state.textFilter"
|
||||||
model="template"
|
model="template"
|
||||||
type-label="{{ template.Type === 1 ? 'swarm' : 'standalone' }}"
|
type-label="{{ $ctrl.typeLabel(template.Type) }}"
|
||||||
on-select="($ctrl.onSelectClick)"
|
on-select="($ctrl.onSelectClick)"
|
||||||
>
|
>
|
||||||
<template-item-actions>
|
<template-item-actions>
|
||||||
<div ng-if="$ctrl.isEditAllowed(template)" style="display: flex;">
|
<div ng-if="$ctrl.isEditAllowed(template)" style="display: flex;">
|
||||||
<a ui-sref="docker.templates.custom.edit({id: template.Id})" ng-click="$event.stopPropagation();" class="btn btn-primary btn-xs" style="margin-right: 10px;">
|
<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;">
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-danger btn-xs" ng-click="$ctrl.onDeleteClick(template.Id); $event.stopPropagation();">Delete</button>
|
<button class="btn btn-danger btn-xs" ng-click="$ctrl.onDeleteClick(template.Id); $event.stopPropagation();">Delete</button>
|
||||||
|
|
|
@ -1,7 +1,28 @@
|
||||||
|
const CUSTOM_TEMPLATES_TYPES = {
|
||||||
|
STANDALONE: 1,
|
||||||
|
SWARM: 2,
|
||||||
|
KUBERNETES: 3,
|
||||||
|
};
|
||||||
|
|
||||||
angular.module('portainer.docker').controller('CustomTemplatesListController', function ($scope, $controller, DatatableService) {
|
angular.module('portainer.docker').controller('CustomTemplatesListController', function ($scope, $controller, DatatableService) {
|
||||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||||
|
|
||||||
this.$onInit = function () {
|
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.setDefaults();
|
this.setDefaults();
|
||||||
this.prepareTableFromDataset();
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
|
@ -32,5 +53,5 @@ angular.module('portainer.docker').controller('CustomTemplatesListController', f
|
||||||
this.settings.open = false;
|
this.settings.open = false;
|
||||||
}
|
}
|
||||||
this.onSettingsRepeaterChange();
|
this.onSettingsRepeaterChange();
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,5 +12,7 @@ angular.module('portainer.app').component('customTemplatesList', {
|
||||||
showSwarmStacks: '<',
|
showSwarmStacks: '<',
|
||||||
onDeleteClick: '<',
|
onDeleteClick: '<',
|
||||||
isEditAllowed: '<',
|
isEditAllowed: '<',
|
||||||
|
createPath: '@',
|
||||||
|
editPath: '@',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
export const fileUploadForm = {
|
||||||
|
templateUrl: './file-upload-form.html',
|
||||||
|
|
||||||
|
bindings: {
|
||||||
|
file: '<',
|
||||||
|
ngRequired: '<',
|
||||||
|
onChange: '<',
|
||||||
|
},
|
||||||
|
|
||||||
|
transclude: {
|
||||||
|
description: '?fileUploadDescription',
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { webEditorForm } from './web-editor-form';
|
import { webEditorForm } from './web-editor-form';
|
||||||
|
import { fileUploadForm } from './file-upload-form';
|
||||||
|
|
||||||
export default angular.module('portainer.app.components.form', []).component('webEditorForm', webEditorForm).name;
|
export default angular.module('portainer.app.components.form', []).component('webEditorForm', webEditorForm).component('fileUploadForm', fileUploadForm).name;
|
||||||
|
|
|
@ -71,7 +71,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormAzure.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormAzure.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'azure' } }"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -93,7 +93,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormCustom.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormCustom.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'custom' }}"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -54,7 +54,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'dockerhub' } }"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -133,6 +133,10 @@
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
ng-disabled="$ctrl.actionInProgress || !$ctrl.state.gitlab.selectedItemCount"
|
ng-disabled="$ctrl.actionInProgress || !$ctrl.state.gitlab.selectedItemCount"
|
||||||
button-spinner="$ctrl.actionInProgress"
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'gitlab' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">Create registries</span>
|
<span ng-hide="$ctrl.actionInProgress">Create registries</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
|
|
|
@ -100,7 +100,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormProGet.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormProGet.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'proget' } }"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -67,7 +67,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormQuay.$valid" button-spinner="$ctrl.actionInProgress">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="$ctrl.actionInProgress || !registryFormQuay.$valid"
|
||||||
|
button-spinner="$ctrl.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-registry-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'quay' } }"
|
||||||
|
>
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -25,7 +25,7 @@ class StackRedeployGitFormController {
|
||||||
RepositoryUsername: '',
|
RepositoryUsername: '',
|
||||||
RepositoryPassword: '',
|
RepositoryPassword: '',
|
||||||
Env: [],
|
Env: [],
|
||||||
// auto upadte
|
// auto update
|
||||||
AutoUpdate: {
|
AutoUpdate: {
|
||||||
RepositoryAutomaticUpdates: false,
|
RepositoryAutomaticUpdates: false,
|
||||||
RepositoryMechanism: 'Interval',
|
RepositoryMechanism: 'Interval',
|
||||||
|
@ -38,6 +38,26 @@ class StackRedeployGitFormController {
|
||||||
this.onChangeRef = this.onChangeRef.bind(this);
|
this.onChangeRef = this.onChangeRef.bind(this);
|
||||||
this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this);
|
this.onChangeAutoUpdate = this.onChangeAutoUpdate.bind(this);
|
||||||
this.onChangeEnvVar = this.onChangeEnvVar.bind(this);
|
this.onChangeEnvVar = this.onChangeEnvVar.bind(this);
|
||||||
|
this.handleEnvVarChange = this.handleEnvVarChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAnalyticsProperties() {
|
||||||
|
const metadata = {};
|
||||||
|
|
||||||
|
if (this.formValues.RepositoryAutomaticUpdates) {
|
||||||
|
metadata.automaticUpdates = autoSyncLabel(this.formValues.RepositoryMechanism);
|
||||||
|
}
|
||||||
|
return { metadata };
|
||||||
|
|
||||||
|
function autoSyncLabel(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'Interval':
|
||||||
|
return 'polling';
|
||||||
|
case 'Webhook':
|
||||||
|
return 'webhook';
|
||||||
|
}
|
||||||
|
return 'off';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(values) {
|
onChange(values) {
|
||||||
|
@ -100,10 +120,17 @@ class StackRedeployGitFormController {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
try {
|
try {
|
||||||
this.state.inProgress = true;
|
this.state.inProgress = true;
|
||||||
await this.StackService.updateGitStackSettings(this.stack.Id, this.stack.EndpointId, this.FormHelper.removeInvalidEnvVars(this.formValues.Env), this.formValues);
|
const stack = 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.savedFormValues = angular.copy(this.formValues);
|
||||||
this.state.hasUnsavedChanges = false;
|
this.state.hasUnsavedChanges = false;
|
||||||
this.Notifications.success('Save stack settings successfully');
|
this.Notifications.success('Save stack settings successfully');
|
||||||
|
|
||||||
|
this.stack = stack;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to save stack settings');
|
this.Notifications.error('Failure', err, 'Unable to save stack settings');
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -116,6 +143,16 @@ class StackRedeployGitFormController {
|
||||||
return this.state.inProgress || this.state.redeployInProgress;
|
return this.state.inProgress || this.state.redeployInProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEnvVarChange(value) {
|
||||||
|
this.formValues.Env = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAutoUpdateChanged() {
|
||||||
|
const wasEnabled = !!(this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook));
|
||||||
|
const isEnabled = this.formValues.AutoUpdate.RepositoryAutomaticUpdates;
|
||||||
|
return isEnabled !== wasEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
this.formValues.RefName = this.model.ReferenceName;
|
this.formValues.RefName = this.model.ReferenceName;
|
||||||
this.formValues.Env = this.stack.Env;
|
this.formValues.Env = this.stack.Env;
|
||||||
|
|
|
@ -41,8 +41,9 @@
|
||||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
||||||
style="margin-top: 7px; margin-left: 0;"
|
style="margin-top: 7px; margin-left: 0;"
|
||||||
button-spinner="$ctrl.state.redeployInProgress"
|
button-spinner="$ctrl.state.redeployInProgress"
|
||||||
style="margin-top: 7px; margin-left: 0;"
|
analytics-on
|
||||||
button-spinner="$ctrl.state.inProgress"
|
analytics-event="docker-stack-pull-redeploy"
|
||||||
|
analytics-category="docker"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and redeploy </span>
|
<span ng-hide="$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and redeploy </span>
|
||||||
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
|
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
|
||||||
|
@ -54,6 +55,10 @@
|
||||||
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges || !$ctrl.redeployGitForm.$valid"
|
||||||
style="margin-top: 7px; margin-left: 0;"
|
style="margin-top: 7px; margin-left: 0;"
|
||||||
button-spinner="$ctrl.state.inProgress"
|
button-spinner="$ctrl.state.inProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-event="docker-stack-update-git-settings"
|
||||||
|
analytics-category="docker"
|
||||||
|
analytics-properties="$ctrl.buildAnalyticsProperties()"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.state.inProgress"> Save settings </span>
|
<span ng-hide="$ctrl.state.inProgress"> Save settings </span>
|
||||||
<span ng-show="$ctrl.state.inProgress">In progress...</span>
|
<span ng-show="$ctrl.state.inProgress">In progress...</span>
|
||||||
|
|
|
@ -18,11 +18,14 @@
|
||||||
{{ $ctrl.model.Title }}
|
{{ $ctrl.model.Title }}
|
||||||
</span>
|
</span>
|
||||||
<span class="space-left blocklist-item-subtitle">
|
<span class="space-left blocklist-item-subtitle">
|
||||||
<span>
|
<span ng-if="$ctrl.model.Type != 3">
|
||||||
<i class="fab fa-linux" aria-hidden="true" ng-if="$ctrl.model.Platform === 1 || $ctrl.model.Platform === 'linux' || !$ctrl.model.Platform"></i>
|
<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>
|
<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>
|
<i class="fab fa-windows" aria-hidden="true" ng-if="$ctrl.model.Platform === 2 || $ctrl.model.Platform === 'windows' || !$ctrl.model.Platform"></i>
|
||||||
</span>
|
</span>
|
||||||
|
<span ng-if="$ctrl.model.Type === 3">
|
||||||
|
<i class="fa fa-dharmachakra" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ $ctrl.typeLabel }}
|
{{ $ctrl.typeLabel }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -51,6 +51,10 @@ class SslCertificateController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wasHTTPsChanged() {
|
||||||
|
return this.originalValues.forceHTTPS !== this.formValues.forceHTTPS;
|
||||||
|
}
|
||||||
|
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -80,6 +80,11 @@
|
||||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.isFormChanged()"
|
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.isFormChanged()"
|
||||||
ng-click="$ctrl.save()"
|
ng-click="$ctrl.save()"
|
||||||
button-spinner="$ctrl.state.actionInProgress"
|
button-spinner="$ctrl.state.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-if="$ctrl.wasHTTPsChanged()"
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-settings-edit"
|
||||||
|
analytics-properties="{ metadata: { forceHTTPS: $ctrl.formValues.forceHTTPS } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="$ctrl.state.actionInProgress || $ctrl.state.reloadingPage">Apply Changes</span>
|
<span ng-hide="$ctrl.state.actionInProgress || $ctrl.state.reloadingPage">Apply Changes</span>
|
||||||
<span ng-show="$ctrl.state.actionInProgress">Saving in progress...</span>
|
<span ng-show="$ctrl.state.actionInProgress">Saving in progress...</span>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal" name="customTemplateForm">
|
<form class="form-horizontal" name="customTemplateForm">
|
||||||
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
|
<custom-template-common-fields form-values="$ctrl.formValues" show-platform-field="true" show-type-field="true"></custom-template-common-fields>
|
||||||
|
|
||||||
<!-- build-method -->
|
<!-- build-method -->
|
||||||
<div ng-if="!$ctrl.state.fromStack">
|
<div ng-if="!$ctrl.state.fromStack">
|
||||||
|
|
|
@ -175,7 +175,7 @@ class CreateCustomTemplateViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.templates = await this.CustomTemplateService.customTemplates();
|
this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
title-icon="fa-rocket"
|
title-icon="fa-rocket"
|
||||||
templates="$ctrl.templates"
|
templates="$ctrl.templates"
|
||||||
table-key="customTemplates"
|
table-key="customTemplates"
|
||||||
|
create-path="docker.templates.custom.new"
|
||||||
is-edit-allowed="$ctrl.isEditAllowed"
|
is-edit-allowed="$ctrl.isEditAllowed"
|
||||||
on-select-click="($ctrl.selectTemplate)"
|
on-select-click="($ctrl.selectTemplate)"
|
||||||
on-delete-click="($ctrl.confirmDelete)"
|
on-delete-click="($ctrl.confirmDelete)"
|
||||||
|
|
|
@ -81,16 +81,12 @@ class CustomTemplatesViewController {
|
||||||
return this.currentUser.isAdmin || this.currentUser.id === template.CreatedByUserId;
|
return this.currentUser.isAdmin || this.currentUser.id === template.CreatedByUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTemplates(endpointMode) {
|
getTemplates() {
|
||||||
return this.$async(this.getTemplatesAsync, endpointMode);
|
return this.$async(this.getTemplatesAsync);
|
||||||
}
|
}
|
||||||
async getTemplatesAsync({ provider, role }) {
|
async getTemplatesAsync() {
|
||||||
try {
|
try {
|
||||||
let stackType = 2;
|
this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
|
||||||
if (provider === 'DOCKER_SWARM_MODE' && role === 'MANAGER') {
|
|
||||||
stackType = 1;
|
|
||||||
}
|
|
||||||
this.templates = await this.CustomTemplateService.customTemplates(stackType);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failed loading templates', err, 'Unable to load custom templates');
|
this.Notifications.error('Failed loading templates', err, 'Unable to load custom templates');
|
||||||
}
|
}
|
||||||
|
@ -237,7 +233,6 @@ class CustomTemplatesViewController {
|
||||||
case 2:
|
case 2:
|
||||||
deployable = endpoint.mode.provider === this.DOCKER_STANDALONE;
|
deployable = endpoint.mode.provider === this.DOCKER_STANDALONE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return deployable;
|
return deployable;
|
||||||
|
@ -251,7 +246,7 @@ class CustomTemplatesViewController {
|
||||||
apiVersion,
|
apiVersion,
|
||||||
} = applicationState;
|
} = applicationState;
|
||||||
|
|
||||||
this.getTemplates(endpointMode);
|
this.getTemplates();
|
||||||
this.getNetworks(endpointMode.provider, apiVersion);
|
this.getNetworks(endpointMode.provider, apiVersion);
|
||||||
|
|
||||||
this.currentUser.isAdmin = this.Authentication.isAdmin();
|
this.currentUser.isAdmin = this.Authentication.isAdmin();
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal" name="customTemplateForm">
|
<form class="form-horizontal" name="customTemplateForm">
|
||||||
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
|
<custom-template-common-fields form-values="$ctrl.formValues" show-platform-field="true" show-type-field="true"></custom-template-common-fields>
|
||||||
|
|
||||||
<!-- web-editor -->
|
<!-- web-editor -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -112,7 +112,7 @@ class EditCustomTemplateViewController {
|
||||||
this.getTemplate();
|
this.getTemplate();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.templates = await this.CustomTemplateService.customTemplates();
|
this.templates = await this.CustomTemplateService.customTemplates([1, 2]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
this.Notifications.error('Failure loading', err, 'Failed loading custom templates');
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ angular
|
||||||
.module('portainer.app')
|
.module('portainer.app')
|
||||||
.controller('CreateEndpointController', function CreateEndpointController(
|
.controller('CreateEndpointController', function CreateEndpointController(
|
||||||
$async,
|
$async,
|
||||||
|
$analytics,
|
||||||
$q,
|
$q,
|
||||||
$scope,
|
$scope,
|
||||||
$state,
|
$state,
|
||||||
|
@ -167,16 +168,32 @@ angular
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addAgentEndpoint = function () {
|
$scope.addAgentEndpoint = addAgentEndpoint;
|
||||||
var name = $scope.formValues.Name;
|
async function addAgentEndpoint() {
|
||||||
// var URL = $filter('stripprotocol')($scope.formValues.URL);
|
return $async(async () => {
|
||||||
var URL = $scope.formValues.URL;
|
const name = $scope.formValues.Name;
|
||||||
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
const URL = $scope.formValues.URL;
|
||||||
var groupId = $scope.formValues.GroupId;
|
const publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
||||||
var tagIds = $scope.formValues.TagIds;
|
const groupId = $scope.formValues.GroupId;
|
||||||
|
const tagIds = $scope.formValues.TagIds;
|
||||||
|
|
||||||
addEndpoint(name, PortainerEndpointCreationTypes.AgentEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null);
|
const endpoint = await addEndpoint(name, PortainerEndpointCreationTypes.AgentEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null);
|
||||||
};
|
$analytics.eventTrack('portainer-endpoint-creation', { category: 'portainer', metadata: { type: 'agent', platform: platformLabel(endpoint.Type) } });
|
||||||
|
});
|
||||||
|
|
||||||
|
function platformLabel(type) {
|
||||||
|
switch (type) {
|
||||||
|
case PortainerEndpointTypes.DockerEnvironment:
|
||||||
|
case PortainerEndpointTypes.AgentOnDockerEnvironment:
|
||||||
|
case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
|
||||||
|
return 'docker';
|
||||||
|
case PortainerEndpointTypes.KubernetesLocalEnvironment:
|
||||||
|
case PortainerEndpointTypes.AgentOnKubernetesEnvironment:
|
||||||
|
case PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment:
|
||||||
|
return 'kubernetes';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$scope.addEdgeAgentEndpoint = function () {
|
$scope.addEdgeAgentEndpoint = function () {
|
||||||
var name = $scope.formValues.Name;
|
var name = $scope.formValues.Name;
|
||||||
|
@ -213,24 +230,26 @@ angular
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) {
|
async function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) {
|
||||||
$scope.state.actionInProgress = true;
|
return $async(async () => {
|
||||||
EndpointService.createRemoteEndpoint(
|
$scope.state.actionInProgress = true;
|
||||||
name,
|
try {
|
||||||
creationType,
|
const endpoint = await EndpointService.createRemoteEndpoint(
|
||||||
URL,
|
name,
|
||||||
PublicURL,
|
creationType,
|
||||||
groupId,
|
URL,
|
||||||
tagIds,
|
PublicURL,
|
||||||
TLS,
|
groupId,
|
||||||
TLSSkipVerify,
|
tagIds,
|
||||||
TLSSkipClientVerify,
|
TLS,
|
||||||
TLSCAFile,
|
TLSSkipVerify,
|
||||||
TLSCertFile,
|
TLSSkipClientVerify,
|
||||||
TLSKeyFile,
|
TLSCAFile,
|
||||||
CheckinInterval
|
TLSCertFile,
|
||||||
)
|
TLSKeyFile,
|
||||||
.then(function success(endpoint) {
|
CheckinInterval
|
||||||
|
);
|
||||||
|
|
||||||
Notifications.success('Endpoint created', name);
|
Notifications.success('Endpoint created', name);
|
||||||
switch (endpoint.Type) {
|
switch (endpoint.Type) {
|
||||||
case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
|
case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
|
||||||
|
@ -244,13 +263,14 @@ angular
|
||||||
$state.go('portainer.endpoints', {}, { reload: true });
|
$state.go('portainer.endpoints', {}, { reload: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
return endpoint;
|
||||||
|
} catch (err) {
|
||||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||||
})
|
} finally {
|
||||||
.finally(function final() {
|
|
||||||
$scope.state.actionInProgress = false;
|
$scope.state.actionInProgress = false;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
|
|
|
@ -482,6 +482,10 @@
|
||||||
ng-click="addDockerEndpoint()"
|
ng-click="addDockerEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
data-cy="endpointCreate-createDockerEndpoint"
|
data-cy="endpointCreate-createDockerEndpoint"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-endpoint-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'docker-api' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
|
@ -506,6 +510,10 @@
|
||||||
ng-click="addEdgeAgentEndpoint()"
|
ng-click="addEdgeAgentEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
data-cy="endpointCreate-createEdgeAgentEndpoint"
|
data-cy="endpointCreate-createEdgeAgentEndpoint"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-endpoint-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'edge-agent' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
|
@ -517,6 +525,10 @@
|
||||||
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
|
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
|
||||||
ng-click="addKubernetesEndpoint()"
|
ng-click="addKubernetesEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-endpoint-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'kubernetes-api' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
|
@ -529,6 +541,10 @@
|
||||||
ng-click="addAzureEndpoint()"
|
ng-click="addAzureEndpoint()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
data-cy="endpointCreate-createAzureEndpoint"
|
data-cy="endpointCreate-createAzureEndpoint"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="portainer"
|
||||||
|
analytics-event="portainer-endpoint-creation"
|
||||||
|
analytics-properties="{ metadata: { type: 'azure-api' } }"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
|
||||||
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
<span ng-show="state.actionInProgress">Creating endpoint...</span>
|
||||||
|
|
|
@ -23,7 +23,16 @@
|
||||||
Edge identifier: <code>{{ endpoint.EdgeID }}</code>
|
Edge identifier: <code>{{ endpoint.EdgeID }}</code>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress" ng-click="onDeassociateEndpoint()" button-spinner="state.actionInProgress">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="state.actionInProgress"
|
||||||
|
ng-click="onDeassociateEndpoint()"
|
||||||
|
button-spinner="state.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-event="edge-endpoint-deassociate"
|
||||||
|
analytics-category="edge"
|
||||||
|
>
|
||||||
<span ng-hide="state.actionInProgress">De-associate</span>
|
<span ng-hide="state.actionInProgress">De-associate</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -26,6 +26,7 @@ angular
|
||||||
clipboard
|
clipboard
|
||||||
) {
|
) {
|
||||||
$scope.onChangeTemplateId = onChangeTemplateId;
|
$scope.onChangeTemplateId = onChangeTemplateId;
|
||||||
|
$scope.buildAnalyticsProperties = buildAnalyticsProperties;
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
|
@ -54,6 +55,8 @@ angular
|
||||||
editorYamlValidationError: '',
|
editorYamlValidationError: '',
|
||||||
uploadYamlValidationError: '',
|
uploadYamlValidationError: '',
|
||||||
isEditorDirty: false,
|
isEditorDirty: false,
|
||||||
|
selectedTemplate: null,
|
||||||
|
selectedTemplateId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
$window.onbeforeunload = () => {
|
$window.onbeforeunload = () => {
|
||||||
|
@ -76,6 +79,47 @@ angular
|
||||||
$scope.formValues.AdditionalFiles.splice(index, 1);
|
$scope.formValues.AdditionalFiles.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildAnalyticsProperties() {
|
||||||
|
const metadata = { type: methodLabel($scope.state.Method) };
|
||||||
|
|
||||||
|
if ($scope.state.Method === 'repository') {
|
||||||
|
metadata.automaticUpdates = 'off';
|
||||||
|
if ($scope.formValues.RepositoryAutomaticUpdates) {
|
||||||
|
metadata.automaticUpdates = autoSyncLabel($scope.formValues.RepositoryMechanism);
|
||||||
|
}
|
||||||
|
metadata.auth = $scope.formValues.RepositoryAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.state.Method === 'template') {
|
||||||
|
metadata.templateName = $scope.state.selectedTemplate.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { metadata };
|
||||||
|
|
||||||
|
function methodLabel(method) {
|
||||||
|
switch (method) {
|
||||||
|
case 'editor':
|
||||||
|
return 'web-editor';
|
||||||
|
case 'repository':
|
||||||
|
return 'git';
|
||||||
|
case 'upload':
|
||||||
|
return 'file-upload';
|
||||||
|
case 'template':
|
||||||
|
return 'custom-template';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoSyncLabel(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'Interval':
|
||||||
|
return 'polling';
|
||||||
|
case 'Webhook':
|
||||||
|
return 'webhook';
|
||||||
|
}
|
||||||
|
return 'off';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validateForm(accessControlData, isAdmin) {
|
function validateForm(accessControlData, isAdmin) {
|
||||||
$scope.state.formValidationError = '';
|
$scope.state.formValidationError = '';
|
||||||
var error = '';
|
var error = '';
|
||||||
|
@ -238,10 +282,11 @@ angular
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function onChangeTemplateId(templateId) {
|
function onChangeTemplateId(templateId, template) {
|
||||||
return $async(async () => {
|
return $async(async () => {
|
||||||
try {
|
try {
|
||||||
$scope.state.templateId = templateId;
|
$scope.state.selectedTemplateId = templateId;
|
||||||
|
$scope.state.selectedTemplate = template;
|
||||||
|
|
||||||
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
|
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
|
||||||
$scope.onChangeFileContent(fileContent);
|
$scope.onChangeFileContent(fileContent);
|
||||||
|
|
|
@ -120,11 +120,11 @@
|
||||||
new-template-path="docker.templates.custom.new"
|
new-template-path="docker.templates.custom.new"
|
||||||
stack-type="state.StackType"
|
stack-type="state.StackType"
|
||||||
on-change="(onChangeTemplateId)"
|
on-change="(onChangeTemplateId)"
|
||||||
value="state.templateId"
|
value="state.selectedTemplateId"
|
||||||
></custom-template-selector>
|
></custom-template-selector>
|
||||||
|
|
||||||
<web-editor-form
|
<web-editor-form
|
||||||
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.templateId)"
|
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.selectedTemplateId)"
|
||||||
identifier="stack-creation-editor"
|
identifier="stack-creation-editor"
|
||||||
value="formValues.StackFileContent"
|
value="formValues.StackFileContent"
|
||||||
on-change="(onChangeFileContent)"
|
on-change="(onChangeFileContent)"
|
||||||
|
@ -164,6 +164,10 @@
|
||||||
|| !formValues.Name"
|
|| !formValues.Name"
|
||||||
ng-click="deployStack()"
|
ng-click="deployStack()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="docker"
|
||||||
|
analytics-event="docker-stack-create"
|
||||||
|
analytics-properties="buildAnalyticsProperties()"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress">Deploy the stack</span>
|
<span ng-hide="state.actionInProgress">Deploy the stack</span>
|
||||||
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
<span ng-show="state.actionInProgress">Deployment in progress...</span>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue