package glue import ( "errors" "fmt" "log" "strings" "time" "github.com/jinzhu/gorm" "github.com/rosti-cz/node-api/apps" "github.com/rosti-cz/node-api/containers" docker "github.com/rosti-cz/node-api/containers" "github.com/rosti-cz/node-api/detector" "github.com/rosti-cz/node-api/node" ) // Wait for the container a little bit longer const ENABLE_TECH_WAIT = 10 // Processor separates logic of apps, containers, detector and node from handlers. // It defines common interface for both types of handlers, HTTP and the events. type Processor struct { AppName string DB *gorm.DB SnapshotProcessor *apps.SnapshotProcessor NodeProcessor *node.Processor WaitForAppLoops uint // each loop is five seconds DockerSock string BindIPHTTP string BindIPSSH string AppsPath string } // Return prepared Container instance func (p *Processor) getContainer() (containers.Container, error) { container := containers.Container{} processor := p.getAppProcessor() app, err := processor.Get(p.AppName) if err != nil { return container, err } container = docker.Container{ App: &app, DockerSock: p.DockerSock, BindIPHTTP: p.BindIPHTTP, BindIPSSH: p.BindIPSSH, AppsPath: p.AppsPath, } return container, nil } // returns instance of getAppProcessor func (p *Processor) getAppProcessor() apps.AppsProcessor { processor := apps.AppsProcessor{ DB: p.DB, } processor.Init() return processor } // waits until app is ready func (p *Processor) waitForApp() error { sleepFor := 5 * time.Second loops := 6 if p.WaitForAppLoops != 0 { loops = int(p.WaitForAppLoops) } statsProcessor := StatsProcessor{ DB: p.DB, DockerSock: p.DockerSock, BindIPHTTP: p.BindIPHTTP, BindIPSSH: p.BindIPSSH, AppsPath: p.AppsPath, } for i := 0; i < loops; i++ { err := statsProcessor.UpdateState(p.AppName) if err != nil { time.Sleep(sleepFor) continue } container, err := p.getContainer() if err != nil { return err } status, err := container.Status() if err != nil { return err } if status.Status == "running" { return nil } time.Sleep(sleepFor) } return errors.New("timeout reached") } // List returns list of apps // noUpdate skips stats gathering to speed things up func (p *Processor) List(noUpdate bool) (apps.Apps, error) { appList := apps.Apps{} if !noUpdate { statsProcessor := StatsProcessor{ DB: p.DB, DockerSock: p.DockerSock, BindIPHTTP: p.BindIPHTTP, BindIPSSH: p.BindIPSSH, AppsPath: p.AppsPath, } err := statsProcessor.GatherStates() if err != nil { return appList, fmt.Errorf("backend error: %v", err) } } processor := p.getAppProcessor() appList, err := processor.List() if err != nil { return appList, fmt.Errorf("backend error: %v", err) } return appList, err } // Get returns one app func (p *Processor) Get(noUpdate bool) (apps.App, error) { app := apps.App{} if !noUpdate { statsProcessor := StatsProcessor{ DB: p.DB, DockerSock: p.DockerSock, BindIPHTTP: p.BindIPHTTP, BindIPSSH: p.BindIPSSH, AppsPath: p.AppsPath, } err := statsProcessor.UpdateState(p.AppName) if err != nil { return app, err } } processor := p.getAppProcessor() app, err := processor.Get(p.AppName) if err != nil { return app, err } // Gather runtime info about the container if !noUpdate { container := docker.Container{ App: &app, DockerSock: p.DockerSock, BindIPHTTP: p.BindIPHTTP, BindIPSSH: p.BindIPSSH, AppsPath: p.AppsPath, } status, err := container.Status() if err != nil { return app, err } if status.Status == "running" { var err error app.Techs, err = container.GetTechs() if err != nil { return app, err } app.PrimaryTech, err = container.GetPrimaryTech() if err != nil { return app, err } processList, err := container.GetSystemProcesses() if err != nil { return app, err } flags, err := detector.Check(processList) if err != nil { return app, err } app.Flags = flags.String() } } return app, nil } // Create creates a single app in the system func (p *Processor) Create(appTemplate apps.App) error { if appTemplate.Env == nil { appTemplate.Env = make(map[string]string) } err := p.Register(appTemplate) if err != nil { return err } container := docker.Container{ App: &appTemplate, DockerSock: p.DockerSock, BindIPHTTP: p.BindIPHTTP, BindIPSSH: p.BindIPSSH, AppsPath: p.AppsPath, } err = container.Create() if err != nil { return err } // Restore from snapshot if it's noted in the request if len(appTemplate.Snapshot) > 0 { log.Printf("App %s is going to be created from %s snapshot\n", appTemplate.Name, appTemplate.Snapshot) // Restore the data err = p.SnapshotProcessor.RestoreSnapshot(appTemplate.Snapshot, appTemplate.Name) if err != nil { return fmt.Errorf("failed to restore snapshot: %v", err) } } err = container.Start() if err != nil { return fmt.Errorf("failed to start container: %v", err) } // Wait for the app to be created err = p.waitForApp() if err != nil { return fmt.Errorf("failed to wait for app: %v", err) } time.Sleep(5 * time.Second) // We wait for a little bit longer to make sure the container is fully started // Setup SSH keys if it's noted in the request log.Println("Checking if SSH key is required") if len(appTemplate.Setup.SSHKeys) > 0 && len(appTemplate.Snapshot) == 0 { log.Println("Setting up SSH keys") err = p.UpdateKeys(appTemplate.Setup.SSHKeys) if err != nil { return fmt.Errorf("failed to update keys: %v", err) } } // Setup technology if it's noted in the request if len(appTemplate.Setup.Tech) > 0 && len(appTemplate.Snapshot) == 0 { err = p.EnableTech(appTemplate.Setup.Tech, appTemplate.Setup.TechVersion) if err != nil { return fmt.Errorf("failed to enable tech: %v", err) } } // Set password if it's noted in the request if len(appTemplate.Setup.Password) > 0 && len(appTemplate.Snapshot) == 0 { err = p.SetPassword(appTemplate.Setup.Password) if err != nil { return fmt.Errorf("failed to set password: %v", err) } } // Changes port of the app hosted inside the container if appTemplate.AppPort != 0 && len(appTemplate.Snapshot) == 0 { err = container.SetAppPort(appTemplate.AppPort) if err != nil { return fmt.Errorf("failed to change app port to %d: %v", appTemplate.AppPort, err) } err = container.RestartProcess("nginx") if err != nil { return fmt.Errorf("failed to restart nginx: %v", err) } } return nil } // Register registers app without creating a container for it // Returns app name and an error func (p *Processor) Register(appTemplate apps.App) error { processor := p.getAppProcessor() err := processor.New(appTemplate.Name, appTemplate.SSHPort, appTemplate.HTTPPort, appTemplate.Image, appTemplate.CPU, appTemplate.Memory) if err != nil { if validationError, ok := err.(apps.ValidationError); ok { return fmt.Errorf("validation error: %v", validationError.Error()) } return err } return nil } // Update updates application func (p *Processor) Update(appTemplate apps.App) error { processor := p.getAppProcessor() app, err := processor.Update(appTemplate.Name, appTemplate.SSHPort, appTemplate.HTTPPort, appTemplate.Image, appTemplate.CPU, appTemplate.Memory) if err != nil { if validationError, ok := err.(apps.ValidationError); ok { return fmt.Errorf("validation error: %v", validationError.Error()) } return err } container := docker.Container{ App: app, DockerSock: p.DockerSock, BindIPHTTP: p.BindIPHTTP, BindIPSSH: p.BindIPSSH, AppsPath: p.AppsPath, } err = container.Destroy() if err != nil && err.Error() == "no container found" { // We don't care if the container didn't exist anyway err = nil } if err != nil { return err } err = container.Create() if err != nil { return err } err = container.Start() if err != nil { return err } return nil } // Delete removes app from the system func (p *Processor) Delete() error { processor := p.getAppProcessor() container, err := p.getContainer() if err != nil { log.Println("ERROR: delete app:", err.Error()) } status, err := container.Status() if err != nil { return err } if status.Status != "no-container" { // We stop the container first err = container.Stop() if err != nil { return err } // Then delete it err = container.Delete() if err != nil { return err } } err = processor.Delete(p.AppName) if err != nil { return err } return nil } // Stop stops app func (p *Processor) Stop() error { container, err := p.getContainer() if err != nil { return err } status, err := container.Status() if err != nil { return err } // Stop the container only when it exists if status.Status != "no-container" { err = container.Stop() if err != nil { return err } err = container.Destroy() if err != nil { return err } } return nil } // Start starts app func (p *Processor) Start() error { container, err := p.getContainer() if err != nil { return err } status, err := container.Status() if err != nil { return err } if status.Status == "no-container" { err = container.Create() if err != nil { return err } } err = container.Start() if err != nil { return err } return nil } // Restart restarts app func (p *Processor) Restart() error { container, err := p.getContainer() if err != nil { return err } err = container.Restart() if err != nil { return err } return nil } // UpdateKeys uploads SSH keys into the container // keys parameters is just a string, one key per line func (p *Processor) UpdateKeys(keys string) error { err := p.waitForApp() if err != nil { return err } container, err := p.getContainer() if err != nil { return err } log.Println("Storing keys into " + sshPubKeysLocation) err = container.AppendFile(sshPubKeysLocation, keys+"\n", "0600") if err != nil { return err } return nil } // SetPassword sets up a new password to access the SSH user func (p *Processor) SetPassword(password string) error { err := p.waitForApp() if err != nil { return err } container, err := p.getContainer() if err != nil { return err } err = container.SetPassword(password) if err != nil { return err } return nil } // Generate SSH key and adds it into authorized_keys // These pair of keys is used for deployment. // Returns private key, pubkey and error. // Keys are returned every time even if it was already generated func (p *Processor) GenerateDeploySSHKeys() (string, string, error) { container, err := p.getContainer() if err != nil { return "", "", err } // If the container is not running we skip this code status, err := container.Status() if err != nil { return "", "", err } if status.Status != "running" { return "", "", nil } created, err := container.GenerateDeploySSHKeys() if err != nil { return "", "", err } privateKey, pubKey, err := container.GetDeploySSHKeys() if err != nil { return "", "", err } if created { err = container.AppendFile(sshPubKeysLocation, pubKey+"\n", "0600") if err != nil { return "", "", err } } return privateKey, pubKey, nil } // Return SSH host key without hostname (first part of the line) func (p *Processor) GetHostKey() (string, error) { container, err := p.getContainer() if err != nil { return "", err } // If the container is not running we skip this code status, err := container.Status() if err != nil { return "", err } if status.Status != "running" { return "", nil } hostKey, err := container.GetHostKey() if err != nil { return "", err } return hostKey, nil } // Processes returns list of supervisord processes func (p *Processor) Processes() ([]docker.Process, error) { container, err := p.getContainer() if err != nil { return nil, err } processes, err := container.GetProcessList() if err != nil { return nil, err } return processes, nil } // EnableTech sets up runtime for new tech or new version a tech func (p *Processor) EnableTech(service, version string) error { err := p.waitForApp() if err != nil { return err } time.Sleep(ENABLE_TECH_WAIT * time.Second) container, err := p.getContainer() if err != nil { return err } err = container.SetTechnology(service, version) if err != nil { return err } return nil } // Rebuild recreates container for app func (p *Processor) Rebuild() error { container, err := p.getContainer() if err != nil { return err } err = container.Destroy() if err != nil && !strings.Contains(err.Error(), "no container found") { return err } err = container.Create() if err != nil { return err } err = container.Start() if err != nil { return err } return nil } // AddLabel adds a label to the app func (p *Processor) AddLabel(label string) error { appProcessor := p.getAppProcessor() err := appProcessor.AddLabel(p.AppName, label) if err != nil { return err } return nil } // RemoveLabel removes a label from the app func (p *Processor) RemoveLabel(label string) error { appProcessor := p.getAppProcessor() err := appProcessor.RemoveLabel(p.AppName, label) if err != nil { return err } return nil } // GetNote returns node's info func (p *Processor) GetNode() (*node.Node, error) { node, err := p.NodeProcessor.GetNodeInfo() if err != nil { return nil, err } return node, nil } // CreateSnapshot creates a snapshot of given app func (p *Processor) CreateSnapshot(labels []string) error { _, err := p.SnapshotProcessor.CreateSnapshot(p.AppName, labels) return err } // RestoreFromSnapshot restores app from given snapshot func (p *Processor) RestoreFromSnapshot(snapshotName string) error { container, err := p.getContainer() // Stop the container status, err := container.Status() if err != nil { return err } // Stop the container only when it exists if status.Status != "no-container" { err = container.Stop() if err != nil { return err } } // Restore the data err = p.SnapshotProcessor.RestoreSnapshot(snapshotName, p.AppName) if err != nil { return err } // Start the container status, err = container.Status() if err != nil { return err } if status.Status == "no-container" { err = container.Create() if err != nil { return err } } err = container.Start() if err != nil { return err } return nil } // ListSnapshots returns list of snapshots func (p *Processor) ListSnapshots() (SnapshotsMetadata, error) { snapshots, err := p.SnapshotProcessor.ListAppSnapshots(p.AppName) if err != nil { return SnapshotsMetadata{}, err } var output SnapshotsMetadata for _, snapshot := range snapshots { output = append(output, SnapshotMetadata{ Key: snapshot.KeyName(p.SnapshotProcessor.IndexLabel), Metadata: snapshot, }) } return output, nil } // ListAppsSnapshots returns list of snapshots for given app list func (p *Processor) ListAppsSnapshots(appNames []string) (SnapshotsMetadata, error) { snapshots, err := p.SnapshotProcessor.ListAppsSnapshots(appNames) if err != nil { return SnapshotsMetadata{}, err } var output SnapshotsMetadata for _, snapshot := range snapshots { output = append(output, SnapshotMetadata{ Key: snapshot.KeyName(p.SnapshotProcessor.IndexLabel), Metadata: snapshot, }) } return output, nil } // ListSnapshotsByLabel returns list of snapshots by given label func (p *Processor) ListSnapshotsByLabel(label string) (SnapshotsMetadata, error) { snapshots, err := p.SnapshotProcessor.ListAppsSnapshotsByLabel(label) if err != nil { return SnapshotsMetadata{}, err } var output SnapshotsMetadata for _, snapshot := range snapshots { output = append(output, SnapshotMetadata{ Key: snapshot.KeyName(p.SnapshotProcessor.IndexLabel), Metadata: snapshot, }) } return output, nil } // GetSnapshot returns info about a single snapshot func (p *Processor) GetSnapshot(snapshotName string) (SnapshotMetadata, error) { snapshot, err := p.SnapshotProcessor.GetSnapshot(snapshotName) if err != nil { return SnapshotMetadata{}, err } output := SnapshotMetadata{ Key: snapshot.KeyName(p.SnapshotProcessor.IndexLabel), Metadata: snapshot, } return output, nil } // GetSnapshotDownloadLink return download link for a snapshot func (p *Processor) GetSnapshotDownloadLink(snapshotName string) (string, error) { link, err := p.SnapshotProcessor.GetDownloadLink(snapshotName) if err != nil { return "", err } return link, nil } // DeleteSnapshot deletes a snapshot func (p *Processor) DeleteSnapshot(snapshotName string) error { err := p.SnapshotProcessor.DeleteSnapshot(snapshotName) if err != nil { return err } return nil } // DeleteAppSnapshots deletes all snapshot of given app func (p *Processor) DeleteAppSnapshots() error { err := p.SnapshotProcessor.DeleteAppSnapshots(p.AppName) if err != nil { return err } return nil } // Returns active technology in the app func (p *Processor) GetActiveTech() (*containers.TechInfo, error) { container, err := p.getContainer() if err != nil { return nil, err } tech, err := container.GetActiveTech() if err != nil { return tech, err } return tech, nil }