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 }