base code for docker events processing & container exposing on startup
This commit is contained in:
parent
214646b227
commit
1da76f397d
17
cmd/root.go
17
cmd/root.go
@ -21,8 +21,23 @@ var rootCmd = &cobra.Command{
|
||||
Short: "Expose your Docker services to the Internet via SSH.",
|
||||
Long: `sshpoke is a CLI application that listens to the docker socket and automatically exposes relevant services to the Internet.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
events, err := docker.ListenToEvents(ctx)
|
||||
docker.Default, err = docker.New(ctx)
|
||||
if err != nil {
|
||||
logger.Sugar.Fatalf("cannot connect to docker daemon: %s", err)
|
||||
}
|
||||
|
||||
for id, item := range docker.Default.Containers() {
|
||||
logger.Sugar.Debugw("registering container",
|
||||
"id", id,
|
||||
"ip", item.IP.String(),
|
||||
"port", item.Port,
|
||||
"server", item.Server,
|
||||
"domain", item.Domain)
|
||||
}
|
||||
|
||||
events, err := docker.Default.Listen()
|
||||
if err != nil {
|
||||
logger.Sugar.Fatalf("cannot listen to docker events: %s", err)
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@ -10,12 +10,10 @@ require (
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.design/x/lockfree v0.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/changkun/lockfree v0.0.1 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -43,8 +43,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/changkun/lockfree v0.0.1 h1:5WefVJLglY4IHRqOQmh6Ao6wkJYaJkarshKU8VUtId4=
|
||||
github.com/changkun/lockfree v0.0.1/go.mod h1:3bKiaXn/iNzIPlSvSOMSVbRQUQtAp8qUAyBUtzU11s4=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@ -227,8 +225,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.design/x/lockfree v0.0.1 h1:IHFNwZgM5bnZYWkEbzn5lWHMYr8WsRBdCJ/RBVY0xMM=
|
||||
golang.design/x/lockfree v0.0.1/go.mod h1:iaZUx6UgZaOdePjzI6wFd+seYMl1i0rsG8+xKvA8c4I=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -7,25 +7,101 @@ import (
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/config"
|
||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||
"github.com/Neur0toxine/sshpoke/internal/model"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func ListenToEvents(ctx context.Context) (chan events.Message, error) {
|
||||
var Default *Docker
|
||||
|
||||
type Docker struct {
|
||||
cli *client.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func New(ctx context.Context) (*Docker, error) {
|
||||
cli, err := client.NewClientWithOpts(config.DefaultConfig.Docker.Opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Docker{
|
||||
cli: cli,
|
||||
ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Docker) Containers() map[string]model.Container {
|
||||
items, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("status", "running")),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Sugar.Errorf("cannot get containers list: %s", err)
|
||||
return nil
|
||||
}
|
||||
containers := map[string]model.Container{}
|
||||
for _, item := range items {
|
||||
container, ok := dockerContainerToInternal(item)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
containers[item.ID] = container
|
||||
}
|
||||
return containers
|
||||
}
|
||||
|
||||
func (d *Docker) Listen() (chan model.ContainerEvent, error) {
|
||||
cli, err := client.NewClientWithOpts(config.DefaultConfig.Docker.Opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output := make(chan events.Message)
|
||||
output := make(chan model.ContainerEvent)
|
||||
go func() {
|
||||
for {
|
||||
eventSource, errSource := cli.Events(ctx, types.EventsOptions{})
|
||||
eventSource, errSource := cli.Events(d.ctx, types.EventsOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("type", "container")),
|
||||
})
|
||||
select {
|
||||
case event := <-eventSource:
|
||||
logger.Sugar.Debugf("docker event: %#v", event)
|
||||
output <- event
|
||||
eventType := model.TypeFromAction(event.Action)
|
||||
if (eventType != model.EventStart && eventType != model.EventStop) || !actorEnabled(event.Actor) {
|
||||
continue
|
||||
}
|
||||
if eventType == model.EventStop {
|
||||
logger.Sugar.Debugw("stopping session",
|
||||
"type", event.Action, "container.id", event.Actor.ID)
|
||||
output <- model.ContainerEvent{
|
||||
Type: eventType,
|
||||
ID: event.Actor.ID,
|
||||
}
|
||||
continue
|
||||
}
|
||||
container, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("id", event.Actor.ID)),
|
||||
})
|
||||
if err != nil || len(container) != 1 {
|
||||
logger.Sugar.Errorw("cannot get container info",
|
||||
"id", event.Actor.ID, "err", err)
|
||||
continue
|
||||
}
|
||||
converted, ok := dockerContainerToInternal(container[0])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
newEvent := model.ContainerEvent{
|
||||
Type: eventType,
|
||||
ID: event.Actor.ID,
|
||||
Container: converted,
|
||||
}
|
||||
logger.Sugar.Debugw("exposing container",
|
||||
"type", event.Action,
|
||||
"container.id", event.Actor.ID,
|
||||
"container.ip", converted.IP.String(),
|
||||
"container.port", converted.Port,
|
||||
"container.server", converted.Server,
|
||||
"container.domain", converted.Domain)
|
||||
output <- newEvent
|
||||
case err := <-errSource:
|
||||
if errors.Is(err, context.Canceled) {
|
||||
logger.Sugar.Debug("stopping docker event listener...")
|
||||
|
92
internal/docker/convert.go
Normal file
92
internal/docker/convert.go
Normal file
@ -0,0 +1,92 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||
"github.com/Neur0toxine/sshpoke/internal/model"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type labelsConfig struct {
|
||||
Enable boolStr `mapstructure:"sshpoke.enable"`
|
||||
Network string `mapstructure:"sshpoke.network"`
|
||||
Server string `mapstructure:"sshpoke.server"`
|
||||
Port string `mapstructure:"sshpoke.port"`
|
||||
Domain string `mapstructure:"sshpoke.domain"`
|
||||
}
|
||||
|
||||
type boolStr string
|
||||
|
||||
func (b boolStr) Bool() bool {
|
||||
return string(b) == "true"
|
||||
}
|
||||
|
||||
func actorEnabled(actor events.Actor) bool {
|
||||
label, ok := actor.Attributes["sshpoke.enable"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return boolStr(label).Bool()
|
||||
}
|
||||
|
||||
func dockerContainerToInternal(container types.Container) (result model.Container, ok bool) {
|
||||
var labels labelsConfig
|
||||
if err := mapstructure.Decode(container.Labels, &labels); err != nil {
|
||||
logger.Sugar.Debugf("skipping container %s because configuration is invalid: %s", container.ID, err)
|
||||
return result, false
|
||||
}
|
||||
if !labels.Enable.Bool() {
|
||||
logger.Sugar.Debugf("skipping container %s because sshpoke is not enabled for it", container.ID)
|
||||
return result, false
|
||||
}
|
||||
if labels.Server == "" {
|
||||
logger.Sugar.Debugf("skipping container %s because 'sshpoke.server' is not defined", container.ID)
|
||||
return result, false
|
||||
}
|
||||
if container.NetworkSettings == nil ||
|
||||
container.NetworkSettings.Networks == nil ||
|
||||
len(container.NetworkSettings.Networks) == 0 {
|
||||
logger.Sugar.Debugf("skipping container %s because network settings are empty", container.ID)
|
||||
return result, false
|
||||
}
|
||||
var ip net.IP
|
||||
if len(container.NetworkSettings.Networks) == 1 {
|
||||
ip = net.ParseIP(getKeyVal(container.NetworkSettings.Networks).IPAddress)
|
||||
} else {
|
||||
if labels.Network == "" {
|
||||
logger.Sugar.Debugf("container %s has %d networks and 'sshpoke.network' label is not specified",
|
||||
container.ID, len(container.NetworkSettings.Networks))
|
||||
return result, false
|
||||
}
|
||||
ntwrk, exists := container.NetworkSettings.Networks[labels.Network]
|
||||
if !exists {
|
||||
logger.Sugar.Debugf("container %s does not have network %s", container.ID, labels.Network)
|
||||
return result, false
|
||||
}
|
||||
ip = net.ParseIP(ntwrk.IPAddress)
|
||||
}
|
||||
port, err := strconv.Atoi(labels.Port)
|
||||
if err != nil {
|
||||
logger.Sugar.Debugf("skipping container %s because 'sshpoke.port' is not numeric", container.ID)
|
||||
return result, false
|
||||
}
|
||||
|
||||
return model.Container{
|
||||
IP: ip,
|
||||
Port: uint16(port),
|
||||
Server: labels.Server,
|
||||
Domain: labels.Domain,
|
||||
}, true
|
||||
}
|
||||
|
||||
func getKeyVal(m map[string]*network.EndpointSettings) *network.EndpointSettings {
|
||||
for _, v := range m {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
35
internal/model/container.go
Normal file
35
internal/model/container.go
Normal file
@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import "net"
|
||||
|
||||
type EventType uint8
|
||||
|
||||
const (
|
||||
EventStart EventType = iota
|
||||
EventStop
|
||||
EventUnknown
|
||||
)
|
||||
|
||||
func TypeFromAction(action string) EventType {
|
||||
switch action {
|
||||
case "start":
|
||||
return EventStart
|
||||
case "stop", "die":
|
||||
return EventStop
|
||||
default:
|
||||
return EventUnknown
|
||||
}
|
||||
}
|
||||
|
||||
type ContainerEvent struct {
|
||||
Type EventType
|
||||
ID string
|
||||
Container Container
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
IP net.IP
|
||||
Port uint16
|
||||
Server string
|
||||
Domain string
|
||||
}
|
Loading…
Reference in New Issue
Block a user