mirror of
https://github.com/clearlinux/rkt.git
synced 2026-08-25 17:45:49 +00:00
Merge pull request #603 from sgotti/rktrmimage
rkt: add command to remove one or more images from the store.
This commit is contained in:
+151
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user