From be87d4b6bd7844cf175f74dd25ffc3bca4d19c59 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 8 Jan 2025 09:40:59 -0800 Subject: [PATCH] kie-issues#1647 [kn-plugin-workflow] Executing kn workflow run creates the container in the background (#2778) --- .../e2e-tests/helper_test.go | 71 +++++++++++++++++++ .../kn-plugin-workflow/e2e-tests/run_test.go | 18 ++++- .../kn-plugin-workflow/pkg/command/run.go | 47 +++++++++++- .../pkg/common/containers.go | 44 ++++++++++++ 4 files changed, 176 insertions(+), 4 deletions(-) diff --git a/packages/kn-plugin-workflow/e2e-tests/helper_test.go b/packages/kn-plugin-workflow/e2e-tests/helper_test.go index 392e959ee92..ad7f283e820 100644 --- a/packages/kn-plugin-workflow/e2e-tests/helper_test.go +++ b/packages/kn-plugin-workflow/e2e-tests/helper_test.go @@ -22,6 +22,7 @@ package e2e_tests import ( + "bufio" "bytes" "fmt" "io" @@ -32,6 +33,7 @@ import ( "syscall" "testing" + "github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/command" "github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/command/quarkus" "github.com/spf13/cobra" "github.com/stretchr/testify/require" @@ -63,6 +65,11 @@ func ExecuteKnWorkflowWithCmd(cmd *exec.Cmd, args ...string) (string, error) { return executeCommandWithOutput(cmd, args...) } +// ExecuteKnWorkflowWithCmdAndStopContainer executes the 'kn-workflow' CLI tool with the given arguments using the provided command and returns the containerID and possible error message. +func ExecuteKnWorkflowWithCmdAndStopContainer(cmd *exec.Cmd, args ...string) (string, error) { + return executeCommandWithOutputAndStopContainer(cmd, args...) +} + // ExecuteKnWorkflowQuarkusWithCmd executes the 'kn-workflow' CLI tool with 'quarkus' command with the given arguments using the provided command and returns the command's output and possible error message. func ExecuteKnWorkflowQuarkusWithCmd(cmd *exec.Cmd, args ...string) (string, error) { newArgs := append([]string{"quarkus"}, args...) @@ -89,6 +96,70 @@ func executeCommandWithOutput(cmd *exec.Cmd, args ...string) (string, error) { return stdout.String(), nil } +func executeCommandWithOutputAndStopContainer(cmd *exec.Cmd, args ...string) (string, error) { + cmd.Args = append([]string{cmd.Path}, args...) + + var containerId string + var stderr bytes.Buffer + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return "", fmt.Errorf("failed to create stdout pipe: %w", err) + } + defer stdoutPipe.Close() + + stdinPipe, err := cmd.StdinPipe() + if err != nil { + return "", fmt.Errorf("failed to create stdin pipe: %w", err) + } + defer stdinPipe.Close() + + cmd.Stderr = &stderr + errorCh := make(chan error, 1) + + go func() { + defer close(errorCh) + scanner := bufio.NewScanner(stdoutPipe) + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, "Created container with ID ") { + id, ok := strings.CutPrefix(line, "Created container with ID ") + if !ok || id == "" { + errorCh <- fmt.Errorf("failed to parse container ID from output: %q", line) + return + } + containerId = id + } + + if line == command.StopContainerMsg { + _, err := io.WriteString(stdinPipe, "any\n") + if err != nil { + errorCh <- fmt.Errorf("failed to write to stdin: %w", err) + return + } + } + } + + if err := scanner.Err(); err != nil { + errorCh <- fmt.Errorf("error reading from stdout: %w", err) + return + } + }() + + err = cmd.Run() + if err != nil { + return "", fmt.Errorf("command run error: %w (stderr: %s)", err, stderr.String()) + } + + readErr := <-errorCh + if readErr != nil { + return "", readErr + } + + return containerId, nil +} + // VerifyFileContent verifies that the content of a file matches the expected content. func VerifyFileContent(t *testing.T, filePath string, expected string) { actual, err := os.ReadFile(filePath) diff --git a/packages/kn-plugin-workflow/e2e-tests/run_test.go b/packages/kn-plugin-workflow/e2e-tests/run_test.go index f3f371b4b21..2cabd4d35e7 100644 --- a/packages/kn-plugin-workflow/e2e-tests/run_test.go +++ b/packages/kn-plugin-workflow/e2e-tests/run_test.go @@ -32,6 +32,7 @@ import ( "github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/command" "github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -80,6 +81,7 @@ func TestRunCommand(t *testing.T) { func RunRunTest(t *testing.T, cfgTestInputPrepareCreate CfgTestInputCreate, test cfgTestInputRun) string { var err error + var containerId string // Create the project RunCreateTest(t, cfgTestInputPrepareCreate) @@ -99,7 +101,8 @@ func RunRunTest(t *testing.T, cfgTestInputPrepareCreate CfgTestInputCreate, test // Run the `run` command go func() { defer wg.Done() - _, err = ExecuteKnWorkflowWithCmd(cmd, transformRunCmdCfgToArgs(test.input)...) + containerId, err = ExecuteKnWorkflowWithCmdAndStopContainer(cmd, transformRunCmdCfgToArgs(test.input)...) + assert.NotNil(t, containerId, "Container ID is nil") require.Truef(t, err == nil || IsSignalInterrupt(err), "Expected nil error or signal interrupt, got %v", err) }() @@ -120,5 +123,18 @@ func RunRunTest(t *testing.T, cfgTestInputPrepareCreate CfgTestInputCreate, test wg.Wait() + stopped := make(chan bool) + t.Logf("Checking if container is stopped") + assert.NotNil(t, containerId, "Container ID is nil") + // Check if the container is stopped within a specified time limit. + go common.PollContainerStoppedCheck(containerId, pollInterval, stopped) + select { + case <-stopped: + fmt.Println("Project is stopped") + case <-time.After(timeout): + t.Fatalf("Test case timed out after %s. The project was not stopped within the specified time.", timeout) + cmd.Process.Signal(os.Interrupt) + } + return projectName } diff --git a/packages/kn-plugin-workflow/pkg/command/run.go b/packages/kn-plugin-workflow/pkg/command/run.go index 27ed3ae5e1c..7cb275c632c 100644 --- a/packages/kn-plugin-workflow/pkg/command/run.go +++ b/packages/kn-plugin-workflow/pkg/command/run.go @@ -20,6 +20,7 @@ package command import ( + "bufio" "fmt" "os" "sync" @@ -34,8 +35,12 @@ import ( type RunCmdConfig struct { PortMapping string OpenDevUI bool + StopContainerOnUserInput bool } +const StopContainerMsg = "Press any key to stop the container" + + func NewRunCommand() *cobra.Command { cmd := &cobra.Command{ Use: "run", @@ -56,9 +61,13 @@ func NewRunCommand() *cobra.Command { # Disable automatic browser launch of SonataFlow Dev UI {{.Name}} run --open-dev-ui=false + + # Stop the container when the user presses any key + {{.Name}} run --stop-container-on-user-input=false + `, SuggestFor: []string{"rnu", "start"}, //nolint:misspell - PreRunE: common.BindEnv("port", "open-dev-ui"), + PreRunE: common.BindEnv("port", "open-dev-ui", "stop-container-on-user-input"), } cmd.RunE = func(cmd *cobra.Command, args []string) error { @@ -67,6 +76,7 @@ func NewRunCommand() *cobra.Command { cmd.Flags().StringP("port", "p", "8080", "Maps a different host port to the running container port.") cmd.Flags().Bool("open-dev-ui", true, "Disable automatic browser launch of SonataFlow Dev UI") + cmd.Flags().Bool("stop-container-on-user-input", true, "Stop the container when the user presses any key") cmd.SetHelpFunc(common.DefaultTemplatedHelp) return cmd @@ -92,8 +102,9 @@ func run() error { func runDevCmdConfig() (cfg RunCmdConfig, err error) { cfg = RunCmdConfig{ - PortMapping: viper.GetString("port"), - OpenDevUI: viper.GetBool("open-dev-ui"), + PortMapping: viper.GetString("port"), + OpenDevUI: viper.GetBool("open-dev-ui"), + StopContainerOnUserInput: viper.GetBool("stop-container-on-user-input"), } return } @@ -137,6 +148,36 @@ func runSWFProjectDevMode(containerTool string, cfg RunCmdConfig) (err error) { pollInterval := 5 * time.Second common.ReadyCheck(readyCheckURL, pollInterval, cfg.PortMapping, cfg.OpenDevUI) + if cfg.StopContainerOnUserInput { + if err := stopContainer(containerTool); err != nil { + return err + } + } + wg.Wait() return err } + +func stopContainer(containerTool string) error { + fmt.Println(StopContainerMsg) + + reader := bufio.NewReader(os.Stdin) + + _, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading from stdin: %w", err) + } + + fmt.Println("⏳ Stopping the container...") + + containerID, err := common.GetContainerID(containerTool) + if err != nil { + return err + } + if err := common.StopContainer(containerTool, containerID); err != nil { + return err + } + return nil +} + + diff --git a/packages/kn-plugin-workflow/pkg/common/containers.go b/packages/kn-plugin-workflow/pkg/common/containers.go index afb2482b69e..44d9e3ab883 100644 --- a/packages/kn-plugin-workflow/pkg/common/containers.go +++ b/packages/kn-plugin-workflow/pkg/common/containers.go @@ -389,3 +389,47 @@ func processOutputDuringContainerExecution(cli *client.Client, ctx context.Conte return nil } + + +func PollContainerStoppedCheck(containerID string, interval time.Duration, ready chan<- bool) { + for { + running, err := IsContainerRunning(containerID) + if err != nil { + fmt.Printf("Error checking if container %s is running: %s", containerID, err) + ready <- false + return + } + if !running { + ready <- true + return + } + time.Sleep(interval) + } +} + +func IsContainerRunning(containerID string) (bool, error) { + if errDocker := CheckDocker(); errDocker == nil { + cli, err := getDockerClient() + if err != nil { + return false, fmt.Errorf("unable to create docker client: %w", err) + } + containerJSON, err := cli.ContainerInspect(context.Background(), containerID) + if err != nil { + if client.IsErrNotFound(err) { + return false, nil + } + return false, fmt.Errorf("unable to inspect container %s with docker: %w", containerID, err) + } + return containerJSON.State.Running, nil + + } else if errPodman := CheckPodman(); errPodman == nil { + cmd := exec.Command("podman", "inspect", containerID, "--format", "{{.State.Running}}") + output, err := cmd.Output() + if err != nil { + return false, fmt.Errorf("unable to inspect container %s with podman: %w", containerID, err) + } + return strings.TrimSpace(string(output)) == "true", nil + } + + return false, fmt.Errorf("there is no docker or podman available") +}