mirror of
https://github.com/bytedream/docker4ssh.git
synced 2025-06-27 01:40:32 +02:00
Initial commit
This commit is contained in:
12
server/docker/client.go
Normal file
12
server/docker/client.go
Normal file
@ -0,0 +1,12 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"docker4ssh/database"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Client *client.Client
|
||||
Database *database.Database
|
||||
Network Network
|
||||
}
|
637
server/docker/container.go
Normal file
637
server/docker/container.go
Normal file
@ -0,0 +1,637 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
c "docker4ssh/config"
|
||||
"docker4ssh/database"
|
||||
"docker4ssh/terminal"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func simpleContainerFromID(ctx context.Context, client *Client, config Config, containerID string) (*SimpleContainer, error) {
|
||||
inspect, err := client.Client.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sc := &SimpleContainer{
|
||||
config: config,
|
||||
Image: Image{
|
||||
ref: inspect.Image,
|
||||
},
|
||||
ContainerID: containerID[:12],
|
||||
FullContainerID: containerID,
|
||||
client: client,
|
||||
cli: client.Client,
|
||||
}
|
||||
|
||||
sc.init(ctx)
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// newSimpleContainer creates a new container.
|
||||
// Currently, only for internal usage, may be changing in future
|
||||
func newSimpleContainer(ctx context.Context, client *Client, config Config, image Image, containerName string) (*SimpleContainer, error) {
|
||||
// create a new container from the given image and activate in- and output
|
||||
resp, err := client.Client.ContainerCreate(ctx, &container.Config{
|
||||
Image: image.Ref(),
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
Tty: true,
|
||||
AttachStdout: true,
|
||||
OpenStdin: true,
|
||||
}, nil, nil, nil, containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sc := &SimpleContainer{
|
||||
config: config,
|
||||
Image: image,
|
||||
ContainerID: resp.ID[:12],
|
||||
FullContainerID: resp.ID,
|
||||
client: client,
|
||||
cli: client.Client,
|
||||
}
|
||||
|
||||
sc.init(ctx)
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// SimpleContainer is the basic struct to control a docker4ssh container
|
||||
type SimpleContainer struct {
|
||||
config Config
|
||||
Image Image
|
||||
ContainerID string
|
||||
FullContainerID string
|
||||
|
||||
started bool
|
||||
|
||||
cancel context.CancelFunc
|
||||
|
||||
client *Client
|
||||
|
||||
// cli is just a shortcut for Client.Client
|
||||
cli *client.Client
|
||||
|
||||
Network struct {
|
||||
ID string
|
||||
IP string
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *SimpleContainer) init(ctx context.Context) {
|
||||
// disconnect from default docker network
|
||||
sc.cli.NetworkDisconnect(ctx, sc.client.Network[Host], sc.FullContainerID, true)
|
||||
}
|
||||
|
||||
// Start starts the container
|
||||
func (sc *SimpleContainer) Start(ctx context.Context) error {
|
||||
if err := sc.cli.ContainerStart(ctx, sc.FullContainerID, types.ContainerStartOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !sc.started {
|
||||
// initializes all settings.
|
||||
// as third argument is a pseudo empty used to
|
||||
// call every function in SimpleContainer.updateConfig.
|
||||
// for the same reason Config.Configurable and
|
||||
// Config.KeepOnExit are negated from their value in
|
||||
// sc.config
|
||||
if err := sc.updateConfig(ctx, Config{
|
||||
Configurable: !sc.config.Configurable,
|
||||
KeepOnExit: !sc.config.KeepOnExit,
|
||||
}, sc.config); err != nil {
|
||||
return err
|
||||
}
|
||||
sc.started = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the container
|
||||
func (sc *SimpleContainer) Stop(ctx context.Context) error {
|
||||
timeout := 0 * time.Second
|
||||
if err := sc.cli.ContainerStop(ctx, sc.FullContainerID, &timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !sc.config.KeepOnExit {
|
||||
if err := sc.cli.ContainerRemove(ctx, sc.FullContainerID, types.ContainerRemoveOptions{Force: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
// delete all references to the container in the database
|
||||
return sc.client.Database.Delete(sc.FullContainerID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *SimpleContainer) Running(ctx context.Context) (bool, error) {
|
||||
resp, err := sc.cli.ContainerInspect(ctx, sc.FullContainerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.State != nil && resp.State.Running, nil
|
||||
}
|
||||
|
||||
// WaitUntilStop waits until the container stops running
|
||||
func (sc *SimpleContainer) WaitUntilStop(ctx context.Context) error {
|
||||
statusChan, errChan := sc.cli.ContainerWait(ctx, sc.FullContainerID, container.WaitConditionNotRunning)
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case <-statusChan:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteConn executes a command in the container and returns the connection to the output
|
||||
func (sc *SimpleContainer) ExecuteConn(ctx context.Context, command string, args ...string) (net.Conn, error) {
|
||||
execID, err := sc.cli.ContainerExecCreate(ctx, sc.FullContainerID, types.ExecConfig{
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Cmd: append([]string{command}, args...),
|
||||
})
|
||||
resp, err := sc.cli.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Conn, err
|
||||
}
|
||||
|
||||
// Execute executes a command in the container and returns the response after finished
|
||||
func (sc *SimpleContainer) Execute(ctx context.Context, command string, args ...string) ([]byte, error) {
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
conn, err := sc.ExecuteConn(ctx, command, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
io.Copy(&buf, conn)
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// CopyFrom copies a file from the host system to the client.
|
||||
// Normal files and directories are accepted
|
||||
func (sc *SimpleContainer) CopyFrom(ctx context.Context, src, dst string) error {
|
||||
r, _, err := sc.cli.CopyFromContainer(ctx, sc.FullContainerID, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
tr := tar.NewReader(r)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
target := filepath.Join(dst, header.Name)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if _, err := os.Stat(target); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case tar.TypeReg:
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(f, tr); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = f.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CopyTo copies a file from the container to host.
|
||||
// Normal files and directories are accepted
|
||||
func (sc *SimpleContainer) CopyTo(ctx context.Context, src, dst string) error {
|
||||
stat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
err = filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := tar.FileInfoHeader(info, info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = strings.TrimPrefix(strings.TrimPrefix(path, src), "/")
|
||||
|
||||
// write every file to the container.
|
||||
// it might be better to write the file content to a buffer or
|
||||
// store the file pointer in a slice and write the buffer / stored
|
||||
// file pointer to the tar writer when every file was walked
|
||||
//
|
||||
// TODO: Test if the two described methods are better than sending every file on it's own
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
tw := tar.NewWriter(buf)
|
||||
if err = tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
defer tw.Close()
|
||||
|
||||
io.Copy(tw, file)
|
||||
|
||||
err = sc.cli.CopyToContainer(ctx, sc.FullContainerID, dst, buf, types.CopyToContainerOptions{
|
||||
AllowOverwriteDirWithFile: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
file, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Lstat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header, err := tar.FileInfoHeader(info, info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = filepath.Base(src)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tw := tar.NewWriter(buf)
|
||||
if err = tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
defer tw.Close()
|
||||
|
||||
_, _ = io.Copy(tw, file)
|
||||
|
||||
err = sc.cli.CopyToContainer(ctx, sc.FullContainerID, dst, buf, types.CopyToContainerOptions{
|
||||
AllowOverwriteDirWithFile: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config returns the current container config
|
||||
func (sc *SimpleContainer) Config() Config {
|
||||
return sc.config
|
||||
}
|
||||
|
||||
// UpdateConfig updates the container config
|
||||
func (sc *SimpleContainer) UpdateConfig(ctx context.Context, config Config) error {
|
||||
oldConfig := sc.config
|
||||
|
||||
if err := sc.updateConfig(ctx, oldConfig, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ocm, ncm, sm map[string]interface{}
|
||||
sm = make(map[string]interface{}, 0)
|
||||
|
||||
ocj, _ := json.Marshal(oldConfig)
|
||||
ncj, _ := json.Marshal(config)
|
||||
|
||||
json.Unmarshal(ocj, &ocm)
|
||||
json.Unmarshal(ncj, &ncm)
|
||||
|
||||
srt := reflect.TypeOf(database.Settings{})
|
||||
|
||||
for k, v := range ocm {
|
||||
newValue := ncm[k]
|
||||
if v != newValue && newValue != nil {
|
||||
field, ok := srt.FieldByName(k)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
sm[field.Tag.Get("json")] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// marshal the map into new settings
|
||||
var settings database.Settings
|
||||
body, _ := json.Marshal(sm)
|
||||
json.Unmarshal(body, &settings)
|
||||
|
||||
err := sc.client.Database.SetSettings(sc.FullContainerID, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.KeepOnExit {
|
||||
if _, ok := sc.client.Database.GetAuthByContainer(sc.FullContainerID); !ok {
|
||||
if err = sc.client.Database.SetAuth(sc.FullContainerID, database.Auth{
|
||||
User: &sc.ContainerID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
sc.config = config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *SimpleContainer) updateConfig(ctx context.Context, oldConfig, newConfig Config) error {
|
||||
if newConfig.NetworkMode != oldConfig.NetworkMode {
|
||||
if err := sc.setNetworkMode(ctx, oldConfig.NetworkMode, newConfig.NetworkMode, sc.client.Network != nil); err != nil {
|
||||
return err
|
||||
}
|
||||
zap.S().Debugf("Set network mode for %s to %s", sc.ContainerID, newConfig.NetworkMode.Name())
|
||||
}
|
||||
if newConfig.Configurable != oldConfig.Configurable {
|
||||
if err := sc.setConfigurable(ctx, newConfig.Configurable); err != nil {
|
||||
return err
|
||||
}
|
||||
zap.S().Debugf("Set configurable for %s to %t", sc.ContainerID, newConfig.Configurable)
|
||||
}
|
||||
if newConfig.ExitAfter != oldConfig.ExitAfter {
|
||||
sc.setExitAfterListener(ctx, newConfig.RunLevel, newConfig.ExitAfter)
|
||||
zap.S().Debugf("Set exit after listener for %s", sc.ContainerID)
|
||||
}
|
||||
|
||||
sc.config = newConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
// setNetworkMode changes the network mode for the container
|
||||
func (sc *SimpleContainer) setNetworkMode(ctx context.Context, oldMode, newMode NetworkMode, networking bool) error {
|
||||
var networkID string
|
||||
|
||||
if !networking {
|
||||
networkID = sc.client.Network[Off]
|
||||
} else {
|
||||
networkID = sc.client.Network[newMode]
|
||||
}
|
||||
|
||||
if networkID != "" {
|
||||
sc.cli.NetworkDisconnect(ctx, sc.client.Network[oldMode], sc.FullContainerID, true)
|
||||
// connect container to a network
|
||||
if err := sc.cli.NetworkConnect(ctx, networkID, sc.FullContainerID, &network.EndpointSettings{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// inspect the container to get its ip address (yes i was too lazy to implement
|
||||
// a service that generates the ips without docker)
|
||||
resp, err := sc.cli.ContainerInspect(ctx, sc.FullContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// update the internal network information
|
||||
sc.Network.ID = networkID
|
||||
sc.Network.IP = resp.NetworkSettings.Networks[newMode.NetworkName()].IPAddress
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *SimpleContainer) setConfigurable(ctx context.Context, configurable bool) error {
|
||||
cconfig := c.GetConfig()
|
||||
|
||||
if configurable {
|
||||
for srcFile, dstDir := range map[string]string{cconfig.Api.Configure.Binary: "/bin", cconfig.Api.Configure.Man: "/usr/share/man/man1"} {
|
||||
if err := sc.CopyTo(ctx, srcFile, dstDir); err != nil {
|
||||
if strings.HasSuffix(dstDir, "/man1") {
|
||||
// man files aren't that necessary, so if the copy fails it throws only a warning.
|
||||
// this error gets thrown when the container is alpine linux, for example.
|
||||
// it does not have a /usr/share/man/man1 directory and the copy fails
|
||||
// TODO: Create a directory if not existing to prevent this error
|
||||
zap.S().Warnf("Failed to copy %s to %s/%s for %s: %v", srcFile, dstDir, filepath.Base(srcFile), sc.ContainerID, err)
|
||||
continue
|
||||
} else {
|
||||
return fmt.Errorf("failed to copy %s to %s/%s for %s: %v", srcFile, dstDir, filepath.Base(srcFile), sc.ContainerID, err)
|
||||
}
|
||||
}
|
||||
zap.S().Debugf("Copied %s to %s (%s)", srcFile, filepath.Join(dstDir, filepath.Base(srcFile)), sc.ContainerID)
|
||||
}
|
||||
resp, err := sc.cli.ContainerInspect(ctx, sc.FullContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = sc.Execute(ctx, "sh", "-c", fmt.Sprintf("echo -n %s:%d > /etc/docker4ssh", resp.NetworkSettings.Networks[sc.config.NetworkMode.NetworkName()].Gateway, cconfig.Api.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zap.S().Debugf("Set ip and port of server for %s", sc.ContainerID)
|
||||
} else {
|
||||
_, err := sc.Execute(ctx, "rm",
|
||||
"-rf",
|
||||
fmt.Sprintf("/bin/%s", filepath.Base(cconfig.Api.Configure.Binary)),
|
||||
fmt.Sprintf("/usr/share/man/man1/%s", filepath.Base(cconfig.Api.Configure.Man)),
|
||||
"/etc/docker4ssh")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zap.S().Debugf("Removed all configurable related files from %s", sc.ContainerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setAPIRoute sets the IP and port for docker container tools
|
||||
func (sc *SimpleContainer) setAPIRoute(ctx context.Context, activate bool) error {
|
||||
var err error
|
||||
if activate {
|
||||
var resp types.ContainerJSON
|
||||
resp, err = sc.cli.ContainerInspect(ctx, sc.FullContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cconfig := c.GetConfig()
|
||||
if resp.NetworkSettings != nil {
|
||||
_, err = sc.Execute(ctx, "sh", "-c", fmt.Sprintf("echo -n %s:%d > /etc/docker4ssh", resp.NetworkSettings.Networks[sc.config.NetworkMode.NetworkName()].Gateway, cconfig.Api.Port))
|
||||
}
|
||||
} else {
|
||||
_, err = sc.Execute(ctx, "rm", "-rf", "/etc/docker4ssh")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// setExitAfterListener listens for exit after processes
|
||||
func (sc *SimpleContainer) setExitAfterListener(ctx context.Context, runlevel RunLevel, process string) {
|
||||
if sc.cancel != nil {
|
||||
sc.cancel()
|
||||
}
|
||||
|
||||
if process == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
sc.cancel = cancel
|
||||
|
||||
go func() {
|
||||
var rawPid []byte
|
||||
var err error
|
||||
|
||||
// check for the pid of Config.ExitAfter and wait 1 second if it wasn't found
|
||||
for {
|
||||
rawPid, err = sc.Execute(cancelCtx, "pidof", "-s", process)
|
||||
if len(rawPid) > 0 || err != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// sometimes garbage bytes are sent as well, they are getting filtered here
|
||||
var pid []byte
|
||||
for _, b := range rawPid {
|
||||
if b > '0' && b < '9' {
|
||||
pid = append(pid, b)
|
||||
}
|
||||
}
|
||||
|
||||
pid = bytes.TrimSuffix(pid, []byte("\n"))
|
||||
|
||||
if _, err = sc.Execute(cancelCtx, "sh", "-c", fmt.Sprintf("tail --pid=%s -f /dev/null", pid)); err != nil && cancelCtx.Err() == nil {
|
||||
zap.S().Errorf("Could not wait on process %s (%s) for %s", process, pid, sc.ContainerID)
|
||||
return
|
||||
}
|
||||
|
||||
if runlevel != Forever {
|
||||
sc.Stop(context.Background())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func InteractiveContainerFromID(ctx context.Context, client *Client, config Config, containerID string) (*InteractiveContainer, error) {
|
||||
sc, err := simpleContainerFromID(ctx, client, config, containerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InteractiveContainer{
|
||||
SimpleContainer: sc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewInteractiveContainer(ctx context.Context, cli *Client, config Config, image Image, containerName string) (*InteractiveContainer, error) {
|
||||
sc, err := newSimpleContainer(ctx, cli, config, image, containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InteractiveContainer{
|
||||
SimpleContainer: sc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type InteractiveContainer struct {
|
||||
*SimpleContainer
|
||||
|
||||
terminalCount int
|
||||
}
|
||||
|
||||
// TerminalCount returns the count of active terminals
|
||||
func (ic *InteractiveContainer) TerminalCount() int {
|
||||
return ic.terminalCount
|
||||
}
|
||||
|
||||
// Terminal creates a new interactive terminal session for the container
|
||||
func (ic *InteractiveContainer) Terminal(ctx context.Context, term *terminal.Terminal) error {
|
||||
// get the default shell for the root user
|
||||
rawShell, err := ic.Execute(ctx, "sh", "-c", "getent passwd root | cut -d : -f 7")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// here we cut out only newlines (which also could've been done via
|
||||
// bytes.ReplaceAll or strings.ReplaceAll) and redundant bytes
|
||||
// which sometimes get returned too and which cannot be interpreted
|
||||
// by the docker engine
|
||||
shell := bytes.Buffer{}
|
||||
for _, b := range rawShell {
|
||||
if b > ' ' {
|
||||
shell.WriteByte(b)
|
||||
}
|
||||
}
|
||||
|
||||
id, err := ic.cli.ContainerExecCreate(ctx, ic.FullContainerID, types.ExecConfig{
|
||||
Tty: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Cmd: []string{shell.String()},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := ic.cli.ContainerExecAttach(ctx, id.ID, types.ExecStartCheck{
|
||||
Tty: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
errChan := make(chan error)
|
||||
|
||||
go func() {
|
||||
// copy every input to the container
|
||||
if _, err = io.Copy(term, resp.Conn); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
errChan <- nil
|
||||
}()
|
||||
go func() {
|
||||
// copy every output from the container
|
||||
if _, err = io.Copy(resp.Conn, term); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
errChan <- nil
|
||||
}()
|
||||
|
||||
ic.terminalCount++
|
||||
select {
|
||||
case err = <-errChan:
|
||||
resp.Conn.Close()
|
||||
}
|
||||
ic.terminalCount--
|
||||
|
||||
return err
|
||||
}
|
120
server/docker/docker.go
Normal file
120
server/docker/docker.go
Normal file
@ -0,0 +1,120 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/client"
|
||||
"os"
|
||||
)
|
||||
|
||||
type NetworkMode int
|
||||
|
||||
const (
|
||||
Off NetworkMode = iota + 1
|
||||
|
||||
// Isolate isolates the container from the host and the host's
|
||||
// network. Therefore, no configurations can be changed from
|
||||
// within the container
|
||||
Isolate
|
||||
|
||||
// Host is the default docker networking configuration
|
||||
Host
|
||||
|
||||
// Docker is the same configuration you get when you start a
|
||||
// container via the command line
|
||||
Docker
|
||||
|
||||
// None disables all isolation between the docker container
|
||||
// and the host, so inside the network the container can act
|
||||
// as the host. So it has access to the host's network directly
|
||||
None
|
||||
)
|
||||
|
||||
func (nm NetworkMode) Name() string {
|
||||
switch nm {
|
||||
case Off:
|
||||
return "Off"
|
||||
case Isolate:
|
||||
return "Iso"
|
||||
case Host:
|
||||
return "Host"
|
||||
case Docker:
|
||||
return "Docker"
|
||||
case None:
|
||||
return "None"
|
||||
}
|
||||
return "invalid network"
|
||||
}
|
||||
|
||||
func (nm NetworkMode) NetworkName() string {
|
||||
switch nm {
|
||||
case Off:
|
||||
return "none"
|
||||
case Isolate:
|
||||
return "docker4ssh-full"
|
||||
case Host:
|
||||
return "bridge"
|
||||
case Docker:
|
||||
return "docker4ssh-def"
|
||||
case None:
|
||||
return "none"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RunLevel int
|
||||
|
||||
const (
|
||||
User RunLevel = iota + 1
|
||||
Container
|
||||
Forever
|
||||
)
|
||||
|
||||
func (rl RunLevel) Name() string {
|
||||
switch rl {
|
||||
case User:
|
||||
return "User"
|
||||
case Container:
|
||||
return "Container"
|
||||
case Forever:
|
||||
return "Forever"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// NetworkMode describes the level of isolation of the container to the host system.
|
||||
// Mostly changes the network of the container, see NetworkNames for more details
|
||||
NetworkMode NetworkMode
|
||||
|
||||
// If Configurable is true, the container can change settings for itself
|
||||
Configurable bool
|
||||
|
||||
// RunLevel describes the container behavior.
|
||||
// If the RunLevel is User, the container will exit when the user disconnects.
|
||||
// If the RunLevel is Container, the container keeps running if the user disconnects
|
||||
// and ExitAfter is specified and the specified process has not finished yet.
|
||||
// If the RunLevel is Forever, the container keeps running forever unless ExitAfter
|
||||
// is specified and the specified process ends.
|
||||
//
|
||||
// Note: It also automatically exits if ExitAfter is specified and the specified
|
||||
// process ends, even if the user is still connected to the container
|
||||
RunLevel RunLevel
|
||||
|
||||
// StartupInformation defines if information about the container like its (shorthand)
|
||||
// container id, NetworkMode, RunLevel, etc. should be shown when connecting to it
|
||||
StartupInformation bool
|
||||
|
||||
// ExitAfter contains a process name after which end the container should stop
|
||||
ExitAfter string
|
||||
|
||||
// When KeepOnExit is true, the container won't get deleted if it stops working
|
||||
KeepOnExit bool
|
||||
}
|
||||
|
||||
func InitCli() (*client.Client, error) {
|
||||
return client.NewClientWithOpts()
|
||||
}
|
||||
|
||||
func IsRunning() bool {
|
||||
_, err := os.Stat("/var/run/docker.sock")
|
||||
return !os.IsNotExist(err)
|
||||
}
|
41
server/docker/image.go
Normal file
41
server/docker/image.go
Normal file
@ -0,0 +1,41 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
ref string
|
||||
}
|
||||
|
||||
func (i Image) Ref() string {
|
||||
return i.ref
|
||||
}
|
||||
|
||||
// NewImage creates a new Image instance
|
||||
func NewImage(ctx context.Context, cli *client.Client, ref string) (Image, io.ReadCloser, error) {
|
||||
summary, err := cli.ImageList(ctx, types.ImageListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("reference", ref)),
|
||||
})
|
||||
if err != nil {
|
||||
return Image{}, nil, err
|
||||
}
|
||||
|
||||
if len(summary) > 0 {
|
||||
return Image{
|
||||
ref: ref,
|
||||
}, nil, nil
|
||||
} else {
|
||||
out, err := cli.ImagePull(ctx, ref, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return Image{}, nil, err
|
||||
}
|
||||
return Image{
|
||||
ref: ref,
|
||||
}, out, nil
|
||||
}
|
||||
}
|
86
server/docker/network.go
Normal file
86
server/docker/network.go
Normal file
@ -0,0 +1,86 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
c "docker4ssh/config"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type Network map[NetworkMode]string
|
||||
|
||||
// InitNetwork initializes a new docker4ssh network
|
||||
func InitNetwork(ctx context.Context, cli *client.Client, config *c.Config) (Network, error) {
|
||||
n := Network{}
|
||||
|
||||
networks, err := cli.NetworkList(ctx, types.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, dockerNetwork := range networks {
|
||||
var mode NetworkMode
|
||||
|
||||
switch dockerNetwork.Name {
|
||||
case "none":
|
||||
mode = Off
|
||||
case "docker4ssh-iso":
|
||||
mode = Isolate
|
||||
case "bridge":
|
||||
mode = Host
|
||||
case "docker4ssh-def":
|
||||
mode = Docker
|
||||
case "host":
|
||||
mode = None
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
n[mode] = dockerNetwork.ID
|
||||
}
|
||||
|
||||
if _, ok := n[Isolate]; !ok {
|
||||
// create a new network which isolates the container from the host,
|
||||
// but not from the network
|
||||
resp, err := cli.NetworkCreate(ctx, "docker4ssh-iso", types.NetworkCreate{
|
||||
CheckDuplicate: true,
|
||||
Driver: "bridge",
|
||||
EnableIPv6: false,
|
||||
IPAM: &network.IPAM{
|
||||
Driver: "default",
|
||||
Config: []network.IPAMConfig{
|
||||
{
|
||||
Subnet: config.Network.Isolate.Subnet,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n[Isolate] = resp.ID
|
||||
}
|
||||
|
||||
if _, ok := n[Docker]; !ok {
|
||||
// the standard network for all containers
|
||||
resp, err := cli.NetworkCreate(ctx, "docker4ssh-def", types.NetworkCreate{
|
||||
CheckDuplicate: true,
|
||||
Driver: "bridge",
|
||||
EnableIPv6: false,
|
||||
IPAM: &network.IPAM{
|
||||
Driver: "default",
|
||||
Config: []network.IPAMConfig{
|
||||
{
|
||||
Subnet: config.Network.Default.Subnet,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n[Docker] = resp.ID
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
Reference in New Issue
Block a user