diff --git a/api/exec/compose_stack.go b/api/exec/compose_stack.go index 3a2776031..9ae3bacd2 100644 --- a/api/exec/compose_stack.go +++ b/api/exec/compose_stack.go @@ -76,15 +76,9 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S defer proxy.Close() } - envFilePath, err := createEnvFile(stack) - if err != nil { - return errors.Wrap(err, "failed to create env file") - } - err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{ - WorkingDir: stack.ProjectPath, - EnvFilePath: envFilePath, - Host: url, + WorkingDir: "", + Host: url, }) return errors.Wrap(err, "failed to remove a stack") @@ -148,28 +142,46 @@ func createEnvFile(stack *portainer.Stack) (string, error) { } defer envfile.Close() - copyDefaultEnvFile(stack, envfile) + // Copy from default .env file + defaultEnvPath := path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env") + if err = copyDefaultEnvFile(envfile, defaultEnvPath); err != nil { + return "", err + } - for _, v := range stack.Env { - envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value)) + // Copy from stack env vars + if err = copyConfigEnvVars(envfile, stack.Env); err != nil { + return "", err } return "stack.env", nil } // copyDefaultEnvFile copies the default .env file if it exists to the provided writer -func copyDefaultEnvFile(stack *portainer.Stack, w io.Writer) { - defaultEnvFile, err := os.Open(path.Join(path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint)), ".env")) +func copyDefaultEnvFile(w io.Writer, defaultEnvFilePath string) error { + defaultEnvFile, err := os.Open(defaultEnvFilePath) if err != nil { // If cannot open a default file, then don't need to copy it. // We could as well stat it and check if it exists, but this is more efficient. - return + return nil } defer defaultEnvFile.Close() if _, err = io.Copy(w, defaultEnvFile); err == nil { - io.WriteString(w, "\n") + if _, err = fmt.Fprintf(w, "\n"); err != nil { + return fmt.Errorf("failed to copy default env file: %w", err) + } } + return nil // If couldn't copy the .env file, then ignore the error and try to continue } + +// copyConfigEnvVars write the environment variables from stack configuration to the writer +func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error { + for _, v := range envs { + if _, err := fmt.Fprintf(w, "%s=%s\n", v.Name, v.Value); err != nil { + return fmt.Errorf("failed to copy config env vars: %w", err) + } + } + return nil +} diff --git a/api/exec/compose_stack_test.go b/api/exec/compose_stack_test.go index b1904438a..ddaba66bb 100644 --- a/api/exec/compose_stack_test.go +++ b/api/exec/compose_stack_test.go @@ -23,6 +23,7 @@ func Test_createEnvFile(t *testing.T) { name: "should not add env file option if stack doesn't have env variables", stack: &portainer.Stack{ ProjectPath: dir, + Env: nil, }, expected: "", }, diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index 36d99d2b9..be82af386 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -157,7 +157,10 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor var stderr bytes.Buffer cmd := exec.Command(command, args...) cmd.Stderr = &stderr - cmd.Dir = workingDir + + if workingDir != "" { + cmd.Dir = workingDir + } if env != nil { cmd.Env = os.Environ() diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index ee63f4de7..a70d46cdf 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -245,7 +245,16 @@ func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.St } out, err := handler.KubernetesDeployer.Remove(userID, endpoint, manifestFiles, stack.Namespace) - + if err != nil { + for _, manifest := range manifestFiles { + if exists, fileExistsErr := filesystem.FileExists(manifest); fileExistsErr != nil || !exists { + // If removal has failed and one of the manifest files is missing, + // we can consider this stack as removed + log.Warn().Err(fileExistsErr).Msgf("failed to find manifest %s, but stack deletion will continue", manifest) + return nil + } + } + } return errors.WithMessagef(err, "failed to remove kubernetes resources: %q", out) } diff --git a/pkg/libstack/compose/internal/composeplugin/composeplugin.go b/pkg/libstack/compose/internal/composeplugin/composeplugin.go index 26d09d4a8..5516da640 100644 --- a/pkg/libstack/compose/internal/composeplugin/composeplugin.go +++ b/pkg/libstack/compose/internal/composeplugin/composeplugin.go @@ -136,7 +136,12 @@ func (wrapper *PluginWrapper) command(command composeCommand, options libstack.O args = append(args, command.ToArgs()...) cmd := exec.Command(program, args...) - cmd.Dir = options.WorkingDir + if options.WorkingDir != "" { + // Specify an non-exist working directory will cause the failure + // of the "docker-compose down" command even if the project name + // is correct. + cmd.Dir = options.WorkingDir + } if wrapper.configPath != "" || len(options.Env) > 0 { cmd.Env = os.Environ()