services configuration inside config file

This commit is contained in:
Pavel 2023-11-29 18:52:59 +03:00
parent 64fa306f8d
commit f169b7f137
13 changed files with 260 additions and 98 deletions

View File

@ -117,7 +117,7 @@ func runRestServer() {
func runDockerEventListener(ctx context.Context) {
var err error
docker.Default, err = docker.New(ctx)
docker.Default, err = docker.New(ctx, config.Default.Services, config.Default.DefaultServer)
if err != nil {
logger.Sugar.Fatalf("cannot connect to docker daemon: %s", err)
}

View File

@ -23,10 +23,31 @@ docker:
version: ~
# Default server to use if `sshpoke.server` is not specified in the target container labels.
default_server: mine
# Services configuration. These values will have higher priority than the container labels.
services:
# Container ID, one of container names or matcher regex. This value will be used to determine which container will
# receive the labels from 'params' section. Add / at the start and the end of the value to use it as regex.
# Examples:
# - name: a77eaa474cc634f0e4da4d4b72ebf71c03d1ae69329a07e42b830375e247e613 # Match by container ID (exact matcher).
# - name: my-container # Match by container name (exact matcher).
# - name: /my-container/ # Match by container name via regular expression (e.g. 'my-container_1' will also match).
- name: /service-web/
# Same params as container labels but without 'sshpoke.' prefix.
params:
# Enable sshpoke for service. Replaces 'sshpoke.enable' in container labels.
enable: true
# Specifies container network. Replaces 'sshpoke.network' in container labels.
network: service_default
# Specifies server which will be used. Replaces 'sshpoke.server' in container labels.
server: ssh-demo-sish
# Specifies container port to be shared. Replaces 'sshpoke.port' in container labels.
port: 80
# Specifies remote host to be used. Replaces 'sshpoke.remote_host' in container labels.
remote_host: remotehost
# Servers configuration.
servers:
# Server name.
- name: mine
- name: ssh-demo-sish
# Server driver. Each driver has its own set of params. Supported drivers: ssh, plugin, null.
driver: ssh
params:

View File

@ -4,6 +4,7 @@ import (
"net/http"
"path/filepath"
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
)
@ -15,9 +16,23 @@ type Config struct {
API API `mapstructure:"api" json:"api"`
Docker DockerConfig `mapstructure:"docker" json:"docker"`
DefaultServer string `mapstructure:"default_server" json:"default_server"`
Services []Service `mapstructure:"services" json:"services,omitempty"`
Servers []Server `mapstructure:"servers" json:"servers"`
}
type Service struct {
Name smarttypes.MatchableString `mapstructure:"name" json:"name"`
Params ServiceLabels `mapstructure:"params" json:"params"`
}
type ServiceLabels struct {
Enable smarttypes.BoolStr `mapstructure:"enable" json:"enable"`
Network string `mapstructure:"network" json:"network,omitempty"`
Server string `mapstructure:"server" json:"server,omitempty"`
Port string `mapstructure:"port" json:"port,omitempty"`
RemoteHost string `mapstructure:"remote_host" json:"remote_host,omitempty"`
}
type API struct {
Rest WebAPI `mapstructure:"rest" json:"rest"`
Plugin PluginAPI `mapstructure:"plugin" json:"plugin"`

View File

@ -8,6 +8,7 @@ import (
"github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
@ -16,21 +17,43 @@ import (
var Default *Docker
type Docker struct {
cli *client.Client
ctx context.Context
cli *client.Client
ctx context.Context
services map[smarttypes.MatchableString]config.ServiceLabels
defaultServer string
}
func New(ctx context.Context) (*Docker, error) {
func New(ctx context.Context, services []config.Service, defaultServer string) (*Docker, error) {
cli, err := client.NewClientWithOpts(config.Default.Docker.Opts)
if err != nil {
return nil, err
}
servicesMap := make(map[smarttypes.MatchableString]config.ServiceLabels)
for _, svc := range services {
servicesMap[svc.Name] = svc.Params
}
return &Docker{
cli: cli,
ctx: ctx,
cli: cli,
ctx: ctx,
services: servicesMap,
defaultServer: defaultServer,
}, nil
}
func (d *Docker) findServiceLabels(id string, names []string) *config.ServiceLabels {
if labels, ok := d.services[smarttypes.MatchableString(id)]; ok {
return &labels
}
for matcher, labels := range d.services {
for _, name := range names {
if matcher.Match(name) {
return &labels
}
}
}
return nil
}
func (d *Docker) Containers() map[string]dto.Container {
items, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
Filters: filters.NewArgs(filters.Arg("status", "running")),
@ -41,7 +64,7 @@ func (d *Docker) Containers() map[string]dto.Container {
}
containers := map[string]dto.Container{}
for _, item := range items {
container, ok := dockerContainerToInternal(item)
container, ok := dockerContainerToInternal(item, d.findServiceLabels(item.ID, item.Names), d.defaultServer)
if !ok {
continue
}
@ -58,7 +81,8 @@ func (d *Docker) GetContainer(id string, all bool) (dto.Container, bool) {
if err != nil || len(container) != 1 {
return dto.Container{}, false
}
converted, ok := dockerContainerToInternal(container[0])
converted, ok := dockerContainerToInternal(
container[0], d.findServiceLabels(container[0].ID, container[0].Names), d.defaultServer)
if !ok {
return dto.Container{}, false
}
@ -92,7 +116,8 @@ func (d *Docker) Listen() (chan dto.Event, error) {
"id", event.Actor.ID, "err", err)
continue
}
converted, ok := dockerContainerToInternal(container[0])
converted, ok := dockerContainerToInternal(container[0],
d.findServiceLabels(container[0].ID, container[0].Names), d.defaultServer)
if !ok {
continue
}

View File

@ -5,8 +5,10 @@ import (
"strconv"
"strings"
"github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/network"
@ -14,40 +16,59 @@ import (
)
type labelsConfig struct {
Enable boolStr `mapstructure:"sshpoke.enable"`
Network string `mapstructure:"sshpoke.network"`
Server string `mapstructure:"sshpoke.server"`
Port string `mapstructure:"sshpoke.port"`
RemoteHost string `mapstructure:"sshpoke.remote_host"`
Enable smarttypes.BoolStr `mapstructure:"sshpoke.enable"`
Network string `mapstructure:"sshpoke.network"`
Server string `mapstructure:"sshpoke.server"`
Port string `mapstructure:"sshpoke.port"`
RemoteHost string `mapstructure:"sshpoke.remote_host"`
}
type boolStr string
func (b boolStr) Bool() bool {
return string(b) == "true"
}
func actorEnabled(actor events.Actor) bool {
func actorEnabled(actor events.Actor) smarttypes.BoolStr {
label, ok := actor.Attributes["sshpoke.enable"]
if !ok {
return false
}
return boolStr(label).Bool()
return smarttypes.BoolFromStr(label)
}
func dockerContainerToInternal(container types.Container) (result dto.Container, ok bool) {
func populateLabelsFromConfig(labels *labelsConfig, config *config.ServiceLabels) {
if labels.Enable != config.Enable {
labels.Enable = config.Enable
}
if labels.Server != config.Server {
labels.Server = config.Server
}
if labels.Network != config.Network {
labels.Network = config.Network
}
if labels.Port != config.Port {
labels.Port = config.Port
}
if labels.RemoteHost != config.RemoteHost {
labels.RemoteHost = config.RemoteHost
}
}
func dockerContainerToInternal(
container types.Container, configLabels *config.ServiceLabels, defaultServer string) (result dto.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() {
if configLabels != nil {
populateLabelsFromConfig(&labels, configLabels)
}
if !labels.Enable {
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 defaultServer == "" {
logger.Sugar.Debugf("skipping container %s because 'sshpoke.server' is not defined", container.ID)
return result, false
}
labels.Server = defaultServer
}
if container.NetworkSettings == nil ||
container.NetworkSettings.Networks == nil ||

View File

@ -21,6 +21,7 @@ import (
"github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh"
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh/knownhosts"
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
"go.uber.org/zap"
)
@ -145,8 +146,8 @@ func (d *SSH) buildHostKeyCallback() ssh.HostKeyCallback {
return sshtun.FixedHostKeys(d.hostKeys)
}()
if d.params.Auth.Type == types.AuthTypeKey && d.params.Auth.Directory != "" && len(d.hostKeys) == 0 {
knownHostsPath := types.SmartPath(path.Join(string(d.params.Auth.Directory), KnownHostsFile))
resolvedPath, err := knownHostsPath.Resolve(false)
knownHostsPath := smarttypes.Path(path.Join(string(d.params.Auth.Directory), KnownHostsFile))
resolvedPath, err := knownHostsPath.File()
if err != nil {
return ssh.InsecureIgnoreHostKey()
}
@ -237,7 +238,7 @@ func (d *SSH) populateFromSSHConfig() {
if d.params.Auth.Directory == "" {
return
}
cfg, err := parseSSHConfig(types.SmartPath(path.Join(string(d.params.Auth.Directory), "config")))
cfg, err := parseSSHConfig(smarttypes.Path(path.Join(string(d.params.Auth.Directory), "config")))
if err != nil {
return
}
@ -255,7 +256,7 @@ func (d *SSH) populateFromSSHConfig() {
d.params.Auth.User = user
}
if keyfile, err := hostCfg.Get("IdentityFile"); err == nil && keyfile != "" {
resolvedKeyFile, err := types.SmartPath(keyfile).Resolve(false)
resolvedKeyFile, err := smarttypes.Path(keyfile).File()
if err == nil {
d.params.Auth.Type = types.AuthTypeKey
d.params.Auth.Keyfile = resolvedKeyFile
@ -354,7 +355,7 @@ func (d *SSH) authenticator() ssh.AuthMethod {
case types.AuthTypeKey:
if d.params.Auth.Keyfile != "" {
keyAuth, err := sshtun.AuthKeyFile(
types.SmartPath(path.Join(d.params.Auth.Directory.String(), d.params.Auth.Keyfile)))
smarttypes.Path(path.Join(d.params.Auth.Directory.String(), d.params.Auth.Keyfile)))
if err != nil {
return nil
}

View File

@ -5,7 +5,7 @@ import (
"os"
"strings"
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
"github.com/kevinburke/ssh_config"
)
@ -19,8 +19,8 @@ func (c *hostConfig) Get(key string) (string, error) {
return strings.TrimSpace(val), err
}
func parseSSHConfig(filePath types.SmartPath) (*ssh_config.Config, error) {
fileName, err := filePath.Resolve(false)
func parseSSHConfig(filePath smarttypes.Path) (*ssh_config.Config, error) {
fileName, err := filePath.File()
if err != nil {
return nil, err
}

View File

@ -7,11 +7,11 @@ import (
"path"
"strings"
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh"
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
)
func AuthKeyFile(keyFile types.SmartPath) (ssh.AuthMethod, error) {
func AuthKeyFile(keyFile smarttypes.Path) (ssh.AuthMethod, error) {
key, err := readKey(keyFile)
if err != nil {
return nil, err
@ -19,7 +19,7 @@ func AuthKeyFile(keyFile types.SmartPath) (ssh.AuthMethod, error) {
return ssh.PublicKeys(key), nil
}
func AuthKeyDir(keyDir types.SmartPath) (ssh.AuthMethod, error) {
func AuthKeyDir(keyDir smarttypes.Path) (ssh.AuthMethod, error) {
keys, err := readKeys(keyDir)
if err != nil {
return nil, err
@ -31,8 +31,8 @@ func AuthPassword(password string) ssh.AuthMethod {
return ssh.Password(password)
}
func readKeys(keyDir types.SmartPath) ([]ssh.Signer, error) {
dir, err := keyDir.Resolve(true)
func readKeys(keyDir smarttypes.Path) ([]ssh.Signer, error) {
dir, err := keyDir.Directory()
if err != nil {
return nil, fmt.Errorf("cannot parse keys: %s", err)
}
@ -62,7 +62,7 @@ func readKeys(keyDir types.SmartPath) ([]ssh.Signer, error) {
if info.Size() < 256 {
continue
}
key, err := readKey(types.SmartPath(path.Join(dir, entry.Name())))
key, err := readKey(smarttypes.Path(path.Join(dir, entry.Name())))
if err != nil {
continue
}
@ -74,8 +74,8 @@ func readKeys(keyDir types.SmartPath) ([]ssh.Signer, error) {
return keys, nil
}
func readKey(keyFile types.SmartPath) (ssh.Signer, error) {
fileName, err := keyFile.Resolve(false)
func readKey(keyFile smarttypes.Path) (ssh.Signer, error) {
fileName, err := keyFile.File()
if err != nil {
return nil, err
}

View File

@ -4,12 +4,12 @@ import (
"bytes"
"os"
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
"github.com/kevinburke/ssh_config"
)
func parseSSHConfig(filePath types.SmartPath) (*ssh_config.Config, error) {
fileName, err := filePath.Resolve(false)
func parseSSHConfig(filePath smarttypes.Path) (*ssh_config.Config, error) {
fileName, err := filePath.File()
if err != nil {
return nil, err
}

View File

@ -1,21 +1,12 @@
package types
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
)
var envMatcherRegExp = regexp.MustCompile(`\$[\w\d\_]`)
type (
AuthType string
SmartPath string
)
type AuthType string
const (
AuthTypePasswordless AuthType = "passwordless"
@ -24,46 +15,11 @@ const (
)
type Auth struct {
Type AuthType `mapstructure:"type" validate:"required,oneof=passwordless password key"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Directory SmartPath `mapstructure:"directory"`
Keyfile string `mapstructure:"keyfile"`
}
func (k SmartPath) Resolve(shouldBeDirectory bool) (result string, err error) {
result = strings.TrimSpace(string(k))
if strings.HasPrefix(result, "~/") {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
result = path.Join(homeDir, result[2:])
}
for _, match := range envMatcherRegExp.FindAllString(string(k), -1) {
envVar := match[1:]
if envVar == "" {
continue
}
envVar = os.Getenv(envVar)
result = strings.ReplaceAll(result, match, envVar)
}
result, err = filepath.Abs(result)
if err != nil {
return
}
stat, err := os.Stat(result)
if err != nil {
return
}
if !stat.IsDir() && shouldBeDirectory {
err = errors.New("is not a directory")
}
return
}
func (k SmartPath) String() string {
return string(k)
Type AuthType `mapstructure:"type" validate:"required,oneof=passwordless password key"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Directory smarttypes.Path `mapstructure:"directory"`
Keyfile string `mapstructure:"keyfile"`
}
func (a Auth) Validate() error {

View File

@ -0,0 +1,19 @@
package smarttypes
type BoolStr bool
func BoolFromStr(str string) BoolStr {
return str == "true" || str == "1"
}
func (b BoolStr) MarshalText() ([]byte, error) {
if b {
return []byte("true"), nil
}
return []byte("false"), nil
}
func (b *BoolStr) UnmarshalText(src []byte) error {
*b = BoolFromStr(string(src))
return nil
}

View File

@ -0,0 +1,57 @@
package smarttypes
import (
"errors"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
var envMatcherRegExp = regexp.MustCompile(`\$[\w\d\_]`)
type Path string
func (k Path) File() (string, error) {
return k.resolve(false)
}
func (k Path) Directory() (string, error) {
return k.resolve(true)
}
func (k Path) resolve(shouldBeDirectory bool) (result string, err error) {
result = strings.TrimSpace(string(k))
if strings.HasPrefix(result, "~/") {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
result = path.Join(homeDir, result[2:])
}
for _, match := range envMatcherRegExp.FindAllString(string(k), -1) {
envVar := match[1:]
if envVar == "" {
continue
}
envVar = os.Getenv(envVar)
result = strings.ReplaceAll(result, match, envVar)
}
result, err = filepath.Abs(result)
if err != nil {
return
}
stat, err := os.Stat(result)
if err != nil {
return
}
if !stat.IsDir() && shouldBeDirectory {
err = errors.New("is not a directory")
}
return
}
func (k Path) String() string {
return string(k)
}

View File

@ -0,0 +1,47 @@
package smarttypes
import (
"regexp"
)
type MatchableString string
func (m MatchableString) isExpr() bool {
if len(m) < 3 {
return false
}
if m[0] == '/' && m[len(m)-1] == '/' {
return true
}
return false
}
func (m MatchableString) expr() *regexp.Regexp {
return regexp.MustCompile(string(m[1 : len(m)-2]))
}
func (m MatchableString) Match(val string) bool {
if m.isExpr() {
return m.expr().Match([]byte(val))
}
return val == string(m)
}
func (m MatchableString) String() string {
return string(m)
}
func (m MatchableString) MarshalText() ([]byte, error) {
return []byte(m), nil
}
func (m *MatchableString) UnmarshalText(src []byte) error {
if len(src) > 2 && src[0] == '/' && src[len(src)-1] == '/' {
_, err := regexp.Compile(string(src[1 : len(src)-2]))
if err != nil {
return err
}
}
*m = MatchableString(src)
return nil
}