Commit 83187273 authored by Yann Hodique's avatar Yann Hodique Committed by GitHub

fix(kurtosis-devnet): cleanup server code (#13704)

Now that we have a fileserver as a kurtosis package, we don't need to
serve files locally. Therefore we don't need to discover how to access
them either.

Incidentally this should make the approach compatible with kurtosis
k8s backend, once we push docker images to a registry that k8s can
access.
parent 4cd07f52
...@@ -14,9 +14,7 @@ import ( ...@@ -14,9 +14,7 @@ import (
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/build" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/build"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/engine" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/engine"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/backend"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/serve"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/tmpl" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/tmpl"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/util" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/util"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
...@@ -24,6 +22,10 @@ import ( ...@@ -24,6 +22,10 @@ import (
const FILESERVER_PACKAGE = "fileserver" const FILESERVER_PACKAGE = "fileserver"
func fileserverURL(path ...string) string {
return fmt.Sprintf("http://%s/%s", FILESERVER_PACKAGE, strings.Join(path, "/"))
}
type config struct { type config struct {
templateFile string templateFile string
dataFile string dataFile string
...@@ -31,7 +33,6 @@ type config struct { ...@@ -31,7 +33,6 @@ type config struct {
enclave string enclave string
environment string environment string
dryRun bool dryRun bool
localHostName string
baseDir string baseDir string
kurtosisBinary string kurtosisBinary string
} }
...@@ -44,7 +45,6 @@ func newConfig(c *cli.Context) (*config, error) { ...@@ -44,7 +45,6 @@ func newConfig(c *cli.Context) (*config, error) {
enclave: c.String("enclave"), enclave: c.String("enclave"),
environment: c.String("environment"), environment: c.String("environment"),
dryRun: c.Bool("dry-run"), dryRun: c.Bool("dry-run"),
localHostName: c.String("local-hostname"),
kurtosisBinary: c.String("kurtosis-binary"), kurtosisBinary: c.String("kurtosis-binary"),
} }
...@@ -57,11 +57,6 @@ func newConfig(c *cli.Context) (*config, error) { ...@@ -57,11 +57,6 @@ func newConfig(c *cli.Context) (*config, error) {
return cfg, nil return cfg, nil
} }
type staticServer struct {
dir string
*serve.Server
}
type engineManager interface { type engineManager interface {
EnsureRunning() error EnsureRunning() error
} }
...@@ -72,34 +67,6 @@ type Main struct { ...@@ -72,34 +67,6 @@ type Main struct {
engineManager engineManager engineManager engineManager
} }
func (m *Main) launchStaticServer(ctx context.Context) (*staticServer, func(), error) {
// we will serve content from this tmpDir for the duration of the devnet creation
tmpDir, err := os.MkdirTemp("", m.cfg.enclave)
if err != nil {
return nil, nil, fmt.Errorf("error creating temporary directory: %w", err)
}
server := serve.NewServer(
serve.WithStaticDir(tmpDir),
serve.WithHostname(m.cfg.localHostName),
)
if err := server.Start(ctx); err != nil {
return nil, nil, fmt.Errorf("error starting server: %w", err)
}
return &staticServer{
dir: tmpDir,
Server: server,
}, func() {
if err := server.Stop(ctx); err != nil {
log.Printf("Error stopping server: %v\n", err)
}
if err := os.RemoveAll(tmpDir); err != nil {
log.Printf("Error removing temporary directory: %v\n", err)
}
}, nil
}
func (m *Main) localDockerImageOption() tmpl.TemplateContextOptions { func (m *Main) localDockerImageOption() tmpl.TemplateContextOptions {
dockerBuilder := build.NewDockerBuilder( dockerBuilder := build.NewDockerBuilder(
build.WithDockerBaseDir(m.cfg.baseDir), build.WithDockerBaseDir(m.cfg.baseDir),
...@@ -115,11 +82,12 @@ func (m *Main) localDockerImageOption() tmpl.TemplateContextOptions { ...@@ -115,11 +82,12 @@ func (m *Main) localDockerImageOption() tmpl.TemplateContextOptions {
}) })
} }
func (m *Main) localContractArtifactsOption(server *staticServer) tmpl.TemplateContextOptions { func (m *Main) localContractArtifactsOption(dir string) tmpl.TemplateContextOptions {
contractsBundle := fmt.Sprintf("contracts-bundle-%s.tar.gz", m.cfg.enclave) contractsBundle := fmt.Sprintf("contracts-bundle-%s.tar.gz", m.cfg.enclave)
contractsBundlePath := func(_ string) string { contractsBundlePath := func(_ string) string {
return filepath.Join(server.dir, contractsBundle) return filepath.Join(dir, contractsBundle)
} }
contractsURL := fileserverURL(contractsBundle)
contractBuilder := build.NewContractBuilder( contractBuilder := build.NewContractBuilder(
build.WithContractBaseDir(m.cfg.baseDir), build.WithContractBaseDir(m.cfg.baseDir),
...@@ -137,9 +105,8 @@ func (m *Main) localContractArtifactsOption(server *staticServer) tmpl.TemplateC ...@@ -137,9 +105,8 @@ func (m *Main) localContractArtifactsOption(server *staticServer) tmpl.TemplateC
} }
} }
url := fmt.Sprintf("%s/%s", server.URL(), contractsBundle) log.Printf("%s: contract artifacts available at: %s\n", layer, contractsURL)
log.Printf("%s: contract artifacts available at: %s\n", layer, url) return contractsURL, nil
return url, nil
}) })
} }
...@@ -148,27 +115,24 @@ type PrestateInfo struct { ...@@ -148,27 +115,24 @@ type PrestateInfo struct {
Hashes map[string]string `json:"hashes"` Hashes map[string]string `json:"hashes"`
} }
func (m *Main) localPrestateOption(server *staticServer) tmpl.TemplateContextOptions { func (m *Main) localPrestateOption(dir string) tmpl.TemplateContextOptions {
prestateBuilder := build.NewPrestateBuilder( prestateBuilder := build.NewPrestateBuilder(
build.WithPrestateBaseDir(m.cfg.baseDir), build.WithPrestateBaseDir(m.cfg.baseDir),
build.WithPrestateDryRun(m.cfg.dryRun), build.WithPrestateDryRun(m.cfg.dryRun),
) )
return tmpl.WithFunction("localPrestate", func() (*PrestateInfo, error) { return tmpl.WithFunction("localPrestate", func() (*PrestateInfo, error) {
prestatePath := []string{"proofs", "op-program", "cannon"}
prestateURL := fileserverURL(prestatePath...)
// Create build directory with the final path structure // Create build directory with the final path structure
buildDir := filepath.Join(server.dir, "proofs", "op-program", "cannon") buildDir := filepath.Join(append([]string{dir}, prestatePath...)...)
if err := os.MkdirAll(buildDir, 0755); err != nil { if err := os.MkdirAll(buildDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create prestate build directory: %w", err) return nil, fmt.Errorf("failed to create prestate build directory: %w", err)
} }
// Get the relative path from server.dir to buildDir for the URL
relPath, err := filepath.Rel(server.dir, buildDir)
if err != nil {
return nil, fmt.Errorf("failed to get relative path: %w", err)
}
info := &PrestateInfo{ info := &PrestateInfo{
URL: fmt.Sprintf("%s/%s", server.URL(), relPath), URL: prestateURL,
Hashes: make(map[string]string), Hashes: make(map[string]string),
} }
...@@ -219,7 +183,7 @@ func (m *Main) localPrestateOption(server *staticServer) tmpl.TemplateContextOpt ...@@ -219,7 +183,7 @@ func (m *Main) localPrestateOption(server *staticServer) tmpl.TemplateContextOpt
if err := os.Rename(filePath, hashedPath); err != nil { if err := os.Rename(filePath, hashedPath); err != nil {
return nil, fmt.Errorf("failed to rename prestate %s: %w", filepath.Base(filePath), err) return nil, fmt.Errorf("failed to rename prestate %s: %w", filepath.Base(filePath), err)
} }
log.Printf("%s available at: %s/%s/%s\n", filepath.Base(filePath), server.URL(), relPath, newFileName) log.Printf("%s available at: %s/%s\n", filepath.Base(filePath), prestateURL, newFileName)
// Rename the corresponding binary file // Rename the corresponding binary file
binFilePath := strings.Replace(strings.TrimSuffix(filePath, ".json"), "-proof", "", 1) + ".bin.gz" binFilePath := strings.Replace(strings.TrimSuffix(filePath, ".json"), "-proof", "", 1) + ".bin.gz"
...@@ -228,18 +192,18 @@ func (m *Main) localPrestateOption(server *staticServer) tmpl.TemplateContextOpt ...@@ -228,18 +192,18 @@ func (m *Main) localPrestateOption(server *staticServer) tmpl.TemplateContextOpt
if err := os.Rename(binFilePath, binHashedPath); err != nil { if err := os.Rename(binFilePath, binHashedPath); err != nil {
return nil, fmt.Errorf("failed to rename prestate %s: %w", filepath.Base(binFilePath), err) return nil, fmt.Errorf("failed to rename prestate %s: %w", filepath.Base(binFilePath), err)
} }
log.Printf("%s available at: %s/%s/%s\n", filepath.Base(binFilePath), server.URL(), relPath, newBinFileName) log.Printf("%s available at: %s/%s\n", filepath.Base(binFilePath), prestateURL, newBinFileName)
} }
return info, nil return info, nil
}) })
} }
func (m *Main) renderTemplate(server *staticServer) (*bytes.Buffer, error) { func (m *Main) renderTemplate(dir string) (*bytes.Buffer, error) {
opts := []tmpl.TemplateContextOptions{ opts := []tmpl.TemplateContextOptions{
m.localDockerImageOption(), m.localDockerImageOption(),
m.localContractArtifactsOption(server), m.localContractArtifactsOption(dir),
m.localPrestateOption(server), m.localPrestateOption(dir),
} }
// Read and parse the data file if provided // Read and parse the data file if provided
...@@ -391,19 +355,19 @@ func (m *Main) run() error { ...@@ -391,19 +355,19 @@ func (m *Main) run() error {
} }
} }
server, cleanup, err := m.launchStaticServer(ctx) tmpDir, err := os.MkdirTemp("", m.cfg.enclave)
if err != nil { if err != nil {
return fmt.Errorf("error launching static server: %w", err) return fmt.Errorf("error creating temporary directory: %w", err)
} }
defer cleanup() defer os.RemoveAll(tmpDir)
buf, err := m.renderTemplate(server) buf, err := m.renderTemplate(tmpDir)
if err != nil { if err != nil {
return fmt.Errorf("error rendering template: %w", err) return fmt.Errorf("error rendering template: %w", err)
} }
// TODO: clean up consumers of static server and replace with fileserver // TODO: clean up consumers of static server and replace with fileserver
err = m.deployFileserver(ctx, server.dir) err = m.deployFileserver(ctx, tmpDir)
if err != nil { if err != nil {
return fmt.Errorf("error deploying fileserver: %w", err) return fmt.Errorf("error deploying fileserver: %w", err)
} }
...@@ -455,11 +419,6 @@ func getFlags() []cli.Flag { ...@@ -455,11 +419,6 @@ func getFlags() []cli.Flag {
Name: "dry-run", Name: "dry-run",
Usage: "Dry run mode (optional)", Usage: "Dry run mode (optional)",
}, },
&cli.StringFlag{
Name: "local-hostname",
Usage: "DNS for localhost from Kurtosis perspective (optional)",
Value: backend.DefaultDockerHost(),
},
&cli.StringFlag{ &cli.StringFlag{
Name: "kurtosis-binary", Name: "kurtosis-binary",
Usage: "Path to kurtosis binary (optional)", Usage: "Path to kurtosis binary (optional)",
......
...@@ -61,12 +61,10 @@ func TestParseFlags(t *testing.T) { ...@@ -61,12 +61,10 @@ func TestParseFlags(t *testing.T) {
args: []string{ args: []string{
"--template", "path/to/template.yaml", "--template", "path/to/template.yaml",
"--enclave", "test-enclave", "--enclave", "test-enclave",
"--local-hostname", "test.local",
}, },
wantCfg: &config{ wantCfg: &config{
templateFile: "path/to/template.yaml", templateFile: "path/to/template.yaml",
enclave: "test-enclave", enclave: "test-enclave",
localHostName: "test.local",
kurtosisPackage: kurtosis.DefaultPackageName, kurtosisPackage: kurtosis.DefaultPackageName,
}, },
wantError: false, wantError: false,
...@@ -86,7 +84,6 @@ func TestParseFlags(t *testing.T) { ...@@ -86,7 +84,6 @@ func TestParseFlags(t *testing.T) {
wantCfg: &config{ wantCfg: &config{
templateFile: "path/to/template.yaml", templateFile: "path/to/template.yaml",
dataFile: "path/to/data.json", dataFile: "path/to/data.json",
localHostName: "host.docker.internal",
enclave: kurtosis.DefaultEnclave, enclave: kurtosis.DefaultEnclave,
kurtosisPackage: kurtosis.DefaultPackageName, kurtosisPackage: kurtosis.DefaultPackageName,
}, },
...@@ -118,7 +115,6 @@ func TestParseFlags(t *testing.T) { ...@@ -118,7 +115,6 @@ func TestParseFlags(t *testing.T) {
require.NotNil(t, cfg) require.NotNil(t, cfg)
assert.Equal(t, tt.wantCfg.templateFile, cfg.templateFile) assert.Equal(t, tt.wantCfg.templateFile, cfg.templateFile)
assert.Equal(t, tt.wantCfg.enclave, cfg.enclave) assert.Equal(t, tt.wantCfg.enclave, cfg.enclave)
assert.Equal(t, tt.wantCfg.localHostName, cfg.localHostName)
assert.Equal(t, tt.wantCfg.kurtosisPackage, cfg.kurtosisPackage) assert.Equal(t, tt.wantCfg.kurtosisPackage, cfg.kurtosisPackage)
if tt.wantCfg.dataFile != "" { if tt.wantCfg.dataFile != "" {
assert.Equal(t, tt.wantCfg.dataFile, cfg.dataFile) assert.Equal(t, tt.wantCfg.dataFile, cfg.dataFile)
...@@ -127,27 +123,6 @@ func TestParseFlags(t *testing.T) { ...@@ -127,27 +123,6 @@ func TestParseFlags(t *testing.T) {
} }
} }
func TestLaunchStaticServer(t *testing.T) {
cfg := &config{
localHostName: "test.local",
}
m := newTestMain(cfg)
ctx := context.Background()
server, cleanup, err := m.launchStaticServer(ctx)
require.NoError(t, err)
defer cleanup()
// Verify server properties
assert.NotEmpty(t, server.dir)
assert.DirExists(t, server.dir)
assert.NotNil(t, server.Server)
// Verify cleanup works
cleanup()
assert.NoDirExists(t, server.dir)
}
func TestRenderTemplate(t *testing.T) { func TestRenderTemplate(t *testing.T) {
// Create a temporary directory for test files // Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "template-test") tmpDir, err := os.MkdirTemp("", "template-test")
...@@ -178,18 +153,13 @@ artifacts: {{localContractArtifacts "l1"}}` ...@@ -178,18 +153,13 @@ artifacts: {{localContractArtifacts "l1"}}`
} }
m := newTestMain(cfg) m := newTestMain(cfg)
ctx := context.Background()
server, cleanup, err := m.launchStaticServer(ctx)
require.NoError(t, err)
defer cleanup()
buf, err := m.renderTemplate(server) buf, err := m.renderTemplate(tmpDir)
require.NoError(t, err) require.NoError(t, err)
// Verify template rendering // Verify template rendering
assert.Contains(t, buf.String(), "test-deployment") assert.Contains(t, buf.String(), "test-deployment")
assert.Contains(t, buf.String(), "test-project:test-enclave") assert.Contains(t, buf.String(), "test-project:test-enclave")
assert.Contains(t, buf.String(), server.URL())
} }
func TestDeploy(t *testing.T) { func TestDeploy(t *testing.T) {
...@@ -312,16 +282,16 @@ _prestate-build target: ...@@ -312,16 +282,16 @@ _prestate-build target:
} }
m := newTestMain(cfg) m := newTestMain(cfg)
ctx := context.Background()
server, cleanup, err := m.launchStaticServer(ctx) tmpDir, err := os.MkdirTemp("", "prestate-test")
require.NoError(t, err) require.NoError(t, err)
defer cleanup() defer os.RemoveAll(tmpDir)
// Create template context with just the prestate function // Create template context with just the prestate function
tmplCtx := tmpl.NewTemplateContext(m.localPrestateOption(server)) tmplCtx := tmpl.NewTemplateContext(m.localPrestateOption(tmpDir))
// Test template // Test template
template := `{"prestate": "{{localPrestate}}"}` template := `prestate_url: {{(localPrestate).URL}}`
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
err = tmplCtx.InstantiateTemplate(bytes.NewBufferString(template), buf) err = tmplCtx.InstantiateTemplate(bytes.NewBufferString(template), buf)
...@@ -331,19 +301,12 @@ _prestate-build target: ...@@ -331,19 +301,12 @@ _prestate-build target:
} }
require.NoError(t, err) require.NoError(t, err)
// Parse the output // Verify the output is valid YAML and contains the static path
var output struct { output := buf.String()
Prestate string `json:"prestate"` assert.Contains(t, output, "url: http://fileserver/proofs/op-program/cannon")
}
err = json.Unmarshal(buf.Bytes(), &output)
require.NoError(t, err)
// Verify the URL structure
assert.Contains(t, output.Prestate, server.URL())
assert.Contains(t, output.Prestate, "/proofs/op-program/cannon")
// Verify the directory was created // Verify the directory was created
prestateDir := filepath.Join(server.dir, "proofs", "op-program", "cannon") prestateDir := filepath.Join(tmpDir, "proofs", "op-program", "cannon")
assert.DirExists(t, prestateDir) assert.DirExists(t, prestateDir)
}) })
} }
......
package backend
import (
"net"
"net/url"
"strings"
)
// IPNetAddr is an interface that allows getting the underlying *net.IPNet
type IPNetAddr interface {
net.Addr
AsIPNet() *net.IPNet
}
// DockerFlavor interface and implementations
type DockerFlavor interface {
GetDockerHost() string
}
type DockerDesktop struct{}
func (d *DockerDesktop) GetDockerHost() string {
return "host.docker.internal"
}
type DockerVM struct {
ipAddress string
networkProvider networkProvider
}
func NewDockerVM(ipAddress string) *DockerVM {
return &DockerVM{
ipAddress: ipAddress,
networkProvider: defaultNetworkProvider{},
}
}
func (d *DockerVM) GetDockerHost() string {
vmIP := net.ParseIP(d.ipAddress)
if vmIP == nil {
return "localhost"
}
ifaces, err := d.networkProvider.Interfaces()
if err != nil {
return "localhost"
}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
for _, addr := range addrs {
ipNetAddr, ok := addr.(IPNetAddr)
if !ok {
continue
}
ipNet := ipNetAddr.AsIPNet()
// Skip loopback addresses
if ipNet.IP.IsLoopback() {
continue
}
// Check if this network contains the VM IP
if ipNet.Contains(vmIP) {
// Return our IP address on this interface
if localIP := ipNet.IP.To4(); localIP != nil {
return localIP.String()
}
}
}
}
return "localhost"
}
type DockerLocal struct {
networkProvider networkProvider
}
func NewDockerLocal() *DockerLocal {
return &DockerLocal{
networkProvider: defaultNetworkProvider{},
}
}
func (d *DockerLocal) GetDockerHost() string {
ifaces, err := d.networkProvider.Interfaces()
if err != nil {
return "localhost"
}
for _, iface := range ifaces {
if strings.HasPrefix(iface.Name(), "docker") {
addrs, err := iface.Addrs()
if err != nil || len(addrs) == 0 {
continue
}
// Get the first IP address
ipNetAddr, ok := addrs[0].(IPNetAddr)
if !ok {
continue
}
ipNet := ipNetAddr.AsIPNet()
return ipNet.IP.String()
}
}
return "localhost"
}
type DockerDetector struct {
envProvider envProvider
runtimeProvider runtimeProvider
}
func NewDockerDetector() *DockerDetector {
return &DockerDetector{
envProvider: defaultEnvProvider{},
runtimeProvider: defaultRuntimeProvider{},
}
}
func (d *DockerDetector) DockerFlavor() (DockerFlavor, error) {
// Check DOCKER_HOST environment variable first as it takes precedence
if dockerHost := d.envProvider.Getenv("DOCKER_HOST"); dockerHost != "" {
parsedURL, err := url.Parse(dockerHost)
if err != nil {
return nil, err
}
if d.runtimeProvider.GOOS() == "linux" && parsedURL.Scheme == "unix" {
return NewDockerLocal(), nil
}
if parsedURL.Scheme == "tcp" {
return NewDockerVM(parsedURL.Hostname()), nil
}
}
if d.runtimeProvider.GOOS() == "darwin" || d.runtimeProvider.GOOS() == "windows" {
// TODO: Add actual Docker version check here when needed
// For now, assume Docker Desktop as it's the most common case
return &DockerDesktop{}, nil
}
// On Linux, default to DockerLocal
return NewDockerLocal(), nil
}
func DefaultDockerHost() string {
detector := NewDockerDetector()
flavor, err := detector.DockerFlavor()
if err != nil {
return "localhost"
}
return flavor.GetDockerHost()
}
package backend
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Mock implementations
type mockInterface struct {
name string
addrs []net.Addr
}
func (m *mockInterface) Name() string {
return m.name
}
func (m *mockInterface) Addrs() ([]net.Addr, error) {
return m.addrs, nil
}
type mockNetworkProvider struct {
interfaces []Interface
}
func (m *mockNetworkProvider) Interfaces() ([]Interface, error) {
return m.interfaces, nil
}
type mockEnvProvider struct {
env map[string]string
}
func (m *mockEnvProvider) Getenv(key string) string {
return m.env[key]
}
type mockRuntimeProvider struct {
goos string
}
func (m *mockRuntimeProvider) GOOS() string {
return m.goos
}
// mockIPNet implements net.Addr and can be type asserted to *net.IPNet
type mockIPNet struct {
addr *net.IPNet
}
func newMockIPNet(cidr string) net.Addr {
ip, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
panic(err)
}
// Set the IP in the IPNet to be the specific IP, not the network address
ipNet.IP = ip
return &mockIPNet{addr: ipNet}
}
func (m *mockIPNet) Network() string { return "ip+net" }
func (m *mockIPNet) String() string { return m.addr.IP.String() }
// Make it possible to type assert to *net.IPNet
func (m *mockIPNet) AsIPNet() *net.IPNet {
return m.addr
}
func TestDockerDesktop(t *testing.T) {
flavor := &DockerDesktop{}
assert.Equal(t, "host.docker.internal", flavor.GetDockerHost())
}
func TestDockerVM(t *testing.T) {
tests := []struct {
name string
vmIP string
ifaces []Interface
expected string
}{
{
name: "matching interface found",
vmIP: "192.168.1.100",
ifaces: []Interface{
&mockInterface{
name: "eth0",
addrs: []net.Addr{newMockIPNet("192.168.1.5/24")},
},
},
expected: "192.168.1.5",
},
{
name: "no matching interface",
vmIP: "10.0.0.100",
ifaces: []Interface{
&mockInterface{
name: "eth0",
addrs: []net.Addr{newMockIPNet("192.168.1.5/24")},
},
},
expected: "localhost",
},
{
name: "invalid VM IP",
vmIP: "invalid-ip",
ifaces: []Interface{},
expected: "localhost",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
vm := NewDockerVM(tt.vmIP)
vm.networkProvider = &mockNetworkProvider{interfaces: tt.ifaces}
assert.Equal(t, tt.expected, vm.GetDockerHost())
})
}
}
func TestDockerLocal(t *testing.T) {
tests := []struct {
name string
ifaces []Interface
expected string
}{
{
name: "docker0 interface found",
ifaces: []Interface{
&mockInterface{
name: "docker0",
addrs: []net.Addr{newMockIPNet("172.17.0.1/16")},
},
},
expected: "172.17.0.1",
},
{
name: "docker1 interface found",
ifaces: []Interface{
&mockInterface{
name: "docker1",
addrs: []net.Addr{newMockIPNet("172.18.0.1/16")},
},
},
expected: "172.18.0.1",
},
{
name: "prefers first docker interface",
ifaces: []Interface{
&mockInterface{
name: "eth0",
addrs: []net.Addr{newMockIPNet("192.168.1.5/24")},
},
&mockInterface{
name: "docker0",
addrs: []net.Addr{newMockIPNet("172.17.0.1/16")},
},
&mockInterface{
name: "docker1",
addrs: []net.Addr{newMockIPNet("172.18.0.1/16")},
},
},
expected: "172.17.0.1",
},
{
name: "skips docker interface with no addresses",
ifaces: []Interface{
&mockInterface{
name: "docker0",
addrs: []net.Addr{},
},
&mockInterface{
name: "docker1",
addrs: []net.Addr{newMockIPNet("172.18.0.1/16")},
},
},
expected: "172.18.0.1",
},
{
name: "no docker interface",
ifaces: []Interface{
&mockInterface{
name: "eth0",
addrs: []net.Addr{newMockIPNet("192.168.1.5/24")},
},
},
expected: "localhost",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
local := NewDockerLocal()
local.networkProvider = &mockNetworkProvider{interfaces: tt.ifaces}
assert.Equal(t, tt.expected, local.GetDockerHost())
})
}
}
func TestDockerDetector(t *testing.T) {
tests := []struct {
name string
env map[string]string
goos string
expectType string
expectError bool
}{
{
name: "unix socket",
env: map[string]string{
"DOCKER_HOST": "unix:///var/run/docker.sock",
},
expectType: "DockerLocal",
},
{
name: "tcp host",
env: map[string]string{
"DOCKER_HOST": "tcp://192.168.1.100:2375",
},
expectType: "DockerVM",
},
{
name: "darwin no docker host",
env: map[string]string{},
goos: "darwin",
expectType: "DockerDesktop",
},
{
name: "windows no docker host",
env: map[string]string{},
goos: "windows",
expectType: "DockerDesktop",
},
{
name: "linux no docker host",
env: map[string]string{},
goos: "linux",
expectType: "DockerLocal",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
detector := NewDockerDetector()
detector.envProvider = &mockEnvProvider{env: tt.env}
detector.runtimeProvider = &mockRuntimeProvider{goos: tt.goos}
flavor, err := detector.DockerFlavor()
if tt.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
var gotType string
switch flavor.(type) {
case *DockerLocal:
gotType = "DockerLocal"
case *DockerVM:
gotType = "DockerVM"
case *DockerDesktop:
gotType = "DockerDesktop"
}
assert.Equal(t, tt.expectType, gotType)
})
}
}
package backend
import (
"net"
"os"
"runtime"
)
// Interface represents a network interface
type Interface interface {
Addrs() ([]net.Addr, error)
Name() string
}
// Providers for external dependencies
type networkProvider interface {
Interfaces() ([]Interface, error)
}
type envProvider interface {
Getenv(key string) string
}
type runtimeProvider interface {
GOOS() string
}
// netInterfaceWrapper wraps net.Interface to implement our Interface
type netInterfaceWrapper struct {
iface net.Interface
}
func (n *netInterfaceWrapper) Addrs() ([]net.Addr, error) {
addrs, err := n.iface.Addrs()
if err != nil {
return nil, err
}
// Wrap each address in our IPNetAddr interface if it's an *net.IPNet
result := make([]net.Addr, len(addrs))
for i, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok {
result[i] = &realIPNetAddr{addr: ipNet}
} else {
result[i] = addr
}
}
return result, nil
}
func (n *netInterfaceWrapper) Name() string {
return n.iface.Name
}
// realIPNetAddr wraps a real *net.IPNet to implement our IPNetAddr interface
type realIPNetAddr struct {
addr *net.IPNet
}
func (r *realIPNetAddr) Network() string { return r.addr.Network() }
func (r *realIPNetAddr) String() string { return r.addr.String() }
func (r *realIPNetAddr) AsIPNet() *net.IPNet {
return r.addr
}
// Default implementations
type defaultNetworkProvider struct{}
func (d defaultNetworkProvider) Interfaces() ([]Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
result := make([]Interface, len(ifaces))
for i, iface := range ifaces {
// Need to create a new variable here to avoid having all wrappers
// point to the last interface in the loop
iface := iface
result[i] = &netInterfaceWrapper{iface: iface}
}
return result, nil
}
type defaultEnvProvider struct{}
func (d defaultEnvProvider) Getenv(key string) string {
return os.Getenv(key)
}
type defaultRuntimeProvider struct{}
func (d defaultRuntimeProvider) GOOS() string {
return runtime.GOOS
}
package serve
import (
"context"
"fmt"
"net"
"net/http"
)
// Server represents an HTTP server that serves static files
type Server struct {
server *http.Server
listener net.Listener
url string
hostname string
}
type ServerOption func(*Server)
func WithStaticDir(staticDir string) ServerOption {
return func(s *Server) {
mux := http.NewServeMux()
fileServer := http.FileServer(http.Dir(staticDir))
mux.Handle("/", fileServer)
s.server.Handler = mux
}
}
func WithHostname(hostname string) ServerOption {
return func(s *Server) {
s.hostname = hostname
}
}
// NewServer creates a new static file server
func NewServer(opts ...ServerOption) *Server {
server := &Server{
server: &http.Server{
Handler: http.NewServeMux(),
},
hostname: "localhost",
}
for _, opt := range opts {
opt(server)
}
return server
}
// Start begins serving files in a goroutine and returns when the server is ready
func (s *Server) Start(ctx context.Context) error {
// Create listener with dynamic port
listener, err := net.Listen("tcp", ":0")
if err != nil {
return fmt.Errorf("failed to create listener: %w", err)
}
s.listener = listener
// Get the actual address
addr := listener.Addr().(*net.TCPAddr)
s.url = fmt.Sprintf("http://%s:%d", s.hostname, addr.Port)
// Channel to signal server is ready to accept connections
ready := make(chan struct{})
go func() {
// Signal ready right before we start serving
close(ready)
if err := s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
// Log server errors that occur after startup
fmt.Printf("Server error: %v\n", err)
}
}()
// Wait for server to be ready or context to be cancelled
select {
case <-ready:
return nil
case <-ctx.Done():
listener.Close()
return ctx.Err()
}
}
// Stop gracefully shuts down the server
func (s *Server) Stop(ctx context.Context) error {
return s.server.Shutdown(ctx)
}
// URL returns the server's URL
func (s *Server) URL() string {
return s.url
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment