- Added `resources` field in `management.register` for defining browser-accessible resources. - Updated examples and documentation to reflect resource-based paths under `/v0/resource/plugins/<pluginID>/...`. - Replaced legacy `GET` menu routes with resource-based implementations for consistent plugin behavior. - Enhanced request handling for resource paths, including proper response headers and streamlined test coverage.
Showing the changes to internal/pluginhost (6 of 22 files). Show all
Unified · Split
| @@ -37,6 +37,7 @@ type Host struct { · 36 unmodified lines | ||
| 37 | 37 | commandLineFlags map[string]commandLineFlagRecord |
| 38 | 38 | commandLineHits map[string]struct{} |
| 39 | 39 | managementRoutes map[string]managementRouteRecord |
| 40 | + resourceRoutes map[string]resourceRouteRecord | |
| 40 | 41 | streams *streamBridge |
| 41 | 42 | httpStreams *hostHTTPStreamBridge |
| 42 | 43 | callbackContexts *callbackContextRegistry |
| @@ -58,6 +59,7 @@ func New() *Host { · 15 unmodified lines | ||
| 58 | 59 | commandLineFlags: make(map[string]commandLineFlagRecord), |
| 59 | 60 | commandLineHits: make(map[string]struct{}), |
| 60 | 61 | managementRoutes: make(map[string]managementRouteRecord), |
| 62 | + resourceRoutes: make(map[string]resourceRouteRecord), | |
| 61 | 63 | streams: newStreamBridge(), |
| 62 | 64 | httpStreams: newHostHTTPStreamBridge(), |
| 63 | 65 | callbackContexts: newCallbackContextRegistry(), |
| @@ -93,6 +95,8 @@ func (h *Host) ApplyConfig(ctx context.Context, cfg *config.Config) { · 29 unmodified lines | ||
| 93 | 95 | h.runtimeConfig = cfg |
| 94 | 96 | |
| 95 | 97 | if !rc.Enabled { |
| 98 | + h.managementRoutes = make(map[string]managementRouteRecord) | |
| 99 | + h.resourceRoutes = make(map[string]resourceRouteRecord) | |
| 96 | 100 | h.snapshot.Store(emptySnapshot()) |
| 97 | 101 | h.mu.Unlock() |
| 98 | 102 | h.refreshThinkingProviders(nil) |
| @@ -102,6 +106,8 @@ func (h *Host) ApplyConfig(ctx context.Context, cfg *config.Config) { · 3 unmodified lines | ||
| 102 | 106 | files, errSelect := selectPluginFiles(rc.Dir) |
| 103 | 107 | if errSelect != nil { |
| 104 | 108 | log.Warnf("pluginhost: failed to select plugin files: %v", errSelect) |
| 109 | + h.managementRoutes = make(map[string]managementRouteRecord) | |
| 110 | + h.resourceRoutes = make(map[string]resourceRouteRecord) | |
| 105 | 111 | h.snapshot.Store(emptySnapshot()) |
| 106 | 112 | h.mu.Unlock() |
| 107 | 113 | h.refreshThinkingProviders(nil) |
| @@ -187,6 +193,7 @@ func (h *Host) ShutdownAll() { · 79 unmodified lines | ||
| 187 | 193 | h.commandLineFlags = make(map[string]commandLineFlagRecord) |
| 188 | 194 | h.commandLineHits = make(map[string]struct{}) |
| 189 | 195 | h.managementRoutes = make(map[string]managementRouteRecord) |
| 196 | + h.resourceRoutes = make(map[string]resourceRouteRecord) | |
| 190 | 197 | h.snapshot.Store(emptySnapshot()) |
| 191 | 198 | h.mu.Unlock() |
| 192 | 199 | |
| @@ -12,20 +12,30 @@ import ( · 11 unmodified lines | ||
| 12 | 12 | log "github.com/sirupsen/logrus" |
| 13 | 13 | ) |
| 14 | 14 | |
| 15 | −const managementBasePath = "/v0/management" | |
| 15 | +const ( | |
| 16 | + managementBasePath = "/v0/management" | |
| 17 | + resourcePluginBasePath = "/v0/resource/plugins" | |
| 18 | + legacyPluginRoutePrefix = "/plugins" | |
| 19 | +) | |
| 16 | 20 | |
| 17 | 21 | type managementRouteRecord struct { |
| 18 | 22 | pluginID string |
| 19 | 23 | route pluginapi.ManagementRoute |
| 20 | 24 | } |
| 21 | 25 | |
| 22 | −// RegisterManagementRoutes rebuilds the plugin-owned Management API route table. | |
| 26 | +type resourceRouteRecord struct { | |
| 27 | + pluginID string | |
| 28 | + route pluginapi.ResourceRoute | |
| 29 | +} | |
| 30 | + | |
| 31 | +// RegisterManagementRoutes rebuilds the plugin-owned Management API and resource route tables. | |
| 23 | 32 | func (h *Host) RegisterManagementRoutes(ctx context.Context, reserved map[string]struct{}) { |
| 24 | 33 | if h == nil { |
| 25 | 34 | return |
| 26 | 35 | } |
| 27 | 36 | |
| 28 | 37 | nextRoutes := make(map[string]managementRouteRecord) |
| 38 | + nextResources := make(map[string]resourceRouteRecord) | |
| 29 | 39 | for _, record := range h.Snapshot().records { |
| 30 | 40 | plugin := record.plugin.Capabilities.ManagementAPI |
| 31 | 41 | if plugin == nil || h.isPluginFused(record.id) { |
| @@ -36,12 +46,19 @@ func (h *Host) RegisterManagementRoutes(ctx context.Context, reserved map[string · 4 unmodified lines | ||
| 36 | 46 | log.Warnf("pluginhost: management registrar %s failed: %v", record.id, errRegister) |
| 37 | 47 | continue |
| 38 | 48 | } |
| 49 | + | |
| 39 | 50 | for _, item := range resp.Routes { |
| 40 | 51 | method, path, okRoute := normalizeManagementRoute(item) |
| 41 | 52 | if !okRoute { |
| 42 | 53 | log.Warnf("pluginhost: plugin %s declared invalid management route %s %s", record.id, item.Method, item.Path) |
| 43 | 54 | continue |
| 44 | 55 | } |
| 56 | + if routeDeclaresLegacyMenuResource(method, item) { | |
| 57 | + if !registerResourceRoute(nextResources, record.id, resourceRouteFromManagementRoute(item)) { | |
| 58 | + log.Warnf("pluginhost: plugin %s declared invalid resource route %s", record.id, item.Path) | |
| 59 | + } | |
| 60 | + continue | |
| 61 | + } | |
| 45 | 62 | key := managementRouteKey(method, path) |
| 46 | 63 | if _, exists := reserved[key]; exists { |
| 47 | 64 | log.Warnf("pluginhost: plugin %s management route %s conflicts with an existing route and was skipped", record.id, key) |
| @@ -58,10 +75,17 @@ func (h *Host) RegisterManagementRoutes(ctx context.Context, reserved map[string · 10 unmodified lines | ||
| 58 | 75 | route: item, |
| 59 | 76 | } |
| 60 | 77 | } |
| 78 | + | |
| 79 | + for _, item := range resp.Resources { | |
| 80 | + if !registerResourceRoute(nextResources, record.id, item) { | |
| 81 | + log.Warnf("pluginhost: plugin %s declared invalid resource route %s", record.id, item.Path) | |
| 82 | + } | |
| 83 | + } | |
| 61 | 84 | } |
| 62 | 85 | |
| 63 | 86 | h.mu.Lock() |
| 64 | 87 | h.managementRoutes = nextRoutes |
| 88 | + h.resourceRoutes = nextResources | |
| 65 | 89 | h.mu.Unlock() |
| 66 | 90 | } |
| 67 | 91 | |
| @@ -77,8 +101,9 @@ func (h *Host) callManagementRegistrar(ctx context.Context, record capabilityRec · 9 unmodified lines | ||
| 77 | 101 | } |
| 78 | 102 | }() |
| 79 | 103 | return plugin.RegisterManagement(ctx, pluginapi.ManagementRegistrationRequest{ |
| 80 | − Plugin: record.meta, | |
| 81 | − BasePath: managementBasePath, | |
| 104 | + Plugin: record.meta, | |
| 105 | + BasePath: managementBasePath, | |
| 106 | + ResourceBasePath: resourcePluginBasePath + "/" + record.id, | |
| 82 | 107 | }) |
| 83 | 108 | } |
| 84 | 109 | |
| @@ -118,6 +143,75 @@ func normalizeManagementRoute(item pluginapi.ManagementRoute) (string, string, b · 33 unmodified lines | ||
| 118 | 143 | return method, fullPath, true |
| 119 | 144 | } |
| 120 | 145 | |
| 146 | +func routeDeclaresLegacyMenuResource(method string, item pluginapi.ManagementRoute) bool { | |
| 147 | + return strings.EqualFold(strings.TrimSpace(method), http.MethodGet) && strings.TrimSpace(item.Menu) != "" | |
| 148 | +} | |
| 149 | + | |
| 150 | +func resourceRouteFromManagementRoute(item pluginapi.ManagementRoute) pluginapi.ResourceRoute { | |
| 151 | + return pluginapi.ResourceRoute{ | |
| 152 | + Path: item.Path, | |
| 153 | + Menu: item.Menu, | |
| 154 | + Description: item.Description, | |
| 155 | + Handler: item.Handler, | |
| 156 | + } | |
| 157 | +} | |
| 158 | + | |
| 159 | +func registerResourceRoute(routes map[string]resourceRouteRecord, pluginID string, item pluginapi.ResourceRoute) bool { | |
| 160 | + path, okRoute := normalizeResourceRoute(pluginID, item) | |
| 161 | + if !okRoute { | |
| 162 | + return false | |
| 163 | + } | |
| 164 | + key := managementRouteKey(http.MethodGet, path) | |
| 165 | + if _, exists := routes[key]; exists { | |
| 166 | + log.Warnf("pluginhost: plugin %s resource route %s conflicts with a higher-priority plugin and was skipped", pluginID, key) | |
| 167 | + return true | |
| 168 | + } | |
| 169 | + item.Path = path | |
| 170 | + routes[key] = resourceRouteRecord{ | |
| 171 | + pluginID: pluginID, | |
| 172 | + route: item, | |
| 173 | + } | |
| 174 | + return true | |
| 175 | +} | |
| 176 | + | |
| 177 | +func normalizeResourceRoute(pluginID string, item pluginapi.ResourceRoute) (string, bool) { | |
| 178 | + if item.Handler == nil { | |
| 179 | + return "", false | |
| 180 | + } | |
| 181 | + pluginID = strings.TrimSpace(pluginID) | |
| 182 | + if pluginID == "" { | |
| 183 | + return "", false | |
| 184 | + } | |
| 185 | + | |
| 186 | + path := strings.TrimSpace(item.Path) | |
| 187 | + if path == "" { | |
| 188 | + return "", false | |
| 189 | + } | |
| 190 | + if !strings.HasPrefix(path, "/") { | |
| 191 | + path = "/" + path | |
| 192 | + } | |
| 193 | + | |
| 194 | + pluginBasePath := resourcePluginBasePath + "/" + pluginID | |
| 195 | + if strings.HasPrefix(path, pluginBasePath+"/") { | |
| 196 | + path = strings.TrimPrefix(path, pluginBasePath) | |
| 197 | + } else if strings.HasPrefix(path, legacyPluginRoutePrefix+"/"+pluginID+"/") { | |
| 198 | + path = strings.TrimPrefix(path, legacyPluginRoutePrefix+"/"+pluginID) | |
| 199 | + } | |
| 200 | + path = strings.TrimRight(path, "/") | |
| 201 | + if path == "" { | |
| 202 | + return "", false | |
| 203 | + } | |
| 204 | + | |
| 205 | + fullPath := pluginBasePath + path | |
| 206 | + if !strings.HasPrefix(fullPath, pluginBasePath+"/") { | |
| 207 | + return "", false | |
| 208 | + } | |
| 209 | + if strings.ContainsAny(fullPath, " \t\r\n") || strings.Contains(fullPath, ":") || strings.Contains(fullPath, "*") || strings.Contains(fullPath, "..") { | |
| 210 | + return "", false | |
| 211 | + } | |
| 212 | + return fullPath, true | |
| 213 | +} | |
| 214 | + | |
| 121 | 215 | func managementRouteKey(method, path string) string { |
| 122 | 216 | return strings.ToUpper(strings.TrimSpace(method)) + " " + strings.TrimSpace(path) |
| 123 | 217 | } |
| @@ -178,6 +272,50 @@ func (h *Host) ServeManagementHTTP(w http.ResponseWriter, r *http.Request) bool · 54 unmodified lines | ||
| 178 | 272 | return true |
| 179 | 273 | } |
| 180 | 274 | |
| 275 | +// ServeResourceHTTP dispatches an unauthenticated browser-navigable resource request to a plugin route. | |
| 276 | +func (h *Host) ServeResourceHTTP(w http.ResponseWriter, r *http.Request) bool { | |
| 277 | + if h == nil || w == nil || r == nil || r.URL == nil { | |
| 278 | + return false | |
| 279 | + } | |
| 280 | + if !strings.EqualFold(r.Method, http.MethodGet) { | |
| 281 | + return false | |
| 282 | + } | |
| 283 | + key := managementRouteKey(http.MethodGet, r.URL.Path) | |
| 284 | + h.mu.Lock() | |
| 285 | + record, okRoute := h.resourceRoutes[key] | |
| 286 | + h.mu.Unlock() | |
| 287 | + if !okRoute || record.route.Handler == nil || h.isPluginFused(record.pluginID) { | |
| 288 | + return false | |
| 289 | + } | |
| 290 | + | |
| 291 | + resp, errHandle := h.callResourceHandler(r.Context(), record, pluginapi.ManagementRequest{ | |
| 292 | + Method: http.MethodGet, | |
| 293 | + Path: r.URL.Path, | |
| 294 | + Headers: cloneHeader(r.Header), | |
| 295 | + Query: cloneValues(r.URL.Query()), | |
| 296 | + }) | |
| 297 | + if errHandle != nil { | |
| 298 | + log.Warnf("pluginhost: resource handler %s failed: %v", record.pluginID, errHandle) | |
| 299 | + http.Error(w, "plugin resource handler failed", http.StatusBadGateway) | |
| 300 | + return true | |
| 301 | + } | |
| 302 | + | |
| 303 | + for keyHeader, values := range resp.Headers { | |
| 304 | + for _, value := range values { | |
| 305 | + w.Header().Add(keyHeader, value) | |
| 306 | + } | |
| 307 | + } | |
| 308 | + statusCode := resp.StatusCode | |
| 309 | + if statusCode == 0 { | |
| 310 | + statusCode = http.StatusOK | |
| 311 | + } | |
| 312 | + w.WriteHeader(statusCode) | |
| 313 | + if _, errWrite := w.Write(resp.Body); errWrite != nil { | |
| 314 | + log.Warnf("pluginhost: failed to write plugin resource response: %v", errWrite) | |
| 315 | + } | |
| 316 | + return true | |
| 317 | +} | |
| 318 | + | |
| 181 | 319 | func (h *Host) callManagementHandler(ctx context.Context, record managementRouteRecord, req pluginapi.ManagementRequest) (resp pluginapi.ManagementResponse, err error) { |
| 182 | 320 | if h == nil || record.route.Handler == nil || h.isPluginFused(record.pluginID) { |
| 183 | 321 | return pluginapi.ManagementResponse{}, nil |
| @@ -191,3 +329,17 @@ func (h *Host) callManagementHandler(ctx context.Context, record managementRoute · 7 unmodified lines | ||
| 191 | 329 | }() |
| 192 | 330 | return record.route.Handler.HandleManagement(ctx, req) |
| 193 | 331 | } |
| 332 | + | |
| 333 | +func (h *Host) callResourceHandler(ctx context.Context, record resourceRouteRecord, req pluginapi.ManagementRequest) (resp pluginapi.ManagementResponse, err error) { | |
| 334 | + if h == nil || record.route.Handler == nil || h.isPluginFused(record.pluginID) { | |
| 335 | + return pluginapi.ManagementResponse{}, nil | |
| 336 | + } | |
| 337 | + defer func() { | |
| 338 | + if recovered := recover(); recovered != nil { | |
| 339 | + h.fusePlugin(record.pluginID, "ResourceHandler.HandleManagement", recovered) | |
| 340 | + resp = pluginapi.ManagementResponse{} | |
| 341 | + err = fmt.Errorf("resource handler panic: %v", recovered) | |
| 342 | + } | |
| 343 | + }() | |
| 344 | + return record.route.Handler.HandleManagement(ctx, req) | |
| 345 | +} | |
| @@ -91,18 +91,77 @@ func TestManagementHandlerPanicFusesPlugin(t *testing.T) { · 90 unmodified lines | ||
| 91 | 91 | } |
| 92 | 92 | } |
| 93 | 93 | |
| 94 | −func TestRegisteredPluginsIncludesGETManagementMenus(t *testing.T) { | |
| 95 | − plugin := &managementPluginDouble{ | |
| 96 | − routes: []pluginapi.ManagementRoute{ | |
| 97 | − { | |
| 98 | − Method: http.MethodGet, | |
| 99 | − Path: "/plugins/menu/status", | |
| 94 | +func TestServeResourceHTTPDispatchesPluginResource(t *testing.T) { | |
| 95 | + host := newHostWithRecords(capabilityRecord{ | |
| 96 | + id: "resource", | |
| 97 | + plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ | |
| 98 | + ManagementAPI: &managementPluginDouble{resources: []pluginapi.ResourceRoute{{ | |
| 99 | + Path: "/status", | |
| 100 | 100 | Menu: "Status", |
| 101 | 101 | Description: "Shows plugin status.", |
| 102 | + Handler: managementHandlerFunc(func(_ context.Context, req pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { | |
| 103 | + if req.Path != "/v0/resource/plugins/resource/status" { | |
| 104 | + t.Fatalf("resource request path = %q, want normalized resource path", req.Path) | |
| 105 | + } | |
| 106 | + return pluginapi.ManagementResponse{ | |
| 107 | + Headers: http.Header{"Content-Type": []string{"text/html; charset=utf-8"}}, | |
| 108 | + Body: []byte("<!doctype html><title>resource</title>"), | |
| 109 | + }, nil | |
| 110 | + }), | |
| 111 | + }}}, | |
| 112 | + }}, | |
| 113 | + }) | |
| 114 | + host.RegisterManagementRoutes(context.Background(), nil) | |
| 115 | + | |
| 116 | + req := httptest.NewRequest(http.MethodGet, "/v0/resource/plugins/resource/status", nil) | |
| 117 | + rec := httptest.NewRecorder() | |
| 118 | + if !host.ServeResourceHTTP(rec, req) { | |
| 119 | + t.Fatal("ServeResourceHTTP() = false, want true") | |
| 120 | + } | |
| 121 | + if rec.Code != http.StatusOK || rec.Body.String() != "<!doctype html><title>resource</title>" { | |
| 122 | + t.Fatalf("response = %d %q, want 200 html", rec.Code, rec.Body.String()) | |
| 123 | + } | |
| 124 | + if got := rec.Header().Get("Content-Type"); got != "text/html; charset=utf-8" { | |
| 125 | + t.Fatalf("Content-Type = %q, want text/html; charset=utf-8", got) | |
| 126 | + } | |
| 127 | +} | |
| 128 | + | |
| 129 | +func TestLegacyGETManagementMenuRegistersAsResource(t *testing.T) { | |
| 130 | + host := newHostWithRecords(capabilityRecord{ | |
| 131 | + id: "legacy", | |
| 132 | + plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{ | |
| 133 | + ManagementAPI: &managementPluginDouble{routes: []pluginapi.ManagementRoute{{ | |
| 134 | + Method: http.MethodGet, | |
| 135 | + Path: "/plugins/legacy/status", | |
| 136 | + Menu: "Legacy Status", | |
| 137 | + Description: "Shows legacy plugin status.", | |
| 102 | 138 | Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { |
| 103 | − return pluginapi.ManagementResponse{}, nil | |
| 139 | + return pluginapi.ManagementResponse{Body: []byte("legacy")}, nil | |
| 104 | 140 | }), |
| 105 | − }, | |
| 141 | + }}}, | |
| 142 | + }}, | |
| 143 | + }) | |
| 144 | + host.RegisterManagementRoutes(context.Background(), nil) | |
| 145 | + | |
| 146 | + managementReq := httptest.NewRequest(http.MethodGet, "/v0/management/plugins/legacy/status", nil) | |
| 147 | + managementRec := httptest.NewRecorder() | |
| 148 | + if host.ServeManagementHTTP(managementRec, managementReq) { | |
| 149 | + t.Fatal("legacy menu route was served as Management API route") | |
| 150 | + } | |
| 151 | + | |
| 152 | + resourceReq := httptest.NewRequest(http.MethodGet, "/v0/resource/plugins/legacy/status", nil) | |
| 153 | + resourceRec := httptest.NewRecorder() | |
| 154 | + if !host.ServeResourceHTTP(resourceRec, resourceReq) { | |
| 155 | + t.Fatal("legacy menu route was not served as resource route") | |
| 156 | + } | |
| 157 | + if resourceRec.Body.String() != "legacy" { | |
| 158 | + t.Fatalf("resource body = %q, want legacy", resourceRec.Body.String()) | |
| 159 | + } | |
| 160 | +} | |
| 161 | + | |
| 162 | +func TestRegisteredPluginsIncludesResourceMenus(t *testing.T) { | |
| 163 | + plugin := &managementPluginDouble{ | |
| 164 | + routes: []pluginapi.ManagementRoute{ | |
| 106 | 165 | { |
| 107 | 166 | Method: http.MethodGet, |
| 108 | 167 | Path: "/plugins/menu/hidden", |
| @@ -110,11 +169,12 @@ func TestRegisteredPluginsIncludesGETManagementMenus(t *testing.T) { · 1 unmodified line | ||
| 110 | 169 | return pluginapi.ManagementResponse{}, nil |
| 111 | 170 | }), |
| 112 | 171 | }, |
| 172 | + }, | |
| 173 | + resources: []pluginapi.ResourceRoute{ | |
| 113 | 174 | { |
| 114 | − Method: http.MethodPost, | |
| 115 | − Path: "/plugins/menu/run", | |
| 116 | − Menu: "Run", | |
| 117 | − Description: "Runs a plugin action.", | |
| 175 | + Path: "/status", | |
| 176 | + Menu: "Status", | |
| 177 | + Description: "Shows plugin status.", | |
| 118 | 178 | Handler: managementHandlerFunc(func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { |
| 119 | 179 | return pluginapi.ManagementResponse{}, nil |
| 120 | 180 | }), |
| @@ -136,17 +196,18 @@ func TestRegisteredPluginsIncludesGETManagementMenus(t *testing.T) { · 15 unmodified lines | ||
| 136 | 196 | t.Fatalf("RegisteredPlugins()[0].Menus = %#v, want one visible GET menu", plugins[0].Menus) |
| 137 | 197 | } |
| 138 | 198 | menu := plugins[0].Menus[0] |
| 139 | − if menu.Path != "/v0/management/plugins/menu/status" || menu.Menu != "Status" || menu.Description != "Shows plugin status." { | |
| 199 | + if menu.Path != "/v0/resource/plugins/menu/status" || menu.Menu != "Status" || menu.Description != "Shows plugin status." { | |
| 140 | 200 | t.Fatalf("menu = %#v, want normalized status menu", menu) |
| 141 | 201 | } |
| 142 | 202 | } |
| 143 | 203 | |
| 144 | 204 | type managementPluginDouble struct { |
| 145 | − routes []pluginapi.ManagementRoute | |
| 205 | + routes []pluginapi.ManagementRoute | |
| 206 | + resources []pluginapi.ResourceRoute | |
| 146 | 207 | } |
| 147 | 208 | |
| 148 | 209 | func (p *managementPluginDouble) RegisterManagement(context.Context, pluginapi.ManagementRegistrationRequest) (pluginapi.ManagementRegistrationResponse, error) { |
| 149 | − return pluginapi.ManagementRegistrationResponse{Routes: p.routes}, nil | |
| 210 | + return pluginapi.ManagementRegistrationResponse{Routes: p.routes, Resources: p.resources}, nil | |
| 150 | 211 | } |
| 151 | 212 | |
| 152 | 213 | type managementHandlerFunc func(context.Context, pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) |
| @@ -498,7 +498,12 @@ func (a *rpcPluginAdapter) RegisterManagement(ctx context.Context, req pluginapi · 497 unmodified lines | ||
| 498 | 498 | route.Handler = a |
| 499 | 499 | routes = append(routes, route) |
| 500 | 500 | } |
| 501 | − return pluginapi.ManagementRegistrationResponse{Routes: routes}, nil | |
| 501 | + resources := make([]pluginapi.ResourceRoute, 0, len(resp.Resources)) | |
| 502 | + for _, route := range resp.Resources { | |
| 503 | + route.Handler = a | |
| 504 | + resources = append(resources, route) | |
| 505 | + } | |
| 506 | + return pluginapi.ManagementRegistrationResponse{Routes: routes, Resources: resources}, nil | |
| 502 | 507 | } |
| 503 | 508 | |
| 504 | 509 | func (a *rpcPluginAdapter) HandleManagement(ctx context.Context, req pluginapi.ManagementRequest) (pluginapi.ManagementResponse, error) { |
| @@ -103,7 +103,8 @@ type rpcThinkingApplyRequest struct { · 102 unmodified lines | ||
| 103 | 103 | } |
| 104 | 104 | |
| 105 | 105 | type rpcManagementRegistrationResponse struct { |
| 106 | − Routes []pluginapi.ManagementRoute `json:"routes,omitempty"` | |
| 106 | + Routes []pluginapi.ManagementRoute `json:"routes,omitempty"` | |
| 107 | + Resources []pluginapi.ResourceRoute `json:"resources,omitempty"` | |
| 107 | 108 | } |
| 108 | 109 | |
| 109 | 110 | type rpcEmptyResponse struct{} |
| @@ -1,7 +1,6 @@ | ||
| 1 | 1 | package pluginhost |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | − "net/http" | |
| 5 | 4 | "sort" |
| 6 | 5 | "strings" |
| 7 | 6 | |
| @@ -29,7 +28,7 @@ type RegisteredPluginInfo struct { · 21 unmodified lines | ||
| 29 | 28 | Menus []RegisteredPluginMenu |
| 30 | 29 | } |
| 31 | 30 | |
| 32 | −// RegisteredPluginMenu describes a plugin-owned GET Management API menu entry. | |
| 31 | +// RegisteredPluginMenu describes a plugin-owned resource menu entry. | |
| 33 | 32 | type RegisteredPluginMenu struct { |
| 34 | 33 | Path string |
| 35 | 34 | Menu string |
| @@ -67,10 +66,7 @@ func (h *Host) registeredPluginMenus() map[string][]RegisteredPluginMenu { · 31 unmodified lines | ||
| 67 | 66 | } |
| 68 | 67 | h.mu.Lock() |
| 69 | 68 | defer h.mu.Unlock() |
| 70 | − for _, record := range h.managementRoutes { | |
| 71 | − if !strings.EqualFold(strings.TrimSpace(record.route.Method), http.MethodGet) { | |
| 72 | − continue | |
| 73 | − } | |
| 69 | + for _, record := range h.resourceRoutes { | |
| 74 | 70 | menu := strings.TrimSpace(record.route.Menu) |
| 75 | 71 | if menu == "" { |
| 76 | 72 | continue |