add import-image command for importing single image
This commit is contained in:
parent
341f0b3cbe
commit
ab2c54ca1d
@ -68,6 +68,10 @@ func createClusterDir(name string) {
|
||||
if err := createDirIfNotExists(clusterPath); err != nil {
|
||||
log.Fatalf("ERROR: couldn't create cluster directory [%s] -> %+v", clusterPath, err)
|
||||
}
|
||||
// create subdir for sharing container images
|
||||
if err := createDirIfNotExists(clusterPath + "/images"); err != nil {
|
||||
log.Fatalf("ERROR: couldn't create cluster sub-directory [%s] -> %+v", clusterPath+"/images", err)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteClusterDir contrary to createClusterDir, this deletes the cluster directory under $HOME/.config/k3d/<cluster_name>
|
||||
|
@ -100,7 +100,7 @@ func CreateCluster(c *cli.Context) error {
|
||||
if c.IsSet("port") {
|
||||
log.Println("INFO: As of v2.0.0 --port will be used for arbitrary port mapping. Please use --api-port/-a instead for configuring the Api Port")
|
||||
}
|
||||
apiPort, err := parseApiPort(c.String("api-port"))
|
||||
apiPort, err := parseAPIPort(c.String("api-port"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -111,7 +111,7 @@ func CreateCluster(c *cli.Context) error {
|
||||
if apiPort.Host == "" {
|
||||
apiPort.Host, err = getDockerMachineIp()
|
||||
// IP address is the same as the host
|
||||
apiPort.HostIp = apiPort.Host
|
||||
apiPort.HostIP = apiPort.Host
|
||||
// In case of error, Log a warning message, and continue on. Since it more likely caused by a miss configured
|
||||
// DOCKER_MACHINE_NAME environment variable.
|
||||
if err != nil {
|
||||
@ -137,7 +137,7 @@ func CreateCluster(c *cli.Context) error {
|
||||
|
||||
clusterSpec := &ClusterSpec{
|
||||
AgentArgs: []string{},
|
||||
ApiPort: *apiPort,
|
||||
APIPort: *apiPort,
|
||||
AutoRestart: c.Bool("auto-restart"),
|
||||
ClusterName: c.String("name"),
|
||||
Env: env,
|
||||
@ -151,6 +151,10 @@ func CreateCluster(c *cli.Context) error {
|
||||
|
||||
// create the server
|
||||
log.Printf("Creating cluster [%s]", c.String("name"))
|
||||
|
||||
// create the directory where we will put the kubeconfig file by default (when running `k3d get-config`)
|
||||
createClusterDir(c.String("name"))
|
||||
|
||||
dockerID, err := createServer(clusterSpec)
|
||||
if err != nil {
|
||||
deleteCluster()
|
||||
@ -192,10 +196,6 @@ func CreateCluster(c *cli.Context) error {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// create the directory where we will put the kubeconfig file by default (when running `k3d get-config`)
|
||||
// TODO: this can probably be moved to `k3d get-config` or be removed in a different approach
|
||||
createClusterDir(c.String("name"))
|
||||
|
||||
// spin up the worker nodes
|
||||
// TODO: do this concurrently in different goroutines
|
||||
if c.Int("workers") > 0 {
|
||||
@ -362,3 +362,8 @@ func GetKubeConfig(c *cli.Context) error {
|
||||
func Shell(c *cli.Context) error {
|
||||
return subShell(c.String("name"), c.String("shell"), c.String("command"))
|
||||
}
|
||||
|
||||
// ImportImage saves an image locally and imports it into the k3d containers
|
||||
func ImportImage(c *cli.Context) error {
|
||||
return importImage(c.String("name"), c.String("image"))
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
|
||||
type ClusterSpec struct {
|
||||
AgentArgs []string
|
||||
ApiPort apiPort
|
||||
APIPort apiPort
|
||||
AutoRestart bool
|
||||
ClusterName string
|
||||
Env []string
|
||||
@ -94,14 +94,14 @@ func createServer(spec *ClusterSpec) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hostIp := "0.0.0.0"
|
||||
hostIP := "0.0.0.0"
|
||||
containerLabels["apihost"] = "localhost"
|
||||
if spec.ApiPort.Host != "" {
|
||||
hostIp = spec.ApiPort.HostIp
|
||||
containerLabels["apihost"] = spec.ApiPort.Host
|
||||
if spec.APIPort.Host != "" {
|
||||
hostIP = spec.APIPort.HostIP
|
||||
containerLabels["apihost"] = spec.APIPort.Host
|
||||
}
|
||||
|
||||
apiPortSpec := fmt.Sprintf("%s:%s:%s/tcp", hostIp, spec.ApiPort.Port, spec.ApiPort.Port)
|
||||
apiPortSpec := fmt.Sprintf("%s:%s:%s/tcp", hostIP, spec.APIPort.Port, spec.APIPort.Port)
|
||||
|
||||
serverPorts = append(serverPorts, apiPortSpec)
|
||||
|
||||
@ -123,6 +123,13 @@ func createServer(spec *ClusterSpec) (string, error) {
|
||||
hostConfig.Binds = spec.Volumes
|
||||
}
|
||||
|
||||
// we need to mount the clusterDir subdirectory `clusterDir/images` to enable importing images without the need for `docker cp`
|
||||
clusterDir, err := getClusterDir(spec.ClusterName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ERROR: couldn't get cluster dir for mounting\n%+v", err)
|
||||
}
|
||||
hostConfig.Binds = append(hostConfig.Binds, fmt.Sprintf("%s:/images", clusterDir+"/images"))
|
||||
|
||||
networkingConfig := &network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
k3dNetworkName(spec.ClusterName): {
|
||||
@ -157,7 +164,7 @@ func createWorker(spec *ClusterSpec, postfix int) (string, error) {
|
||||
|
||||
containerName := GetContainerName("worker", spec.ClusterName, postfix)
|
||||
|
||||
env := append(spec.Env, fmt.Sprintf("K3S_URL=https://k3d-%s-server:%s", spec.ClusterName, spec.ApiPort.Port))
|
||||
env := append(spec.Env, fmt.Sprintf("K3S_URL=https://k3d-%s-server:%s", spec.ClusterName, spec.APIPort.Port))
|
||||
|
||||
// ports to be assigned to the server belong to roles
|
||||
// all, server or <server-container-name>
|
||||
@ -192,6 +199,13 @@ func createWorker(spec *ClusterSpec, postfix int) (string, error) {
|
||||
hostConfig.Binds = spec.Volumes
|
||||
}
|
||||
|
||||
// we need to mount the clusterDir subdirectory `clusterDir/images` to enable importing images without the need for `docker cp`
|
||||
clusterDir, err := getClusterDir(spec.ClusterName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ERROR: couldn't get cluster dir for mounting\n%+v", err)
|
||||
}
|
||||
hostConfig.Binds = append(hostConfig.Binds, fmt.Sprintf("%s:/images", clusterDir+"/images"))
|
||||
|
||||
networkingConfig := &network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
k3dNetworkName(spec.ClusterName): {
|
||||
|
131
cli/image.go
Normal file
131
cli/image.go
Normal file
@ -0,0 +1,131 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
const imageBasePathRemote = "/images/"
|
||||
|
||||
func importImage(clusterName, image string) 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
|
||||
imageBasePathLocal, err := getClusterDir(clusterName)
|
||||
imageBasePathLocal = imageBasePathLocal + "/images/"
|
||||
if err != nil {
|
||||
return fmt.Errorf("ERROR: couldn't get cluster directory for cluster [%s]\n%+v", clusterName, err)
|
||||
}
|
||||
|
||||
// TODO: extend to enable importing a list of images
|
||||
imageList := []string{image}
|
||||
|
||||
//*** first, save the images using the local docker daemon
|
||||
log.Printf("INFO: Saving image [%s] from local docker daemon...", image)
|
||||
imageReader, err := docker.ImageSave(ctx, imageList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ERROR: failed to save image [%s] locally\n%+v", image, err)
|
||||
}
|
||||
|
||||
// create tarball
|
||||
imageTarName := strings.ReplaceAll(strings.ReplaceAll(image, ":", "_"), "/", "_") + ".tar"
|
||||
imageTar, err := os.Create(imageBasePathLocal + imageTarName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer imageTar.Close()
|
||||
|
||||
_, err = io.Copy(imageTar, imageReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ERROR: couldn't save image [%s] to file [%s]\n%+v", image, imageTar.Name(), err)
|
||||
}
|
||||
|
||||
// TODO: get correct container ID by cluster name
|
||||
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", imageBasePathRemote + imageTarName}
|
||||
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: create a shared image cache volume, so we don't need to import it separately
|
||||
for _, container := range containerList {
|
||||
|
||||
containerName := container.Names[0][1:] // trimming the leading "/" from name
|
||||
log.Printf("INFO: Importing image [%s] in container [%s]", image, 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 image [%s] in all nodes of cluster [%s]", image, clusterName)
|
||||
|
||||
log.Println("INFO: Cleaning up tarball...")
|
||||
if err := os.Remove(imageBasePathLocal + imageTarName); err != nil {
|
||||
return fmt.Errorf("ERROR: Couldn't remove tarball [%s]\n%+v", imageBasePathLocal+imageTarName, err)
|
||||
}
|
||||
log.Println("INFO: ...Done")
|
||||
|
||||
return nil
|
||||
}
|
@ -11,7 +11,7 @@ import (
|
||||
|
||||
type apiPort struct {
|
||||
Host string
|
||||
HostIp string
|
||||
HostIP string
|
||||
Port string
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ func ValidateHostname(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseApiPort(portSpec string) (*apiPort, error) {
|
||||
func parseAPIPort(portSpec string) (*apiPort, error) {
|
||||
var port *apiPort
|
||||
split := strings.Split(portSpec, ":")
|
||||
if len(split) > 2 {
|
||||
@ -105,7 +105,7 @@ func parseApiPort(portSpec string) (*apiPort, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port = &apiPort{Host: split[0], HostIp: addrs[0], Port: split[1]}
|
||||
port = &apiPort{Host: split[0], HostIP: addrs[0], Port: split[1]}
|
||||
}
|
||||
|
||||
// Verify 'port' is an integer and within port ranges
|
||||
|
17
main.go
17
main.go
@ -214,6 +214,23 @@ func main() {
|
||||
},
|
||||
Action: run.GetKubeConfig,
|
||||
},
|
||||
{
|
||||
// get-kubeconfig grabs the kubeconfig from the cluster and prints the path to it
|
||||
Name: "import-image",
|
||||
Usage: "Import a container image from your local docker daemon into the cluster",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name, n",
|
||||
Value: defaultK3sClusterName,
|
||||
Usage: "Name of the cluster",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "image, i",
|
||||
Usage: "Name of the image that you want to import, e.g. `nginx:local`",
|
||||
},
|
||||
},
|
||||
Action: run.ImportImage,
|
||||
},
|
||||
}
|
||||
|
||||
// Global flags
|
||||
|
Loading…
Reference in New Issue
Block a user