Merge pull request #603 from sgotti/rktrmimage

rkt: add command to remove one or more images from the store.
This commit is contained in:
Jonathan Boulle
2015-04-30 11:58:59 -07:00
5 changed files with 375 additions and 5 deletions
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"flag"
"fmt"
"github.com/coreos/rkt/store"
"github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema/types"
)
var (
cmdRmImage = &Command{
Name: "rmimage",
Summary: "Remove image(s) with the given key(s) from the local store",
Usage: "IMAGEID...",
Run: runRmImage,
Flags: &rmImageFlags,
}
rmImageFlags flag.FlagSet
)
func init() {
commands = append(commands, cmdRmImage)
}
func runRmImage(args []string) (exit int) {
if len(args) < 1 {
stderr("rkt: Must provide at least one image key")
return 1
}
s, err := store.NewStore(globalFlags.Dir)
if err != nil {
stderr("rkt: cannot open store: %v\n", err)
return 1
}
referencedImgs, err := getReferencedImgs(s)
if err != nil {
stderr("rkt: cannot get referenced images: %v\n", err)
return 1
}
//TODO(sgotti) Which return code to use when the removal fails only for some images?
done := 0
errors := 0
staleErrors := 0
for _, pkey := range args {
errors++
h, err := types.NewHash(pkey)
if err != nil {
stderr("rkt: wrong imageID %q: %v\n", pkey, err)
continue
}
key, err := s.ResolveKey(h.String())
if err != nil {
stderr("rkt: imageID %q not valid: %v\n", pkey, err)
continue
}
if key == "" {
stderr("rkt: imageID %q doesn't exists\n", pkey)
continue
}
err = s.RemoveACI(key)
if err != nil {
if serr, ok := err.(*store.StoreRemovalError); ok {
staleErrors++
stderr("rkt: some files cannot be removed for imageID %q: %v\n", pkey, serr)
} else {
stderr("rkt: error removing aci for imageID %q: %v\n", pkey, err)
}
continue
}
stdout("rkt: successfully removed aci for imageID: %q\n", pkey)
// Remove the treestore only if the image isn't referenced by
// some containers
// TODO(sgotti) there's a windows between getting refenced
// images and this check where a new container could be
// prepared/runned with this image. To avoid this a global lock
// is needed.
if _, ok := referencedImgs[key]; ok {
stderr("rkt: imageID is referenced by some containers, cannot remove the tree store")
continue
} else {
err = s.RemoveTreeStore(key)
if err != nil {
staleErrors++
stderr("rkt: error removing treestore for imageID %q: %v\n", pkey, err)
continue
}
}
errors--
done++
}
if done > 0 {
stdout("rkt: %d image(s) successfully removed\n", done)
}
if errors > 0 {
stdout("rkt: %d image(s) cannot be removed\n", errors)
}
if staleErrors > 0 {
stdout("rkt: %d image(s) removed but left some stale files\n", staleErrors)
}
return 0
}
func getReferencedImgs(s *store.Store) (map[string]struct{}, error) {
imgs := map[string]struct{}{}
walkErrors := []error{}
// Consider pods in preparing, prepared, run, exitedgarbage state
if err := walkPods(includeMostDirs, func(p *pod) {
appImgs, err := p.getAppsHashes()
if err != nil {
// Ignore errors reading/parsing pod file
return
}
for _, appImg := range appImgs {
key, err := s.ResolveKey(appImg.String())
if err != nil && err != store.ErrKeyNotFound {
walkErrors = append(walkErrors, fmt.Errorf("bad imageID %q in pod definition: %v", appImg.String(), err))
return
}
imgs[key] = struct{}{}
}
}); err != nil {
return nil, fmt.Errorf("failed to get pod handles: %v", err)
}
if len(walkErrors) > 0 {
return nil, fmt.Errorf("errors occured walking pods. errors: %v", walkErrors)
}
return imgs, nil
}
+32
View File
@@ -72,6 +72,29 @@ func GetACIInfosWithAppName(tx *sql.Tx, appname string) ([]*ACIInfo, bool, error
return aciinfos, found, err
}
// GetAciInfosWithBlobKey returns the ACIInfo with the given blobKey. found will be
// false if no aciinfo exists.
func GetACIInfoWithBlobKey(tx *sql.Tx, blobKey string) (*ACIInfo, bool, error) {
aciinfo := &ACIInfo{}
found := false
rows, err := tx.Query("SELECT * from aciinfo WHERE blobkey == $1", blobKey)
if err != nil {
return nil, false, err
}
for rows.Next() {
found = true
if err := rows.Scan(&aciinfo.BlobKey, &aciinfo.AppName, &aciinfo.ImportTime, &aciinfo.Latest); err != nil {
return nil, false, err
}
// No more than one row for blobkey must exist.
break
}
if err := rows.Err(); err != nil {
return nil, false, err
}
return aciinfo, found, err
}
// GetAllACIInfos returns all the ACIInfos sorted by optional sortfields and
// with ascending or descending order.
func GetAllACIInfos(tx *sql.Tx, sortfields []string, ascending bool) ([]*ACIInfo, error) {
@@ -117,3 +140,12 @@ func WriteACIInfo(tx *sql.Tx, aciinfo *ACIInfo) error {
return nil
}
// RemoveACIInfo removes the ACIInfo with the given blobKey.
func RemoveACIInfo(tx *sql.Tx, blobKey string) error {
_, err := tx.Exec("DELETE from aciinfo where blobkey == $1", blobKey)
if err != nil {
return err
}
return nil
}
+9
View File
@@ -70,3 +70,12 @@ func WriteRemote(tx *sql.Tx, remote *Remote) error {
}
return nil
}
// RemoveRemote removes the remote with the given blobKey.
func RemoveRemote(tx *sql.Tx, blobKey string) error {
_, err := tx.Exec("DELETE FROM remote WHERE blobkey == $1", blobKey)
if err != nil {
return err
}
return nil
}
+89 -5
View File
@@ -20,6 +20,7 @@ import (
"crypto/sha512"
"database/sql"
"encoding/json"
"errors"
"fmt"
"hash"
"io"
@@ -59,6 +60,26 @@ var diskvStores = [...]string{
"imageManifest",
}
var (
ErrKeyNotFound = errors.New("no keys found")
)
// StoreRemovalError defines an error removing a non transactional store (like
// a diskv store or the tree store).
// When this happen there's the possibility that the store is left in an
// unclean state (for example with some stale files).
type StoreRemovalError struct {
errors []error
}
func (e *StoreRemovalError) Error() string {
s := fmt.Sprintf("some aci disk entries cannot be removed: ")
for _, err := range e.errors {
s = s + fmt.Sprintf("[%v]", err)
}
return s
}
// Store encapsulates a content-addressable-storage for storing ACIs on disk.
type Store struct {
base string
@@ -220,7 +241,7 @@ func (s Store) ResolveKey(key string) (string, error) {
keyCount := len(aciInfos)
if keyCount == 0 {
return "", fmt.Errorf("no keys found")
return "", ErrKeyNotFound
}
if keyCount != 1 {
return "", fmt.Errorf("ambiguous key: %q", key)
@@ -326,6 +347,57 @@ func (s Store) WriteACI(r io.Reader, latest bool) (string, error) {
return key, nil
}
// RemoveACI removes the ACI with the given key. It firstly removes the aci
// infos inside the db, then it tries to remove the non transactional data.
// If some error occurs removing some non transactional data a
// StoreRemovalError is returned.
func (ds Store) RemoveACI(key string) error {
imageKeyLock, err := lock.ExclusiveKeyLock(ds.imageLockDir, key)
if err != nil {
return fmt.Errorf("error locking image: %v", err)
}
defer imageKeyLock.Close()
// Firstly remove aciinfo and remote from the db in an unique transaction.
// remote needs to be removed or a GetRemote will return a blobKey not
// referenced by any ACIInfo.
err = ds.db.Do(func(tx *sql.Tx) error {
if _, found, err := GetACIInfoWithBlobKey(tx, key); err != nil {
return fmt.Errorf("error getting aciinfo: %v", err)
} else if !found {
return fmt.Errorf("cannot find image with key: %s", key)
}
if err := RemoveACIInfo(tx, key); err != nil {
return err
}
if err := RemoveRemote(tx, key); err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("cannot remove image with key: %s from db: %v", key, err)
}
// Then remove non transactional entries from the blob, imageManifest
// and tree store.
// TODO(sgotti). Now that the ACIInfo is removed the image doesn't
// exists anymore, but errors removing non transactional entries can
// leave stale data that will require a cas GC to be implemented.
storeErrors := []error{}
for _, s := range ds.stores {
if err := s.Erase(key); err != nil {
// If there's an error save it and continue with the other stores
storeErrors = append(storeErrors, err)
}
}
if len(storeErrors) > 0 {
return &StoreRemovalError{errors: storeErrors}
}
return nil
}
// RenderTreeStore renders a treestore for the given image key if it's not
// already fully rendered.
// Users of treestore should call s.RenderTreeStore before using it to ensure
@@ -353,12 +425,10 @@ func (s Store) RenderTreeStore(key string, rebuild bool) error {
// Firstly remove a possible partial treestore if existing.
// This is needed as a previous ACI removal operation could have failed
// cleaning the tree store leaving some stale files.
err = s.treestore.Remove(key)
if err != nil {
if err := s.treestore.Remove(key); err != nil {
return err
}
err = s.treestore.Write(key, &s)
if err != nil {
if err := s.treestore.Write(key, &s); err != nil {
return err
}
return nil
@@ -390,6 +460,20 @@ func (s Store) GetTreeStoreRootFS(key string) string {
return s.treestore.GetRootFS(key)
}
// RemoveTreeStore removes the rendered image in tree store with the given key.
func (ds Store) RemoveTreeStore(key string) error {
treeStoreKeyLock, err := lock.ExclusiveKeyLock(ds.treeStoreLockDir, key)
if err != nil {
return fmt.Errorf("error locking tree store: %v", err)
}
defer treeStoreKeyLock.Close()
if err := ds.treestore.Remove(key); err != nil {
return fmt.Errorf("error removing the tree store: %v", err)
}
return nil
}
// GetRemote tries to retrieve a remote with the given ACIURL. found will be
// false if remote doesn't exist.
func (s Store) GetRemote(aciURL string) (*Remote, bool, error) {
+94
View File
@@ -449,5 +449,99 @@ func TestTreeStore(t *testing.T) {
if err == nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRemoveACI(t *testing.T) {
dir, err := ioutil.TempDir("", tstprefix)
if err != nil {
t.Fatalf("error creating tempdir: %v", err)
}
defer os.RemoveAll(dir)
ds, err := NewStore(dir)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
imj := `{
"acKind": "ImageManifest",
"acVersion": "0.4.0",
"name": "example.com/test01"
}`
aciFile, err := aci.NewACI(dir, imj, nil)
if err != nil {
t.Fatalf("error creating test tar: %v", err)
}
// Rewind the ACI
if _, err := aciFile.Seek(0, 0); err != nil {
t.Fatalf("unexpected error %v", err)
}
key, err := ds.WriteACI(aciFile, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
aciURL := "http://example.com/test01.aci"
// Create our first Remote, and simulate Store() to create row in the table
na := NewRemote(aciURL, "")
na.BlobKey = key
ds.WriteRemote(na)
err = ds.RemoveACI(key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify that no remote for the specified key exists
_, found, err := ds.GetRemote(aciURL)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if found {
t.Fatalf("expected to find no remote, but a remote was found")
}
// Try to remove a non-existent key
err = ds.RemoveACI("sha512-aaaaaaaaaaaaaaaaa")
if err == nil {
t.Fatalf("expected error")
}
// Simulate error removing from the
imj = `{
"acKind": "ImageManifest",
"acVersion": "0.3.0",
"name": "example.com/test01"
}`
aciFile, err = aci.NewACI(dir, imj, nil)
if err != nil {
t.Fatalf("error creating test tar: %v", err)
}
// Rewind the ACI
if _, err := aciFile.Seek(0, 0); err != nil {
t.Fatalf("unexpected error %v", err)
}
key, err = ds.WriteACI(aciFile, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
aciURL = "http://example.com/test02.aci"
// Create our first Remote, and simulate Store() to create row in the table
na = NewRemote(aciURL, "")
na.BlobKey = key
ds.WriteRemote(na)
err = os.Remove(filepath.Join(dir, "cas", "blob", blockTransform(key)[0], blockTransform(key)[1], key))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
err = ds.RemoveACI(key)
if err == nil {
t.Fatalf("expected error: %v", err)
}
if _, ok := err.(*StoreRemovalError); !ok {
t.Fatalf("expected StoreRemovalError got: %v", err)
}
}