2022-02-05 01:22:53 +00:00
|
|
|
package containers
|
2020-07-09 22:27:23 +00:00
|
|
|
|
|
|
|
import (
|
2021-12-20 03:53:22 +00:00
|
|
|
"errors"
|
2022-04-20 12:06:16 +00:00
|
|
|
"fmt"
|
2020-07-11 21:14:45 +00:00
|
|
|
"log"
|
2020-07-09 22:27:23 +00:00
|
|
|
"path"
|
2023-01-28 10:46:59 +00:00
|
|
|
"strconv"
|
2020-08-23 00:08:05 +00:00
|
|
|
"strings"
|
2020-07-09 22:27:23 +00:00
|
|
|
|
2020-07-13 22:01:42 +00:00
|
|
|
"github.com/rosti-cz/node-api/apps"
|
2022-02-05 01:22:53 +00:00
|
|
|
"github.com/rosti-cz/node-api/detector"
|
2020-07-09 22:27:23 +00:00
|
|
|
)
|
|
|
|
|
2020-08-06 22:36:40 +00:00
|
|
|
// username in the containers under which all containers run
|
|
|
|
const appUsername = "app"
|
|
|
|
const passwordFile = "/srv/.rosti"
|
|
|
|
|
2020-08-23 00:08:05 +00:00
|
|
|
// Process contains info about background application usually running in supervisor
|
|
|
|
type Process struct {
|
2020-08-27 21:46:03 +00:00
|
|
|
Name string `json:"name"`
|
|
|
|
State string `json:"state"`
|
2020-08-23 00:08:05 +00:00
|
|
|
}
|
|
|
|
|
2022-10-04 17:54:26 +00:00
|
|
|
// Current status of the container
|
|
|
|
type ContainerStatus struct {
|
|
|
|
Status string `json:"status"`
|
|
|
|
OOMKilled bool `json:"oom_killed"`
|
|
|
|
}
|
|
|
|
|
2020-07-09 22:27:23 +00:00
|
|
|
// Container extends App struct from App
|
|
|
|
type Container struct {
|
2022-02-05 23:49:32 +00:00
|
|
|
App *apps.App `json:"app"`
|
|
|
|
DockerSock string `json:"-"`
|
|
|
|
BindIPHTTP string `json:"-"`
|
|
|
|
BindIPSSH string `json:"-"`
|
|
|
|
AppsPath string `json:"-"`
|
2020-07-11 11:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Container) getDriver() *Driver {
|
2021-01-09 21:32:52 +00:00
|
|
|
driver := &Driver{
|
2022-02-05 23:49:32 +00:00
|
|
|
DockerSock: c.DockerSock,
|
|
|
|
BindIPHTTP: c.BindIPHTTP,
|
|
|
|
BindIPSSH: c.BindIPSSH,
|
2021-01-09 21:32:52 +00:00
|
|
|
}
|
2020-07-11 11:04:37 +00:00
|
|
|
return driver
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// volumeHostPath each container has one volume mounted into it,
|
|
|
|
func (c *Container) volumeHostPath() string {
|
2022-02-05 23:49:32 +00:00
|
|
|
return path.Join(c.AppsPath, c.App.Name)
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
|
|
|
|
2020-07-25 22:34:16 +00:00
|
|
|
// 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
|
2020-07-16 17:05:38 +00:00
|
|
|
func (c *Container) GetState() (*apps.AppState, error) {
|
2020-07-11 21:14:45 +00:00
|
|
|
status, err := c.Status()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-04-02 16:10:34 +00:00
|
|
|
// 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
|
|
|
|
// }
|
2020-07-11 21:14:45 +00:00
|
|
|
|
2020-07-15 21:32:28 +00:00
|
|
|
bytes, inodes, err := c.DiskUsage()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-02-05 01:22:53 +00:00
|
|
|
processes, err := c.GetSystemProcesses()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
flags, err := detector.Check(processes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-07-15 21:32:28 +00:00
|
|
|
state := apps.AppState{
|
2022-10-04 17:54:26 +00:00
|
|
|
State: status.Status,
|
|
|
|
OOMKilled: status.OOMKilled,
|
2021-04-02 16:10:34 +00:00
|
|
|
// CPUUsage: cpu,
|
|
|
|
// MemoryUsage: memory,
|
|
|
|
CPUUsage: -1.0,
|
|
|
|
MemoryUsage: -1.0,
|
2020-07-15 21:32:28 +00:00
|
|
|
DiskUsageBytes: bytes,
|
2020-07-16 17:05:38 +00:00
|
|
|
DiskUsageInodes: inodes,
|
2022-02-05 01:22:53 +00:00
|
|
|
Flags: flags,
|
2020-07-15 21:32:28 +00:00
|
|
|
}
|
2020-07-11 21:14:45 +00:00
|
|
|
|
2020-07-15 21:32:28 +00:00
|
|
|
return &state, nil
|
2020-07-11 21:14:45 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 22:27:23 +00:00
|
|
|
// Status returns state of the container
|
2022-02-07 17:30:42 +00:00
|
|
|
// Possible values: running, exited (stopped), no-container, unknown
|
2022-10-04 17:54:26 +00:00
|
|
|
func (c *Container) Status() (ContainerStatus, error) {
|
|
|
|
status := ContainerStatus{
|
|
|
|
Status: "unknown",
|
|
|
|
}
|
2020-07-11 11:04:37 +00:00
|
|
|
|
|
|
|
driver := c.getDriver()
|
|
|
|
containerStatus, err := driver.Status(c.App.Name)
|
2020-07-11 21:14:45 +00:00
|
|
|
if err != nil && err.Error() == "no container found" {
|
2022-10-04 17:54:26 +00:00
|
|
|
return ContainerStatus{Status: "no-container"}, nil
|
2020-07-11 21:14:45 +00:00
|
|
|
}
|
2020-07-11 11:04:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return status, err
|
|
|
|
}
|
|
|
|
|
2022-10-04 17:54:26 +00:00
|
|
|
return containerStatus, nil
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
|
|
|
|
2020-07-25 22:34:16 +00:00
|
|
|
// DiskUsage returns number of bytes and inodes used by the container in it's mounted volume
|
2020-07-11 11:04:37 +00:00
|
|
|
func (c *Container) DiskUsage() (int, int, error) {
|
2020-07-09 22:27:23 +00:00
|
|
|
return du(c.volumeHostPath())
|
|
|
|
}
|
|
|
|
|
2020-07-16 21:24:09 +00:00
|
|
|
// ResourceUsage returns amount of memory in B and CPU in % that the app occupies
|
2020-07-11 11:04:37 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-07-09 22:27:23 +00:00
|
|
|
// Create creates the container
|
|
|
|
func (c *Container) Create() error {
|
2020-07-11 11:04:37 +00:00
|
|
|
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
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Start starts the container
|
|
|
|
func (c *Container) Start() error {
|
2020-07-11 11:04:37 +00:00
|
|
|
driver := c.getDriver()
|
|
|
|
|
|
|
|
return driver.Start(c.App.Name)
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Stop stops the container
|
|
|
|
func (c *Container) Stop() error {
|
2020-07-11 11:04:37 +00:00
|
|
|
driver := c.getDriver()
|
|
|
|
|
|
|
|
return driver.Stop(c.App.Name)
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
|
|
|
|
2020-07-11 11:04:37 +00:00
|
|
|
// 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)
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Destroy removes the container but keeps the data so it can be created again
|
|
|
|
func (c *Container) Destroy() error {
|
2020-07-11 11:04:37 +00:00
|
|
|
driver := c.getDriver()
|
|
|
|
|
|
|
|
return driver.Remove(c.App.Name)
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete removes both data and the container
|
|
|
|
func (c *Container) Delete() error {
|
2020-10-05 09:43:09 +00:00
|
|
|
status, err := c.Status()
|
2020-07-11 11:04:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-05 09:43:09 +00:00
|
|
|
// 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.
|
2022-10-04 17:54:26 +00:00
|
|
|
if status.Status != "no-container" {
|
2020-10-05 09:43:09 +00:00
|
|
|
err = c.Destroy()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-05 23:49:32 +00:00
|
|
|
volumePath := path.Join(c.AppsPath, c.App.Name)
|
2020-07-11 11:04:37 +00:00
|
|
|
err = removeDirectory(volumePath)
|
2020-07-11 21:14:45 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
}
|
2020-07-11 11:04:37 +00:00
|
|
|
|
2020-07-11 21:14:45 +00:00
|
|
|
return nil
|
2020-07-09 22:27:23 +00:00
|
|
|
}
|
2020-08-06 22:36:40 +00:00
|
|
|
|
|
|
|
// SetPassword configures password for system user app in the container
|
|
|
|
func (c *Container) SetPassword(password string) error {
|
|
|
|
driver := c.getDriver()
|
|
|
|
|
2020-08-23 00:08:05 +00:00
|
|
|
_, err := driver.Exec(c.App.Name, []string{"chpasswd"}, appUsername+":"+password, []string{}, false)
|
2020-08-06 22:36:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-08-23 00:08:05 +00:00
|
|
|
_, err = driver.Exec(c.App.Name, []string{"tee", passwordFile}, password, []string{}, false)
|
2020-08-06 22:36:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-28 10:46:59 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-08-06 22:36:40 +00:00
|
|
|
// 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)
|
|
|
|
|
2020-08-23 00:08:05 +00:00
|
|
|
_, err := driver.Exec(c.App.Name, []string{"mkdir", "-p", directory}, "", []string{}, false)
|
2020-08-06 22:36:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-08-23 00:08:05 +00:00
|
|
|
_, err = driver.Exec(c.App.Name, []string{"tee", filename}, text, []string{}, false)
|
2020-08-06 22:36:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-08-31 10:29:34 +00:00
|
|
|
_, err = driver.Exec(c.App.Name, []string{"chown", "app:app", directory}, "", []string{}, false)
|
2020-08-06 22:36:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-08-31 10:29:34 +00:00
|
|
|
_, err = driver.Exec(c.App.Name, []string{"chown", "app:app", filename}, "", []string{}, false)
|
2020-08-06 22:36:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-08-23 00:08:05 +00:00
|
|
|
_, err = driver.Exec(c.App.Name, []string{"chmod", mode, filename}, "", []string{}, false)
|
2020-08-20 23:00:24 +00:00
|
|
|
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.
|
2021-12-23 13:01:27 +00:00
|
|
|
// If version is empty string default version will be used.
|
|
|
|
func (c *Container) SetTechnology(tech string, version string) error {
|
2020-08-20 23:00:24 +00:00
|
|
|
driver := c.getDriver()
|
|
|
|
|
2021-12-23 13:01:27 +00:00
|
|
|
var err error
|
|
|
|
|
|
|
|
// TODO: script injection here?
|
2022-06-15 18:16:20 +00:00
|
|
|
var output *[]byte
|
2021-12-23 13:01:27 +00:00
|
|
|
if version == "" {
|
2022-06-15 18:16:20 +00:00
|
|
|
output, err = driver.Exec(c.App.Name, []string{"su", "app", "-c", "rosti " + tech}, "", []string{}, false)
|
2021-12-23 13:01:27 +00:00
|
|
|
} else {
|
2022-06-15 18:16:20 +00:00
|
|
|
output, err = driver.Exec(c.App.Name, []string{"su", "app", "-c", "rosti " + tech + " " + version}, "", []string{}, false)
|
2021-12-23 13:01:27 +00:00
|
|
|
}
|
2022-06-15 18:16:20 +00:00
|
|
|
|
|
|
|
log.Printf("DEBUG: enable tech for %s output: %s", c.App.Name, string(*output))
|
|
|
|
|
2020-08-06 22:36:40 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-08-23 00:08:05 +00:00
|
|
|
|
|
|
|
// GetProcessList returns list of processes managed by supervisor.
|
2022-02-05 23:01:05 +00:00
|
|
|
func (c *Container) GetProcessList() ([]Process, error) {
|
2020-08-23 00:08:05 +00:00
|
|
|
driver := c.getDriver()
|
|
|
|
|
|
|
|
processes := []Process{}
|
|
|
|
|
|
|
|
stdouterr, err := driver.Exec(c.App.Name, []string{"supervisorctl", "status"}, "", []string{}, true)
|
|
|
|
if err != nil {
|
2022-02-05 23:01:05 +00:00
|
|
|
return processes, nil
|
2020-08-23 00:08:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-05 23:01:05 +00:00
|
|
|
return processes, nil
|
2020-08-23 00:08:05 +00:00
|
|
|
}
|
2021-12-20 03:53:22 +00:00
|
|
|
|
2023-01-28 10:46:59 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-02-05 01:22:53 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-12-20 03:53:22 +00:00
|
|
|
// 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 {
|
2021-12-20 18:13:45 +00:00
|
|
|
// in case there is an error just return empty response
|
|
|
|
return tech, nil
|
2021-12-20 03:53:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-22 00:12:42 +00:00
|
|
|
// Probably single technology image in case the output is empty
|
|
|
|
return tech, nil
|
2021-12-20 03:53:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetTechs returns all techs available in the container
|
|
|
|
func (c *Container) GetTechs() (apps.AppTechs, error) {
|
|
|
|
techs := apps.AppTechs{}
|
|
|
|
|
|
|
|
driver := c.getDriver()
|
|
|
|
|
2022-04-20 12:06:16 +00:00
|
|
|
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)
|
2021-12-20 03:53:22 +00:00
|
|
|
if err != nil {
|
2021-12-20 18:13:45 +00:00
|
|
|
// in case there is an error just return empty response
|
|
|
|
return techs, nil
|
2021-12-20 03:53:22 +00:00
|
|
|
}
|
|
|
|
|
2021-12-22 00:02:23 +00:00
|
|
|
// If the directory doesn't exist it's single technology image
|
|
|
|
if strings.Contains(string(*stdouterr), "No such file or directory") {
|
|
|
|
return techs, nil
|
|
|
|
}
|
|
|
|
|
2021-12-20 17:51:34 +00:00
|
|
|
techsRaw := strings.Fields(string(*stdouterr))
|
2021-12-20 03:53:22 +00:00
|
|
|
for _, techRaw := range techsRaw {
|
|
|
|
techParts := strings.Split(techRaw, "-")
|
|
|
|
if len(techParts) == 2 {
|
|
|
|
techs = append(techs, apps.AppTech{
|
|
|
|
Name: techParts[0],
|
|
|
|
Version: techParts[1],
|
|
|
|
})
|
|
|
|
} else {
|
2022-04-20 12:06:16 +00:00
|
|
|
return techs, fmt.Errorf("one of the tech has wrong number of parts (%s)", techRaw)
|
2021-12-20 03:53:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return techs, nil
|
|
|
|
}
|