refactor(plugins): migrate external plugin manager to grpc/connect
Replace manual HTTP/JSON RPC implementation with generated gRPC/Connect client, providing type-safe plugin communication. Code Generation: - Generate plugin.pb.go and pluginv1connect/plugin.connect.go from plugin.proto - Add generate-plugin-proto Makefile target - Delete hand-written types.go (replaced by generated code) ExternalPluginManager Refactoring: - Replace httpClient with pluginv1connect.PluginServiceClient - Use h2c (cleartext HTTP/2) transport for gRPC without TLS - Replace all manual callRPC/callRPCWithContext calls with typed Connect methods - Remove JSON serialization/deserialization code - Simplify error handling with native gRPC status codes Benefits: - Type safety: compile-time verification of request/response types - Protocol compatibility: standard gRPC wire format - Reduced code: ~100 lines of manual RPC code removed - Better errors: structured gRPC status codes instead of string parsing - Matches existing Actions runner pattern (Connect RPC over HTTP/2) This completes the plugin framework migration to production-grade RPC transport.
This commit is contained in:
6
Makefile
6
Makefile
@@ -763,6 +763,12 @@ generate-go: $(TAGS_PREREQ)
|
|||||||
@echo "Running go generate..."
|
@echo "Running go generate..."
|
||||||
@CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
|
@CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
|
||||||
|
|
||||||
|
.PHONY: generate-plugin-proto
|
||||||
|
generate-plugin-proto:
|
||||||
|
protoc --go_out=. --go_opt=paths=source_relative \
|
||||||
|
--connect-go_out=. --connect-go_opt=paths=source_relative \
|
||||||
|
modules/plugins/pluginv1/plugin.proto
|
||||||
|
|
||||||
.PHONY: security-check
|
.PHONY: security-check
|
||||||
security-check:
|
security-check:
|
||||||
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||||
|
|||||||
@@ -4,12 +4,11 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"maps"
|
"maps"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -17,10 +16,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
|
||||||
"code.gitcaddy.com/server/v3/modules/graceful"
|
"code.gitcaddy.com/server/v3/modules/graceful"
|
||||||
"code.gitcaddy.com/server/v3/modules/json"
|
|
||||||
"code.gitcaddy.com/server/v3/modules/log"
|
"code.gitcaddy.com/server/v3/modules/log"
|
||||||
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
||||||
|
"code.gitcaddy.com/server/v3/modules/plugins/pluginv1/pluginv1connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PluginStatus represents the status of an external plugin
|
// PluginStatus represents the status of an external plugin
|
||||||
@@ -35,14 +37,14 @@ const (
|
|||||||
|
|
||||||
// ManagedPlugin tracks the state of an external plugin
|
// ManagedPlugin tracks the state of an external plugin
|
||||||
type ManagedPlugin struct {
|
type ManagedPlugin struct {
|
||||||
config *ExternalPluginConfig
|
config *ExternalPluginConfig
|
||||||
process *os.Process
|
process *os.Process
|
||||||
status PluginStatus
|
status PluginStatus
|
||||||
lastSeen time.Time
|
lastSeen time.Time
|
||||||
manifest *pluginv1.PluginManifest
|
manifest *pluginv1.PluginManifest
|
||||||
failCount int
|
failCount int
|
||||||
httpClient *http.Client
|
client pluginv1connect.PluginServiceClient
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalPluginManager manages external plugins (both managed and external mode)
|
// ExternalPluginManager manages external plugins (both managed and external mode)
|
||||||
@@ -85,12 +87,23 @@ func (m *ExternalPluginManager) StartAll() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
address := cfg.Address
|
||||||
|
if address == "" {
|
||||||
|
log.Error("External plugin %s has no address configured", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
||||||
|
address = "http://" + address
|
||||||
|
}
|
||||||
|
|
||||||
mp := &ManagedPlugin{
|
mp := &ManagedPlugin{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
status: PluginStatusStarting,
|
status: PluginStatusStarting,
|
||||||
httpClient: &http.Client{
|
client: pluginv1connect.NewPluginServiceClient(
|
||||||
Timeout: cfg.HealthTimeout,
|
newH2CClient(cfg.HealthTimeout),
|
||||||
},
|
address,
|
||||||
|
connect.WithGRPC(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
m.plugins[name] = mp
|
m.plugins[name] = mp
|
||||||
|
|
||||||
@@ -127,7 +140,7 @@ func (m *ExternalPluginManager) StopAll() {
|
|||||||
for name, mp := range m.plugins {
|
for name, mp := range m.plugins {
|
||||||
log.Info("Shutting down external plugin: %s", name)
|
log.Info("Shutting down external plugin: %s", name)
|
||||||
|
|
||||||
// Send shutdown request
|
// Send shutdown request via Connect RPC
|
||||||
m.shutdownPlugin(mp)
|
m.shutdownPlugin(mp)
|
||||||
|
|
||||||
// Kill managed process
|
// Kill managed process
|
||||||
@@ -190,8 +203,13 @@ func (m *ExternalPluginManager) OnEvent(event *pluginv1.PluginEvent) {
|
|||||||
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := m.callOnEvent(ctx, p, event); err != nil {
|
resp, err := p.client.OnEvent(ctx, connect.NewRequest(event))
|
||||||
|
if err != nil {
|
||||||
log.Error("Failed to dispatch event %s to plugin %s: %v", event.EventType, pluginName, err)
|
log.Error("Failed to dispatch event %s to plugin %s: %v", event.EventType, pluginName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.Msg.Error != "" {
|
||||||
|
log.Error("Plugin %s returned error for event %s: %s", pluginName, event.EventType, resp.Msg.Error)
|
||||||
}
|
}
|
||||||
}(name, mp)
|
}(name, mp)
|
||||||
}
|
}
|
||||||
@@ -210,22 +228,22 @@ func (m *ExternalPluginManager) HandleHTTP(method, path string, headers map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, route := range mp.manifest.Routes {
|
for _, route := range mp.manifest.Routes {
|
||||||
if route.Method == method && matchRoute(route.Path, path) {
|
if route.Method == method && strings.HasPrefix(path, route.Path) {
|
||||||
mp.mu.RUnlock()
|
mp.mu.RUnlock()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
resp, err := m.callHandleHTTP(ctx, mp, &pluginv1.HTTPRequest{
|
resp, err := mp.client.HandleHTTP(ctx, connect.NewRequest(&pluginv1.HTTPRequest{
|
||||||
Method: method,
|
Method: method,
|
||||||
Path: path,
|
Path: path,
|
||||||
Headers: headers,
|
Headers: headers,
|
||||||
Body: body,
|
Body: body,
|
||||||
})
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("plugin %s HandleHTTP failed: %w", name, err)
|
return nil, fmt.Errorf("plugin %s HandleHTTP failed: %w", name, err)
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp.Msg, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mp.mu.RUnlock()
|
mp.mu.RUnlock()
|
||||||
@@ -276,107 +294,45 @@ func (m *ExternalPluginManager) startManagedPlugin(mp *ManagedPlugin) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ExternalPluginManager) initializePlugin(mp *ManagedPlugin) error {
|
func (m *ExternalPluginManager) initializePlugin(mp *ManagedPlugin) error {
|
||||||
req := &pluginv1.InitializeRequest{
|
resp, err := mp.client.Initialize(m.ctx, connect.NewRequest(&pluginv1.InitializeRequest{
|
||||||
ServerVersion: "3.0.0",
|
ServerVersion: "3.0.0",
|
||||||
Config: map[string]string{},
|
Config: map[string]string{},
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plugin Initialize RPC failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &pluginv1.InitializeResponse{}
|
if !resp.Msg.Success {
|
||||||
if err := m.callRPC(mp, "initialize", req, resp); err != nil {
|
return fmt.Errorf("plugin initialization failed: %s", resp.Msg.Error)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success {
|
|
||||||
return fmt.Errorf("plugin initialization failed: %s", resp.Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mp.mu.Lock()
|
mp.mu.Lock()
|
||||||
mp.manifest = resp.Manifest
|
mp.manifest = resp.Msg.Manifest
|
||||||
mp.mu.Unlock()
|
mp.mu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ExternalPluginManager) shutdownPlugin(mp *ManagedPlugin) {
|
func (m *ExternalPluginManager) shutdownPlugin(mp *ManagedPlugin) {
|
||||||
req := &pluginv1.ShutdownRequest{Reason: "server shutdown"}
|
_, err := mp.client.Shutdown(m.ctx, connect.NewRequest(&pluginv1.ShutdownRequest{
|
||||||
resp := &pluginv1.ShutdownResponse{}
|
Reason: "server shutdown",
|
||||||
if err := m.callRPC(mp, "shutdown", req, resp); err != nil {
|
}))
|
||||||
|
if err != nil {
|
||||||
log.Warn("Plugin shutdown call failed: %v", err)
|
log.Warn("Plugin shutdown call failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ExternalPluginManager) callOnEvent(ctx context.Context, mp *ManagedPlugin, event *pluginv1.PluginEvent) error {
|
// newH2CClient creates an HTTP client that supports cleartext HTTP/2 (h2c)
|
||||||
resp := &pluginv1.EventResponse{}
|
// for communicating with gRPC services without TLS.
|
||||||
if err := m.callRPCWithContext(ctx, mp, "on-event", event, resp); err != nil {
|
func newH2CClient(timeout time.Duration) *http.Client {
|
||||||
return err
|
return &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
Transport: &http2.Transport{
|
||||||
|
AllowHTTP: true,
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
|
||||||
|
var d net.Dialer
|
||||||
|
return d.DialContext(ctx, network, addr)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
|
||||||
return fmt.Errorf("plugin event error: %s", resp.Error)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ExternalPluginManager) callHandleHTTP(ctx context.Context, mp *ManagedPlugin, req *pluginv1.HTTPRequest) (*pluginv1.HTTPResponse, error) {
|
|
||||||
resp := &pluginv1.HTTPResponse{}
|
|
||||||
if err := m.callRPCWithContext(ctx, mp, "handle-http", req, resp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// callRPC makes a JSON-over-HTTP call to the plugin (simplified RPC)
|
|
||||||
func (m *ExternalPluginManager) callRPC(mp *ManagedPlugin, method string, req, resp any) error {
|
|
||||||
return m.callRPCWithContext(m.ctx, mp, method, req, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ExternalPluginManager) callRPCWithContext(ctx context.Context, mp *ManagedPlugin, method string, reqBody, respBody any) error {
|
|
||||||
address := mp.config.Address
|
|
||||||
if address == "" {
|
|
||||||
return errors.New("plugin has no address configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure address has scheme
|
|
||||||
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
|
||||||
address = "http://" + address
|
|
||||||
}
|
|
||||||
|
|
||||||
url := address + "/plugin/v1/" + method
|
|
||||||
|
|
||||||
body, err := json.Marshal(reqBody)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
httpResp, err := mp.httpClient.Do(httpReq)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("RPC call to %s failed: %w", method, err)
|
|
||||||
}
|
|
||||||
defer httpResp.Body.Close()
|
|
||||||
|
|
||||||
respData, err := io.ReadAll(httpResp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpResp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("RPC %s returned status %d: %s", method, httpResp.StatusCode, string(respData))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(respData, respBody); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchRoute checks if a URL path matches a route pattern (simple prefix matching)
|
|
||||||
func matchRoute(pattern, path string) bool {
|
|
||||||
// Simple prefix match for now
|
|
||||||
return strings.HasPrefix(path, pattern)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
|
||||||
"code.gitcaddy.com/server/v3/modules/graceful"
|
"code.gitcaddy.com/server/v3/modules/graceful"
|
||||||
"code.gitcaddy.com/server/v3/modules/log"
|
"code.gitcaddy.com/server/v3/modules/log"
|
||||||
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
||||||
@@ -59,8 +61,7 @@ func (m *ExternalPluginManager) checkPlugin(ctx context.Context, name string, mp
|
|||||||
healthCtx, cancel := context.WithTimeout(ctx, mp.config.HealthTimeout)
|
healthCtx, cancel := context.WithTimeout(ctx, mp.config.HealthTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
resp := &pluginv1.HealthCheckResponse{}
|
resp, err := mp.client.HealthCheck(healthCtx, connect.NewRequest(&pluginv1.HealthCheckRequest{}))
|
||||||
err := m.callRPCWithContext(healthCtx, mp, "health-check", &pluginv1.HealthCheckRequest{}, resp)
|
|
||||||
|
|
||||||
mp.mu.Lock()
|
mp.mu.Lock()
|
||||||
defer mp.mu.Unlock()
|
defer mp.mu.Unlock()
|
||||||
@@ -90,8 +91,8 @@ func (m *ExternalPluginManager) checkPlugin(ctx context.Context, name string, mp
|
|||||||
mp.status = PluginStatusOnline
|
mp.status = PluginStatusOnline
|
||||||
mp.lastSeen = time.Now()
|
mp.lastSeen = time.Now()
|
||||||
|
|
||||||
if !resp.Healthy {
|
if !resp.Msg.Healthy {
|
||||||
log.Warn("Plugin %s reports unhealthy: %s", name, resp.Status)
|
log.Warn("Plugin %s reports unhealthy: %s", name, resp.Msg.Status)
|
||||||
mp.status = PluginStatusError
|
mp.status = PluginStatusError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1214
modules/plugins/pluginv1/plugin.pb.go
generated
Normal file
1214
modules/plugins/pluginv1/plugin.pb.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package plugin.v1;
|
package plugin.v1;
|
||||||
|
|
||||||
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1";
|
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1;pluginv1";
|
||||||
|
|
||||||
import "google/protobuf/struct.proto";
|
import "google/protobuf/struct.proto";
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|||||||
264
modules/plugins/pluginv1/pluginv1connect/plugin.connect.go
Normal file
264
modules/plugins/pluginv1/pluginv1connect/plugin.connect.go
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// Source: modules/plugins/pluginv1/plugin.proto
|
||||||
|
|
||||||
|
package pluginv1connect
|
||||||
|
|
||||||
|
import (
|
||||||
|
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
||||||
|
connect "connectrpc.com/connect"
|
||||||
|
context "context"
|
||||||
|
errors "errors"
|
||||||
|
http "net/http"
|
||||||
|
strings "strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file and the connect package are
|
||||||
|
// compatible. If you get a compiler error that this constant is not defined, this code was
|
||||||
|
// generated with a version of connect newer than the one compiled into your binary. You can fix the
|
||||||
|
// problem by either regenerating this code with an older version of connect or updating the connect
|
||||||
|
// version compiled into your binary.
|
||||||
|
const _ = connect.IsAtLeastVersion1_13_0
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PluginServiceName is the fully-qualified name of the PluginService service.
|
||||||
|
PluginServiceName = "plugin.v1.PluginService"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These constants are the fully-qualified names of the RPCs defined in this package. They're
|
||||||
|
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
|
||||||
|
//
|
||||||
|
// Note that these are different from the fully-qualified method names used by
|
||||||
|
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
|
||||||
|
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
|
||||||
|
// period.
|
||||||
|
const (
|
||||||
|
// PluginServiceInitializeProcedure is the fully-qualified name of the PluginService's Initialize
|
||||||
|
// RPC.
|
||||||
|
PluginServiceInitializeProcedure = "/plugin.v1.PluginService/Initialize"
|
||||||
|
// PluginServiceShutdownProcedure is the fully-qualified name of the PluginService's Shutdown RPC.
|
||||||
|
PluginServiceShutdownProcedure = "/plugin.v1.PluginService/Shutdown"
|
||||||
|
// PluginServiceHealthCheckProcedure is the fully-qualified name of the PluginService's HealthCheck
|
||||||
|
// RPC.
|
||||||
|
PluginServiceHealthCheckProcedure = "/plugin.v1.PluginService/HealthCheck"
|
||||||
|
// PluginServiceGetManifestProcedure is the fully-qualified name of the PluginService's GetManifest
|
||||||
|
// RPC.
|
||||||
|
PluginServiceGetManifestProcedure = "/plugin.v1.PluginService/GetManifest"
|
||||||
|
// PluginServiceOnEventProcedure is the fully-qualified name of the PluginService's OnEvent RPC.
|
||||||
|
PluginServiceOnEventProcedure = "/plugin.v1.PluginService/OnEvent"
|
||||||
|
// PluginServiceHandleHTTPProcedure is the fully-qualified name of the PluginService's HandleHTTP
|
||||||
|
// RPC.
|
||||||
|
PluginServiceHandleHTTPProcedure = "/plugin.v1.PluginService/HandleHTTP"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginServiceClient is a client for the plugin.v1.PluginService service.
|
||||||
|
type PluginServiceClient interface {
|
||||||
|
// Initialize is called when the server starts or the plugin is loaded
|
||||||
|
Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error)
|
||||||
|
// Shutdown is called when the server is shutting down
|
||||||
|
Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error)
|
||||||
|
// HealthCheck checks if the plugin is healthy
|
||||||
|
HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error)
|
||||||
|
// GetManifest returns the plugin's manifest describing its capabilities
|
||||||
|
GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error)
|
||||||
|
// OnEvent is called when an event the plugin is subscribed to occurs
|
||||||
|
OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error)
|
||||||
|
// HandleHTTP proxies an HTTP request to the plugin
|
||||||
|
HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginServiceClient constructs a client for the plugin.v1.PluginService service. By default,
|
||||||
|
// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and
|
||||||
|
// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC()
|
||||||
|
// or connect.WithGRPCWeb() options.
|
||||||
|
//
|
||||||
|
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||||
|
// http://api.acme.com or https://acme.com/grpc).
|
||||||
|
func NewPluginServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginServiceClient {
|
||||||
|
baseURL = strings.TrimRight(baseURL, "/")
|
||||||
|
pluginServiceMethods := pluginv1.File_modules_plugins_pluginv1_plugin_proto.Services().ByName("PluginService").Methods()
|
||||||
|
return &pluginServiceClient{
|
||||||
|
initialize: connect.NewClient[pluginv1.InitializeRequest, pluginv1.InitializeResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginServiceInitializeProcedure,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("Initialize")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
shutdown: connect.NewClient[pluginv1.ShutdownRequest, pluginv1.ShutdownResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginServiceShutdownProcedure,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("Shutdown")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
healthCheck: connect.NewClient[pluginv1.HealthCheckRequest, pluginv1.HealthCheckResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginServiceHealthCheckProcedure,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("HealthCheck")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
getManifest: connect.NewClient[pluginv1.GetManifestRequest, pluginv1.PluginManifest](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginServiceGetManifestProcedure,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("GetManifest")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
onEvent: connect.NewClient[pluginv1.PluginEvent, pluginv1.EventResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginServiceOnEventProcedure,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("OnEvent")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
handleHTTP: connect.NewClient[pluginv1.HTTPRequest, pluginv1.HTTPResponse](
|
||||||
|
httpClient,
|
||||||
|
baseURL+PluginServiceHandleHTTPProcedure,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("HandleHTTP")),
|
||||||
|
connect.WithClientOptions(opts...),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pluginServiceClient implements PluginServiceClient.
|
||||||
|
type pluginServiceClient struct {
|
||||||
|
initialize *connect.Client[pluginv1.InitializeRequest, pluginv1.InitializeResponse]
|
||||||
|
shutdown *connect.Client[pluginv1.ShutdownRequest, pluginv1.ShutdownResponse]
|
||||||
|
healthCheck *connect.Client[pluginv1.HealthCheckRequest, pluginv1.HealthCheckResponse]
|
||||||
|
getManifest *connect.Client[pluginv1.GetManifestRequest, pluginv1.PluginManifest]
|
||||||
|
onEvent *connect.Client[pluginv1.PluginEvent, pluginv1.EventResponse]
|
||||||
|
handleHTTP *connect.Client[pluginv1.HTTPRequest, pluginv1.HTTPResponse]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize calls plugin.v1.PluginService.Initialize.
|
||||||
|
func (c *pluginServiceClient) Initialize(ctx context.Context, req *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error) {
|
||||||
|
return c.initialize.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown calls plugin.v1.PluginService.Shutdown.
|
||||||
|
func (c *pluginServiceClient) Shutdown(ctx context.Context, req *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error) {
|
||||||
|
return c.shutdown.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck calls plugin.v1.PluginService.HealthCheck.
|
||||||
|
func (c *pluginServiceClient) HealthCheck(ctx context.Context, req *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error) {
|
||||||
|
return c.healthCheck.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetManifest calls plugin.v1.PluginService.GetManifest.
|
||||||
|
func (c *pluginServiceClient) GetManifest(ctx context.Context, req *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error) {
|
||||||
|
return c.getManifest.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnEvent calls plugin.v1.PluginService.OnEvent.
|
||||||
|
func (c *pluginServiceClient) OnEvent(ctx context.Context, req *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error) {
|
||||||
|
return c.onEvent.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleHTTP calls plugin.v1.PluginService.HandleHTTP.
|
||||||
|
func (c *pluginServiceClient) HandleHTTP(ctx context.Context, req *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error) {
|
||||||
|
return c.handleHTTP.CallUnary(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginServiceHandler is an implementation of the plugin.v1.PluginService service.
|
||||||
|
type PluginServiceHandler interface {
|
||||||
|
// Initialize is called when the server starts or the plugin is loaded
|
||||||
|
Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error)
|
||||||
|
// Shutdown is called when the server is shutting down
|
||||||
|
Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error)
|
||||||
|
// HealthCheck checks if the plugin is healthy
|
||||||
|
HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error)
|
||||||
|
// GetManifest returns the plugin's manifest describing its capabilities
|
||||||
|
GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error)
|
||||||
|
// OnEvent is called when an event the plugin is subscribed to occurs
|
||||||
|
OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error)
|
||||||
|
// HandleHTTP proxies an HTTP request to the plugin
|
||||||
|
HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginServiceHandler builds an HTTP handler from the service implementation. It returns the
|
||||||
|
// path on which to mount the handler and the handler itself.
|
||||||
|
//
|
||||||
|
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||||
|
// and JSON codecs. They also support gzip compression.
|
||||||
|
func NewPluginServiceHandler(svc PluginServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||||
|
pluginServiceMethods := pluginv1.File_modules_plugins_pluginv1_plugin_proto.Services().ByName("PluginService").Methods()
|
||||||
|
pluginServiceInitializeHandler := connect.NewUnaryHandler(
|
||||||
|
PluginServiceInitializeProcedure,
|
||||||
|
svc.Initialize,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("Initialize")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginServiceShutdownHandler := connect.NewUnaryHandler(
|
||||||
|
PluginServiceShutdownProcedure,
|
||||||
|
svc.Shutdown,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("Shutdown")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginServiceHealthCheckHandler := connect.NewUnaryHandler(
|
||||||
|
PluginServiceHealthCheckProcedure,
|
||||||
|
svc.HealthCheck,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("HealthCheck")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginServiceGetManifestHandler := connect.NewUnaryHandler(
|
||||||
|
PluginServiceGetManifestProcedure,
|
||||||
|
svc.GetManifest,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("GetManifest")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginServiceOnEventHandler := connect.NewUnaryHandler(
|
||||||
|
PluginServiceOnEventProcedure,
|
||||||
|
svc.OnEvent,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("OnEvent")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
pluginServiceHandleHTTPHandler := connect.NewUnaryHandler(
|
||||||
|
PluginServiceHandleHTTPProcedure,
|
||||||
|
svc.HandleHTTP,
|
||||||
|
connect.WithSchema(pluginServiceMethods.ByName("HandleHTTP")),
|
||||||
|
connect.WithHandlerOptions(opts...),
|
||||||
|
)
|
||||||
|
return "/plugin.v1.PluginService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case PluginServiceInitializeProcedure:
|
||||||
|
pluginServiceInitializeHandler.ServeHTTP(w, r)
|
||||||
|
case PluginServiceShutdownProcedure:
|
||||||
|
pluginServiceShutdownHandler.ServeHTTP(w, r)
|
||||||
|
case PluginServiceHealthCheckProcedure:
|
||||||
|
pluginServiceHealthCheckHandler.ServeHTTP(w, r)
|
||||||
|
case PluginServiceGetManifestProcedure:
|
||||||
|
pluginServiceGetManifestHandler.ServeHTTP(w, r)
|
||||||
|
case PluginServiceOnEventProcedure:
|
||||||
|
pluginServiceOnEventHandler.ServeHTTP(w, r)
|
||||||
|
case PluginServiceHandleHTTPProcedure:
|
||||||
|
pluginServiceHandleHTTPHandler.ServeHTTP(w, r)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedPluginServiceHandler returns CodeUnimplemented from all methods.
|
||||||
|
type UnimplementedPluginServiceHandler struct{}
|
||||||
|
|
||||||
|
func (UnimplementedPluginServiceHandler) Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.Initialize is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginServiceHandler) Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.Shutdown is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginServiceHandler) HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.HealthCheck is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginServiceHandler) GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.GetManifest is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginServiceHandler) OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.OnEvent is not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPluginServiceHandler) HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error) {
|
||||||
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.HandleHTTP is not implemented"))
|
||||||
|
}
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// Copyright 2026 MarketAlly. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
// Package pluginv1 defines the plugin service contract types.
|
|
||||||
// These types mirror the plugin.proto definitions and will be replaced
|
|
||||||
// by generated code when protoc-gen-go and protoc-gen-connect-go are run.
|
|
||||||
package pluginv1
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type InitializeRequest struct {
|
|
||||||
ServerVersion string `json:"server_version"`
|
|
||||||
Config map[string]string `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitializeResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
Manifest *PluginManifest `json:"manifest"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShutdownRequest struct {
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShutdownResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HealthCheckRequest struct{}
|
|
||||||
|
|
||||||
type HealthCheckResponse struct {
|
|
||||||
Healthy bool `json:"healthy"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Details map[string]string `json:"details"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetManifestRequest struct{}
|
|
||||||
|
|
||||||
type PluginManifest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
SubscribedEvents []string `json:"subscribed_events"`
|
|
||||||
Routes []PluginRoute `json:"routes"`
|
|
||||||
RequiredPermissions []string `json:"required_permissions"`
|
|
||||||
LicenseTier string `json:"license_tier"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PluginRoute struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PluginEvent struct {
|
|
||||||
EventType string `json:"event_type"`
|
|
||||||
Payload map[string]any `json:"payload"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
RepoID int64 `json:"repo_id"`
|
|
||||||
OrgID int64 `json:"org_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventResponse struct {
|
|
||||||
Handled bool `json:"handled"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPRequest struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Headers map[string]string `json:"headers"`
|
|
||||||
Body []byte `json:"body"`
|
|
||||||
QueryParams map[string]string `json:"query_params"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPResponse struct {
|
|
||||||
StatusCode int `json:"status_code"`
|
|
||||||
Headers map[string]string `json:"headers"`
|
|
||||||
Body []byte `json:"body"`
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user