mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 12:25:22 +02:00
resolve conflicts
This commit is contained in:
commit
0ee42f76c3
84 changed files with 1475 additions and 205 deletions
|
@ -105,9 +105,10 @@ type customTemplateFromFileContentPayload struct {
|
|||
Note string `example:"This is my <b>custom</b> template"`
|
||||
// Platform associated to the template.
|
||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
|
||||
// Type of created stack (1 - swarm, 2 - compose)
|
||||
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
||||
// Required for Docker stacks
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
|
||||
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||
// Content of stack file
|
||||
FileContent string `validate:"required"`
|
||||
}
|
||||
|
@ -122,10 +123,10 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
|
|||
if govalidator.IsNull(payload.FileContent) {
|
||||
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")
|
||||
}
|
||||
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 nil
|
||||
|
@ -171,7 +172,8 @@ type customTemplateFromGitRepositoryPayload struct {
|
|||
Note string `example:"This is my <b>custom</b> template"`
|
||||
// Platform associated to the template.
|
||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||
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 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) {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
if payload.Type == portainer.KubernetesStack {
|
||||
return errors.New("Creating a Kubernetes custom template from git is not supported")
|
||||
}
|
||||
|
||||
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
|
@ -278,20 +285,21 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
|
|||
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
|
||||
payload.Note = note
|
||||
|
||||
platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true)
|
||||
templatePlatform := portainer.CustomTemplatePlatform(platform)
|
||||
if templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
payload.Platform = templatePlatform
|
||||
|
||||
typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true)
|
||||
templateType := portainer.StackType(typeNumeral)
|
||||
if templateType != portainer.DockerComposeStack && templateType != portainer.DockerSwarmStack {
|
||||
if templateType != portainer.KubernetesStack && templateType != portainer.DockerSwarmStack && templateType != portainer.DockerComposeStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
payload.Type = templateType
|
||||
|
||||
platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true)
|
||||
templatePlatform := portainer.CustomTemplatePlatform(platform)
|
||||
if templateType != portainer.KubernetesStack && templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
|
||||
payload.Platform = templatePlatform
|
||||
|
||||
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "File")
|
||||
if err != nil {
|
||||
return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
|
||||
|
|
|
@ -2,7 +2,9 @@ package customtemplates
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
@ -17,10 +19,16 @@ import (
|
|||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param type query []int true "Template types" Enums(1,2,3)
|
||||
// @success 200 {array} portainer.CustomTemplate "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /custom_templates [get]
|
||||
func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
templateTypes, err := parseTemplateTypes(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template type", err}
|
||||
}
|
||||
|
||||
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
|
||||
|
@ -52,5 +60,52 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
|
|||
customTemplates = authorization.FilterAuthorizedCustomTemplates(customTemplates, user, userTeamIDs)
|
||||
}
|
||||
|
||||
customTemplates = filterByType(customTemplates, templateTypes)
|
||||
|
||||
return response.JSON(w, customTemplates)
|
||||
}
|
||||
|
||||
func parseTemplateTypes(r *http.Request) ([]portainer.StackType, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to parse request params")
|
||||
}
|
||||
|
||||
types, exist := r.Form["type"]
|
||||
if !exist {
|
||||
return []portainer.StackType{}, nil
|
||||
}
|
||||
|
||||
res := []portainer.StackType{}
|
||||
for _, templateTypeStr := range types {
|
||||
templateType, err := strconv.Atoi(templateTypeStr)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed parsing template type")
|
||||
}
|
||||
|
||||
res = append(res, portainer.StackType(templateType))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func filterByType(customTemplates []portainer.CustomTemplate, templateTypes []portainer.StackType) []portainer.CustomTemplate {
|
||||
if len(templateTypes) == 0 {
|
||||
return customTemplates
|
||||
}
|
||||
|
||||
typeSet := map[portainer.StackType]bool{}
|
||||
for _, templateType := range templateTypes {
|
||||
typeSet[templateType] = true
|
||||
}
|
||||
|
||||
filtered := []portainer.CustomTemplate{}
|
||||
|
||||
for _, template := range customTemplates {
|
||||
if typeSet[template.Type] {
|
||||
filtered = append(filtered, template)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
|
|
@ -27,9 +27,10 @@ type customTemplateUpdatePayload struct {
|
|||
Note string `example:"This is my <b>custom</b> template"`
|
||||
// Platform associated to the template.
|
||||
// Valid values are: 1 - 'linux', 2 - 'windows'
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2" validate:"required"`
|
||||
// Type of created stack (1 - swarm, 2 - compose)
|
||||
Type portainer.StackType `example:"1" enums:"1,2" validate:"required"`
|
||||
// Required for Docker stacks
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
|
||||
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||
// Content of stack file
|
||||
FileContent string `validate:"required"`
|
||||
}
|
||||
|
@ -41,10 +42,10 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
|||
if govalidator.IsNull(payload.FileContent) {
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
if govalidator.IsNull(payload.Description) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
)
|
||||
|
||||
|
@ -38,6 +39,12 @@ type kubernetesGitDeploymentPayload struct {
|
|||
FilePathInRepository string
|
||||
}
|
||||
|
||||
type kubernetesManifestURLDeploymentPayload struct {
|
||||
Namespace string
|
||||
ComposeFormat bool
|
||||
ManifestURL string
|
||||
}
|
||||
|
||||
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return errors.New("Invalid stack file content")
|
||||
|
@ -67,6 +74,13 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
|
||||
return errors.New("Invalid manifest URL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type createKubernetesStackResponse struct {
|
||||
Output string `json:"Output"`
|
||||
}
|
||||
|
@ -132,6 +146,8 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
|||
Output: output,
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
|
||||
|
@ -209,6 +225,70 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
|||
resp := &createKubernetesStackResponse{
|
||||
Output: output,
|
||||
}
|
||||
|
||||
doCleanUp = false
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
|
||||
func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, 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)
|
||||
}
|
||||
|
||||
|
@ -231,7 +311,6 @@ func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *p
|
|||
}
|
||||
|
||||
return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
case "repository":
|
||||
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)}
|
||||
}
|
||||
|
|
|
@ -135,21 +135,25 @@ func (handler *Handler) hijackPodExecStartOperation(
|
|||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer stdoutWriter.Close()
|
||||
|
||||
// errorChan is used to propagate errors from the go routines to the caller.
|
||||
errorChan := make(chan error, 1)
|
||||
go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
|
||||
go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
|
||||
|
||||
err = cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err}
|
||||
}
|
||||
// StartExecProcess is a blocking operation which streams IO to/from pod;
|
||||
// this must execute in asynchronously, since the websocketConn could return errors (e.g. client disconnects) before
|
||||
// the blocking operation is completed.
|
||||
go cli.StartExecProcess(serviceAccountToken, isAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter, errorChan)
|
||||
|
||||
err = <-errorChan
|
||||
|
||||
// websocket client successfully disconnected
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
log.Printf("websocket error: %s \n", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"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
|
||||
// to the stdout parameter.
|
||||
// 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()
|
||||
if err != nil {
|
||||
return err
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
if !useAdminToken {
|
||||
|
@ -44,7 +46,8 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
|
|||
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
||||
if err != nil {
|
||||
return err
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
err = exec.Stream(remotecommand.StreamOptions{
|
||||
|
@ -54,9 +57,7 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
|
|||
})
|
||||
if err != nil {
|
||||
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)
|
||||
GetServiceAccountBearerToken(userID int) (string, error)
|
||||
CreateUserShellPod(ctx context.Context, serviceAccountName string) (*KubernetesShellPod, error)
|
||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
||||
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
|
||||
GetNodesLimits() (K8sNodesLimits, error)
|
||||
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue