Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
806 lines
26 KiB
Go
806 lines
26 KiB
Go
package pluginmanager
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tursom/mc-gateway/internal/admindb"
|
|
"github.com/tursom/mc-gateway/plugin/api"
|
|
)
|
|
|
|
func TestManagerUploadDoesNotLoadPlugin(t *testing.T) {
|
|
adapter := &fakeAdapter{}
|
|
manager := newManagerForTest(t, adapter)
|
|
|
|
artifact := uploadTestArtifact(t, manager, "plugin-a")
|
|
if artifact.PluginID != "plugin-a" {
|
|
t.Fatalf("artifact plugin = %q, want plugin-a", artifact.PluginID)
|
|
}
|
|
if adapter.loads != 0 {
|
|
t.Fatalf("adapter loads = %d, want 0 for upload-only validation", adapter.loads)
|
|
}
|
|
}
|
|
|
|
func TestManagerUploadSourceQueuesBuild(t *testing.T) {
|
|
manager := newManagerForTest(t, &fakeAdapter{})
|
|
source := uploadTestSource(t, manager, "plugin-a")
|
|
builds, err := manager.ListBuilds(context.Background(), "plugin-a")
|
|
if err != nil {
|
|
t.Fatalf("ListBuilds() error = %v", err)
|
|
}
|
|
if len(builds) != 1 {
|
|
t.Fatalf("builds = %d, want 1", len(builds))
|
|
}
|
|
if builds[0].SourceID != source.ID || builds[0].Status != BuildStatusQueued {
|
|
t.Fatalf("queued build = %+v, want source %s queued", builds[0], source.ID)
|
|
}
|
|
}
|
|
|
|
func TestManagerBuildSourceCreatesBinaryArtifact(t *testing.T) {
|
|
t.Setenv("GOCACHE", t.TempDir())
|
|
t.Setenv("GOWORK", "off")
|
|
manager := newManagerForTest(t, &fakeAdapter{})
|
|
_ = uploadBuildableTestSource(t, manager, "source-enable")
|
|
builds, err := manager.ListBuilds(context.Background(), "source-enable")
|
|
if err != nil {
|
|
t.Fatalf("ListBuilds() error = %v", err)
|
|
}
|
|
if len(builds) != 1 {
|
|
t.Fatalf("builds = %d, want 1", len(builds))
|
|
}
|
|
build, err := manager.RunBuild(context.Background(), "admin", builds[0].ID)
|
|
if err != nil {
|
|
t.Fatalf("RunBuild() error = %v", err)
|
|
}
|
|
if build.Status != BuildStatusSucceeded || build.ArtifactID == "" {
|
|
t.Fatalf("build = %+v, want succeeded with artifact", build)
|
|
}
|
|
artifact, err := manager.Artifact(context.Background(), build.ArtifactID)
|
|
if err != nil {
|
|
t.Fatalf("Artifact() error = %v", err)
|
|
}
|
|
if artifact.ArtifactType != ArtifactTypeBinary || artifact.Status != ArtifactStatusLoadable {
|
|
t.Fatalf("artifact = %+v, want loadable binary", artifact)
|
|
}
|
|
if _, err := manager.SetDesired(context.Background(), "admin", artifact.PluginID, artifact.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired() error = %v", err)
|
|
}
|
|
plugin, err := manager.Enable(context.Background(), "admin", artifact.PluginID)
|
|
if err != nil {
|
|
t.Fatalf("Enable() error = %v", err)
|
|
}
|
|
if plugin.RuntimeState != RuntimeEnabled || plugin.ActiveArtifactID != artifact.ID {
|
|
t.Fatalf("plugin = %+v, want enabled built artifact", plugin)
|
|
}
|
|
}
|
|
|
|
func TestManagerBuildFailureDoesNotChangeActiveArtifact(t *testing.T) {
|
|
adapter := &fakeAdapter{}
|
|
builder := &fakeBuilder{err: errors.New("compile failed")}
|
|
manager := newManagerForTestWithBuilders(t, adapter, map[string]SourceBuilder{
|
|
BuilderTypeLocalProcess: builder,
|
|
})
|
|
active := uploadTestArtifact(t, manager, "plugin-a")
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", active.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired() error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", "plugin-a"); err != nil {
|
|
t.Fatalf("Enable() error = %v", err)
|
|
}
|
|
source := uploadTestSource(t, manager, "plugin-a")
|
|
builds, err := manager.ListBuilds(context.Background(), "plugin-a")
|
|
if err != nil {
|
|
t.Fatalf("ListBuilds() error = %v", err)
|
|
}
|
|
if len(builds) == 0 || builds[0].SourceID != source.ID {
|
|
t.Fatalf("queued builds = %+v, want source %s", builds, source.ID)
|
|
}
|
|
build, err := manager.RunBuild(context.Background(), "admin", builds[0].ID)
|
|
if err != nil {
|
|
t.Fatalf("RunBuild() unexpected manager error = %v", err)
|
|
}
|
|
if build.Status != BuildStatusFailed {
|
|
t.Fatalf("build status = %q, want failed", build.Status)
|
|
}
|
|
plugin, err := manager.Plugin(context.Background(), "plugin-a")
|
|
if err != nil {
|
|
t.Fatalf("Plugin() error = %v", err)
|
|
}
|
|
if plugin.ActiveArtifactID != active.ID {
|
|
t.Fatalf("active artifact = %q, want unchanged %q", plugin.ActiveArtifactID, active.ID)
|
|
}
|
|
}
|
|
|
|
func TestManagerRejectsSourceArtifactLoad(t *testing.T) {
|
|
manager := newManagerForTest(t, &fakeAdapter{})
|
|
source := uploadTestSource(t, manager, "plugin-a")
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", source.ID, DesiredEnabled, `{}`, 10); err == nil || !strings.Contains(err.Error(), "binary artifact") {
|
|
t.Fatalf("SetDesired(source) error = %v, want binary artifact rejection", err)
|
|
}
|
|
}
|
|
|
|
func TestManagerEnableDisableAndDispatch(t *testing.T) {
|
|
adapter := &fakeAdapter{}
|
|
manager := newManagerForTest(t, adapter)
|
|
artifact := uploadTestArtifact(t, manager, "plugin-a")
|
|
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", artifact.ID, DesiredDisabled, `{"upstream":"override"}`, 10); err != nil {
|
|
t.Fatalf("SetDesired() error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", "plugin-a"); err != nil {
|
|
t.Fatalf("Enable() error = %v", err)
|
|
}
|
|
if adapter.loads != 1 {
|
|
t.Fatalf("adapter loads = %d, want 1", adapter.loads)
|
|
}
|
|
result, err := manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{
|
|
Host: "play.example",
|
|
Upstream: "backend.example:25565",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ConnectUpstream() error = %v", err)
|
|
}
|
|
if !result.Handled || result.Conn == nil {
|
|
t.Fatalf("ConnectUpstream() = %+v, want handled conn", result)
|
|
}
|
|
|
|
plugin, err := manager.Disable(context.Background(), "admin", "plugin-a")
|
|
if err != nil {
|
|
t.Fatalf("Disable() error = %v", err)
|
|
}
|
|
if plugin.RuntimeState != RuntimeDisabled {
|
|
t.Fatalf("disabled runtime state = %q, want %q", plugin.RuntimeState, RuntimeDisabled)
|
|
}
|
|
result, err = manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{
|
|
Host: "play.example",
|
|
Upstream: "backend.example:25565",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ConnectUpstream(disabled) error = %v", err)
|
|
}
|
|
if result.Handled {
|
|
t.Fatalf("ConnectUpstream(disabled) = %+v, want pass-through", result)
|
|
}
|
|
}
|
|
|
|
func TestManagerErrPassContinuesToNextHandler(t *testing.T) {
|
|
adapter := &fakeAdapter{
|
|
handlers: map[string]api.UpstreamConnectHandler{
|
|
"plugin-a": func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
return nil, api.ErrPass
|
|
},
|
|
"plugin-b": func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
return newMemoryConn(), nil
|
|
},
|
|
},
|
|
}
|
|
manager := newManagerForTest(t, adapter)
|
|
artifactA := uploadTestArtifact(t, manager, "plugin-a")
|
|
artifactB := uploadTestArtifact(t, manager, "plugin-b")
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", artifactA.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired(a) error = %v", err)
|
|
}
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-b", artifactB.ID, DesiredEnabled, `{}`, 20); err != nil {
|
|
t.Fatalf("SetDesired(b) error = %v", err)
|
|
}
|
|
if err := manager.Reconcile(context.Background()); err != nil {
|
|
t.Fatalf("Reconcile() error = %v", err)
|
|
}
|
|
|
|
result, err := manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{Host: "play.example", Upstream: "backend"})
|
|
if err != nil {
|
|
t.Fatalf("ConnectUpstream() error = %v", err)
|
|
}
|
|
if !result.Handled || result.Conn == nil {
|
|
t.Fatalf("ConnectUpstream() = %+v, want second handler conn", result)
|
|
}
|
|
plan := manager.DispatchPlan(context.Background())
|
|
if len(plan.Handlers) != 2 {
|
|
t.Fatalf("dispatch handlers = %d, want 2", len(plan.Handlers))
|
|
}
|
|
if plan.Handlers[0].PluginID != "plugin-a" || plan.Handlers[1].PluginID != "plugin-b" {
|
|
t.Fatalf("dispatch order = %+v, want plugin-a then plugin-b", plan.Handlers)
|
|
}
|
|
}
|
|
|
|
func TestManagerErrBlockedStopsDispatch(t *testing.T) {
|
|
adapter := &fakeAdapter{
|
|
handlers: map[string]api.UpstreamConnectHandler{
|
|
"plugin-a": func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
return nil, api.ErrBlocked
|
|
},
|
|
"plugin-b": func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
return newMemoryConn(), nil
|
|
},
|
|
},
|
|
}
|
|
manager := newManagerForTest(t, adapter)
|
|
artifactA := uploadTestArtifact(t, manager, "plugin-a")
|
|
artifactB := uploadTestArtifact(t, manager, "plugin-b")
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", artifactA.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired(a) error = %v", err)
|
|
}
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-b", artifactB.ID, DesiredEnabled, `{}`, 20); err != nil {
|
|
t.Fatalf("SetDesired(b) error = %v", err)
|
|
}
|
|
if err := manager.Reconcile(context.Background()); err != nil {
|
|
t.Fatalf("Reconcile() error = %v", err)
|
|
}
|
|
|
|
result, err := manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{Host: "play.example", Upstream: "backend"})
|
|
if !errors.Is(err, api.ErrBlocked) {
|
|
t.Fatalf("ConnectUpstream() error = %v, want ErrBlocked", err)
|
|
}
|
|
if !result.Handled {
|
|
t.Fatalf("ConnectUpstream() = %+v, want handled", result)
|
|
}
|
|
plan := manager.DispatchPlan(context.Background())
|
|
if got := plan.Handlers[0].Blocked; got != 1 {
|
|
t.Fatalf("blocked count = %d, want 1", got)
|
|
}
|
|
if got := plan.Handlers[1].Calls; got != 0 {
|
|
t.Fatalf("second handler calls = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestManagerPanicDoesNotReplaceExistingDispatch(t *testing.T) {
|
|
adapter := &fakeAdapter{
|
|
handlers: map[string]api.UpstreamConnectHandler{
|
|
"plugin-a": func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
return newMemoryConn(), nil
|
|
},
|
|
"plugin-b": func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
panic("boom")
|
|
},
|
|
},
|
|
}
|
|
manager := newManagerForTest(t, adapter)
|
|
artifactA := uploadTestArtifact(t, manager, "plugin-a")
|
|
artifactB := uploadTestArtifact(t, manager, "plugin-b")
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", artifactA.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired(a) error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", "plugin-a"); err != nil {
|
|
t.Fatalf("Enable(a) error = %v", err)
|
|
}
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-b", artifactB.ID, DesiredEnabled, `{}`, 5); err != nil {
|
|
t.Fatalf("SetDesired(b) error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", "plugin-b"); err != nil {
|
|
t.Fatalf("Enable(b) error = %v", err)
|
|
}
|
|
_, err := manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{Host: "play.example", Upstream: "backend"})
|
|
if err == nil {
|
|
t.Fatal("ConnectUpstream() error = nil, want panic converted to error")
|
|
}
|
|
plan := manager.DispatchPlan(context.Background())
|
|
if len(plan.Handlers) != 2 {
|
|
t.Fatalf("dispatch handlers = %d, want 2", len(plan.Handlers))
|
|
}
|
|
if plan.Handlers[0].PluginID != "plugin-b" || plan.Handlers[0].Panics != 1 {
|
|
t.Fatalf("first handler summary = %+v, want plugin-b panic count", plan.Handlers[0])
|
|
}
|
|
}
|
|
|
|
func TestManagerLoadFailureKeepsExistingDispatch(t *testing.T) {
|
|
adapter := &fakeAdapter{
|
|
loadErrs: map[string]error{
|
|
"plugin-b": errors.New("open failed"),
|
|
},
|
|
}
|
|
manager := newManagerForTest(t, adapter)
|
|
artifactA := uploadTestArtifact(t, manager, "plugin-a")
|
|
artifactB := uploadTestArtifact(t, manager, "plugin-b")
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", artifactA.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired(a) error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", "plugin-a"); err != nil {
|
|
t.Fatalf("Enable(a) error = %v", err)
|
|
}
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-b", artifactB.ID, DesiredEnabled, `{}`, 5); err != nil {
|
|
t.Fatalf("SetDesired(b) error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", "plugin-b"); err == nil {
|
|
t.Fatal("Enable(b) error = nil, want load failure")
|
|
}
|
|
|
|
plan := manager.DispatchPlan(context.Background())
|
|
if len(plan.Handlers) != 1 || plan.Handlers[0].PluginID != "plugin-a" {
|
|
t.Fatalf("dispatch plan after failed enable = %+v, want only plugin-a", plan)
|
|
}
|
|
result, err := manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{Host: "play.example", Upstream: "backend"})
|
|
if err != nil {
|
|
t.Fatalf("ConnectUpstream() error = %v", err)
|
|
}
|
|
if !result.Handled || result.Conn == nil {
|
|
t.Fatalf("ConnectUpstream() = %+v, want existing plugin-a conn", result)
|
|
}
|
|
}
|
|
|
|
func TestManagerReconcileRestoresEnabledPlugin(t *testing.T) {
|
|
db := openPluginManagerTestDB(t)
|
|
root := t.TempDir()
|
|
firstAdapter := &fakeAdapter{}
|
|
first := New(Options{
|
|
DB: db,
|
|
ArtifactRoot: root,
|
|
Adapter: firstAdapter,
|
|
})
|
|
artifact := uploadTestArtifact(t, first, "plugin-a")
|
|
if _, err := first.SetDesired(context.Background(), "admin", "plugin-a", artifact.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired() error = %v", err)
|
|
}
|
|
if _, err := first.Enable(context.Background(), "admin", "plugin-a"); err != nil {
|
|
t.Fatalf("Enable() error = %v", err)
|
|
}
|
|
|
|
secondAdapter := &fakeAdapter{}
|
|
second := New(Options{
|
|
DB: db,
|
|
ArtifactRoot: root,
|
|
Adapter: secondAdapter,
|
|
})
|
|
if err := second.Reconcile(context.Background()); err != nil {
|
|
t.Fatalf("Reconcile() error = %v", err)
|
|
}
|
|
if secondAdapter.loads != 1 {
|
|
t.Fatalf("reconcile loads = %d, want 1", secondAdapter.loads)
|
|
}
|
|
result, err := second.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{Host: "play.example", Upstream: "backend"})
|
|
if err != nil {
|
|
t.Fatalf("ConnectUpstream() error = %v", err)
|
|
}
|
|
if !result.Handled || result.Conn == nil {
|
|
t.Fatalf("ConnectUpstream() = %+v, want restored handler conn", result)
|
|
}
|
|
}
|
|
|
|
func TestAcceptorPanicIsRecovered(t *testing.T) {
|
|
handler := &upstreamHandler{
|
|
pluginID: "acceptor",
|
|
accept: func(api.UpstreamConnectRequest) bool {
|
|
panic("boom")
|
|
},
|
|
}
|
|
accepted, err := handler.accepts(api.UpstreamConnectRequest{})
|
|
if err == nil {
|
|
t.Fatal("accepts() error = nil, want panic error")
|
|
}
|
|
if accepted {
|
|
t.Fatal("accepts() accepted = true, want false")
|
|
}
|
|
if handler.panics.Load() != 1 {
|
|
t.Fatalf("panics = %d, want 1", handler.panics.Load())
|
|
}
|
|
}
|
|
|
|
func TestProtocolProxyTrackDrainAndForceClose(t *testing.T) {
|
|
clientGateway, clientSide := net.Pipe()
|
|
defer clientSide.Close()
|
|
pluginGateway, pluginSide := net.Pipe()
|
|
defer pluginSide.Close()
|
|
handlerReturned := make(chan struct{})
|
|
adapter := &fakeAdapter{
|
|
handlers: map[string]api.UpstreamConnectHandler{
|
|
"plugin-a": func(req api.UpstreamConnectRequest) (net.Conn, error) {
|
|
go func() {
|
|
buf := make([]byte, len(req.InitialData))
|
|
if _, err := io.ReadFull(pluginSide, buf); err != nil {
|
|
t.Errorf("plugin side initial read error = %v", err)
|
|
}
|
|
close(handlerReturned)
|
|
_, _ = pluginSide.Read(make([]byte, 1))
|
|
}()
|
|
return pluginGateway, nil
|
|
},
|
|
},
|
|
}
|
|
manager := newManagerForTest(t, adapter)
|
|
artifact := uploadTestArtifactWithCapabilities(t, manager, "plugin-a", json.RawMessage(`{"upstream_connect":{"mode":"protocol-proxy"}}`))
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", artifact.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired() error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", "plugin-a"); err != nil {
|
|
t.Fatalf("Enable() error = %v", err)
|
|
}
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{
|
|
Host: "play.example",
|
|
Upstream: "backend",
|
|
Source: clientGateway,
|
|
InitialData: []byte("hello"),
|
|
})
|
|
errCh <- err
|
|
}()
|
|
<-handlerReturned
|
|
waitForPluginManagerTest(t, func() bool {
|
|
return manager.DispatchPlan(context.Background()).Handlers[0].ActiveProxy == 1
|
|
})
|
|
plan := manager.DispatchPlan(context.Background())
|
|
if got := plan.Handlers[0].ActiveProxy; got != 1 {
|
|
t.Fatalf("active proxy = %d, want 1", got)
|
|
}
|
|
if _, err := manager.Disable(context.Background(), "admin", "plugin-a"); err != nil {
|
|
t.Fatalf("Disable() error = %v", err)
|
|
}
|
|
closed, err := manager.ForceCloseDraining(context.Background(), "admin", "plugin-a")
|
|
if err != nil {
|
|
t.Fatalf("ForceCloseDraining() error = %v", err)
|
|
}
|
|
if closed != 1 {
|
|
t.Fatalf("ForceCloseDraining() = %d, want 1", closed)
|
|
}
|
|
if err := <-errCh; err != nil {
|
|
t.Fatalf("ConnectUpstream() error = %v", err)
|
|
}
|
|
waitForPluginManagerTest(t, func() bool {
|
|
return manager.activeProxyCountLocked("plugin-a") == 0
|
|
})
|
|
}
|
|
|
|
func TestProtocolProxyReplaysInitialAndForwardsClientBytes(t *testing.T) {
|
|
clientGateway, clientSide := net.Pipe()
|
|
defer clientSide.Close()
|
|
|
|
initial := []byte("initial-handshake")
|
|
next := []byte("login-start")
|
|
pluginRead := make(chan []byte, 1)
|
|
adapter := &fakeAdapter{
|
|
handlers: map[string]api.UpstreamConnectHandler{
|
|
"plugin-a": func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
gatewayEnd, pluginEnd := net.Pipe()
|
|
go func() {
|
|
defer pluginEnd.Close()
|
|
buf := make([]byte, len(initial)+len(next))
|
|
if _, err := io.ReadFull(pluginEnd, buf); err != nil {
|
|
t.Errorf("plugin read error = %v", err)
|
|
return
|
|
}
|
|
pluginRead <- buf
|
|
}()
|
|
return gatewayEnd, nil
|
|
},
|
|
},
|
|
}
|
|
manager := newManagerForTest(t, adapter)
|
|
enableProtocolProxyTestPlugin(t, manager, "plugin-a")
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{
|
|
Source: clientGateway,
|
|
InitialData: initial,
|
|
})
|
|
errCh <- err
|
|
}()
|
|
if _, err := clientSide.Write(next); err != nil {
|
|
t.Fatalf("client write error = %v", err)
|
|
}
|
|
got := <-pluginRead
|
|
if !bytes.Equal(got, append(append([]byte(nil), initial...), next...)) {
|
|
t.Fatalf("plugin bytes = %q, want initial+next", got)
|
|
}
|
|
_ = clientSide.Close()
|
|
if err := <-errCh; err != nil {
|
|
t.Fatalf("ConnectUpstream() error = %v", err)
|
|
}
|
|
plan := manager.DispatchPlan(context.Background())
|
|
if got := plan.Handlers[0].ProxyBytesIn; got != uint64(len(next)) {
|
|
t.Fatalf("proxy bytes in = %d, want %d", got, len(next))
|
|
}
|
|
}
|
|
|
|
func TestProtocolProxyInitialWriteTimeoutClosesUnreadableConn(t *testing.T) {
|
|
reader, writer := net.Pipe()
|
|
defer reader.Close()
|
|
adapter := &fakeAdapter{
|
|
handlers: map[string]api.UpstreamConnectHandler{
|
|
"plugin-a": func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
return writer, nil
|
|
},
|
|
},
|
|
}
|
|
manager := newManagerForTest(t, adapter)
|
|
artifact := uploadTestArtifactWithCapabilities(t, manager, "plugin-a", json.RawMessage(`{"upstream_connect":{"mode":"protocol-proxy"}}`))
|
|
if _, err := manager.SetDesired(context.Background(), "admin", "plugin-a", artifact.ID, DesiredEnabled, `{"unused":true}`, 10); err != nil {
|
|
t.Fatalf("SetDesired() error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", "plugin-a"); err != nil {
|
|
t.Fatalf("Enable() error = %v", err)
|
|
}
|
|
sourceGateway, sourceClient := net.Pipe()
|
|
defer sourceGateway.Close()
|
|
defer sourceClient.Close()
|
|
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
initial := bytes.Repeat([]byte("x"), 2*1024*1024)
|
|
_, err := manager.ConnectUpstream(context.Background(), api.UpstreamConnectRequest{
|
|
Source: sourceGateway,
|
|
InitialData: initial,
|
|
})
|
|
done <- err
|
|
}()
|
|
select {
|
|
case err := <-done:
|
|
if err == nil {
|
|
t.Fatal("ConnectUpstream() error = nil, want initial replay failure")
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("ConnectUpstream() did not return after initial write deadline")
|
|
}
|
|
}
|
|
|
|
func enableProtocolProxyTestPlugin(t *testing.T, manager *Manager, pluginID string) ArtifactRecord {
|
|
t.Helper()
|
|
artifact := uploadTestArtifactWithCapabilities(t, manager, pluginID, json.RawMessage(`{"upstream_connect":{"mode":"protocol-proxy"}}`))
|
|
if _, err := manager.SetDesired(context.Background(), "admin", pluginID, artifact.ID, DesiredEnabled, `{}`, 10); err != nil {
|
|
t.Fatalf("SetDesired() error = %v", err)
|
|
}
|
|
if _, err := manager.Enable(context.Background(), "admin", pluginID); err != nil {
|
|
t.Fatalf("Enable() error = %v", err)
|
|
}
|
|
return artifact
|
|
}
|
|
|
|
func newManagerForTest(t *testing.T, adapter RuntimeAdapter) *Manager {
|
|
t.Helper()
|
|
return newManagerForTestWithBuilders(t, adapter, nil)
|
|
}
|
|
|
|
func newManagerForTestWithBuilders(t *testing.T, adapter RuntimeAdapter, builders map[string]SourceBuilder) *Manager {
|
|
t.Helper()
|
|
db := openPluginManagerTestDB(t)
|
|
return New(Options{
|
|
DB: db,
|
|
ArtifactRoot: t.TempDir(),
|
|
Adapter: adapter,
|
|
Builders: builders,
|
|
})
|
|
}
|
|
|
|
func openPluginManagerTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := admindb.Open(filepath.Join(t.TempDir(), "gateway.sqlite3"))
|
|
if err != nil {
|
|
t.Fatalf("Open() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
if err := admindb.Migrate(db); err != nil {
|
|
t.Fatalf("Migrate() error = %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func uploadTestArtifact(t *testing.T, manager *Manager, pluginID string) ArtifactRecord {
|
|
t.Helper()
|
|
return uploadTestArtifactWithCapabilities(t, manager, pluginID, nil)
|
|
}
|
|
|
|
func uploadTestArtifactWithCapabilities(t *testing.T, manager *Manager, pluginID string, capabilities json.RawMessage) ArtifactRecord {
|
|
t.Helper()
|
|
packagePath := writeTestMCGP(t, map[string][]byte{
|
|
"manifest.json": testManifestBytesWithCapabilities(t, pluginID, capabilities),
|
|
"plugin.so": []byte("fake plugin bytes " + pluginID),
|
|
})
|
|
artifact, err := manager.UploadArtifact(context.Background(), ArtifactUpload{
|
|
SourcePath: packagePath,
|
|
FileName: pluginID + ".mcgp",
|
|
Actor: "admin",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UploadArtifact(%s) error = %v", pluginID, err)
|
|
}
|
|
return artifact
|
|
}
|
|
|
|
func uploadTestSource(t *testing.T, manager *Manager, pluginID string) ArtifactRecord {
|
|
t.Helper()
|
|
packagePath := writeTestMCGP(t, map[string][]byte{
|
|
"manifest.json": testSourceManifestBytes(t, pluginID),
|
|
"go.mod": []byte("module example.com/" + pluginID + "\n\ngo 1.24.0\n"),
|
|
"main.go": []byte("package main\n"),
|
|
})
|
|
source, err := manager.UploadSource(context.Background(), ArtifactUpload{
|
|
SourcePath: packagePath,
|
|
FileName: pluginID + "-source.mcgp",
|
|
Actor: "admin",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UploadSource(%s) error = %v", pluginID, err)
|
|
}
|
|
return source
|
|
}
|
|
|
|
func uploadBuildableTestSource(t *testing.T, manager *Manager, pluginID string) ArtifactRecord {
|
|
t.Helper()
|
|
repoRoot, err := filepath.Abs("../..")
|
|
if err != nil {
|
|
t.Fatalf("Abs(repo root) error = %v", err)
|
|
}
|
|
manifest := Manifest{
|
|
SchemaVersion: SchemaVersion,
|
|
ID: pluginID,
|
|
Name: "Buildable Source",
|
|
Version: "0.1.0",
|
|
ArtifactType: ArtifactTypeSource,
|
|
Runtime: RuntimeManifest{
|
|
Type: RuntimeGoPlugin,
|
|
EntrySymbol: "Plugin",
|
|
MetadataSymbol: "MCGatewayPluginMetadata",
|
|
},
|
|
Build: BuildManifest{
|
|
Type: BuildTypeGo,
|
|
Entry: ".",
|
|
GoVersion: runtime.Version(),
|
|
Tags: []string{},
|
|
VendorRequired: false,
|
|
Output: RuntimeEntry,
|
|
},
|
|
APIVersion: APIVersion,
|
|
SDKModule: "github.com/tursom/mc-gateway/plugin/api",
|
|
SDKModuleVersion: "v0.1.0",
|
|
GoVersion: runtime.Version(),
|
|
GOOS: runtime.GOOS,
|
|
GOARCH: runtime.GOARCH,
|
|
ExtensionPoints: []ExtensionPoint{{
|
|
Type: "hook",
|
|
Key: ExtensionUpstreamConnect,
|
|
}},
|
|
Capabilities: json.RawMessage(`{"extension_points":["upstream.connect/v1"]}`),
|
|
}
|
|
manifestBytes, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
t.Fatalf("Marshal manifest error = %v", err)
|
|
}
|
|
mainSource := `package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net"
|
|
"runtime"
|
|
|
|
"github.com/tursom/mc-gateway/plugin/api"
|
|
)
|
|
|
|
type pluginImpl struct{ api.AbstractPlugin }
|
|
|
|
func Plugin() api.Plugin { return &pluginImpl{} }
|
|
|
|
func MCGatewayPluginMetadata() string { return manifestJSON }
|
|
|
|
func (p *pluginImpl) Init(gateway api.Gateway) error {
|
|
return api.RegisterHookHandler(
|
|
gateway,
|
|
api.HookUpstreamConnect,
|
|
func(api.UpstreamConnectRequest) bool { return true },
|
|
func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
left, right := net.Pipe()
|
|
_ = right.Close()
|
|
return left, nil
|
|
},
|
|
)
|
|
}
|
|
|
|
var manifestJSON = compactJSON(map[string]any{
|
|
"schema_version": "mc-gateway.plugin/v1",
|
|
"id": "` + pluginID + `",
|
|
"name": "Buildable Source",
|
|
"version": "0.1.0",
|
|
"artifact_type": "binary",
|
|
"runtime": map[string]any{
|
|
"type": "go-plugin",
|
|
"entry": "plugin.so",
|
|
"entry_symbol": "Plugin",
|
|
"metadata_symbol": "MCGatewayPluginMetadata",
|
|
},
|
|
"api_version": "plugin-api/v1",
|
|
"sdk_module": "github.com/tursom/mc-gateway/plugin/api",
|
|
"sdk_module_version": "v0.1.0",
|
|
"go_version": runtime.Version(),
|
|
"go_os": runtime.GOOS,
|
|
"go_arch": runtime.GOARCH,
|
|
"extension_points": []map[string]any{{"type": "hook", "key": "upstream.connect/v1"}},
|
|
"capabilities": map[string]any{"extension_points": []string{"upstream.connect/v1"}},
|
|
})
|
|
|
|
func compactJSON(value any) string {
|
|
data, _ := json.Marshal(value)
|
|
return string(data)
|
|
}
|
|
`
|
|
packagePath := writeTestMCGP(t, map[string][]byte{
|
|
"manifest.json": manifestBytes,
|
|
"go.mod": []byte("module example.com/" + pluginID + "\n\ngo 1.24.0\n\nrequire github.com/tursom/mc-gateway v0.0.0\n\nreplace github.com/tursom/mc-gateway => " + filepath.ToSlash(repoRoot) + "\n"),
|
|
"main.go": []byte(mainSource),
|
|
})
|
|
source, err := manager.UploadSource(context.Background(), ArtifactUpload{
|
|
SourcePath: packagePath,
|
|
FileName: pluginID + "-source.mcgp",
|
|
Actor: "admin",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UploadSource(%s) error = %v", pluginID, err)
|
|
}
|
|
return source
|
|
}
|
|
|
|
func waitForPluginManagerTest(t *testing.T, done func() bool) {
|
|
t.Helper()
|
|
deadline := time.After(2 * time.Second)
|
|
ticker := time.NewTicker(time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-deadline:
|
|
t.Fatal("timed out waiting for plugin manager condition")
|
|
case <-ticker.C:
|
|
if done() {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type fakeAdapter struct {
|
|
loads int
|
|
handlers map[string]api.UpstreamConnectHandler
|
|
loadErr error
|
|
loadErrs map[string]error
|
|
}
|
|
|
|
func (a *fakeAdapter) Load(_ context.Context, artifact ArtifactRecord, _ PluginRecord, gateway *Gateway) (api.Plugin, error) {
|
|
a.loads++
|
|
if a.loadErr != nil {
|
|
return nil, a.loadErr
|
|
}
|
|
if a.loadErrs != nil && a.loadErrs[artifact.PluginID] != nil {
|
|
return nil, a.loadErrs[artifact.PluginID]
|
|
}
|
|
handler := api.UpstreamConnectHandler(func(api.UpstreamConnectRequest) (net.Conn, error) {
|
|
return newMemoryConn(), nil
|
|
})
|
|
if a.handlers != nil && a.handlers[artifact.PluginID] != nil {
|
|
handler = a.handlers[artifact.PluginID]
|
|
}
|
|
if err := api.RegisterHookHandler(
|
|
gateway,
|
|
api.HookUpstreamConnect,
|
|
func(api.UpstreamConnectRequest) bool { return true },
|
|
handler,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return &fakePlugin{}, nil
|
|
}
|
|
|
|
type fakePlugin struct {
|
|
api.AbstractPlugin
|
|
}
|
|
|
|
type fakeBuilder struct {
|
|
result BuildResult
|
|
err error
|
|
}
|
|
|
|
func (b *fakeBuilder) Build(context.Context, ArtifactRecord, BuildRequest, BuildRecord) (BuildResult, error) {
|
|
return b.result, b.err
|
|
}
|
|
|
|
func newMemoryConn() net.Conn {
|
|
left, right := net.Pipe()
|
|
_ = right.Close()
|
|
return left
|
|
}
|