Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ RUN apt-get update && \
iperf3 \
curl \
iproute2 \
wireguard-tools \
net-tools \
tcpdump \
dnsutils \
netcat-openbsd && \
netcat-openbsd \
python3 && \
rm -rf /var/lib/apt/lists/*

COPY --from=builder /nylon /usr/local/bin/nylon
Expand Down
194 changes: 194 additions & 0 deletions e2e/distribution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//go:build e2e

package e2e

import (
"context"
"net/netip"
"os"
"path/filepath"
"testing"
"time"

"github.com/encodeous/nylon/state"
"github.com/goccy/go-yaml"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

func TestDistribution(t *testing.T) {
h := NewHarness(t)
// Cleanup is handled by Harness via t.Cleanup

ctx := context.Background()

// 1. Setup Keys
privKey := state.GenerateKey()
pubKey := privKey.Pubkey()

// 2. Prepare Directories
runDir := filepath.Join(h.RootDir, "e2e", "runs", t.Name())
if err := os.MkdirAll(runDir, 0755); err != nil {
t.Fatal(err)
}

// 3. Prepare Initial Bundle (v1)
distCfg := &state.DistributionCfg{
Key: pubKey,
Repos: []string{"http://repo:80/bundle"},
}

nodeKey := state.GenerateKey()
nodeId := "node-1"

cfg1 := state.CentralCfg{
Timestamp: 1,
Dist: distCfg,
Routers: []state.RouterCfg{
{
NodeCfg: state.NodeCfg{
Id: state.NodeId(nodeId),
PubKey: nodeKey.Pubkey(),
},
Endpoints: []netip.AddrPort{},
},
},
Clients: []state.ClientCfg{},
Graph: []string{},
}

cfg1Bytes, err := yaml.Marshal(cfg1)
if err != nil {
t.Fatal(err)
}
bundle1Str, err := state.BundleConfig(string(cfg1Bytes), privKey)
if err != nil {
t.Fatal(err)
}
bundle1Path := filepath.Join(runDir, "bundle1")
if err := os.WriteFile(bundle1Path, []byte(bundle1Str), 0644); err != nil {
t.Fatal(err)
}

// 4. Start Repo Server
t.Log("Starting Repo Server...")
repoReq := testcontainers.ContainerRequest{
Image: "python:3-alpine",
Cmd: []string{"sh", "-c", "mkdir -p /data && cd /data && python3 -u -m http.server 80"},
ExposedPorts: []string{"80/tcp"},
Networks: []string{h.Network.Name},
NetworkAliases: map[string][]string{
h.Network.Name: {"repo"},
},
WaitingFor: wait.ForListeningPort("80/tcp"),
}
repoContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: repoReq,
Started: true,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
repoContainer.Terminate(context.Background())
})

// Copy bundle1 to repo
if err := repoContainer.CopyFileToContainer(ctx, bundle1Path, "/data/bundle", 0644); err != nil {
t.Fatal(err)
}

// 5. Start Nylon Node
// Write central.yaml (v1) to disk for initial startup
centralConfigPath := filepath.Join(runDir, "central.yaml")
if err := os.WriteFile(centralConfigPath, cfg1Bytes, 0644); err != nil {
t.Fatal(err)
}

// Write node.yaml
nodeCfg := state.LocalCfg{
Id: state.NodeId(nodeId),
Key: nodeKey,
Port: 51820,
// We can omit Dist here since we are providing central.yaml,
// but providing it doesn't hurt.
Dist: &state.LocalDistributionCfg{
Key: pubKey,
Url: "http://repo:80/bundle",
},
}
nodeCfgBytes, err := yaml.Marshal(nodeCfg)
if err != nil {
t.Fatal(err)
}
nodeConfigPath := filepath.Join(runDir, "node.yaml")
if err := os.WriteFile(nodeConfigPath, nodeCfgBytes, 0644); err != nil {
t.Fatal(err)
}

t.Log("Starting Nylon Node...")
h.StartNode(nodeId, "", centralConfigPath, nodeConfigPath)

// Wait for start
h.WaitForLog(nodeId, "Nylon has been initialized")
t.Log("Nylon Node started (v1).")

// 6. Create and Push Bundle 2
t.Log("Preparing Bundle 2...")
// Wait a bit to ensure timestamp is different if using UnixNano
time.Sleep(1 * time.Second)

cfg2 := cfg1
// BundleConfig will overwrite this timestamp anyway
cfg2Bytes, err := yaml.Marshal(cfg2)
if err != nil {
t.Fatal(err)
}
bundle2Str, err := state.BundleConfig(string(cfg2Bytes), privKey)
if err != nil {
t.Fatal(err)
}

// We need to know the timestamp of bundle 2 to verify
unbundled2, err := state.UnbundleConfig(bundle2Str, pubKey)
if err != nil {
t.Fatal(err)
}
bundle2Timestamp := unbundled2.Timestamp

bundle2Path := filepath.Join(runDir, "bundle2")
if err := os.WriteFile(bundle2Path, []byte(bundle2Str), 0644); err != nil {
t.Fatal(err)
}

t.Logf("Updating Repo with Bundle 2 (timestamp: %d)...", bundle2Timestamp)
if err := repoContainer.CopyFileToContainer(ctx, bundle2Path, "/data/bundle", 0644); err != nil {
t.Fatal(err)
}

// 7. Verify Update
t.Log("Waiting for update detection...")
h.WaitForLog(nodeId, "Found a new config update in repo")

t.Log("Waiting for restart...")
h.WaitForLog(nodeId, "Restarting Nylon...")

// Allow some time for the restart to complete and write the file
time.Sleep(5 * time.Second)

t.Log("Verifying config version on node...")
stdout, _, err := h.Exec(nodeId, []string{"cat", "/app/config/central.yaml"})
if err != nil {
t.Fatal(err)
}

var verifyCfg state.CentralCfg
if err := yaml.Unmarshal([]byte(stdout), &verifyCfg); err != nil {
t.Fatalf("Failed to parse config from node: %v", err)
}

if verifyCfg.Timestamp != bundle2Timestamp {
t.Fatalf("Expected timestamp %d, got %d. Config content: %s", bundle2Timestamp, verifyCfg.Timestamp, stdout)
}
t.Logf("Successfully updated to timestamp %d.", verifyCfg.Timestamp)
}
57 changes: 57 additions & 0 deletions e2e/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"io"
"net/netip"
"os"
"path/filepath"
"regexp"
Expand All @@ -18,6 +19,8 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/stdcopy"
"github.com/encodeous/nylon/state"
"github.com/goccy/go-yaml"
"github.com/testcontainers/testcontainers-go"
tcnetwork "github.com/testcontainers/testcontainers-go/network"
"github.com/testcontainers/testcontainers-go/wait"
Expand Down Expand Up @@ -366,3 +369,57 @@ func (h *Harness) PrintLogs(nodeName string) {
io.Copy(buf, r)
h.t.Logf("Logs for %s:\n%s", nodeName, buf.String())
}

// SetupTestDir creates a directory for the current test run
func (h *Harness) SetupTestDir() string {
dir := filepath.Join(h.RootDir, "e2e", "runs", h.t.Name())
// Clean up previous run
os.RemoveAll(dir)
if err := os.MkdirAll(dir, 0755); err != nil {
h.t.Fatal(err)
}
return dir
}

// WriteConfig marshals the config to YAML and writes it to the specified directory with the given filename
func (h *Harness) WriteConfig(dir, filename string, cfg any) string {
path := filepath.Join(dir, filename)
data, err := yaml.Marshal(cfg)
if err != nil {
h.t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
h.t.Fatal(err)
}
return path
}

// SimpleRouter creates a basic RouterCfg with the given parameters
func SimpleRouter(id string, pubKey state.NyPublicKey, nylonIP string, endpointIP string) state.RouterCfg {
cfg := state.RouterCfg{
NodeCfg: state.NodeCfg{
Id: state.NodeId(id),
PubKey: pubKey,
Addresses: []netip.Addr{
netip.MustParseAddr(nylonIP),
},
},
}
if endpointIP != "" {
cfg.Endpoints = []netip.AddrPort{
netip.MustParseAddrPort(fmt.Sprintf("%s:57175", endpointIP)),
}
}
return cfg
}

// SimpleLocal creates a basic LocalCfg with the given parameters and defaults
func SimpleLocal(id string, key state.NyPrivateKey) state.LocalCfg {
return state.LocalCfg{
Id: state.NodeId(id),
Key: key,
Port: 57175,
NoNetConfigure: false,
InterfaceName: "nylon0",
}
}
Loading