fix(stack): persist CreatedBy before deployment to prevent broken auto update [BE-12939] (#2588)

This commit is contained in:
Oscar Zhou
2026-05-11 12:54:04 +12:00
committed by GitHub
parent 8d098a2bb9
commit a117e514e4
17 changed files with 58 additions and 55 deletions
@@ -27,13 +27,17 @@ func CreateComposeStackFileBuilder(securityContext *security.RestrictedRequestCo
}
}
func (b *ComposeStackFileBuilder) prepare(_ context.Context, payload *StackPayload) error {
func (b *ComposeStackFileBuilder) prepare(_ context.Context, payload *StackPayload, userID portainer.UserID) error {
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerComposeStack
b.stack.EntryPoint = filesystem.ComposeFileDefaultName
b.stack.Env = payload.Env
b.stack.FromAppTemplate = payload.FromAppTemplate
if err := b.initCreatedBy(userID); err != nil {
return err
}
return b.storeStackFile(payload.StackFileContent)
}
@@ -33,14 +33,14 @@ func CreateComposeStackGitBuilder(securityContext *security.RestrictedRequestCon
}
}
func (b *ComposeStackGitBuilder) prepare(ctx context.Context, payload *StackPayload) error {
func (b *ComposeStackGitBuilder) prepare(ctx context.Context, payload *StackPayload, userID portainer.UserID) error {
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerComposeStack
b.stack.EntryPoint = payload.ComposeFile
b.stack.FromAppTemplate = payload.FromAppTemplate
b.stack.Env = payload.Env
return b.GitMethodStackBuilder.prepare(ctx, payload)
return b.GitMethodStackBuilder.prepare(ctx, payload, userID)
}
func (b *ComposeStackGitBuilder) deploy(ctx context.Context, endpoint *portainer.Endpoint) error {
+3 -3
View File
@@ -17,7 +17,7 @@ type stackBuildProcess interface {
setGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint)
// prepare handles all pre-save steps: sets type-specific metadata, stores
// files on disk, or clones the git repository.
prepare(ctx context.Context, payload *StackPayload) error
prepare(ctx context.Context, payload *StackPayload, userID portainer.UserID) error
saveStack() (*portainer.Stack, error)
deploy(ctx context.Context, endpoint *portainer.Endpoint) error
// postDeploy runs after a successful deployment: for git builders it enables
@@ -33,10 +33,10 @@ type stackBuildProcess interface {
// The stack is saved to DB with Status=Deploying and returned immediately.
// Deployment runs in a background goroutine. The caller must poll
// GET /stacks/{id} to track completion.
func Build(ctx context.Context, dataStore dataservices.DataStore, builder stackBuildProcess, payload *StackPayload, endpoint *portainer.Endpoint) (*portainer.Stack, *httperror.HandlerError) {
func Build(ctx context.Context, dataStore dataservices.DataStore, builder stackBuildProcess, payload *StackPayload, endpoint *portainer.Endpoint, userID portainer.UserID) (*portainer.Stack, *httperror.HandlerError) {
builder.setGeneralInfo(payload, endpoint)
if err := builder.prepare(ctx, payload); err != nil {
if err := builder.prepare(ctx, payload, userID); err != nil {
return nil, httperror.InternalServerError("Failed to prepare stack", err)
}
+9 -7
View File
@@ -37,7 +37,9 @@ func (s *stubBuilder) setGeneralInfo(_ *StackPayload, _ *portainer.Endpoint) {
}
}
func (s *stubBuilder) prepare(_ context.Context, _ *StackPayload) error { return nil }
func (s *stubBuilder) prepare(_ context.Context, _ *StackPayload, _ portainer.UserID) error {
return nil
}
func (s *stubBuilder) saveStack() (*portainer.Stack, error) {
if s.saveErr != nil {
@@ -80,7 +82,7 @@ func TestBuild_SaveError_ErrUnauthorized_ReturnsInternalServerError(t *testing.T
t.Parallel()
builder := &stubBuilder{saveErr: httperrors.ErrUnauthorized}
_, herr := Build(t.Context(), nil, builder, &StackPayload{}, &portainer.Endpoint{})
_, herr := Build(t.Context(), nil, builder, &StackPayload{}, &portainer.Endpoint{}, 0)
require.NotNil(t, herr)
assert.Equal(t, http.StatusInternalServerError, herr.StatusCode)
@@ -90,7 +92,7 @@ func TestBuild_SaveError_ReturnsInternalServerError(t *testing.T) {
t.Parallel()
builder := &stubBuilder{saveErr: errors.New("db error")}
_, herr := Build(t.Context(), nil, builder, &StackPayload{}, &portainer.Endpoint{})
_, herr := Build(t.Context(), nil, builder, &StackPayload{}, &portainer.Endpoint{}, 0)
require.NotNil(t, herr)
assert.Equal(t, http.StatusInternalServerError, herr.StatusCode)
@@ -102,7 +104,7 @@ func TestBuild_SpawnAsync_DeploySuccess_UpdatesStackStatusToActive(t *testing.T)
stack := &portainer.Stack{ID: 1}
builder := &stubBuilder{store: store, savedStack: stack}
_, herr := Build(t.Context(), store, builder, &StackPayload{}, &portainer.Endpoint{})
_, herr := Build(t.Context(), store, builder, &StackPayload{}, &portainer.Endpoint{}, 0)
require.Nil(t, herr)
updated := waitForStackStatus(t, store, stack.ID, portainer.StackStatusActive)
@@ -120,7 +122,7 @@ func TestBuild_SpawnAsync_DeployFailure_UpdatesStackStatusToError(t *testing.T)
stack := &portainer.Stack{ID: 1}
builder := &stubBuilder{store: store, savedStack: stack, deployErr: deployErr}
_, herr := Build(t.Context(), store, builder, &StackPayload{}, &portainer.Endpoint{})
_, herr := Build(t.Context(), store, builder, &StackPayload{}, &portainer.Endpoint{}, 0)
require.Nil(t, herr)
updated := waitForStackStatus(t, store, stack.ID, portainer.StackStatusError)
@@ -139,7 +141,7 @@ func TestBuild_SpawnAsync_PostDeployHook_CalledOnSuccess(t *testing.T) {
stack := &portainer.Stack{ID: 1}
builder := &stubBuilder{store: store, savedStack: stack}
_, herr := Build(t.Context(), store, builder, &StackPayload{}, &portainer.Endpoint{})
_, herr := Build(t.Context(), store, builder, &StackPayload{}, &portainer.Endpoint{}, 0)
require.Nil(t, herr)
waitForStackStatus(t, store, stack.ID, portainer.StackStatusActive)
@@ -153,7 +155,7 @@ func TestBuild_SpawnAsync_PostDeployHook_NotCalledOnDeployFailure(t *testing.T)
stack := &portainer.Stack{ID: 1}
builder := &stubBuilder{store: store, savedStack: stack, deployErr: errors.New("failed to deploy")}
_, herr := Build(t.Context(), store, builder, &StackPayload{}, &portainer.Endpoint{})
_, herr := Build(t.Context(), store, builder, &StackPayload{}, &portainer.Endpoint{}, 0)
require.Nil(t, herr)
waitForStackStatus(t, store, stack.ID, portainer.StackStatusError)
+2 -4
View File
@@ -35,14 +35,12 @@ func CreateKubernetesStackGitBuilder(dataStore dataservices.DataStore,
}
}
func (b *KubernetesStackGitBuilder) prepare(ctx context.Context, payload *StackPayload) error {
func (b *KubernetesStackGitBuilder) prepare(ctx context.Context, payload *StackPayload, userID portainer.UserID) error {
b.stack.Type = portainer.KubernetesStack
b.stack.Namespace = payload.Namespace
b.stack.Name = payload.StackName
b.stack.EntryPoint = payload.ManifestFile
b.stack.CreatedBy = b.user.Username
if err := b.GitMethodStackBuilder.prepare(ctx, payload); err != nil {
if err := b.GitMethodStackBuilder.prepare(ctx, payload, userID); err != nil {
return err
}
@@ -57,14 +57,17 @@ func CreateKubernetesStackUrlBuilder(dataStore dataservices.DataStore,
}
}
func (b *KubernetesStackBuilder) prepare(_ context.Context, payload *StackPayload) error {
func (b *KubernetesStackBuilder) prepare(_ context.Context, payload *StackPayload, userID portainer.UserID) error {
b.stack.Name = payload.StackName
b.stack.Type = portainer.KubernetesStack
b.stack.EntryPoint = filesystem.ManifestFileDefaultName
b.stack.Namespace = payload.Namespace
b.stack.CreatedBy = b.user.Username
b.stack.FromAppTemplate = payload.FromAppTemplate
if err := b.initCreatedBy(userID); err != nil {
return err
}
content, err := b.contentFn(payload)
if err != nil {
return fmt.Errorf("unable to retrieve manifest content: %w", err)
+11 -4
View File
@@ -45,8 +45,6 @@ func (b *StackBuilder) setGeneralInfo(_ *StackPayload, endpoint *portainer.Endpo
stackutils.PrepareStackStatusForDeployment(b.stack)
}
func (b *StackBuilder) prepare(_ context.Context, _ *StackPayload) error { return nil }
func (b *StackBuilder) deploy(ctx context.Context, _ *portainer.Endpoint) error {
return b.deploymentConfiger.Deploy(ctx)
}
@@ -83,6 +81,17 @@ func (b *StackBuilder) cleanUp() error {
return nil
}
func (b *StackBuilder) initCreatedBy(userID portainer.UserID) error {
return b.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
user, err := tx.User().Read(userID)
if err != nil {
return fmt.Errorf("failed to find stack author: %w", err)
}
b.stack.CreatedBy = user.Username
return nil
})
}
func (b *StackBuilder) storeStackFile(content []byte) error {
stackFolder := strconv.Itoa(int(b.stack.ID))
projectPath, err := b.fileService.StoreStackFileFromBytes(stackFolder, b.stack.EntryPoint, content)
@@ -102,7 +111,6 @@ func (b *StackBuilder) initComposeDeployment(secCtx *security.RestrictedRequestC
}
b.deploymentConfiger = config
b.stack.CreatedBy = config.GetUsername()
return nil
}
@@ -114,7 +122,6 @@ func (b *StackBuilder) initSwarmDeployment(secCtx *security.RestrictedRequestCon
}
b.deploymentConfiger = config
b.stack.CreatedBy = config.GetUsername()
return nil
}
@@ -20,10 +20,14 @@ type GitMethodStackBuilder struct {
scheduler *scheduler.Scheduler
}
func (b *GitMethodStackBuilder) prepare(ctx context.Context, payload *StackPayload) error {
func (b *GitMethodStackBuilder) prepare(ctx context.Context, payload *StackPayload, userID portainer.UserID) error {
b.stack.AdditionalFiles = payload.AdditionalFiles
b.stack.AutoUpdate = payload.AutoUpdate
if err := b.initCreatedBy(userID); err != nil {
return err
}
var repoConfig gittypes.RepoConfig
if payload.Authentication {
repoConfig.Authentication = &gittypes.GitAuthentication{
@@ -27,7 +27,7 @@ func CreateSwarmStackFileBuilder(securityContext *security.RestrictedRequestCont
}
}
func (b *SwarmStackFileBuilder) prepare(_ context.Context, payload *StackPayload) error {
func (b *SwarmStackFileBuilder) prepare(_ context.Context, payload *StackPayload, userID portainer.UserID) error {
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerSwarmStack
b.stack.SwarmID = payload.SwarmID
@@ -35,6 +35,10 @@ func (b *SwarmStackFileBuilder) prepare(_ context.Context, payload *StackPayload
b.stack.Env = payload.Env
b.stack.FromAppTemplate = payload.FromAppTemplate
if err := b.initCreatedBy(userID); err != nil {
return err
}
return b.storeStackFile(payload.StackFileContent)
}
@@ -33,7 +33,7 @@ func CreateSwarmStackGitBuilder(securityContext *security.RestrictedRequestConte
}
}
func (b *SwarmStackGitBuilder) prepare(ctx context.Context, payload *StackPayload) error {
func (b *SwarmStackGitBuilder) prepare(ctx context.Context, payload *StackPayload, userID portainer.UserID) error {
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerSwarmStack
b.stack.SwarmID = payload.SwarmID
@@ -41,7 +41,7 @@ func (b *SwarmStackGitBuilder) prepare(ctx context.Context, payload *StackPayloa
b.stack.FromAppTemplate = payload.FromAppTemplate
b.stack.Env = payload.Env
return b.GitMethodStackBuilder.prepare(ctx, payload)
return b.GitMethodStackBuilder.prepare(ctx, payload, userID)
}
// deploy creates deployment configuration for swarm stack