1
0
Fork 0
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:
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

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

View file

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

View file

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

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

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

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

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

View file

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

View file

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

View file

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