1
0
Fork 0
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:
Chaim Lev-Ari 2022-09-13 16:56:38 +03:00 committed by GitHub
parent dd1662c8b8
commit 6c4c958bf0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1952 additions and 96 deletions

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View 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
}

View 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
}

View file

@ -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"):