fix(stack): persist CreatedBy before deployment to prevent broken auto update [BE-12939] (#2588)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user