Add the shared packages for the sharks stack
model, tunnel, wg, authtoken, netguard, docker and dotenv, split out of the original single repository so Hammerhead, Blacktip and Bamboo can live in their own repos and import them. README extended; LICENSE unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c7c2ae8f8d
commit
2259580adf
16 files changed
+1359
-1
No files matched your search
@@ -0,0 +1,116 @@
|
||||
// Package docker lists running containers through the Docker Engine API so agents
|
||||
// can offer them as targets. It speaks plain HTTP over the engine's socket, with
|
||||
// no dependency on the Docker SDK.
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.veillax.com/Veillax/reef/model"
|
||||
)
|
||||
|
||||
func host() string {
|
||||
if h := os.Getenv("DOCKER_HOST"); h != "" {
|
||||
return h
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return "npipe:////./pipe/docker_engine"
|
||||
}
|
||||
return "unix:///var/run/docker.sock"
|
||||
}
|
||||
|
||||
func dialer(h string) (func(ctx context.Context, network, addr string) (net.Conn, error), error) {
|
||||
u, err := url.Parse(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "unix":
|
||||
path := u.Path
|
||||
return func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "unix", path)
|
||||
}, nil
|
||||
case "tcp", "http":
|
||||
addr := u.Host
|
||||
return func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", addr)
|
||||
}, nil
|
||||
case "npipe":
|
||||
return dialPipe(strings.ReplaceAll(u.Path, "/", `\`)), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported DOCKER_HOST %q", h)
|
||||
}
|
||||
|
||||
// List returns the running containers, or an error if Docker isn't reachable.
|
||||
func List(ctx context.Context) ([]model.Container, error) {
|
||||
dial, err := dialer(host())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{DialContext: dial, DisableKeepAlives: true}}
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", "http://docker/containers/json", nil)
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("docker returned %d", resp.StatusCode)
|
||||
}
|
||||
var raw []struct {
|
||||
ID string `json:"Id"`
|
||||
Names []string `json:"Names"`
|
||||
Image string
|
||||
State string
|
||||
Ports []struct {
|
||||
PrivatePort int
|
||||
PublicPort int
|
||||
Type string
|
||||
}
|
||||
NetworkSettings struct {
|
||||
Networks map[string]struct{ IPAddress string }
|
||||
}
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := []model.Container{}
|
||||
for _, r := range raw {
|
||||
c := model.Container{ID: r.ID, Image: r.Image, State: r.State, Ports: []model.ContainerPort{}, Networks: map[string]string{}}
|
||||
if len(c.ID) > 12 {
|
||||
c.ID = c.ID[:12]
|
||||
}
|
||||
if len(r.Names) > 0 {
|
||||
c.Name = strings.TrimPrefix(r.Names[0], "/")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, p := range r.Ports {
|
||||
key := fmt.Sprintf("%d/%d/%s", p.PrivatePort, p.PublicPort, p.Type)
|
||||
if seen[key] {
|
||||
continue // docker lists IPv4 and IPv6 bindings separately
|
||||
}
|
||||
seen[key] = true
|
||||
c.Ports = append(c.Ports, model.ContainerPort{Private: p.PrivatePort, Public: p.PublicPort, Type: p.Type})
|
||||
}
|
||||
for n, v := range r.NetworkSettings.Networks {
|
||||
if v.IPAddress != "" {
|
||||
c.Networks[n] = v.IPAddress
|
||||
}
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListParsesContainers(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/containers/json" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(`[
|
||||
{"Id":"abcdef1234567890","Names":["/minecraft"],"Image":"itzg/minecraft-server","State":"running",
|
||||
"Ports":[{"PrivatePort":25565,"PublicPort":25565,"Type":"tcp"},{"PrivatePort":25565,"PublicPort":25565,"Type":"tcp"},{"PrivatePort":8123,"Type":"tcp"}],
|
||||
"NetworkSettings":{"Networks":{"bridge":{"IPAddress":"172.17.0.2"},"none":{"IPAddress":""}}}},
|
||||
{"Id":"1111","Names":["/db"],"Image":"postgres","State":"running","Ports":[],"NetworkSettings":{"Networks":{}}}
|
||||
]`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("DOCKER_HOST", "tcp://"+strings.TrimPrefix(srv.URL, "http://"))
|
||||
|
||||
got, err := List(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 2 || got[0].Name != "db" || got[1].Name != "minecraft" {
|
||||
t.Fatalf("unexpected list: %+v", got)
|
||||
}
|
||||
mc := got[1]
|
||||
if mc.ID != "abcdef123456" || len(mc.Ports) != 2 || mc.Ports[0].Public != 25565 || mc.Ports[1].Public != 0 {
|
||||
t.Errorf("ports/id wrong: %+v", mc)
|
||||
}
|
||||
if mc.Networks["bridge"] != "172.17.0.2" || len(mc.Networks) != 1 {
|
||||
t.Errorf("networks wrong: %+v", mc.Networks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUnreachable(t *testing.T) {
|
||||
t.Setenv("DOCKER_HOST", "tcp://127.0.0.1:1")
|
||||
if _, err := List(context.Background()); err == nil {
|
||||
t.Fatal("expected an error when docker is not running")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build !windows
|
||||
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
func dialPipe(string) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return func(context.Context, string, string) (net.Conn, error) {
|
||||
return nil, errors.New("named pipes are only available on Windows")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build windows
|
||||
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
)
|
||||
|
||||
func dialPipe(path string) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return winio.DialPipeContext(ctx, path)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user