semi-alpha version
ci/woodpecker/push/woodpecker Pipeline failed Details

main
Johannes Bülow 2024-02-03 11:11:10 +01:00
parent 7ff1ec233d
commit 37f2801d53
Signed by untrusted user who does not match committer: jmb
GPG Key ID: B56971CF7B8F83A6
10 changed files with 99 additions and 41 deletions

View File

@ -10,3 +10,4 @@
5533801d-70e3-4c21-9942-82d20930c789 5533801d-70e3-4c21-9942-82d20930c789
5533801d-70e3-4c21-9942-82d20930c789 5533801d-70e3-4c21-9942-82d20930c789
5533801d-70e3-4c21-9942-82d20930c789 5533801d-70e3-4c21-9942-82d20930c789
5533801d-70e3-4c21-9942-82d20930c789

View File

@ -8,14 +8,18 @@ Users to use Webtop in their own, separate session. A good illustration is this:
To build the project, you can just use "make" To build the project, you can just use "make"
On Debian 12, you will need the following dependencies: On Debian 12, you will need the following dependencies:
``` ```sh
apt install -y git wget podman make gcc libgpgme-dev build-essential pkgconf pkgconf-bin libdevmapper-dev libbtrfs-dev apt install -y git wget podman make gcc libgpgme-dev build-essential pkgconf pkgconf-bin libdevmapper-dev libbtrfs-dev
``` ```
## Installing ## Installing
Executing `make install` will install and start podterminal as a systemd service, including an example config file. Executing `make install` will install and start podterminal as a systemd service, including an example config file.
## Running ## Usage
To run the built binary, just execute it as root. You will have to have Podman installed and its socket enabled. There is some basic Documentation in the config file to explain the usage of the keys. For more in-depth explanations
Currently you have to set the Image, port etc. directly in the Source Code, however that should be eventually moved into feel free to contact me, and I will add it to the documentation
a config file.
## Known Bugs
- Images need to be pulled first as root
- Not compatible with default KASM Docker images (needs http WSS access)
- reloading browser on startup might be necessary, especially if the target image is slow to start

View File

@ -24,3 +24,5 @@ ssl_cert: /etc/ssl/certs/ssl-cert-snakeoil.pem
ssl_cert_key: /etc/ssl/private/ssl-cert-snakeoil.key ssl_cert_key: /etc/ssl/private/ssl-cert-snakeoil.key
# Blocks accessing /files and /files/socket.io to avoid File transfer # Blocks accessing /files and /files/socket.io to avoid File transfer
block_filebrowser: false block_filebrowser: false
# Delete running containers when restarting podterminal
timeout_on_restart: true

View File

@ -25,6 +25,7 @@ func main() {
log.Println("Dropped Privileges") log.Println("Dropped Privileges")
g.Go(pods.GarbageCollector) g.Go(pods.GarbageCollector)
g.Go(pods.PullImage) g.Go(pods.PullImage)
g.Go(web.IdleSessionCleanup)
// prevent main thread from dying // prevent main thread from dying
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {

View File

@ -10,6 +10,8 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// CreateContainer connects to the Podman socket defined in the "Socket" variable in this scope
// and creates a new container using the Image defined in the config file
func CreateContainer() (string, error) { func CreateContainer() (string, error) {
image := viper.GetString("image") image := viper.GetString("image")
conn := Socket conn := Socket

View File

@ -1,10 +1,18 @@
package pods package pods
import ( import (
"log"
"time" "time"
"github.com/containers/podman/v4/pkg/bindings/volumes"
"github.com/spf13/viper"
) )
// GarbageCollector is a goroutine that cleans up old Containers
func GarbageCollector() error { func GarbageCollector() error {
if viper.GetBool("timeout_on_restart") {
timeoutExistingContainers()
}
for { for {
err := Cleanup() err := Cleanup()
if err != nil { if err != nil {
@ -13,3 +21,28 @@ func GarbageCollector() error {
time.Sleep(time.Minute * 10) time.Sleep(time.Minute * 10)
} }
} }
func timeoutExistingContainers() {
var oldContainers []string
for _, container := range containerList() {
oldContainers = append(oldContainers, container.ID)
}
OldContainers = append(OldContainers, oldContainers...)
log.Println("old Containers: ", oldContainers)
}
func pruneVolumes() error {
results, err := volumes.Prune(Socket, nil)
if err != nil {
return err
}
resultLen := len(results)
for i, result := range results {
log.Printf("[%d/%d] %s %d MB", i, resultLen, result.Id, result.Size/1024/1024)
}
return nil
}

View File

@ -3,7 +3,6 @@ package pods
import ( import (
"context" "context"
"log" "log"
"net"
"time" "time"
"github.com/containers/podman/v4/pkg/bindings" "github.com/containers/podman/v4/pkg/bindings"
@ -14,7 +13,6 @@ import (
) )
var Socket context.Context var Socket context.Context
var rawSocket net.Conn
var OldContainers []string var OldContainers []string
@ -27,19 +25,8 @@ func socketConnection() context.Context {
return conn return conn
} }
func rawConnection() net.Conn {
connection, err := net.Dial("unix", "unix:///run/podman/podman.sock")
if err != nil {
log.Println(
"Could not establish raw UNIX socket connection, certain features will not work properly",
)
}
return connection
}
func ConnectSocket() { func ConnectSocket() {
Socket = socketConnection() Socket = socketConnection()
rawSocket = rawConnection()
} }
func PullImage() error { func PullImage() error {
@ -64,22 +51,21 @@ func Cleanup() error {
containerList := containerList() containerList := containerList()
for _, container := range containerList { for _, container := range containerList {
now := time.Now() for _, ctid := range OldContainers {
maxAge := time.Second * time.Duration(viper.GetInt("maxAge")) if container.ID == ctid {
containerAge := now.Sub(container.Created) err := containers.Kill(Socket, container.ID, nil)
if containerAge > maxAge { if err != nil {
log.Println(err)
err := containers.Kill(Socket, container.ID, nil) return err
if err != nil { }
log.Println(err) _, err = containers.Remove(Socket, container.ID, nil)
return err if err != nil {
} log.Println(err)
_, err = containers.Remove(Socket, container.ID, nil) return err
if err != nil { }
log.Println(err)
return err
} }
} }
} }
return nil return nil
} }

View File

@ -26,6 +26,8 @@ func readConfigFile() {
viper.SetDefault("block_filebrowser", false) viper.SetDefault("block_filebrowser", false)
viper.SetDefault("session_key", sessionKey) viper.SetDefault("session_key", sessionKey)
viper.SetDefault("container_port", 3000) viper.SetDefault("container_port", 3000)
viper.SetDefault("container_protocol", "http")
viper.SetDefault("timeout_on_restart", true)
viper.SetDefault("envvars", viper.SetDefault("envvars",
map[string]string{ map[string]string{
"CUSTOM_USER": "user", "CUSTOM_USER": "user",

View File

@ -3,12 +3,14 @@ package web
import ( import (
"fmt" "fmt"
"log" "log"
"net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"time" "time"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/spf13/viper"
"git.jmbit.de/jmb/podterminal/pods" "git.jmbit.de/jmb/podterminal/pods"
) )
@ -27,6 +29,15 @@ func createReverseProxy(backendService string) (*httputil.ReverseProxy, error) {
request.SetURL(backendURL) request.SetURL(backendURL)
request.Out.Host = request.In.Host request.Out.Host = request.In.Host
}, },
ModifyResponse: func(response *http.Response) error {
if response.StatusCode == http.StatusBadGateway {
time.Sleep(time.Second)
response.StatusCode = 307
response.Header.Set("Location", "/")
}
return nil
},
} }
return proxy, err return proxy, err
@ -34,9 +45,10 @@ func createReverseProxy(backendService string) (*httputil.ReverseProxy, error) {
func containerProxy(c *gin.Context) { func containerProxy(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
session.Save() sessionID := ""
sessionID := sessions.Session.ID(session) if session.Get("ct") == nil && session.Get("ready") == nil {
if session.Get("ct") == nil { session.Set("ready", false)
session.Save()
log.Println("Creating Container for Session ", sessionID) log.Println("Creating Container for Session ", sessionID)
ct, err := pods.CreateContainer() ct, err := pods.CreateContainer()
session.Set("ct", ct) session.Set("ct", ct)
@ -67,7 +79,9 @@ func containerProxy(c *gin.Context) {
// Soft fail Skel // Soft fail Skel
// _ = pods.CopySkelToContainer(ct) // _ = pods.CopySkelToContainer(ct)
proxies[ct], err = createReverseProxy(fmt.Sprintf("http://%s:3000", ctip)) proxies[ct], err = createReverseProxy(
fmt.Sprintf("http://%s:%d", ctip, viper.GetInt("container_port")),
)
if err != nil { if err != nil {
c.HTML( c.HTML(
@ -81,7 +95,7 @@ func containerProxy(c *gin.Context) {
} }
session.Set("ready", true) session.Set("ready", true)
session.Save() session.Save()
c.Redirect(301, "/") c.Redirect(307, "/")
} else { } else {
sessionCT := session.Get("ct") sessionCT := session.Get("ct")
switch sessionCT.(type) { switch sessionCT.(type) {
@ -90,16 +104,25 @@ func containerProxy(c *gin.Context) {
default: default:
c.HTML(500, "Error", "Session Container ID is not a string") c.HTML(500, "Error", "Session Container ID is not a string")
session.Delete("ct") session.Delete("ct")
session.Delete("ready")
session.Save() session.Save()
c.Abort() c.Abort()
} }
if session.Get("ready") == nil { if session.Get("ready").(bool) == false {
time.Sleep(100 * time.Millisecond) time.Sleep(time.Second)
c.Redirect(301, "/") c.Redirect(307, "/")
} }
id := session.Get("ct").(string) id := session.Get("ct").(string)
proxy := proxies[id] proxy := proxies[id]
proxy.ServeHTTP(c.Writer, c.Request) if proxy != nil {
proxy.ServeHTTP(c.Writer, c.Request)
} else {
session.Delete("ct")
session.Delete("ready")
session.Save()
c.Abort()
}
} }
} }

View File

@ -1,6 +1,7 @@
package web package web
import ( import (
"log"
"time" "time"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -33,9 +34,12 @@ func deleteIdleSessions() {
tenMinutesAgo := -time.Duration(idleTimout) * time.Second tenMinutesAgo := -time.Duration(idleTimout) * time.Second
oldAge := time.Now().Add(tenMinutesAgo) oldAge := time.Now().Add(tenMinutesAgo)
for session, sessionData := range sessionLastAccess { for session, sessionData := range sessionLastAccess {
log.Printf("Session %s last connected at %s", session, sessionData.lastAccess.String())
if oldAge.After(*sessionData.lastAccess) { if oldAge.After(*sessionData.lastAccess) {
pods.DestroyContainer(session) pods.DestroyContainer(session)
invalidSessions = append(invalidSessions, sessionData.sessionID) invalidSessions = append(invalidSessions, sessionData.sessionID)
// Delete Proxy entry to avoid 502s
delete(proxies, session)
} }