Add testing to CI, add more utils and modify config paths
ci/woodpecker/push/build Pipeline was successful Details

This commit is contained in:
Melon 2022-11-07 00:42:10 +00:00
parent f9ab4323e6
commit dbe8c9d959
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
26 changed files with 466 additions and 62 deletions

46
.woodpecker/build.yml Normal file
View File

@ -0,0 +1,46 @@
platform: linux/amd64
pipeline:
format:
image: golang
commands:
- files=$(gofmt -l .) && echo "$files" && [ -z "$files" ]
test:
image: golang
commands:
- make test
build:
image: golang
commands:
- make build
prepare:
image: alpine
commands:
- mkdir release-out
- mkdir summer
- mv dist/* summer
archive:
image: joseluisq/drone-archive
settings:
format: tar
src_base_path: .
src: ./summer
dest: ./release-out/summer.tar.gz
checksum: true
checksum_algo: sha256
checksum_dest: release-out/summer.CHECKSUM.tar.gz.txt
publish:
image: plugins/gitea-release
settings:
api_key:
from_secret: release-token
base_url: https://code.mrmelon54.com
files: release-out/*
when:
event:
- tag

View File

@ -1,10 +1,14 @@
.PHONY: all test setup-docker restart-docker run-cli
SHELL := bash
VERSION = 0.0.1
HASH = $(shell git rev-parse --short HEAD)
COMMIT_DATE = $(shell date '+%Y-%m-%d %H:%M:%S')
VERSION := $(shell git describe --tags --dirty --always)
ifdef CI_BUILD_NUMBER
VERSION := "build.$(CI_BUILD_NUMBER)-$(VERSION)"
endif
BUILD_DATE = $(shell date '+%Y-%m-%d %H:%M:%S')
UTILS_PKG = code.mrmelon54.com/melon/summer/pkg/utils
LD_FLAGS = -s -w -X '${UTILS_PKG}.BuildVersion=${VERSION}' -X '${UTILS_PKG}.BuildCommit=${HASH}' -X '${UTILS_PKG}.BuildDate=${COMMIT_DATE}'
LD_FLAGS = -s -w -X '${UTILS_PKG}.BuildVersion=${VERSION}' -X '${UTILS_PKG}.BuildDate=${BUILD_DATE}'
CC = go
BUILD_DIR = $(shell pwd)
PROGRAMS = azalea buttercup marigold rose
@ -19,7 +23,7 @@ build:
"$(BUILD_DIR)/scripts/build.sh" "$(BUILD_DIR)" "$(CC)" "$(LD_FLAGS)" "$(PROGRAMS)" "$(TAGS)"
test:
$(CC) test ./...
$(CC) test ./... -tags TEST
setup-docker: build
"$(BUILD_DIR)/scripts/setup-docker.sh" "$(BUILD_DIR)" "$(PROGRAMS)"

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# Summer
[![status-badge](https://ci.mrmelon54.com/api/badges/melon/summer/status.svg)](https://ci.mrmelon54.com/melon/summer)
APIs and server side code for connecting multiple backend services.
| Program | Description |
|-----------|--------------------------------------------------------------------------------|
| Azalea | HTTP server with subdomain based internal proxying and API endpoint management |
| Buttercup | SSL certificate management and renewal |
| Lily | Process management and logging |
| Marigold | User authentication and management with OAuth application support |
| Rose | TCP and UDP forwarding server |
## Building
```bash
make build
```
## Development
```bash
# first time docker setup
debug=1 make setup-docker
# rebuild and restart docker containers
debug=1 make restart-docker
```

View File

@ -5,4 +5,4 @@ listen:
api: :7071
auth:
public: public.key.pem
apiDomain: api.summer-test
apiDomain: api.summer.test

View File

@ -1,7 +1,7 @@
package main
import (
"code.mrmelon54.com/melon/summer/cmd/azalea/servers"
"code.mrmelon54.com/melon/summer/pkg/api"
_ "embed"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
@ -22,13 +22,7 @@ func TestConfig(t *testing.T) {
Https: ":443",
Api: ":7071",
},
Auth: servers.ApiAuthConfig{
Public: "public.key.pem",
Perm: map[string]uint32{
"httpService": 3,
"httpRedirect": 4,
},
SudoPerm: []uint32{1, 2},
},
Auth: api.AuthConfig{Public: "public.key.pem"},
ApiDomain: "api.summer.test",
})
}

View File

@ -1,4 +1,4 @@
//go:build DEBUG
//go:build DEBUG || TEST
package quick_cert

View File

@ -3,14 +3,16 @@ package quick_cert
import (
"code.mrmelon54.com/melon/summer/pkg/tables/certificate"
"code.mrmelon54.com/melon/summer/pkg/utils"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
"testing"
"xorm.io/xorm"
)
func TestQuickCertDebug(t *testing.T) {
db, err := xorm.NewEngine("sqlite3", ":memory")
db, err := xorm.NewEngine("sqlite3", ":memory:")
assert.NoError(t, err)
assert.NoError(t, db.CreateTables(&certificate.Certificate{}, &certificate.CertificateData{}, &certificate.CertificateDomain{}))
QuickCert(db)
var c []certificate.Certificate

View File

@ -1,4 +1,4 @@
//go:build !DEBUG
//go:build !DEBUG && !TEST
package quick_cert

View File

@ -1,4 +1,4 @@
//go:build DEBUG
//go:build DEBUG || TEST
package pebble_dev

View File

@ -1,4 +1,4 @@
//go:build !DEBUG
//go:build !DEBUG && !TEST
package pebble_dev

51
cmd/lily/daemon/conn.go Normal file
View File

@ -0,0 +1,51 @@
package daemon
import (
"bufio"
"code.mrmelon54.com/melon/summer/pkg/utils"
"fmt"
"net"
"strings"
)
type LilyConn struct {
daemon *LilyDaemon
conn net.Conn
}
func HandleLilyConn(daemon *LilyDaemon, conn net.Conn) *LilyConn {
defer func(conn net.Conn) {
_ = conn.Close()
}(conn)
l := &LilyConn{daemon, conn}
go l.background()
return l
}
func (l *LilyConn) background() {
a := bufio.NewReader(l.conn)
for {
// read string until newline
line, err := a.ReadString('\n')
if err != nil {
return
}
// get first space to split off command
i := strings.IndexByte(line, ' ')
cmd := line[:i]
hasArgs := i > 0
if hasArgs {
args := utils.QuotedStringToArray(line[i+1:])
pairs := utils.ParseKeyValueFromStringArray(args)
l.daemon.RunCmd(cmd, pairs)
} else {
l.daemon.RunCmd(cmd, []utils.KeyValuePair{})
}
}
}
func (l *LilyConn) sendError(msg string) {
_, _ = fmt.Fprintln(l.conn, "ERROR", msg)
}

47
cmd/lily/daemon/daemon.go Normal file
View File

@ -0,0 +1,47 @@
package daemon
import (
"code.mrmelon54.com/melon/summer/pkg/utils"
"log"
"net"
)
type LilyDaemon struct {
sock net.Listener
}
func NewLilyDaemon(sock net.Listener) *LilyDaemon {
return &LilyDaemon{sock: sock}
}
func (l *LilyDaemon) Start() {
log.Println("[LilyDaemon] Starting watchers")
for {
conn, err := l.sock.Accept()
if err != nil {
log.Println("[LilyDaemon] Accept error:", err)
}
go HandleLilyConn(l, conn)
}
}
func (l *LilyDaemon) Destroy() {
_ = l.sock.Close()
}
func (l *LilyDaemon) RunCmd(cmd string, pairs []utils.KeyValuePair) {
switch cmd {
case "ADD":
log.Printf("ADD %#v\n", pairs)
case "REMOVE":
log.Printf("REMOVE %#v\n", pairs)
case "START":
log.Printf("START %#v\n", pairs)
case "STOP":
log.Printf("STOP %#v\n", pairs)
case "RESTART":
log.Printf("RESTART %#v\n", pairs)
case "LOG":
log.Printf("LOG %#v\n", pairs)
}
}

View File

@ -0,0 +1,3 @@
package daemon
// TODO: make a watcher, run and log the subprocess

73
cmd/lily/main.go Normal file
View File

@ -0,0 +1,73 @@
package main
import (
"code.mrmelon54.com/melon/summer/cmd/lily/daemon"
utils2 "code.mrmelon54.com/melon/summer/cmd/lily/utils"
"code.mrmelon54.com/melon/summer/pkg/utils"
"flag"
"fmt"
"github.com/juju/fslock"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
var isVersion, isDaemon bool
flag.BoolVar(&isVersion, "version", false, "Show program version")
flag.BoolVar(&isDaemon, "daemon", false, "Run daemon process")
flag.Parse()
if isVersion {
fmt.Printf("lily %s\n", utils.BuildVersion)
return
}
log.Printf("[Main] Starting up lily (%s from %s)\n", utils.BuildVersion, utils.BuildDate)
sharedPaths, err := utils2.NewSharedPaths()
if err != nil {
log.Fatalf("[Main] Failed to generate shared paths")
return
}
if isDaemon {
confLock := fslock.New(sharedPaths.GetConfLockPath())
err := confLock.TryLock()
if err != nil {
log.Fatal("[Main] Failed to acquire lock, is another daemon process running:", err)
}
if err := os.RemoveAll(sharedPaths.GetSockAddr()); err != nil {
log.Fatal("[Main] Failed to remove old unix socket:", err)
}
sock, err := net.Listen("unix", sharedPaths.GetSockAddr())
if err != nil {
log.Fatal("[Main] Failed to listen on unix socket:", err)
}
defer func(sock net.Listener) {
_ = sock.Close()
}(sock)
l := daemon.NewLilyDaemon(sock)
l.Start()
// Wait for exit signal
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
<-sc
fmt.Println()
// Stop runner
log.Printf("[Main] Stopping lily")
n := time.Now()
l.Destroy()
log.Printf("[Main] Took '%s' to shutdown\n", time.Now().Sub(n))
log.Println("[Main] Goodbye")
return
}
// Lily client
}

View File

@ -0,0 +1,39 @@
package utils
import (
"os"
"path"
)
type SharedPaths struct {
config, cache string
}
func NewSharedPaths() (s SharedPaths, err error) {
s.config, err = os.UserConfigDir()
if err != nil {
return
}
s.cache, err = os.UserCacheDir()
return
}
func (s SharedPaths) getConfPath() string {
return path.Join(s.config, "summer-lily")
}
func (s SharedPaths) GetConfLockPath() string {
return path.Join(s.getConfPath(), "config.lock")
}
func (s SharedPaths) getConfFilePath() string {
return path.Join(s.getConfPath(), "config.json")
}
func (s SharedPaths) getConfOldFilePath() string {
return path.Join(s.getConfPath(), "config.old.json")
}
func (s SharedPaths) GetSockAddr() string {
return path.Join(s.cache, "summer-lily.sock")
}

8
go.mod
View File

@ -1,6 +1,6 @@
module code.mrmelon54.com/melon/summer
go 1.18
go 1.19
require (
code.mrmelon54.com/melon/certgen v0.0.0-20220830133534-0fb4cb7e67d1
@ -11,7 +11,9 @@ require (
github.com/google/uuid v1.3.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/mrmelon54/favicon v0.0.0-20220830075604-72b3eafe69b9
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b
github.com/mattn/go-sqlite3 v1.14.9
github.com/mrmelon54/favicon v1.0.0
github.com/pkg/errors v0.9.1
github.com/sec51/twofactor v1.0.0
github.com/sethvargo/go-limiter v0.7.2
@ -24,7 +26,6 @@ require (
)
require (
code.mrmelon54.xyz/sean/png2ico v0.0.0-20220321230631-311127b42237 // indirect
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
github.com/adrg/strutil v0.2.2 // indirect
github.com/adrg/sysfont v0.1.2 // indirect
@ -44,6 +45,7 @@ require (
github.com/miekg/dns v1.1.47 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mrmelon54/png2ico v1.0.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sec51/convert v1.0.2 // indirect

10
go.sum
View File

@ -2,8 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.mrmelon54.com/melon/certgen v0.0.0-20220830133534-0fb4cb7e67d1 h1:tll8DwvO1CL+xXJIMLyDmQYoYr/gA4BkcUFtNHB1BFo=
code.mrmelon54.com/melon/certgen v0.0.0-20220830133534-0fb4cb7e67d1/go.mod h1:Liyhe1bkNyeVfw6LicCgrQ+4oUT/w/qONLjvejkUim0=
code.mrmelon54.xyz/sean/png2ico v0.0.0-20220321230631-311127b42237 h1:CIOWl5Xe64MAxdiqHy6kG52M2q3sYjN0qZWw5Z62600=
code.mrmelon54.xyz/sean/png2ico v0.0.0-20220321230631-311127b42237/go.mod h1:Dr9YQ0FwIMNC+GGRCahsp5IhB3BZkvyZbrtFni+3GDk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
fyne.io/fyne/v2 v2.1.2/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
@ -315,6 +313,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b h1:FQ7+9fxhyp82ks9vAuyPzG0/vVbWwMwLJ+P6yJI5FN8=
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b/go.mod h1:HMcgvsgd0Fjj4XXDkbjdmlbI505rUPBs6WBMYg2pXks=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
@ -383,8 +383,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mrmelon54/favicon v0.0.0-20220830075604-72b3eafe69b9 h1:37A6XlY1rNqbnD2ksPtHSHIUobNaFsBI1zDbuH/saFw=
github.com/mrmelon54/favicon v0.0.0-20220830075604-72b3eafe69b9/go.mod h1:11VXnD2+bF1vuLcw4K/2vCTz3atY0kf0kD5iA3Hop9s=
github.com/mrmelon54/favicon v1.0.0 h1:01e9jgulx1oLoR2SEA4vI0gti0w3Nhf10MukdJLLioI=
github.com/mrmelon54/favicon v1.0.0/go.mod h1:YrUKmXdn2dCmR21btYPJqRlkm1cUyoPE45s3nzDlUIE=
github.com/mrmelon54/png2ico v1.0.0 h1:YE20i0xao8rkuYaCq3Xj2hUkVkJ6xp412aGDMrGqufA=
github.com/mrmelon54/png2ico v1.0.0/go.mod h1:vp8Be9y5cz102ANon+BnsIzTUdet3VQRvOuWJTH9h0M=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=

View File

@ -0,0 +1,19 @@
//go:build DEBUG
package cli
import (
"os"
"path"
)
func getConfigPath(software string) string {
// path running locally
p := path.Join("cmd", software, "config.yml")
_, err := os.Stat(p)
if err == nil {
return p
}
// path inside docker container
return path.Join("/etc/melon-summer/conf", software+".yml")
}

11
pkg/cli/config-path.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !DEBUG
package cli
import (
"fmt"
)
func getConfigPath(software string) string {
return fmt.Sprintf("%s.conf.yml", software)
}

View File

@ -9,7 +9,6 @@ import (
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"xorm.io/xorm"
@ -20,10 +19,12 @@ func Run(software, dbString string, executor Executor) {
flag.BoolVar(&version, "version", false, "Show program version")
flag.Parse()
if version {
fmt.Println(utils.FullVersionString())
fmt.Printf("%s %s\n", software, utils.BuildVersion)
return
}
log.Printf("[Main] Starting up %s (%s from %s)\n", software, utils.BuildVersion, utils.BuildDate)
// Load database
engine, err := xorm.NewEngine("mysql", dbString)
utils.Check("Failed to connect to database", err)
@ -31,7 +32,6 @@ func Run(software, dbString string, executor Executor) {
// Start runner
runner := &Runner{software: software, executor: executor, Database: engine}
executor.Init(runner)
log.Printf("[Main] Starting up %s v%s #%s (%s)\n", software, utils.BuildVersion, utils.BuildCommit, utils.BuildDate)
// Wait for exit signal
sc := make(chan os.Signal, 1)
@ -48,12 +48,8 @@ func Run(software, dbString string, executor Executor) {
}
func DecodeConfig[T any](software string) (t T) {
f := filepath.Join("cmd", software, "config.yml")
f := getConfigPath(software)
open, err := os.Open(f)
if os.IsNotExist(err) {
f = filepath.Join("/etc/melon-summer/conf", software+".yml")
open, err = os.Open(f)
}
utils.Check("Failed to open config file", err)
decoder := yaml.NewDecoder(open)
utils.Check("Failed to decode config file", decoder.Decode(&t))

View File

@ -3,27 +3,4 @@ package utils
var (
BuildVersion = ""
BuildDate = ""
BuildCommit = ""
BuildDirty = false
)
//goland:noinspection GoBoolExpressions
func IsDebug() bool {
return BuildVersion == "" || BuildCommit == "" || BuildDate == ""
}
func FullVersionString() string {
return BuildVersion + "+" + getVersionMetadata()
}
//goland:noinspection GoBoolExpressions
func getVersionMetadata() string {
if BuildCommit != "" {
a := "rev." + BuildCommit
if BuildDirty {
a += "-dirty"
}
return a
}
return "unknown"
}

View File

@ -0,0 +1,24 @@
package utils
import "strings"
type KeyValuePair struct{ key, value string }
func ParseKeyValueString(text string) *KeyValuePair {
v := strings.SplitN(text, "=", 2)
if len(v) != 2 {
return nil
}
return &KeyValuePair{v[0], v[1]}
}
func ParseKeyValueFromStringArray(text []string) []KeyValuePair {
out := make([]KeyValuePair, 0)
for _, a := range text {
b := ParseKeyValueString(a)
if b != nil {
out = append(out, *b)
}
}
return out
}

View File

@ -0,0 +1,19 @@
package utils
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestParseKeyValueString(t *testing.T) {
assert.Nil(t, ParseKeyValueString("hello world"))
assert.Equal(t, &KeyValuePair{"hello", "world"}, ParseKeyValueString("hello=world"))
}
func TestParseKeyValueFromStringArray(t *testing.T) {
assert.Equal(t, []KeyValuePair{}, ParseKeyValueFromStringArray([]string{}))
assert.Equal(t, []KeyValuePair{}, ParseKeyValueFromStringArray([]string{"hello world"}))
assert.Equal(t, []KeyValuePair{{"hello", "world"}}, ParseKeyValueFromStringArray([]string{"hello=world"}))
assert.Equal(t, []KeyValuePair{{"hello", "world"}}, ParseKeyValueFromStringArray([]string{"hello=world", "test"}))
assert.Equal(t, []KeyValuePair{{"hello", "world"}, {"test", "1"}}, ParseKeyValueFromStringArray([]string{"hello=world", "test=1"}))
}

View File

@ -0,0 +1,36 @@
package utils
func QuotedStringToArray(text string) []string {
var escape, quoted bool
out := make([]string, 0)
var a string
runeArr := []rune(text)
for _, char := range runeArr {
if escape {
a += string(char)
escape = false
continue
}
switch char {
case ' ':
if quoted {
a += " "
} else if a != "" {
out = append(out, a)
a = ""
}
case '\\':
escape = true
case '"':
quoted = !quoted
default:
a += string(char)
}
}
if a != "" {
out = append(out, a)
a = ""
}
return out
}

View File

@ -0,0 +1,15 @@
package utils
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestQuotedStringToArray(t *testing.T) {
assert.Equal(t, []string{"hello", "world"}, QuotedStringToArray("hello world"))
assert.Equal(t, []string{"hello world"}, QuotedStringToArray("\"hello world\""))
assert.Equal(t, []string{"test", "hello world", "message"}, QuotedStringToArray("test \"hello world\" message"))
assert.Equal(t, []string{"test", "hello world \"with extra data\" inside", "message"}, QuotedStringToArray("test \"hello world \\\"with extra data\\\" inside\" message"))
assert.Equal(t, []string{"hello world"}, QuotedStringToArray("hello\\ world"))
assert.Equal(t, []string{"test", "hello world", "message"}, QuotedStringToArray("test hello\\ world message"))
}

View File

@ -1,9 +1,24 @@
#!/bin/bash
function countdown() {
for i in $(seq "$1" -1 1); do
echo -ne "\r$i "
sleep 1
done
}
PROGRAMS="$1"
echo "Restarting summer-mariadb..."
docker restart "summer-mariadb" >/dev/null
echo "Restarting summer-pebble..."
docker restart "summer-pebble" >/dev/null
if [ "$(docker inspect summer-mariadb | jq '.[0].State.Status' -r)" != "running" ]; then
echo "Restarting summer-mariadb..."
docker restart "summer-mariadb" >/dev/null
countdown 10
echo -e "\rFinished"
fi
if [ "$(docker inspect summer-pebble | jq '.[0].State.Status' -r)" != "running" ]; then
echo "Restarting summer-pebble..."
docker restart "summer-pebble" >/dev/null
countdown 10
echo -e "\rFinished"
fi
for PROG in ${PROGRAMS}; do
echo "Restarting ${PROG}..."
docker restart "${PROG}-test" >/dev/null