mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 12:25:22 +02:00
feat(edge/update): select endpoints to update [EE-4043] (#7602)
This commit is contained in:
parent
36e7981ab7
commit
4d123895ea
37 changed files with 1192 additions and 130 deletions
32
api/http/handler/edgeupdateschedules/agentversions.go
Normal file
32
api/http/handler/edgeupdateschedules/agentversions.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id AgentVersions
|
||||
// @summary Fetches the supported versions of the agent to update/rollback
|
||||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags edge_update_schedules
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {array} string
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /edge_update_schedules/agent_versions [get]
|
||||
func (h *Handler) agentVersions(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
return response.JSON(w, []string{
|
||||
"2.13.0",
|
||||
"2.13.1",
|
||||
"2.14.0",
|
||||
"2.14.1",
|
||||
"2.14.2",
|
||||
"2.15", // for develop only
|
||||
"develop", // for develop only
|
||||
})
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type activeSchedulePayload struct {
|
||||
EnvironmentIDs []portainer.EndpointID
|
||||
}
|
||||
|
||||
func (payload *activeSchedulePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id EdgeUpdateScheduleActiveSchedulesList
|
||||
// @summary Fetches the list of Active Edge Update Schedules
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_update_schedules
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @param body body activeSchedulePayload true "Active schedule query"
|
||||
// @produce json
|
||||
// @success 200 {array} edgetypes.EdgeUpdateSchedule
|
||||
// @failure 500
|
||||
// @router /edge_update_schedules/active [get]
|
||||
func (handler *Handler) activeSchedules(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload activeSchedulePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
list := handler.dataStore.EdgeUpdateSchedule().ActiveSchedules(payload.EnvironmentIDs)
|
||||
|
||||
return response.JSON(w, list)
|
||||
}
|
|
@ -15,11 +15,11 @@ import (
|
|||
)
|
||||
|
||||
type createPayload struct {
|
||||
Name string
|
||||
GroupIDs []portainer.EdgeGroupID
|
||||
Type edgetypes.UpdateScheduleType
|
||||
Version string
|
||||
Time int64
|
||||
Name string
|
||||
GroupIDs []portainer.EdgeGroupID
|
||||
Type edgetypes.UpdateScheduleType
|
||||
Environments map[portainer.EndpointID]string
|
||||
Time int64
|
||||
}
|
||||
|
||||
func (payload *createPayload) Validate(r *http.Request) error {
|
||||
|
@ -35,8 +35,8 @@ func (payload *createPayload) Validate(r *http.Request) error {
|
|||
return errors.New("Invalid schedule type")
|
||||
}
|
||||
|
||||
if payload.Version == "" {
|
||||
return errors.New("Invalid version")
|
||||
if len(payload.Environments) == 0 {
|
||||
return errors.New("No Environment is scheduled for update")
|
||||
}
|
||||
|
||||
if payload.Time < time.Now().Unix() {
|
||||
|
@ -85,7 +85,44 @@ func (handler *Handler) create(w http.ResponseWriter, r *http.Request) *httperro
|
|||
Created: time.Now().Unix(),
|
||||
CreatedBy: tokenData.ID,
|
||||
Type: payload.Type,
|
||||
Version: payload.Version,
|
||||
}
|
||||
|
||||
schedules, err := handler.dataStore.EdgeUpdateSchedule().List()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to list edge update schedules", err)
|
||||
}
|
||||
|
||||
prevVersions := map[portainer.EndpointID]string{}
|
||||
if item.Type == edgetypes.UpdateScheduleRollback {
|
||||
prevVersions = previousVersions(schedules)
|
||||
}
|
||||
|
||||
for environmentID, version := range payload.Environments {
|
||||
environment, err := handler.dataStore.Endpoint().Endpoint(environmentID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
// TODO check that env is standalone (snapshots)
|
||||
if environment.Type != portainer.EdgeAgentOnDockerEnvironment {
|
||||
return httperror.BadRequest("Only standalone docker Environments are supported for remote update", nil)
|
||||
}
|
||||
|
||||
// validate version id is valid for rollback
|
||||
if item.Type == edgetypes.UpdateScheduleRollback {
|
||||
if prevVersions[environmentID] == "" {
|
||||
return httperror.BadRequest("No previous version found for environment", nil)
|
||||
}
|
||||
|
||||
if version != prevVersions[environmentID] {
|
||||
return httperror.BadRequest("Rollback version must match previous version", nil)
|
||||
}
|
||||
}
|
||||
|
||||
item.Status[environmentID] = edgetypes.UpdateScheduleStatus{
|
||||
TargetVersion: version,
|
||||
CurrentVersion: environment.Agent.Version,
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.dataStore.EdgeUpdateSchedule().Create(item)
|
||||
|
|
|
@ -15,11 +15,11 @@ import (
|
|||
)
|
||||
|
||||
type updatePayload struct {
|
||||
Name string
|
||||
GroupIDs []portainer.EdgeGroupID
|
||||
Type edgetypes.UpdateScheduleType
|
||||
Version string
|
||||
Time int64
|
||||
Name string
|
||||
GroupIDs []portainer.EdgeGroupID
|
||||
Environments map[portainer.EndpointID]string
|
||||
Type edgetypes.UpdateScheduleType
|
||||
Time int64
|
||||
}
|
||||
|
||||
func (payload *updatePayload) Validate(r *http.Request) error {
|
||||
|
@ -35,8 +35,8 @@ func (payload *updatePayload) Validate(r *http.Request) error {
|
|||
return errors.New("Invalid schedule type")
|
||||
}
|
||||
|
||||
if payload.Version == "" {
|
||||
return errors.New("Invalid version")
|
||||
if len(payload.Environments) == 0 {
|
||||
return errors.New("No Environment is scheduled for update")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -80,7 +80,23 @@ func (handler *Handler) update(w http.ResponseWriter, r *http.Request) *httperro
|
|||
item.GroupIDs = payload.GroupIDs
|
||||
item.Time = payload.Time
|
||||
item.Type = payload.Type
|
||||
item.Version = payload.Version
|
||||
|
||||
item.Status = map[portainer.EndpointID]edgetypes.UpdateScheduleStatus{}
|
||||
for environmentID, version := range payload.Environments {
|
||||
environment, err := handler.dataStore.Endpoint().Endpoint(environmentID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
if environment.Type != portainer.EdgeAgentOnDockerEnvironment {
|
||||
return httperror.BadRequest("Only standalone docker Environments are supported for remote update", nil)
|
||||
}
|
||||
|
||||
item.Status[environmentID] = edgetypes.UpdateScheduleStatus{
|
||||
TargetVersion: version,
|
||||
CurrentVersion: environment.Agent.Version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.dataStore.EdgeUpdateSchedule().Update(item.ID, item)
|
||||
|
|
|
@ -15,14 +15,14 @@ import (
|
|||
|
||||
const contextKey = "edgeUpdateSchedule_item"
|
||||
|
||||
// Handler is the HTTP handler used to handle edge environment(endpoint) operations.
|
||||
// Handler is the HTTP handler used to handle edge environment update operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
dataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
// NewHandler creates a handler to manage environment update operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
|
@ -40,6 +40,15 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
|||
router.Handle("",
|
||||
httperror.LoggerHandler(h.create)).Methods(http.MethodPost)
|
||||
|
||||
router.Handle("/active",
|
||||
httperror.LoggerHandler(h.activeSchedules)).Methods(http.MethodPost)
|
||||
|
||||
router.Handle("/agent_versions",
|
||||
httperror.LoggerHandler(h.agentVersions)).Methods(http.MethodGet)
|
||||
|
||||
router.Handle("/previous_versions",
|
||||
httperror.LoggerHandler(h.previousVersions)).Methods(http.MethodGet)
|
||||
|
||||
itemRouter := router.PathPrefix("/{id}").Subrouter()
|
||||
itemRouter.Use(middlewares.WithItem(func(id edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error) {
|
||||
return dataStore.EdgeUpdateSchedule().Item(id)
|
||||
|
|
86
api/http/handler/edgeupdateschedules/previous_versions.go
Normal file
86
api/http/handler/edgeupdateschedules/previous_versions.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// @id EdgeUpdatePreviousVersions
|
||||
// @summary Fetches the previous versions of updated agents
|
||||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags edge_update_schedules
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {array} string
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /edge_update_schedules/agent_versions [get]
|
||||
func (handler *Handler) previousVersions(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
schedules, err := handler.dataStore.EdgeUpdateSchedule().List()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve the edge update schedules list", err)
|
||||
}
|
||||
|
||||
versionMap := previousVersions(schedules)
|
||||
|
||||
return response.JSON(w, versionMap)
|
||||
}
|
||||
|
||||
type EnvironmentVersionDetails struct {
|
||||
version string
|
||||
skip bool
|
||||
skipReason string
|
||||
}
|
||||
|
||||
func previousVersions(schedules []edgetypes.UpdateSchedule) map[portainer.EndpointID]string {
|
||||
|
||||
slices.SortFunc(schedules, func(a edgetypes.UpdateSchedule, b edgetypes.UpdateSchedule) bool {
|
||||
return a.Created > b.Created
|
||||
})
|
||||
|
||||
environmentMap := map[portainer.EndpointID]*EnvironmentVersionDetails{}
|
||||
// to all schedules[:schedule index -1].Created > schedule.Created
|
||||
for _, schedule := range schedules {
|
||||
for environmentId, status := range schedule.Status {
|
||||
props, ok := environmentMap[environmentId]
|
||||
if !ok {
|
||||
environmentMap[environmentId] = &EnvironmentVersionDetails{}
|
||||
props = environmentMap[environmentId]
|
||||
}
|
||||
|
||||
if props.version != "" || props.skip {
|
||||
continue
|
||||
}
|
||||
|
||||
if schedule.Type == edgetypes.UpdateScheduleRollback {
|
||||
props.skip = true
|
||||
props.skipReason = "has rollback"
|
||||
continue
|
||||
}
|
||||
|
||||
if status.Status == edgetypes.UpdateScheduleStatusPending || status.Status == edgetypes.UpdateScheduleStatusError {
|
||||
props.skip = true
|
||||
props.skipReason = "has active schedule"
|
||||
continue
|
||||
}
|
||||
|
||||
props.version = status.CurrentVersion
|
||||
}
|
||||
}
|
||||
|
||||
versionMap := map[portainer.EndpointID]string{}
|
||||
for environmentId, props := range environmentMap {
|
||||
if !props.skip {
|
||||
versionMap[environmentId] = props.version
|
||||
}
|
||||
}
|
||||
|
||||
return versionMap
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package edgeupdateschedules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPreviousVersions(t *testing.T) {
|
||||
|
||||
schedules := []edgetypes.UpdateSchedule{
|
||||
{
|
||||
ID: 1,
|
||||
Type: edgetypes.UpdateScheduleUpdate,
|
||||
Status: map[portainer.EndpointID]edgetypes.UpdateScheduleStatus{
|
||||
1: {
|
||||
TargetVersion: "2.14.0",
|
||||
CurrentVersion: "2.11.0",
|
||||
Status: edgetypes.UpdateScheduleStatusSuccess,
|
||||
},
|
||||
2: {
|
||||
TargetVersion: "2.13.0",
|
||||
CurrentVersion: "2.12.0",
|
||||
Status: edgetypes.UpdateScheduleStatusSuccess,
|
||||
},
|
||||
},
|
||||
Created: 1500000000,
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Type: edgetypes.UpdateScheduleRollback,
|
||||
Status: map[portainer.EndpointID]edgetypes.UpdateScheduleStatus{
|
||||
1: {
|
||||
TargetVersion: "2.11.0",
|
||||
CurrentVersion: "2.14.0",
|
||||
Status: edgetypes.UpdateScheduleStatusSuccess,
|
||||
},
|
||||
},
|
||||
Created: 1500000001,
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Type: edgetypes.UpdateScheduleUpdate,
|
||||
Status: map[portainer.EndpointID]edgetypes.UpdateScheduleStatus{
|
||||
2: {
|
||||
TargetVersion: "2.14.0",
|
||||
CurrentVersion: "2.13.0",
|
||||
Status: edgetypes.UpdateScheduleStatusSuccess,
|
||||
},
|
||||
},
|
||||
Created: 1500000002,
|
||||
},
|
||||
}
|
||||
|
||||
actual := previousVersions(schedules)
|
||||
|
||||
assert.Equal(t, map[portainer.EndpointID]string{
|
||||
2: "2.13.0",
|
||||
}, actual)
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue