Compare commits

..

No commits in common. "main" and "snapshots" have entirely different histories.

44 changed files with 1504 additions and 6693 deletions

25
.drone.yml Normal file
View File

@ -0,0 +1,25 @@
kind: pipeline
type: docker
name: testing
steps:
- name: test
image: golang
environment:
SNAPSHOTS_S3_ENDPOINT: minio:9000
TEST_S3_ENDPOINT: minio:9000
commands:
- go mod tidy
- make test
services:
- name: minio
image: minio/minio:latest
environment:
MINIO_ROOT_USER: test
MINIO_ROOT_PASSWORD: testtest
command:
- server
- /data
- --console-address
- :9001

View File

@ -1,33 +0,0 @@
name: Release
on:
release:
types: [published]
# push:
# branches: [main]
workflow_dispatch: {}
jobs:
deploy-production:
runs-on: [amd64, prod]
env:
NODES: node-22.rosti.cz node-23.rosti.cz node-24.rosti.cz node-25.rosti.cz node-26.rosti.cz
steps:
- uses: actions/checkout@v4
- name: deploy
run: |
echo "Building for Debian 12 .."
docker run --rm --privileged -ti -v `pwd`:/srv golang:1.21-bookworm /bin/sh -c "cd /srv && go build"
for NODE in $NODES; do
echo "\033[0;32mDeploying $NODE\033[0m"
echo "\033[1;33m.. scanning SSH keys\033[0m"
ssh -o "StrictHostKeyChecking=no" root@$NODE echo "Setting up key"
echo "\033[1;33m.. copying the binary\033[0m"
scp node-api root@$NODE:/usr/local/bin/node-api_
echo "\033[1;33m.. replacing the binary\033[0m"
ssh root@$NODE mv /usr/local/bin/node-api_ /usr/local/bin/node-api
echo "\033[1;33m.. restarting service\033[0m"
ssh root@$NODE systemctl restart node-api
done

View File

@ -1,62 +0,0 @@
name: Unittests
on:
push:
branches: [main]
workflow_dispatch: {}
jobs:
unittests:
runs-on: [amd64, moon]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v3
with:
go-version: 1.21
- name: start minio
run: |
docker run -d --rm --name nodeapi_minio -p 9000:9000 -p 9001:9001 -e MINIO_ROOT_USER=test -e MINIO_ROOT_PASSWORD=testtest minio/minio:latest server /data --console-address :9001
- name: deps
run: apt update && apt install -y tar zstd
- name: Test
run: |
make test
- name: stop minio
if: always()
run: |
docker stop nodeapi_minio
# TODO: probably not supported by Gitea workflows yet
# services:
# minio:
# image: minio/minio:latest
# env:
# MINIO_ROOT_USER: test
# MINIO_ROOT_PASSWORD: testtest
# ports:
# - 9001:9001
# options: server /data --console-address :9001
deploy-dev:
runs-on: [amd64, moon]
env:
NODES: "192.168.1.33"
steps:
- uses: actions/checkout@v4
- name: deploy
run: |
# echo LS1
# ls
docker run --rm --privileged -ti -v `pwd`:/srv golang:1.20-buster /bin/sh -c "cd /srv && go build"
# docker run --rm --privileged -ti -v `pwd`:/srv golang:1.20-buster /bin/sh -c "cd /srv && echo LS2 && ls"
for NODE in $NODES; do
echo "\033[0;32mDeploying $NODE\033[0m"
echo "\033[1;33m.. scanning SSH keys\033[0m"
ssh -o "StrictHostKeyChecking=no" root@$NODE echo "Setting up key"
echo "\033[1;33m.. copying the binary\033[0m"
scp node-api root@$NODE:/usr/local/bin/node-api_
echo "\033[1;33m.. replacing the binary\033[0m"
ssh root@$NODE mv /usr/local/bin/node-api_ /usr/local/bin/node-api
echo "\033[1;33m.. restarting service\033[0m"
ssh root@$NODE systemctl restart node-api
done

View File

@ -1,9 +1,7 @@
.PHONY: test
test:
go test -race -v apps/*.go
go test -race -v apps/drivers/*.go
go test -race -v detector/*.go
# env DOCKER_SOCKET="unix:///var/run/docker.sock" go test -v containers/*.go # Doesn't work in Drone right now
go test -v apps/*.go
go test -v apps/drivers/*.go
build:
#podman run --rm --privileged -ti -v ${shell pwd}:/srv docker.io/library/golang:1.14-stretch /bin/sh -c "cd /srv && go build"
@ -20,9 +18,4 @@ minio:
-p 9001:9001 \
-e MINIO_ROOT_USER=test \
-e MINIO_ROOT_PASSWORD=testtest \
docker.io/minio/minio:latest server /data --console-address ":9001"
.PHONY: clean
clean:
-podman stop rosti-snapshots
-podman rm rosti-snapshots
minio/minio server /data --console-address ":9001"

View File

@ -4,16 +4,3 @@
Node API is an microservice that runs on node servers. It provides interface between
Docker and the admin site.
## Test
On Fedora run podman API:
Root: sudo systemctl enable --now podman.socket
Rootless: podman system service -t 0 --log-level=debug
set -x DOCKER_SOCKET unix:/run/user/1000/podman/podman.sock
# or
export DOCKER_SOCKET=unix:/run/user/1000/podman/podman.sock

View File

@ -1,7 +1,6 @@
package drivers
import (
"errors"
"io"
"io/ioutil"
"os"
@ -84,8 +83,3 @@ func (f FSDriver) List(prefix string) ([]string, error) {
func (f FSDriver) Delete(key string) error {
return os.Remove(path.Join(f.Path, key))
}
// GetDownloadLink is not implemented in this driver
func (f FSDriver) GetDownloadLink(key string) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -5,14 +5,11 @@ import (
"context"
"fmt"
"io/ioutil"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
const s3LinkExpiration = 72 // in hours
// S3Driver provides basic interface for S3 storage compatible with Driver interface
type S3Driver struct {
// S3 storage related things
@ -158,22 +155,3 @@ func (s S3Driver) Delete(key string) error {
return nil
}
// GetDownloadLink returns URL of object with given key. That URL can be used to download the object.
func (s S3Driver) GetDownloadLink(key string) (string, error) {
client, err := s.getMinioClient()
if err != nil {
return "", fmt.Errorf("getting minio client error: %v", err)
}
expiry := time.Second * s3LinkExpiration * 60 * 60 // 1 day.
presignedURL, err := client.PresignedPutObject(context.Background(), s.Bucket, key, expiry)
if err != nil {
return "", fmt.Errorf("generating presign URL error: %v", err)
}
if s.S3SSL {
return fmt.Sprintf("https://%s%s?%s", s.S3Endpoint, presignedURL.Path, presignedURL.RawQuery), nil
}
return fmt.Sprintf("http://%s%s?%s", s.S3Endpoint, presignedURL.Path, presignedURL.RawQuery), nil
}

View File

@ -77,12 +77,3 @@ func TestS3Delete(t *testing.T) {
assert.Nil(t, err)
assert.NotContains(t, keys, "testkey")
}
func TestGetDownloadLink(t *testing.T) {
err := testS3Driver.Write("testkey", []byte(testContent))
assert.Nil(t, err)
link, err := testS3Driver.GetDownloadLink("testkey")
assert.Nil(t, err)
assert.Contains(t, link, "/testsnapshots/testkey?X-Amz-Algorithm=AWS4-HMAC-SHA256")
}

View File

@ -19,7 +19,4 @@ type DriverInterface interface {
// Delete deletes one object
Delete(key string) error
// GetDownloadLink returns URL of object with given key. That URL can be used to download the object.
GetDownloadLink(key string) (string, error)
}

View File

@ -3,11 +3,9 @@ package apps
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/rosti-cz/node-api/detector"
)
// AppsProcessor encapsulates functions for apps manipulation
// This handles only the database part but not containers
type AppsProcessor struct {
DB *gorm.DB
}
@ -18,19 +16,19 @@ func (a *AppsProcessor) Init() {
}
// Get returns one app
func (a *AppsProcessor) Get(name string) (App, error) {
func (a *AppsProcessor) Get(name string) (*App, error) {
var app App
err := a.DB.Preload("Labels").Where("name = ?", name).First(&app).Error
if err != nil {
return app, err
return nil, err
}
return app, nil
return &app, nil
}
// List returns all apps located on this node
func (a *AppsProcessor) List() (Apps, error) {
func (a *AppsProcessor) List() (*Apps, error) {
var apps Apps
err := a.DB.Preload("Labels").Find(&apps).Error
@ -38,7 +36,7 @@ func (a *AppsProcessor) List() (Apps, error) {
return nil, err
}
return apps, nil
return &apps, nil
}
// New creates new record about application in the database
@ -67,7 +65,7 @@ func (a *AppsProcessor) New(name string, SSHPort int, HTTPPort int, image string
}
// Update changes value about app in the database
func (a *AppsProcessor) Update(name string, SSHPort int, HTTPPort int, image string, CPU int, memory int, env map[string]string) (*App, error) {
func (a *AppsProcessor) Update(name string, SSHPort int, HTTPPort int, image string, CPU int, memory int) (*App, error) {
var app App
err := a.DB.Where("name = ?", name).First(&app).Error
@ -98,10 +96,6 @@ func (a *AppsProcessor) Update(name string, SSHPort int, HTTPPort int, image str
app.HTTPPort = HTTPPort
}
if len(env) != 0 {
app.SetEnv(env)
}
validationErrors := app.Validate()
if len(validationErrors) != 0 {
return &app, ValidationError{
@ -115,25 +109,21 @@ func (a *AppsProcessor) Update(name string, SSHPort int, HTTPPort int, image str
}
// UpdateResources updates various metrics saved in the database
func (a *AppsProcessor) UpdateResources(name string, state string, OOMKilled bool, CPUUsage float64, memory int, diskUsageBytes int, diskUsageInodes int, flags detector.Flags, isPasswordSet bool) error {
func (a *AppsProcessor) UpdateResources(name string, state string, CPUUsage float64, memory int, diskUsageBytes int, diskUsageInodes int) error {
err := a.DB.Model(&App{}).Where("name = ?", name).Updates(App{
State: state,
OOMKilled: OOMKilled,
CPUUsage: CPUUsage,
MemoryUsage: memory,
DiskUsageBytes: diskUsageBytes,
DiskUsageInodes: diskUsageInodes,
Flags: flags.String(),
IsPasswordSet: isPasswordSet,
}).Error
return err
}
// UpdateState sets container's state
func (a *AppsProcessor) UpdateState(name string, state string, OOMKilled bool) error {
func (a *AppsProcessor) UpdateState(name string, state string) error {
err := a.DB.Model(&App{}).Where("name = ?", name).Updates(App{
State: state,
OOMKilled: OOMKilled,
State: state,
}).Error
return err
}
@ -188,5 +178,5 @@ func (a *AppsProcessor) RemoveLabel(appName string, label string) error {
return err
}
return a.DB.Where("value = ? AND app_id = ?", label, app.ID).Delete(&Label{}).Error
return a.DB.Where("label = ? AND app_id = ?", label, app.ID).Delete(&Label{}).Error
}

View File

@ -1,194 +0,0 @@
package apps
import (
"log"
"testing"
"github.com/jinzhu/gorm"
"github.com/rosti-cz/node-api/detector"
"github.com/stretchr/testify/assert"
// This is line from GORM documentation that imports database dialect
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
const testDBPath = "file::memory:?cache=shared"
func testDB() *gorm.DB {
db, err := gorm.Open("sqlite3", testDBPath)
if err != nil {
log.Fatalln(err)
}
return db
}
func TestAppsProcessorGet(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("testapp_1234", 1000, 1001, "testimage", 2, 256)
assert.Nil(t, err)
app, err := processor.Get("testapp_1234")
assert.Nil(t, err)
assert.Greater(t, int(app.ID), 0)
}
func TestAppsProcessorList(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("testapp_2234", 1002, 1003, "testimage", 2, 256)
assert.Nil(t, err)
apps, err := processor.List()
assert.Nil(t, err)
assert.Equal(t, "testapp_1234", apps[0].Name)
}
func TestAppsProcessorNew(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("testapp_1234", 1002, 1003, "testimage", 2, 256)
assert.Nil(t, err)
}
func TestAppsProcessorUpdate(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("updateapp_1224", 1002, 1003, "testimage", 2, 256)
assert.Nil(t, err)
env := make(map[string]string)
env["TEST"] = "test"
_, err = processor.Update("updateapp_1224", 1052, 1053, "testimage2", 4, 512, env)
assert.Nil(t, err)
app, err := processor.Get("updateapp_1224")
assert.Nil(t, err)
assert.Equal(t, 1052, app.SSHPort)
assert.Equal(t, 1053, app.HTTPPort)
assert.Equal(t, "testimage2", app.Image)
assert.Equal(t, 4, app.CPU)
assert.Equal(t, 512, app.Memory)
}
func TestAppsProcessorUpdateResources(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("updateresources_1224", 1002, 1003, "testimage", 2, 256)
assert.Nil(t, err)
err = processor.UpdateResources("updateresources_1224", "running", true, 1000, 256, 100, 200, detector.Flags{"test"}, true)
assert.Nil(t, err)
app, err := processor.Get("updateresources_1224")
assert.Nil(t, err)
assert.Equal(t, "running", app.State)
assert.Equal(t, true, app.OOMKilled)
assert.Equal(t, float64(1000), app.CPUUsage)
assert.Equal(t, 256, app.MemoryUsage)
assert.Equal(t, 100, app.DiskUsageBytes)
assert.Equal(t, 200, app.DiskUsageInodes)
assert.Equal(t, true, app.IsPasswordSet)
assert.Contains(t, app.Flags, "test")
}
func TestAppsProcessorUpdateState(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("update_1224", 1002, 1003, "testimage", 2, 256)
assert.Nil(t, err)
err = processor.UpdateState("update_1224", "no-container", false)
assert.Nil(t, err)
app, err := processor.Get("update_1224")
assert.Nil(t, err)
assert.Equal(t, "no-container", app.State)
}
func TestAppsProcessorAddLabel(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("label_1224", 1002, 1003, "testimage", 2, 256)
assert.Nil(t, err)
err = processor.AddLabel("label_1224", "testlabel")
assert.Nil(t, err)
app, err := processor.Get("label_1224")
assert.Nil(t, err)
assert.Equal(t, "testlabel", app.Labels[0].Value)
}
func TestAppsProcessorRemoveLabel(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("label_1223", 1002, 1003, "testimage", 2, 256)
assert.Nil(t, err)
err = processor.AddLabel("label_1223", "testlabel")
assert.Nil(t, err)
app, err := processor.Get("label_1223")
assert.Nil(t, err)
assert.Equal(t, "testlabel", app.Labels[0].Value)
err = processor.RemoveLabel("label_1223", "testlabel")
assert.Nil(t, err)
app, err = processor.Get("label_1223")
assert.Nil(t, err)
assert.Equal(t, 0, len(app.Labels))
}
func TestAppsProcessorDelete(t *testing.T) {
processor := AppsProcessor{
DB: testDB(),
}
processor.Init()
err := processor.New("testapp_5234", 1002, 1003, "testimage", 2, 256)
assert.Nil(t, err)
app, err := processor.Get("testapp_5234")
assert.Nil(t, err)
assert.Equal(t, 256, app.Memory)
err = processor.Delete("testapp_5234")
assert.Nil(t, err)
_, err = processor.Get("testapp_5234")
assert.Error(t, err, "record not found")
}

View File

@ -1,33 +1,29 @@
package apps
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path"
"strings"
"time"
"github.com/mholt/archiver/v3"
"github.com/rosti-cz/node-api/apps/drivers"
uuid "github.com/satori/go.uuid"
)
const bucketName = "snapshots"
const dateFormat = "20060102_150405"
const keySplitCharacter = ":"
const metadataPrefix = "_metadata"
const metadataKeyTemplate = metadataPrefix + "/%s"
const tarBin = "/bin/tar"
// Snapshot contains metadata about a single snapshot
type Snapshot struct {
UUID string `json:"uuid"`
AppName string `json:"app_name"`
TimeStamp int64 `json:"ts"`
Labels []string `json:"labels"`
AppName string
TimeStamp int64
Labels []string
}
// SnapshotIndexLine is struct holding information about a single snapshot
@ -38,15 +34,31 @@ func (s *Snapshot) ToString() string {
}
// KeyName returns keyname used to store the snapshot in the storage
func (s *Snapshot) KeyName(indexLabel string) string {
labelValue := "-"
for _, label := range s.Labels {
if strings.HasPrefix(label, indexLabel+":") {
labelValue = label[len(indexLabel)+1:]
}
func (s *Snapshot) KeyName() string {
metadata := base64.StdEncoding.EncodeToString([]byte(s.ToString()))
// TODO: this can't be bigger than 1kB
return fmt.Sprintf("%s%s%d%s%s", s.AppName, keySplitCharacter, s.TimeStamp, keySplitCharacter, metadata)
}
// DecodeKeyName returns snapshot structure containing all metadata about a single snapshot
func DecodeKeyName(key string) (Snapshot, error) {
parts := strings.Split(key, keySplitCharacter)
if len(parts) != 3 {
return Snapshot{}, errors.New("key name in incorrect format")
}
return fmt.Sprintf("%s%s%s%s%s%s%d", s.AppName, keySplitCharacter, s.UUID, keySplitCharacter, labelValue, keySplitCharacter, s.TimeStamp)
_metadata, err := base64.StdEncoding.DecodeString(parts[2])
if len(parts) != 3 {
return Snapshot{}, fmt.Errorf("base64 decoding error: %v", err)
}
snapshot := Snapshot{}
err = json.Unmarshal(_metadata, &snapshot)
if err != nil {
return snapshot, fmt.Errorf("metadata unmarshal error: %v", err)
}
return snapshot, nil
}
type Snapshots []Snapshot
@ -54,104 +66,47 @@ type Snapshots []Snapshot
// SnapshotProcessor encapsulates everything realted to snapshots. Snapshot is an archive of app's
// directory content. It's stored in S3.
// The biggest problem in the implementation is speed of looking for snapshots by labels.
// So we can setup one label that can be used as index and it will speed up filtering a little bit in
// certain situations - all snapshot of one owner for example.
// This is distributed interface for the snapshot storage and any node can handle the request message
// so we don't have any locking mechanism here and we cannot created a single index of snapshots without
// so we don't have any locking mechanism here and we cannot created index of snapshots without
// significant time spend on it. Let's deal with it later. I think we are fine for first 10k snapshots.
type SnapshotProcessor struct {
AppsPath string // Where apps are stored
TmpSnapshotPath string // where temporary location for snapshots is
IndexLabel string // Label that will be used as index to make listing faster
Driver drivers.DriverInterface
}
// saveMetadata saves metadata of single snapshot into the metadata storage
func (s *SnapshotProcessor) saveMetadata(snapshot Snapshot) error {
body, err := json.Marshal(snapshot)
if err != nil {
return fmt.Errorf("marshal snapshot into JSON error: %v", err)
}
err = s.Driver.Write(fmt.Sprintf(metadataKeyTemplate, snapshot.UUID), body)
if err != nil {
return fmt.Errorf("copying metadata into S3 error: %v", err)
}
return nil
}
// loadMetadata returns metadata for given snapshot's UUID
func (s *SnapshotProcessor) loadMetadata(snapshotUUID string) (Snapshot, error) {
snapshot := Snapshot{}
body, err := s.Driver.Read(fmt.Sprintf(metadataKeyTemplate, snapshotUUID))
if err != nil {
return snapshot, fmt.Errorf("reading metadata from S3 error: %v", err)
}
err = json.Unmarshal(body, &snapshot)
if err != nil {
return snapshot, fmt.Errorf("decoding metadata from JSON error: %v", err)
}
return snapshot, nil
}
// deleteMetadata deletes metadata object
func (s *SnapshotProcessor) deleteMetadata(snapshotUUID string) error {
err := s.Driver.Delete(fmt.Sprintf(metadataKeyTemplate, snapshotUUID))
if err != nil {
return fmt.Errorf("delete metadata from S3 error: %v", err)
}
return nil
}
// metadataForSnapshotKey returns metadata for snapshot key
func (s *SnapshotProcessor) metadataForSnapshotKey(snapshotKey string) (Snapshot, error) {
parts := strings.Split(snapshotKey, keySplitCharacter)
if len(parts) != 4 {
return Snapshot{}, errors.New("wrong snapshot key format")
}
snapshot, err := s.loadMetadata(parts[1])
return snapshot, err
}
// CreateSnapshot creates an archive of existing application and stores it in S3 storage
// Returns key under which is the snapshot stored and/or error if there is any.
// Metadata about the snapshot are stored in extra object under metadata/ prefix.
// Metadata about the snapshot are encoded in the third part of the keyname.
// The keyname cannot be bigger than 1 kB.
func (s *SnapshotProcessor) CreateSnapshot(appName string, labels []string) (string, error) {
// Create an archive
archive := archiver.Zip{
CompressionLevel: 6,
MkdirAll: true,
SelectiveCompression: true,
ContinueOnError: false,
OverwriteExisting: false,
ImplicitTopLevelFolder: false,
}
snapshot := Snapshot{
UUID: uuid.NewV4().String(),
AppName: appName,
TimeStamp: time.Now().Unix(),
Labels: labels,
}
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, snapshot.KeyName(s.IndexLabel)+".tar.zst")
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, snapshot.KeyName()+".zip")
err := os.Chdir(path.Join(s.AppsPath, appName))
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("change working directory error: %v", err)
return snapshot.KeyName(), fmt.Errorf("change working directory error: %v", err)
}
err = exec.Command(tarBin, "-acf", tmpSnapshotArchivePath, "./").Run()
err = archive.Archive([]string{"./"}, tmpSnapshotArchivePath)
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("compression error: %v", err)
}
info, err := os.Stat(tmpSnapshotArchivePath)
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("temporary file stat error: %v", err)
}
snapshot.Labels = append(snapshot.Labels, fmt.Sprintf("size:%d", info.Size()))
err = s.saveMetadata(snapshot)
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("saving metadata error: %v", err)
return snapshot.KeyName(), fmt.Errorf("compression error: %v", err)
}
// Clean after myself
@ -163,19 +118,19 @@ func (s *SnapshotProcessor) CreateSnapshot(appName string, labels []string) (str
}()
// Pipe it into the storage
err = s.Driver.Create(snapshot.KeyName(s.IndexLabel), tmpSnapshotArchivePath)
err = s.Driver.Create(snapshot.KeyName(), tmpSnapshotArchivePath)
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("copying snapshot into S3 error: %v", err)
return snapshot.KeyName(), fmt.Errorf("copying snapshot into S3 error: %v", err)
}
return snapshot.KeyName(s.IndexLabel), nil
return snapshot.KeyName(), nil
}
// RestoreSnapshot restores snapshot into an existing application
// If you need a new app from existing snapshot just create it.
// This restores only content on the disk, doesn't create the container.
// This restored only content of the disk, doesn't create the container.
func (s *SnapshotProcessor) RestoreSnapshot(key string, newAppName string) error {
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, key+".tar.zst")
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, key+".zip")
err := os.MkdirAll(path.Join(s.AppsPath, newAppName), 0755)
if err != nil {
@ -187,12 +142,21 @@ func (s *SnapshotProcessor) RestoreSnapshot(key string, newAppName string) error
return fmt.Errorf("creating destination path error: %v", err)
}
err = s.Driver.Get(key, tmpSnapshotArchivePath)
s.Driver.Get(key, tmpSnapshotArchivePath)
if err != nil {
return fmt.Errorf("getting the archive from S3 error: %v", err)
}
err = exec.Command(tarBin, "-axf", tmpSnapshotArchivePath).Run()
archive := archiver.Zip{
CompressionLevel: 6,
MkdirAll: true,
SelectiveCompression: true,
ContinueOnError: false,
OverwriteExisting: false,
ImplicitTopLevelFolder: false,
}
err = archive.Unarchive(tmpSnapshotArchivePath, "./")
if err != nil {
return fmt.Errorf("unarchiving error: %v", err)
}
@ -202,12 +166,6 @@ func (s *SnapshotProcessor) RestoreSnapshot(key string, newAppName string) error
return fmt.Errorf("removing the archive error: %v", err)
}
// remove .chowned file to tell the container to setup ownership of the files again
err = os.Remove("./.chowned")
if err != nil && !strings.Contains(err.Error(), "no such file or directory") {
return fmt.Errorf("removing error: %v", err)
}
return nil
}
@ -221,12 +179,8 @@ func (s *SnapshotProcessor) ListAppSnapshots(appName string) ([]Snapshot, error)
}
for _, key := range keys {
if key == metadataPrefix+"/" {
continue
}
if strings.HasPrefix(key, appName+keySplitCharacter) {
snapshot, err := s.metadataForSnapshotKey(key)
snapshot, err := DecodeKeyName(key)
if err != nil {
return snapshots, err
}
@ -248,13 +202,9 @@ func (s *SnapshotProcessor) ListAppsSnapshots(appNames []string) ([]Snapshot, er
}
for _, key := range keys {
if key == metadataPrefix+"/" {
continue
}
for _, appName := range appNames {
if strings.HasPrefix(key, appName+keySplitCharacter) {
snapshot, err := s.metadataForSnapshotKey(key)
snapshot, err := DecodeKeyName(key)
if err != nil {
return snapshots, err
}
@ -267,9 +217,9 @@ func (s *SnapshotProcessor) ListAppsSnapshots(appNames []string) ([]Snapshot, er
return snapshots, nil
}
// ListAppsSnapshotsByLabel returns list of snapshots with given label
// ListAppsSnapshotsByLabels returns list of snapshots with given label
// TODO: this will be ok for now but probably little slow when users start using it more
func (s *SnapshotProcessor) ListAppsSnapshotsByLabel(labelValue string) ([]Snapshot, error) {
func (s *SnapshotProcessor) ListAppsSnapshotsByLabels(desiredLabel string) ([]Snapshot, error) {
snapshots := []Snapshot{}
keys, err := s.Driver.List("")
@ -278,20 +228,13 @@ func (s *SnapshotProcessor) ListAppsSnapshotsByLabel(labelValue string) ([]Snaps
}
for _, key := range keys {
if key == metadataPrefix+"/" {
continue
}
snapshot, err := s.metadataForSnapshotKey(key)
if err != nil && strings.Contains(err.Error(), "wrong snapshot key format") {
log.Printf("WARNING: Snapshot storage: invalid key found (%s)", key)
continue
} else if err != nil {
snapshot, err := DecodeKeyName(key)
if err != nil {
return snapshots, err
}
for _, label := range snapshot.Labels {
if label == fmt.Sprintf("%s:%s", s.IndexLabel, labelValue) {
if label == desiredLabel {
snapshots = append(snapshots, snapshot)
}
}
@ -300,28 +243,6 @@ func (s *SnapshotProcessor) ListAppsSnapshotsByLabel(labelValue string) ([]Snaps
return snapshots, nil
}
// GetSnapshot returns a single snapshot's metadata for given key.
func (s *SnapshotProcessor) GetSnapshot(key string) (Snapshot, error) {
snapshot := Snapshot{}
snapshot, err := s.metadataForSnapshotKey(key)
if err != nil {
return snapshot, err
}
return snapshot, nil
}
// GetDownloadLink returns an URL for given snapshot
func (s *SnapshotProcessor) GetDownloadLink(key string) (string, error) {
link, err := s.Driver.GetDownloadLink(key)
if err != nil {
return link, err
}
return link, nil
}
// DeleteSnapshot delete's one snapshot
func (s *SnapshotProcessor) DeleteSnapshot(key string) error {
err := s.Driver.Delete(key)
@ -340,7 +261,7 @@ func (s *SnapshotProcessor) DeleteAppSnapshots(appName string) error {
}
for _, snapshot := range snapshots {
err = s.DeleteSnapshot(snapshot.KeyName(s.IndexLabel))
err = s.DeleteSnapshot(snapshot.KeyName())
if err != nil {
return fmt.Errorf("removing snapshots error: %v", err)
}

View File

@ -47,7 +47,6 @@ func TestMain(m *testing.M) {
snapshotProcessor = &SnapshotProcessor{
AppsPath: path.Join(initialPwd, "tmp/apps"),
TmpSnapshotPath: path.Join(initialPwd, "tmp/snapshots"),
IndexLabel: "testlabel",
Driver: drivers.S3Driver{
S3AccessKey: "test",
@ -77,18 +76,14 @@ func TestMain(m *testing.M) {
func TestSnapshot(t *testing.T) {
snapshot := Snapshot{
UUID: "ABCDEF",
AppName: "app_0102",
TimeStamp: 1634510035,
Labels: []string{"userid:1"},
}
assert.Equal(t, "app_0102:ABCDEF:1:1634510035", snapshot.KeyName("userid"))
assert.Equal(t, "app_0102:1634510035:eyJBcHBOYW1lIjoiYXBwXzAxMDIiLCJUaW1lU3RhbXAiOjE2MzQ1MTAwMzUsIkxhYmVscyI6WyJ1c2VyaWQ6MSJdfQ==", snapshot.KeyName())
err := snapshotProcessor.saveMetadata(snapshot)
assert.Nil(t, err)
snapshot2, err := snapshotProcessor.metadataForSnapshotKey("app_0102:ABCDEF:1:1634510035")
snapshot2, err := DecodeKeyName("app_0102:1634510035:eyJBcHBOYW1lIjoiYXBwXzAxMDIiLCJUaW1lU3RhbXAiOjE2MzQ1MTAwMzUsIkxhYmVscyI6WyJ1c2VyaWQ6MSJdfQ==")
assert.Nil(t, err)
assert.Equal(t, "app_0102", snapshot2.AppName)
assert.Equal(t, int64(1634510035), snapshot2.TimeStamp)
@ -158,37 +153,3 @@ func TestCreateRestoreListSnapshot(t *testing.T) {
assert.Equal(t, os.IsNotExist(err), false)
}
func TestGetSnapshot(t *testing.T) {
snapshot, err := snapshotProcessor.GetSnapshot("app_0102:ABCDEF:1:1634510035")
assert.Nil(t, err)
assert.Equal(t, "app_0102", snapshot.AppName)
}
func TestListAppsSnapshotsByLabel(t *testing.T) {
appName := "app_0102"
// Create an app structure
err := os.MkdirAll(path.Join(snapshotProcessor.AppsPath, appName), 0755)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
_, err = exec.Command("bash", "-c", "echo content > "+path.Join(snapshotProcessor.AppsPath, appName)+"/a_file.txt").CombinedOutput()
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
// Create an snapshot
snapshotName, err := snapshotProcessor.CreateSnapshot(appName, []string{"app:test2", "almost_no_content", "testlabel:abcde"})
assert.Nil(t, err)
assert.Equal(t, strings.HasPrefix(snapshotName, appName+":"), true)
snapshots, err := snapshotProcessor.ListAppsSnapshotsByLabel("abcde")
assert.Nil(t, err)
assert.True(t, len(snapshots) > 0)
assert.Equal(t, appName, snapshots[0].AppName)
}

View File

@ -5,8 +5,8 @@ import (
"strings"
"time"
"github.com/rosti-cz/node-api/detector"
"github.com/rosti-cz/node-api/jsonmap"
// This is line from GORM documentation that imports database dialect
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
// ValidationError is error that holds multiple validation error messages
@ -31,14 +31,11 @@ type Label struct {
// AppState contains info about runnint application, it's not saved in the database
type AppState struct {
State string `json:"state"`
OOMKilled bool `json:"oom_killed"`
CPUUsage float64 `json:"cpu_usage"` // in percents
MemoryUsage int `json:"memory_usage"` // in MB
DiskUsageBytes int `json:"disk_usage_bytes"`
DiskUsageInodes int `json:"disk_usage_inodes"`
Flags detector.Flags `json:"flags"`
IsPasswordSet bool `json:"is_password_set"`
State string `json:"state"`
CPUUsage float64 `json:"cpu_usage"` // in percents
MemoryUsage int `json:"memory_usage"` // in MB
DiskUsageBytes int `json:"disk_usage_bytes"`
DiskUsageInodes int `json:"disk_usage_inodes"`
}
// Apps is list of applications
@ -56,8 +53,6 @@ type App struct {
// Datetime of deletion
DeletedAt *time.Time `sql:"index" json:"deleted_at"`
// ####################################################
// This part is used in app template
// Name of the application
// Example: test_1234
Name string `json:"name" gorm:"unique,index,not_null"`
@ -69,9 +64,6 @@ type App struct {
// Unique: true
// Example: 10002
HTTPPort int `json:"http_port"`
// Port of the application inside the container. Default is 0 which means default by the image.
// But it has to be between 1024 and 65536 with exception of 8000.
AppPort int `json:"app_port"`
// Runtime image
Image string `json:"image"`
// Number of CPUs ticks assigned, 100 means one CPU, 200 are two
@ -80,12 +72,9 @@ type App struct {
Memory int `json:"memory"` // Limit in MB
// Custom labels
Labels []Label `json:"labels" gorm:"foreignkey:AppID"` // username:cx or user_id:1
// ####################################################
// Current status of the application (underlaying container)
State string `json:"state"`
// True if the current container has been killed by OOM killer
OOMKilled bool `json:"oom_killed"`
// CPU usage in percents
CPUUsage float64 `json:"cpu_usage"` // in percents
// Memory usage in bytes
@ -94,44 +83,6 @@ type App struct {
DiskUsageBytes int `json:"disk_usage_bytes"`
// Disk usage in inodes
DiskUsageInodes int `json:"disk_usage_inodes"`
// Flags from detector of problems in the container
Flags string `json:"flags"` // flags are separated by comma
IsPasswordSet bool `json:"is_password_set"` // True if the password is set in the container (file with the password exists)
// this is gathered in docker package and has to be assembled externally
Techs AppTechs `json:"techs,omitempty" gorm:"-"` // list of available technologies in the image
PrimaryTech AppTech `json:"primary_tech,omitempty" gorm:"-"` // Technology that was selected as primary in the environment
// This is not store in the database but used in create request when the app suppose to be created from an existing snapshot
Snapshot string `json:"snapshot" gorm:"-"`
EnvRaw jsonmap.JSONMap `json:"env" sql:"type:json"`
// Fields to setup during creating of the app, this is not stored in the database
Setup struct {
SSHKeys string `json:"ssh_keys"`
Tech string `json:"tech"`
TechVersion string `json:"tech_version"`
Password string `json:"password"`
ArchiveURL string `json:"archive_url"` // Archive with content of /srv
} `json:"setup,omitempty" gorm:"-"`
}
// Return env as map[string]string
func (a *App) GetEnv() map[string]string {
env := make(map[string]string)
for key, value := range a.EnvRaw {
env[key] = value.(string)
}
return env
}
// SetEnv sets env from map[string]string
func (a *App) SetEnv(env map[string]string) {
a.EnvRaw = make(jsonmap.JSONMap)
for key, value := range env {
a.EnvRaw[key] = value
}
}
// Validate do basic checks of the struct values
@ -151,10 +102,6 @@ func (a *App) Validate() []string {
errors = append(errors, "HTTP port has to be between 1 and 65536")
}
if a.AppPort != 0 && ((a.AppPort < 1024 && a.AppPort > 65536) || a.AppPort == 8000) {
errors = append(errors, "App port has to be between 1024 and 65536 with exception of 8000")
}
if a.Image == "" {
errors = append(errors, "image cannot be empty")
}
@ -169,12 +116,3 @@ func (a *App) Validate() []string {
return errors
}
// AppTechs is list of technologies available in the app
type AppTechs []AppTech
// AppTech holds info about one technology in the app
type AppTech struct {
Name string `json:"name"`
Version string `json:"version"`
}

View File

@ -3,7 +3,7 @@ package main
import (
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo"
)
var skipPaths []string = []string{"/metrics"}

View File

@ -8,7 +8,6 @@ import (
// Config keeps info about configuration of this daemon
type Config struct {
DockerSocket string `envconfig:"DOCKER_SOCKET" default:"unix:///var/run/docker.sock"`
Token string `envconfig:"TOKEN" required:"true"`
AppsPath string `envconfig:"APPS_PATH" default:"/srv"` // Where applications are located
AppsBindIPHTTP string `envconfig:"APPS_BIND_IP_HTTP" default:"0.0.0.0"` // On what IP apps' HTTP port gonna be bound
@ -22,9 +21,6 @@ type Config struct {
SnapshotsS3Endpoint string `envconfig:"SNAPSHOTS_S3_ENDPOINT" required:"false"`
SnapshotsS3SSL bool `envconfig:"SNAPSHOTS_S3_SSL" required:"false" default:"true"`
SnapshotsS3Bucket string `envconfig:"SNAPSHOTS_S3_BUCKET" required:"false" default:"snapshots"`
SnapshotsIndexLabel string `envconfig:"SNAPSHOTS_INDEX_LABEL" required:"false" default:"owner_id"` // Label that will be part of the object name and it will be used as index to quick listing
SentryDSN string `envconfig:"SENTRY_DSN" required:"false"`
SentryENV string `envconfig:"SENTRY_ENV" default:"development"`
}
// GetConfig return configuration created based on environment variables

View File

@ -1,44 +0,0 @@
package containers
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func getTestDockerSock() string {
dockerSocket := os.Getenv("DOCKER_SOCKET")
if dockerSocket == "" {
return "unix:///run/user/1000/podman/podman.sock"
}
return dockerSocket
}
func TestGetProcesses(t *testing.T) {
driver := Driver{
DockerSock: getTestDockerSock(),
BindIPHTTP: "127.0.0.1",
BindIPSSH: "127.0.0.1",
}
driver.Remove("test")
env := make(map[string]string)
env["TEST"] = "test"
_, err := driver.Create("test", "docker.io/library/busybox", "/tmp", 8990, 8922, 1, 128, []string{"sleep", "3600"}, env)
assert.Nil(t, err)
err = driver.Start("test")
assert.Nil(t, err)
time.Sleep(5 * time.Second)
processes, err := driver.GetProcesses("test")
assert.Nil(t, err)
assert.Contains(t, processes, "sleep 3600")
driver.Remove("test")
}

View File

@ -1,19 +0,0 @@
package containers
// ContainerStats contains fields returned in docker stats function stream
type ContainerStats struct {
Pids struct {
Current int `json:"current"`
} `json:"pids_stats"`
CPU struct {
Usage struct {
Total int64 `json:"total_usage"`
} `json:"cpu_usage"`
} `json:"cpu_stats"`
Memory struct {
Usage int `json:"usage"`
MaxUsage int `json:"max_usage"`
Limit int `json:"limit"`
} `json:"memory_stats"`
ID string `json:"id"`
}

View File

@ -1,640 +0,0 @@
package containers
import (
"bytes"
"errors"
"fmt"
"log"
"os"
"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 passwordFileNoPath = ".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
}
isPasswordSet, err := c.IsPasswordSet()
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,
IsPasswordSet: isPasswordSet,
}
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())
}
// IsPasswordSet returns true if the password is set for the container (file with the password exists)
func (c *Container) IsPasswordSet() (bool, error) {
_, err := os.Stat(path.Join(c.VolumeHostPath(), passwordFileNoPath))
if err != nil && os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// 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
}
// ClearPassword removes password for system user app in the container
func (c *Container) ClearPassword() error {
driver := c.getDriver()
_, err := driver.Exec(c.App.Name, []string{"passwd", "-d", "app"}, "", []string{}, false)
if err != nil {
return err
}
_, err = driver.Exec(c.App.Name, []string{"rm", "-f", passwordFile}, "", []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 %s/%s for %s output: %s", tech, version, 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]
if rawTech == "default" {
return apps.AppTech{
Name: "default",
Version: "",
}, nil
}
techParts := strings.Split(rawTech, "-")
if len(techParts) != 2 {
return tech, errors.New("wrong number of tech parts (" + rawTech + ")")
}
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
}

View File

@ -1,39 +0,0 @@
package containers
import (
"os"
"path/filepath"
"testing"
)
func TestGetTechAndVersion(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
// Create a fake language directory with version in the temporary directory
fakeLangDir := filepath.Join(tempDir, "techs", "python-3.10.4", "bin")
err := os.MkdirAll(fakeLangDir, 0755)
if err != nil {
t.Fatalf("Failed to create fake language directory: %v", err)
}
// Create a symlink for testing
symlink := filepath.Join(tempDir, "primary_tech")
err = os.Symlink(fakeLangDir, symlink)
if err != nil {
t.Fatalf("Failed to create symlink: %v", err)
}
// Test parseLanguageAndVersion function
info, err := getTechAndVersion(symlink)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expectedLanguage := "python"
expectedVersion := "3.10.4"
if info.Tech != expectedLanguage || info.Version != expectedVersion {
t.Errorf("Expected language: %s, version: %s, but got language: %s, version: %s",
expectedLanguage, expectedVersion, info.Tech, info.Version)
}
}

View File

@ -3,5 +3,4 @@
nats pub --count 1 admin.apps.node-x.requests -w '{"type": "list"}'
nats pub --count 1 admin.apps.node-x.requests -w '{"type": "get", "app_name": "test_1234"}'
nats pub --count 1 admin.apps.node-local.requests -w '{"type": "create", "name": "testrunnats_5372", "payload": "{\"name\": \"testrunnats_5372\", \"image\": \"docker.io/rosti/runtime:2022.01-1\", \"cpu\": 50, \"memory\": 512, \"ssh_port\": 25372, \"http_port\": 35372}"}'
nats pub --count 1 admin.apps.node-local.requests -w '{"type": "create", "name": "testrunnats_5372", "payload": "{\"name\": \"testrunnats_5372\", \"image\": \"harbor.hq.rosti.cz/public/runtime:2022.01-1\", \"cpu\": 50, \"memory\": 512, \"ssh_port\": 25372, \"http_port\": 35372}"}'
nats pub --count 1 admin.apps.node-x.requests -w '{"type": "create", "app_name": "natstest_0004", "payload": {"name": "natstest_0004", "image": "docker.io/rosti/runtime:2020.09-1", "cpu": 50, "memory": 256, "ssh_port": 20004, "http_port": 30004}}'

View File

@ -1,40 +0,0 @@
package detector
import (
"regexp"
"strings"
)
// Flags is list of strings describing problems found among the processes
type Flags []string
func (f *Flags) String() string {
return strings.Join(*f, ",")
}
// Check goes over patterns and tries to flag given list of processes with flags.
func Check(processes []string) (Flags, error) {
flags := Flags{}
tmpFlags := make(map[string]bool)
for _, process := range processes {
for flag, patternSet := range patterns {
for _, pattern := range patternSet {
matched, err := regexp.MatchString(".*"+pattern+".*", process)
if err != nil {
return flags, err
}
if matched {
tmpFlags[flag] = true
break
}
}
}
}
for flag, _ := range tmpFlags {
flags = append(flags, flag)
}
return flags, nil
}

View File

@ -1,35 +0,0 @@
package detector
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCheck(t *testing.T) {
flags, err := Check([]string{
"sleep",
"apache2",
"miner",
"verus-solve",
})
assert.Nil(t, err)
assert.Contains(t, flags, "miner")
flags, err = Check([]string{
"sleep",
"apache2",
"hellminer",
})
assert.Nil(t, err)
assert.Contains(t, flags, "miner")
flags, err = Check([]string{
"sleep",
"apache2",
"miner", // This is not among patterns map
})
assert.Nil(t, err)
assert.NotContains(t, flags, "miner")
}

View File

@ -1,12 +0,0 @@
package detector
var patterns map[string][]string = map[string][]string{
"miner": {
`verus\-solve`,
`hellminer`,
},
"bot": {
`youtube\-dl`,
`shiziyama`,
},
}

View File

@ -1,10 +1,9 @@
package containers
package docker
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
@ -18,8 +17,6 @@ import (
"github.com/docker/docker/api/types/network"
dockerClient "github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/rosti-cz/node-api/detector"
)
// Stats delay in seconds
@ -28,22 +25,29 @@ const statsDelay = 1
// Docker timeout
const dockerTimeout = 10
// DOCKER_API_VERSION set API version of Docker, 1.41 is needed for the platform struct
const dockerAPIVersion = "1.41"
// DOCKER_SOCK tells where to connect to docker, it will be always local sock
const dockerSock = "/var/run/docker.sock"
const podmanSock = "/run/podman/podman.sock"
// DOCKER_API_VERSION set API version of Docker, 1.40 belongs to Docker 19.03.11
const dockerAPIVersion = "1.38"
// Driver keeps everything for connection to Docker
type Driver struct {
DockerSock string // Docker socket
BindIPHTTP string // IP to which containers are bound
BindIPSSH string // IP to which containers are bound
}
func (d *Driver) getClient() (*dockerClient.Client, error) {
cli, err := dockerClient.NewClient(d.DockerSock, dockerAPIVersion, nil, nil)
if err != nil {
return cli, fmt.Errorf("get docker client error: %v", err)
var connectTo string
if _, err := os.Stat(podmanSock); !os.IsNotExist(err) {
connectTo = podmanSock
} else {
connectTo = dockerSock
}
return cli, nil
cli, err := dockerClient.NewClient("unix://"+connectTo, dockerAPIVersion, nil, nil)
return cli, err
}
// ConnectionStatus checks connection to the Docker daemon
@ -79,10 +83,8 @@ func (d *Driver) nameToID(name string) (string, error) {
}
// Status return current status of container with given name
func (d *Driver) Status(name string) (ContainerStatus, error) {
status := ContainerStatus{
Status: "unknown",
}
func (d *Driver) Status(name string) (string, error) {
status := "unknown"
cli, err := d.getClient()
if err != nil {
@ -92,8 +94,7 @@ func (d *Driver) Status(name string) (ContainerStatus, error) {
containerID, err := d.nameToID(name)
if err != nil && err.Error() == "no container found" {
status.Status = "no-container"
return status, err
return "no-container", err
}
if err != nil {
return status, err
@ -104,10 +105,7 @@ func (d *Driver) Status(name string) (ContainerStatus, error) {
return status, err
}
status.Status = info.State.Status
status.OOMKilled = info.State.OOMKilled
return status, nil
return info.State.Status, nil
}
@ -202,13 +200,13 @@ func (d *Driver) Remove(name string) error {
return err
}
timeout := dockerTimeout
err = cli.ContainerStop(context.TODO(), containerID, container.StopOptions{Timeout: &timeout})
timeout := time.Duration(dockerTimeout * time.Second)
err = cli.ContainerStop(context.TODO(), containerID, &timeout)
if err != nil {
return err
}
err = cli.ContainerRemove(context.TODO(), containerID, container.RemoveOptions{})
err = cli.ContainerRemove(context.TODO(), containerID, types.ContainerRemoveOptions{})
return err
}
@ -246,8 +244,8 @@ func (d *Driver) Stop(name string) error {
return err
}
timeout := dockerTimeout
err = cli.ContainerStop(context.TODO(), containerID, container.StopOptions{Timeout: &timeout})
timeout := time.Duration(dockerTimeout * time.Second)
err = cli.ContainerStop(context.TODO(), containerID, &timeout)
return err
}
@ -307,7 +305,7 @@ func (d *Driver) pullImage(image string) error {
// cmd - string slice of command and its arguments
// volumePath - host's directory to mount into the container
// returns container ID
func (d *Driver) Create(name string, image string, volumePath string, HTTPPort int, SSHPort int, CPU int, memory int, cmd []string, env map[string]string) (string, error) {
func (d *Driver) Create(name string, image string, volumePath string, HTTPPort int, SSHPort int, CPU int, memory int, cmd []string) (string, error) {
log.Println("Creating container " + name)
cli, err := d.getClient()
if err != nil {
@ -338,21 +336,11 @@ func (d *Driver) Create(name string, image string, volumePath string, HTTPPort i
}
}
OOMKillDisable := false
if memory < 1500 {
OOMKillDisable = true
}
envList := []string{}
for key, value := range env {
envList = append(envList, key+"="+value)
}
createdContainer, err := cli.ContainerCreate(
context.Background(),
&container.Config{
Hostname: name,
Env: envList,
Env: []string{},
Image: image,
Cmd: cmd,
ExposedPorts: nat.PortSet{
@ -366,7 +354,6 @@ func (d *Driver) Create(name string, image string, volumePath string, HTTPPort i
CPUQuota: int64(CPU) * 1000,
Memory: int64(memory*110/100) * 1024 * 1024, // Allow 10 % more memory so we have space for MemoryReservation
MemoryReservation: int64(memory) * 1024 * 1024, // This should provide softer way how to limit the memory of our containers
OomKillDisable: &OOMKillDisable,
},
PortBindings: portBindings,
AutoRemove: false,
@ -379,10 +366,6 @@ func (d *Driver) Create(name string, image string, volumePath string, HTTPPort i
},
},
&network.NetworkingConfig{},
&specs.Platform{
Architecture: "amd64",
OS: "linux",
},
name,
)
if err != nil {
@ -435,9 +418,11 @@ func (d *Driver) Exec(name string, cmd []string, stdin string, env []string, att
return &[]byte{}, err
}
respAttach, err := cli.ContainerExecAttach(ctx, resp.ID, types.ExecStartCheck{
Detach: false,
Tty: true,
respAttach, err := cli.ContainerExecAttach(ctx, resp.ID, types.ExecConfig{
AttachStdin: stdinEnabled,
AttachStdout: attachStdout,
AttachStderr: false,
Tty: attachStdout,
})
if err != nil {
return &[]byte{}, err
@ -463,44 +448,3 @@ func (d *Driver) Exec(name string, cmd []string, stdin string, env []string, att
return &stdouterr, err
}
// GetProcesses return list of processes running under this container
func (d *Driver) GetProcesses(name string) ([]string, error) {
processes := []string{}
ctx := context.Background()
cli, err := d.getClient()
if err != nil {
return processes, err
}
defer cli.Close()
processList, err := cli.ContainerTop(ctx, name, []string{"-eo", "pid,args"})
if err != nil {
return processes, fmt.Errorf("docker container top call error: %v", err)
}
for _, process := range processList.Processes {
if len(process) > 0 {
// This removes PID from the list. PID has to be printed otherwise docker daemon can't handle it.
processes = append(processes, strings.Join(strings.Fields(process[0])[1:], " "))
}
}
return processes, nil
}
// GetFlags returns list of flags with problems found in the container, mainly used to detect miners or viruses
func (d *Driver) GetFlags(name string) (detector.Flags, error) {
processes, err := d.GetProcesses(name)
if err != nil {
return detector.Flags{}, err
}
flags, err := detector.Check(processes)
if err != nil {
return flags, err
}
return flags, nil
}

19
docker/stats.go Normal file
View File

@ -0,0 +1,19 @@
package docker
// ContainerStats contains fields returned in docker stats function stream
type ContainerStats struct {
Pids struct {
Current int `json:"current"`
} `json:"pids_stats"`
CPU struct {
Usage struct {
Total int64 `json:"total_usage"`
} `json:"cpu_usage"`
} `json:"cpu_stats"`
Memory struct {
Usage int `json:"usage"`
MaxUsage int `json:"max_usage"`
Limit int `json:"limit"`
} `json:"memory_stats"`
ID string `json:"id"`
}

View File

@ -1,13 +1,11 @@
package containers
package docker
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
@ -64,7 +62,7 @@ func du(path string) (int, int, error) {
return space, inodes, nil
}
// Removes content of given directory and then the directory itself
// Removes content of given directory
func removeDirectory(dir string) error {
d, err := os.Open(dir)
if err != nil {
@ -81,12 +79,6 @@ func removeDirectory(dir string) error {
return err
}
}
err = os.Remove(filepath.Join(dir))
if err != nil {
return err
}
return nil
}
@ -137,53 +129,3 @@ func CPUMemoryStats(applist *[]apps.App, sample int) (*[]apps.App, error) {
return &updatedApps, nil
}
func getTechAndVersion(symlink string) (*TechInfo, error) {
link, err := os.Readlink(symlink)
if os.IsNotExist(err) {
return &TechInfo{
Tech: "default",
Version: "",
}, nil
}
if err != nil {
return nil, fmt.Errorf("error reading symlink: %w", err)
}
absLink, err := filepath.Abs(link)
if err != nil {
return nil, fmt.Errorf("error getting absolute path: %w", err)
}
absLink = strings.TrimSuffix(absLink, "/bin")
dirName := filepath.Base(absLink)
parts := strings.Split(dirName, "-")
fmt.Println("DEBUG", symlink)
fmt.Println("DEBUG", absLink)
fmt.Println("DEBUG", dirName)
fmt.Println("DEBUG", parts)
if len(parts) < 2 {
return &TechInfo{
Tech: "default",
Version: "",
}, nil
}
re := regexp.MustCompile(`\d+\.\d+\.\d+`)
version := re.FindString(parts[1])
if version == "" {
// In case version couldn't be determined we return "unknown", otherwise returning
// error in this case was crashing admin when user fucked up the symlink for the
// tech manually.
log.Println("failed to extract version from symlink")
return &TechInfo{
Tech: "unknown",
Version: "",
}, nil
}
return &TechInfo{
Tech: parts[0],
Version: version,
}, nil
}

279
docker/types.go Normal file
View File

@ -0,0 +1,279 @@
package docker
import (
"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.
func (c *Container) SetTechnology(tech string) error {
driver := c.getDriver()
_, err := driver.Exec(c.App.Name, []string{"su", "app", "-c", "rosti " + tech}, "", []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
}

View File

@ -1,975 +0,0 @@
package glue
import (
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path"
"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,
}
isPasswordSet, err := container.IsPasswordSet()
if err != nil {
return app, err
}
app.IsPasswordSet = isPasswordSet
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
}
// Takes URL with an tar archive and prepares container's volume from it.
func (p *Processor) volumeFromURL(url string, container *docker.Container) error {
// Validation, check if url ends with tar.zst
if !strings.HasSuffix(url, ".tar.zst") {
return fmt.Errorf("archive has to end with .tar.zst")
}
volumePath := container.VolumeHostPath()
// Prepare volume path
err := os.MkdirAll(volumePath, 0755)
if err != nil {
return fmt.Errorf("failed to create volume path: %v", err)
}
// Download the archive
archivePath := path.Join(volumePath, "archive.tar.zst")
log.Printf("%s: downloading archive from %s\n", container.App.Name, url)
f, err := os.Create(archivePath)
if err != nil {
return fmt.Errorf("failed to create archive file: %v", err)
}
defer f.Close()
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to download archive: %v", err)
}
defer resp.Body.Close()
n, err := io.Copy(f, resp.Body)
if err != nil {
return fmt.Errorf("failed to download archive: %v", err)
}
log.Printf("downloaded %d bytes\n", n)
// Extract the archive
log.Printf("%s: extracting archive\n", container.App.Name)
// Call tar xf archive.tar.zst -C /volume
cmd := exec.Command("tar", "-xf", archivePath, "-C", volumePath)
err = cmd.Run()
if err != nil {
log.Printf("%s: failed to extract archive: %v", container.App.Name, err)
return err
}
// Remove archive
log.Printf("%s: removing archive\n", container.App.Name)
err = os.Remove(volumePath + "/archive.tar.zst")
if err != nil {
return fmt.Errorf("failed to remove archive: %v", err)
}
log.Printf("%s: volume preparing done\n", container.App.Name)
return nil
}
// Create creates a single app in the system
func (p *Processor) Create(appTemplate apps.App) error {
if appTemplate.EnvRaw == nil {
appTemplate.EnvRaw = make(map[string]interface{})
}
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,
}
if len(appTemplate.Snapshot) > 0 && len(appTemplate.Setup.ArchiveURL) > 0 {
return fmt.Errorf("snapshot and archive_url cannot be used together")
}
if len(appTemplate.Setup.ArchiveURL) > 0 {
err = p.volumeFromURL(appTemplate.Setup.ArchiveURL, &container)
if err != nil {
return fmt.Errorf("failed to prepare volume: %v", err)
}
}
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 {
if appTemplate.EnvRaw == nil {
appTemplate.EnvRaw = make(map[string]interface{})
}
processor := p.getAppProcessor()
app, err := processor.Update(appTemplate.Name, appTemplate.SSHPort, appTemplate.HTTPPort, appTemplate.Image, appTemplate.CPU, appTemplate.Memory, appTemplate.GetEnv())
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
}
// Setup technology if it's noted in the request
if len(appTemplate.Setup.Tech) > 0 {
err := p.waitForApp()
if err != nil {
return err
}
err = p.EnableTech(appTemplate.Setup.Tech, appTemplate.Setup.TechVersion)
if err != nil {
return fmt.Errorf("failed to enable tech: %v", err)
}
// We restart the container so everything can use the new tech
err = container.Restart()
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())
return err
}
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
}
// ClearPassword removes password from the SSH user
func (p *Processor) ClearPassword() error {
err := p.waitForApp()
if err != nil {
return err
}
container, err := p.getContainer()
if err != nil {
return err
}
err = container.ClearPassword()
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
}
// Save meta data about app into a file
func (p *Processor) SaveMetadata(metadata string) error {
container, err := p.getContainer()
if err != nil {
return err
}
volumePath := container.VolumeHostPath()
f, err := os.Create(path.Join(volumePath, ".metadata.json"))
if err != nil {
return err
}
defer f.Close()
_, err = f.Write([]byte(metadata))
if err != nil {
return err
}
// Set permissions
err = os.Chmod(path.Join(volumePath, ".metadata.json"), 0600)
if err != nil {
return err
}
// Set owner
err = os.Chown(path.Join(volumePath, ".metadata.json"), ownerUID, ownerGID)
if err != nil {
return err
}
return 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
}

View File

@ -1,244 +0,0 @@
package glue
import (
"log"
"os"
"path"
"testing"
"github.com/jinzhu/gorm"
"github.com/rosti-cz/node-api/apps"
"github.com/stretchr/testify/assert"
// This is line from GORM documentation that imports database dialect
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
const testDBPath = "file::memory:?cache=shared"
var testAppTemplate apps.App = apps.App{
Name: "test_1234",
SSHPort: 10000,
HTTPPort: 10001,
Image: "harbor.hq.rosti.cz/public/runtime:2022.01-1",
CPU: 50,
Memory: 256,
}
func getPWD() string {
dir, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
return dir
}
func testDB() *gorm.DB {
db, err := gorm.Open("sqlite3", testDBPath)
if err != nil {
log.Fatalln(err)
}
return db
}
func getTestDockerSock() string {
dockerSocket := os.Getenv("DOCKER_SOCKET")
if dockerSocket == "" {
return "unix:///run/user/1000/podman/podman.sock"
}
return dockerSocket
}
var processor Processor
func TestMain(m *testing.M) {
// This processor is used to test all methods
processor = Processor{
AppName: testAppTemplate.Name,
DB: testDB(),
// SnapshotProcessor *apps.SnapshotProcessor
DockerSock: getTestDockerSock(),
BindIPHTTP: "127.0.0.1",
BindIPSSH: "127.0.0.1",
AppsPath: path.Join(getPWD(), "tmp/apps"),
}
exitVal := m.Run()
os.Exit(exitVal)
}
func TestProcessorCreate(t *testing.T) {
err := processor.Create(testAppTemplate)
assert.Nil(t, err)
processor.Delete()
assert.Nil(t, err)
}
// func TestProcessorList(t *testing.T) {
// }
func TestProcessorGet(t *testing.T) {
err := processor.Create(testAppTemplate)
assert.Nil(t, err)
app, err := processor.Get(false)
assert.Nil(t, err)
assert.Equal(t, "running", app.State)
processor.Delete()
assert.Nil(t, err)
}
func TestProcessorRegister(t *testing.T) {
err := processor.Register(testAppTemplate)
assert.Nil(t, err)
app, err := processor.Get(false)
assert.Nil(t, err)
assert.Equal(t, "no-container", app.State)
processor.Delete()
assert.Nil(t, err)
}
func TestProcessorUpdate(t *testing.T) {
err := processor.Create(testAppTemplate)
assert.Nil(t, err)
app, err := processor.Get(false)
assert.Nil(t, err)
assert.Equal(t, "running", app.State)
updatedTemplate := testAppTemplate
updatedTemplate.Memory = 1024
err = processor.Update(updatedTemplate)
assert.Nil(t, err)
assert.Equal(t, "running", app.State)
app, err = processor.Get(false)
assert.Nil(t, err)
assert.Equal(t, 1024, app.Memory)
processor.Delete()
assert.Nil(t, err)
}
func TestProcessorDelete(t *testing.T) {
err := processor.Create(testAppTemplate)
assert.Nil(t, err)
processor.Delete()
assert.Nil(t, err)
}
func TestProcessorStop(t *testing.T) {
err := processor.Create(testAppTemplate)
assert.Nil(t, err)
err = processor.Stop()
assert.Nil(t, err)
app, err := processor.Get(false)
assert.Nil(t, err)
assert.Equal(t, "exited", app.State)
processor.Delete()
assert.Nil(t, err)
}
func TestProcessorStart(t *testing.T) {
err := processor.Create(testAppTemplate)
assert.Nil(t, err)
err = processor.Stop()
assert.Nil(t, err)
app, err := processor.Get(false)
assert.Nil(t, err)
assert.Equal(t, "exited", app.State)
err = processor.Start()
assert.Nil(t, err)
app, err = processor.Get(false)
assert.Nil(t, err)
assert.Equal(t, "running", app.State)
processor.Delete()
assert.Nil(t, err)
}
// func TestProcessorRestart(t *testing.T) {
// }
// func TestProcessorUpdateKeys(t *testing.T) {
// }
// func TestProcessorSetPassword(t *testing.T) {
// }
// func TestProcessorProcesses(t *testing.T) {
// }
// func TestProcessorEnableTech(t *testing.T) {
// }
// func TestProcessorRebuild(t *testing.T) {
// }
// func TestProcessorAddLabel(t *testing.T) {
// }
// func TestProcessorRemoveLabel(t *testing.T) {
// }
// func TestProcessorGetNode(t *testing.T) {
// }
// func TestProcessorCreateSnapshot(t *testing.T) {
// }
// func TestProcessorRestoreFromSnapshot(t *testing.T) {
// }
// func TestProcessorListSnapshots(t *testing.T) {
// }
// func TestProcessorListAppsSnapshots(t *testing.T) {
// }
// func TestProcessorListSnapshotsByLabel(t *testing.T) {
// }
// func TestProcessorGetSnapshot(t *testing.T) {
// }
// func TestProcessorGetSnapshotDownloadLink(t *testing.T) {
// }
// func TestProcessorDeleteSnapshot(t *testing.T) {
// }
// func TestProcessorDeleteAppSnapshots(t *testing.T) {
// }

View File

@ -1,132 +0,0 @@
package glue
import (
"log"
"github.com/getsentry/sentry-go"
"github.com/jinzhu/gorm"
"github.com/rosti-cz/node-api/apps"
docker "github.com/rosti-cz/node-api/containers"
)
// StatsProcessor covers all methods that are needed
// to gather information about application containers.
type StatsProcessor struct {
DB *gorm.DB
DockerSock string
BindIPHTTP string
BindIPSSH string
AppsPath string
}
// returns instance of getAppProcessor
func (s *StatsProcessor) getAppProcessor() apps.AppsProcessor {
processor := apps.AppsProcessor{
DB: s.DB,
}
processor.Init()
return processor
}
// updateUsage updates various resource usage of the container/app in the database
func (s *StatsProcessor) UpdateUsage(name string) error {
processor := s.getAppProcessor()
app, err := processor.Get(name)
if err != nil {
return err
}
container := docker.Container{
App: &app,
DockerSock: s.DockerSock,
BindIPHTTP: s.BindIPHTTP,
BindIPSSH: s.BindIPSSH,
AppsPath: s.AppsPath,
}
state, err := container.GetState()
if err != nil {
return err
}
err = processor.UpdateResources(
name,
state.State,
state.OOMKilled,
state.CPUUsage,
state.MemoryUsage,
state.DiskUsageBytes,
state.DiskUsageInodes,
state.Flags,
state.IsPasswordSet,
)
return err
}
// Updates only container's state. Check current status of the container and saves it into the database.
func (s *StatsProcessor) UpdateState(name string) error {
processor := s.getAppProcessor()
app, err := processor.Get(name)
if err != nil {
return err
}
container := docker.Container{
App: &app,
DockerSock: s.DockerSock,
BindIPHTTP: s.BindIPHTTP,
BindIPSSH: s.BindIPSSH,
AppsPath: s.AppsPath,
}
state, err := container.Status()
if err != nil {
return err
}
err = processor.UpdateState(
app.Name,
state.Status,
state.OOMKilled,
)
return err
}
// gatherStats loops over all applications and calls updateUsage to write various metric into the database.
func (s *StatsProcessor) GatherStats() error {
processor := s.getAppProcessor()
appList, err := processor.List()
if err != nil {
return err
}
for _, app := range appList {
err := s.UpdateUsage(app.Name)
if err != nil {
sentry.CaptureException(err)
log.Println("STATS ERROR:", err.Error())
}
}
return nil
}
// gatherStates loops over all apps and updates their container state
func (s *StatsProcessor) GatherStates() error {
processor := s.getAppProcessor()
appList, err := processor.List()
if err != nil {
return err
}
for _, app := range appList {
err := s.UpdateState(app.Name)
if err != nil {
sentry.CaptureException(err)
log.Println("STATE ERROR:", err.Error())
}
}
return nil
}

View File

@ -1,18 +0,0 @@
package glue
import "github.com/rosti-cz/node-api/apps"
const ownerUID = 1000
const ownerGID = 1000
// Path where authorized keys are
const sshPubKeysLocation = "/srv/.ssh/authorized_keys"
// SnapshotMetadata is snapshot structure encapsulation that combines key and metadata about the snapshot
type SnapshotMetadata struct {
Key string `json:"key"`
Metadata apps.Snapshot `json:"metadata"`
}
// SnapshotsMetadata is returned by handlers
type SnapshotsMetadata []SnapshotMetadata

35
go.mod
View File

@ -3,31 +3,26 @@ module github.com/rosti-cz/node-api
go 1.14
require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/docker v25.0.3+incompatible
github.com/Microsoft/go-winio v0.4.18 // indirect
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.5.0 // indirect
github.com/getsentry/sentry-go v0.26.0
github.com/docker/go-units v0.4.0 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/gobuffalo/packr v1.30.1
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/jinzhu/gorm v1.9.14
github.com/kelseyhightower/envconfig v1.4.0
github.com/labstack/echo/v4 v4.10.0
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0 // indirect
github.com/mholt/archiver/v3 v3.5.0
github.com/minio/minio-go/v7 v7.0.14
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/nats-io/nats.go v1.23.0
github.com/opencontainers/image-spec v1.0.2
github.com/nats-io/nats-server/v2 v2.6.1 // indirect
github.com/nats-io/nats.go v1.12.3
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v2.20.6+incompatible
github.com/stretchr/testify v1.8.4
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
gorm.io/driver/mysql v1.4.7
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55
gotest.tools/v3 v3.5.1 // indirect
github.com/stretchr/testify v1.4.0
google.golang.org/protobuf v1.27.1 // indirect
)

2638
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -2,16 +2,16 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo"
"github.com/rosti-cz/node-api/apps"
"github.com/rosti-cz/node-api/common"
"github.com/rosti-cz/node-api/glue"
"github.com/rosti-cz/node-api/docker"
"github.com/rosti-cz/node-api/node"
)
func homeHandler(c echo.Context) error {
@ -21,15 +21,16 @@ func homeHandler(c echo.Context) error {
}
func listAppsHandler(c echo.Context) error {
processor := glue.Processor{
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
err := gatherStates()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
applications, err := processor.List(false)
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
applications, err := processor.List()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -40,17 +41,16 @@ func listAppsHandler(c echo.Context) error {
// Returns one app
func getAppHandler(c echo.Context) error {
name := c.Param("name")
fast := c.Param("fast")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
err := updateState(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
app, err := processor.Get(fast == "1")
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -63,33 +63,35 @@ func getAppHandler(c echo.Context) error {
func createAppHandler(c echo.Context) error {
registerOnly := c.QueryParam("register_only") == "1"
appTemplate := apps.App{}
err := c.Bind(&appTemplate)
app := apps.App{}
err := c.Bind(&app)
if err != nil {
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: appTemplate.Name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err = processor.New(app.Name, app.SSHPort, app.HTTPPort, app.Image, app.CPU, app.Memory)
if err != nil {
if validationError, ok := err.(apps.ValidationError); ok {
return c.JSONPretty(http.StatusBadRequest, Message{Errors: validationError.Errors}, JSONIndent)
}
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
if registerOnly {
err = processor.Register(appTemplate)
if err != nil && strings.Contains(err.Error(), "validation error") {
return c.JSONPretty(http.StatusBadRequest, Message{Errors: []string{err.Error()}}, JSONIndent)
} else if err != nil {
if !registerOnly {
container := docker.Container{
App: &app,
}
err = container.Create()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
} else {
err = processor.Create(appTemplate)
if err != nil && strings.Contains(err.Error(), "validation error") {
return c.JSONPretty(http.StatusBadRequest, Message{Errors: []string{err.Error()}}, JSONIndent)
} else if err != nil {
err = container.Start()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
@ -101,24 +103,45 @@ func createAppHandler(c echo.Context) error {
func updateAppHandler(c echo.Context) error {
name := c.Param("name")
appTemplate := apps.App{}
err := c.Bind(&appTemplate)
app := apps.App{}
err := c.Bind(&app)
if err != nil {
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err = processor.Update(appTemplate)
if err != nil && strings.Contains(err.Error(), "validation error") {
return c.JSONPretty(http.StatusBadRequest, Message{Errors: []string{err.Error()}}, JSONIndent)
} else if err != nil {
appPointer, err := processor.Update(name, app.SSHPort, app.HTTPPort, app.Image, app.CPU, app.Memory)
if err != nil {
if validationError, ok := err.(apps.ValidationError); ok {
return c.JSONPretty(http.StatusBadRequest, Message{Errors: validationError.Errors}, JSONIndent)
}
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
app = *appPointer
container := docker.Container{
App: &app,
}
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 c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
err = container.Create()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
err = container.Start()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -129,19 +152,31 @@ func updateAppHandler(c echo.Context) error {
func stopAppHandler(c echo.Context) error {
name := c.Param("name")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err := processor.Stop()
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
container := docker.Container{
App: app,
}
status, err := container.Status()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
// Stop the container only when it exists
if status != "no-container" {
err = container.Stop()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
return c.JSON(http.StatusOK, Message{Message: "ok"})
}
@ -149,15 +184,30 @@ func stopAppHandler(c echo.Context) error {
func startAppHandler(c echo.Context) error {
name := c.Param("name")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err := processor.Start()
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
container := docker.Container{
App: app,
}
status, err := container.Status()
if err != nil {
return err
}
if status == "no-container" {
err = container.Create()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
err = container.Start()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -169,15 +219,19 @@ func startAppHandler(c echo.Context) error {
func restartAppHandler(c echo.Context) error {
name := c.Param("name")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err := processor.Restart()
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
container := docker.Container{
App: app,
}
err = container.Restart()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -195,35 +249,19 @@ func setPasswordHandler(c echo.Context) error {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err = processor.SetPassword(password.Password)
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
return c.JSON(http.StatusOK, Message{Message: "ok"})
}
// Clear password for the app user in the container
func clearPasswordHandler(c echo.Context) error {
name := c.Param("name")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
container := docker.Container{
App: app,
}
err := processor.ClearPassword()
err = container.SetPassword(password.Password)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -235,20 +273,24 @@ func clearPasswordHandler(c echo.Context) error {
func setKeysHandler(c echo.Context) error {
name := c.Param("name")
body, err := io.ReadAll(c.Request().Body)
body, err := ioutil.ReadAll(c.Request().Body)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err = processor.UpdateKeys(string(body) + "\n")
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
container := docker.Container{
App: app,
}
err = container.SetFileContent(sshPubKeysLocation, string(body)+"\n", "0600")
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -259,26 +301,73 @@ func setKeysHandler(c echo.Context) error {
func setServicesHandler(c echo.Context) error {
name := c.Param("name")
tech := &Technology{}
err := c.Bind(tech)
quickServices := &QuickServices{}
err := c.Bind(quickServices)
if err != nil {
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err = processor.EnableTech(tech.Name, tech.Version)
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
container := docker.Container{
App: app,
}
if quickServices.Python {
err = container.SetTechnology("python")
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
if quickServices.PHP {
err = container.SetTechnology("php")
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
if quickServices.Node {
err = container.SetTechnology("node")
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
if quickServices.Ruby {
err = container.SetTechnology("ruby")
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
if quickServices.Deno {
err = container.SetTechnology("deno")
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
if quickServices.Memcached {
err = container.SetTechnology("memcached")
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
if quickServices.Redis {
err = container.SetTechnology("redis")
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
}
return c.JSON(http.StatusOK, Message{Message: "ok"})
}
@ -286,15 +375,33 @@ func setServicesHandler(c echo.Context) error {
func rebuildAppHandler(c echo.Context) error {
name := c.Param("name")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err := processor.Rebuild()
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
container := docker.Container{
App: app,
}
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 c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
err = container.Create()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
err = container.Start()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -312,15 +419,10 @@ func addLabelHandler(c echo.Context) error {
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err = processor.AddLabel(label.Value)
err = processor.AddLabel(name, label.Value)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -338,15 +440,10 @@ func deleteLabelHandler(c echo.Context) error {
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
err = processor.RemoveLabel(label.Value)
err = processor.RemoveLabel(name, label.Value)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -359,20 +456,36 @@ func deleteLabelHandler(c echo.Context) error {
func deleteAppHandler(c echo.Context) error {
name := c.Param("name")
go func(name string) {
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
go func(app *apps.App) {
container := docker.Container{
App: app,
}
err := processor.Delete()
status, err := container.Status()
if err != nil {
log.Printf("Deletion of application failed: %v", err)
log.Println("ERROR delete application problem: " + err.Error())
}
}(name)
if status != "no-container" {
err = container.Delete()
if err != nil {
log.Println("ERROR delete application problem: " + err.Error())
}
}
err = processor.Delete(app.Name)
if err != nil {
log.Println("ERROR delete application problem: " + err.Error())
}
}(app)
return c.JSON(http.StatusOK, Message{Message: "deleted"})
}
@ -382,44 +495,9 @@ func getOrphansHander(c echo.Context) error {
return c.JSON(http.StatusOK, []string{})
}
// Save metadata for the app
func saveMetadataHandler(c echo.Context) error {
name := c.Param("name")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
SnapshotProcessor: &snapshotProcessor,
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
}
body, err := io.ReadAll(c.Request().Body)
if err != nil {
return fmt.Errorf("error reading request body: %v", err)
}
err = processor.SaveMetadata(string(body))
if err != nil {
return fmt.Errorf("error while save metadata: %v", err.Error())
}
return nil
}
// Return info about the node including performance index
func getNodeInfoHandler(c echo.Context) error {
processor := glue.Processor{
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
NodeProcessor: &nodeProcessor,
}
node, err := processor.GetNode()
node, err := node.GetNodeInfo()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -431,15 +509,19 @@ func getNodeInfoHandler(c echo.Context) error {
func getAppProcessesHandler(c echo.Context) error {
name := c.Param("name")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
processes, err := processor.Processes()
app, err := processor.Get(name)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
container := docker.Container{
App: app,
}
processes, err := container.GetProcessList()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -452,15 +534,7 @@ func metricsHandler(c echo.Context) error {
var metrics string
// Node indexes
processor := glue.Processor{
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
NodeProcessor: &nodeProcessor,
}
node, err := processor.GetNode()
node, err := node.GetNodeInfo()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
@ -478,135 +552,18 @@ func metricsHandler(c echo.Context) error {
metrics += fmt.Sprintf("rosti_node_memory_index{hostname=\"%s\"} %f\n", hostname, node.MemoryIndex)
metrics += fmt.Sprintf("rosti_node_sold_memory{hostname=\"%s\"} %d\n", hostname, node.SoldMemory)
if elapsedMetric != -1 {
metrics += fmt.Sprintf("rosti_node_stats_time_elapsed{hostname=\"%s\"} %f\n", hostname, float64(elapsedMetric)/1000000000)
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
apps, err := processor.List(true)
apps, err := processor.List()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
for _, app := range apps {
for _, app := range *apps {
metrics += fmt.Sprintf("rosti_node_app_disk_usage_bytes{hostname=\"%s\", app=\"%s\"} %d\n", hostname, app.Name, app.DiskUsageBytes)
metrics += fmt.Sprintf("rosti_node_app_disk_usage_inodes{hostname=\"%s\", app=\"%s\"} %d\n", hostname, app.Name, app.DiskUsageInodes)
}
return c.String(http.StatusOK, metrics)
}
func CreateSnapshotHandler(c echo.Context) error {
name := c.Param("name")
body := createSnapshotBody{}
err := c.Bind(body)
if err != nil {
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
SnapshotProcessor: &snapshotProcessor,
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
}
err = processor.CreateSnapshot(body.Labels)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
return c.JSONPretty(http.StatusOK, Message{Message: "ok"}, JSONIndent)
}
func RestoreFromSnapshotHandler(c echo.Context) error {
name := c.Param("name")
snapshot := c.Param("snapshot")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
SnapshotProcessor: &snapshotProcessor,
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
}
err := processor.RestoreFromSnapshot(snapshot)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
return c.JSONPretty(http.StatusOK, Message{Message: "ok"}, JSONIndent)
}
func ListSnapshotsHandler(c echo.Context) error {
name := c.Param("name")
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
SnapshotProcessor: &snapshotProcessor,
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
}
snapshots, err := processor.ListSnapshots()
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
return c.JSON(http.StatusOK, snapshots)
}
func ListAppsSnapshotsHandler(c echo.Context) error {
name := c.Param("name")
apps := []string{}
err := c.Bind(&apps)
if err != nil {
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
}
processor := glue.Processor{
AppName: name,
DB: common.GetDBConnection(),
SnapshotProcessor: &snapshotProcessor,
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
}
snapshots, err := processor.ListAppsSnapshots(apps)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
return c.JSON(http.StatusOK, snapshots)
}
func ListSnapshotsByLabelHandler(c echo.Context) error {
label := c.Param("label")
processor := glue.Processor{
AppName: "",
DB: common.GetDBConnection(),
SnapshotProcessor: &snapshotProcessor,
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
}
snapshots, err := processor.ListSnapshotsByLabel(label)
if err != nil {
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
}
return c.JSON(http.StatusOK, snapshots)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,100 +0,0 @@
package jsonmap
import (
"bytes"
"context"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"strings"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
)
// JSONMap defined JSON data type, need to implements driver.Valuer, sql.Scanner interface
type JSONMap map[string]interface{}
// Value return json value, implement driver.Valuer interface
func (m JSONMap) Value() (driver.Value, error) {
if m == nil {
return nil, nil
}
ba, err := m.MarshalJSON()
return string(ba), err
}
// Scan scan value into Jsonb, implements sql.Scanner interface
func (m *JSONMap) Scan(val interface{}) error {
if val == nil {
*m = make(JSONMap)
return nil
}
var ba []byte
switch v := val.(type) {
case []byte:
ba = v
case string:
ba = []byte(v)
default:
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", val))
}
t := map[string]interface{}{}
rd := bytes.NewReader(ba)
decoder := json.NewDecoder(rd)
decoder.UseNumber()
err := decoder.Decode(&t)
*m = t
return err
}
// MarshalJSON to output non base64 encoded []byte
func (m JSONMap) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("null"), nil
}
t := (map[string]interface{})(m)
return json.Marshal(t)
}
// UnmarshalJSON to deserialize []byte
func (m *JSONMap) UnmarshalJSON(b []byte) error {
t := map[string]interface{}{}
err := json.Unmarshal(b, &t)
*m = JSONMap(t)
return err
}
// GormDataType gorm common data type
func (m JSONMap) GormDataType() string {
return "jsonmap"
}
// GormDBDataType gorm db data type
func (JSONMap) GormDBDataType(db *gorm.DB, field *schema.Field) string {
switch db.Dialector.Name() {
case "sqlite3":
return "JSON"
case "mysql":
return "JSON"
case "postgres":
return "JSONB"
case "sqlserver":
return "NVARCHAR(MAX)"
}
return ""
}
func (jm JSONMap) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
data, _ := jm.MarshalJSON()
switch db.Dialector.Name() {
case "mysql":
if v, ok := db.Dialector.(*mysql.Dialector); ok && !strings.Contains(v.ServerVersion, "MariaDB") {
return gorm.Expr("CAST(? AS JSON)", string(data))
}
}
return gorm.Expr("?", string(data))
}

72
main.go
View File

@ -5,14 +5,10 @@ import (
"log"
"time"
"github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/labstack/echo/v4"
"github.com/labstack/echo"
"github.com/nats-io/nats.go"
"github.com/rosti-cz/node-api/apps"
"github.com/rosti-cz/node-api/apps/drivers"
"github.com/rosti-cz/node-api/common"
"github.com/rosti-cz/node-api/glue"
"github.com/rosti-cz/node-api/node"
)
@ -21,10 +17,6 @@ const JSONIndent = " "
var config common.Config
var nc *nats.Conn
var snapshotProcessor apps.SnapshotProcessor
var nodeProcessor node.Processor
var elapsedMetric int // time elapsed while loading stats about apps
func _init() {
var err error
@ -32,18 +24,9 @@ func _init() {
// Load config from environment variables
config = *common.GetConfig()
// Sentry
sentry.Init(sentry.ClientOptions{
Dsn: config.SentryDSN,
AttachStacktrace: true,
Environment: config.SentryENV,
TracesSampleRate: 0.1,
})
// Connect to the NATS service
nc, err = nats.Connect(config.NATSURL)
if err != nil {
sentry.CaptureException(err)
log.Fatalln(err)
}
@ -52,35 +35,11 @@ func _init() {
DB: common.GetDBConnection(),
}
processor.Init()
// Prepare snapshot processor
snapshotProcessor = apps.SnapshotProcessor{
AppsPath: config.AppsPath,
TmpSnapshotPath: config.SnapshotsPath,
IndexLabel: config.SnapshotsIndexLabel,
Driver: drivers.S3Driver{
S3AccessKey: config.SnapshotsS3AccessKey,
S3SecretKey: config.SnapshotsS3SecretKey,
S3Endpoint: config.SnapshotsS3Endpoint,
S3SSL: config.SnapshotsS3SSL,
Bucket: config.SnapshotsS3Bucket,
},
}
nodeProcessor = node.Processor{
DB: common.GetDBConnection(),
}
nodeProcessor.Init()
// Reset elapsed stats
elapsedMetric = -1
}
func main() {
_init()
defer nc.Drain()
defer sentry.Flush(time.Second * 10)
// Close database at the end
db := common.GetDBConnection()
@ -91,24 +50,14 @@ func main() {
// Stats loop
go func() {
statsProcessor := glue.StatsProcessor{
DB: common.GetDBConnection(),
DockerSock: config.DockerSocket,
BindIPHTTP: config.AppsBindIPHTTP,
BindIPSSH: config.AppsBindIPSSH,
AppsPath: config.AppsPath,
}
for {
log.Println("Stats gathering started")
start := time.Now()
err := statsProcessor.GatherStats()
err := gatherStats()
if err != nil {
sentry.CaptureException(err)
log.Println("LOOP ERROR:", err.Error())
}
elapsed := time.Since(start)
elapsedMetric = int(elapsed)
log.Printf("Stats gathering elapsed time: %.2fs\n", elapsed.Seconds())
time.Sleep(300 * time.Second)
}
@ -117,9 +66,8 @@ func main() {
// Node stats
go func() {
for {
err := nodeProcessor.Log()
err := node.Log()
if err != nil {
sentry.CaptureException(err)
log.Println("NODE PERFORMANCE LOG ERROR:", err.Error())
}
time.Sleep(5 * time.Minute)
@ -131,7 +79,6 @@ func main() {
e.Renderer = t
e.Use(TokenMiddleware)
e.Use(sentryecho.New(sentryecho.Options{}))
// NATS handling
// admin.apps.ALIAS.events
@ -174,9 +121,6 @@ func main() {
// Set password for the app user in the container
e.PUT("/v1/apps/:name/password", setPasswordHandler)
// Clear password for the app user in the container
e.DELETE("/v1/apps/:name/password", clearPasswordHandler)
// Copies body of the request into /srv/.ssh/authorized_keys
e.PUT("/v1/apps/:name/keys", setKeysHandler)
@ -186,9 +130,6 @@ func main() {
// Rebuilds existing app, it keeps the data but creates the container again
e.PUT("/v1/apps/:name/rebuild", rebuildAppHandler)
// Save metadata about app
e.POST("/v1/apps/:name/metadata", saveMetadataHandler)
// Adds new label
e.POST("/v1/apps/:name/labels", addLabelHandler)
@ -198,13 +139,6 @@ func main() {
// Delete one app
e.DELETE("/v1/apps/:name", deleteAppHandler)
// Snapshots
e.POST("/v1/apps/:name/snapshots", CreateSnapshotHandler)
e.POST("/v1/apps/:name/snapshots/restore/:snapshot", RestoreFromSnapshotHandler)
e.GET("/v1/apps/:name/snapshots", ListSnapshotsHandler)
e.GET("/v1/snapshots", ListAppsSnapshotsHandler)
e.GET("/v1/snapshots/by-label", ListSnapshotsByLabelHandler)
// Orphans returns directories in /srv that doesn't match any hosted application
e.GET("/v1/orphans", getOrphansHander)

View File

@ -1,33 +1,23 @@
package node
import (
"github.com/rosti-cz/node-api/common"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"github.com/jinzhu/gorm"
)
const history = 72 * 3600 / 300 // 3 days, one record every five minutes
// Processor covers Node related methods for monitoring and calculating performance indexes.
type Processor struct {
DB *gorm.DB
}
func (p *Processor) Init() {
p.DB.AutoMigrate(PerformanceLog{})
}
// GetNodeInfo returns information about this node
func (p *Processor) GetNodeInfo() (*Node, error) {
node, err := p.Index()
return node, err
func init() {
db := common.GetDBConnection()
db.AutoMigrate(PerformanceLog{})
}
// Log creates a record for all important metrics used as
func (p *Processor) Log() error {
func Log() error {
performanceLog := PerformanceLog{}
// Load
@ -55,7 +45,8 @@ func (p *Processor) Log() error {
performanceLog.DiskSpaceUsage = diskUsage.UsedPercent / 100.0
// Save
err = p.DB.Create(&performanceLog).Error
db := common.GetDBConnection()
err = db.Create(&performanceLog).Error
if err != nil {
return err
}
@ -63,13 +54,13 @@ func (p *Processor) Log() error {
// and clean
// we have to use this stupid approach because DELETE doesn't support ORDER BY and LIMIT
toDeleteLogs := []PerformanceLog{}
err = p.DB.Order("id DESC").Limit("99").Offset(history).Find(&toDeleteLogs).Error
err = db.Order("id DESC").Limit("99").Offset(history).Find(&toDeleteLogs).Error
if err != nil {
return err
}
for _, toDeleteLog := range toDeleteLogs {
err = p.DB.Delete(&toDeleteLog).Error
err = db.Delete(&toDeleteLog).Error
if err != nil {
return err
}
@ -80,7 +71,7 @@ func (p *Processor) Log() error {
// Index returns number from 0 to 1 where 0 means least loaded and 1 maximally loaded.
// It uses history of last 72 hours
func (p *Processor) Index() (*Node, error) {
func index() (*Node, error) {
node := Node{
Index: 1.0,
}
@ -91,9 +82,11 @@ func (p *Processor) Index() (*Node, error) {
return &node, err
}
db := common.GetDBConnection()
logs := []PerformanceLog{}
err = p.DB.Find(&logs).Error
err = db.Find(&logs).Error
if err != nil {
return &node, err
}

View File

@ -1 +1,7 @@
package node
// GetNodeInfo returns information about this node
func GetNodeInfo() (*Node, error) {
node, err := index()
return node, err
}

106
stats.go Normal file
View File

@ -0,0 +1,106 @@
package main
import (
"log"
"github.com/rosti-cz/node-api/apps"
"github.com/rosti-cz/node-api/common"
"github.com/rosti-cz/node-api/docker"
)
// updateUsage updates various resource usage of the container/app in the database
func updateUsage(name string) error {
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
app, err := processor.Get(name)
if err != nil {
return err
}
container := docker.Container{
App: app,
}
state, err := container.GetState()
if err != nil {
return err
}
err = processor.UpdateResources(
name,
state.State,
state.CPUUsage,
state.MemoryUsage,
state.DiskUsageBytes,
state.DiskUsageInodes,
)
return err
}
// Updates only container's state
func updateState(name string) error {
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
app, err := processor.Get(name)
if err != nil {
return err
}
container := docker.Container{
App: app,
}
state, err := container.Status()
if err != nil {
return err
}
err = processor.UpdateState(
app.Name,
state,
)
return err
}
// gatherStats loops over all applications and calls updateUsage to write various metric into the database.
func gatherStats() error {
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
appList, err := processor.List()
if err != nil {
return err
}
for _, app := range *appList {
err := updateUsage(app.Name)
if err != nil {
log.Println("STATS ERROR:", err.Error())
}
}
return nil
}
// gatherStates loops over all apps and updates their container state
func gatherStates() error {
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
appList, err := processor.List()
if err != nil {
return err
}
for _, app := range *appList {
err := updateState(app.Name)
if err != nil {
log.Println("STATE ERROR:", err.Error())
}
}
return nil
}

View File

@ -5,7 +5,7 @@ import (
"io"
"github.com/gobuffalo/packr"
"github.com/labstack/echo/v4"
"github.com/labstack/echo"
)
// Template struct

View File

@ -2,10 +2,15 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"time"
"github.com/nats-io/nats.go"
"github.com/rosti-cz/node-api/apps"
"github.com/rosti-cz/node-api/common"
"github.com/rosti-cz/node-api/docker"
)
func errorReplyFormater(m *nats.Msg, message string, err error) error {
@ -49,3 +54,43 @@ func publish(appName string, state string, isErr bool) {
log.Println("ERROR: publish:", err.Error())
}
}
// waitForApp waits until app is ready or timeout is reached.
// It's used in some async calls that need at least part of the
// environment prepared.
func waitForApp(appName string) error {
processor := apps.AppsProcessor{
DB: common.GetDBConnection(),
}
sleepFor := 5 * time.Second
loops := 6
for i := 0; i < loops; i++ {
err := updateState(appName)
if err != nil {
time.Sleep(sleepFor)
continue
}
app, err := processor.Get(appName)
if err != nil {
return err
}
container := docker.Container{
App: app,
}
status, err := container.Status()
if status == "running" {
return nil
}
time.Sleep(sleepFor)
continue
}
return errors.New("timeout reached")
}

View File

@ -1,8 +1,9 @@
package main
import (
"encoding/json"
)
import "encoding/json"
// Path where authorized keys are
const sshPubKeysLocation = "/srv/.ssh/authorized_keys"
// RequestMessage message
type RequestMessage struct {
@ -56,17 +57,6 @@ type QuickServices struct {
PHP bool `json:"php"`
Ruby bool `json:"ruby"`
Deno bool `json:"deno"`
Bun bool `json:"bun"`
Memcached bool `json:"memcached"`
Redis bool `json:"redis"`
}
// Set technology
type Technology struct {
Name string
Version string
}
type createSnapshotBody struct {
Labels []string `json:"labels"`
}