diff --git a/api/datastore/migrate_legacyversion.go b/api/datastore/migrate_legacyversion.go index f6a3227f2..8d725ca72 100644 --- a/api/datastore/migrate_legacyversion.go +++ b/api/datastore/migrate_legacyversion.go @@ -1,7 +1,7 @@ package datastore import ( - portaineree "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/database/models" "github.com/portainer/portainer/api/dataservices" ) @@ -72,7 +72,7 @@ func dbVersionToSemanticVersion(dbVersion int) string { func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) { // Very old versions of portainer did not have a version bucket, lets set some defaults dbVersion := 24 - edition := int(portaineree.PortainerCE) + edition := int(portainer.PortainerCE) instanceId := "" // If we already have a version key, we don't need to migrate diff --git a/api/http/handler/customtemplates/customtemplate_create.go b/api/http/handler/customtemplates/customtemplate_create.go index b22f9ba27..a93f1facf 100644 --- a/api/http/handler/customtemplates/customtemplate_create.go +++ b/api/http/handler/customtemplates/customtemplate_create.go @@ -3,6 +3,7 @@ package customtemplates import ( "encoding/json" "errors" + "fmt" "net/http" "os" "regexp" @@ -473,3 +474,29 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po return customTemplate, nil } + +// @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 body object true "for body documentation see the relevant /custom_templates/{method} endpoint" +// @success 200 {object} portainer.CustomTemplate +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @deprecated +// @router /custom_templates [post] +func deprecatedCustomTemplateCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { + method, err := request.RetrieveQueryParameter(r, "method", false) + if err != nil { + return "", httperror.BadRequest("Invalid query parameter: method", err) + } + + url := fmt.Sprintf("/custom_templates/create/%s", method) + return url, nil +} diff --git a/api/http/handler/customtemplates/handler.go b/api/http/handler/customtemplates/handler.go index 0da63d81f..1bb148af6 100644 --- a/api/http/handler/customtemplates/handler.go +++ b/api/http/handler/customtemplates/handler.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/mux" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/security" httperror "github.com/portainer/portainer/pkg/libhttp/error" ) @@ -32,6 +33,7 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor h.Handle("/custom_templates/create/{method}", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost) + h.Handle("/custom_templates", middlewares.Deprecated(h, deprecatedCustomTemplateCreateUrlParser)).Methods(http.MethodPost) // Deprecated h.Handle("/custom_templates", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet) h.Handle("/custom_templates/{id}", diff --git a/api/http/handler/edgejobs/edgejob_create.go b/api/http/handler/edgejobs/edgejob_create.go index d5b142450..631fc3426 100644 --- a/api/http/handler/edgejobs/edgejob_create.go +++ b/api/http/handler/edgejobs/edgejob_create.go @@ -2,6 +2,7 @@ package edgejobs import ( "errors" + "fmt" "maps" "net/http" "strconv" @@ -278,3 +279,26 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo return tx.EdgeJob().CreateWithID(edgeJob.ID, edgeJob) } + +// @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 body object true "for body documentation see the relevant /edge_jobs/create/{method} endpoint" +// @success 200 {object} portainer.EdgeGroup +// @failure 503 "Edge compute features are disabled" +// @failure 500 +// @deprecated +// @router /edge_jobs [post] +func deprecatedEdgeJobCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { + method, err := request.RetrieveQueryParameter(r, "method", false) + if err != nil { + return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err) + } + + return fmt.Sprintf("/edge_jobs/create/%s", method), nil +} diff --git a/api/http/handler/edgejobs/handler.go b/api/http/handler/edgejobs/handler.go index ab3d66b3b..93f210bb5 100644 --- a/api/http/handler/edgejobs/handler.go +++ b/api/http/handler/edgejobs/handler.go @@ -6,6 +6,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/security" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/response" @@ -29,6 +30,8 @@ func NewHandler(bouncer security.BouncerService) *Handler { h.Handle("/edge_jobs", bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobList)))).Methods(http.MethodGet) + h.Handle("/edge_jobs", + bouncer.AdminAccess(bouncer.EdgeComputeOperation(middlewares.Deprecated(h, deprecatedEdgeJobCreateUrlParser)))).Methods(http.MethodPost) h.Handle("/edge_jobs/create/{method}", bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobCreate)))).Methods(http.MethodPost) h.Handle("/edge_jobs/{id}", diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index a683a9d70..78345bebd 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -1,6 +1,7 @@ package edgestacks import ( + "fmt" "net/http" portainer "github.com/portainer/portainer/api" @@ -17,6 +18,7 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) if err != nil { return httperror.BadRequest("Invalid query parameter: method", err) } + dryrun, _ := request.RetrieveBooleanQueryParameter(r, "dryrun", true) tokenData, err := security.RetrieveTokenData(r) @@ -55,3 +57,26 @@ func (handler *Handler) createSwarmStack(tx dataservices.DataStoreTx, method str return nil, httperrors.NewInvalidPayloadError("Invalid value for query parameter: method. Value must be one of: string, repository or file") } + +// @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 body object true "for body documentation see the relevant /edge_stacks/create/{method} endpoint" +// @success 200 {object} portainer.EdgeStack +// @failure 500 +// @failure 503 "Edge compute features are disabled" +// @deprecated +// @router /edge_stacks [post] +func deprecatedEdgeStackCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { + method, err := request.RetrieveQueryParameter(r, "method", false) + if err != nil { + return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err) + } + + return fmt.Sprintf("/edge_stacks/create/%s", method), nil +} diff --git a/api/http/handler/edgestacks/handler.go b/api/http/handler/edgestacks/handler.go index 3ca757ce8..b084726f3 100644 --- a/api/http/handler/edgestacks/handler.go +++ b/api/http/handler/edgestacks/handler.go @@ -39,6 +39,8 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor 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(middlewares.Deprecated(h, deprecatedEdgeStackCreateUrlParser)))).Methods(http.MethodPost) // Deprecated h.Handle("/edge_stacks", bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackList)))).Methods(http.MethodGet) h.Handle("/edge_stacks/{id}", diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index e79651c39..7615738db 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -10,6 +10,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" dockerclient "github.com/portainer/portainer/api/docker/client" + "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/endpointutils" @@ -59,6 +60,8 @@ func NewHandler(bouncer security.BouncerService) *Handler { h.Handle("/stacks/create/{type}/{method}", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost) + h.Handle("/stacks", + bouncer.AuthenticatedAccess(middlewares.Deprecated(h, deprecatedStackCreateUrlParser))).Methods(http.MethodPost) // Deprecated h.Handle("/stacks", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackList))).Methods(http.MethodGet) h.Handle("/stacks/{id}", diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index cb297f9d7..f45592d09 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -1,6 +1,7 @@ package stacks import ( + "fmt" "net/http" portainer "github.com/portainer/portainer/api" @@ -140,3 +141,53 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port return response.JSON(w, stack) } + +func getStackTypeFromQueryParameter(r *http.Request) (string, error) { + stackType, err := request.RetrieveNumericQueryParameter(r, "type", false) + if err != nil { + return "", err + } + + switch stackType { + case 1: + return "swarm", nil + case 2: + return "standalone", nil + case 3: + return "kubernetes", nil + } + + return "", errors.New(request.ErrInvalidQueryParameter) +} + +// @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 body object true "for body documentation see the relevant /stacks/create/{type}/{method} endpoint" +// @success 200 {object} portainer.Stack +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @deprecated +// @router /stacks [post] +func deprecatedStackCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { + method, err := request.RetrieveQueryParameter(r, "method", false) + if err != nil { + return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err) + } + + stackType, err := getStackTypeFromQueryParameter(r) + if err != nil { + return "", httperror.BadRequest("Invalid query parameter: type", err) + } + + return fmt.Sprintf("/stacks/create/%s/%s", stackType, method), nil +} diff --git a/api/http/middlewares/deprecated.go b/api/http/middlewares/deprecated.go new file mode 100644 index 000000000..8a510e079 --- /dev/null +++ b/api/http/middlewares/deprecated.go @@ -0,0 +1,25 @@ +package middlewares + +import ( + "net/http" + + httperror "github.com/portainer/portainer/pkg/libhttp/error" + "github.com/rs/zerolog/log" +) + +// deprecate api route +func Deprecated(router http.Handler, urlBuilder func(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError)) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + newUrl, err := urlBuilder(w, r) + if err != nil { + httperror.WriteError(w, err.StatusCode, err.Error(), err) + return + } + + log.Warn().Msgf("This api is deprecated. Use %s instead", newUrl) + + redirectedRequest := r.Clone(r.Context()) + redirectedRequest.URL.Path = newUrl + router.ServeHTTP(w, redirectedRequest) + }) +}