Compare commits
157 Commits
Author | SHA1 | Date | |
---|---|---|---|
622dd91ba6 | |||
bc875b83a4 | |||
27152bad72 | |||
27948ee5b6 | |||
8c5a419efc | |||
88262a27d8 | |||
088b6bdcf6 | |||
5f80da8cbd | |||
2077271306 | |||
9e82bfc2b5 | |||
e0b5832e75 | |||
863d857283 | |||
1c5b8d8f50 | |||
bc4b6c7bff | |||
45899f3b0c | |||
5513da35b3 | |||
31ba1ce5a3 | |||
a3d0ee92ce | |||
7a170e56d6 | |||
4e9398512e | |||
a4e2bac0ff | |||
02cdf5f815 | |||
036587a77a | |||
6d62b200a4 | |||
9564118f40 | |||
0ad979b240 | |||
9224675139 | |||
5fc6f39529 | |||
fe8aa885e7 | |||
00beda8137 | |||
37ca4ece39 | |||
072a643c1d | |||
ed5061fd58 | |||
7f8ed1c018 | |||
dab9b52953 | |||
0b5ded2abf | |||
494e31c4b1 | |||
d25e0aba36 | |||
6390fb19bb | |||
e9fb6a6b46 | |||
4a4da2c080 | |||
617f818df4 | |||
c6f9620945 | |||
af1e69e594 | |||
6fd3278ee8 | |||
1ccf4b8301 | |||
37a5297c88 | |||
5b0a46951b | |||
16bb4e71d5 | |||
8c8ecc6379 | |||
d465421fa0 | |||
4c2bcdd3e7 | |||
45996c4d1b | |||
3ae383d1cd | |||
7f07a50bfd | |||
036aa4fc28 | |||
bcc641307a | |||
da5ad13ae3 | |||
d7b878bbbc | |||
9a37518431 | |||
8051310677 | |||
df5a390680 | |||
f3f50b0ace | |||
779d9ba95a | |||
d469c813a1 | |||
b7dfa22f94 | |||
938f6f89b6 | |||
b15e85474e | |||
8adbf84362 | |||
5b63a0d9aa | |||
797bc6d654 | |||
0725b352c6 | |||
96071cdfcb | |||
37d884e665 | |||
0ea302d688 | |||
5caa570055 | |||
61adbd7a28 | |||
e8d952cac0 | |||
69e147ccf9 | |||
1017288a4c | |||
0cc986d22f | |||
24fffe736e | |||
473d561b84 | |||
52d8f7b250 | |||
02ebfb8eea | |||
68121fda15 | |||
35a0c5acd1 | |||
3f1a0d9a8a | |||
29866d0e75 | |||
fea1a46a11 | |||
7334337c93 | |||
7add860d83 | |||
00f2d418d6 | |||
2405383ff1 | |||
157afdcdbe | |||
c11de8a3e9 | |||
81b3d45a4b | |||
1d1a225be6 | |||
69e46f577a | |||
676ddf2136 | |||
722d2a1c86 | |||
c8a93d7993 | |||
1fb659b1e6 | |||
e50e139f60 | |||
8924db5712 | |||
d43529fbb8 | |||
e58d6462a9 | |||
fdcdca237f | |||
49f1a03b42 | |||
caf791548d | |||
27754caaf0 | |||
a213fac34a | |||
2a34177ab8 | |||
6f532b052e | |||
a4beb9727e | |||
c2e9b1cc18 | |||
7fe8dfcd9b | |||
fd3c85bed4 | |||
2a545daedb | |||
7c46fdd886 | |||
3fdabb2229 | |||
c5667f99b8 | |||
3120a8c6ee | |||
872826c0b1 | |||
66476df2bc | |||
564e9ec4f1 | |||
4e56ab7657 | |||
d2b935b85c | |||
12c2b14b6f | |||
f65702b416 | |||
66476275e4 | |||
801b082785 | |||
d4dddc0df3 | |||
95ae31fdfb | |||
fc78a7f375 | |||
dae67e6be6 | |||
1a107e2a28 | |||
7616eef12b | |||
81bcdcf00c | |||
615029f6c0 | |||
03eb87d9dd | |||
dc478714bf | |||
4e56466b07 | |||
fa277e9f08 | |||
fa857b1913 | |||
65bdabd78c | |||
2640690cc6 | |||
3241db2ef1 | |||
03b9dd1c58 | |||
d15aee9fa0 | |||
b534ee7c05 | |||
5ba061895b | |||
119baa48c5 | |||
4b823afc51 | |||
a9cf273650 | |||
0a6789c56c | |||
3fb5c217e4 |
25
.drone.yml
25
.drone.yml
@ -1,25 +0,0 @@
|
||||
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
|
33
.gitea/workflows/release.yml
Normal file
33
.gitea/workflows/release.yml
Normal file
@ -0,0 +1,33 @@
|
||||
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
|
||||
|
62
.gitea/workflows/unittests.yml
Normal file
62
.gitea/workflows/unittests.yml
Normal file
@ -0,0 +1,62 @@
|
||||
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
|
||||
|
13
Makefile
13
Makefile
@ -1,7 +1,9 @@
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v apps/*.go
|
||||
go test -v apps/drivers/*.go
|
||||
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
|
||||
|
||||
build:
|
||||
#podman run --rm --privileged -ti -v ${shell pwd}:/srv docker.io/library/golang:1.14-stretch /bin/sh -c "cd /srv && go build"
|
||||
@ -18,4 +20,9 @@ minio:
|
||||
-p 9001:9001 \
|
||||
-e MINIO_ROOT_USER=test \
|
||||
-e MINIO_ROOT_PASSWORD=testtest \
|
||||
minio/minio server /data --console-address ":9001"
|
||||
docker.io/minio/minio:latest server /data --console-address ":9001"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-podman stop rosti-snapshots
|
||||
-podman rm rosti-snapshots
|
||||
|
13
README.md
13
README.md
@ -4,3 +4,16 @@
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@ -83,3 +84,8 @@ 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")
|
||||
}
|
||||
|
@ -5,11 +5,14 @@ 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
|
||||
@ -155,3 +158,22 @@ 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
|
||||
}
|
||||
|
@ -77,3 +77,12 @@ 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")
|
||||
}
|
||||
|
@ -19,4 +19,7 @@ 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)
|
||||
}
|
||||
|
30
apps/main.go
30
apps/main.go
@ -3,9 +3,11 @@ 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
|
||||
}
|
||||
@ -16,19 +18,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 nil, err
|
||||
return app, 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
|
||||
@ -36,7 +38,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
|
||||
@ -65,7 +67,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) (*App, error) {
|
||||
func (a *AppsProcessor) Update(name string, SSHPort int, HTTPPort int, image string, CPU int, memory int, env map[string]string) (*App, error) {
|
||||
var app App
|
||||
|
||||
err := a.DB.Where("name = ?", name).First(&app).Error
|
||||
@ -96,6 +98,10 @@ 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{
|
||||
@ -109,21 +115,25 @@ 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, CPUUsage float64, memory int, diskUsageBytes int, diskUsageInodes int) error {
|
||||
func (a *AppsProcessor) UpdateResources(name string, state string, OOMKilled bool, CPUUsage float64, memory int, diskUsageBytes int, diskUsageInodes int, flags detector.Flags, isPasswordSet bool) 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) error {
|
||||
func (a *AppsProcessor) UpdateState(name string, state string, OOMKilled bool) error {
|
||||
err := a.DB.Model(&App{}).Where("name = ?", name).Updates(App{
|
||||
State: state,
|
||||
State: state,
|
||||
OOMKilled: OOMKilled,
|
||||
}).Error
|
||||
return err
|
||||
}
|
||||
@ -178,5 +188,5 @@ func (a *AppsProcessor) RemoveLabel(appName string, label string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.DB.Where("label = ? AND app_id = ?", label, app.ID).Delete(&Label{}).Error
|
||||
return a.DB.Where("value = ? AND app_id = ?", label, app.ID).Delete(&Label{}).Error
|
||||
}
|
||||
|
194
apps/main_test.go
Normal file
194
apps/main_test.go
Normal file
@ -0,0 +1,194 @@
|
||||
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")
|
||||
}
|
@ -1,29 +1,33 @@
|
||||
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 {
|
||||
AppName string
|
||||
TimeStamp int64
|
||||
Labels []string
|
||||
UUID string `json:"uuid"`
|
||||
AppName string `json:"app_name"`
|
||||
TimeStamp int64 `json:"ts"`
|
||||
Labels []string `json:"labels"`
|
||||
}
|
||||
|
||||
// SnapshotIndexLine is struct holding information about a single snapshot
|
||||
@ -34,31 +38,15 @@ func (s *Snapshot) ToString() string {
|
||||
}
|
||||
|
||||
// KeyName returns keyname used to store the snapshot in the storage
|
||||
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")
|
||||
func (s *Snapshot) KeyName(indexLabel string) string {
|
||||
labelValue := "-"
|
||||
for _, label := range s.Labels {
|
||||
if strings.HasPrefix(label, indexLabel+":") {
|
||||
labelValue = label[len(indexLabel)+1:]
|
||||
}
|
||||
}
|
||||
|
||||
_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
|
||||
return fmt.Sprintf("%s%s%s%s%s%s%d", s.AppName, keySplitCharacter, s.UUID, keySplitCharacter, labelValue, keySplitCharacter, s.TimeStamp)
|
||||
}
|
||||
|
||||
type Snapshots []Snapshot
|
||||
@ -66,47 +54,104 @@ 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 index of snapshots without
|
||||
// so we don't have any locking mechanism here and we cannot created a single 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
|
||||
}
|
||||
|
||||
// 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 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,
|
||||
// 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.
|
||||
func (s *SnapshotProcessor) CreateSnapshot(appName string, labels []string) (string, error) {
|
||||
snapshot := Snapshot{
|
||||
UUID: uuid.NewV4().String(),
|
||||
AppName: appName,
|
||||
TimeStamp: time.Now().Unix(),
|
||||
Labels: labels,
|
||||
}
|
||||
|
||||
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, snapshot.KeyName()+".zip")
|
||||
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, snapshot.KeyName(s.IndexLabel)+".tar.zst")
|
||||
|
||||
err := os.Chdir(path.Join(s.AppsPath, appName))
|
||||
if err != nil {
|
||||
return snapshot.KeyName(), fmt.Errorf("change working directory error: %v", err)
|
||||
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("change working directory error: %v", err)
|
||||
}
|
||||
|
||||
err = archive.Archive([]string{"./"}, tmpSnapshotArchivePath)
|
||||
err = exec.Command(tarBin, "-acf", tmpSnapshotArchivePath, "./").Run()
|
||||
if err != nil {
|
||||
return snapshot.KeyName(), fmt.Errorf("compression error: %v", err)
|
||||
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)
|
||||
}
|
||||
|
||||
// Clean after myself
|
||||
@ -118,19 +163,19 @@ func (s *SnapshotProcessor) CreateSnapshot(appName string, labels []string) (str
|
||||
}()
|
||||
|
||||
// Pipe it into the storage
|
||||
err = s.Driver.Create(snapshot.KeyName(), tmpSnapshotArchivePath)
|
||||
err = s.Driver.Create(snapshot.KeyName(s.IndexLabel), tmpSnapshotArchivePath)
|
||||
if err != nil {
|
||||
return snapshot.KeyName(), fmt.Errorf("copying snapshot into S3 error: %v", err)
|
||||
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("copying snapshot into S3 error: %v", err)
|
||||
}
|
||||
|
||||
return snapshot.KeyName(), nil
|
||||
return snapshot.KeyName(s.IndexLabel), nil
|
||||
}
|
||||
|
||||
// RestoreSnapshot restores snapshot into an existing application
|
||||
// If you need a new app from existing snapshot just create it.
|
||||
// This restored only content of the disk, doesn't create the container.
|
||||
// This restores only content on the disk, doesn't create the container.
|
||||
func (s *SnapshotProcessor) RestoreSnapshot(key string, newAppName string) error {
|
||||
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, key+".zip")
|
||||
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, key+".tar.zst")
|
||||
|
||||
err := os.MkdirAll(path.Join(s.AppsPath, newAppName), 0755)
|
||||
if err != nil {
|
||||
@ -142,21 +187,12 @@ func (s *SnapshotProcessor) RestoreSnapshot(key string, newAppName string) error
|
||||
return fmt.Errorf("creating destination path error: %v", err)
|
||||
}
|
||||
|
||||
s.Driver.Get(key, tmpSnapshotArchivePath)
|
||||
err = s.Driver.Get(key, tmpSnapshotArchivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting the archive from S3 error: %v", err)
|
||||
}
|
||||
|
||||
archive := archiver.Zip{
|
||||
CompressionLevel: 6,
|
||||
MkdirAll: true,
|
||||
SelectiveCompression: true,
|
||||
ContinueOnError: false,
|
||||
OverwriteExisting: false,
|
||||
ImplicitTopLevelFolder: false,
|
||||
}
|
||||
|
||||
err = archive.Unarchive(tmpSnapshotArchivePath, "./")
|
||||
err = exec.Command(tarBin, "-axf", tmpSnapshotArchivePath).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unarchiving error: %v", err)
|
||||
}
|
||||
@ -166,6 +202,12 @@ 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
|
||||
}
|
||||
|
||||
@ -179,8 +221,12 @@ func (s *SnapshotProcessor) ListAppSnapshots(appName string) ([]Snapshot, error)
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if key == metadataPrefix+"/" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(key, appName+keySplitCharacter) {
|
||||
snapshot, err := DecodeKeyName(key)
|
||||
snapshot, err := s.metadataForSnapshotKey(key)
|
||||
if err != nil {
|
||||
return snapshots, err
|
||||
}
|
||||
@ -202,9 +248,13 @@ 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 := DecodeKeyName(key)
|
||||
snapshot, err := s.metadataForSnapshotKey(key)
|
||||
if err != nil {
|
||||
return snapshots, err
|
||||
}
|
||||
@ -217,9 +267,9 @@ func (s *SnapshotProcessor) ListAppsSnapshots(appNames []string) ([]Snapshot, er
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
// ListAppsSnapshotsByLabels returns list of snapshots with given label
|
||||
// ListAppsSnapshotsByLabel 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) ListAppsSnapshotsByLabels(desiredLabel string) ([]Snapshot, error) {
|
||||
func (s *SnapshotProcessor) ListAppsSnapshotsByLabel(labelValue string) ([]Snapshot, error) {
|
||||
snapshots := []Snapshot{}
|
||||
|
||||
keys, err := s.Driver.List("")
|
||||
@ -228,13 +278,20 @@ func (s *SnapshotProcessor) ListAppsSnapshotsByLabels(desiredLabel string) ([]Sn
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
snapshot, err := DecodeKeyName(key)
|
||||
if err != nil {
|
||||
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 {
|
||||
return snapshots, err
|
||||
}
|
||||
|
||||
for _, label := range snapshot.Labels {
|
||||
if label == desiredLabel {
|
||||
if label == fmt.Sprintf("%s:%s", s.IndexLabel, labelValue) {
|
||||
snapshots = append(snapshots, snapshot)
|
||||
}
|
||||
}
|
||||
@ -243,6 +300,28 @@ func (s *SnapshotProcessor) ListAppsSnapshotsByLabels(desiredLabel string) ([]Sn
|
||||
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)
|
||||
@ -261,7 +340,7 @@ func (s *SnapshotProcessor) DeleteAppSnapshots(appName string) error {
|
||||
}
|
||||
|
||||
for _, snapshot := range snapshots {
|
||||
err = s.DeleteSnapshot(snapshot.KeyName())
|
||||
err = s.DeleteSnapshot(snapshot.KeyName(s.IndexLabel))
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing snapshots error: %v", err)
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ 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",
|
||||
@ -76,14 +77,18 @@ 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:1634510035:eyJBcHBOYW1lIjoiYXBwXzAxMDIiLCJUaW1lU3RhbXAiOjE2MzQ1MTAwMzUsIkxhYmVscyI6WyJ1c2VyaWQ6MSJdfQ==", snapshot.KeyName())
|
||||
assert.Equal(t, "app_0102:ABCDEF:1:1634510035", snapshot.KeyName("userid"))
|
||||
|
||||
snapshot2, err := DecodeKeyName("app_0102:1634510035:eyJBcHBOYW1lIjoiYXBwXzAxMDIiLCJUaW1lU3RhbXAiOjE2MzQ1MTAwMzUsIkxhYmVscyI6WyJ1c2VyaWQ6MSJdfQ==")
|
||||
err := snapshotProcessor.saveMetadata(snapshot)
|
||||
assert.Nil(t, err)
|
||||
|
||||
snapshot2, err := snapshotProcessor.metadataForSnapshotKey("app_0102:ABCDEF:1:1634510035")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "app_0102", snapshot2.AppName)
|
||||
assert.Equal(t, int64(1634510035), snapshot2.TimeStamp)
|
||||
@ -153,3 +158,37 @@ 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)
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// This is line from GORM documentation that imports database dialect
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/rosti-cz/node-api/detector"
|
||||
"github.com/rosti-cz/node-api/jsonmap"
|
||||
)
|
||||
|
||||
// ValidationError is error that holds multiple validation error messages
|
||||
@ -31,11 +31,14 @@ type Label struct {
|
||||
|
||||
// AppState contains info about runnint application, it's not saved in the database
|
||||
type AppState struct {
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// Apps is list of applications
|
||||
@ -53,6 +56,8 @@ 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"`
|
||||
@ -64,6 +69,9 @@ 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
|
||||
@ -72,9 +80,12 @@ 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
|
||||
@ -83,6 +94,44 @@ 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
|
||||
@ -102,6 +151,10 @@ 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")
|
||||
}
|
||||
@ -116,3 +169,12 @@ 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"`
|
||||
}
|
||||
|
2
auth.go
2
auth.go
@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var skipPaths []string = []string{"/metrics"}
|
||||
|
@ -8,6 +8,7 @@ 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
|
||||
@ -21,6 +22,9 @@ 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
|
||||
|
@ -1,9 +1,10 @@
|
||||
package docker
|
||||
package containers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@ -17,6 +18,8 @@ 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
|
||||
@ -25,29 +28,22 @@ const statsDelay = 1
|
||||
// Docker timeout
|
||||
const dockerTimeout = 10
|
||||
|
||||
// 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"
|
||||
// DOCKER_API_VERSION set API version of Docker, 1.41 is needed for the platform struct
|
||||
const dockerAPIVersion = "1.41"
|
||||
|
||||
// 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) {
|
||||
var connectTo string
|
||||
if _, err := os.Stat(podmanSock); !os.IsNotExist(err) {
|
||||
connectTo = podmanSock
|
||||
} else {
|
||||
connectTo = dockerSock
|
||||
cli, err := dockerClient.NewClient(d.DockerSock, dockerAPIVersion, nil, nil)
|
||||
if err != nil {
|
||||
return cli, fmt.Errorf("get docker client error: %v", err)
|
||||
}
|
||||
|
||||
cli, err := dockerClient.NewClient("unix://"+connectTo, dockerAPIVersion, nil, nil)
|
||||
return cli, err
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
// ConnectionStatus checks connection to the Docker daemon
|
||||
@ -83,8 +79,10 @@ func (d *Driver) nameToID(name string) (string, error) {
|
||||
}
|
||||
|
||||
// Status return current status of container with given name
|
||||
func (d *Driver) Status(name string) (string, error) {
|
||||
status := "unknown"
|
||||
func (d *Driver) Status(name string) (ContainerStatus, error) {
|
||||
status := ContainerStatus{
|
||||
Status: "unknown",
|
||||
}
|
||||
|
||||
cli, err := d.getClient()
|
||||
if err != nil {
|
||||
@ -94,7 +92,8 @@ func (d *Driver) Status(name string) (string, error) {
|
||||
|
||||
containerID, err := d.nameToID(name)
|
||||
if err != nil && err.Error() == "no container found" {
|
||||
return "no-container", err
|
||||
status.Status = "no-container"
|
||||
return status, err
|
||||
}
|
||||
if err != nil {
|
||||
return status, err
|
||||
@ -105,7 +104,10 @@ func (d *Driver) Status(name string) (string, error) {
|
||||
return status, err
|
||||
}
|
||||
|
||||
return info.State.Status, nil
|
||||
status.Status = info.State.Status
|
||||
status.OOMKilled = info.State.OOMKilled
|
||||
|
||||
return status, nil
|
||||
|
||||
}
|
||||
|
||||
@ -200,13 +202,13 @@ func (d *Driver) Remove(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
timeout := time.Duration(dockerTimeout * time.Second)
|
||||
err = cli.ContainerStop(context.TODO(), containerID, &timeout)
|
||||
timeout := dockerTimeout
|
||||
err = cli.ContainerStop(context.TODO(), containerID, container.StopOptions{Timeout: &timeout})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cli.ContainerRemove(context.TODO(), containerID, types.ContainerRemoveOptions{})
|
||||
err = cli.ContainerRemove(context.TODO(), containerID, container.RemoveOptions{})
|
||||
|
||||
return err
|
||||
}
|
||||
@ -244,8 +246,8 @@ func (d *Driver) Stop(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
timeout := time.Duration(dockerTimeout * time.Second)
|
||||
err = cli.ContainerStop(context.TODO(), containerID, &timeout)
|
||||
timeout := dockerTimeout
|
||||
err = cli.ContainerStop(context.TODO(), containerID, container.StopOptions{Timeout: &timeout})
|
||||
|
||||
return err
|
||||
}
|
||||
@ -305,7 +307,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) (string, error) {
|
||||
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) {
|
||||
log.Println("Creating container " + name)
|
||||
cli, err := d.getClient()
|
||||
if err != nil {
|
||||
@ -336,11 +338,21 @@ 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: []string{},
|
||||
Env: envList,
|
||||
Image: image,
|
||||
Cmd: cmd,
|
||||
ExposedPorts: nat.PortSet{
|
||||
@ -354,6 +366,7 @@ 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,
|
||||
@ -366,6 +379,10 @@ func (d *Driver) Create(name string, image string, volumePath string, HTTPPort i
|
||||
},
|
||||
},
|
||||
&network.NetworkingConfig{},
|
||||
&specs.Platform{
|
||||
Architecture: "amd64",
|
||||
OS: "linux",
|
||||
},
|
||||
name,
|
||||
)
|
||||
if err != nil {
|
||||
@ -418,11 +435,9 @@ func (d *Driver) Exec(name string, cmd []string, stdin string, env []string, att
|
||||
return &[]byte{}, err
|
||||
}
|
||||
|
||||
respAttach, err := cli.ContainerExecAttach(ctx, resp.ID, types.ExecConfig{
|
||||
AttachStdin: stdinEnabled,
|
||||
AttachStdout: attachStdout,
|
||||
AttachStderr: false,
|
||||
Tty: attachStdout,
|
||||
respAttach, err := cli.ContainerExecAttach(ctx, resp.ID, types.ExecStartCheck{
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
})
|
||||
if err != nil {
|
||||
return &[]byte{}, err
|
||||
@ -448,3 +463,44 @@ 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
|
||||
}
|
44
containers/docker_test.go
Normal file
44
containers/docker_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
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")
|
||||
}
|
19
containers/stats.go
Normal file
19
containers/stats.go
Normal file
@ -0,0 +1,19 @@
|
||||
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"`
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package docker
|
||||
package containers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -62,7 +64,7 @@ func du(path string) (int, int, error) {
|
||||
return space, inodes, nil
|
||||
}
|
||||
|
||||
// Removes content of given directory
|
||||
// Removes content of given directory and then the directory itself
|
||||
func removeDirectory(dir string) error {
|
||||
d, err := os.Open(dir)
|
||||
if err != nil {
|
||||
@ -79,6 +81,12 @@ func removeDirectory(dir string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Remove(filepath.Join(dir))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -129,3 +137,53 @@ 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
|
||||
}
|
640
containers/types.go
Normal file
640
containers/types.go
Normal file
@ -0,0 +1,640 @@
|
||||
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
|
||||
}
|
39
containers/types_test.go
Normal file
39
containers/types_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -3,4 +3,5 @@
|
||||
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-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}}'
|
||||
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}"}'
|
||||
|
40
detector/main.go
Normal file
40
detector/main.go
Normal file
@ -0,0 +1,40 @@
|
||||
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
|
||||
}
|
35
detector/main_test.go
Normal file
35
detector/main_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
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")
|
||||
|
||||
}
|
12
detector/patterns.go
Normal file
12
detector/patterns.go
Normal file
@ -0,0 +1,12 @@
|
||||
package detector
|
||||
|
||||
var patterns map[string][]string = map[string][]string{
|
||||
"miner": {
|
||||
`verus\-solve`,
|
||||
`hellminer`,
|
||||
},
|
||||
"bot": {
|
||||
`youtube\-dl`,
|
||||
`shiziyama`,
|
||||
},
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
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"`
|
||||
}
|
279
docker/types.go
279
docker/types.go
@ -1,279 +0,0 @@
|
||||
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
|
||||
}
|
975
glue/main.go
Normal file
975
glue/main.go
Normal file
@ -0,0 +1,975 @@
|
||||
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
|
||||
}
|
244
glue/main_test.go
Normal file
244
glue/main_test.go
Normal file
@ -0,0 +1,244 @@
|
||||
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) {
|
||||
|
||||
// }
|
132
glue/stats.go
Normal file
132
glue/stats.go
Normal file
@ -0,0 +1,132 @@
|
||||
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
|
||||
}
|
18
glue/types.go
Normal file
18
glue/types.go
Normal file
@ -0,0 +1,18 @@
|
||||
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
35
go.mod
@ -3,26 +3,31 @@ module github.com/rosti-cz/node-api
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
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/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/docker/go-connections v0.4.0
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/getsentry/sentry-go v0.26.0
|
||||
github.com/gobuffalo/packr v1.30.1
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/jinzhu/gorm v1.9.14
|
||||
github.com/kelseyhightower/envconfig v1.4.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/labstack/echo/v4 v4.10.0
|
||||
github.com/minio/minio-go/v7 v7.0.14
|
||||
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/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/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.4.0
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
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
|
||||
)
|
||||
|
613
handlers.go
613
handlers.go
@ -2,16 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rosti-cz/node-api/apps"
|
||||
"github.com/rosti-cz/node-api/common"
|
||||
"github.com/rosti-cz/node-api/docker"
|
||||
"github.com/rosti-cz/node-api/node"
|
||||
"github.com/rosti-cz/node-api/glue"
|
||||
)
|
||||
|
||||
func homeHandler(c echo.Context) error {
|
||||
@ -21,16 +21,15 @@ func homeHandler(c echo.Context) error {
|
||||
}
|
||||
|
||||
func listAppsHandler(c echo.Context) error {
|
||||
err := gatherStates()
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
processor := glue.Processor{
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
}
|
||||
applications, err := processor.List()
|
||||
|
||||
applications, err := processor.List(false)
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -41,16 +40,17 @@ func listAppsHandler(c echo.Context) error {
|
||||
// Returns one app
|
||||
func getAppHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
fast := c.Param("fast")
|
||||
|
||||
err := updateState(name)
|
||||
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(),
|
||||
}
|
||||
app, err := processor.Get(name)
|
||||
app, err := processor.Get(fast == "1")
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -63,35 +63,33 @@ func getAppHandler(c echo.Context) error {
|
||||
func createAppHandler(c echo.Context) error {
|
||||
registerOnly := c.QueryParam("register_only") == "1"
|
||||
|
||||
app := apps.App{}
|
||||
err := c.Bind(&app)
|
||||
appTemplate := apps.App{}
|
||||
err := c.Bind(&appTemplate)
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
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)
|
||||
processor := glue.Processor{
|
||||
AppName: appTemplate.Name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
|
||||
if !registerOnly {
|
||||
container := docker.Container{
|
||||
App: &app,
|
||||
}
|
||||
|
||||
err = container.Create()
|
||||
if err != nil {
|
||||
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 {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
err = container.Start()
|
||||
if err != nil {
|
||||
} 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 {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
}
|
||||
@ -103,45 +101,24 @@ func createAppHandler(c echo.Context) error {
|
||||
func updateAppHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
app := apps.App{}
|
||||
err := c.Bind(&app)
|
||||
appTemplate := apps.App{}
|
||||
err := c.Bind(&appTemplate)
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
@ -152,31 +129,19 @@ func updateAppHandler(c echo.Context) error {
|
||||
func stopAppHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
app, err := processor.Get(name)
|
||||
err := processor.Stop()
|
||||
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"})
|
||||
}
|
||||
|
||||
@ -184,30 +149,15 @@ func stopAppHandler(c echo.Context) error {
|
||||
func startAppHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
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()
|
||||
err := processor.Start()
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -219,19 +169,15 @@ func startAppHandler(c echo.Context) error {
|
||||
func restartAppHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
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()
|
||||
err := processor.Restart()
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -249,19 +195,35 @@ func setPasswordHandler(c echo.Context) error {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
app, err := processor.Get(name)
|
||||
err = processor.SetPassword(password.Password)
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
container := docker.Container{
|
||||
App: app,
|
||||
}
|
||||
return c.JSON(http.StatusOK, Message{Message: "ok"})
|
||||
}
|
||||
|
||||
err = container.SetPassword(password.Password)
|
||||
// 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,
|
||||
}
|
||||
err := processor.ClearPassword()
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -273,24 +235,20 @@ func setPasswordHandler(c echo.Context) error {
|
||||
func setKeysHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
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")
|
||||
err = processor.UpdateKeys(string(body) + "\n")
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -301,73 +259,26 @@ func setKeysHandler(c echo.Context) error {
|
||||
func setServicesHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
quickServices := &QuickServices{}
|
||||
err := c.Bind(quickServices)
|
||||
tech := &Technology{}
|
||||
err := c.Bind(tech)
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
app, err := processor.Get(name)
|
||||
|
||||
err = processor.EnableTech(tech.Name, tech.Version)
|
||||
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"})
|
||||
}
|
||||
|
||||
@ -375,33 +286,15 @@ func setServicesHandler(c echo.Context) error {
|
||||
func rebuildAppHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
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()
|
||||
err := processor.Rebuild()
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -419,10 +312,15 @@ func addLabelHandler(c echo.Context) error {
|
||||
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
err = processor.AddLabel(name, label.Value)
|
||||
err = processor.AddLabel(label.Value)
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -440,10 +338,15 @@ func deleteLabelHandler(c echo.Context) error {
|
||||
return c.JSONPretty(http.StatusBadRequest, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
err = processor.RemoveLabel(name, label.Value)
|
||||
err = processor.RemoveLabel(label.Value)
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -456,36 +359,20 @@ func deleteLabelHandler(c echo.Context) error {
|
||||
func deleteAppHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
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,
|
||||
go func(name string) {
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
|
||||
status, err := container.Status()
|
||||
err := processor.Delete()
|
||||
if err != nil {
|
||||
log.Println("ERROR delete application problem: " + err.Error())
|
||||
|
||||
log.Printf("Deletion of application failed: %v", err)
|
||||
}
|
||||
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)
|
||||
}(name)
|
||||
|
||||
return c.JSON(http.StatusOK, Message{Message: "deleted"})
|
||||
}
|
||||
@ -495,9 +382,44 @@ 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 {
|
||||
node, err := node.GetNodeInfo()
|
||||
processor := glue.Processor{
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
NodeProcessor: &nodeProcessor,
|
||||
}
|
||||
node, err := processor.GetNode()
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -509,19 +431,15 @@ func getNodeInfoHandler(c echo.Context) error {
|
||||
func getAppProcessesHandler(c echo.Context) error {
|
||||
name := c.Param("name")
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
processor := glue.Processor{
|
||||
AppName: name,
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
}
|
||||
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()
|
||||
processes, err := processor.Processes()
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -534,7 +452,15 @@ func metricsHandler(c echo.Context) error {
|
||||
var metrics string
|
||||
|
||||
// Node indexes
|
||||
node, err := node.GetNodeInfo()
|
||||
processor := glue.Processor{
|
||||
DB: common.GetDBConnection(),
|
||||
DockerSock: config.DockerSocket,
|
||||
BindIPHTTP: config.AppsBindIPHTTP,
|
||||
BindIPSSH: config.AppsBindIPSSH,
|
||||
AppsPath: config.AppsPath,
|
||||
NodeProcessor: &nodeProcessor,
|
||||
}
|
||||
node, err := processor.GetNode()
|
||||
if err != nil {
|
||||
return c.JSONPretty(http.StatusInternalServerError, Message{Message: err.Error()}, JSONIndent)
|
||||
}
|
||||
@ -552,18 +478,135 @@ 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)
|
||||
|
||||
processor := apps.AppsProcessor{
|
||||
DB: common.GetDBConnection(),
|
||||
if elapsedMetric != -1 {
|
||||
metrics += fmt.Sprintf("rosti_node_stats_time_elapsed{hostname=\"%s\"} %f\n", hostname, float64(elapsedMetric)/1000000000)
|
||||
}
|
||||
apps, err := processor.List()
|
||||
|
||||
apps, err := processor.List(true)
|
||||
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)
|
||||
}
|
||||
|
1100
handlers_nats.go
1100
handlers_nats.go
File diff suppressed because it is too large
Load Diff
100
jsonmap/jsonmap.go
Normal file
100
jsonmap/jsonmap.go
Normal file
@ -0,0 +1,100 @@
|
||||
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
72
main.go
@ -5,10 +5,14 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentryecho "github.com/getsentry/sentry-go/echo"
|
||||
"github.com/labstack/echo/v4"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -17,6 +21,10 @@ 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
|
||||
@ -24,9 +32,18 @@ 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)
|
||||
}
|
||||
|
||||
@ -35,11 +52,35 @@ 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()
|
||||
@ -50,14 +91,24 @@ 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 := gatherStats()
|
||||
err := statsProcessor.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)
|
||||
}
|
||||
@ -66,8 +117,9 @@ func main() {
|
||||
// Node stats
|
||||
go func() {
|
||||
for {
|
||||
err := node.Log()
|
||||
err := nodeProcessor.Log()
|
||||
if err != nil {
|
||||
sentry.CaptureException(err)
|
||||
log.Println("NODE PERFORMANCE LOG ERROR:", err.Error())
|
||||
}
|
||||
time.Sleep(5 * time.Minute)
|
||||
@ -79,6 +131,7 @@ func main() {
|
||||
e.Renderer = t
|
||||
|
||||
e.Use(TokenMiddleware)
|
||||
e.Use(sentryecho.New(sentryecho.Options{}))
|
||||
|
||||
// NATS handling
|
||||
// admin.apps.ALIAS.events
|
||||
@ -121,6 +174,9 @@ 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)
|
||||
|
||||
@ -130,6 +186,9 @@ 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)
|
||||
|
||||
@ -139,6 +198,13 @@ 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)
|
||||
|
||||
|
35
node/load.go
35
node/load.go
@ -1,23 +1,33 @@
|
||||
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
|
||||
|
||||
func init() {
|
||||
db := common.GetDBConnection()
|
||||
db.AutoMigrate(PerformanceLog{})
|
||||
// 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
|
||||
}
|
||||
|
||||
// Log creates a record for all important metrics used as
|
||||
func Log() error {
|
||||
func (p *Processor) Log() error {
|
||||
performanceLog := PerformanceLog{}
|
||||
|
||||
// Load
|
||||
@ -45,8 +55,7 @@ func Log() error {
|
||||
performanceLog.DiskSpaceUsage = diskUsage.UsedPercent / 100.0
|
||||
|
||||
// Save
|
||||
db := common.GetDBConnection()
|
||||
err = db.Create(&performanceLog).Error
|
||||
err = p.DB.Create(&performanceLog).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -54,13 +63,13 @@ func Log() error {
|
||||
// and clean
|
||||
// we have to use this stupid approach because DELETE doesn't support ORDER BY and LIMIT
|
||||
toDeleteLogs := []PerformanceLog{}
|
||||
err = db.Order("id DESC").Limit("99").Offset(history).Find(&toDeleteLogs).Error
|
||||
err = p.DB.Order("id DESC").Limit("99").Offset(history).Find(&toDeleteLogs).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, toDeleteLog := range toDeleteLogs {
|
||||
err = db.Delete(&toDeleteLog).Error
|
||||
err = p.DB.Delete(&toDeleteLog).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -71,7 +80,7 @@ func 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 index() (*Node, error) {
|
||||
func (p *Processor) Index() (*Node, error) {
|
||||
node := Node{
|
||||
Index: 1.0,
|
||||
}
|
||||
@ -82,11 +91,9 @@ func index() (*Node, error) {
|
||||
return &node, err
|
||||
}
|
||||
|
||||
db := common.GetDBConnection()
|
||||
|
||||
logs := []PerformanceLog{}
|
||||
|
||||
err = db.Find(&logs).Error
|
||||
err = p.DB.Find(&logs).Error
|
||||
if err != nil {
|
||||
return &node, err
|
||||
}
|
||||
|
@ -1,7 +1 @@
|
||||
package node
|
||||
|
||||
// GetNodeInfo returns information about this node
|
||||
func GetNodeInfo() (*Node, error) {
|
||||
node, err := index()
|
||||
return node, err
|
||||
}
|
||||
|
106
stats.go
106
stats.go
@ -1,106 +0,0 @@
|
||||
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
|
||||
}
|
@ -5,7 +5,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Template struct
|
||||
|
45
tools.go
45
tools.go
@ -2,15 +2,10 @@ 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 {
|
||||
@ -54,43 +49,3 @@ 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")
|
||||
}
|
||||
|
18
types.go
18
types.go
@ -1,9 +1,8 @@
|
||||
package main
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Path where authorized keys are
|
||||
const sshPubKeysLocation = "/srv/.ssh/authorized_keys"
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// RequestMessage message
|
||||
type RequestMessage struct {
|
||||
@ -57,6 +56,17 @@ 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"`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user