diff --git a/Dockerfile b/Dockerfile index 67ff0b7..539c5a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,14 @@ -# from alpine -# RUN apk update && apk add --no-cache docker-cli -# COPY ./get_stats.sh /get_stats.sh - -# CMD [ "/bin/sh", "/get_stats.sh" ] -# # ENTRYPOINT ["tail", "-f", "/dev/null"] - -#### old sad sh version above here from golang:alpine3.14 - WORKDIR /go/src/app COPY ./sandman.go . -RUN go build sandman.go +RUN go build sandman.go + +FROM alpine:latest +RUN apk --no-cache add docker-cli +WORKDIR /root/ +COPY --from=0 /go/src/app/sandman ./ +ENV TIMEOUT=30 +ENV LABEL=sandman + CMD ["./sandman"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..91a5f44 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Sandman +Putting your containers to sleep + +*man me a sand* + +--- + +## How it works +### Lazy loading containers +monitor network traffic for active connections and recieved packets , +if traffic looks to be idle, your container stops +if it looks like you're trying to access a stopped container, it starts + +### Want to test it? +``` +$ docker-compose up -d --build +``` + +## TODO +- support multiple ports +- test on common services +- better time adjustment +- 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 \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 0140e13..95e80b0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,27 +1,24 @@ version: "3" services: - whoami: - container_name: whoami + sandman: + container_name: sandman build: . - # image: sandman environment: - - PORT=81 - - LABEL=sandman + - PORT=81 # TODO make this work with more than one port + - LABEL=sandman # sandman checks + - TIMEOUT=30 # number of seconds to let container idle ports: - 81:81 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - # command: tail -F asdf - # command: /bin/sh /get_stats.sh - whoami2: container_name: whoami2 image: containous/whoami command: --port 81 - network_mode: service:whoami + network_mode: service:sandman depends_on: - - whoami + - sandman # ports: # - 80:80 labels: diff --git a/get_stats.sh b/get_stats.sh deleted file mode 100644 index fd145a3..0000000 --- a/get_stats.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -while [ 1 ]; -do - echo "$(netstat -n | grep $PORT | wc -l) active connections on $PORT" - echo "tx packets: $(cat /sys/class/net/eth0/statistics/tx_packets)" - echo "rx packets: $(cat /sys/class/net/eth0/statistics/rx_packets)" - sleep 1; -done \ No newline at end of file diff --git a/sandman b/sandman deleted file mode 100755 index 1e88a9c..0000000 Binary files a/sandman and /dev/null differ diff --git a/sandman.go b/sandman.go index 949f905..2b5c402 100644 --- a/sandman.go +++ b/sandman.go @@ -5,65 +5,126 @@ import ( "os" "os/exec" "strconv" + "strings" "time" ) func main() { - label := os.Getenv("LABEL") - // port := os.Getenv("PORT") inactive_seconds := 0 - host := true // true will represent on and false will represent off - + // label := os.Getenv("LABEL") + inactive_timeout, err := strconv.Atoi(os.Getenv("TIMEOUT")) + check(err) + // tx + // port := os.Getenv("PORT") + rx_history := []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} for { - // if the host is turned on - if host { - 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() - if err != nil { - fmt.Println(err.Error()) - return - } - fmt.Println("active clients: ", string(out)) - active_clients, _ := strconv.Atoi(string(out)) - if active_clients == 0 { + // check if container is on + container_state_on := is_container_on() + + // get rx packets outside of the if bc we do it either way + rx, err := os.ReadFile("/sys/class/net/eth0/statistics/rx_packets") + check(err) + rx_packets, err := strconv.Atoi(strings.TrimSpace(string(rx))) + check(err) + rx_history = append(rx_history[1:], rx_packets) + + // if the container is running, see if it needs to be stopped + 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 + println(active_clients, "active clients") + println(rx_packets, "rx packets") + fmt.Printf("%v rx history\n\n", rx_history) + + if active_clients == 0 && rx_history[0]+10 > rx_history[9] { + // 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++ + println(inactive_seconds, "seconds without an active client") + if inactive_seconds > inactive_timeout { + stop_containers() + } } - println(inactive_seconds, "seconds without an active client") - if inactive_seconds > 10 { - stop_containers(label) - host = false + } else { + // if more than 10 rx in last 10 seconds, start the container + if rx_history[0]+10 < rx_history[9] { + start_containers() } } - if !host { - println("time to check net stats for attempted connections") - // do more checking here - // check around in here to make sure the host doesn't magically restart and we have to deal with those again - // really we should probably be checking that all the time anyways - // 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() - - } - time.Sleep(time.Second) } - - fmt.Println("hello world") - // cmd := exec.Command(app, arg0, arg1, arg2, arg3) - // stdout, err := cmd.Output() - - // if err != nil { - // fmt.Println(err.Error()) - // return - // } - - // Print the output - // fmt.Println(string(stdout)) } -func stop_containers(label string) { - println("stopping contianer") - +type container struct { + id string + state string } -func start_containers(label string) { - // docker ps -a --no-trunc --filter label="com.sandman.marker=sandman" +func newContainer(id string, state string) *container { + c := container{id: id} + c.state = state + return &c +} + +func get_containers() []container { + containers := []container{} + out, err := exec.Command("/bin/sh", "-c", "docker ps -a --no-trunc --filter label=\"com.sandman.marker=$LABEL\" --format \"{{.ID}} {{.State}}\"").Output() // todo make this handle multiple ports? + check(err) + fmt.Println(string(out)) + if strings.TrimSpace(string(out)) == "" { + return nil + } + lines := strings.Split(string(strings.TrimSpace(string(out))), "\n") + for _, s := range lines { + containers = append(containers, container{strings.Split(s, " ")[0], strings.Split(s, " ")[1]}) + } + return containers +} + +func is_container_on() bool { + containers := get_containers() + for _, c := range containers { + if c.state == "running" { + return true + } + } + return false +} + +func stop_containers() { + println("stopping container(s)") + containers := get_containers() + idString := "" + for _, c := range containers { + idString = idString + " " + c.id + } + + out, err := exec.Command("/bin/sh", "-c", "docker stop "+idString).Output() + check(err) + fmt.Println(string(out)) +} + +func start_containers() { + println("starting container(s)") + containers := get_containers() + idString := "" + for _, c := range containers { + idString = idString + " " + c.id + } + + out, err := exec.Command("/bin/sh", "-c", "docker start "+idString).Output() + check(err) + fmt.Println(string(out)) +} + +func check(err error) { + if err != nil { + panic(err) + } }