Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cmd/thv/app/skill_builds_remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package app

import (
"fmt"

"github.com/spf13/cobra"
)

var skillBuildsRemoveCmd = &cobra.Command{
Use: "remove <tag>",
Short: "Remove a locally-built skill artifact",
Long: `Remove a locally-built OCI skill artifact and its blobs from the local OCI store.`,
Args: cobra.ExactArgs(1),
RunE: skillBuildsRemoveCmdFunc,
}

func init() {
skillBuildsCmd.AddCommand(skillBuildsRemoveCmd)
}

func skillBuildsRemoveCmdFunc(cmd *cobra.Command, args []string) error {
c := newSkillClient(cmd.Context())
if err := c.DeleteBuild(cmd.Context(), args[0]); err != nil {
return formatSkillError("remove build", err)
}
fmt.Printf("Removed build %q\n", args[0])
return nil
}
1 change: 1 addition & 0 deletions docs/cli/thv_skill_builds.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions docs/cli/thv_skill_builds_remove.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions docs/server/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions docs/server/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions docs/server/swagger.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ require (
github.com/shirou/gopsutil/v4 v4.26.2
github.com/spf13/viper v1.21.0
github.com/stacklok/toolhive-catalog v0.20260406.0
github.com/stacklok/toolhive-core v0.0.13
github.com/stacklok/toolhive-core v0.0.14
github.com/stretchr/testify v1.11.1
github.com/swaggo/swag/v2 v2.0.0-rc5
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,8 @@ github.com/stacklok/toolhive-catalog v0.20260406.0 h1:MzcSoYJmjwf+sOXiw9uw6wzfz/
github.com/stacklok/toolhive-catalog v0.20260406.0/go.mod h1:VeHaVQx4dP484wxQT947GA+ocP4YiuKCTN+v7upEPiU=
github.com/stacklok/toolhive-core v0.0.13 h1:iKMnI7VIVeBXkeCccF5UdOMy5RUPeL+Bmg4DjgFeiHU=
github.com/stacklok/toolhive-core v0.0.13/go.mod h1:AAeOC8CxDVtburJEkVVdR2bO5z2fiY9dgYRnhcjkHvI=
github.com/stacklok/toolhive-core v0.0.14 h1:/tyTrtoAMDPH66q1aeKIDDe50P4RGxKGP+bG+7MZ7gs=
github.com/stacklok/toolhive-core v0.0.14/go.mod h1:MQ+SN7cUwoKj5TX/LmuY1WLgDBm2vRpRwwwYOlT3hug=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
Expand Down
20 changes: 20 additions & 0 deletions pkg/api/v1/skills.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func SkillsRouter(skillService skills.SkillService) http.Handler {
r.Post("/build", apierrors.ErrorHandler(routes.buildSkill))
r.Post("/push", apierrors.ErrorHandler(routes.pushSkill))
r.Get("/builds", apierrors.ErrorHandler(routes.listBuilds))
r.Delete("/builds/{tag}", apierrors.ErrorHandler(routes.deleteBuild))

return r
}
Expand Down Expand Up @@ -302,3 +303,22 @@ func (s *SkillsRoutes) listBuilds(w http.ResponseWriter, r *http.Request) error
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(buildListResponse{Builds: builds})
}

// deleteBuild removes a locally-built OCI skill artifact from the local store.
//
// @Summary Delete a locally-built skill artifact
// @Description Remove a locally-built OCI skill artifact and its blobs from the local store
// @Tags skills
// @Param tag path string true "Artifact tag"
// @Success 204 {string} string "No Content"
// @Failure 404 {string} string "Not Found"
// @Failure 500 {string} string "Internal Server Error"
// @Router /api/v1beta/skills/builds/{tag} [delete]
func (s *SkillsRoutes) deleteBuild(w http.ResponseWriter, r *http.Request) error {
tag := chi.URLParam(r, "tag")
if err := s.skillService.DeleteBuild(r.Context(), tag); err != nil {
return err
}
w.WriteHeader(http.StatusNoContent)
return nil
}
30 changes: 30 additions & 0 deletions pkg/api/v1/skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,36 @@ func TestSkillsRouter(t *testing.T) {
expectedStatus: http.StatusInternalServerError,
expectedBody: "Internal Server Error",
},
{
name: "delete build success",
method: "DELETE",
path: "/builds/my-skill",
setupMock: func(svc *skillsmocks.MockSkillService, _ string) {
svc.EXPECT().DeleteBuild(gomock.Any(), "my-skill").Return(nil)
},
expectedStatus: http.StatusNoContent,
},
{
name: "delete build not found",
method: "DELETE",
path: "/builds/missing",
setupMock: func(svc *skillsmocks.MockSkillService, _ string) {
svc.EXPECT().DeleteBuild(gomock.Any(), "missing").
Return(httperr.WithCode(fmt.Errorf("tag not found"), http.StatusNotFound))
},
expectedStatus: http.StatusNotFound,
},
{
name: "delete build service error",
method: "DELETE",
path: "/builds/my-skill",
setupMock: func(svc *skillsmocks.MockSkillService, _ string) {
svc.EXPECT().DeleteBuild(gomock.Any(), "my-skill").
Return(httperr.WithCode(fmt.Errorf("oci store not configured"), http.StatusInternalServerError))
},
expectedStatus: http.StatusInternalServerError,
expectedBody: "Internal Server Error",
},
}

for _, tt := range tests {
Expand Down
5 changes: 5 additions & 0 deletions pkg/skills/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ func (c *Client) ListBuilds(ctx context.Context) ([]skills.LocalBuild, error) {
return resp.Builds, nil
}

// DeleteBuild removes a locally-built OCI skill artifact from the local store.
func (c *Client) DeleteBuild(ctx context.Context, tag string) error {
return c.doJSONRequest(ctx, http.MethodDelete, "/builds/"+url.PathEscape(tag), nil, nil, nil)
}

// --- internal helpers ---

func (c *Client) buildURL(path string, query url.Values) string {
Expand Down
14 changes: 14 additions & 0 deletions pkg/skills/mocks/mock_service.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/skills/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ type SkillService interface {
Push(ctx context.Context, opts PushOptions) error
// ListBuilds returns all locally-built OCI skill artifacts in the local store.
ListBuilds(ctx context.Context) ([]LocalBuild, error)
// DeleteBuild removes a locally-built OCI skill artifact from the local store.
DeleteBuild(ctx context.Context, tag string) error
}
13 changes: 13 additions & 0 deletions pkg/skills/skillsvc/skillsvc.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,19 @@ func (s *service) ListBuilds(ctx context.Context) ([]skills.LocalBuild, error) {
return builds, nil
}

// DeleteBuild removes a locally-built OCI skill artifact from the local store.
// It deletes the tag and, when no other tag shares the same digest, also
// garbage-collects all associated blobs.
func (s *service) DeleteBuild(ctx context.Context, tag string) error {
if s.ociStore == nil {
return httperr.WithCode(
errors.New("OCI packaging is not configured"),
http.StatusInternalServerError,
)
}
return s.ociStore.DeleteBuild(ctx, tag)
}

// ociPullTimeout is the maximum time allowed for pulling an OCI artifact.
const ociPullTimeout = 5 * time.Minute

Expand Down
Loading
Loading