Files
mc-gateway/internal/pluginmanager/manager.go
tursom eae2319328
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
feat(plugin): add source package builder
2026-06-26 10:53:41 +08:00

1194 lines
37 KiB
Go

package pluginmanager
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net"
stdplugin "plugin"
"reflect"
"runtime"
"sort"
"sync"
"sync/atomic"
"time"
"github.com/tursom/mc-gateway/plugin/api"
)
type RuntimeAdapter interface {
Load(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord, gateway *Gateway) (api.Plugin, error)
}
type GoPluginAdapter struct{}
func (a GoPluginAdapter) Load(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord, gateway *Gateway) (api.Plugin, error) {
_ = ctx
opened, err := stdplugin.Open(artifact.FilePath)
if err != nil {
return nil, err
}
symbolName := "Plugin"
var manifest Manifest
if err := json.Unmarshal([]byte(artifact.MetadataJSON), &manifest); err == nil && manifest.Runtime.EntrySymbol != "" {
symbolName = manifest.Runtime.EntrySymbol
}
symbol, err := opened.Lookup(symbolName)
if err != nil {
return nil, err
}
factory, ok := symbol.(func() api.Plugin)
if !ok {
return nil, fmt.Errorf("plugin symbol %q has invalid signature", symbolName)
}
instance := factory()
cfg := instance.NewConfigObj()
if cfg != nil && pluginRecord.ConfigJSON != "" && canUnmarshalInto(cfg) {
if err := json.Unmarshal([]byte(pluginRecord.ConfigJSON), cfg); err != nil {
return nil, fmt.Errorf("decode plugin config: %w", err)
}
}
if err := instance.ReloadConfig(cfg); err != nil {
return nil, err
}
if err := instance.Init(gateway); err != nil {
return nil, err
}
return instance, nil
}
func canUnmarshalInto(value any) bool {
if value == nil {
return false
}
kind := reflect.TypeOf(value).Kind()
return kind == reflect.Pointer || kind == reflect.Map || kind == reflect.Slice
}
type Manager struct {
repo Repository
store ArtifactStore
adapter RuntimeAdapter
builders map[string]SourceBuilder
handleConn func(net.Conn)
wg *sync.WaitGroup
mu sync.Mutex
loaded map[string]*loadedPlugin
snapshot atomic.Value
proxyMu sync.Mutex
proxySeq uint64
proxyConns map[uint64]*proxyConnection
drainingIDs map[string]bool
}
type loadedPlugin struct {
record PluginRecord
artifact ArtifactRecord
instance api.Plugin
gateway *Gateway
handlers []*upstreamHandler
}
type upstreamHandler struct {
pluginID string
artifactID string
priority int
handlerID string
mode string
timeout time.Duration
initialWriteTimeout time.Duration
accept func(api.UpstreamConnectRequest) bool
handle func(api.UpstreamConnectRequest) (net.Conn, error)
calls atomic.Uint64
errors atomic.Uint64
panics atomic.Uint64
timeouts atomic.Uint64
blocked atomic.Uint64
activeProxy atomic.Int64
proxyStarted atomic.Uint64
proxyCompleted atomic.Uint64
proxyErrors atomic.Uint64
proxyBytesIn atomic.Uint64
proxyBytesOut atomic.Uint64
proxyDuration atomic.Uint64
}
type proxyConnection struct {
id uint64
pluginID string
artifactID string
handlerID string
handler *upstreamHandler
client net.Conn
endpoint net.Conn
startedAt time.Time
draining bool
}
type ProxyConnectionHandle struct {
manager *Manager
id uint64
}
type ProxyConnectionStats struct {
BytesToPlugin int64
BytesToClient int64
Duration time.Duration
Err error
}
type Options struct {
DB *sql.DB
ArtifactRoot string
HandleConn func(net.Conn)
WaitGroup *sync.WaitGroup
Adapter RuntimeAdapter
Builders map[string]SourceBuilder
}
func New(options Options) *Manager {
adapter := options.Adapter
if adapter == nil {
adapter = GoPluginAdapter{}
}
manager := &Manager{
repo: NewRepository(options.DB),
store: NewArtifactStore(options.ArtifactRoot),
adapter: adapter,
builders: options.Builders,
handleConn: options.HandleConn,
wg: options.WaitGroup,
loaded: make(map[string]*loadedPlugin),
proxyConns: make(map[uint64]*proxyConnection),
drainingIDs: make(map[string]bool),
}
if manager.builders == nil {
manager.builders = map[string]SourceBuilder{
BuilderTypeLocalProcess: LocalProcessBuilder{StoreRoot: options.ArtifactRoot},
BuilderTypeContainer: ContainerBuilder{},
}
}
manager.publish(nil)
return manager
}
func (m *Manager) UploadArtifact(ctx context.Context, upload ArtifactUpload) (ArtifactRecord, error) {
artifact, err := m.store.ValidateAndStore(upload)
if err != nil {
_ = m.repo.RecordOperation(ctx, "", "", "artifact_upload", "failed", upload.Actor, err.Error(), nil)
return ArtifactRecord{}, err
}
if artifact.ArtifactType == ArtifactTypeSource {
return m.saveSourceArtifact(ctx, upload.Actor, artifact, "artifact_upload")
}
if err := m.repo.SaveArtifact(ctx, artifact); err != nil {
return ArtifactRecord{}, err
}
_ = m.repo.RecordOperation(ctx, artifact.PluginID, artifact.ID, "artifact_upload", "succeeded", upload.Actor, "artifact uploaded", map[string]any{
"sha256": artifact.SHA256,
"package_sha256": artifact.PackageSHA256,
"api_version": artifact.APIVersion,
"extension_points": artifact.ExtensionPointsJSON,
})
return artifact, nil
}
func (m *Manager) UploadSource(ctx context.Context, upload ArtifactUpload) (ArtifactRecord, error) {
artifact, err := m.store.ValidateAndStoreSource(upload)
if err != nil {
_ = m.repo.RecordOperation(ctx, "", "", "source_upload", "failed", upload.Actor, err.Error(), nil)
return ArtifactRecord{}, err
}
return m.saveSourceArtifact(ctx, upload.Actor, artifact, "source_upload")
}
func (m *Manager) saveSourceArtifact(ctx context.Context, actor string, artifact ArtifactRecord, operation string) (ArtifactRecord, error) {
if err := m.repo.SaveArtifact(ctx, artifact); err != nil {
return ArtifactRecord{}, err
}
build, err := m.CreateBuild(ctx, actor, BuildRequest{SourceID: artifact.ID})
if err != nil {
_ = m.repo.RecordOperation(ctx, artifact.PluginID, artifact.ID, "source_build_queue", "failed", actor, err.Error(), nil)
return ArtifactRecord{}, err
}
_ = m.repo.RecordOperation(ctx, artifact.PluginID, artifact.ID, operation, "succeeded", actor, "source package uploaded", map[string]any{
"source_sha256": artifact.SHA256,
"api_version": artifact.APIVersion,
"go_version": artifact.GoVersion,
"build_id": build.ID,
})
return artifact, nil
}
func (m *Manager) BuildSource(ctx context.Context, actor string, req BuildRequest) (BuildRecord, error) {
build, err := m.CreateBuild(ctx, actor, req)
if err != nil {
return BuildRecord{}, err
}
return m.RunBuild(ctx, actor, build.ID)
}
func (m *Manager) CreateBuild(ctx context.Context, actor string, req BuildRequest) (BuildRecord, error) {
source, err := m.repo.Artifact(ctx, req.SourceID)
if err != nil {
return BuildRecord{}, err
}
if source.ArtifactType != ArtifactTypeSource {
return BuildRecord{}, fmt.Errorf("artifact %s is %q, want source", source.ID, source.ArtifactType)
}
req = defaultBuildRequest(req, source)
if req.GOOS != runtime.GOOS || req.GOARCH != runtime.GOARCH {
return BuildRecord{}, fmt.Errorf("build target %s/%s does not match gateway %s/%s", req.GOOS, req.GOARCH, runtime.GOOS, runtime.GOARCH)
}
builder := m.builders[req.BuilderType]
if builder == nil {
return BuildRecord{}, fmt.Errorf("builder type %q is not available", req.BuilderType)
}
build, err := m.repo.CreateBuild(ctx, BuildRecord{
PluginID: source.PluginID,
SourceID: source.ID,
Status: BuildStatusQueued,
BuilderType: req.BuilderType,
BuilderImage: req.BuilderImage,
BuilderVersion: req.BuilderVersion,
GOOS: req.GOOS,
GOARCH: req.GOARCH,
GOAMD64: req.GOAMD64,
GOARM64: req.GOARM64,
CGOEnabled: req.CGOEnabled,
BuildTags: req.BuildTags,
SDKModule: req.SDKModule,
SDKVersion: req.SDKVersion,
GOPROXY: req.GOPROXY,
GONOSUMDB: req.GONOSUMDB,
GOPRIVATE: req.GOPRIVATE,
VendorRequired: req.VendorRequired,
SourceSHA256: source.SHA256,
ModuleSummary: "[]",
GoVersionM: "{}",
MetadataJSON: "{}",
CreatedBy: actor,
})
if err != nil {
return BuildRecord{}, err
}
_ = m.repo.RecordOperation(ctx, source.PluginID, source.ID, "source_build_queue", "succeeded", actor, "source build queued", map[string]any{
"build_id": build.ID,
"builder_type": req.BuilderType,
"source_sha256": source.SHA256,
})
return build, nil
}
func (m *Manager) RunBuild(ctx context.Context, actor string, buildID int64) (BuildRecord, error) {
build, err := m.repo.Build(ctx, buildID)
if err != nil {
return BuildRecord{}, err
}
if build.Status != BuildStatusQueued {
return BuildRecord{}, fmt.Errorf("build status %q cannot be run", build.Status)
}
source, err := m.repo.Artifact(ctx, build.SourceID)
if err != nil {
return BuildRecord{}, err
}
req := BuildRequest{
SourceID: build.SourceID,
BuilderType: build.BuilderType,
BuilderImage: build.BuilderImage,
BuilderVersion: build.BuilderVersion,
GOOS: build.GOOS,
GOARCH: build.GOARCH,
GOAMD64: build.GOAMD64,
GOARM64: build.GOARM64,
CGOEnabled: build.CGOEnabled,
BuildTags: build.BuildTags,
SDKModule: build.SDKModule,
SDKVersion: build.SDKVersion,
GOPROXY: build.GOPROXY,
GONOSUMDB: build.GONOSUMDB,
GOPRIVATE: build.GOPRIVATE,
VendorRequired: build.VendorRequired,
}
builder := m.builders[build.BuilderType]
if builder == nil {
return BuildRecord{}, fmt.Errorf("builder type %q is not available", build.BuilderType)
}
start := time.Now()
if err := m.repo.MarkBuildRunning(ctx, build.ID); err != nil {
return BuildRecord{}, err
}
build, _ = m.repo.Build(ctx, build.ID)
result, buildErr := builder.Build(ctx, source, req, build)
build.StartedAt = start.Unix()
build.EndedAt = time.Now().Unix()
build.DurationMS = buildDurationMS(start)
build.GoVersion = result.GoVersion
build.ModuleSummary = result.ModuleSummary
build.GoVersionM = result.GoVersionM
build.ABIFingerprint = result.ABIFingerprint
build.LogSummary = result.LogSummary
build.SourceSHA256 = source.SHA256
if buildErr != nil {
build.Status = BuildStatusFailed
build.Error = buildErr.Error()
_ = m.repo.FinishBuild(ctx, build)
_ = m.repo.RecordOperation(ctx, source.PluginID, source.ID, "source_build", "failed", actor, buildErr.Error(), map[string]any{
"build_id": build.ID,
"source_sha256": source.SHA256,
"builder_type": req.BuilderType,
"log_summary": result.LogSummary,
"active_changed": false,
})
return m.repo.Build(ctx, build.ID)
}
if result.Manifest.GoVersion != result.GoVersion {
build.Status = BuildStatusFailed
build.Error = fmt.Sprintf("manifest go_version %q does not match built Go version %q", result.Manifest.GoVersion, result.GoVersion)
_ = m.repo.FinishBuild(ctx, build)
return m.repo.Build(ctx, build.ID)
}
metadata := result.Metadata
if metadata == nil {
metadata = map[string]any{}
}
metadata["build_id"] = build.ID
metadata["module_summary"] = json.RawMessage(defaultJSONArray(result.ModuleSummary))
metadata["go_version_m"] = json.RawMessage(defaultJSONObject(result.GoVersionM))
artifact, err := m.store.StoreBuiltBinary(ArtifactUpload{
FileName: result.Manifest.ID + "-" + result.Manifest.Version + ".mcgp",
Actor: actor,
}, result.Manifest, result.ArtifactBytes, source.PackageSHA256, metadata)
if err != nil {
build.Status = BuildStatusFailed
build.Error = err.Error()
_ = m.repo.FinishBuild(ctx, build)
return m.repo.Build(ctx, build.ID)
}
if err := m.repo.SaveArtifact(ctx, artifact); err != nil {
build.Status = BuildStatusFailed
build.Error = err.Error()
_ = m.repo.FinishBuild(ctx, build)
return m.repo.Build(ctx, build.ID)
}
build.Status = BuildStatusSucceeded
build.ArtifactID = artifact.ID
build.ArtifactSHA256 = artifact.SHA256
metadataBytes, _ := json.Marshal(metadata)
build.MetadataJSON = string(metadataBytes)
if err := m.repo.FinishBuild(ctx, build); err != nil {
return BuildRecord{}, err
}
_ = m.repo.RecordOperation(ctx, source.PluginID, artifact.ID, "source_build", "succeeded", actor, "source build succeeded", map[string]any{
"build_id": build.ID,
"source_id": source.ID,
"source_sha256": source.SHA256,
"artifact_sha256": artifact.SHA256,
"builder_type": req.BuilderType,
"go_version": result.GoVersion,
})
return m.repo.Build(ctx, build.ID)
}
func (m *Manager) SetDesired(ctx context.Context, actor, pluginID, artifactID, desiredState, configJSON string, priority int) (PluginRecord, error) {
pluginRecord, err := m.repo.UpsertDesired(ctx, actor, pluginID, artifactID, desiredState, configJSON, priority)
if err != nil {
_ = m.repo.RecordOperation(ctx, pluginID, artifactID, "desired_update", "failed", actor, err.Error(), nil)
return PluginRecord{}, err
}
_ = m.repo.RecordOperation(ctx, pluginID, artifactID, "desired_update", "succeeded", actor, "desired state updated", map[string]any{
"desired_state": desiredState,
"desired_generation": pluginRecord.DesiredGeneration,
"priority": pluginRecord.Priority,
})
return pluginRecord, nil
}
func (m *Manager) Load(ctx context.Context, actor, pluginID string) (PluginRecord, error) {
m.mu.Lock()
defer m.mu.Unlock()
pluginRecord, err := m.repo.Plugin(ctx, pluginID)
if err != nil {
return PluginRecord{}, err
}
loaded, err := m.loadLocked(ctx, pluginRecord)
if err != nil {
_ = m.repo.RecordOperation(ctx, pluginID, pluginRecord.DesiredArtifactID, "load", "failed", actor, err.Error(), nil)
return PluginRecord{}, err
}
_ = m.repo.RecordOperation(ctx, pluginID, loaded.artifact.ID, "load", "succeeded", actor, "plugin loaded", nil)
return m.repo.Plugin(ctx, pluginID)
}
func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRecord, error) {
m.mu.Lock()
defer m.mu.Unlock()
pluginRecord, err := m.repo.Plugin(ctx, pluginID)
if err != nil {
return PluginRecord{}, err
}
if pluginRecord.DesiredState != DesiredEnabled {
pluginRecord, err = m.repo.UpsertDesired(ctx, actor, pluginRecord.ID, pluginRecord.DesiredArtifactID, DesiredEnabled, pluginRecord.ConfigJSON, pluginRecord.Priority)
if err != nil {
return PluginRecord{}, err
}
}
loaded, err := m.loadLocked(ctx, pluginRecord)
if err != nil {
_ = m.repo.RecordOperation(ctx, pluginID, pluginRecord.DesiredArtifactID, "enable", "failed", actor, err.Error(), nil)
return PluginRecord{}, err
}
if len(loaded.handlers) == 0 {
err := fmt.Errorf("plugin %q did not register %s", pluginID, ExtensionUpstreamConnect)
_ = m.repo.MarkRuntime(ctx, pluginID, RuntimeFailed, "", loaded.artifact.ID, pluginRecord.AppliedGeneration, err.Error(), map[string]any{"error": err.Error()}, nil)
_ = m.repo.RecordOperation(ctx, pluginID, pluginRecord.DesiredArtifactID, "enable", "failed", actor, err.Error(), nil)
return PluginRecord{}, err
}
current := m.currentHandlersLocked()
current[pluginID] = loaded.handlers
next := flattenHandlers(current)
if err := m.markEnabled(ctx, loaded); err != nil {
return PluginRecord{}, err
}
m.clearDrainingLocked(pluginID)
m.publish(next)
_ = m.repo.UpdateArtifactStatus(ctx, loaded.artifact.ID, ArtifactStatusLoaded, "")
_ = m.repo.RecordOperation(ctx, pluginID, loaded.artifact.ID, "enable", "succeeded", actor, "plugin enabled", map[string]any{
"desired_generation": loaded.record.DesiredGeneration,
"handler_count": len(loaded.handlers),
})
return m.repo.Plugin(ctx, pluginID)
}
func (m *Manager) Disable(ctx context.Context, actor, pluginID string) (PluginRecord, error) {
m.mu.Lock()
defer m.mu.Unlock()
pluginRecord, err := m.repo.Plugin(ctx, pluginID)
if err != nil {
return PluginRecord{}, err
}
pluginRecord, err = m.repo.UpsertDesired(ctx, actor, pluginRecord.ID, pluginRecord.DesiredArtifactID, DesiredDisabled, pluginRecord.ConfigJSON, pluginRecord.Priority)
if err != nil {
return PluginRecord{}, err
}
m.removeFromDispatchLocked(pluginID)
m.markDrainingLocked(pluginID)
if loaded := m.loaded[pluginID]; loaded != nil && loaded.instance != nil {
if err := loaded.instance.Destroy(); err != nil {
_ = m.repo.RecordOperation(ctx, pluginID, loaded.artifact.ID, "disable", "warning", actor, err.Error(), nil)
}
}
runtimeState := RuntimeDisabled
if m.activeProxyCountLocked(pluginID) > 0 {
runtimeState = RuntimeDraining
}
delete(m.loaded, pluginID)
if err := m.repo.MarkRuntime(ctx, pluginID, runtimeState, "", "", pluginRecord.DesiredGeneration, "", map[string]any{
"active_proxy_connections": m.activeProxyCountLocked(pluginID),
}, nil); err != nil {
return PluginRecord{}, err
}
_ = m.repo.RecordOperation(ctx, pluginID, pluginRecord.DesiredArtifactID, "disable", "succeeded", actor, "plugin disabled", nil)
return m.repo.Plugin(ctx, pluginID)
}
func (m *Manager) Delete(ctx context.Context, actor, pluginID string) error {
m.mu.Lock()
defer m.mu.Unlock()
pluginRecord, err := m.repo.Plugin(ctx, pluginID)
if err != nil {
return err
}
m.removeFromDispatchLocked(pluginID)
m.markDrainingLocked(pluginID)
if loaded := m.loaded[pluginID]; loaded != nil && loaded.instance != nil {
_ = loaded.instance.Destroy()
}
delete(m.loaded, pluginID)
if _, err := m.repo.UpsertDesired(ctx, actor, pluginRecord.ID, pluginRecord.DesiredArtifactID, DesiredDeleted, pluginRecord.ConfigJSON, pluginRecord.Priority); err != nil {
return err
}
_ = m.repo.RecordOperation(ctx, pluginID, pluginRecord.DesiredArtifactID, "delete", "succeeded", actor, "plugin deleted", map[string]any{
"cleanup": "pending_restart_for_loaded_go_plugin",
})
return nil
}
func (m *Manager) Reconcile(ctx context.Context) error {
m.mu.Lock()
defer m.mu.Unlock()
desired, err := m.repo.DesiredEnabled(ctx)
if err != nil {
return err
}
nextByPlugin := make(map[string][]*upstreamHandler)
for _, pluginRecord := range desired {
loaded, err := m.loadLocked(ctx, pluginRecord)
if err != nil {
_ = m.repo.MarkRuntime(ctx, pluginRecord.ID, RuntimeFailed, "", "", pluginRecord.AppliedGeneration, err.Error(), map[string]any{"error": err.Error()}, nil)
_ = m.repo.RecordOperation(ctx, pluginRecord.ID, pluginRecord.DesiredArtifactID, "reconcile", "failed", "system", err.Error(), nil)
continue
}
if len(loaded.handlers) == 0 {
err := fmt.Errorf("plugin %q did not register %s", pluginRecord.ID, ExtensionUpstreamConnect)
_ = m.repo.MarkRuntime(ctx, pluginRecord.ID, RuntimeFailed, "", loaded.artifact.ID, pluginRecord.AppliedGeneration, err.Error(), map[string]any{"error": err.Error()}, nil)
_ = m.repo.RecordOperation(ctx, pluginRecord.ID, pluginRecord.DesiredArtifactID, "reconcile", "failed", "system", err.Error(), nil)
continue
}
nextByPlugin[pluginRecord.ID] = loaded.handlers
_ = m.markEnabled(ctx, loaded)
m.clearDrainingLocked(pluginRecord.ID)
}
m.publish(flattenHandlers(nextByPlugin))
return nil
}
func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRequest) (UpstreamResult, error) {
value := m.snapshot.Load()
if value == nil {
return UpstreamResult{}, nil
}
handlers, ok := value.([]*upstreamHandler)
if !ok {
return UpstreamResult{}, nil
}
if req.Context == nil {
req.Context = ctx
}
req.InitialData = append([]byte(nil), req.InitialData...)
for _, handler := range handlers {
accepted, err := handler.accepts(req)
if err != nil {
return UpstreamResult{Handled: true}, err
}
if !accepted {
continue
}
conn, err := handler.invoke(req)
if errors.Is(err, api.ErrPass) {
continue
}
if err != nil {
return UpstreamResult{Handled: true}, err
}
if conn != nil {
result := UpstreamResult{
Conn: conn,
Handled: true,
Mode: handler.mode,
PluginID: handler.pluginID,
HandlerID: handler.handlerID,
}
if handler.mode == UpstreamModeProtocolProxy {
return m.startProtocolProxy(ctx, handler, result, req)
}
return result, nil
}
}
return UpstreamResult{}, nil
}
func (m *Manager) startProtocolProxy(ctx context.Context, handler *upstreamHandler, result UpstreamResult, req api.UpstreamConnectRequest) (UpstreamResult, error) {
endpoint := result.Conn
initial := append([]byte(nil), req.InitialData...)
if len(initial) > 0 {
if handler.initialWriteTimeout > 0 {
_ = endpoint.SetWriteDeadline(time.Now().Add(handler.initialWriteTimeout))
defer endpoint.SetWriteDeadline(time.Time{})
}
if err := writeAll(endpoint, initial); err != nil {
handler.proxyErrors.Add(1)
_ = endpoint.Close()
return UpstreamResult{Handled: true, Mode: handler.mode, PluginID: handler.pluginID, HandlerID: handler.handlerID}, fmt.Errorf("plugin %s protocol-proxy initial replay failed: %w", handler.pluginID, err)
}
}
handle := m.TrackProxyConnection(result, req.Source, endpoint)
if handle == nil {
_ = endpoint.Close()
return UpstreamResult{Handled: true, Mode: handler.mode, PluginID: handler.pluginID, HandlerID: handler.handlerID}, fmt.Errorf("plugin %s protocol-proxy tracking failed", handler.pluginID)
}
runProtocolProxy(ctx, handle, req.Source, endpoint)
return UpstreamResult{
Handled: true,
Mode: handler.mode,
PluginID: handler.pluginID,
HandlerID: handler.handlerID,
InitialDataSent: len(initial) > 0,
Proxied: true,
}, nil
}
func (h *upstreamHandler) accepts(req api.UpstreamConnectRequest) (accepted bool, err error) {
if h.accept == nil {
return true, nil
}
defer func() {
if rec := recover(); rec != nil {
h.panics.Add(1)
accepted = false
err = fmt.Errorf("plugin %s acceptor panic: %v", h.pluginID, rec)
}
}()
return h.accept(req), nil
}
func (m *Manager) ListArtifacts(ctx context.Context, pluginID string) ([]ArtifactRecord, error) {
return m.repo.ListArtifacts(ctx, pluginID)
}
func (m *Manager) ListBuilds(ctx context.Context, pluginID string) ([]BuildRecord, error) {
return m.repo.ListBuilds(ctx, pluginID)
}
func (m *Manager) Build(ctx context.Context, id int64) (BuildRecord, error) {
return m.repo.Build(ctx, id)
}
func (m *Manager) CancelBuild(ctx context.Context, actor string, id int64) (BuildRecord, error) {
build, err := m.repo.CancelBuild(ctx, id, actor)
if err != nil {
return BuildRecord{}, err
}
_ = m.repo.RecordOperation(ctx, build.PluginID, build.SourceID, "source_build_cancel", "succeeded", actor, "build canceled", map[string]any{"build_id": id})
return build, nil
}
func (m *Manager) RetryBuild(ctx context.Context, actor string, id int64) (BuildRecord, error) {
build, err := m.repo.Build(ctx, id)
if err != nil {
return BuildRecord{}, err
}
if build.Status != BuildStatusFailed && build.Status != BuildStatusCanceled {
return BuildRecord{}, fmt.Errorf("build status %q cannot be retried", build.Status)
}
next, err := m.CreateBuild(ctx, actor, BuildRequest{
SourceID: build.SourceID,
BuilderType: build.BuilderType,
BuilderImage: build.BuilderImage,
BuilderVersion: build.BuilderVersion,
GOOS: build.GOOS,
GOARCH: build.GOARCH,
GOAMD64: build.GOAMD64,
GOARM64: build.GOARM64,
CGOEnabled: build.CGOEnabled,
BuildTags: build.BuildTags,
SDKModule: build.SDKModule,
SDKVersion: build.SDKVersion,
GOPROXY: build.GOPROXY,
GONOSUMDB: build.GONOSUMDB,
GOPRIVATE: build.GOPRIVATE,
VendorRequired: build.VendorRequired,
})
if err != nil {
return BuildRecord{}, err
}
return m.RunBuild(ctx, actor, next.ID)
}
func (m *Manager) Artifact(ctx context.Context, id string) (ArtifactRecord, error) {
return m.repo.Artifact(ctx, id)
}
func (m *Manager) ListPlugins(ctx context.Context) ([]PluginRecord, error) {
return m.repo.ListPlugins(ctx)
}
func (m *Manager) Plugin(ctx context.Context, id string) (PluginRecord, error) {
return m.repo.Plugin(ctx, id)
}
func (m *Manager) DispatchPlan(ctx context.Context) DispatchPlan {
value := m.snapshot.Load()
plan := DispatchPlan{UpdatedAt: time.Now().Unix()}
if handlers, ok := value.([]*upstreamHandler); ok {
plan.Handlers = handlerSummaries(handlers)
}
return plan
}
func (m *Manager) TrackProxyConnection(result UpstreamResult, client, endpoint net.Conn) *ProxyConnectionHandle {
if result.Mode != UpstreamModeProtocolProxy || client == nil || endpoint == nil {
return nil
}
handler := m.findHandler(result.PluginID, result.HandlerID)
if handler == nil {
return nil
}
id := atomic.AddUint64(&m.proxySeq, 1)
proxyConn := &proxyConnection{
id: id,
pluginID: result.PluginID,
artifactID: handler.artifactID,
handlerID: result.HandlerID,
handler: handler,
client: client,
endpoint: endpoint,
startedAt: time.Now(),
}
handler.activeProxy.Add(1)
handler.proxyStarted.Add(1)
m.proxyMu.Lock()
proxyConn.draining = m.drainingIDs[result.PluginID]
m.proxyConns[id] = proxyConn
m.proxyMu.Unlock()
return &ProxyConnectionHandle{manager: m, id: id}
}
func (h *ProxyConnectionHandle) Finish(stats ProxyConnectionStats) {
if h == nil || h.manager == nil {
return
}
h.manager.finishProxyConnection(h.id, stats)
}
func (m *Manager) ForceCloseDraining(ctx context.Context, actor, pluginID string) (int, error) {
_ = ctx
var conns []*proxyConnection
m.proxyMu.Lock()
for _, conn := range m.proxyConns {
if conn.pluginID == pluginID && conn.draining {
conns = append(conns, conn)
}
}
m.proxyMu.Unlock()
for _, conn := range conns {
_ = conn.client.Close()
_ = conn.endpoint.Close()
}
_ = m.repo.RecordOperation(ctx, pluginID, "", "force_close_draining", "succeeded", actor, "draining protocol-proxy connections force closed", map[string]any{
"closed": len(conns),
})
return len(conns), nil
}
func (m *Manager) findHandler(pluginID, handlerID string) *upstreamHandler {
value := m.snapshot.Load()
if handlers, ok := value.([]*upstreamHandler); ok {
for _, handler := range handlers {
if handler.pluginID == pluginID && handler.handlerID == handlerID {
return handler
}
}
}
m.mu.Lock()
defer m.mu.Unlock()
if loaded := m.loaded[pluginID]; loaded != nil {
for _, handler := range loaded.handlers {
if handler.handlerID == handlerID {
return handler
}
}
}
return nil
}
func (m *Manager) finishProxyConnection(id uint64, stats ProxyConnectionStats) {
m.proxyMu.Lock()
proxyConn := m.proxyConns[id]
delete(m.proxyConns, id)
m.proxyMu.Unlock()
if proxyConn == nil || proxyConn.handler == nil {
return
}
proxyConn.handler.activeProxy.Add(-1)
proxyConn.handler.proxyCompleted.Add(1)
if stats.Err != nil {
proxyConn.handler.proxyErrors.Add(1)
}
if stats.BytesToPlugin > 0 {
proxyConn.handler.proxyBytesIn.Add(uint64(stats.BytesToPlugin))
}
if stats.BytesToClient > 0 {
proxyConn.handler.proxyBytesOut.Add(uint64(stats.BytesToClient))
}
if stats.Duration > 0 {
proxyConn.handler.proxyDuration.Add(uint64(stats.Duration.Milliseconds()))
}
}
func (m *Manager) loadLocked(ctx context.Context, pluginRecord PluginRecord) (*loadedPlugin, error) {
if loaded := m.loaded[pluginRecord.ID]; loaded != nil &&
loaded.artifact.ID == pluginRecord.DesiredArtifactID &&
loaded.record.DesiredGeneration == pluginRecord.DesiredGeneration {
return loaded, nil
}
artifact, err := m.repo.Artifact(ctx, pluginRecord.DesiredArtifactID)
if err != nil {
return nil, err
}
if artifact.Status == ArtifactStatusDeleted || artifact.Status == ArtifactStatusRejected {
return nil, fmt.Errorf("artifact status %q is not loadable", artifact.Status)
}
if artifact.ArtifactType != ArtifactTypeBinary {
return nil, fmt.Errorf("artifact type %q is not loadable", artifact.ArtifactType)
}
if artifact.GoVersion != runtime.Version() {
return nil, fmt.Errorf("artifact go_version %q does not match gateway %q", artifact.GoVersion, runtime.Version())
}
if artifact.GOOS != runtime.GOOS || artifact.GOARCH != runtime.GOARCH {
return nil, fmt.Errorf("artifact target %s/%s does not match gateway %s/%s", artifact.GOOS, artifact.GOARCH, runtime.GOOS, runtime.GOARCH)
}
gateway := NewGateway(pluginRecord.ID, m.handleConn, m.wg)
instance, err := m.adapter.Load(ctx, artifact, pluginRecord, gateway)
if err != nil {
_ = m.repo.MarkRuntime(ctx, pluginRecord.ID, RuntimeFailed, "", "", pluginRecord.AppliedGeneration, err.Error(), map[string]any{"error": err.Error()}, nil)
return nil, err
}
handlers := buildHandlers(pluginRecord, artifact, gateway)
loaded := &loadedPlugin{
record: pluginRecord,
artifact: artifact,
instance: instance,
gateway: gateway,
handlers: handlers,
}
m.loaded[pluginRecord.ID] = loaded
if err := m.repo.MarkRuntime(ctx, pluginRecord.ID, RuntimeLoaded, "", artifact.ID, pluginRecord.AppliedGeneration, "", map[string]any{
"handler_count": len(handlers),
}, handlerSummaries(handlers)); err != nil {
return nil, err
}
return loaded, nil
}
func (m *Manager) markEnabled(ctx context.Context, loaded *loadedPlugin) error {
return m.repo.MarkRuntime(ctx, loaded.record.ID, RuntimeEnabled, loaded.artifact.ID, loaded.artifact.ID, loaded.record.DesiredGeneration, "", map[string]any{
"handler_count": len(loaded.handlers),
}, handlerSummaries(loaded.handlers))
}
func (m *Manager) currentHandlersLocked() map[string][]*upstreamHandler {
current := make(map[string][]*upstreamHandler)
value := m.snapshot.Load()
if handlers, ok := value.([]*upstreamHandler); ok {
for _, handler := range handlers {
current[handler.pluginID] = append(current[handler.pluginID], handler)
}
}
return current
}
func (m *Manager) removeFromDispatchLocked(pluginID string) {
current := m.currentHandlersLocked()
delete(current, pluginID)
m.publish(flattenHandlers(current))
}
func (m *Manager) markDrainingLocked(pluginID string) {
m.proxyMu.Lock()
defer m.proxyMu.Unlock()
m.drainingIDs[pluginID] = true
for _, conn := range m.proxyConns {
if conn.pluginID == pluginID {
conn.draining = true
}
}
}
func (m *Manager) clearDrainingLocked(pluginID string) {
m.proxyMu.Lock()
delete(m.drainingIDs, pluginID)
m.proxyMu.Unlock()
}
func (m *Manager) activeProxyCountLocked(pluginID string) int {
m.proxyMu.Lock()
defer m.proxyMu.Unlock()
count := 0
for _, conn := range m.proxyConns {
if conn.pluginID == pluginID {
count++
}
}
return count
}
func (m *Manager) publish(handlers []*upstreamHandler) {
sort.SliceStable(handlers, func(i, j int) bool {
if handlers[i].priority != handlers[j].priority {
return handlers[i].priority < handlers[j].priority
}
if handlers[i].pluginID != handlers[j].pluginID {
return handlers[i].pluginID < handlers[j].pluginID
}
return handlers[i].handlerID < handlers[j].handlerID
})
m.snapshot.Store(handlers)
}
func buildHandlers(pluginRecord PluginRecord, artifact ArtifactRecord, gateway *Gateway) []*upstreamHandler {
timeout := DefaultHandlerTimeout
initialWriteTimeout := DefaultInitialWriteTimeout
var manifest Manifest
if err := json.Unmarshal([]byte(artifact.MetadataJSON), &manifest); err == nil {
if manifest.RuntimeLimits.HandlerTimeoutMS > 0 {
timeout = time.Duration(manifest.RuntimeLimits.HandlerTimeoutMS) * time.Millisecond
}
if manifest.RuntimeLimits.InitialWriteTimeoutMS > 0 {
initialWriteTimeout = time.Duration(manifest.RuntimeLimits.InitialWriteTimeoutMS) * time.Millisecond
}
}
var handlers []*upstreamHandler
mode := upstreamModeFromArtifact(artifact)
if hook, ok := gateway.UpstreamConnectHandler(); ok {
handlers = append(handlers, &upstreamHandler{
pluginID: pluginRecord.ID,
artifactID: artifact.ID,
priority: pluginRecord.Priority,
handlerID: "upstream.connect/v1",
mode: mode,
timeout: timeout,
initialWriteTimeout: initialWriteTimeout,
accept: hook.Acceptor(),
handle: hook.Handler(),
})
}
if hook, ok := gateway.LegacyUpstreamHandler(); ok {
acceptor := hook.Acceptor()
handler := hook.Handler()
handlers = append(handlers, &upstreamHandler{
pluginID: pluginRecord.ID,
artifactID: artifact.ID,
priority: pluginRecord.Priority,
handlerID: "legacy-upstream",
mode: UpstreamModeDialer,
timeout: timeout,
initialWriteTimeout: initialWriteTimeout,
accept: func(req api.UpstreamConnectRequest) bool {
return acceptor(req.Source, req.Upstream)
},
handle: func(req api.UpstreamConnectRequest) (net.Conn, error) {
return handler(req.Source, req.Upstream)
},
})
}
return handlers
}
func upstreamModeFromArtifact(artifact ArtifactRecord) string {
var summary CapabilitySummary
if err := json.Unmarshal([]byte(artifact.CapabilitiesSummaryJSON), &summary); err == nil {
switch summary.UpstreamConnect.Mode {
case UpstreamModeProtocolProxy:
return UpstreamModeProtocolProxy
case UpstreamModeDialer:
return UpstreamModeDialer
}
}
return UpstreamModeDialer
}
func (h *upstreamHandler) invoke(req api.UpstreamConnectRequest) (conn net.Conn, err error) {
h.calls.Add(1)
ctx := req.Context
if ctx == nil {
ctx = context.Background()
}
if h.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, h.timeout)
defer cancel()
}
req.Context = ctx
done := make(chan result, 1)
go func() {
defer func() {
if rec := recover(); rec != nil {
h.panics.Add(1)
done <- result{err: fmt.Errorf("plugin %s panic: %v", h.pluginID, rec)}
}
}()
conn, err := h.handle(req)
done <- result{conn: conn, err: err}
}()
select {
case <-ctx.Done():
h.timeouts.Add(1)
go closeLateConn(done)
return nil, ctx.Err()
case result := <-done:
if errors.Is(result.err, api.ErrBlocked) {
h.blocked.Add(1)
}
if result.err != nil && !errors.Is(result.err, api.ErrPass) {
h.errors.Add(1)
}
return result.conn, result.err
}
}
type result struct {
conn net.Conn
err error
}
func closeLateConn(done <-chan result) {
result := <-done
if result.conn != nil {
_ = result.conn.Close()
}
}
type proxyCopyResult struct {
toPlugin bool
bytes int64
err error
}
type closeWriter interface {
CloseWrite() error
}
type closeReader interface {
CloseRead() error
}
func runProtocolProxy(ctx context.Context, handle *ProxyConnectionHandle, client, endpoint net.Conn) {
start := time.Now()
defer client.Close()
defer endpoint.Close()
done := make(chan proxyCopyResult, 2)
stopContext := make(chan struct{})
if ctx != nil {
go func() {
select {
case <-ctx.Done():
_ = client.Close()
_ = endpoint.Close()
case <-stopContext:
}
}()
}
go copyProtocolProxy(endpoint, client, true, done)
go copyProtocolProxy(client, endpoint, false, done)
var stats ProxyConnectionStats
for i := 0; i < 2; i++ {
result := <-done
if result.toPlugin {
stats.BytesToPlugin += result.bytes
} else {
stats.BytesToClient += result.bytes
}
if result.err != nil && !errors.Is(result.err, io.EOF) && stats.Err == nil {
stats.Err = result.err
}
}
close(stopContext)
stats.Duration = time.Since(start)
handle.Finish(stats)
}
func copyProtocolProxy(dst io.Writer, src io.Reader, toPlugin bool, done chan<- proxyCopyResult) {
result := proxyCopyResult{toPlugin: toPlugin}
defer func() {
if rec := recover(); rec != nil {
result.err = fmt.Errorf("protocol-proxy copy panic: %v", rec)
}
closeRead(src)
if toPlugin {
closeWriteOnly(dst)
} else {
closeWrite(dst)
}
done <- result
}()
result.bytes, result.err = copyForward(dst, src)
}
func copyForward(dst io.Writer, src io.Reader) (int64, error) {
return io.Copy(dst, src)
}
func writeAll(w io.Writer, buf []byte) error {
for len(buf) > 0 {
n, err := w.Write(buf)
if n > 0 {
buf = buf[n:]
}
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
}
return nil
}
func closeWrite(conn any) {
if closer, ok := conn.(closeWriter); ok {
_ = closer.CloseWrite()
return
}
if closer, ok := conn.(io.Closer); ok {
_ = closer.Close()
}
}
func closeWriteOnly(conn any) {
if closer, ok := conn.(closeWriter); ok {
_ = closer.CloseWrite()
}
}
func closeRead(conn any) {
if closer, ok := conn.(closeReader); ok {
_ = closer.CloseRead()
}
}
func flattenHandlers(byPlugin map[string][]*upstreamHandler) []*upstreamHandler {
var handlers []*upstreamHandler
for _, pluginHandlers := range byPlugin {
handlers = append(handlers, pluginHandlers...)
}
return handlers
}
func handlerSummaries(handlers []*upstreamHandler) []DispatchHandlerSummary {
summaries := make([]DispatchHandlerSummary, 0, len(handlers))
for _, handler := range handlers {
summaries = append(summaries, DispatchHandlerSummary{
PluginID: handler.pluginID,
ArtifactID: handler.artifactID,
Priority: handler.priority,
HandlerID: handler.handlerID,
ExtensionPoint: ExtensionUpstreamConnect,
Mode: handler.mode,
TimeoutMS: handler.timeout.Milliseconds(),
Calls: handler.calls.Load(),
Errors: handler.errors.Load(),
Panics: handler.panics.Load(),
Timeouts: handler.timeouts.Load(),
Blocked: handler.blocked.Load(),
ActiveProxy: handler.activeProxy.Load(),
ProxyStarted: handler.proxyStarted.Load(),
ProxyCompleted: handler.proxyCompleted.Load(),
ProxyErrors: handler.proxyErrors.Load(),
ProxyBytesIn: handler.proxyBytesIn.Load(),
ProxyBytesOut: handler.proxyBytesOut.Load(),
ProxyDurationMS: handler.proxyDuration.Load(),
})
}
return summaries
}