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.",
|
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.`,
|
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) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var err error
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
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 {
|
if err != nil {
|
||||||
logger.Sugar.Fatalf("cannot listen to docker events: %s", err)
|
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/cobra v1.8.0
|
||||||
github.com/spf13/viper v1.17.0
|
github.com/spf13/viper v1.17.0
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.design/x/lockfree v0.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
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/distribution/reference v0.5.0 // indirect
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/go-units v0.5.0 // 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 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
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/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/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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
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/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 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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/config"
|
||||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||||
|
"github.com/Neur0toxine/sshpoke/internal/model"
|
||||||
"github.com/docker/docker/api/types"
|
"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"
|
"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)
|
cli, err := client.NewClientWithOpts(config.DefaultConfig.Docker.Opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
output := make(chan events.Message)
|
output := make(chan model.ContainerEvent)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
eventSource, errSource := cli.Events(ctx, types.EventsOptions{})
|
eventSource, errSource := cli.Events(d.ctx, types.EventsOptions{
|
||||||
|
Filters: filters.NewArgs(filters.Arg("type", "container")),
|
||||||
|
})
|
||||||
select {
|
select {
|
||||||
case event := <-eventSource:
|
case event := <-eventSource:
|
||||||
logger.Sugar.Debugf("docker event: %#v", event)
|
eventType := model.TypeFromAction(event.Action)
|
||||||
output <- event
|
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:
|
case err := <-errSource:
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
logger.Sugar.Debug("stopping docker event listener...")
|
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