From 7bfc542c501f7321f66013fd0ac53997e9c16dfb Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sun, 2 Aug 2026 14:40:00 +0300 Subject: [PATCH] refactor(edgestacks): move update machinery into the service [BE-13183] (#3301) Co-authored-by: Claude Sonnet 5 Co-authored-by: andres-portainer <91705312+andres-portainer@users.noreply.github.com> --- .../handler/edgestacks/edgestack_create.go | 4 + .../edgestacks/edgestack_create_git.go | 6 +- api/http/handler/edgestacks/git_sync_error.go | 87 +++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 api/http/handler/edgestacks/git_sync_error.go diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index 05fbb977e0..da5c1143c3 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -30,6 +30,10 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) edgeStack, err = handler.createSwarmStack(tx, method, dryrun, tokenData, r) return err }); err != nil { + if securityContext, secErr := security.RetrieveRestrictedRequestContext(r); secErr == nil { + handler.persistGitSyncFailure(securityContext, err) + } + switch { case httperrors.IsInvalidPayloadError(err): return httperror.BadRequest("Invalid payload", err) diff --git a/api/http/handler/edgestacks/edgestack_create_git.go b/api/http/handler/edgestacks/edgestack_create_git.go index 3541460d1a..fd53a50600 100644 --- a/api/http/handler/edgestacks/edgestack_create_git.go +++ b/api/http/handler/edgestacks/edgestack_create_git.go @@ -184,11 +184,7 @@ func (handler *Handler) storeManifestFromGitRepository(ctx context.Context, tx d repositoryPassword, repositoryConfig.TLSSkipVerify, ); err != nil { - if statusErr := workflows.SaveSourceStatus(tx, userContext, sourceID, err); statusErr != nil { - return "", "", "", fmt.Errorf("%w (and failed to persist status: %w)", err, statusErr) - } - - return "", "", "", err + return "", "", "", NewGitSyncSourceError(sourceID, "Failed cloning repository", err) } if err := workflows.SaveSourceStatus(tx, userContext, sourceID, nil); err != nil { diff --git a/api/http/handler/edgestacks/git_sync_error.go b/api/http/handler/edgestacks/git_sync_error.go new file mode 100644 index 0000000000..fb46d3d8e3 --- /dev/null +++ b/api/http/handler/edgestacks/git_sync_error.go @@ -0,0 +1,87 @@ +package edgestacks + +import ( + "errors" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/dataservices/source" + "github.com/portainer/portainer/api/gitops/workflows" + "github.com/portainer/portainer/api/http/security" + httperror "github.com/portainer/portainer/pkg/libhttp/error" + + "github.com/rs/zerolog/log" +) + +// GitSyncSourceError wraps a failed git operation together with the source it must be recorded +// against. The transaction observing the failure returns a non-nil error and gets rolled back, +// so the source status can't be persisted as part of it: the caller persists it separately, +// once the rollback has already happened. +type GitSyncSourceError struct { + SourceID portainer.SourceID + Cause error + httpErr *httperror.HandlerError +} + +func (e *GitSyncSourceError) Error() string { return e.httpErr.Error() } +func (e *GitSyncSourceError) Unwrap() error { return e.httpErr } + +// NewGitSyncSourceError builds a GitSyncSourceError, pre-rendering the HTTP response from msg and +// cause so persistGitSyncFailure can persist the source status after rollback without +// re-deriving it. +func NewGitSyncSourceError(sourceID portainer.SourceID, msg string, cause error) *GitSyncSourceError { + return &GitSyncSourceError{SourceID: sourceID, Cause: cause, httpErr: httperror.InternalServerError(msg, cause)} +} + +// GitSyncWorkflowError wraps a failed git operation scoped to a specific workflow's edge stack +// artifact, together with the identifiers needed to record it once the transaction observing the +// failure has already been rolled back. +type GitSyncWorkflowError struct { + WorkflowID portainer.WorkflowID + EdgeStackID portainer.EdgeStackID + SourceID portainer.SourceID + Cause error + httpErr *httperror.HandlerError +} + +func (e *GitSyncWorkflowError) Error() string { return e.httpErr.Error() } +func (e *GitSyncWorkflowError) Unwrap() error { return e.httpErr } + +// NewGitSyncWorkflowError builds a GitSyncWorkflowError, pre-rendering the HTTP response from msg +// and cause so persistGitSyncFailure can persist the edge stack's artifact status after rollback +// without re-deriving it. +func NewGitSyncWorkflowError(workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID, sourceID portainer.SourceID, msg string, cause error) *GitSyncWorkflowError { + return &GitSyncWorkflowError{WorkflowID: workflowID, EdgeStackID: edgeStackID, SourceID: sourceID, Cause: cause, httpErr: httperror.InternalServerError(msg, cause)} +} + +// persistGitSyncFailure records a failed git operation in a follow-up transaction, after the +// transaction that observed the failure has already been rolled back. Workflow-scoped failures +// update the edge stack's artifact status; other failures update only the source's sync status. +func (handler *Handler) persistGitSyncFailure(securityContext *security.RestrictedRequestContext, err error) { + userContext := source.NewUserContext(securityContext.User, securityContext.UserMemberships) + + if workflowErr, ok := errors.AsType[*GitSyncWorkflowError](err); ok { + handler.persistWorkflowGitSyncFailure(userContext, workflowErr) + return + } + + if sourceErr, ok := errors.AsType[*GitSyncSourceError](err); ok { + handler.persistSourceGitSyncFailure(userContext, sourceErr) + } +} + +func (handler *Handler) persistWorkflowGitSyncFailure(userContext source.UserContext, syncErr *GitSyncWorkflowError) { + if persistErr := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error { + return workflows.SaveEdgeStackStatus(tx, userContext, syncErr.WorkflowID, syncErr.EdgeStackID, syncErr.SourceID, syncErr.Cause) + }); persistErr != nil { + log.Warn().Err(persistErr).Msg("failed to persist git sync status after a failed edge stack operation") + } +} + +func (handler *Handler) persistSourceGitSyncFailure(userContext source.UserContext, syncErr *GitSyncSourceError) { + if persistErr := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error { + return workflows.SaveSourceStatus(tx, userContext, syncErr.SourceID, syncErr.Cause) + }); persistErr != nil { + log.Warn().Err(persistErr).Msg("failed to persist git sync status after a failed edge stack operation") + } +}