package docker import ( "errors" "log" "path" "strings" "github.com/rosti-cz/node-api/apps" "github.com/rosti-cz/node-api/common" ) // username in the containers under which all containers run const appUsername = "app" const passwordFile = "/srv/.rosti" // Process contains info about background application usually running in supervisor type Process struct { Name string `json:"name"` State string `json:"state"` } // Container extends App struct from App type Container struct { App *apps.App `json:"app"` } func (c *Container) getDriver() *Driver { config := common.GetConfig() driver := &Driver{ BindIPHTTP: config.AppsBindIPHTTP, BindIPSSH: config.AppsBindIPSSH, } return driver } // volumeHostPath each container has one volume mounted into it, func (c *Container) volumeHostPath() string { config := common.GetConfig() return path.Join(config.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 } state := apps.AppState{ State: status, // CPUUsage: cpu, // MemoryUsage: memory, CPUUsage: -1.0, MemoryUsage: -1.0, DiskUsageBytes: bytes, DiskUsageInodes: inodes, } return &state, nil } // Status returns state of the container // Possible values: running, stopped, no-container, unknown func (c *Container) Status() (string, error) { status := "unknown" // config := common.GetConfig() // if _, err := os.Stat(path.Join(config.AppsPath, c.App.Name)); !os.IsNotExist(err) { // status = "data-only" // } driver := c.getDriver() containerStatus, err := driver.Status(c.App.Name) if err != nil && err.Error() == "no container found" { return "no-container", nil } if err != nil { return status, err } status = containerStatus return status, 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{}, ) 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 != "no-container" { err = c.Destroy() if err != nil { return err } } config := common.GetConfig() volumePath := path.Join(config.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 } // 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", "app:app", directory}, "", []string{}, false) if err != nil { return err } _, err = driver.Exec(c.App.Name, []string{"chown", "app:app", 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? if version == "" { _, err = driver.Exec(c.App.Name, []string{"su", "app", "-c", "rosti " + tech}, "", []string{}, false) } else { _, err = driver.Exec(c.App.Name, []string{"su", "app", "-c", "rosti " + tech + " " + version}, "", []string{}, false) } 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 } // 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/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, errors.New("one of the tech has wrong number of parts") } } return techs, nil }