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..."
|
||||
@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
|
||||
security-check:
|
||||
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -17,10 +16,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"code.gitcaddy.com/server/v3/modules/graceful"
|
||||
"code.gitcaddy.com/server/v3/modules/json"
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
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
|
||||
@@ -35,14 +37,14 @@ const (
|
||||
|
||||
// ManagedPlugin tracks the state of an external plugin
|
||||
type ManagedPlugin struct {
|
||||
config *ExternalPluginConfig
|
||||
process *os.Process
|
||||
status PluginStatus
|
||||
lastSeen time.Time
|
||||
manifest *pluginv1.PluginManifest
|
||||
failCount int
|
||||
httpClient *http.Client
|
||||
mu sync.RWMutex
|
||||
config *ExternalPluginConfig
|
||||
process *os.Process
|
||||
status PluginStatus
|
||||
lastSeen time.Time
|
||||
manifest *pluginv1.PluginManifest
|
||||
failCount int
|
||||
client pluginv1connect.PluginServiceClient
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// ExternalPluginManager manages external plugins (both managed and external mode)
|
||||
@@ -85,12 +87,23 @@ func (m *ExternalPluginManager) StartAll() error {
|
||||
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{
|
||||
config: cfg,
|
||||
status: PluginStatusStarting,
|
||||
httpClient: &http.Client{
|
||||
Timeout: cfg.HealthTimeout,
|
||||
},
|
||||
client: pluginv1connect.NewPluginServiceClient(
|
||||
newH2CClient(cfg.HealthTimeout),
|
||||
address,
|
||||
connect.WithGRPC(),
|
||||
),
|
||||
}
|
||||
m.plugins[name] = mp
|
||||
|
||||
@@ -127,7 +140,7 @@ func (m *ExternalPluginManager) StopAll() {
|
||||
for name, mp := range m.plugins {
|
||||
log.Info("Shutting down external plugin: %s", name)
|
||||
|
||||
// Send shutdown request
|
||||
// Send shutdown request via Connect RPC
|
||||
m.shutdownPlugin(mp)
|
||||
|
||||
// Kill managed process
|
||||
@@ -190,8 +203,13 @@ func (m *ExternalPluginManager) OnEvent(event *pluginv1.PluginEvent) {
|
||||
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
||||
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)
|
||||
return
|
||||
}
|
||||
if resp.Msg.Error != "" {
|
||||
log.Error("Plugin %s returned error for event %s: %s", pluginName, event.EventType, resp.Msg.Error)
|
||||
}
|
||||
}(name, mp)
|
||||
}
|
||||
@@ -210,22 +228,22 @@ func (m *ExternalPluginManager) HandleHTTP(method, path string, headers map[stri
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := m.callHandleHTTP(ctx, mp, &pluginv1.HTTPRequest{
|
||||
resp, err := mp.client.HandleHTTP(ctx, connect.NewRequest(&pluginv1.HTTPRequest{
|
||||
Method: method,
|
||||
Path: path,
|
||||
Headers: headers,
|
||||
Body: body,
|
||||
})
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin %s HandleHTTP failed: %w", name, err)
|
||||
}
|
||||
return resp, nil
|
||||
return resp.Msg, nil
|
||||
}
|
||||
}
|
||||
mp.mu.RUnlock()
|
||||
@@ -276,107 +294,45 @@ func (m *ExternalPluginManager) startManagedPlugin(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",
|
||||
Config: map[string]string{},
|
||||
}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("plugin Initialize RPC failed: %w", err)
|
||||
}
|
||||
|
||||
resp := &pluginv1.InitializeResponse{}
|
||||
if err := m.callRPC(mp, "initialize", req, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("plugin initialization failed: %s", resp.Error)
|
||||
if !resp.Msg.Success {
|
||||
return fmt.Errorf("plugin initialization failed: %s", resp.Msg.Error)
|
||||
}
|
||||
|
||||
mp.mu.Lock()
|
||||
mp.manifest = resp.Manifest
|
||||
mp.manifest = resp.Msg.Manifest
|
||||
mp.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ExternalPluginManager) shutdownPlugin(mp *ManagedPlugin) {
|
||||
req := &pluginv1.ShutdownRequest{Reason: "server shutdown"}
|
||||
resp := &pluginv1.ShutdownResponse{}
|
||||
if err := m.callRPC(mp, "shutdown", req, resp); err != nil {
|
||||
_, err := mp.client.Shutdown(m.ctx, connect.NewRequest(&pluginv1.ShutdownRequest{
|
||||
Reason: "server shutdown",
|
||||
}))
|
||||
if err != nil {
|
||||
log.Warn("Plugin shutdown call failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ExternalPluginManager) callOnEvent(ctx context.Context, mp *ManagedPlugin, event *pluginv1.PluginEvent) error {
|
||||
resp := &pluginv1.EventResponse{}
|
||||
if err := m.callRPCWithContext(ctx, mp, "on-event", event, resp); err != nil {
|
||||
return err
|
||||
// newH2CClient creates an HTTP client that supports cleartext HTTP/2 (h2c)
|
||||
// for communicating with gRPC services without TLS.
|
||||
func newH2CClient(timeout time.Duration) *http.Client {
|
||||
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"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
|
||||
"code.gitcaddy.com/server/v3/modules/graceful"
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
resp := &pluginv1.HealthCheckResponse{}
|
||||
err := m.callRPCWithContext(healthCtx, mp, "health-check", &pluginv1.HealthCheckRequest{}, resp)
|
||||
resp, err := mp.client.HealthCheck(healthCtx, connect.NewRequest(&pluginv1.HealthCheckRequest{}))
|
||||
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
@@ -90,8 +91,8 @@ func (m *ExternalPluginManager) checkPlugin(ctx context.Context, name string, mp
|
||||
mp.status = PluginStatusOnline
|
||||
mp.lastSeen = time.Now()
|
||||
|
||||
if !resp.Healthy {
|
||||
log.Warn("Plugin %s reports unhealthy: %s", name, resp.Status)
|
||||
if !resp.Msg.Healthy {
|
||||
log.Warn("Plugin %s reports unhealthy: %s", name, resp.Msg.Status)
|
||||
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;
|
||||
|
||||
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/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