package docker import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "github.com/Akilan1999/p2p-rendering-computation/config" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/archive" "github.com/docker/go-connections/nat" "github.com/lithammer/shortuuid" "github.com/phayes/freeport" "io" "io/ioutil" "os/exec" "time" ) type DockerVM struct { SSHUsername string `json:"SSHUsername"` SSHPassword string `json:"SSHPassword"` ID string `json:"ID"` TagName string `json:"TagName"` ImagePath string `json:"ImagePath"` Ports Ports `json:"Ports"` GPU string `json:"GPU"` } type DockerContainers struct { DockerContainer []DockerContainer `json:"DockerContainer"` } type DockerContainer struct { ContainerName string `json:"DockerContainerName"` ContainerDescription string `json:"ContainerDescription"` } type Ports struct { PortSet []Port `json:"Port"` } type Port struct { PortName string `json:"PortName"` InternalPort int `json:"InternalPort"` Type string `json:"Type"` ExternalPort int `json:"ExternalPort"` IsUsed bool `json:"IsUsed"` Description string `json:"Description"` } type ErrorLine struct { Error string `json:"error"` ErrorDetail ErrorDetail `json:"errorDetail"` } type ErrorDetail struct { Message string `json:"message"` } var dockerRegistryUserID = "" // BuildRunContainer Function is incharge to invoke building and running contianer and also allocating external // ports func BuildRunContainer(NumPorts int, GPU string, ContainerName string) (*DockerVM, error) { //Docker Struct Variable var RespDocker *DockerVM = new(DockerVM) // Sets if GPU is selected or not RespDocker.GPU = GPU // Sets Free port to Struct //RespDocker.SSHPort = Ports[0] //RespDocker.VNCPort = Ports[1] // Sets appropriate username and password to the // variables in the struct RespDocker.SSHUsername = "master" RespDocker.SSHPassword = "password" //RespDocker.VNCPassword = "vncpassword" //Default parameters RespDocker.TagName = "p2p-ubuntu" // Get Path from config config, err := config.ConfigInit() if err != nil { return nil, err } RespDocker.ImagePath = config.DefaultDockerFile // We are checking if the container name is not nil and not equal to the default one used // which is docker-ubuntu-sshd if ContainerName != "" && ContainerName != "docker-ubuntu-sshd" { Containers, err := ViewAllContainers() if err != nil { return nil, err } for _, dockerContainer := range Containers.DockerContainer { if dockerContainer.ContainerName == ContainerName { RespDocker.ImagePath = config.DockerContainers + ContainerName + "/" RespDocker.TagName = ContainerName break } } if RespDocker.ImagePath == config.DefaultDockerFile { return nil, errors.New("Container " + ContainerName + " does not exist in the server") } } PortsInformation, err := OpenPortsFile(RespDocker.ImagePath + "/ports.json") if err != nil { return nil, err } // Number of perts we want to open + number of ports required inside the // docker container count := NumPorts + len(PortsInformation.PortSet) // Creates number of ports OpenPorts, err := freeport.GetFreePorts(count) if err != nil { return nil, err } // Allocate external ports to ports available in the ports.json file for i := range PortsInformation.PortSet { // Setting external ports PortsInformation.PortSet[i].ExternalPort = OpenPorts[i] PortsInformation.PortSet[i].IsUsed = true } //Length of Ports allocated from thr port file portFileLength := len(PortsInformation.PortSet) // Allocate New ports the user wants to generate for i := 0; i < NumPorts; i++ { var TempPort Port TempPort.PortName = "AutoGen Port" TempPort.Type = "tcp" TempPort.InternalPort = OpenPorts[portFileLength+i] TempPort.ExternalPort = OpenPorts[portFileLength+i] TempPort.Description = "Auto generated TCP port" TempPort.IsUsed = false //Append temp port to port information PortsInformation.PortSet = append(PortsInformation.PortSet, TempPort) } // Setting ports to the docker VM struct RespDocker.Ports = *PortsInformation // Gets docker information from env variables cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return nil, err } // Builds docker image err = RespDocker.imageBuild(cli) if err != nil { return nil, err } // Runs docker contianer err = RespDocker.runContainer(cli) if err != nil { return nil, err } return RespDocker, nil } // Builds docker image (TODO: relative path for Dockerfile deploy) func (d *DockerVM) imageBuild(dockerClient *client.Client) error { ctx, _ := context.WithTimeout(context.Background(), time.Second*2000) //defer cancel() tar, err := archive.TarWithOptions(d.ImagePath, &archive.TarOptions{}) if err != nil { return err } opts := types.ImageBuildOptions{ Dockerfile: "Dockerfile", Tags: []string{d.TagName}, Remove: true, } res, err := dockerClient.ImageBuild(ctx, tar, opts) if err != nil { return err } defer res.Body.Close() err = print(res.Body) if err != nil { return err } return nil } // Starts container and assigns port numbers // Sample Docker run Command // docker run -d=true --name=Test123 --restart=always --gpus all // -p 3443:6901 -p 3453:22 -p 3434:3434 -p 3245:3245 -v=/opt/data:/data // p2p-ubuntu /start > /dev/null func (d *DockerVM) runContainer(dockerClient *client.Client) error { ctx, _ := context.WithTimeout(context.Background(), time.Second*2000) // The first mode runs using the Docker Api. As the API supports using // CPU and uses a shell script for GPU call because till this point of // implementation docker api does not support the flag "--gpu all" if d.GPU != "true" { //Exposed ports for docker config file var ExposedPort nat.PortSet ExposedPort = nat.PortSet{ "22/tcp": struct{}{}, //"6901/tcp": struct{}{}, } // Port forwarding for VNC and SSH ports PortForwarding := nat.PortMap{ //"22/tcp": []nat.PortBinding{ // { // HostIP: "0.0.0.0", // HostPort: fmt.Sprint(d.SSHPort), // }, //}, //"6901/tcp": []nat.PortBinding{ // { // HostIP: "0.0.0.0", // HostPort: fmt.Sprint(d.VNCPort), // }, //}, } for i := range d.Ports.PortSet { // Parameters "tcp or udp", external port Port, err := nat.NewPort(d.Ports.PortSet[i].Type, fmt.Sprint(d.Ports.PortSet[i].InternalPort)) if err != nil { return err } // Exposed Ports ExposedPort[Port] = struct{}{} PortForwarding[Port] = []nat.PortBinding{ { HostIP: "0.0.0.0", HostPort: fmt.Sprint(d.Ports.PortSet[i].ExternalPort), }, } } config := &container.Config{ Image: d.TagName, Entrypoint: []string{"/start"}, Volumes: map[string]struct{}{"/opt/data:/data": {}}, ExposedPorts: ExposedPort, } hostConfig := &container.HostConfig{ PortBindings: PortForwarding, } res, err := dockerClient.ContainerCreate(ctx, config, hostConfig, nil, nil, "") // Set response ID d.ID = res.ID if err != nil { return err } err = dockerClient.ContainerStart(ctx, res.ID, types.ContainerStartOptions{}) if err != nil { return err } } else { // Generate Random ID id := shortuuid.New() d.ID = id var cmd bytes.Buffer cmd.WriteString("docker run -d=true --name=" + id + " --restart=always --gpus all ") for i := range d.Ports.PortSet { cmd.WriteString("-p " + fmt.Sprint(d.Ports.PortSet[i].ExternalPort) + ":" + fmt.Sprint(d.Ports.PortSet[i].InternalPort) + " ") } cmd.WriteString("-v=/opt/data:/data " + d.TagName + " /start > /dev/null") //"-v=/opt/data:/data p2p-ubuntu /start > /dev/null" cmdStr := cmd.String() _, err := exec.Command("/bin/sh", "-c", cmdStr).Output() if err != nil { return err } } return nil } // StopAndRemoveContainer // Stop and remove a container // Reference (https://gist.github.com/frikky/e2efcea6c733ea8d8d015b7fe8a91bf6) func StopAndRemoveContainer(containername string) error { ctx := context.Background() // Gets docker information from env variables client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return err } if err = client.ContainerStop(ctx, containername, nil); err != nil { return err } removeOptions := types.ContainerRemoveOptions{ RemoveVolumes: true, Force: true, } if err = client.ContainerRemove(ctx, containername, removeOptions); err != nil { return err } return nil } // ViewAllContainers returns all containers runnable and which can be built func ViewAllContainers() (*DockerContainers, error) { // Traverse the deploy path as per given in the config file config, err := config.ConfigInit() if err != nil { return nil, err } folders, err := ioutil.ReadDir(config.DockerContainers) if err != nil { return nil, err } //Declare variable DockerContainers of type struct var Containers *DockerContainers = new(DockerContainers) for _, f := range folders { if f.IsDir() { //Declare variable DockerContainer of type struct var Container DockerContainer // Setting container name to deploy name Container.ContainerName = f.Name() // Getting Description from file description.txt Description, err := ioutil.ReadFile(config.DockerContainers + "/" + Container.ContainerName + "/description.txt") // if we os.Open returns an error then handle it if err != nil { return nil, err } // Get Description from description.txt Container.ContainerDescription = string(Description) Containers.DockerContainer = append(Containers.DockerContainer, Container) } } return Containers, nil } func print(rd io.Reader) error { var lastLine string scanner := bufio.NewScanner(rd) for scanner.Scan() { lastLine = scanner.Text() } errLine := &ErrorLine{} json.Unmarshal([]byte(lastLine), errLine) if errLine.Error != "" { return errors.New(errLine.Error) } if err := scanner.Err(); err != nil { return err } return nil } func OpenPortsFile(filename string) (*Ports, error) { buf, err := ioutil.ReadFile(filename) if err != nil { return nil, err } c := &Ports{} err = json.Unmarshal(buf, c) if err != nil { return nil, fmt.Errorf("in file %q: %v", filename, err) } return c, nil }