diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 7d0883062..23cbb3dad 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -160,7 +160,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, handler.FileService, handler.StackDeployer) - stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, composeStackBuilder, &stackPayload, endpoint) + stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, composeStackBuilder, &stackPayload, endpoint, userID) if httpErr != nil { return httpErr } @@ -297,7 +297,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite handler.Scheduler, handler.StackDeployer) - stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, composeStackBuilder, &stackPayload, endpoint) + stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, composeStackBuilder, &stackPayload, endpoint, userID) if httpErr != nil { return httpErr } @@ -383,7 +383,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, handler.FileService, handler.StackDeployer) - stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, composeStackBuilder, &stackPayload, endpoint) + stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, composeStackBuilder, &stackPayload, endpoint, userID) if httpErr != nil { return httpErr } diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index d1b9808e5..915ea53e6 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -168,7 +168,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit } } - if _, err := stackbuilders.Build(r.Context(), handler.DataStore, k8sStackBuilder, &stackPayload, endpoint); err != nil { + if _, err := stackbuilders.Build(r.Context(), handler.DataStore, k8sStackBuilder, &stackPayload, endpoint, userID); err != nil { return err } @@ -240,7 +240,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr handler.KubernetesDeployer, user) - if _, err := stackbuilders.Build(r.Context(), handler.DataStore, k8sStackBuilder, &stackPayload, endpoint); err != nil { + if _, err := stackbuilders.Build(r.Context(), handler.DataStore, k8sStackBuilder, &stackPayload, endpoint, userID); err != nil { return err } @@ -285,7 +285,7 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit handler.KubernetesDeployer, user) - if _, err := stackbuilders.Build(r.Context(), handler.DataStore, k8sStackBuilder, &stackPayload, endpoint); err != nil { + if _, err := stackbuilders.Build(r.Context(), handler.DataStore, k8sStackBuilder, &stackPayload, endpoint, userID); err != nil { return err } diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 29419213c..23c205809 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -96,7 +96,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r handler.FileService, handler.StackDeployer) - stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, swarmStackBuilder, &stackPayload, endpoint) + stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, swarmStackBuilder, &stackPayload, endpoint, userID) if httpErr != nil { return httpErr } @@ -237,7 +237,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, handler.Scheduler, handler.StackDeployer) - stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, swarmStackBuilder, &stackPayload, endpoint) + stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, swarmStackBuilder, &stackPayload, endpoint, userID) if httpErr != nil { return httpErr } @@ -337,7 +337,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r handler.FileService, handler.StackDeployer) - stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, swarmStackBuilder, &stackPayload, endpoint) + stack, httpErr := stackbuilders.Build(r.Context(), handler.DataStore, swarmStackBuilder, &stackPayload, endpoint, userID) if httpErr != nil { return httpErr } diff --git a/api/stacks/deployments/deployment_compose_config.go b/api/stacks/deployments/deployment_compose_config.go index 1b60b4701..14f6b000f 100644 --- a/api/stacks/deployments/deployment_compose_config.go +++ b/api/stacks/deployments/deployment_compose_config.go @@ -60,13 +60,6 @@ func CreateComposeStackDeploymentConfigTx(tx dataservices.DataStoreTx, securityC return config, nil } -func (config *ComposeStackDeploymentConfig) GetUsername() string { - if config.user != nil { - return config.user.Username - } - return "" -} - func (config *ComposeStackDeploymentConfig) Deploy(ctx context.Context) error { if config.FileService == nil || config.StackDeployer == nil { log.Debug().Msg("file service or stack deployer is not initialized") diff --git a/api/stacks/deployments/deployment_config.go b/api/stacks/deployments/deployment_config.go index e8a4f96ff..01ee2e744 100644 --- a/api/stacks/deployments/deployment_config.go +++ b/api/stacks/deployments/deployment_config.go @@ -3,7 +3,6 @@ package deployments import "context" type StackDeploymentConfiger interface { - GetUsername() string Deploy(ctx context.Context) error Undeploy(ctx context.Context) error GetResponse() string diff --git a/api/stacks/deployments/deployment_kubernetes_config.go b/api/stacks/deployments/deployment_kubernetes_config.go index 454528546..30848d370 100644 --- a/api/stacks/deployments/deployment_kubernetes_config.go +++ b/api/stacks/deployments/deployment_kubernetes_config.go @@ -32,10 +32,6 @@ func CreateKubernetesStackDeploymentConfig(stack *portainer.Stack, kubeDeployer } } -func (config *KubernetesStackDeploymentConfig) GetUsername() string { - return config.user.Username -} - func (config *KubernetesStackDeploymentConfig) Deploy(ctx context.Context) error { fileNames := stackutils.GetStackFilePaths(config.stack, false) diff --git a/api/stacks/deployments/deployment_swarm_config.go b/api/stacks/deployments/deployment_swarm_config.go index 5c12abb77..8f70d0e2a 100644 --- a/api/stacks/deployments/deployment_swarm_config.go +++ b/api/stacks/deployments/deployment_swarm_config.go @@ -57,13 +57,6 @@ func CreateSwarmStackDeploymentConfigTx(tx dataservices.DataStoreTx, securityCon return config, nil } -func (config *SwarmStackDeploymentConfig) GetUsername() string { - if config.user != nil { - return config.user.Username - } - return "" -} - func (config *SwarmStackDeploymentConfig) Deploy(ctx context.Context) error { if config.FileService == nil || config.StackDeployer == nil { log.Println("[deployment, swarm] file service or stack deployer is not initialised") diff --git a/api/stacks/stackbuilders/compose_file_builder.go b/api/stacks/stackbuilders/compose_file_builder.go index caeecf97d..a2f7bd561 100644 --- a/api/stacks/stackbuilders/compose_file_builder.go +++ b/api/stacks/stackbuilders/compose_file_builder.go @@ -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) } diff --git a/api/stacks/stackbuilders/compose_git_builder.go b/api/stacks/stackbuilders/compose_git_builder.go index 938dd54b6..6eb77f314 100644 --- a/api/stacks/stackbuilders/compose_git_builder.go +++ b/api/stacks/stackbuilders/compose_git_builder.go @@ -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 { diff --git a/api/stacks/stackbuilders/director.go b/api/stacks/stackbuilders/director.go index 6b6643a65..714352de7 100644 --- a/api/stacks/stackbuilders/director.go +++ b/api/stacks/stackbuilders/director.go @@ -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) } diff --git a/api/stacks/stackbuilders/director_test.go b/api/stacks/stackbuilders/director_test.go index 95aa92b91..a5e10570f 100644 --- a/api/stacks/stackbuilders/director_test.go +++ b/api/stacks/stackbuilders/director_test.go @@ -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) diff --git a/api/stacks/stackbuilders/k8s_git_builder.go b/api/stacks/stackbuilders/k8s_git_builder.go index 48bd1f4fa..6b4c9db22 100644 --- a/api/stacks/stackbuilders/k8s_git_builder.go +++ b/api/stacks/stackbuilders/k8s_git_builder.go @@ -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 } diff --git a/api/stacks/stackbuilders/k8s_stack_builder.go b/api/stacks/stackbuilders/k8s_stack_builder.go index dc47439ef..ac3b1dcc9 100644 --- a/api/stacks/stackbuilders/k8s_stack_builder.go +++ b/api/stacks/stackbuilders/k8s_stack_builder.go @@ -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) diff --git a/api/stacks/stackbuilders/stack_builder.go b/api/stacks/stackbuilders/stack_builder.go index f11cf57ab..5691e7b31 100644 --- a/api/stacks/stackbuilders/stack_builder.go +++ b/api/stacks/stackbuilders/stack_builder.go @@ -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 } diff --git a/api/stacks/stackbuilders/stack_git_builder.go b/api/stacks/stackbuilders/stack_git_builder.go index 47aedc941..52430a1f3 100644 --- a/api/stacks/stackbuilders/stack_git_builder.go +++ b/api/stacks/stackbuilders/stack_git_builder.go @@ -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{ diff --git a/api/stacks/stackbuilders/swarm_file_builder.go b/api/stacks/stackbuilders/swarm_file_builder.go index ddb235022..13720b91c 100644 --- a/api/stacks/stackbuilders/swarm_file_builder.go +++ b/api/stacks/stackbuilders/swarm_file_builder.go @@ -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) } diff --git a/api/stacks/stackbuilders/swarm_git_builder.go b/api/stacks/stackbuilders/swarm_git_builder.go index 9e9b28666..e9ecdbcd8 100644 --- a/api/stacks/stackbuilders/swarm_git_builder.go +++ b/api/stacks/stackbuilders/swarm_git_builder.go @@ -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