package run import ( "context" "fmt" "io/ioutil" "log" "strings" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" ) const ( imageBasePathRemote = "/images" k3dToolsImage = "docker.io/iwilltry42/k3d-tools:v0.0.1" ) func importImage(clusterName string, images []string, noRemove bool) error { // get a docker client ctx := context.Background() docker, err := client.NewEnvClient() if err != nil { return fmt.Errorf("ERROR: couldn't create docker client\n%+v", err) } // get cluster directory to temporarily save the image tarball there imageVolume, err := getImageVolume(clusterName) if err != nil { return fmt.Errorf("ERROR: couldn't get image volume for cluster [%s]\n%+v", clusterName, err) } //*** first, save the images using the local docker daemon log.Printf("INFO: Saving images %s from local docker daemon...", images) toolsContainerName := fmt.Sprintf("k3d-%s-tools", clusterName) tarFileName := fmt.Sprintf("%s/k3d-%s-images-%s.tar", imageBasePathRemote, clusterName, time.Now().Format("20060102150405")) // create a tools container to get the tarball into the named volume containerConfig := container.Config{ Hostname: toolsContainerName, Image: k3dToolsImage, Labels: map[string]string{ "app": "k3d", "cluster": clusterName, "component": "tools", }, Cmd: append([]string{"save-image", "-d", tarFileName}, images...), AttachStdout: true, AttachStderr: true, } hostConfig := container.HostConfig{ Binds: []string{ "/var/run/docker.sock:/var/run/docker.sock", fmt.Sprintf("%s:%s:rw", imageVolume.Name, imageBasePathRemote), }, } toolsContainerID, err := startContainer(false, &containerConfig, &hostConfig, &network.NetworkingConfig{}, toolsContainerName) if err != nil { return err } defer func() { if err = docker.ContainerRemove(ctx, toolsContainerID, types.ContainerRemoveOptions{ Force: true, }); err != nil { log.Println(fmt.Errorf("WARN: couldn't remove tools container\n%+v", err)) } }() // loop to wait for tools container to exit (failed or successfully saved images) for { cont, err := docker.ContainerInspect(ctx, toolsContainerID) if err != nil { return fmt.Errorf("ERROR: couldn't get helper container's exit code\n%+v", err) } if !cont.State.Running { // container finished... if cont.State.ExitCode == 0 { // ...successfully log.Println("INFO: saved images to shared docker volume") break } else if cont.State.ExitCode != 0 { // ...failed errTxt := "ERROR: helper container failed to save images" logReader, err := docker.ContainerLogs(ctx, toolsContainerID, types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, }) if err != nil { return fmt.Errorf("%s\n> couldn't get logs from helper container\n%+v", errTxt, err) } logs, err := ioutil.ReadAll(logReader) // let's show somw logs indicating what happened if err != nil { return fmt.Errorf("%s\n> couldn't get logs from helper container\n%+v", errTxt, err) } return fmt.Errorf("%s -> Logs from [%s]:\n>>>>>>\n%s\n<<<<<<", errTxt, toolsContainerName, string(logs)) } } time.Sleep(time.Second / 2) // wait for half a second so we don't spam the docker API too much } // Get the container IDs for all containers in the cluster clusters, err := getClusters(false, clusterName) if err != nil { return fmt.Errorf("ERROR: couldn't get cluster by name [%s]\n%+v", clusterName, err) } containerList := []types.Container{clusters[clusterName].server} containerList = append(containerList, clusters[clusterName].workers...) // *** second, import the images using ctr in the k3d nodes // create exec configuration cmd := []string{"ctr", "image", "import", tarFileName} execConfig := types.ExecConfig{ AttachStderr: true, AttachStdout: true, Cmd: cmd, Tty: true, Detach: true, } execAttachConfig := types.ExecConfig{ Tty: true, } execStartConfig := types.ExecStartCheck{ Tty: true, } // import in each node separately // TODO: import concurrently using goroutines or find a way to share the image cache for _, container := range containerList { containerName := container.Names[0][1:] // trimming the leading "/" from name log.Printf("INFO: Importing images %s in container [%s]", images, containerName) // create exec configuration execResponse, err := docker.ContainerExecCreate(ctx, container.ID, execConfig) if err != nil { return fmt.Errorf("ERROR: Failed to create exec command for container [%s]\n%+v", containerName, err) } // attach to exec process in container containerConnection, err := docker.ContainerExecAttach(ctx, execResponse.ID, execAttachConfig) if err != nil { return fmt.Errorf("ERROR: couldn't attach to container [%s]\n%+v", containerName, err) } defer containerConnection.Close() // start exec err = docker.ContainerExecStart(ctx, execResponse.ID, execStartConfig) if err != nil { return fmt.Errorf("ERROR: couldn't execute command in container [%s]\n%+v", containerName, err) } // get output from container content, err := ioutil.ReadAll(containerConnection.Reader) if err != nil { return fmt.Errorf("ERROR: couldn't read output from container [%s]\n%+v", containerName, err) } // example output "unpacking image........ ...done" if !strings.Contains(string(content), "done") { return fmt.Errorf("ERROR: seems like something went wrong using `ctr image import` in container [%s]. Full output below:\n%s", containerName, string(content)) } } log.Printf("INFO: Successfully imported images %s in all nodes of cluster [%s]", images, clusterName) // remove tarball from inside the server container if !noRemove { log.Println("INFO: Cleaning up tarball") execID, err := docker.ContainerExecCreate(ctx, clusters[clusterName].server.ID, types.ExecConfig{ Cmd: []string{"rm", "-f", tarFileName}, }) if err != nil { log.Printf("WARN: failed to delete tarball: couldn't create remove in container [%s]\n%+v", clusters[clusterName].server.ID, err) } err = docker.ContainerExecStart(ctx, execID.ID, types.ExecStartCheck{ Detach: true, }) if err != nil { log.Printf("WARN: couldn't start tarball deletion action\n%+v", err) } for { execInspect, err := docker.ContainerExecInspect(ctx, execID.ID) if err != nil { log.Printf("WARN: couldn't verify deletion of tarball\n%+v", err) } if !execInspect.Running { if execInspect.ExitCode == 0 { log.Println("INFO: deleted tarball") break } else { log.Println("WARN: failed to delete tarball") break } } } } log.Println("INFO: ...Done") return nil }