1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 12:25:22 +02:00

feat(edge/stacks): increase status transparency [EE-5554] (#9094)

This commit is contained in:
Chaim Lev-Ari 2023-07-13 23:55:52 +03:00 committed by GitHub
parent db61fb149b
commit 0bcb57568c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1305 additions and 316 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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