mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(edge/stacks): increase status transparency [EE-5554] (#9094)
This commit is contained in:
parent
db61fb149b
commit
0bcb57568c
45 changed files with 1305 additions and 316 deletions
|
@ -78,7 +78,6 @@ type (
|
|||
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
|
||||
EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
|
||||
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
||||
// Deprecated: Use UpdateEdgeStackFunc instead.
|
||||
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
||||
UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
DeleteEdgeStack(ID portainer.EdgeStackID) error
|
||||
|
|
|
@ -2,8 +2,9 @@ package migrator
|
|||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/chisel/crypto"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -73,3 +74,77 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEdgeStackStatusForDB100() error {
|
||||
log.Info().Msg("update edge stack status to have deployment steps")
|
||||
|
||||
edgeStacks, err := m.edgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
|
||||
for environmentID, environmentStatus := range edgeStack.Status {
|
||||
// skip if status is already updated
|
||||
if len(environmentStatus.Status) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
statusArray := []portainer.EdgeStackDeploymentStatus{}
|
||||
if environmentStatus.Details.Pending {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusPending,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Acknowledged {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusAcknowledged,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Error {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusError,
|
||||
Error: environmentStatus.Error,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Ok {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusRunning,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.ImagesPulled {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusImagesPulled,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Remove {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusRemoving,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
environmentStatus.Status = statusArray
|
||||
|
||||
edgeStack.Status[environmentID] = environmentStatus
|
||||
}
|
||||
|
||||
err = m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -78,13 +78,13 @@ func (m *Migrator) updateEdgeStackStatusForDB80() error {
|
|||
switch status.Type {
|
||||
case portainer.EdgeStackStatusPending:
|
||||
status.Details.Pending = true
|
||||
case portainer.EdgeStackStatusOk:
|
||||
case portainer.EdgeStackStatusDeploymentReceived:
|
||||
status.Details.Ok = true
|
||||
case portainer.EdgeStackStatusError:
|
||||
status.Details.Error = true
|
||||
case portainer.EdgeStackStatusAcknowledged:
|
||||
status.Details.Acknowledged = true
|
||||
case portainer.EdgeStackStatusRemove:
|
||||
case portainer.EdgeStackStatusRemoved:
|
||||
status.Details.Remove = true
|
||||
case portainer.EdgeStackStatusRemoteUpdateSuccess:
|
||||
status.Details.RemoteUpdateSuccess = true
|
||||
|
|
|
@ -215,6 +215,7 @@ func (m *Migrator) initMigrations() {
|
|||
m.addMigrations("2.19",
|
||||
m.convertSeedToPrivateKeyForDB100,
|
||||
m.migrateDockerDesktopExtentionSetting,
|
||||
m.updateEdgeStackStatusForDB100,
|
||||
)
|
||||
|
||||
// Add new migrations below...
|
||||
|
|
|
@ -944,6 +944,6 @@
|
|||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.19.0\",\"MigratorCount\":2,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.19.0\",\"MigratorCount\":3,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package edgestacks
|
|||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
@ -25,6 +26,7 @@ import (
|
|||
// @failure 400
|
||||
// @failure 404
|
||||
// @failure 403
|
||||
// @deprecated
|
||||
// @router /edge_stacks/{id}/status/{environmentId} [delete]
|
||||
func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
|
@ -69,7 +71,17 @@ func (handler *Handler) deleteEdgeStackStatus(tx dataservices.DataStoreTx, stack
|
|||
return nil, handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
delete(stack.Status, endpoint.ID)
|
||||
environmentStatus, ok := stack.Status[endpoint.ID]
|
||||
if !ok {
|
||||
environmentStatus = portainer.EdgeStackStatus{}
|
||||
}
|
||||
|
||||
environmentStatus.Status = append(environmentStatus.Status, portainer.EdgeStackDeploymentStatus{
|
||||
Time: time.Now().Unix(),
|
||||
Type: portainer.EdgeStackStatusRemoved,
|
||||
})
|
||||
|
||||
stack.Status[endpoint.ID] = environmentStatus
|
||||
|
||||
err = tx.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
|
|
|
@ -3,21 +3,24 @@ package edgestacks
|
|||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
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/dataservices"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
)
|
||||
|
||||
type updateStatusPayload struct {
|
||||
Error string
|
||||
Status *portainer.EdgeStackStatusType
|
||||
EndpointID portainer.EndpointID
|
||||
Time int64
|
||||
}
|
||||
|
||||
func (payload *updateStatusPayload) Validate(r *http.Request) error {
|
||||
|
@ -33,6 +36,10 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error {
|
|||
return errors.New("error message is mandatory when status is error")
|
||||
}
|
||||
|
||||
if payload.Time == 0 {
|
||||
payload.Time = time.Now().Unix()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -43,6 +50,7 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error {
|
|||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param body body updateStatusPayload true "EdgeStack status payload"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
@ -84,6 +92,21 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
|||
}
|
||||
|
||||
func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, r *http.Request, stackID portainer.EdgeStackID, payload updateStatusPayload) (*portainer.EdgeStack, error) {
|
||||
stack, err := tx.EdgeStack().EdgeStack(stackID)
|
||||
if err != nil {
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
// skip error because agent tries to report on deleted stack
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Int("stackID", int(stackID)).
|
||||
Int("status", int(*payload.Status)).
|
||||
Msg("Unable to find a stack inside the database, skipping error")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint, err := tx.Endpoint().Endpoint(payload.EndpointID)
|
||||
if err != nil {
|
||||
return nil, handler.handlerDBErr(err, "Unable to find an environment with the specified identifier inside the database")
|
||||
|
@ -94,67 +117,50 @@ func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, r *ht
|
|||
return nil, httperror.Forbidden("Permission denied to access environment", err)
|
||||
}
|
||||
|
||||
var stack *portainer.EdgeStack
|
||||
status := *payload.Status
|
||||
|
||||
log.Debug().
|
||||
Int("stackID", int(stackID)).
|
||||
Int("status", int(status)).
|
||||
Msg("Updating stack status")
|
||||
|
||||
deploymentStatus := portainer.EdgeStackDeploymentStatus{
|
||||
Type: status,
|
||||
Error: payload.Error,
|
||||
Time: payload.Time,
|
||||
}
|
||||
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
err = tx.EdgeStack().UpdateEdgeStackFunc(portainer.EdgeStackID(stackID), func(edgeStack *portainer.EdgeStack) {
|
||||
details := edgeStack.Status[payload.EndpointID].Details
|
||||
details.Pending = false
|
||||
|
||||
switch *payload.Status {
|
||||
case portainer.EdgeStackStatusOk:
|
||||
details.Ok = true
|
||||
case portainer.EdgeStackStatusError:
|
||||
details.Error = true
|
||||
case portainer.EdgeStackStatusAcknowledged:
|
||||
details.Acknowledged = true
|
||||
case portainer.EdgeStackStatusRemove:
|
||||
details.Remove = true
|
||||
case portainer.EdgeStackStatusImagesPulled:
|
||||
details.ImagesPulled = true
|
||||
}
|
||||
|
||||
edgeStack.Status[payload.EndpointID] = portainer.EdgeStackStatus{
|
||||
Details: details,
|
||||
Error: payload.Error,
|
||||
EndpointID: payload.EndpointID,
|
||||
}
|
||||
err = tx.EdgeStack().UpdateEdgeStackFunc(stackID, func(edgeStack *portainer.EdgeStack) {
|
||||
updateEnvStatus(payload.EndpointID, edgeStack, deploymentStatus)
|
||||
|
||||
stack = edgeStack
|
||||
})
|
||||
} else {
|
||||
stack, err = tx.EdgeStack().EdgeStack(stackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
details := stack.Status[payload.EndpointID].Details
|
||||
details.Pending = false
|
||||
|
||||
switch *payload.Status {
|
||||
case portainer.EdgeStackStatusOk:
|
||||
details.Ok = true
|
||||
case portainer.EdgeStackStatusError:
|
||||
details.Error = true
|
||||
case portainer.EdgeStackStatusAcknowledged:
|
||||
details.Acknowledged = true
|
||||
case portainer.EdgeStackStatusRemove:
|
||||
details.Remove = true
|
||||
case portainer.EdgeStackStatusImagesPulled:
|
||||
details.ImagesPulled = true
|
||||
}
|
||||
|
||||
stack.Status[payload.EndpointID] = portainer.EdgeStackStatus{
|
||||
Details: details,
|
||||
Error: payload.Error,
|
||||
EndpointID: payload.EndpointID,
|
||||
return nil, handler.handlerDBErr(err, "Unable to persist the stack changes inside the database")
|
||||
}
|
||||
} else {
|
||||
updateEnvStatus(payload.EndpointID, stack, deploymentStatus)
|
||||
|
||||
err = tx.EdgeStack().UpdateEdgeStack(stackID, stack)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, handler.handlerDBErr(err, "Unable to persist the stack changes inside the database")
|
||||
if err != nil {
|
||||
return nil, handler.handlerDBErr(err, "Unable to persist the stack changes inside the database")
|
||||
}
|
||||
}
|
||||
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
func updateEnvStatus(environmentId portainer.EndpointID, stack *portainer.EdgeStack, deploymentStatus portainer.EdgeStackDeploymentStatus) {
|
||||
environmentStatus, ok := stack.Status[environmentId]
|
||||
if !ok {
|
||||
environmentStatus = portainer.EdgeStackStatus{
|
||||
EndpointID: environmentId,
|
||||
Status: []portainer.EdgeStackDeploymentStatus{},
|
||||
}
|
||||
}
|
||||
|
||||
environmentStatus.Status = append(environmentStatus.Status, deploymentStatus)
|
||||
|
||||
stack.Status[environmentId] = environmentStatus
|
||||
}
|
||||
|
|
|
@ -59,23 +59,31 @@ func TestUpdateStatusAndInspect(t *testing.T) {
|
|||
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
updatedStack := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&updatedStack)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if !data.Status[endpoint.ID].Details.Error {
|
||||
t.Fatalf("expected EdgeStackStatusType %d, found %t", payload.Status, data.Status[endpoint.ID].Details.Error)
|
||||
endpointStatus, ok := updatedStack.Status[payload.EndpointID]
|
||||
if !ok {
|
||||
t.Fatal("Missing status")
|
||||
}
|
||||
|
||||
if data.Status[endpoint.ID].Error != payload.Error {
|
||||
t.Fatalf("expected EdgeStackStatusError %s, found %s", payload.Error, data.Status[endpoint.ID].Error)
|
||||
lastStatus := endpointStatus.Status[len(endpointStatus.Status)-1]
|
||||
|
||||
if len(endpointStatus.Status) == len(edgeStack.Status[payload.EndpointID].Status) {
|
||||
t.Fatal("expected status array to be updated")
|
||||
}
|
||||
|
||||
if data.Status[endpoint.ID].EndpointID != payload.EndpointID {
|
||||
t.Fatalf("expected EndpointID %d, found %d", payload.EndpointID, data.Status[endpoint.ID].EndpointID)
|
||||
if lastStatus.Type != *payload.Status {
|
||||
t.Fatalf("expected EdgeStackStatusType %d, found %d", *payload.Status, lastStatus.Type)
|
||||
}
|
||||
|
||||
if endpointStatus.EndpointID != portainer.EndpointID(payload.EndpointID) {
|
||||
t.Fatalf("expected EndpointID %d, found %d", payload.EndpointID, endpointStatus.EndpointID)
|
||||
}
|
||||
|
||||
}
|
||||
func TestUpdateStatusWithInvalidPayload(t *testing.T) {
|
||||
handler, _ := setupHandler(t)
|
||||
|
@ -85,7 +93,7 @@ func TestUpdateStatusWithInvalidPayload(t *testing.T) {
|
|||
|
||||
// Update edge stack status
|
||||
statusError := portainer.EdgeStackStatusError
|
||||
statusOk := portainer.EdgeStackStatusOk
|
||||
statusOk := portainer.EdgeStackStatusDeploymentReceived
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload updateStatusPayload
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package edgestacks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -41,20 +42,22 @@ func setupHandler(t *testing.T) (*Handler, string) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
edgeStacksService := edgestacks.NewService(store)
|
||||
|
||||
handler := NewHandler(
|
||||
security.NewRequestBouncer(store, jwtService, apiKeyService),
|
||||
store,
|
||||
edgeStacksService,
|
||||
)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
tmpDir, err := os.MkdirTemp(t.TempDir(), "portainer-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fs, err := filesystem.NewService(tmpDir, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := NewHandler(
|
||||
security.NewRequestBouncer(store, jwtService, apiKeyService),
|
||||
store,
|
||||
edgestacks.NewService(store),
|
||||
)
|
||||
|
||||
handler.FileService = fs
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
|
@ -116,11 +119,9 @@ func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID port
|
|||
|
||||
edgeStackID := portainer.EdgeStackID(14)
|
||||
edgeStack := portainer.EdgeStack{
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-" + strconv.Itoa(int(edgeStackID)),
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
|
||||
endpointID: {Details: portainer.EdgeStackStatusDetails{Ok: true}, Error: "", EndpointID: endpointID},
|
||||
},
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-" + strconv.Itoa(int(edgeStackID)),
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{},
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
|
||||
ProjectPath: "/project/path",
|
||||
|
|
|
@ -2,7 +2,7 @@ package edgestacks
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -10,12 +10,9 @@ import (
|
|||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/set"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type updateEdgeStackPayload struct {
|
||||
|
@ -116,24 +113,6 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
|
|||
|
||||
}
|
||||
|
||||
entryPoint := stack.EntryPoint
|
||||
manifestPath := stack.ManifestPath
|
||||
deploymentType := stack.DeploymentType
|
||||
|
||||
if deploymentType != payload.DeploymentType {
|
||||
// deployment type was changed - need to delete the old file
|
||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Unable to clear old files")
|
||||
}
|
||||
|
||||
entryPoint = ""
|
||||
manifestPath = ""
|
||||
deploymentType = payload.DeploymentType
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
|
||||
hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, payload.DeploymentType)
|
||||
if err != nil {
|
||||
return nil, httperror.BadRequest("unable to check for existence of non fitting environments: %w", err)
|
||||
|
@ -142,50 +121,20 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
|
|||
return nil, httperror.BadRequest("edge stack with config do not match the environment type", nil)
|
||||
}
|
||||
|
||||
if payload.DeploymentType == portainer.EdgeStackDeploymentCompose {
|
||||
if entryPoint == "" {
|
||||
entryPoint = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
stack.NumDeployments = len(relatedEndpointIds)
|
||||
|
||||
_, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, entryPoint, []byte(payload.StackFileContent))
|
||||
stack.UseManifestNamespaces = payload.UseManifestNamespaces
|
||||
|
||||
stack.EdgeGroups = groupsIds
|
||||
|
||||
if payload.UpdateVersion {
|
||||
err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist updated Compose file on disk", err)
|
||||
}
|
||||
|
||||
tempManifestPath, err := handler.convertAndStoreKubeManifestIfNeeded(stackFolder, stack.ProjectPath, entryPoint, relatedEndpointIds)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to convert and persist updated Kubernetes manifest file on disk", err)
|
||||
}
|
||||
|
||||
manifestPath = tempManifestPath
|
||||
}
|
||||
|
||||
if deploymentType == portainer.EdgeStackDeploymentKubernetes {
|
||||
if manifestPath == "" {
|
||||
manifestPath = filesystem.ManifestFileDefaultName
|
||||
}
|
||||
|
||||
_, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, manifestPath, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist updated Kubernetes manifest file on disk", err)
|
||||
return nil, httperror.InternalServerError("Unable to update stack version", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.EdgeStack().UpdateEdgeStackFunc(stack.ID, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments = len(relatedEndpointIds)
|
||||
if payload.UpdateVersion {
|
||||
edgeStack.Status = make(map[portainer.EndpointID]portainer.EdgeStackStatus)
|
||||
edgeStack.Version++
|
||||
}
|
||||
|
||||
edgeStack.UseManifestNamespaces = payload.UseManifestNamespaces
|
||||
|
||||
edgeStack.DeploymentType = deploymentType
|
||||
edgeStack.EntryPoint = entryPoint
|
||||
edgeStack.ManifestPath = manifestPath
|
||||
|
||||
edgeStack.EdgeGroups = groupsIds
|
||||
})
|
||||
err = tx.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
|
||||
}
|
||||
|
@ -246,3 +195,26 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
|
|||
|
||||
return newRelatedEnvironmentIDs, endpointsToAdd, nil
|
||||
}
|
||||
|
||||
func newStatus(oldStatus map[portainer.EndpointID]portainer.EdgeStackStatus, relatedEnvironmentIds []portainer.EndpointID) map[portainer.EndpointID]portainer.EdgeStackStatus {
|
||||
newStatus := make(map[portainer.EndpointID]portainer.EdgeStackStatus)
|
||||
for _, endpointID := range relatedEnvironmentIds {
|
||||
newEnvStatus := portainer.EdgeStackStatus{}
|
||||
|
||||
oldEnvStatus, ok := oldStatus[endpointID]
|
||||
if ok {
|
||||
newEnvStatus = oldEnvStatus
|
||||
}
|
||||
|
||||
newEnvStatus.Status = []portainer.EdgeStackDeploymentStatus{
|
||||
{
|
||||
Time: time.Now().Unix(),
|
||||
Type: portainer.EdgeStackStatusPending,
|
||||
},
|
||||
}
|
||||
|
||||
newStatus[endpointID] = newEnvStatus
|
||||
}
|
||||
|
||||
return newStatus
|
||||
}
|
||||
|
|
|
@ -94,21 +94,21 @@ func TestUpdateAndInspect(t *testing.T) {
|
|||
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
updatedStack := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&updatedStack)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if payload.UpdateVersion && data.Version != edgeStack.Version+1 {
|
||||
t.Fatalf("expected EdgeStackID %d, found %d", edgeStack.Version, data.Version)
|
||||
if payload.UpdateVersion && updatedStack.Version != edgeStack.Version+1 {
|
||||
t.Fatalf("expected EdgeStack version %d, found %d", edgeStack.Version+1, updatedStack.Version+1)
|
||||
}
|
||||
|
||||
if data.DeploymentType != payload.DeploymentType {
|
||||
t.Fatalf("expected DeploymentType %d, found %d", edgeStack.DeploymentType, data.DeploymentType)
|
||||
if updatedStack.DeploymentType != payload.DeploymentType {
|
||||
t.Fatalf("expected DeploymentType %d, found %d", edgeStack.DeploymentType, updatedStack.DeploymentType)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(data.EdgeGroups, payload.EdgeGroups) {
|
||||
if !reflect.DeepEqual(updatedStack.EdgeGroups, payload.EdgeGroups) {
|
||||
t.Fatalf("expected EdgeGroups to be equal")
|
||||
}
|
||||
}
|
||||
|
|
59
api/http/handler/edgestacks/utils_update_stack_version.go
Normal file
59
api/http/handler/edgestacks/utils_update_stack_version.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package edgestacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
edgestackutils "github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (handler *Handler) updateStackVersion(stack *portainer.EdgeStack, deploymentType portainer.EdgeStackDeploymentType, config []byte, oldGitHash string, relatedEnvironmentsIDs []portainer.EndpointID) error {
|
||||
|
||||
stack.Version = stack.Version + 1
|
||||
stack.Status = edgestackutils.NewStatus(stack.Status, relatedEnvironmentsIDs)
|
||||
|
||||
return handler.storeStackFile(stack, deploymentType, config)
|
||||
}
|
||||
|
||||
func (handler *Handler) storeStackFile(stack *portainer.EdgeStack, deploymentType portainer.EdgeStackDeploymentType, config []byte) error {
|
||||
|
||||
if deploymentType != stack.DeploymentType {
|
||||
// deployment type was changed - need to delete all old files
|
||||
err := handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Unable to clear old files")
|
||||
}
|
||||
|
||||
stack.EntryPoint = ""
|
||||
stack.ManifestPath = ""
|
||||
stack.DeploymentType = deploymentType
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
entryPoint := ""
|
||||
if deploymentType == portainer.EdgeStackDeploymentCompose {
|
||||
if stack.EntryPoint == "" {
|
||||
stack.EntryPoint = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
entryPoint = stack.EntryPoint
|
||||
}
|
||||
|
||||
if deploymentType == portainer.EdgeStackDeploymentKubernetes {
|
||||
if stack.ManifestPath == "" {
|
||||
stack.ManifestPath = filesystem.ManifestFileDefaultName
|
||||
}
|
||||
|
||||
entryPoint = stack.ManifestPath
|
||||
}
|
||||
|
||||
_, err := handler.FileService.StoreEdgeStackFileFromBytesByVersion(stackFolder, entryPoint, stack.Version, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to persist updated Compose file with version on disk: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -291,7 +291,7 @@ func TestEdgeStackStatus(t *testing.T) {
|
|||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-17",
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
|
||||
endpointID: {Details: portainer.EdgeStackStatusDetails{Ok: true}, Error: "", EndpointID: endpoint.ID},
|
||||
endpointID: {},
|
||||
},
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: []portainer.EdgeGroupID{1, 2},
|
||||
|
|
|
@ -17,18 +17,6 @@ import (
|
|||
"github.com/portainer/portainer/api/internal/unique"
|
||||
)
|
||||
|
||||
type EdgeStackStatusFilter string
|
||||
|
||||
const (
|
||||
statusFilterPending EdgeStackStatusFilter = "Pending"
|
||||
statusFilterOk EdgeStackStatusFilter = "Ok"
|
||||
statusFilterError EdgeStackStatusFilter = "Error"
|
||||
statusFilterAcknowledged EdgeStackStatusFilter = "Acknowledged"
|
||||
statusFilterRemove EdgeStackStatusFilter = "Remove"
|
||||
statusFilterRemoteUpdateSuccess EdgeStackStatusFilter = "RemoteUpdateSuccess"
|
||||
statusFilterImagesPulled EdgeStackStatusFilter = "ImagesPulled"
|
||||
)
|
||||
|
||||
type EnvironmentsQuery struct {
|
||||
search string
|
||||
types []portainer.EndpointType
|
||||
|
@ -45,7 +33,7 @@ type EnvironmentsQuery struct {
|
|||
agentVersions []string
|
||||
edgeCheckInPassedSeconds int
|
||||
edgeStackId portainer.EdgeStackID
|
||||
edgeStackStatus EdgeStackStatusFilter
|
||||
edgeStackStatus *portainer.EdgeStackStatusType
|
||||
}
|
||||
|
||||
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
|
@ -99,7 +87,18 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
|||
|
||||
edgeStackId, _ := request.RetrieveNumericQueryParameter(r, "edgeStackId", true)
|
||||
|
||||
edgeStackStatus, _ := request.RetrieveQueryParameter(r, "edgeStackStatus", true)
|
||||
edgeStackStatusQuery, _ := request.RetrieveQueryParameter(r, "edgeStackStatus", true)
|
||||
var edgeStackStatus *portainer.EdgeStackStatusType
|
||||
if edgeStackStatusQuery != "" {
|
||||
edgeStackStatusNumber, err := strconv.Atoi(edgeStackStatusQuery)
|
||||
if err != nil ||
|
||||
edgeStackStatusNumber < 0 ||
|
||||
edgeStackStatusNumber > int(portainer.EdgeStackStatusRemoving) {
|
||||
return EnvironmentsQuery{}, errors.New("invalid edgeStackStatus parameter")
|
||||
}
|
||||
|
||||
edgeStackStatus = ptr(portainer.EdgeStackStatusType(edgeStackStatusNumber))
|
||||
}
|
||||
|
||||
return EnvironmentsQuery{
|
||||
search: search,
|
||||
|
@ -116,7 +115,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
|||
agentVersions: agentVersions,
|
||||
edgeCheckInPassedSeconds: edgeCheckInPassedSeconds,
|
||||
edgeStackId: portainer.EdgeStackID(edgeStackId),
|
||||
edgeStackStatus: EdgeStackStatusFilter(edgeStackStatus),
|
||||
edgeStackStatus: edgeStackStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -213,30 +212,21 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End
|
|||
return filteredEndpoints, totalAvailableEndpoints, nil
|
||||
}
|
||||
|
||||
func endpointStatusInStackMatchesFilter(stackStatus map[portainer.EndpointID]portainer.EdgeStackStatus, envId portainer.EndpointID, statusFilter EdgeStackStatusFilter) bool {
|
||||
status, ok := stackStatus[envId]
|
||||
func endpointStatusInStackMatchesFilter(edgeStackStatus map[portainer.EndpointID]portainer.EdgeStackStatus, envId portainer.EndpointID, statusFilter portainer.EdgeStackStatusType) bool {
|
||||
status, ok := edgeStackStatus[envId]
|
||||
|
||||
// consider that if the env has no status in the stack it is in Pending state
|
||||
// workaround because Stack.Status[EnvId].Details.Pending is never set to True in the codebase
|
||||
if !ok && statusFilter == statusFilterPending {
|
||||
if !ok && statusFilter == portainer.EdgeStackStatusPending {
|
||||
return true
|
||||
}
|
||||
|
||||
valueMap := map[EdgeStackStatusFilter]bool{
|
||||
statusFilterPending: status.Details.Pending,
|
||||
statusFilterOk: status.Details.Ok,
|
||||
statusFilterError: status.Details.Error,
|
||||
statusFilterAcknowledged: status.Details.Acknowledged,
|
||||
statusFilterRemove: status.Details.Remove,
|
||||
statusFilterRemoteUpdateSuccess: status.Details.RemoteUpdateSuccess,
|
||||
statusFilterImagesPulled: status.Details.ImagesPulled,
|
||||
}
|
||||
|
||||
currentStatus, ok := valueMap[statusFilter]
|
||||
return ok && currentStatus
|
||||
return slices.ContainsFunc(status.Status, func(s portainer.EdgeStackDeploymentStatus) bool {
|
||||
return s.Type == statusFilter
|
||||
})
|
||||
}
|
||||
|
||||
func filterEndpointsByEdgeStack(endpoints []portainer.Endpoint, edgeStackId portainer.EdgeStackID, statusFilter EdgeStackStatusFilter, datastore dataservices.DataStore) ([]portainer.Endpoint, error) {
|
||||
func filterEndpointsByEdgeStack(endpoints []portainer.Endpoint, edgeStackId portainer.EdgeStackID, statusFilter *portainer.EdgeStackStatusType, datastore dataservices.DataStore) ([]portainer.Endpoint, error) {
|
||||
stack, err := datastore.EdgeStack().EdgeStack(edgeStackId)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "Unable to retrieve edge stack from the database")
|
||||
|
@ -258,10 +248,10 @@ func filterEndpointsByEdgeStack(endpoints []portainer.Endpoint, edgeStackId port
|
|||
envIds = append(envIds, edgeGroup.Endpoints...)
|
||||
}
|
||||
|
||||
if statusFilter != "" {
|
||||
if statusFilter != nil {
|
||||
n := 0
|
||||
for _, envId := range envIds {
|
||||
if endpointStatusInStackMatchesFilter(stack.Status, envId, statusFilter) {
|
||||
if endpointStatusInStackMatchesFilter(stack.Status, envId, *statusFilter) {
|
||||
envIds[n] = envId
|
||||
n++
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package endpoints
|
||||
|
||||
func ptr[T any](i T) *T { return &i }
|
||||
|
||||
func BoolAddr(b bool) *bool {
|
||||
boolVar := b
|
||||
return &boolVar
|
||||
return ptr(b)
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func (service *Service) BuildEdgeStack(
|
|||
DeploymentType: deploymentType,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: edgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus, 0),
|
||||
Version: 1,
|
||||
UseManifestNamespaces: useManifestNamespaces,
|
||||
}, nil
|
||||
|
|
27
api/internal/edge/edgestacks/status.go
Normal file
27
api/internal/edge/edgestacks/status.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package edgestacks
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// NewStatus returns a new status object for an Edge stack
|
||||
func NewStatus(oldStatus map[portainer.EndpointID]portainer.EdgeStackStatus, relatedEnvironmentIDs []portainer.EndpointID) map[portainer.EndpointID]portainer.EdgeStackStatus {
|
||||
status := map[portainer.EndpointID]portainer.EdgeStackStatus{}
|
||||
|
||||
for _, environmentID := range relatedEnvironmentIDs {
|
||||
|
||||
newEnvStatus := portainer.EdgeStackStatus{
|
||||
Status: []portainer.EdgeStackDeploymentStatus{},
|
||||
EndpointID: portainer.EndpointID(environmentID),
|
||||
}
|
||||
|
||||
oldEnvStatus, ok := oldStatus[environmentID]
|
||||
if ok {
|
||||
newEnvStatus.DeploymentInfo = oldEnvStatus.DeploymentInfo
|
||||
}
|
||||
|
||||
status[environmentID] = newEnvStatus
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
|
@ -2,14 +2,33 @@ package slices
|
|||
|
||||
// Contains is a generic function that returns true if the element is contained within the slice
|
||||
func Contains[T comparable](elems []T, v T) bool {
|
||||
return ContainsFunc(elems, func(s T) bool {
|
||||
return s == v
|
||||
})
|
||||
}
|
||||
|
||||
// Contains is a generic function that returns true if the element is contained within the slice
|
||||
func ContainsFunc[T any](elems []T, f func(T) bool) bool {
|
||||
for _, s := range elems {
|
||||
if v == s {
|
||||
if f(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Find[T any](elems []T, f func(T) bool) (T, bool) {
|
||||
for _, s := range elems {
|
||||
if f(s) {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
|
||||
// return default value
|
||||
var result T
|
||||
return result, false
|
||||
}
|
||||
|
||||
// IndexFunc returns the first index i satisfying f(s[i]),
|
||||
// or -1 if none do.
|
||||
func IndexFunc[E any](s []E, f func(E) bool) int {
|
||||
|
|
|
@ -310,15 +310,16 @@ type (
|
|||
//EdgeStack represents an edge stack
|
||||
EdgeStack struct {
|
||||
// EdgeStack Identifier
|
||||
ID EdgeStackID `json:"Id" example:"1"`
|
||||
Name string `json:"Name"`
|
||||
Status map[EndpointID]EdgeStackStatus `json:"Status"`
|
||||
CreationDate int64 `json:"CreationDate"`
|
||||
EdgeGroups []EdgeGroupID `json:"EdgeGroups"`
|
||||
ProjectPath string `json:"ProjectPath"`
|
||||
EntryPoint string `json:"EntryPoint"`
|
||||
Version int `json:"Version"`
|
||||
NumDeployments int `json:"NumDeployments"`
|
||||
ID EdgeStackID `json:"Id" example:"1"`
|
||||
Name string `json:"Name"`
|
||||
Status map[EndpointID]EdgeStackStatus `json:"Status"`
|
||||
// StatusArray map[EndpointID][]EdgeStackStatus `json:"StatusArray"`
|
||||
CreationDate int64 `json:"CreationDate"`
|
||||
EdgeGroups []EdgeGroupID `json:"EdgeGroups"`
|
||||
ProjectPath string `json:"ProjectPath"`
|
||||
EntryPoint string `json:"EntryPoint"`
|
||||
Version int `json:"Version"`
|
||||
NumDeployments int `json:"NumDeployments"`
|
||||
ManifestPath string
|
||||
DeploymentType EdgeStackDeploymentType
|
||||
// Uses the manifest's namespaces instead of the default one
|
||||
|
@ -345,16 +346,26 @@ type (
|
|||
|
||||
//EdgeStackStatus represents an edge stack status
|
||||
EdgeStackStatus struct {
|
||||
Details EdgeStackStatusDetails `json:"Details"`
|
||||
Error string `json:"Error"`
|
||||
EndpointID EndpointID `json:"EndpointID"`
|
||||
Status []EdgeStackDeploymentStatus
|
||||
EndpointID EndpointID
|
||||
// EE only feature
|
||||
DeploymentInfo StackDeploymentInfo `json:"DeploymentInfo"`
|
||||
DeploymentInfo StackDeploymentInfo
|
||||
|
||||
// Deprecated
|
||||
Details EdgeStackStatusDetails
|
||||
// Deprecated
|
||||
Error string
|
||||
// Deprecated
|
||||
Type EdgeStackStatusType `json:"Type"`
|
||||
}
|
||||
|
||||
// EdgeStackDeploymentStatus represents an edge stack deployment status
|
||||
EdgeStackDeploymentStatus struct {
|
||||
Time int64
|
||||
Type EdgeStackStatusType
|
||||
Error string
|
||||
}
|
||||
|
||||
//EdgeStackStatusType represents an edge stack status type
|
||||
EdgeStackStatusType int
|
||||
|
||||
|
@ -1647,18 +1658,24 @@ const (
|
|||
const (
|
||||
// EdgeStackStatusPending represents a pending edge stack
|
||||
EdgeStackStatusPending EdgeStackStatusType = iota
|
||||
//EdgeStackStatusOk represents a successfully deployed edge stack
|
||||
EdgeStackStatusOk
|
||||
//EdgeStackStatusError represents an edge environment(endpoint) which failed to deploy its edge stack
|
||||
//EdgeStackStatusDeploymentReceived represents an edge environment which received the edge stack deployment
|
||||
EdgeStackStatusDeploymentReceived
|
||||
//EdgeStackStatusError represents an edge environment which failed to deploy its edge stack
|
||||
EdgeStackStatusError
|
||||
//EdgeStackStatusAcknowledged represents an acknowledged edge stack
|
||||
EdgeStackStatusAcknowledged
|
||||
//EdgeStackStatusRemove represents a removed edge stack (status isn't persisted)
|
||||
EdgeStackStatusRemove
|
||||
//EdgeStackStatusRemoved represents a removed edge stack
|
||||
EdgeStackStatusRemoved
|
||||
// StatusRemoteUpdateSuccess represents a successfully updated edge stack
|
||||
EdgeStackStatusRemoteUpdateSuccess
|
||||
// EdgeStackStatusImagesPulled represents a successfully images-pulling
|
||||
EdgeStackStatusImagesPulled
|
||||
// EdgeStackStatusRunning represents a running Edge stack
|
||||
EdgeStackStatusRunning
|
||||
// EdgeStackStatusDeploying represents an Edge stack which is being deployed
|
||||
EdgeStackStatusDeploying
|
||||
// EdgeStackStatusRemoving represents an Edge stack which is being removed
|
||||
EdgeStackStatusRemoving
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue