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"
)
// @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 {
method, err := request.RetrieveQueryParameter(r, "method", false)
method, err := request.RetrieveRouteVariableValue(r, "method")
if err != nil {
return httperror.BadRequest("Invalid query parameter: method", err)
}
@ -154,6 +132,20 @@ func isValidNote(note string) bool {
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) {
var payload customTemplateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -251,6 +243,20 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
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) {
var payload customTemplateFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -405,6 +411,25 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
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) {
payload := &customTemplateFromFileUploadPayload{}
err := payload.Validate(r)

View File

@ -29,7 +29,8 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
GitService: gitService,
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)
h.Handle("/custom_templates",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet)

View File

@ -27,22 +27,8 @@ type edgeJobBasePayload struct {
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 {
method, err := request.RetrieveQueryParameter(r, "method", false)
method, err := request.RetrieveRouteVariableValue(r, "method")
if err != nil {
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
}
// @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 {
var payload edgeJobCreateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -177,6 +175,24 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
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 {
payload := &edgeJobCreateFromFilePayload{}
err := payload.Validate(r)

View File

@ -28,7 +28,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/edge_jobs",
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)
h.Handle("/edge_jobs/{id}",
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
}
// @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 {
method, err := request.RetrieveQueryParameter(r, "method", false)
method, err := request.RetrieveRouteVariableValue(r, "method")
if err != nil {
return httperror.BadRequest("Invalid query parameter: method", err)
}
@ -74,7 +59,7 @@ func (handler *Handler) createSwarmStack(method string, dryrun bool, userID port
case "file":
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 {
@ -109,6 +94,19 @@ func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error
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) {
var payload swarmStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -229,6 +227,20 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
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) {
var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -299,14 +311,14 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
}
payload.EdgeGroups = edgeGroups
deploymentType, err := request.RetrieveNumericMultiPartFormValue(r, "DeploymentType", true)
deploymentType, err := request.RetrieveNumericMultiPartFormValue(r, "DeploymentType", false)
if err != nil {
return &InvalidPayloadError{msg: "Invalid deployment type"}
}
payload.DeploymentType = portainer.EdgeStackDeploymentType(deploymentType)
var registries []portainer.RegistryID
request.RetrieveMultiPartFormJSONValue(r, "Registries", &registries, false)
err = request.RetrieveMultiPartFormJSONValue(r, "Registries", &registries, true)
if err != nil {
return errors.New("Invalid registry type")
}
@ -318,6 +330,27 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
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) {
payload := &swarmStackFromFileUploadPayload{}
err := payload.Validate(r)

View File

@ -16,12 +16,13 @@ import (
// @tags edge_stacks
// @produce json
// @param id path int true "EdgeStack Id"
// @param environmentId path int true "Environment identifier"
// @success 200 {object} portainer.EdgeStack
// @failure 500
// @failure 400
// @failure 404
// @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 {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@ -237,7 +237,7 @@ func TestCreateAndInspect(t *testing.T) {
r := bytes.NewBuffer(jsonPayload)
// 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 {
t.Fatal("request error:", err)
}
@ -290,37 +290,32 @@ func TestCreateWithInvalidPayload(t *testing.T) {
cases := []struct {
Name string
Payload interface{}
QueryString string
ExpectedStatusCode int
Method string
}{
{
Name: "Invalid query string parameter",
Name: "Invalid method parameter",
Payload: swarmStackFromFileContentPayload{},
QueryString: "invalid=query-string",
Method: "invalid",
ExpectedStatusCode: 400,
},
{
Name: "Invalid creation method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=invalid-creation-method",
ExpectedStatusCode: 500,
},
{
Name: "Empty swarmStackFromFileContentPayload with string method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=string",
Method: "string",
ExpectedStatusCode: 400,
},
{
Name: "Empty swarmStackFromFileContentPayload with repository method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=repository",
Method: "repository",
ExpectedStatusCode: 400,
},
{
Name: "Empty swarmStackFromFileContentPayload with file method",
Payload: swarmStackFromFileContentPayload{},
QueryString: "method=file",
Method: "file",
ExpectedStatusCode: 400,
},
{
@ -331,7 +326,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: edgeStack.EdgeGroups,
DeploymentType: edgeStack.DeploymentType,
},
QueryString: "method=string",
Method: "string",
ExpectedStatusCode: 500,
},
{
@ -342,7 +337,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: []portainer.EdgeGroupID{},
DeploymentType: edgeStack.DeploymentType,
},
QueryString: "method=string",
Method: "string",
ExpectedStatusCode: 400,
},
{
@ -353,7 +348,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
},
QueryString: "method=string",
Method: "string",
ExpectedStatusCode: 500,
},
{
@ -364,7 +359,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose,
},
QueryString: "method=string",
Method: "string",
ExpectedStatusCode: 400,
},
{
@ -380,7 +375,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
EdgeGroups: []portainer.EdgeGroupID{1},
DeploymentType: portainer.EdgeStackDeploymentCompose,
},
QueryString: "method=repository",
Method: "repository",
ExpectedStatusCode: 500,
},
}
@ -394,7 +389,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
r := bytes.NewBuffer(jsonPayload)
// 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 {
t.Fatal("request error:", err)
}

View File

@ -34,7 +34,7 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
edgeStacksService: edgeStacksService,
}
h.Handle("/edge_stacks",
h.Handle("/edge_stacks/create/{method}",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
h.Handle("/edge_stacks",
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 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 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"
// @failure 400 "Invalid request"
// @failure 500 "Server error"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ type composeStackFromFileContentPayload struct {
Name string `example:"myStack" validate:"required"`
// Content of the Stack file
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
// Whether the stack is from a app template
FromAppTemplate bool `example:"false"`
@ -87,6 +87,21 @@ func (handler *Handler) checkAndCleanStackDupFromSwarm(w http.ResponseWriter, r
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 {
var payload composeStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -158,7 +173,7 @@ type composeStackFromGitRepositoryPayload struct {
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
// Optional auto update configuration
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
// Whether the stack is from a app template
FromAppTemplate bool `example:"false"`
@ -201,6 +216,21 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
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 {
var payload composeStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -318,6 +348,23 @@ func decodeRequestForm(r *http.Request) (*composeStackFromFileUploadPayload, err
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 {
payload, err := decodeRequestForm(r)
if err != nil {

View File

@ -12,6 +12,7 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/git/update"
"github.com/portainer/portainer/api/internal/endpointutils"
k "github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackbuilders"
@ -131,7 +132,25 @@ type createKubernetesStackResponse struct {
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 {
if !endpointutils.IsKubernetesEndpoint(endpoint) {
return httperror.BadRequest("Environment type does not match", errors.New("Environment type does not match"))
}
var payload kubernetesStringDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
@ -170,7 +189,25 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
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 {
if !endpointutils.IsKubernetesEndpoint(endpoint) {
return httperror.BadRequest("Environment type does not match", errors.New("Environment type does not match"))
}
var payload kubernetesGitDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
@ -234,6 +271,20 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
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 {
var payload kubernetesManifestURLDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {

View File

@ -23,7 +23,7 @@ type swarmStackFromFileContentPayload struct {
SwarmID string `example:"jpofkc0i9uo9wtx1zesuk649w" validate:"required"`
// Content of the Stack file
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
// Whether the stack is from a app template
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 {
var payload swarmStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -96,7 +111,7 @@ type swarmStackFromGitRepositoryPayload struct {
Name string `example:"myStack" validate:"required"`
// Swarm cluster identifier
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
// 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 {
var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@ -267,8 +297,26 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
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 {
payload := &swarmStackFromFileUploadPayload{}
var payload swarmStackFromFileUploadPayload
err := payload.Validate(r)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)

View File

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

View File

@ -13,42 +13,15 @@ import (
"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 {
stackType, err := request.RetrieveNumericQueryParameter(r, "type", false)
stackType, err := request.RetrieveRouteVariableValue(r, "type")
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 {
return httperror.BadRequest("Invalid query parameter: method", err)
return httperror.BadRequest("Invalid path parameter: method", err)
}
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)
}
switch portainer.StackType(stackType) {
case portainer.DockerSwarmStack:
switch stackType {
case "swarm":
return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID)
case portainer.DockerComposeStack:
case "standalone":
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
case portainer.KubernetesStack:
case "kubernetes":
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 **Access policy**: public
// @tags webhooks
// @param token path string true "Webhook token"
// @param id path string true "Webhook token"
// @success 202 "Webhook executed"
// @failure 400
// @failure 500
// @router /webhooks/{token} [post]
// @router /webhooks/{id} [post]
func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
webhookToken, err := request.RetrieveRouteVariableValue(r, "token")

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ angular.module('portainer.edge').factory('EdgeStackService', function EdgeStackS
service.createStackFromFileContent = async function createStackFromFileContent(payload) {
try {
return await EdgeStacks.create({ method: 'string' }, payload).$promise;
return await EdgeStacks.create({}, { method: 'string', ...payload }).$promise;
} catch (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) {
try {
return await EdgeStacks.create(
{ method: 'repository' },
{},
{
...payload,
method: 'repository',
RepositoryURL: repositoryOptions.RepositoryURL,
RepositoryReferenceName: repositoryOptions.RepositoryReferenceName,
FilePathInRepository: repositoryOptions.FilePathInRepository,

View File

@ -7,7 +7,7 @@ function CustomTemplatesFactory($resource, API_ENDPOINT_CUSTOM_TEMPLATES) {
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 },
get: { method: 'GET', 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' } },
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 },
associate: { method: 'PUT', params: { id: '@id', swarmId: '@swarmId', endpointId: '@endpointId', orphanedRunning: '@orphanedRunning', action: 'associate' } },
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) {
try {
return await CustomTemplates.create({ method: 'string' }, payload).$promise;
return await CustomTemplates.create({}, { method: 'string', ...payload }).$promise;
} catch (err) {
throw { msg: 'Unable to create the customTemplate', err };
}
@ -57,7 +57,7 @@ function CustomTemplateServiceFactory($sanitize, CustomTemplates, FileUploadServ
service.createCustomTemplateFromGitRepository = async function createCustomTemplateFromGitRepository(payload) {
try {
return await CustomTemplates.create({ method: 'repository' }, payload).$promise;
return await CustomTemplates.create({}, { method: 'repository', ...payload }).$promise;
} catch (err) {
throw { msg: 'Unable to create the customTemplate', err };
}

View File

@ -317,7 +317,7 @@ angular.module('portainer.app').factory('StackService', [
StackFileContent: stackFileContent,
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) {
@ -331,7 +331,7 @@ angular.module('portainer.app').factory('StackService', [
StackFileContent: stackFileContent,
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) {
deferred.resolve(data);
@ -362,7 +362,7 @@ angular.module('portainer.app').factory('StackService', [
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) {
@ -390,7 +390,7 @@ angular.module('portainer.app').factory('StackService', [
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) {
deferred.resolve(data);
@ -409,7 +409,7 @@ angular.module('portainer.app').factory('StackService', [
async function kubernetesDeployAsync(endpointId, method, payload) {
try {
await Stack.create({ endpointId: endpointId, method: method, type: 3 }, payload).$promise;
await Stack.create({ endpointId: endpointId }, { method, type: 'kubernetes', ...payload }).$promise;
} catch (err) {
throw { err: err };
}

View File

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