mirror of
https://github.com/portainer/portainer.git
synced 2025-07-25 00:09:40 +02:00
feat(edge/update): remote update structure [EE-4040] (#7553)
This commit is contained in:
parent
dd1662c8b8
commit
6c4c958bf0
61 changed files with 1952 additions and 96 deletions
|
@ -8,7 +8,7 @@ import (
|
|||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
@ -43,7 +43,7 @@ func (handler *Handler) containerGpusInspect(w http.ResponseWriter, r *http.Requ
|
|||
return httperror.NotFound("Unable to find an environment on request context", err)
|
||||
}
|
||||
|
||||
agentTargetHeader := r.Header.Get(portaineree.PortainerAgentTargetHeader)
|
||||
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
|
||||
cli, err := handler.dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type createPayload struct {
|
||||
Name string
|
||||
GroupIDs []portainer.EdgeGroupID
|
||||
Type edgetypes.UpdateScheduleType
|
||||
Version string
|
||||
Time int64
|
||||
}
|
||||
|
||||
func (payload *createPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("Invalid tag name")
|
||||
}
|
||||
|
||||
if len(payload.GroupIDs) == 0 {
|
||||
return errors.New("Required to choose at least one group")
|
||||
}
|
||||
|
||||
if payload.Type != edgetypes.UpdateScheduleRollback && payload.Type != edgetypes.UpdateScheduleUpdate {
|
||||
return errors.New("Invalid schedule type")
|
||||
}
|
||||
|
||||
if payload.Version == "" {
|
||||
return errors.New("Invalid version")
|
||||
}
|
||||
|
||||
if payload.Time < time.Now().Unix() {
|
||||
return errors.New("Invalid time")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id EdgeUpdateScheduleCreate
|
||||
// @summary Creates a new Edge Update Schedule
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_update_schedules
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @param body body createPayload true "Schedule details"
|
||||
// @produce json
|
||||
// @success 200 {object} edgetypes.UpdateSchedule
|
||||
// @failure 500
|
||||
// @router /edge_update_schedules [post]
|
||||
func (handler *Handler) create(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
var payload createPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
err = handler.validateUniqueName(payload.Name, 0)
|
||||
if err != nil {
|
||||
return httperror.NewError(http.StatusConflict, "Edge update schedule name already in use", err)
|
||||
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve user information from token", err)
|
||||
}
|
||||
|
||||
item := &edgetypes.UpdateSchedule{
|
||||
Name: payload.Name,
|
||||
Time: payload.Time,
|
||||
GroupIDs: payload.GroupIDs,
|
||||
Status: map[portainer.EndpointID]edgetypes.UpdateScheduleStatus{},
|
||||
Created: time.Now().Unix(),
|
||||
CreatedBy: tokenData.ID,
|
||||
Type: payload.Type,
|
||||
Version: payload.Version,
|
||||
}
|
||||
|
||||
err = handler.dataStore.EdgeUpdateSchedule().Create(item)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist the edge update schedule", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, item)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
)
|
||||
|
||||
// @id EdgeUpdateScheduleDelete
|
||||
// @summary Deletes an Edge Update Schedule
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_update_schedules
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @router /edge_update_schedules/{id} [delete]
|
||||
func (handler *Handler) delete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
item, err := middlewares.FetchItem[edgetypes.UpdateSchedule](r, contextKey)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
err = handler.dataStore.EdgeUpdateSchedule().Delete(item.ID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to delete the edge update schedule", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
)
|
||||
|
||||
// @id EdgeUpdateScheduleInspect
|
||||
// @summary Returns the Edge Update Schedule with the given ID
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_update_schedules
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {object} edgetypes.UpdateSchedule
|
||||
// @failure 500
|
||||
// @router /edge_update_schedules/{id} [get]
|
||||
func (handler *Handler) inspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
item, err := middlewares.FetchItem[edgetypes.UpdateSchedule](r, contextKey)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
return response.JSON(w, item)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id EdgeUpdateScheduleList
|
||||
// @summary Fetches the list of Edge Update Schedules
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_update_schedules
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {array} edgetypes.UpdateSchedule
|
||||
// @failure 500
|
||||
// @router /edge_update_schedules [get]
|
||||
func (handler *Handler) list(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
list, err := handler.dataStore.EdgeUpdateSchedule().List()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve the edge update schedules list", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, list)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
)
|
||||
|
||||
type updatePayload struct {
|
||||
Name string
|
||||
GroupIDs []portainer.EdgeGroupID
|
||||
Type edgetypes.UpdateScheduleType
|
||||
Version string
|
||||
Time int64
|
||||
}
|
||||
|
||||
func (payload *updatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("Invalid tag name")
|
||||
}
|
||||
|
||||
if len(payload.GroupIDs) == 0 {
|
||||
return errors.New("Required to choose at least one group")
|
||||
}
|
||||
|
||||
if payload.Type != edgetypes.UpdateScheduleRollback && payload.Type != edgetypes.UpdateScheduleUpdate {
|
||||
return errors.New("Invalid schedule type")
|
||||
}
|
||||
|
||||
if payload.Version == "" {
|
||||
return errors.New("Invalid version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id EdgeUpdateScheduleUpdate
|
||||
// @summary Updates an Edge Update Schedule
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_update_schedules
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @param body body updatePayload true "Schedule details"
|
||||
// @produce json
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @router /edge_update_schedules [post]
|
||||
func (handler *Handler) update(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
item, err := middlewares.FetchItem[edgetypes.UpdateSchedule](r, contextKey)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
var payload updatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
if payload.Name != item.Name {
|
||||
err = handler.validateUniqueName(payload.Name, item.ID)
|
||||
if err != nil {
|
||||
return httperror.NewError(http.StatusConflict, "Edge update schedule name already in use", err)
|
||||
}
|
||||
|
||||
item.Name = payload.Name
|
||||
}
|
||||
|
||||
// if scheduled time didn't passed, then can update the schedule
|
||||
if item.Time > time.Now().Unix() {
|
||||
item.GroupIDs = payload.GroupIDs
|
||||
item.Time = payload.Time
|
||||
item.Type = payload.Type
|
||||
item.Version = payload.Version
|
||||
}
|
||||
|
||||
err = handler.dataStore.EdgeUpdateSchedule().Update(item.ID, item)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist the edge update schedule", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, item)
|
||||
}
|
58
api/http/handler/edgeupdateschedules/handler.go
Normal file
58
api/http/handler/edgeupdateschedules/handler.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
const contextKey = "edgeUpdateSchedule_item"
|
||||
|
||||
// Handler is the HTTP handler used to handle edge environment(endpoint) operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
dataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
dataStore: dataStore,
|
||||
}
|
||||
|
||||
router := h.PathPrefix("/edge_update_schedules").Subrouter()
|
||||
router.Use(bouncer.AdminAccess)
|
||||
router.Use(middlewares.FeatureFlag(dataStore.Settings(), portainer.FeatureFlagEdgeRemoteUpdate))
|
||||
|
||||
router.Handle("",
|
||||
httperror.LoggerHandler(h.list)).Methods(http.MethodGet)
|
||||
|
||||
router.Handle("",
|
||||
httperror.LoggerHandler(h.create)).Methods(http.MethodPost)
|
||||
|
||||
itemRouter := router.PathPrefix("/{id}").Subrouter()
|
||||
itemRouter.Use(middlewares.WithItem(func(id edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error) {
|
||||
return dataStore.EdgeUpdateSchedule().Item(id)
|
||||
}, "id", contextKey))
|
||||
|
||||
itemRouter.Handle("",
|
||||
httperror.LoggerHandler(h.inspect)).Methods(http.MethodGet)
|
||||
|
||||
itemRouter.Handle("",
|
||||
httperror.LoggerHandler(h.update)).Methods(http.MethodPut)
|
||||
|
||||
itemRouter.Handle("",
|
||||
httperror.LoggerHandler(h.delete)).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
21
api/http/handler/edgeupdateschedules/utils.go
Normal file
21
api/http/handler/edgeupdateschedules/utils.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
)
|
||||
|
||||
func (handler *Handler) validateUniqueName(name string, id edgetypes.UpdateScheduleID) error {
|
||||
list, err := handler.dataStore.EdgeUpdateSchedule().List()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to list edge update schedules")
|
||||
}
|
||||
|
||||
for _, schedule := range list {
|
||||
if id != schedule.ID && schedule.Name == name {
|
||||
return errors.New("Edge update schedule name already in use")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||
"github.com/portainer/portainer/api/http/handler/edgetemplates"
|
||||
"github.com/portainer/portainer/api/http/handler/edgeupdateschedules"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointedge"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
|
@ -43,42 +44,43 @@ import (
|
|||
|
||||
// Handler is a collection of all the service handlers.
|
||||
type Handler struct {
|
||||
AuthHandler *auth.Handler
|
||||
BackupHandler *backup.Handler
|
||||
CustomTemplatesHandler *customtemplates.Handler
|
||||
DockerHandler *docker.Handler
|
||||
EdgeGroupsHandler *edgegroups.Handler
|
||||
EdgeJobsHandler *edgejobs.Handler
|
||||
EdgeStacksHandler *edgestacks.Handler
|
||||
EdgeTemplatesHandler *edgetemplates.Handler
|
||||
EndpointEdgeHandler *endpointedge.Handler
|
||||
EndpointGroupHandler *endpointgroups.Handler
|
||||
EndpointHandler *endpoints.Handler
|
||||
EndpointHelmHandler *helm.Handler
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
HelmTemplatesHandler *helm.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
FileHandler *file.Handler
|
||||
LDAPHandler *ldap.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
RoleHandler *roles.Handler
|
||||
SettingsHandler *settings.Handler
|
||||
SSLHandler *ssl.Handler
|
||||
OpenAMTHandler *openamt.Handler
|
||||
FDOHandler *fdo.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
StorybookHandler *storybook.Handler
|
||||
TagHandler *tags.Handler
|
||||
TeamMembershipHandler *teammemberships.Handler
|
||||
TeamHandler *teams.Handler
|
||||
TemplatesHandler *templates.Handler
|
||||
UploadHandler *upload.Handler
|
||||
UserHandler *users.Handler
|
||||
WebSocketHandler *websocket.Handler
|
||||
WebhookHandler *webhooks.Handler
|
||||
AuthHandler *auth.Handler
|
||||
BackupHandler *backup.Handler
|
||||
CustomTemplatesHandler *customtemplates.Handler
|
||||
DockerHandler *docker.Handler
|
||||
EdgeGroupsHandler *edgegroups.Handler
|
||||
EdgeJobsHandler *edgejobs.Handler
|
||||
EdgeUpdateScheduleHandler *edgeupdateschedules.Handler
|
||||
EdgeStacksHandler *edgestacks.Handler
|
||||
EdgeTemplatesHandler *edgetemplates.Handler
|
||||
EndpointEdgeHandler *endpointedge.Handler
|
||||
EndpointGroupHandler *endpointgroups.Handler
|
||||
EndpointHandler *endpoints.Handler
|
||||
EndpointHelmHandler *helm.Handler
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
HelmTemplatesHandler *helm.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
FileHandler *file.Handler
|
||||
LDAPHandler *ldap.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
RoleHandler *roles.Handler
|
||||
SettingsHandler *settings.Handler
|
||||
SSLHandler *ssl.Handler
|
||||
OpenAMTHandler *openamt.Handler
|
||||
FDOHandler *fdo.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
StorybookHandler *storybook.Handler
|
||||
TagHandler *tags.Handler
|
||||
TeamMembershipHandler *teammemberships.Handler
|
||||
TeamHandler *teams.Handler
|
||||
TemplatesHandler *templates.Handler
|
||||
UploadHandler *upload.Handler
|
||||
UserHandler *users.Handler
|
||||
WebSocketHandler *websocket.Handler
|
||||
WebhookHandler *webhooks.Handler
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
|
@ -167,6 +169,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
http.StripPrefix("/api", h.BackupHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/custom_templates"):
|
||||
http.StripPrefix("/api", h.CustomTemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_update_schedules"):
|
||||
http.StripPrefix("/api", h.EdgeUpdateScheduleHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"):
|
||||
http.StripPrefix("/api", h.EdgeStacksHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_groups"):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue