1
0
Fork 0
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:
Chaim Lev-Ari 2022-09-18 14:42:18 +03:00 committed by GitHub
parent 36e7981ab7
commit 4d123895ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1192 additions and 130 deletions

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

View file

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

View file

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

View file

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

View file

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

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

View file

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