refactor(stacks): break swagger docs by type [EE-5381] (#8820)

pull/8854/head
Chaim Lev-Ari 2023-04-27 11:03:55 +07:00 committed by GitHub
parent bbea0bc8a5
commit 77f8b9333a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 347 additions and 144 deletions

View File

@ -21,30 +21,8 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// @id CustomTemplateCreate
// @summary Create a custom template
// @description Create a custom template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept json,multipart/form-data
// @produce json
// @param method query string true "method for creating template" Enums(string, file, repository)
// @param body_string body customTemplateFromFileContentPayload false "Required when using method=string"
// @param body_repository body customTemplateFromGitRepositoryPayload false "Required when using method=repository"
// @param Title formData string false "Title of the template. required when method is file"
// @param Description formData string false "Description of the template. required when method is file"
// @param Note formData string false "A note that will be displayed in the UI. Supports HTML content"
// @param Platform formData int false "Platform associated to the template (1 - 'linux', 2 - 'windows'). required when method is file" Enums(1,2)
// @param Type formData int false "Type of created stack (1 - swarm, 2 - compose), required when method is file" Enums(1,2)
// @param file formData file false "required when method is file"
// @success 200 {object} portainer.CustomTemplate
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /custom_templates [post]
func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
method, err := request.RetrieveQueryParameter(r, "method", false) method, err := request.RetrieveRouteVariableValue(r, "method")
if err != nil { if err != nil {
return httperror.BadRequest("Invalid query parameter: method", err) return httperror.BadRequest("Invalid query parameter: method", err)
} }
@ -154,6 +132,20 @@ func isValidNote(note string) bool {
return !match return !match
} }
// @id CustomTemplateCreateString
// @summary Create a custom template
// @description Create a custom template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param body body customTemplateFromFileContentPayload true "body"
// @success 200 {object} portainer.CustomTemplate
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /custom_templates/string [post]
func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) { func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromFileContentPayload var payload customTemplateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -251,6 +243,20 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
return validateVariablesDefinitions(payload.Variables) return validateVariablesDefinitions(payload.Variables)
} }
// @id CustomTemplateCreateRepository
// @summary Create a custom template
// @description Create a custom template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param body body customTemplateFromGitRepositoryPayload true "Required when using method=repository"
// @success 200 {object} portainer.CustomTemplate
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /custom_templates/repository [post]
func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (*portainer.CustomTemplate, error) { func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromGitRepositoryPayload var payload customTemplateFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -405,6 +411,25 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
return nil return nil
} }
// @id CustomTemplateCreateFile
// @summary Create a custom template
// @description Create a custom template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept multipart/form-data
// @produce json
// @param Title formData string false "Title of the template"
// @param Description formData string false "Description of the template"
// @param Note formData string false "A note that will be displayed in the UI. Supports HTML content"
// @param Platform formData int false "Platform associated to the template (1 - 'linux', 2 - 'windows')" Enums(1,2)
// @param Type formData int false "Type of created stack (1 - swarm, 2 - compose)" Enums(1,2)
// @param file formData file false "File"
// @success 200 {object} portainer.CustomTemplate
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /custom_templates/file [post]
func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*portainer.CustomTemplate, error) { func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*portainer.CustomTemplate, error) {
payload := &customTemplateFromFileUploadPayload{} payload := &customTemplateFromFileUploadPayload{}
err := payload.Validate(r) err := payload.Validate(r)

View File

@ -29,7 +29,8 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
GitService: gitService, GitService: gitService,
gitFetchMutexs: make(map[portainer.TemplateID]*sync.Mutex), gitFetchMutexs: make(map[portainer.TemplateID]*sync.Mutex),
} }
h.Handle("/custom_templates",
h.Handle("/custom_templates/create/{method}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost)
h.Handle("/custom_templates", h.Handle("/custom_templates",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet)

View File

@ -27,22 +27,8 @@ type edgeJobBasePayload struct {
EdgeGroups []portainer.EdgeGroupID EdgeGroups []portainer.EdgeGroupID
} }
// @id EdgeJobCreate
// @summary Create an EdgeJob
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param method query string true "Creation Method" Enums(file, string)
// @param body_string body edgeJobCreateFromFileContentPayload true "EdgeGroup data when method is string"
// @param body_file body edgeJobCreateFromFilePayload true "EdgeGroup data when method is file"
// @success 200 {object} portainer.EdgeGroup
// @failure 503 "Edge compute features are disabled"
// @failure 500
// @router /edge_jobs [post]
func (handler *Handler) edgeJobCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) edgeJobCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
method, err := request.RetrieveQueryParameter(r, "method", false) method, err := request.RetrieveRouteVariableValue(r, "method")
if err != nil { if err != nil {
return httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err) return httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
} }
@ -86,6 +72,18 @@ func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) er
return nil return nil
} }
// @id EdgeJobCreateString
// @summary Create an EdgeJob from a text
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body edgeJobCreateFromFileContentPayload true "EdgeGroup data when method is string"
// @success 200 {object} portainer.EdgeGroup
// @failure 503 "Edge compute features are disabled"
// @failure 500
// @router /edge_jobs/create/string [post]
func (handler *Handler) createEdgeJobFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) createEdgeJobFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload edgeJobCreateFromFileContentPayload var payload edgeJobCreateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -177,6 +175,24 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
return nil return nil
} }
// @id EdgeJobCreateFile
// @summary Create an EdgeJob from a file
// @description **Access policy**: administrator
// @tags edge_jobs
// @accept multipart/form-data
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param file formData file true "Content of the Stack file"
// @param Name formData string true "Name of the stack"
// @param CronExpression formData string true "A cron expression to schedule this job"
// @param EdgeGroups formData string true "JSON stringified array of Edge Groups ids"
// @param Endpoints formData string true "JSON stringified array of Environment ids"
// @param Recurring formData bool false "If recurring"
// @success 200 {object} portainer.EdgeGroup
// @failure 503 "Edge compute features are disabled"
// @failure 500
// @router /edge_jobs/create/file [post]
func (handler *Handler) createEdgeJobFromFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) createEdgeJobFromFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
payload := &edgeJobCreateFromFilePayload{} payload := &edgeJobCreateFromFilePayload{}
err := payload.Validate(r) err := payload.Validate(r)

View File

@ -28,7 +28,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/edge_jobs", h.Handle("/edge_jobs",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobList)))).Methods(http.MethodGet) bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobList)))).Methods(http.MethodGet)
h.Handle("/edge_jobs", h.Handle("/edge_jobs/create/{method}",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobCreate)))).Methods(http.MethodPost) bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobCreate)))).Methods(http.MethodPost)
h.Handle("/edge_jobs/{id}", h.Handle("/edge_jobs/{id}",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobInspect)))).Methods(http.MethodGet) bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobInspect)))).Methods(http.MethodGet)

View File

@ -23,23 +23,8 @@ func (e *InvalidPayloadError) Error() string {
return e.msg return e.msg
} }
// @id EdgeStackCreate
// @summary Create an EdgeStack
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param method query string true "Creation Method" Enums(file,string,repository)
// @param body_string body swarmStackFromFileContentPayload true "Required when using method=string"
// @param body_file body swarmStackFromFileUploadPayload true "Required when using method=file"
// @param body_repository body swarmStackFromGitRepositoryPayload true "Required when using method=repository"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 503 "Edge compute features are disabled"
// @router /edge_stacks [post]
func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
method, err := request.RetrieveQueryParameter(r, "method", false) method, err := request.RetrieveRouteVariableValue(r, "method")
if err != nil { if err != nil {
return httperror.BadRequest("Invalid query parameter: method", err) return httperror.BadRequest("Invalid query parameter: method", err)
} }
@ -74,7 +59,7 @@ func (handler *Handler) createSwarmStack(method string, dryrun bool, userID port
case "file": case "file":
return handler.createSwarmStackFromFileUpload(r, dryrun) return handler.createSwarmStackFromFileUpload(r, dryrun)
} }
return nil, errors.New("Invalid value for query parameter: method. Value must be one of: string, repository or file") return nil, &InvalidPayloadError{"Invalid value for query parameter: method. Value must be one of: string, repository or file"}
} }
type swarmStackFromFileContentPayload struct { type swarmStackFromFileContentPayload struct {
@ -109,6 +94,19 @@ func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error
return nil return nil
} }
// @id EdgeStackCreateString
// @summary Create an EdgeStack from a text
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body swarmStackFromFileContentPayload true "stack config"
// @param dryrun query string false "if true, will not create an edge stack, but just will check the settings and return a non-persisted edge stack object"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 503 "Edge compute features are disabled"
// @router /edge_stacks/create/string [post]
func (handler *Handler) createSwarmStackFromFileContent(r *http.Request, dryrun bool) (*portainer.EdgeStack, error) { func (handler *Handler) createSwarmStackFromFileContent(r *http.Request, dryrun bool) (*portainer.EdgeStack, error) {
var payload swarmStackFromFileContentPayload var payload swarmStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -229,6 +227,20 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
return nil return nil
} }
// @id EdgeStackCreateRepository
// @summary Create an EdgeStack from a git repository
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param method query string true "Creation Method" Enums(file,string,repository)
// @param body body swarmStackFromGitRepositoryPayload true "stack config"
// @param dryrun query string false "if true, will not create an edge stack, but just will check the settings and return a non-persisted edge stack object"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 503 "Edge compute features are disabled"
// @router /edge_stacks/create/repository [post]
func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request, dryrun bool, userID portainer.UserID) (*portainer.EdgeStack, error) { func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request, dryrun bool, userID portainer.UserID) (*portainer.EdgeStack, error) {
var payload swarmStackFromGitRepositoryPayload var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -299,14 +311,14 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
} }
payload.EdgeGroups = edgeGroups payload.EdgeGroups = edgeGroups
deploymentType, err := request.RetrieveNumericMultiPartFormValue(r, "DeploymentType", true) deploymentType, err := request.RetrieveNumericMultiPartFormValue(r, "DeploymentType", false)
if err != nil { if err != nil {
return &InvalidPayloadError{msg: "Invalid deployment type"} return &InvalidPayloadError{msg: "Invalid deployment type"}
} }
payload.DeploymentType = portainer.EdgeStackDeploymentType(deploymentType) payload.DeploymentType = portainer.EdgeStackDeploymentType(deploymentType)
var registries []portainer.RegistryID var registries []portainer.RegistryID
request.RetrieveMultiPartFormJSONValue(r, "Registries", &registries, false) err = request.RetrieveMultiPartFormJSONValue(r, "Registries", &registries, true)
if err != nil { if err != nil {
return errors.New("Invalid registry type") return errors.New("Invalid registry type")
} }
@ -318,6 +330,27 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
return nil return nil
} }
// @id EdgeStackCreateFile
// @summary Create an EdgeStack from file
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @accept multipart/form-data
// @produce json
// @param Name formData string true "Name of the stack"
// @param file formData file true "Content of the Stack file"
// @param EdgeGroups formData string true "JSON stringified array of Edge Groups ids"
// @param DeploymentType formData int true "deploy type 0 - 'compose', 1 - 'kubernetes', 2 - 'nomad'"
// @param Registries formData string false "JSON stringified array of Registry ids to use for this stack"
// @param UseManifestNamespaces formData bool false "Uses the manifest's namespaces instead of the default one, relevant only for kube environments"
// @param PrePullImage formData bool false "Pre Pull image"
// @param RetryDeploy formData bool false "Retry deploy"
// @param dryrun query string false "if true, will not create an edge stack, but just will check the settings and return a non-persisted edge stack object"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 503 "Edge compute features are disabled"
// @router /edge_stacks/create/file [post]
func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request, dryrun bool) (*portainer.EdgeStack, error) { func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request, dryrun bool) (*portainer.EdgeStack, error) {
payload := &swarmStackFromFileUploadPayload{} payload := &swarmStackFromFileUploadPayload{}
err := payload.Validate(r) err := payload.Validate(r)

View File

@ -16,12 +16,13 @@ import (
// @tags edge_stacks // @tags edge_stacks
// @produce json // @produce json
// @param id path int true "EdgeStack Id" // @param id path int true "EdgeStack Id"
// @param environmentId path int true "Environment identifier"
// @success 200 {object} portainer.EdgeStack // @success 200 {object} portainer.EdgeStack
// @failure 500 // @failure 500
// @failure 400 // @failure 400
// @failure 404 // @failure 404
// @failure 403 // @failure 403
// @router /edge_stacks/{id}/status/{endpoint_id} [delete] // @router /edge_stacks/{id}/status/{environmentId} [delete]
func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil { if err != nil {

View File

@ -237,7 +237,7 @@ func TestCreateAndInspect(t *testing.T) {
r := bytes.NewBuffer(jsonPayload) r := bytes.NewBuffer(jsonPayload)
// Create EdgeStack // Create EdgeStack
req, err := http.NewRequest(http.MethodPost, "/edge_stacks?method=string", r) req, err := http.NewRequest(http.MethodPost, "/edge_stacks/create/string", r)
if err != nil { if err != nil {
t.Fatal("request error:", err) t.Fatal("request error:", err)
} }
@ -290,37 +290,32 @@ func TestCreateWithInvalidPayload(t *testing.T) {
cases := []struct { cases := []struct {
Name string Name string
Payload interface{} Payload interface{}
QueryString string
ExpectedStatusCode int ExpectedStatusCode int
Method string
}{ }{
{ {
Name: "Invalid query string parameter", Name: "Invalid method parameter",
Payload: swarmStackFromFileContentPayload{}, Payload: swarmStackFromFileContentPayload{},
QueryString: "invalid=query-string", Method: "invalid",
ExpectedStatusCode: 400, ExpectedStatusCode: 400,
}, },
{
Name: "Invalid creation method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=invalid-creation-method",
ExpectedStatusCode: 500,
},
{ {
Name: "Empty swarmStackFromFileContentPayload with string method", Name: "Empty swarmStackFromFileContentPayload with string method",
Payload: swarmStackFromFileContentPayload{}, Payload: swarmStackFromFileContentPayload{},
QueryString: "method=string", Method: "string",
ExpectedStatusCode: 400, ExpectedStatusCode: 400,
}, },
{ {
Name: "Empty swarmStackFromFileContentPayload with repository method", Name: "Empty swarmStackFromFileContentPayload with repository method",
Payload: swarmStackFromFileContentPayload{}, Payload: swarmStackFromFileContentPayload{},
QueryString: "method=repository", Method: "repository",
ExpectedStatusCode: 400, ExpectedStatusCode: 400,
}, },
{ {
Name: "Empty swarmStackFromFileContentPayload with file method", Name: "Empty swarmStackFromFileContentPayload with file method",
Payload: swarmStackFromFileContentPayload{}, Payload: swarmStackFromFileContentPayload{},
QueryString: "method=file", Method: "file",
ExpectedStatusCode: 400, ExpectedStatusCode: 400,
}, },
{ {
@ -331,7 +326,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: edgeStack.EdgeGroups, EdgeGroups: edgeStack.EdgeGroups,
DeploymentType: edgeStack.DeploymentType, DeploymentType: edgeStack.DeploymentType,
}, },
QueryString: "method=string", Method: "string",
ExpectedStatusCode: 500, ExpectedStatusCode: 500,
}, },
{ {
@ -342,7 +337,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: []portainer.EdgeGroupID{}, EdgeGroups: []portainer.EdgeGroupID{},
DeploymentType: edgeStack.DeploymentType, DeploymentType: edgeStack.DeploymentType,
}, },
QueryString: "method=string", Method: "string",
ExpectedStatusCode: 400, ExpectedStatusCode: 400,
}, },
{ {
@ -353,7 +348,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: []portainer.EdgeGroupID{1}, EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentKubernetes, DeploymentType: portainer.EdgeStackDeploymentKubernetes,
}, },
QueryString: "method=string", Method: "string",
ExpectedStatusCode: 500, ExpectedStatusCode: 500,
}, },
{ {
@ -364,7 +359,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: []portainer.EdgeGroupID{1}, EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose, DeploymentType: portainer.EdgeStackDeploymentCompose,
}, },
QueryString: "method=string", Method: "string",
ExpectedStatusCode: 400, ExpectedStatusCode: 400,
}, },
{ {
@ -380,7 +375,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: []portainer.EdgeGroupID{1}, EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose, DeploymentType: portainer.EdgeStackDeploymentCompose,
}, },
QueryString: "method=repository", Method: "repository",
ExpectedStatusCode: 500, ExpectedStatusCode: 500,
}, },
} }
@ -394,7 +389,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
r := bytes.NewBuffer(jsonPayload) r := bytes.NewBuffer(jsonPayload)
// Create EdgeStack // Create EdgeStack
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/edge_stacks?%s", tc.QueryString), r) req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/edge_stacks/create/%s", tc.Method), r)
if err != nil { if err != nil {
t.Fatal("request error:", err) t.Fatal("request error:", err)
} }

View File

@ -34,7 +34,7 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
edgeStacksService: edgeStacksService, edgeStacksService: edgeStacksService,
} }
h.Handle("/edge_stacks", h.Handle("/edge_stacks/create/{method}",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost) bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
h.Handle("/edge_stacks", h.Handle("/edge_stacks",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackList)))).Methods(http.MethodGet) bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackList)))).Methods(http.MethodGet)

View File

@ -192,7 +192,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
// @param TagIds formData []int false "List of tag identifiers to which this environment(endpoint) is associated" // @param TagIds formData []int false "List of tag identifiers to which this environment(endpoint) is associated"
// @param EdgeCheckinInterval formData int false "The check in interval for edge agent (in seconds)" // @param EdgeCheckinInterval formData int false "The check in interval for edge agent (in seconds)"
// @param EdgeTunnelServerAddress formData string true "URL or IP address that will be used to establish a reverse tunnel" // @param EdgeTunnelServerAddress formData string true "URL or IP address that will be used to establish a reverse tunnel"
// @param Gpus formData array false "List of GPUs" // @param Gpus formData string false "List of GPUs - json stringified array of {name, value} structs"
// @success 200 {object} portainer.Endpoint "Success" // @success 200 {object} portainer.Endpoint "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"
// @failure 500 "Server error" // @failure 500 "Server error"

View File

@ -54,6 +54,7 @@ func (payload *deviceConfigurePayload) Validate(r *http.Request) error {
// @tags intel // @tags intel
// @security jwt // @security jwt
// @produce json // @produce json
// @param guid path int true "Guid"
// @param body body deviceConfigurePayload true "Device Configuration" // @param body body deviceConfigurePayload true "Device Configuration"
// @success 200 "Success" // @success 200 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"

View File

@ -16,6 +16,7 @@ import (
// @description **Access policy**: administrator // @description **Access policy**: administrator
// @tags intel // @tags intel
// @security jwt // @security jwt
// @param id path int true "FDO Profile identifier"
// @produce json // @produce json
// @success 200 "Success" // @success 200 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"

View File

@ -20,6 +20,7 @@ import (
// @tags intel // @tags intel
// @security jwt // @security jwt
// @produce json // @produce json
// @param id path int true "FDO Profile identifier"
// @success 200 "Success" // @success 200 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"
// @failure 500 "Server error" // @failure 500 "Server error"

View File

@ -22,6 +22,7 @@ type fdoProfileResponse struct {
// @tags intel // @tags intel
// @security jwt // @security jwt
// @produce json // @produce json
// @param id path int true "FDO Profile identifier"
// @success 200 "Success" // @success 200 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"
// @failure 500 "Server error" // @failure 500 "Server error"

View File

@ -19,6 +19,7 @@ import (
// @tags intel // @tags intel
// @security jwt // @security jwt
// @produce json // @produce json
// @param id path int true "FDO Profile identifier"
// @success 200 "Success" // @success 200 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"
// @failure 409 "Profile name already exists" // @failure 409 "Profile name already exists"

View File

@ -20,7 +20,7 @@ import (
// @tags intel // @tags intel
// @security jwt // @security jwt
// @produce json // @produce json
// @param id path int true "Environment(Endpoint) identifier" // @param id path int true "Environment identifier"
// @success 200 "Success" // @success 200 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings" // @failure 403 "Permission denied to access settings"

View File

@ -80,6 +80,8 @@ func (payload *deviceActionPayload) Validate(r *http.Request) error {
// @security jwt // @security jwt
// @accept json // @accept json
// @produce json // @produce json
// @param id path int true "Environment identifier"
// @param deviceId path int true "Device identifier"
// @param body body deviceActionPayload true "Device Action" // @param body body deviceActionPayload true "Device Action"
// @success 204 "Success" // @success 204 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"
@ -140,6 +142,8 @@ type AuthorizationResponse struct {
// @security jwt // @security jwt
// @accept json // @accept json
// @produce json // @produce json
// @param id path int true "Environment identifier"
// @param deviceId path int true "Device identifier"
// @param body body deviceFeaturesPayload true "Device Features" // @param body body deviceFeaturesPayload true "Device Features"
// @success 204 "Success" // @success 204 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"

View File

@ -48,6 +48,7 @@ const (
// @tags intel // @tags intel
// @security jwt // @security jwt
// @produce json // @produce json
// @param id path int true "Environment identifier"
// @success 200 "Success" // @success 200 "Success"
// @failure 400 "Invalid request" // @failure 400 "Invalid request"
// @failure 403 "Permission denied to access settings" // @failure 403 "Permission denied to access settings"

View File

@ -24,7 +24,7 @@ type composeStackFromFileContentPayload struct {
Name string `example:"myStack" validate:"required"` Name string `example:"myStack" validate:"required"`
// Content of the Stack file // Content of the Stack file
StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"` StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"`
// A list of environment(endpoint) variables used during stack deployment // A list of environment variables used during stack deployment
Env []portainer.Pair Env []portainer.Pair
// Whether the stack is from a app template // Whether the stack is from a app template
FromAppTemplate bool `example:"false"` FromAppTemplate bool `example:"false"`
@ -87,6 +87,21 @@ func (handler *Handler) checkAndCleanStackDupFromSwarm(w http.ResponseWriter, r
return nil return nil
} }
// @id StackCreateDockerStandaloneString
// @summary Deploy a new compose stack from a text
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param body body composeStackFromFileContentPayload true "stack config"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/standalone/string [post]
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload composeStackFromFileContentPayload var payload composeStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -158,7 +173,7 @@ type composeStackFromGitRepositoryPayload struct {
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"` AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
// Optional auto update configuration // Optional auto update configuration
AutoUpdate *portainer.AutoUpdateSettings AutoUpdate *portainer.AutoUpdateSettings
// A list of environment(endpoint) variables used during stack deployment // A list of environment variables used during stack deployment
Env []portainer.Pair Env []portainer.Pair
// Whether the stack is from a app template // Whether the stack is from a app template
FromAppTemplate bool `example:"false"` FromAppTemplate bool `example:"false"`
@ -201,6 +216,21 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
return nil return nil
} }
// @id StackCreateDockerStandaloneRepository
// @summary Deploy a new compose stack from repository
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @accept json
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @param body body composeStackFromGitRepositoryPayload true "stack config"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/standalone/repository [post]
func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload composeStackFromGitRepositoryPayload var payload composeStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -318,6 +348,23 @@ func decodeRequestForm(r *http.Request) (*composeStackFromFileUploadPayload, err
return payload, nil return payload, nil
} }
// @id StackCreateDockerStandaloneFile
// @summary Deploy a new compose stack from a file
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @accept multipart/form-data
// @produce json
// @param Name formData string true "Name of the stack"
// @param Env formData string false "Environment variables passed during deployment, represented as a JSON array [{'name': 'name', 'value': 'value'}]."
// @param file formData file false "Stack file"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/standalone/file [post]
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
payload, err := decodeRequestForm(r) payload, err := decodeRequestForm(r)
if err != nil { if err != nil {

View File

@ -12,6 +12,7 @@ import (
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/git/update" "github.com/portainer/portainer/api/git/update"
"github.com/portainer/portainer/api/internal/endpointutils"
k "github.com/portainer/portainer/api/kubernetes" k "github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/stacks/deployments" "github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackbuilders" "github.com/portainer/portainer/api/stacks/stackbuilders"
@ -131,7 +132,25 @@ type createKubernetesStackResponse struct {
Output string `json:"Output"` Output string `json:"Output"`
} }
// @id StackCreateKubernetesFile
// @summary Deploy a new kubernetes stack from a file
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body kubernetesStringDeploymentPayload true "stack config"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/kubernetes/string [post]
func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
if !endpointutils.IsKubernetesEndpoint(endpoint) {
return httperror.BadRequest("Environment type does not match", errors.New("Environment type does not match"))
}
var payload kubernetesStringDeploymentPayload var payload kubernetesStringDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil { if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err) return httperror.BadRequest("Invalid request payload", err)
@ -170,7 +189,25 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
return response.JSON(w, resp) return response.JSON(w, resp)
} }
// @id StackCreateKubernetesGit
// @summary Deploy a new kubernetes stack from a git repository
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body kubernetesGitDeploymentPayload true "stack config"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/kubernetes/repository [post]
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
if !endpointutils.IsKubernetesEndpoint(endpoint) {
return httperror.BadRequest("Environment type does not match", errors.New("Environment type does not match"))
}
var payload kubernetesGitDeploymentPayload var payload kubernetesGitDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil { if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err) return httperror.BadRequest("Invalid request payload", err)
@ -234,6 +271,20 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
return response.JSON(w, resp) return response.JSON(w, resp)
} }
// @id StackCreateKubernetesUrl
// @summary Deploy a new kubernetes stack from a url
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body kubernetesManifestURLDeploymentPayload true "stack config"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/kubernetes/url [post]
func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload kubernetesManifestURLDeploymentPayload var payload kubernetesManifestURLDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil { if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {

View File

@ -23,7 +23,7 @@ type swarmStackFromFileContentPayload struct {
SwarmID string `example:"jpofkc0i9uo9wtx1zesuk649w" validate:"required"` SwarmID string `example:"jpofkc0i9uo9wtx1zesuk649w" validate:"required"`
// Content of the Stack file // Content of the Stack file
StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"` StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"`
// A list of environment(endpoint) variables used during stack deployment // A list of environment variables used during stack deployment
Env []portainer.Pair Env []portainer.Pair
// Whether the stack is from a app template // Whether the stack is from a app template
FromAppTemplate bool `example:"false"` FromAppTemplate bool `example:"false"`
@ -52,6 +52,21 @@ func createStackPayloadFromSwarmFileContentPayload(name string, swarmID string,
} }
} }
// @id StackCreateDockerSwarmString
// @summary Deploy a new swarm stack from a text
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param body body swarmStackFromFileContentPayload true "stack config"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/swarm/string [post]
func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload swarmStackFromFileContentPayload var payload swarmStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -96,7 +111,7 @@ type swarmStackFromGitRepositoryPayload struct {
Name string `example:"myStack" validate:"required"` Name string `example:"myStack" validate:"required"`
// Swarm cluster identifier // Swarm cluster identifier
SwarmID string `example:"jpofkc0i9uo9wtx1zesuk649w" validate:"required"` SwarmID string `example:"jpofkc0i9uo9wtx1zesuk649w" validate:"required"`
// A list of environment(endpoint) variables used during stack deployment // A list of environment variables used during stack deployment
Env []portainer.Pair Env []portainer.Pair
// URL of a Git repository hosting the Stack file // URL of a Git repository hosting the Stack file
@ -159,6 +174,21 @@ func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference
} }
} }
// @id StackCreateDockerSwarmRepository
// @summary Deploy a new swarm stack from a git repository
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @accept json
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @param body body swarmStackFromGitRepositoryPayload true "stack config"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/swarm/repository [post]
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload swarmStackFromGitRepositoryPayload var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload) err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -267,8 +297,26 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
return nil return nil
} }
// @id StackCreateDockerSwarmFile
// @summary Deploy a new swarm stack from a file
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @accept multipart/form-data
// @produce json
// @param Name formData string false "Name of the stack"
// @param SwarmID formData string false "Swarm cluster identifier."
// @param Env formData string false "Environment variables passed during deployment, represented as a JSON array [{'name': 'name', 'value': 'value'}]. Optional"
// @param file formData file false "Stack file"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/swarm/file [post]
func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
payload := &swarmStackFromFileUploadPayload{} var payload swarmStackFromFileUploadPayload
err := payload.Validate(r) err := payload.Validate(r)
if err != nil { if err != nil {
return httperror.BadRequest("Invalid request payload", err) return httperror.BadRequest("Invalid request payload", err)

View File

@ -55,7 +55,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
stackDeletionMutex: &sync.Mutex{}, stackDeletionMutex: &sync.Mutex{},
requestBouncer: bouncer, requestBouncer: bouncer,
} }
h.Handle("/stacks",
h.Handle("/stacks/create/{type}/{method}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost)
h.Handle("/stacks", h.Handle("/stacks",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackList))).Methods(http.MethodGet) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackList))).Methods(http.MethodGet)

View File

@ -13,42 +13,15 @@ import (
"github.com/portainer/portainer/api/stacks/stackutils" "github.com/portainer/portainer/api/stacks/stackutils"
) )
// @id StackCreate
// @summary Deploy a new stack
// @description Deploy a new stack into a Docker environment(endpoint) specified via the environment(endpoint) identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @accept json,multipart/form-data
// @produce json
// @param type query int true "Stack deployment type. Possible values: 1 (Swarm stack), 2 (Compose stack) or 3 (Kubernetes stack)." Enums(1,2,3)
// @param method query string true "Stack deployment method. Possible values: file, string, repository or url." Enums(string, file, repository, url)
// @param endpointId query int true "Identifier of the environment(endpoint) that will be used to deploy the stack"
// @param body_swarm_string body swarmStackFromFileContentPayload false "Required when using method=string and type=1"
// @param body_swarm_repository body swarmStackFromGitRepositoryPayload false "Required when using method=repository and type=1"
// @param body_compose_string body composeStackFromFileContentPayload false "Required when using method=string and type=2"
// @param body_compose_repository body composeStackFromGitRepositoryPayload false "Required when using method=repository and type=2"
// @param body_kubernetes_string body kubernetesStringDeploymentPayload false "Required when using method=string and type=3"
// @param body_kubernetes_repository body kubernetesGitDeploymentPayload false "Required when using method=repository and type=3"
// @param body_kubernetes_url body kubernetesManifestURLDeploymentPayload false "Required when using method=url and type=3"
// @param Name formData string false "Name of the stack. required when method is file"
// @param SwarmID formData string false "Swarm cluster identifier. Required when method equals file and type equals 1. required when method is file"
// @param Env formData string false "Environment(Endpoint) variables passed during deployment, represented as a JSON array [{'name': 'name', 'value': 'value'}]. Optional, used when method equals file and type equals 1."
// @param file formData file false "Stack file. required when method is file"
// @success 200 {object} portainer.CustomTemplate
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks [post]
func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackType, err := request.RetrieveNumericQueryParameter(r, "type", false) stackType, err := request.RetrieveRouteVariableValue(r, "type")
if err != nil { if err != nil {
return httperror.BadRequest("Invalid query parameter: type", err) return httperror.BadRequest("Invalid path parameter: type", err)
} }
method, err := request.RetrieveQueryParameter(r, "method", false) method, err := request.RetrieveRouteVariableValue(r, "method")
if err != nil { if err != nil {
return httperror.BadRequest("Invalid query parameter: method", err) return httperror.BadRequest("Invalid path parameter: method", err)
} }
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false) endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
@ -87,12 +60,12 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return httperror.InternalServerError("Unable to retrieve user details from authentication token", err) return httperror.InternalServerError("Unable to retrieve user details from authentication token", err)
} }
switch portainer.StackType(stackType) { switch stackType {
case portainer.DockerSwarmStack: case "swarm":
return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID) return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID)
case portainer.DockerComposeStack: case "standalone":
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID) return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
case portainer.KubernetesStack: case "kubernetes":
return handler.createKubernetesStack(w, r, method, endpoint, tokenData.ID) return handler.createKubernetesStack(w, r, method, endpoint, tokenData.ID)
} }

View File

@ -20,11 +20,11 @@ import (
// @description Acts on a passed in token UUID to restart the docker service // @description Acts on a passed in token UUID to restart the docker service
// @description **Access policy**: public // @description **Access policy**: public
// @tags webhooks // @tags webhooks
// @param token path string true "Webhook token" // @param id path string true "Webhook token"
// @success 202 "Webhook executed" // @success 202 "Webhook executed"
// @failure 400 // @failure 400
// @failure 500 // @failure 500
// @router /webhooks/{token} [post] // @router /webhooks/{id} [post]
func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
webhookToken, err := request.RetrieveRouteVariableValue(r, "token") webhookToken, err := request.RetrieveRouteVariableValue(r, "token")

View File

@ -28,6 +28,7 @@ func (payload *webhookUpdatePayload) Validate(r *http.Request) error {
// @tags webhooks // @tags webhooks
// @accept json // @accept json
// @produce json // @produce json
// @param id path int true "Webhook id"
// @param body body webhookUpdatePayload true "Webhook data" // @param body body webhookUpdatePayload true "Webhook data"
// @success 200 {object} portainer.Webhook // @success 200 {object} portainer.Webhook
// @failure 400 // @failure 400

View File

@ -5,7 +5,7 @@ function EdgeJobsFactory($resource, API_ENDPOINT_EDGE_JOBS) {
API_ENDPOINT_EDGE_JOBS + '/:id/:action', API_ENDPOINT_EDGE_JOBS + '/:id/:action',
{}, {},
{ {
create: { method: 'POST' }, create: { method: 'POST', params: { id: 'create', action: '@method' } },
query: { method: 'GET', isArray: true }, query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } }, get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } },

View File

@ -5,7 +5,7 @@ angular.module('portainer.edge').factory('EdgeStacks', function EdgeStacksFactor
API_ENDPOINT_EDGE_STACKS + '/:id/:action', API_ENDPOINT_EDGE_STACKS + '/:id/:action',
{}, {},
{ {
create: { method: 'POST', ignoreLoadingBar: true }, create: { method: 'POST', ignoreLoadingBar: true, params: { id: 'create', action: '@method' } },
query: { method: 'GET', isArray: true }, query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } }, get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } },

View File

@ -49,7 +49,7 @@ function EdgeJobService(EdgeJobs, EdgeJobResults, FileUploadService) {
service.createEdgeJobFromFileContent = function (model) { service.createEdgeJobFromFileContent = function (model) {
var payload = new ScheduleCreateRequest(model); var payload = new ScheduleCreateRequest(model);
return EdgeJobs.create({ method: 'string' }, payload).$promise; return EdgeJobs.create({}, { method: 'string', ...payload }).$promise;
}; };
service.createEdgeJobFromFileUpload = function (model) { service.createEdgeJobFromFileUpload = function (model) {

View File

@ -30,7 +30,7 @@ angular.module('portainer.edge').factory('EdgeStackService', function EdgeStackS
service.createStackFromFileContent = async function createStackFromFileContent(payload) { service.createStackFromFileContent = async function createStackFromFileContent(payload) {
try { try {
return await EdgeStacks.create({ method: 'string' }, payload).$promise; return await EdgeStacks.create({}, { method: 'string', ...payload }).$promise;
} catch (err) { } catch (err) {
throw { msg: 'Unable to create the stack', err }; throw { msg: 'Unable to create the stack', err };
} }
@ -47,9 +47,10 @@ angular.module('portainer.edge').factory('EdgeStackService', function EdgeStackS
service.createStackFromGitRepository = async function createStackFromGitRepository(payload, repositoryOptions) { service.createStackFromGitRepository = async function createStackFromGitRepository(payload, repositoryOptions) {
try { try {
return await EdgeStacks.create( return await EdgeStacks.create(
{ method: 'repository' }, {},
{ {
...payload, ...payload,
method: 'repository',
RepositoryURL: repositoryOptions.RepositoryURL, RepositoryURL: repositoryOptions.RepositoryURL,
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName, RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
FilePathInRepository: repositoryOptions.FilePathInRepository, FilePathInRepository: repositoryOptions.FilePathInRepository,

View File

@ -7,7 +7,7 @@ function CustomTemplatesFactory($resource, API_ENDPOINT_CUSTOM_TEMPLATES) {
API_ENDPOINT_CUSTOM_TEMPLATES + '/:id/:action', API_ENDPOINT_CUSTOM_TEMPLATES + '/:id/:action',
{}, {},
{ {
create: { method: 'POST', ignoreLoadingBar: true }, create: { method: 'POST', ignoreLoadingBar: true, params: { id: 'create', action: '@method' } },
query: { method: 'GET', isArray: true }, query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } }, get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } },

View File

@ -10,7 +10,7 @@ function StackFactory($resource, API_ENDPOINT_STACKS) {
{ {
get: { method: 'GET', params: { id: '@id' } }, get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true }, query: { method: 'GET', isArray: true },
create: { method: 'POST', ignoreLoadingBar: true }, create: { method: 'POST', ignoreLoadingBar: true, params: { id: 'create', subaction: '@method', action: '@type' } },
update: { method: 'PUT', params: { id: '@id' }, ignoreLoadingBar: true }, update: { method: 'PUT', params: { id: '@id' }, ignoreLoadingBar: true },
associate: { method: 'PUT', params: { id: '@id', swarmId: '@swarmId', endpointId: '@endpointId', orphanedRunning: '@orphanedRunning', action: 'associate' } }, associate: { method: 'PUT', params: { id: '@id', swarmId: '@swarmId', endpointId: '@endpointId', orphanedRunning: '@orphanedRunning', action: 'associate' } },
remove: { method: 'DELETE', params: { id: '@id', external: '@external', endpointId: '@endpointId' } }, remove: { method: 'DELETE', params: { id: '@id', external: '@external', endpointId: '@endpointId' } },

View File

@ -40,7 +40,7 @@ function CustomTemplateServiceFactory($sanitize, CustomTemplates, FileUploadServ
service.createCustomTemplateFromFileContent = async function createCustomTemplateFromFileContent(payload) { service.createCustomTemplateFromFileContent = async function createCustomTemplateFromFileContent(payload) {
try { try {
return await CustomTemplates.create({ method: 'string' }, payload).$promise; return await CustomTemplates.create({}, { method: 'string', ...payload }).$promise;
} catch (err) { } catch (err) {
throw { msg: 'Unable to create the customTemplate', err }; throw { msg: 'Unable to create the customTemplate', err };
} }
@ -57,7 +57,7 @@ function CustomTemplateServiceFactory($sanitize, CustomTemplates, FileUploadServ
service.createCustomTemplateFromGitRepository = async function createCustomTemplateFromGitRepository(payload) { service.createCustomTemplateFromGitRepository = async function createCustomTemplateFromGitRepository(payload) {
try { try {
return await CustomTemplates.create({ method: 'repository' }, payload).$promise; return await CustomTemplates.create({}, { method: 'repository', ...payload }).$promise;
} catch (err) { } catch (err) {
throw { msg: 'Unable to create the customTemplate', err }; throw { msg: 'Unable to create the customTemplate', err };
} }

View File

@ -317,7 +317,7 @@ angular.module('portainer.app').factory('StackService', [
StackFileContent: stackFileContent, StackFileContent: stackFileContent,
Env: env, Env: env,
}; };
return Stack.create({ method: 'string', type: 2, endpointId: endpointId }, payload).$promise; return Stack.create({ endpointId: endpointId }, { method: 'string', type: 'standalone', ...payload }).$promise;
}; };
service.createSwarmStackFromFileContent = function (name, stackFileContent, env, endpointId) { service.createSwarmStackFromFileContent = function (name, stackFileContent, env, endpointId) {
@ -331,7 +331,7 @@ angular.module('portainer.app').factory('StackService', [
StackFileContent: stackFileContent, StackFileContent: stackFileContent,
Env: env, Env: env,
}; };
return Stack.create({ method: 'string', type: 1, endpointId: endpointId }, payload).$promise; return Stack.create({ endpointId: endpointId }, { method: 'string', type: 'swarm', ...payload }).$promise;
}) })
.then(function success(data) { .then(function success(data) {
deferred.resolve(data); deferred.resolve(data);
@ -362,7 +362,7 @@ angular.module('portainer.app').factory('StackService', [
payload.AutoUpdate = repositoryOptions.AutoUpdate; payload.AutoUpdate = repositoryOptions.AutoUpdate;
} }
return Stack.create({ method: 'repository', type: 2, endpointId: endpointId }, payload).$promise; return Stack.create({ endpointId: endpointId }, { method: 'repository', type: 'standalone', ...payload }).$promise;
}; };
service.createSwarmStackFromGitRepository = function (name, repositoryOptions, env, endpointId) { service.createSwarmStackFromGitRepository = function (name, repositoryOptions, env, endpointId) {
@ -390,7 +390,7 @@ angular.module('portainer.app').factory('StackService', [
payload.AutoUpdate = repositoryOptions.AutoUpdate; payload.AutoUpdate = repositoryOptions.AutoUpdate;
} }
return Stack.create({ method: 'repository', type: 1, endpointId: endpointId }, payload).$promise; return Stack.create({ endpointId: endpointId }, { method: 'repository', type: 'swarm', ...payload }).$promise;
}) })
.then(function success(data) { .then(function success(data) {
deferred.resolve(data); deferred.resolve(data);
@ -409,7 +409,7 @@ angular.module('portainer.app').factory('StackService', [
async function kubernetesDeployAsync(endpointId, method, payload) { async function kubernetesDeployAsync(endpointId, method, payload) {
try { try {
await Stack.create({ endpointId: endpointId, method: method, type: 3 }, payload).$promise; await Stack.create({ endpointId: endpointId }, { method, type: 'kubernetes', ...payload }).$promise;
} catch (err) { } catch (err) {
throw { err: err }; throw { err: err };
} }

View File

@ -65,7 +65,7 @@ angular.module('portainer.app').factory('FileUploadService', [
service.createSchedule = function (payload) { service.createSchedule = function (payload) {
return Upload.upload({ return Upload.upload({
url: 'api/edge_jobs?method=file', url: 'api/edge_jobs/create/file',
data: { data: {
file: payload.File, file: payload.File,
Name: payload.Name, Name: payload.Name,
@ -90,7 +90,7 @@ angular.module('portainer.app').factory('FileUploadService', [
service.createSwarmStack = function (stackName, swarmId, file, env, endpointId) { service.createSwarmStack = function (stackName, swarmId, file, env, endpointId) {
return Upload.upload({ return Upload.upload({
url: 'api/stacks?method=file&type=1&endpointId=' + endpointId, url: `api/stacks/create/swarm/file?endpointId=${endpointId}`,
data: { data: {
file: file, file: file,
Name: stackName, Name: stackName,
@ -103,7 +103,7 @@ angular.module('portainer.app').factory('FileUploadService', [
service.createComposeStack = function (stackName, file, env, endpointId) { service.createComposeStack = function (stackName, file, env, endpointId) {
return Upload.upload({ return Upload.upload({
url: 'api/stacks?method=file&type=2&endpointId=' + endpointId, url: `api/stacks/create/standalone/file?endpointId=${endpointId}`,
data: { data: {
file: file, file: file,
Name: stackName, Name: stackName,
@ -115,7 +115,7 @@ angular.module('portainer.app').factory('FileUploadService', [
service.createEdgeStack = function createEdgeStack({ EdgeGroups, ...payload }, file) { service.createEdgeStack = function createEdgeStack({ EdgeGroups, ...payload }, file) {
return Upload.upload({ return Upload.upload({
url: 'api/edge_stacks?method=file', url: `api/edge_stacks/create/file`,
data: { data: {
file, file,
EdgeGroups: Upload.json(EdgeGroups), EdgeGroups: Upload.json(EdgeGroups),
@ -127,7 +127,7 @@ angular.module('portainer.app').factory('FileUploadService', [
service.createCustomTemplate = function createCustomTemplate(data) { service.createCustomTemplate = function createCustomTemplate(data) {
return Upload.upload({ return Upload.upload({
url: 'api/custom_templates?method=file', url: 'api/custom_templates/create/file',
data, data,
ignoreLoadingBar: true, ignoreLoadingBar: true,
}); });