diff --git a/api/logic/layers.go b/api/logic/layers.go index 35ca24e2..47c1f9bc 100644 --- a/api/logic/layers.go +++ b/api/logic/layers.go @@ -51,6 +51,18 @@ func POSTLayers(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { jsonhttp.Render(w, http.StatusCreated, struct{ Version string }{Version: strconv.Itoa(worker.Version)}) } +// DeleteLayer deletes the specified layer and any child layers that are +// dependent on the specified layer. +func DELETELayers(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + err := database.DeleteLayer(p.ByName("id")) + if err != nil { + jsonhttp.RenderError(w, 0, err) + return + } + + jsonhttp.Render(w, http.StatusNoContent, nil) +} + // GETLayersOS returns the operating system of a layer if it exists. // It uses not only the specified layer but also its parent layers if necessary. // An empty OS string is returned if no OS has been detected. diff --git a/api/router.go b/api/router.go index 2e07a2b8..ff0c44ef 100644 --- a/api/router.go +++ b/api/router.go @@ -68,6 +68,7 @@ func NewRouterV1(to time.Duration) *httprouter.Router { // Layers router.POST("/layers", wrap(logic.POSTLayers)) + router.DELETE("/layers/:id", wrap(logic.DELETELayers)) router.GET("/layers/:id/os", wrap(logic.GETLayersOS)) router.GET("/layers/:id/parent", wrap(logic.GETLayersParent)) router.GET("/layers/:id/packages", wrap(logic.GETLayersPackages)) diff --git a/database/layer.go b/database/layer.go index 1ee9613c..50fbc484 100644 --- a/database/layer.go +++ b/database/layer.go @@ -131,6 +131,60 @@ func InsertLayer(layer *Layer) error { return nil } +// DeleteLayer deletes the specified layer and any child layers that are +// dependent on the specified layer. +func DeleteLayer(ID string) error { + layer, err := FindOneLayerByID(ID, []string{}) + if err != nil { + return err + } + return deleteLayerTreeFrom(layer.Node, nil) +} + +func deleteLayerTreeFrom(node string, t *graph.Transaction) error { + // Determine if that function call is the root call of the recursivity + // And create transaction if its the case. + root := (t == nil) + if root { + t = cayley.NewTransaction() + } + + // Find layer. + layer, err := FindOneLayerByNode(node, FieldLayerAll) + if err != nil { + // Ignore missing layer. + return nil + } + + // Remove all successor layers. + for _, succNode := range layer.SuccessorsNodes { + deleteLayerTreeFrom(succNode, t) + } + + // Remove layer. + t.RemoveQuad(cayley.Quad(layer.Node, FieldIs, FieldLayerIsValue, "")) + t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerID, layer.ID, "")) + t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerParent, layer.ParentNode, "")) + t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerOS, layer.OS, "")) + t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerEngineVersion, strconv.Itoa(layer.EngineVersion), "")) + for _, pkg := range layer.InstalledPackagesNodes { + t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerInstalledPackages, pkg, "")) + } + for _, pkg := range layer.RemovedPackagesNodes { + t.RemoveQuad(cayley.Quad(layer.Node, FieldLayerRemovedPackages, pkg, "")) + } + + // Apply transaction if root call. + if root { + if err = store.ApplyTransaction(t); err != nil { + log.Errorf("failed transaction (deleteLayerTreeFrom): %s", err) + return ErrTransaction + } + } + + return nil +} + // FindOneLayerByID finds and returns a single layer having the given ID, // selecting the specified fields and hardcoding its ID func FindOneLayerByID(ID string, selectedFields []string) (*Layer, error) { diff --git a/database/layer_test.go b/database/layer_test.go index 96415446..dfcb6cdb 100644 --- a/database/layer_test.go +++ b/database/layer_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/coreos/clair/utils" + cerrors "github.com/coreos/clair/utils/errors" "github.com/stretchr/testify/assert" ) @@ -38,7 +39,7 @@ func TestLayerSimple(t *testing.T) { // Insert a layer and find it back l1 := &Layer{ID: "l1", OS: "os1", InstalledPackagesNodes: []string{"p1", "p2"}, EngineVersion: 1} if assert.Nil(t, InsertLayer(l1)) { - fl1, err := FindOneLayerByID("l1", FieldLayerAll) + fl1, err := FindOneLayerByID(l1.ID, FieldLayerAll) if assert.Nil(t, err) && assert.NotNil(t, fl1) { // Saved = found assert.True(t, layerEqual(l1, fl1), "layers are not equal, expected %v, have %s", l1, fl1) @@ -66,6 +67,12 @@ func TestLayerSimple(t *testing.T) { if assert.Nil(t, err) && assert.Len(t, al1, 1) { assert.Equal(t, al1[0].Node, l1.Node) } + + // Delete + if assert.Nil(t, DeleteLayer(l1.ID)) { + _, err := FindOneLayerByID(l1.ID, FieldLayerAll) + assert.Equal(t, cerrors.ErrNotFound, err) + } } } @@ -119,6 +126,14 @@ func TestLayerTree(t *testing.T) { fl4bpkg, err := flayers[4].AllPackages() assert.Nil(t, err) assert.Len(t, fl4bpkg, 0) + + // Delete a layer in the middle of the tree. + if assert.Nil(t, DeleteLayer(flayers[1].ID)) { + for _, l := range layers[1:] { + _, err := FindOneLayerByID(l.ID, FieldLayerAll) + assert.Equal(t, cerrors.ErrNotFound, err) + } + } } } diff --git a/docs/API.md b/docs/API.md index 09b7ef7a..26db2e68 100644 --- a/docs/API.md +++ b/docs/API.md @@ -150,6 +150,41 @@ HTTP/1.1 400 Bad Request It could also return a `415 Unsupported Media Type` response with a `Message` if the request content is not valid JSON. +## Delete a Layer + +It deletes a layer from the database and any child layers that are dependent on the specified layer. + + DELETE /v1/layers/{ID} + +### Parameters + +|Name|Type|Description| +|------|-----|-------------| +|ID|String|Unique ID of the Layer| + +### Example + +``` +curl -s -X DELETE 127.0.0.1:6060/v1/layers/39bb80489af75406073b5364c9c326134015140e1f7976a370a8bd446889e6f8 +``` + +### Success Response + +``` +HTTP/1.1 204 No Content +``` + +### Error Response + +``` +HTTP/1.1 404 Not Found +{ + "Message": "the resource cannot be found" +} +``` + +////////// + ## Get a Layer's operating system It returns the operating system a given Layer. @@ -210,6 +245,7 @@ HTTP/1.1 200 OK ``` ### Error Response + ``` HTTP/1.1 404 Not Found {