mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +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
91
api/dataservices/edgeupdateschedule/edgeupdateschedule.go
Normal file
91
api/dataservices/edgeupdateschedule/edgeupdateschedule.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package edgeupdateschedule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edge_update_schedule"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Edge Update Schedule data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List return an array containing all the items in the bucket.
|
||||
func (service *Service) List() ([]edgetypes.UpdateSchedule, error) {
|
||||
var list = make([]edgetypes.UpdateSchedule, 0)
|
||||
|
||||
err := service.connection.GetAll(
|
||||
BucketName,
|
||||
&edgetypes.UpdateSchedule{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
item, ok := obj.(*edgetypes.UpdateSchedule)
|
||||
if !ok {
|
||||
logrus.WithField("obj", obj).Errorf("Failed to convert to EdgeUpdateSchedule object")
|
||||
return nil, fmt.Errorf("Failed to convert to EdgeUpdateSchedule object: %s", obj)
|
||||
}
|
||||
list = append(list, *item)
|
||||
return &edgetypes.UpdateSchedule{}, nil
|
||||
})
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// Item returns a item by ID.
|
||||
func (service *Service) Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error) {
|
||||
var item edgetypes.UpdateSchedule
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.connection.GetObject(BucketName, identifier, &item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
// Create assign an ID to a new object and saves it.
|
||||
func (service *Service) Create(item *edgetypes.UpdateSchedule) error {
|
||||
return service.connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
item.ID = edgetypes.UpdateScheduleID(id)
|
||||
return int(item.ID), item
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Update updates an item.
|
||||
func (service *Service) Update(ID edgetypes.UpdateScheduleID, item *edgetypes.UpdateSchedule) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, item)
|
||||
}
|
||||
|
||||
// Delete deletes an item.
|
||||
func (service *Service) Delete(ID edgetypes.UpdateScheduleID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
@ -28,6 +29,7 @@ type (
|
|||
EdgeGroup() EdgeGroupService
|
||||
EdgeJob() EdgeJobService
|
||||
EdgeStack() EdgeStackService
|
||||
EdgeUpdateSchedule() EdgeUpdateScheduleService
|
||||
Endpoint() EndpointService
|
||||
EndpointGroup() EndpointGroupService
|
||||
EndpointRelation() EndpointRelationService
|
||||
|
@ -81,6 +83,15 @@ type (
|
|||
BucketName() string
|
||||
}
|
||||
|
||||
EdgeUpdateScheduleService interface {
|
||||
List() ([]edgetypes.UpdateSchedule, error)
|
||||
Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error)
|
||||
Create(edgeUpdateSchedule *edgetypes.UpdateSchedule) error
|
||||
Update(ID edgetypes.UpdateScheduleID, edgeUpdateSchedule *edgetypes.UpdateSchedule) error
|
||||
Delete(ID edgetypes.UpdateScheduleID) error
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
// EdgeStackService represents a service to manage Edge stacks
|
||||
EdgeStackService interface {
|
||||
EdgeStacks() ([]portainer.EdgeStack, error)
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/portainer/portainer/api/dataservices/edgegroup"
|
||||
"github.com/portainer/portainer/api/dataservices/edgejob"
|
||||
"github.com/portainer/portainer/api/dataservices/edgestack"
|
||||
"github.com/portainer/portainer/api/dataservices/edgeupdateschedule"
|
||||
"github.com/portainer/portainer/api/dataservices/endpoint"
|
||||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||
|
@ -46,6 +47,7 @@ type Store struct {
|
|||
DockerHubService *dockerhub.Service
|
||||
EdgeGroupService *edgegroup.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
EdgeUpdateScheduleService *edgeupdateschedule.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
|
@ -89,6 +91,12 @@ func (store *Store) initServices() error {
|
|||
}
|
||||
store.DockerHubService = dockerhubService
|
||||
|
||||
edgeUpdateScheduleService, err := edgeupdateschedule.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.EdgeUpdateScheduleService = edgeUpdateScheduleService
|
||||
|
||||
edgeStackService, err := edgestack.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -245,6 +253,11 @@ func (store *Store) EdgeJob() dataservices.EdgeJobService {
|
|||
return store.EdgeJobService
|
||||
}
|
||||
|
||||
// EdgeUpdateSchedule gives access to the EdgeUpdateSchedule data management layer
|
||||
func (store *Store) EdgeUpdateSchedule() dataservices.EdgeUpdateScheduleService {
|
||||
return store.EdgeUpdateScheduleService
|
||||
}
|
||||
|
||||
// EdgeStack gives access to the EdgeStack data management layer
|
||||
func (store *Store) EdgeStack() dataservices.EdgeStackService {
|
||||
return store.EdgeStackService
|
||||
|
|
93
api/edgetypes/edgetypes.go
Normal file
93
api/edgetypes/edgetypes.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package edgetypes
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
const (
|
||||
// PortainerAgentUpdateScheduleIDHeader represents the name of the header containing the update schedule id
|
||||
PortainerAgentUpdateScheduleIDHeader = "X-Portainer-Update-Schedule-ID"
|
||||
// PortainerAgentUpdateStatusHeader is the name of the header that will have the update status
|
||||
PortainerAgentUpdateStatusHeader = "X-Portainer-Update-Status"
|
||||
// PortainerAgentUpdateErrorHeader is the name of the header that will have the update error
|
||||
PortainerAgentUpdateErrorHeader = "X-Portainer-Update-Error"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// VersionUpdateStatus represents the status of an agent version update
|
||||
VersionUpdateStatus struct {
|
||||
Status UpdateScheduleStatusType
|
||||
ScheduleID UpdateScheduleID
|
||||
Error string
|
||||
}
|
||||
|
||||
// UpdateScheduleID represents an Edge schedule identifier
|
||||
UpdateScheduleID int
|
||||
|
||||
// UpdateSchedule represents a schedule for update/rollback of edge devices
|
||||
UpdateSchedule struct {
|
||||
// EdgeUpdateSchedule Identifier
|
||||
ID UpdateScheduleID `json:"id" example:"1"`
|
||||
// Name of the schedule
|
||||
Name string `json:"name" example:"Update Schedule"`
|
||||
// Type of the schedule
|
||||
Time int64 `json:"time" example:"1564897200"`
|
||||
// EdgeGroups to be updated
|
||||
GroupIDs []portainer.EdgeGroupID `json:"groupIds" example:"1"`
|
||||
// Type of the update (1 - update, 2 - rollback)
|
||||
Type UpdateScheduleType `json:"type" example:"1" enums:"1,2"`
|
||||
// Status of the schedule, grouped by environment id
|
||||
Status map[portainer.EndpointID]UpdateScheduleStatus `json:"status"`
|
||||
// Created timestamp
|
||||
Created int64 `json:"created" example:"1564897200"`
|
||||
// Created by user id
|
||||
CreatedBy portainer.UserID `json:"createdBy" example:"1"`
|
||||
// Version of the edge agent
|
||||
Version string `json:"version" example:"1"`
|
||||
}
|
||||
|
||||
// UpdateScheduleType represents type of an Edge update schedule
|
||||
UpdateScheduleType int
|
||||
|
||||
// UpdateScheduleStatus represents status of an Edge update schedule
|
||||
UpdateScheduleStatus struct {
|
||||
// Status of the schedule (0 - pending, 1 - failed, 2 - success)
|
||||
Status UpdateScheduleStatusType `json:"status" example:"1" enums:"1,2,3"`
|
||||
// Error message if status is failed
|
||||
Error string `json:"error" example:""`
|
||||
// Target version of the edge agent
|
||||
TargetVersion string `json:"targetVersion" example:"1"`
|
||||
// Current version of the edge agent
|
||||
CurrentVersion string `json:"currentVersion" example:"1"`
|
||||
}
|
||||
|
||||
// UpdateScheduleStatusType represents status type of an Edge update schedule
|
||||
UpdateScheduleStatusType int
|
||||
|
||||
VersionUpdateRequest struct {
|
||||
// Target version
|
||||
Version string
|
||||
// Scheduled time
|
||||
ScheduledTime int64
|
||||
// If need to update
|
||||
Active bool
|
||||
// Update schedule ID
|
||||
ScheduleID UpdateScheduleID
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
_ UpdateScheduleType = iota
|
||||
// UpdateScheduleUpdate represents an edge device scheduled for an update
|
||||
UpdateScheduleUpdate
|
||||
// UpdateScheduleRollback represents an edge device scheduled for a rollback
|
||||
UpdateScheduleRollback
|
||||
)
|
||||
|
||||
const (
|
||||
// UpdateScheduleStatusPending represents a pending edge update schedule
|
||||
UpdateScheduleStatusPending UpdateScheduleStatusType = iota
|
||||
// UpdateScheduleStatusError represents a failed edge update schedule
|
||||
UpdateScheduleStatusError
|
||||
// UpdateScheduleStatusSuccess represents a successful edge update schedule
|
||||
UpdateScheduleStatusSuccess
|
||||
)
|
|
@ -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"):
|
||||
|
|
25
api/http/middlewares/featureflag.go
Normal file
25
api/http/middlewares/featureflag.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
func FeatureFlag(settingsService dataservices.SettingsService, feature portainer.Feature) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
|
||||
enabled := settingsService.IsFeatureFlagEnabled(feature)
|
||||
|
||||
if !enabled {
|
||||
httperror.WriteError(rw, http.StatusForbidden, "This feature is not enabled", nil)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(rw, request)
|
||||
})
|
||||
}
|
||||
}
|
59
api/http/middlewares/withitem.go
Normal file
59
api/http/middlewares/withitem.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
bolterrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
|
||||
type ItemContextKey string
|
||||
|
||||
type ItemGetter[TId ~int, TObject any] func(id TId) (*TObject, error)
|
||||
|
||||
func WithItem[TId ~int, TObject any](getter ItemGetter[TId, TObject], idParam string, contextKey ItemContextKey) mux.MiddlewareFunc {
|
||||
if idParam == "" {
|
||||
idParam = "id"
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
itemId, err := request.RetrieveNumericRouteVariableValue(req, idParam)
|
||||
if err != nil {
|
||||
httperror.WriteError(rw, http.StatusBadRequest, "Invalid identifier route variable", err)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := getter(TId(itemId))
|
||||
if err != nil {
|
||||
statusCode := http.StatusInternalServerError
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
statusCode = http.StatusNotFound
|
||||
}
|
||||
httperror.WriteError(rw, statusCode, "Unable to find a object with the specified identifier inside the database", err)
|
||||
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), contextKey, item)
|
||||
next.ServeHTTP(rw, req.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FetchItem[T any](request *http.Request, contextKey string) (*T, error) {
|
||||
contextData := request.Context().Value(contextKey)
|
||||
if contextData == nil {
|
||||
return nil, errors.New("unable to find item in request context")
|
||||
}
|
||||
|
||||
item, ok := contextData.(*T)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to cast context item")
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
|
@ -26,6 +26,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"
|
||||
|
@ -152,6 +153,8 @@ func (server *Server) Start() error {
|
|||
edgeJobsHandler.FileService = server.FileService
|
||||
edgeJobsHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
edgeUpdateScheduleHandler := edgeupdateschedules.NewHandler(requestBouncer, server.DataStore)
|
||||
|
||||
var edgeStacksHandler = edgestacks.NewHandler(requestBouncer, server.DataStore)
|
||||
edgeStacksHandler.FileService = server.FileService
|
||||
edgeStacksHandler.GitService = server.GitService
|
||||
|
@ -274,42 +277,43 @@ func (server *Server) Start() error {
|
|||
webhookHandler.DockerClientFactory = server.DockerClientFactory
|
||||
|
||||
server.Handler = &handler.Handler{
|
||||
RoleHandler: roleHandler,
|
||||
AuthHandler: authHandler,
|
||||
BackupHandler: backupHandler,
|
||||
CustomTemplatesHandler: customTemplatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
EdgeGroupsHandler: edgeGroupsHandler,
|
||||
EdgeJobsHandler: edgeJobsHandler,
|
||||
EdgeStacksHandler: edgeStacksHandler,
|
||||
EdgeTemplatesHandler: edgeTemplatesHandler,
|
||||
EndpointGroupHandler: endpointGroupHandler,
|
||||
EndpointHandler: endpointHandler,
|
||||
EndpointHelmHandler: endpointHelmHandler,
|
||||
EndpointEdgeHandler: endpointEdgeHandler,
|
||||
EndpointProxyHandler: endpointProxyHandler,
|
||||
FileHandler: fileHandler,
|
||||
LDAPHandler: ldapHandler,
|
||||
HelmTemplatesHandler: helmTemplatesHandler,
|
||||
KubernetesHandler: kubernetesHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
OpenAMTHandler: openAMTHandler,
|
||||
FDOHandler: fdoHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
SSLHandler: sslHandler,
|
||||
StatusHandler: statusHandler,
|
||||
StackHandler: stackHandler,
|
||||
StorybookHandler: storybookHandler,
|
||||
TagHandler: tagHandler,
|
||||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
TemplatesHandler: templatesHandler,
|
||||
UploadHandler: uploadHandler,
|
||||
UserHandler: userHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
WebhookHandler: webhookHandler,
|
||||
RoleHandler: roleHandler,
|
||||
AuthHandler: authHandler,
|
||||
BackupHandler: backupHandler,
|
||||
CustomTemplatesHandler: customTemplatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
EdgeGroupsHandler: edgeGroupsHandler,
|
||||
EdgeJobsHandler: edgeJobsHandler,
|
||||
EdgeUpdateScheduleHandler: edgeUpdateScheduleHandler,
|
||||
EdgeStacksHandler: edgeStacksHandler,
|
||||
EdgeTemplatesHandler: edgeTemplatesHandler,
|
||||
EndpointGroupHandler: endpointGroupHandler,
|
||||
EndpointHandler: endpointHandler,
|
||||
EndpointHelmHandler: endpointHelmHandler,
|
||||
EndpointEdgeHandler: endpointEdgeHandler,
|
||||
EndpointProxyHandler: endpointProxyHandler,
|
||||
FileHandler: fileHandler,
|
||||
LDAPHandler: ldapHandler,
|
||||
HelmTemplatesHandler: helmTemplatesHandler,
|
||||
KubernetesHandler: kubernetesHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
OpenAMTHandler: openAMTHandler,
|
||||
FDOHandler: fdoHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
SSLHandler: sslHandler,
|
||||
StatusHandler: statusHandler,
|
||||
StackHandler: stackHandler,
|
||||
StorybookHandler: storybookHandler,
|
||||
TagHandler: tagHandler,
|
||||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
TemplatesHandler: templatesHandler,
|
||||
UploadHandler: uploadHandler,
|
||||
UserHandler: userHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
WebhookHandler: webhookHandler,
|
||||
}
|
||||
|
||||
handler := adminMonitor.WithRedirect(offlineGate.WaitingMiddleware(time.Minute, server.Handler))
|
||||
|
|
|
@ -12,6 +12,7 @@ type testDatastore struct {
|
|||
customTemplate dataservices.CustomTemplateService
|
||||
edgeGroup dataservices.EdgeGroupService
|
||||
edgeJob dataservices.EdgeJobService
|
||||
edgeUpdateSchedule dataservices.EdgeUpdateScheduleService
|
||||
edgeStack dataservices.EdgeStackService
|
||||
endpoint dataservices.EndpointService
|
||||
endpointGroup dataservices.EndpointGroupService
|
||||
|
@ -47,6 +48,9 @@ func (d *testDatastore) EdgeJob() dataservices.EdgeJobService { re
|
|||
func (d *testDatastore) EdgeStack() dataservices.EdgeStackService { return d.edgeStack }
|
||||
func (d *testDatastore) Endpoint() dataservices.EndpointService { return d.endpoint }
|
||||
func (d *testDatastore) EndpointGroup() dataservices.EndpointGroupService { return d.endpointGroup }
|
||||
func (d *testDatastore) EdgeUpdateSchedule() dataservices.EdgeUpdateScheduleService {
|
||||
return d.edgeUpdateSchedule
|
||||
}
|
||||
func (d *testDatastore) FDOProfile() dataservices.FDOProfileService {
|
||||
return d.fdoProfile
|
||||
}
|
||||
|
|
|
@ -254,7 +254,8 @@ type (
|
|||
EdgeJobLogsStatus int
|
||||
|
||||
// EdgeSchedule represents a scheduled job that can run on Edge environments(endpoints).
|
||||
// Deprecated in favor of EdgeJob
|
||||
//
|
||||
// Deprecated: in favor of EdgeJob
|
||||
EdgeSchedule struct {
|
||||
// EdgeSchedule Identifier
|
||||
ID ScheduleID `json:"Id" example:"1"`
|
||||
|
@ -1449,8 +1450,12 @@ const (
|
|||
WebSocketKeepAlive = 1 * time.Hour
|
||||
)
|
||||
|
||||
const FeatureFlagEdgeRemoteUpdate Feature = "edgeRemoteUpdate"
|
||||
|
||||
// List of supported features
|
||||
var SupportedFeatureFlags = []Feature{}
|
||||
var SupportedFeatureFlags = []Feature{
|
||||
FeatureFlagEdgeRemoteUpdate,
|
||||
}
|
||||
|
||||
const (
|
||||
_ AuthenticationMethod = iota
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue