package containers import ( "bytes" "errors" "fmt" "log" "path" "strconv" "strings" "github.com/rosti-cz/node-api/apps" "github.com/rosti-cz/node-api/detector" ) // username in the containers under which all containers run const appUsername = "app" const owner = "app:app" const passwordFile = "/srv/.rosti" const deployKeyType = "ed25519" const deployKeyPrefix = "rosti_deploy" // Structure containing info about technology and its version type TechInfo struct { Tech string `json:"tech"` Version string `json:"version"` } // Process contains info about background application usually running in supervisor type Process struct { Name string `json:"name"` State string `json:"state"` } // Current status of the container type ContainerStatus struct { Status string `json:"status"` OOMKilled bool `json:"oom_killed"` } // Container extends App struct from App type Container struct { App *apps.App `json:"app"` DockerSock string `json:"-"` BindIPHTTP string `json:"-"` BindIPSSH string `json:"-"` AppsPath string `json:"-"` } func (c *Container) getDriver() *Driver { driver := &Driver{ DockerSock: c.DockerSock, BindIPHTTP: c.BindIPHTTP, BindIPSSH: c.BindIPSSH, } return driver } // volumeHostPath each container has one volume mounted into it, func (c *Container) VolumeHostPath() string { return path.Join(c.AppsPath, c.App.Name) } // GetRawResourceStats returns RAW CPU and memory usage directly from Docker API func (c *Container) GetRawResourceStats() (int64, int, error) { driver := c.getDriver() cpu, memory, err := driver.RawStats(c.App.Name) return cpu, memory, err } // GetState returns app state object with populated state fields func (c *Container) GetState() (*apps.AppState, error) { status, err := c.Status() if err != nil { return nil, err } // TODO: this implementation takes more than one hour for 470 containers. It needs to be implemented differently. // cpu, memory, err := c.ResourceUsage() // if err != nil { // return nil, err // } bytes, inodes, err := c.DiskUsage() if err != nil { return nil, err } processes, err := c.GetSystemProcesses() if err != nil { return nil, err } flags, err := detector.Check(processes) if err != nil { return nil, err } state := apps.AppState{ State: status.Status, OOMKilled: status.OOMKilled, // CPUUsage: cpu, // MemoryUsage: memory, CPUUsage: -1.0, MemoryUsage: -1.0, DiskUsageBytes: bytes, DiskUsageInodes: inodes, Flags: flags, } return &state, nil } // Status returns state of the container // Possible values: running, exited (stopped), no-container, unknown func (c *Container) Status() (ContainerStatus, error) { status := ContainerStatus{ Status: "unknown", } driver := c.getDriver() containerStatus, err := driver.Status(c.App.Name) if err != nil && err.Error() == "no container found" { return ContainerStatus{Status: "no-container"}, nil } if err != nil { return status, err } return containerStatus, nil } // DiskUsage returns number of bytes and inodes used by the container in it's mounted volume func (c *Container) DiskUsage() (int, int, error) { return du(c.VolumeHostPath()) } // ResourceUsage returns amount of memory in B and CPU in % that the app occupies func (c *Container) ResourceUsage() (float64, int, error) { driver := c.getDriver() cpu, memory, err := driver.Stats(c.App.Name) if err != nil { return 0.0, 0, err } return cpu, memory, nil } // Create creates the container func (c *Container) Create() error { driver := c.getDriver() _, err := driver.Create( c.App.Name, c.App.Image, c.VolumeHostPath(), c.App.HTTPPort, c.App.SSHPort, c.App.CPU, c.App.Memory, []string{}, c.App.GetEnv(), ) return err } // Start starts the container func (c *Container) Start() error { driver := c.getDriver() return driver.Start(c.App.Name) } // Stop stops the container func (c *Container) Stop() error { driver := c.getDriver() return driver.Stop(c.App.Name) } // Restart restarts the container func (c *Container) Restart() error { driver := c.getDriver() err := driver.Stop(c.App.Name) if err != nil { return err } return driver.Start(c.App.Name) } // Destroy removes the container but keeps the data so it can be created again func (c *Container) Destroy() error { driver := c.getDriver() return driver.Remove(c.App.Name) } // Delete removes both data and the container func (c *Container) Delete() error { status, err := c.Status() if err != nil { return err } // It's questionable to have this here. The problem is this method // does two things, deleting the container and the data and when // the deleted container doesn't exist we actually don't care // and we can continue to remove the data. if status.Status != "no-container" { err = c.Destroy() if err != nil { return err } } volumePath := path.Join(c.AppsPath, c.App.Name) err = removeDirectory(volumePath) if err != nil { log.Println(err) } return nil } // SetPassword configures password for system user app in the container func (c *Container) SetPassword(password string) error { driver := c.getDriver() _, err := driver.Exec(c.App.Name, []string{"chpasswd"}, appUsername+":"+password, []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"tee", passwordFile}, password, []string{}, false) if err != nil { return err } return err } // Generate SSH keys and copies it into authorized keys // Returns true if the key was generated in this call and error if there is any. // The container has to run for this to work. func (c *Container) GenerateDeploySSHKeys() (bool, error) { driver := c.getDriver() privateKey, pubKey, _ := c.GetDeploySSHKeys() if privateKey != "" || pubKey != "" { return false, nil } _, err := driver.Exec(c.App.Name, []string{"mkdir", "-p", "/srv/.ssh"}, "", []string{}, true) if err != nil { return false, err } _, err = driver.Exec(c.App.Name, []string{"ssh-keygen", "-t", deployKeyType, "-f", "/srv/.ssh/" + deployKeyPrefix + "_id_" + deployKeyType, "-P", ""}, "", []string{}, true) if err != nil { return false, err } _, err = driver.Exec(c.App.Name, []string{"chown", "app:app", "-R", "/srv/.ssh"}, "", []string{}, true) if err != nil { return false, err } return true, nil } // Generate SSH keys and copies it into authorized keys // Return private key, public key and error. // The container has to run for this to work. func (c *Container) GetDeploySSHKeys() (string, string, error) { driver := c.getDriver() privateKey, err := driver.Exec(c.App.Name, []string{"cat", "/srv/.ssh/" + deployKeyPrefix + "_id_" + deployKeyType}, "", []string{}, true) if err != nil { return "", "", err } pubKey, err := driver.Exec(c.App.Name, []string{"cat", "/srv/.ssh/" + deployKeyPrefix + "_id_" + deployKeyType + ".pub"}, "", []string{}, true) if err != nil { return "", "", err } if privateKey != nil && pubKey != nil && !bytes.Contains(*privateKey, []byte("No such file")) && !bytes.Contains(*pubKey, []byte("No such file")) { return string(*privateKey), string(*pubKey), nil } return "", "", nil } // Return host key without hostname // The container has to run for this to work. func (c *Container) GetHostKey() (string, error) { driver := c.getDriver() hostKeyRaw, err := driver.Exec(c.App.Name, []string{"ssh-keyscan", "localhost"}, "", []string{}, true) if err != nil { return "", err } // Loop over lines and search for localhost ssh line := "" if hostKeyRaw != nil { for _, line = range strings.Split(string(*hostKeyRaw), "\n") { if strings.HasPrefix(line, "localhost ssh") { line = strings.TrimSpace(line) break } } } if line == "" { return "", errors.New("key not found") } parts := strings.SplitN(line, " ", 2) if len(parts) > 1 { return parts[1], nil } return "", errors.New("key not found") } // Append text to a file in the container func (c *Container) AppendFile(filename string, text string, mode string) error { driver := c.getDriver() directory := path.Dir(filename) _, err := driver.Exec(c.App.Name, []string{"mkdir", "-p", directory}, "", []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"tee", "-a", filename}, text, []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"chmod", mode, filename}, "", []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"chown", owner, directory}, "", []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"chown", owner, filename}, "", []string{}, false) if err != nil { return err } return nil } // SetAppPort changes application port in the container func (c *Container) SetAppPort(port int) error { driver := c.getDriver() _, err := driver.Exec( c.App.Name, []string{ "sed", "-i", "s+proxy_pass[ ]*http://127.0.0.1:8080/;+proxy_pass http://127.0.0.1:" + strconv.Itoa(port) + "/;+g", "/srv/conf/nginx.d/app.conf", }, "", []string{}, false, ) if err != nil { return err } return err } // SetFileContent uploads text into a file inside the container. It's greate for uploading SSH keys. // The method creates the diretory where the file is located and sets mode of the final file func (c *Container) SetFileContent(filename string, text string, mode string) error { driver := c.getDriver() directory := path.Dir(filename) _, err := driver.Exec(c.App.Name, []string{"mkdir", "-p", directory}, "", []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"tee", filename}, text, []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"chown", owner, directory}, "", []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"chown", owner, filename}, "", []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"chmod", mode, filename}, "", []string{}, false) return err } // SetTechnology prepares container for given technology (Python, PHP, Node.js, ...) // Where tech can be php, python or node and latest available version is used. // If version is empty string default version will be used. func (c *Container) SetTechnology(tech string, version string) error { driver := c.getDriver() var err error // TODO: script injection here? var output *[]byte if version == "" { output, err = driver.Exec(c.App.Name, []string{"su", "app", "-c", "rosti " + tech}, "", []string{}, false) } else { output, err = driver.Exec(c.App.Name, []string{"su", "app", "-c", "rosti " + tech + " " + version}, "", []string{}, false) } log.Printf("DEBUG: enable tech for %s output: %s", c.App.Name, string(*output)) return err } // GetProcessList returns list of processes managed by supervisor. func (c *Container) GetProcessList() ([]Process, error) { driver := c.getDriver() processes := []Process{} stdouterr, err := driver.Exec(c.App.Name, []string{"supervisorctl", "status"}, "", []string{}, true) if err != nil { return processes, nil } trimmed := strings.TrimSpace(string(*stdouterr)) for _, row := range strings.Split(trimmed, "\n") { fields := strings.Fields(row) if len(fields) > 2 { processes = append(processes, Process{ Name: fields[0], State: fields[1], }) } } return processes, nil } // Restarts supervisord process func (c *Container) RestartProcess(name string) error { driver := c.getDriver() _, err := driver.Exec(c.App.Name, []string{"supervisorctl", "restart", name}, "", []string{}, false) return err } // Starts supervisord process func (c *Container) StartProcess(name string) error { driver := c.getDriver() _, err := driver.Exec(c.App.Name, []string{"supervisorctl", "start", name}, "", []string{}, false) return err } // Stops supervisord process func (c *Container) StopProcess(name string) error { driver := c.getDriver() _, err := driver.Exec(c.App.Name, []string{"supervisorctl", "stop", name}, "", []string{}, false) return err } // Reread supervisord config func (c *Container) ReloadSupervisor() error { driver := c.getDriver() _, err := driver.Exec(c.App.Name, []string{"supervisorctl", "reread"}, "", []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"supervisorctl", "update"}, "", []string{}, false) if err != nil { return err } return err } // GetSystemProcesses return list of running system processes func (c *Container) GetSystemProcesses() ([]string, error) { driver := c.getDriver() processes, err := driver.GetProcesses(c.App.Name) return processes, err } // GetPrimaryTech returns primary tech configured in the container. func (c *Container) GetPrimaryTech() (apps.AppTech, error) { tech := apps.AppTech{} driver := c.getDriver() stdouterr, err := driver.Exec(c.App.Name, []string{"readlink", "/srv/bin/primary_tech"}, "", []string{}, true) if err != nil { // in case there is an error just return empty response return tech, nil } if len(string(*stdouterr)) > 0 { parts := strings.Split(string(*stdouterr), "/") if len(parts) == 5 { rawTech := parts[3] techParts := strings.Split(rawTech, "-") if len(techParts) != 2 { return tech, errors.New("wrong number of tech parts") } return apps.AppTech{ Name: techParts[0], Version: techParts[1], }, nil } } // Probably single technology image in case the output is empty return tech, nil } // GetTechs returns all techs available in the container func (c *Container) GetTechs() (apps.AppTechs, error) { techs := apps.AppTechs{} driver := c.getDriver() stdouterr, err := driver.Exec(c.App.Name, []string{"ls", "/opt"}, "", []string{}, true) if err != nil { // in case there is an error just return empty response return techs, nil } // Check if /opt/techs exists if !strings.Contains(string(*stdouterr), "techs") { return techs, nil } stdouterr, err = driver.Exec(c.App.Name, []string{"ls", "/opt/techs"}, "", []string{}, true) if err != nil { // in case there is an error just return empty response return techs, nil } // If the directory doesn't exist it's single technology image if strings.Contains(string(*stdouterr), "No such file or directory") { return techs, nil } techsRaw := strings.Fields(string(*stdouterr)) for _, techRaw := range techsRaw { techParts := strings.Split(techRaw, "-") if len(techParts) == 2 { techs = append(techs, apps.AppTech{ Name: techParts[0], Version: techParts[1], }) } else { return techs, fmt.Errorf("one of the tech has wrong number of parts (%s)", techRaw) } } return techs, nil } // Returns info about active technology func (c *Container) GetActiveTech() (*TechInfo, error) { info, err := getTechAndVersion(path.Join(c.VolumeHostPath(), "bin", "primary_tech")) if err != nil { return info, err } return info, nil }