node-api/containers/types.go

595 lines
15 KiB
Go

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{},
)
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
}