add multi-port support

This commit is contained in:
vmorganp
2021-10-13 22:46:05 -07:00
parent 524b40dd44
commit 99dedcfccf
3 changed files with 193 additions and 140 deletions

View File

@@ -18,11 +18,32 @@ $ cd Lazytainer
$ docker-compose up $ docker-compose up
``` ```
### Or put in your docker compose
```
lazytainer:
container_name: lazytainer
build: .
environment:
- PORT=81,82 # comma separated list of ports...or just the one
- LABEL=lazytainer # value of com.lazytainer.marker for other containers that lazytainer checks
# - TIMEOUT=30 # OPTIONAL number of seconds to let container idle
# - RXHISTLENGTH=10 # OPTIONAL number of seconds to keep rx history, uptime is calculated as first item and last item from this and must have a gap of at least $MINPACKETTHRESH
# - MINPACKETTHRESH=10 # OPTIONAL number of packets that must be recieved to keepalive/start container
ports:
- 81:81
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
whoami1:
container_name: whoami1
image: containous/whoami
command: --port 81 # make this run on the port passed through on lazytainer
network_mode: service:lazytainer
depends_on:
- lazytainer # wait for lazytainer to start before starting
labels:
- "com.lazytainer.marker=lazytainer" # required label to make it work
```
## TODO ## TODO
- support multiple ports - improve logging - verbosity flags
- test on common services
- docker security probably? this really shouldn't be getting exposed except to forward traffic so idk probably firewall all non listed ports?
- improve logging
- inevitable bugfixes
- ???
- profit

View File

@@ -4,22 +4,33 @@ services:
container_name: lazytainer container_name: lazytainer
build: . build: .
environment: environment:
- PORT=81 # TODO make this work with more than one port - PORT=81,82 # comma separated list of ports...or just the one
- LABEL=lazytainer # value of com.lazytainer.marker for other containers that lazytainer checks - LABEL=lazytainer # value of com.lazytainer.marker for other containers that lazytainer checks
- TIMEOUT=30 # number of seconds to let container idle # - TIMEOUT=30 # OPTIONAL number of seconds to let container idle
# - RXHISTLENGTH=10 # OPTIONAL number of seconds to keep rx history, uptime is calculated as first item and last item from this and must have a gap of at least $MINPACKETTHRESH
# - MINPACKETTHRESH=10 # OPTIONAL number of packets that must be recieved to keepalive/start container
ports: ports:
- 81:81 - 81:81
- 82:82
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
whoami2: whoami1:
container_name: whoami2 container_name: whoami1
image: containous/whoami image: containous/whoami
command: --port 81 command: --port 81
network_mode: service:lazytainer network_mode: service:lazytainer
depends_on: depends_on:
- lazytainer - lazytainer
# ports: labels:
# - 80:80 - "com.lazytainer.marker=lazytainer"
whoami2:
container_name: whoami2
image: containous/whoami
command: --port 82
network_mode: service:lazytainer
depends_on:
- lazytainer
labels: labels:
- "com.lazytainer.marker=lazytainer" - "com.lazytainer.marker=lazytainer"

View File

@@ -1,172 +1,193 @@
package main package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
func main() { func main() {
inactive_seconds := 0 inactive_seconds := 0
// check if all of the necessary env vars are set, otherwise, use defaults // check if all of the necessary env vars are set, otherwise, use defaults
// how long a container is allowed to have no traffic before being stopped // how long a container is allowed to have no traffic before being stopped
inactive_timeout, err := strconv.Atoi(os.Getenv("TIMEOUT")) inactive_timeout, err := strconv.Atoi(os.Getenv("TIMEOUT"))
if check_env_fetch_recoverable(err) { if check_env_fetch_recoverable(err) {
fmt.Println("using default because env variable TIMEOUT not set ") fmt.Println("using default because env variable TIMEOUT not set ")
inactive_timeout = 30 inactive_timeout = 30
} }
// how many polls to keep around // how many polls to keep around
rx_history_length, err := strconv.Atoi(os.Getenv("RXHISTLENGTH")) rx_history_length, err := strconv.Atoi(os.Getenv("RXHISTLENGTH"))
if check_env_fetch_recoverable(err) { if check_env_fetch_recoverable(err) {
fmt.Println("using default because env variable RXHISTLENGTH not set ") fmt.Println("using default because env variable RXHISTLENGTH not set ")
rx_history_length = 10 rx_history_length = 10
} }
// number of packets required between first and last poll to keep container alive // number of packets required between first and last poll to keep container alive
min_packet_threshhold, err := strconv.Atoi(os.Getenv("MINPACKETTHRESH")) min_packet_threshhold, err := strconv.Atoi(os.Getenv("MINPACKETTHRESH"))
if check_env_fetch_recoverable(err) { if check_env_fetch_recoverable(err) {
fmt.Println("using default because env variable MINPACKETTHRESH not set ") fmt.Println("using default because env variable MINPACKETTHRESH not set ")
min_packet_threshhold = 10 min_packet_threshhold = 10
} }
// how many seconds to wait in between polls // ports to check for active connections
poll_rate, err := strconv.Atoi(os.Getenv("POLLRATE")) ports := os.Getenv("PORT")
if check_env_fetch_recoverable(err) { check(err)
fmt.Println("using default because env variable POLLRATE not set ") if ports == "" {
poll_rate = 1 panic("you must set a port for this to work")
} }
ports_arr := strings.Split(string(strings.TrimSpace(string(ports))), ",")
portsarg := ""
for i, port := range ports_arr {
if i == 0 {
portsarg += "'"
}
if i +1 < len(ports_arr){
portsarg += string(port)+"\\|"
rx_history := make([]int, rx_history_length) }else{
portsarg += string(port)+"'"
}
}
for {
// check if container is on
container_state_on := is_container_on()
// get rx packets outside of the if bc we do it either way // how many seconds to wait in between polls
rx, err := os.ReadFile("/sys/class/net/eth0/statistics/rx_packets") poll_rate, err := strconv.Atoi(os.Getenv("POLLRATE"))
check(err) if check_env_fetch_recoverable(err) {
rx_packets, err := strconv.Atoi(strings.TrimSpace(string(rx))) fmt.Println("using default because env variable POLLRATE not set ")
check(err) poll_rate = 1
rx_history = append(rx_history[1:], rx_packets) }
fmt.Println(rx_packets, "rx packets")
// if the container is running, see if it needs to be stopped rx_history := make([]int, rx_history_length)
if container_state_on {
// get active clients
out, err := exec.Command("/bin/sh", "-c", "netstat -n | grep ESTABLISHED | awk '{ print $4 }' | rev | cut -d: -f1| rev | grep -w \"$PORT\" | wc -l").Output() // todo make this handle multiple ports?
check(err)
active_clients, err := strconv.Atoi(strings.TrimSpace(string(out)))
check(err)
// log out the results for {
fmt.Println(active_clients, "active clients") // check if container is on
// println(rx_packets, "rx packets") container_state_on := is_container_on()
// fmt.Printf("%v rx history", rx_history)
if active_clients == 0 && rx_history[0]+min_packet_threshhold > rx_history[len(rx_history)-1] { // get rx packets outside of the if bc we do it either way
// count up if we have no active clients rx, err := os.ReadFile("/sys/class/net/eth0/statistics/rx_packets")
// if no clients are active and less than 10 packets recieved in the last 10 seconds check(err)
inactive_seconds++ rx_packets, err := strconv.Atoi(strings.TrimSpace(string(rx)))
fmt.Println(inactive_seconds, "seconds without an active client") check(err)
if inactive_seconds > inactive_timeout { rx_history = append(rx_history[1:], rx_packets)
stop_containers() fmt.Println(rx_packets, "rx packets")
}
} else { // if the container is running, see if it needs to be stopped
inactive_seconds = 0 if container_state_on {
} // get active clients
} else { out, err := exec.Command("/bin/sh", "-c", "netstat -n | grep ESTABLISHED | awk '{ print $4 }' | rev | cut -d: -f1| rev | grep "+portsarg+" | wc -l").Output() // todo make this handle multiple ports?
// if more than 10 rx in last 10 seconds, start the container check(err)
if rx_history[0]+min_packet_threshhold < rx_history[len(rx_history)-1] { active_clients, err := strconv.Atoi(strings.TrimSpace(string(out)))
inactive_seconds = 0 check(err)
start_containers()
} // log out the results
} fmt.Println(active_clients, "active clients")
fmt.Println("//////////////////////////////////////////////////////////////////////////////////") // println(rx_packets, "rx packets")
for i := 0; i < poll_rate; i++ { // fmt.Printf("%v rx history", rx_history)
time.Sleep(time.Second)
} if active_clients == 0 && rx_history[0]+min_packet_threshhold > rx_history[len(rx_history)-1] {
} // count up if we have no active clients
// if no clients are active and less than 10 packets recieved in the last 10 seconds
inactive_seconds++
fmt.Println(inactive_seconds, "seconds without an active client")
if inactive_seconds > inactive_timeout {
stop_containers()
}
} else {
inactive_seconds = 0
}
} else {
// if more than 10 rx in last 10 seconds, start the container
if rx_history[0]+min_packet_threshhold < rx_history[len(rx_history)-1] {
inactive_seconds = 0
start_containers()
}
}
fmt.Println("//////////////////////////////////////////////////////////////////////////////////")
for i := 0; i < poll_rate; i++ {
time.Sleep(time.Second)
}
}
} }
type container struct { type container struct {
id string id string
state string state string
} }
func newContainer(id string, state string) *container { func newContainer(id string, state string) *container {
c := container{id: id} c := container{id: id}
c.state = state c.state = state
return &c return &c
} }
func get_containers() []container { func get_containers() []container {
containers := []container{} containers := []container{}
out, err := exec.Command("/bin/sh", "-c", "docker ps -a --no-trunc --filter label=\"com.lazytainer.marker=$LABEL\" --format \"{{.ID}} {{.State}}\"").Output() // todo make this handle multiple ports? out, err := exec.Command("/bin/sh", "-c", "docker ps -a --no-trunc --filter label=\"com.lazytainer.marker=$LABEL\" --format \"{{.ID}} {{.State}}\"").Output() // todo make this handle multiple ports?
check(err) check(err)
fmt.Println(string(out)) fmt.Println(string(out))
if strings.TrimSpace(string(out)) == "" { if strings.TrimSpace(string(out)) == "" {
return nil return nil
} }
lines := strings.Split(string(strings.TrimSpace(string(out))), "\n") lines := strings.Split(string(strings.TrimSpace(string(out))), "\n")
for _, s := range lines { for _, s := range lines {
containers = append(containers, container{strings.Split(s, " ")[0], strings.Split(s, " ")[1]}) containers = append(containers, container{strings.Split(s, " ")[0], strings.Split(s, " ")[1]})
} }
return containers return containers
} }
func is_container_on() bool { func is_container_on() bool {
containers := get_containers() containers := get_containers()
for _, c := range containers { for _, c := range containers {
if c.state == "running" { if c.state == "running" {
return true return true
} }
} }
return false return false
} }
func stop_containers() { func stop_containers() {
fmt.Println("stopping container(s)") fmt.Println("stopping container(s)")
containers := get_containers() containers := get_containers()
idString := "" idString := ""
for _, c := range containers { for _, c := range containers {
idString = idString + " " + c.id idString = idString + " " + c.id
} }
out, err := exec.Command("/bin/sh", "-c", "docker stop "+idString).Output() out, err := exec.Command("/bin/sh", "-c", "docker stop "+idString).Output()
check(err) check(err)
fmt.Println(string(out)) fmt.Println(string(out))
} }
func start_containers() { func start_containers() {
fmt.Println("starting container(s)") fmt.Println("starting container(s)")
containers := get_containers() containers := get_containers()
idString := "" idString := ""
for _, c := range containers { for _, c := range containers {
idString = idString + " " + c.id idString = idString + " " + c.id
} }
out, err := exec.Command("/bin/sh", "-c", "docker start "+idString).Output() out, err := exec.Command("/bin/sh", "-c", "docker start "+idString).Output()
check(err) check(err)
fmt.Println(string(out)) fmt.Println(string(out))
} }
func check(err error) { func check(err error) {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
// panic(err) // panic(err)
} }
} }
func check_env_fetch_recoverable(err error) bool { func check_env_fetch_recoverable(err error) bool {
if err != nil { if err != nil {
if strings.Contains(err.Error(), "strconv.Atoi: parsing \"\": invalid syntax") { if strings.Contains(err.Error(), "strconv.Atoi: parsing \"\": invalid syntax") {
return true return true
} }
panic(err) panic(err)
} }
return false return false
} }