github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow@v0.28.1-0.20240311201729-34c6856b157f/pkg/common/containers.go (about)

     1  /*
     2   * Licensed to the Apache Software Foundation (ASF) under one
     3   * or more contributor license agreements.  See the NOTICE file
     4   * distributed with this work for additional information
     5   * regarding copyright ownership.  The ASF licenses this file
     6   * to you under the Apache License, Version 2.0 (the
     7   * "License"); you may not use this file except in compliance
     8   * with the License.  You may obtain a copy of the License at
     9   *
    10   *  http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing,
    13   * software distributed under the License is distributed on an
    14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    15   * KIND, either express or implied.  See the License for the
    16   * specific language governing permissions and limitations
    17   * under the License.
    18   */
    19  
    20  package common
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  	"os/exec"
    30  	"os/signal"
    31  	"runtime"
    32  	"strings"
    33  	"syscall"
    34  
    35  	"github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/metadata"
    36  	"github.com/docker/docker/api/types"
    37  	"github.com/docker/docker/api/types/container"
    38  	"github.com/docker/docker/api/types/filters"
    39  	"github.com/docker/docker/client"
    40  	"github.com/docker/docker/pkg/stdcopy"
    41  	"github.com/docker/go-connections/nat"
    42  )
    43  
    44  const (
    45  	Docker = "docker"
    46  	Podman = "podman"
    47  )
    48  
    49  type DockerLogMessage struct {
    50  	Status string `json:"status,omitempty"`
    51  	ID     string `json:"id,omitempty"`
    52  }
    53  
    54  func getDockerClient() (*client.Client, error) {
    55  	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
    56  	if err != nil {
    57  		return nil, fmt.Errorf("failed to create Docker client: %s", err)
    58  	}
    59  	return cli, nil
    60  }
    61  
    62  func GetContainerID(containerTool string) (string, error) {
    63  
    64  	switch containerTool {
    65  	case Podman:
    66  		return getPodmanContainerID()
    67  	case Docker:
    68  		return getDockerContainerID()
    69  	default:
    70  		return "", fmt.Errorf("no matching container type found")
    71  	}
    72  }
    73  
    74  func getPodmanContainerID() (string, error) {
    75  	cmd := exec.Command("podman",
    76  		"ps",
    77  		"-a",
    78  		"--filter",
    79  		fmt.Sprintf("ancestor=%s", metadata.DevModeImage),
    80  		"--filter",
    81  		"status=running",
    82  		"--format", "{{.ID}}")
    83  	fmt.Println(cmd)
    84  	output, err := cmd.CombinedOutput()
    85  	if err != nil {
    86  		return "", fmt.Errorf("error getting podman container id: %w", err)
    87  	}
    88  	containerID := strings.TrimSpace(string(output))
    89  	return containerID, nil
    90  }
    91  
    92  func getDockerContainerID() (string, error) {
    93  	cli, err := getDockerClient()
    94  	if err != nil {
    95  		return "", err
    96  	}
    97  
    98  	containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
    99  	if err != nil {
   100  		return "", err
   101  	}
   102  
   103  	for _, container := range containers {
   104  		// Check if the container has the expected image name or other identifying information
   105  		if strings.Contains(container.Image, metadata.DevModeImage) {
   106  			return container.ID, nil
   107  		}
   108  	}
   109  
   110  	return "", fmt.Errorf("no matching container found")
   111  }
   112  
   113  func StopContainer(containerTool string, containerID string) error {
   114  	if containerTool == Podman {
   115  		stopCmd := exec.Command(containerTool, "stop", containerID)
   116  		if err := stopCmd.Run(); err != nil {
   117  			fmt.Printf("Unable to stop container %s: %s", containerID, err)
   118  			return err
   119  		}
   120  	} else if containerTool == Docker {
   121  		cli, err := getDockerClient()
   122  		if err != nil {
   123  			fmt.Printf("unable to create client for docker")
   124  			return err
   125  		}
   126  		if err := cli.ContainerStop(context.Background(), containerID, container.StopOptions{}); err != nil {
   127  			fmt.Printf("Unable to stop container %s: %s", containerID, err)
   128  			return err
   129  		}
   130  	} else {
   131  		return errors.New(fmt.Sprintf("The specified containerTool:%s does not exist", containerTool))
   132  	}
   133  	fmt.Printf("\nšŸ›‘ Container %s stopped successfully.\n", containerID)
   134  	return nil
   135  }
   136  
   137  func resolveVolumeBindPath(containerTool string) string {
   138  	if containerTool == "podman" && runtime.GOOS == "linux" {
   139  		return metadata.VolumeBindPathSELinux
   140  	}
   141  	return metadata.VolumeBindPath
   142  }
   143  
   144  func RunContainerCommand(containerTool string, portMapping string, path string) error {
   145  	volumeBindPath := resolveVolumeBindPath(containerTool)
   146  	fmt.Printf("šŸ”Ž Warming up SonataFlow containers (%s), this could take some time...\n", metadata.DevModeImage)
   147  	if containerTool == Podman {
   148  		c := exec.Command(
   149  			containerTool,
   150  			"run",
   151  			"--rm",
   152  			"-p",
   153  			fmt.Sprintf("%s:8080", portMapping),
   154  			"-v",
   155  			fmt.Sprintf("%s:%s", path, volumeBindPath),
   156  			fmt.Sprintf("%s", metadata.DevModeImage),
   157  		)
   158  		if err := RunCommand(
   159  			c,
   160  			"container run",
   161  		); err != nil {
   162  			return err
   163  		}
   164  	} else if containerTool == Docker {
   165  		if err := runDockerContainer(portMapping, path); err != nil {
   166  			return err
   167  		}
   168  	} else {
   169  		return errors.New(fmt.Sprintf("The specified containerTool:%s does not exist", containerTool))
   170  	}
   171  	return nil
   172  }
   173  
   174  func GracefullyStopTheContainerWhenInterrupted(containerTool string) {
   175  	c := make(chan os.Signal, 1)
   176  	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
   177  
   178  	go func() {
   179  		<-c // Wait for the interrupt signal
   180  		containerID, err := GetContainerID(containerTool)
   181  		if err != nil {
   182  			fmt.Printf("\nerror getting container id: %v\n", err)
   183  			os.Exit(1) // Exit the program with error
   184  		}
   185  
   186  		fmt.Println("\nšŸ”Ø Stopping the container id: " + containerID)
   187  		if containerID != "" {
   188  			err := StopContainer(containerTool, containerID)
   189  			if err != nil {
   190  				fmt.Println("āŒ ERROR: Error stopping container id: " + containerID)
   191  				os.Exit(1)
   192  			} else {
   193  				fmt.Println("šŸŽ‰ Successfully stopped container id: " + containerID)
   194  			}
   195  		}
   196  
   197  		os.Exit(0) // Exit the program gracefully
   198  	}()
   199  }
   200  
   201  func pullDockerImage(cli *client.Client, ctx context.Context) (io.ReadCloser, error) {
   202  	// Check if the image exists locally
   203  	imageFilters := filters.NewArgs()
   204  	imageFilters.Add("reference", metadata.DevModeImage)
   205  	images, err := cli.ImageList(ctx, types.ImageListOptions{Filters: imageFilters})
   206  	if err != nil {
   207  		return nil, fmt.Errorf("error listing images: %s", err)
   208  	}
   209  
   210  	// If the image is not found locally, pull it from the remote registry
   211  	if len(images) == 0 {
   212  		reader, err := cli.ImagePull(ctx, metadata.DevModeImage, types.ImagePullOptions{})
   213  		if err != nil {
   214  			return nil, fmt.Errorf("\nError pulling image: %s. Error is: %s", metadata.DevModeImage, err)
   215  		}
   216  		return reader, nil
   217  	}
   218  
   219  	return nil, nil
   220  }
   221  
   222  func processDockerImagePullLogs(reader io.ReadCloser) error {
   223  	for {
   224  		err := waitToImageBeReady(reader)
   225  		if err == io.EOF {
   226  			break
   227  		} else if err != nil {
   228  			return fmt.Errorf("error decoding ImagePull JSON: %s", err)
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  func waitToImageBeReady(reader io.ReadCloser) error {
   235  	var message DockerLogMessage
   236  	decoder := json.NewDecoder(reader)
   237  	if err := decoder.Decode(&message); err != nil {
   238  		return err
   239  	}
   240  	if message.Status != "" {
   241  		fmt.Print(".")
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  func createDockerContainer(cli *client.Client, ctx context.Context, portMapping string, path string) (container.CreateResponse, error) {
   248  	containerConfig := &container.Config{
   249  		Image: metadata.DevModeImage,
   250  	}
   251  	hostConfig := &container.HostConfig{
   252  		AutoRemove: true,
   253  		PortBindings: nat.PortMap{
   254  			metadata.DockerInternalPort: []nat.PortBinding{
   255  				{
   256  					HostIP:   "0.0.0.0",
   257  					HostPort: portMapping,
   258  				},
   259  			},
   260  		},
   261  		Binds: []string{
   262  			fmt.Sprintf("%s:%s", path, metadata.VolumeBindPath),
   263  		},
   264  	}
   265  
   266  	resp, err := cli.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, "")
   267  	if err != nil {
   268  		return resp, fmt.Errorf("\nUnable to create container %s: %s", metadata.DevModeImage, err)
   269  	}
   270  	return resp, nil
   271  }
   272  
   273  func startDockerContainer(cli *client.Client, ctx context.Context, resp container.CreateResponse) error {
   274  	fmt.Printf("\nCreated container with ID %s", resp.ID)
   275  	fmt.Println("\nā³ Starting your container and SonataFlow project...")
   276  
   277  	if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
   278  		return fmt.Errorf("\nUnable to start container %s", resp.ID)
   279  	}
   280  
   281  	return nil
   282  }
   283  
   284  func runDockerContainer(portMapping string, path string) error {
   285  	ctx := context.Background()
   286  	cli, err := getDockerClient()
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	reader, err := pullDockerImage(cli, ctx)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	if reader != nil {
   297  		fmt.Printf("\nā³ Retrieving (%s), this could take some time...\n", metadata.DevModeImage)
   298  		if err := processDockerImagePullLogs(reader); err != nil {
   299  			return err
   300  		}
   301  	}
   302  
   303  	resp, err := createDockerContainer(cli, ctx, portMapping, path)
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	if err := startDockerContainer(cli, ctx, resp); err != nil {
   309  		return err
   310  	}
   311  
   312  	if err := processOutputDuringContainerExecution(cli, ctx, resp); err != nil {
   313  		return err
   314  	}
   315  
   316  	return nil
   317  }
   318  func processOutputDuringContainerExecution(cli *client.Client, ctx context.Context, resp container.CreateResponse) error {
   319  	statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
   320  
   321  	//Print all container logs
   322  	out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: false, ShowStderr: true, Follow: true})
   323  	if err != nil {
   324  		return fmt.Errorf("\nError getting container logs: %s", err)
   325  	}
   326  
   327  	go func() {
   328  		_, err := stdcopy.StdCopy(os.Stdout, os.Stderr, out)
   329  		if err != nil {
   330  			fmt.Errorf("\nError copying container logs to stdout: %s", err)
   331  		}
   332  	}()
   333  
   334  	select {
   335  	case err := <-errCh:
   336  		if err != nil {
   337  			return fmt.Errorf("\nError starting the container %s: %s", resp.ID, err)
   338  		}
   339  	case <-statusCh:
   340  		//state of the container matches the condition, in our case WaitConditionNotRunning
   341  	}
   342  
   343  	return nil
   344  }