Skip to content

Commit 7cf064f

Browse files
committed
feat: add DELETE endpoint for admin endpoint
1 parent e637b53 commit 7cf064f

File tree

4 files changed

+294
-1
lines changed

4 files changed

+294
-1
lines changed

internal/server/admin.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,51 @@ func adminRouter(s *Server) (chi.Router, error) {
3636
r.Use(render.SetContentType(render.ContentTypeJSON))
3737

3838
r.Put("/{kind}/{name}", func(w http.ResponseWriter, r *http.Request) { createOrUpdatePrimitives(s, w, r) })
39+
r.Delete("/{kind}/{name}", func(w http.ResponseWriter, r *http.Request) { deletePrimitives(s, w, r) })
3940

4041
return r, nil
4142
}
4243

44+
// deletePrimitives handles the deletion of primitives
45+
// Invalid primitive kind will result in http.StatusBadRequest
46+
// Primitive not found will result in http.StatusNotFound
47+
func deletePrimitives(s *Server, w http.ResponseWriter, r *http.Request) {
48+
kind := chi.URLParam(r, "kind")
49+
name := chi.URLParam(r, "name")
50+
ctx := r.Context()
51+
ctx = util.WithInstrumentation(ctx, s.instrumentation)
52+
53+
var deleted bool
54+
switch strings.ToLower(kind) {
55+
case "source":
56+
deleted = s.ResourceMgr.DeleteSource(name)
57+
case "authservice":
58+
deleted = s.ResourceMgr.DeleteAuthService(name)
59+
case "embeddingmodel":
60+
deleted = s.ResourceMgr.DeleteEmbeddingModel(name)
61+
case "tool":
62+
deleted = s.ResourceMgr.DeleteTool(name)
63+
case "toolset":
64+
deleted = s.ResourceMgr.DeleteToolset(name)
65+
case "prompt":
66+
deleted = s.ResourceMgr.DeletePrompt(name)
67+
default:
68+
err := fmt.Errorf("invalid primitive kind provided")
69+
s.logger.DebugContext(ctx, err.Error())
70+
_ = render.Render(w, r, newErrResponse(err, http.StatusBadRequest))
71+
return
72+
}
73+
74+
if !deleted {
75+
err := fmt.Errorf("%s %s not found", kind, name)
76+
s.logger.DebugContext(ctx, err.Error())
77+
_ = render.Render(w, r, newErrResponse(err, http.StatusNotFound))
78+
return
79+
}
80+
81+
w.WriteHeader(http.StatusOK)
82+
}
83+
4384
// createOrUpdatePrimitives handles the creation or updating of primitives
4485
// changing name will result in creation of a new primitive instead of replacing
4586
// existing primitive

internal/server/admin_test.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func init() {
6868
})
6969
}
7070

71-
func TestUpdateEndpoint(t *testing.T) {
71+
func TestAdminUpdateEndpoint(t *testing.T) {
7272
r, shutdown := setUpServer(t, "admin", map[string]sources.Source{}, map[string]auth.AuthService{}, map[string]embeddingmodels.EmbeddingModel{}, map[string]tools.Tool{}, map[string]tools.Toolset{}, map[string]prompts.Prompt{}, map[string]prompts.Promptset{})
7373
defer shutdown()
7474
ts := runServer(r, false)
@@ -144,3 +144,84 @@ func TestUpdateEndpoint(t *testing.T) {
144144
})
145145
}
146146
}
147+
148+
func TestAdminDeleteEndpoint(t *testing.T) {
149+
mockSources := map[string]sources.Source{"test-source": testutils.MockSource{}}
150+
mockAuthServices := map[string]auth.AuthService{"test-auth-service": testutils.MockAuthService{}}
151+
mockEmbeddingModel := map[string]embeddingmodels.EmbeddingModel{"test-embedding-model": testutils.MockEmbeddingModel{}}
152+
mockTool := map[string]tools.Tool{"test-tool": testutils.MockTool{}}
153+
mockToolset := map[string]tools.Toolset{"test-toolset": tools.Toolset{}}
154+
mockPrompt := map[string]prompts.Prompt{"test-prompt": testutils.MockPrompt{}}
155+
r, shutdown := setUpServer(t, "admin", mockSources, mockAuthServices, mockEmbeddingModel, mockTool, mockToolset, mockPrompt, map[string]prompts.Promptset{})
156+
defer shutdown()
157+
ts := runServer(r, false)
158+
defer ts.Close()
159+
160+
tests := []struct {
161+
name string
162+
kind string
163+
resourceName string
164+
expectedStatusCode int
165+
}{
166+
{
167+
name: "Delete Source - Success",
168+
kind: "source",
169+
resourceName: "test-source",
170+
expectedStatusCode: http.StatusOK,
171+
},
172+
{
173+
name: "Delete Auth Service - Success",
174+
kind: "authService",
175+
resourceName: "test-auth-service",
176+
expectedStatusCode: http.StatusOK,
177+
},
178+
{
179+
name: "Delete Embedding Model - Success",
180+
kind: "embeddingModel",
181+
resourceName: "test-embedding-model",
182+
expectedStatusCode: http.StatusOK,
183+
},
184+
{
185+
name: "Delete Tool - Success",
186+
kind: "tool",
187+
resourceName: "test-tool",
188+
expectedStatusCode: http.StatusOK,
189+
},
190+
{
191+
name: "Delete Toolset - Success",
192+
kind: "toolset",
193+
resourceName: "test-toolset",
194+
expectedStatusCode: http.StatusOK,
195+
},
196+
{
197+
name: "Delete Prompt - Success",
198+
kind: "prompt",
199+
resourceName: "test-prompt",
200+
expectedStatusCode: http.StatusOK,
201+
},
202+
{
203+
name: "Delete Non-existent Primitive - Not Found",
204+
kind: "source",
205+
resourceName: "non-existent-source",
206+
expectedStatusCode: http.StatusNotFound,
207+
},
208+
{
209+
name: "Delete with Invalid Kind - Bad Request",
210+
kind: "invalidKind",
211+
resourceName: "some-name",
212+
expectedStatusCode: http.StatusBadRequest,
213+
},
214+
}
215+
216+
for _, tt := range tests {
217+
t.Run(tt.name, func(t *testing.T) {
218+
resp, body, err := runRequest(ts, http.MethodDelete, fmt.Sprintf("/%s/%s", tt.kind, tt.resourceName), nil, nil)
219+
if err != nil {
220+
t.Fatalf("unexpected error during request: %s", err)
221+
}
222+
if resp.StatusCode != tt.expectedStatusCode {
223+
t.Fatalf("response status code is not %d, got %d, %s", tt.expectedStatusCode, resp.StatusCode, string(body))
224+
}
225+
})
226+
}
227+
}

internal/server/resources/resources.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ func (r *ResourceManager) UpdateEmbeddingModel(ctx context.Context, name string,
260260
return nil
261261
}
262262

263+
// removeFirstFromToolset removes a target tool from toolset
263264
func removeFirstFromToolset(ts tools.Toolset, target string) tools.Toolset {
264265
for i, tool := range ts.Tools {
265266
if tool != nil && (*tool) != nil && (*tool).McpManifest().Name == target {
@@ -375,3 +376,82 @@ func (r *ResourceManager) UpdatePrompt(ctx context.Context, name string, config
375376
r.prompts[name] = p
376377
return nil
377378
}
379+
380+
// DeleteSource deletes a source.
381+
func (r *ResourceManager) DeleteSource(name string) bool {
382+
r.mu.Lock()
383+
defer r.mu.Unlock()
384+
_, ok := r.sources[name]
385+
if !ok {
386+
return false
387+
}
388+
delete(r.sources, name)
389+
return true
390+
}
391+
392+
// DeleteAuthService deletes an auth service.
393+
func (r *ResourceManager) DeleteAuthService(name string) bool {
394+
r.mu.Lock()
395+
defer r.mu.Unlock()
396+
_, ok := r.authServices[name]
397+
if !ok {
398+
return false
399+
}
400+
delete(r.authServices, name)
401+
return true
402+
}
403+
404+
// DeleteEmbeddingModel deletes an embedding model.
405+
func (r *ResourceManager) DeleteEmbeddingModel(name string) bool {
406+
r.mu.Lock()
407+
defer r.mu.Unlock()
408+
_, ok := r.embeddingModels[name]
409+
if !ok {
410+
return false
411+
}
412+
delete(r.embeddingModels, name)
413+
return true
414+
}
415+
416+
// DeleteTool deletes a tool.
417+
func (r *ResourceManager) DeleteTool(name string) bool {
418+
r.mu.Lock()
419+
defer r.mu.Unlock()
420+
_, ok := r.tools[name]
421+
if !ok {
422+
return false
423+
}
424+
delete(r.tools, name)
425+
426+
// Also remove the tool from the default toolset
427+
// This function do not remove tool from other toolset. User have to update
428+
// toolset explicitly
429+
if defaultToolset, toolsetExists := r.toolsets[""]; toolsetExists {
430+
r.toolsets[""] = removeFirstFromToolset(defaultToolset, name)
431+
}
432+
return true
433+
}
434+
435+
// DeleteToolset deletes a toolset.
436+
func (r *ResourceManager) DeleteToolset(name string) bool {
437+
r.mu.Lock()
438+
defer r.mu.Unlock()
439+
_, ok := r.toolsets[name]
440+
if !ok {
441+
return false
442+
}
443+
delete(r.toolsets, name)
444+
return true
445+
}
446+
447+
// DeletePrompt deletes a prompt.
448+
func (r *ResourceManager) DeletePrompt(name string) bool {
449+
r.mu.Lock()
450+
defer r.mu.Unlock()
451+
_, ok := r.prompts[name]
452+
if !ok {
453+
return false
454+
}
455+
delete(r.prompts, name)
456+
return true
457+
}

internal/server/resources/resources_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,94 @@ func TestCreateAndUpdatePrimitives(t *testing.T) {
255255
t.Fatalf("update failed: got %+v, want %+v", pc, pConfig)
256256
}
257257
}
258+
259+
func TestDeletePrimitives(t *testing.T) {
260+
resMgr := resources.NewResourceManager(
261+
map[string]sources.Source{"foo": testutils.MockSource{}},
262+
map[string]auth.AuthService{"foo": testutils.MockAuthService{}},
263+
map[string]embeddingmodels.EmbeddingModel{"foo": testutils.MockEmbeddingModel{}},
264+
map[string]tools.Tool{"foo": testutils.MockTool{}},
265+
map[string]tools.Toolset{"foo": tools.Toolset{}},
266+
map[string]prompts.Prompt{"foo": testutils.MockPrompt{}},
267+
map[string]prompts.Promptset{},
268+
)
269+
270+
var deleted, found bool
271+
deleted = resMgr.DeleteSource("foo")
272+
if !deleted {
273+
t.Fatalf("expected delete to be successful")
274+
}
275+
_, found = resMgr.GetSource("foo")
276+
if found {
277+
t.Fatalf("found source but expected to be deleted")
278+
}
279+
deleted = resMgr.DeleteSource("nonexistent")
280+
if deleted {
281+
t.Fatalf("expected delete to be successful")
282+
}
283+
284+
deleted = resMgr.DeleteAuthService("foo")
285+
if !deleted {
286+
t.Fatalf("expected delete to be successful")
287+
}
288+
_, found = resMgr.GetAuthService("foo")
289+
if found {
290+
t.Fatalf("found auth service but expected to be deleted")
291+
}
292+
deleted = resMgr.DeleteAuthService("nonexistent")
293+
if deleted {
294+
t.Fatalf("expected delete to be successful")
295+
}
296+
297+
deleted = resMgr.DeleteEmbeddingModel("foo")
298+
if !deleted {
299+
t.Fatalf("expected delete to be successful")
300+
}
301+
_, found = resMgr.GetEmbeddingModel("foo")
302+
if found {
303+
t.Fatalf("found embedding model but expected to be deleted")
304+
}
305+
deleted = resMgr.DeleteEmbeddingModel("nonexistent")
306+
if deleted {
307+
t.Fatalf("expected delete to be successful")
308+
}
309+
310+
deleted = resMgr.DeleteTool("foo")
311+
if !deleted {
312+
t.Fatalf("expected delete to be successful")
313+
}
314+
_, found = resMgr.GetTool("foo")
315+
if found {
316+
t.Fatalf("found tool but expected to be deleted")
317+
}
318+
deleted = resMgr.DeleteTool("nonexistent")
319+
if deleted {
320+
t.Fatalf("expected delete to be successful")
321+
}
322+
323+
deleted = resMgr.DeleteToolset("foo")
324+
if !deleted {
325+
t.Fatalf("expected delete to be successful")
326+
}
327+
_, found = resMgr.GetToolset("foo")
328+
if found {
329+
t.Fatalf("found toolset but expected to be deleted")
330+
}
331+
deleted = resMgr.DeleteToolset("nonexistent")
332+
if deleted {
333+
t.Fatalf("expected delete to be successful")
334+
}
335+
336+
deleted = resMgr.DeletePrompt("foo")
337+
if !deleted {
338+
t.Fatalf("expected delete to be successful")
339+
}
340+
_, found = resMgr.GetPrompt("foo")
341+
if found {
342+
t.Fatalf("found prompt but expected to be deleted")
343+
}
344+
deleted = resMgr.DeletePrompt("nonexistent")
345+
if deleted {
346+
t.Fatalf("expected delete to be successful")
347+
}
348+
}

0 commit comments

Comments
 (0)