Add support for image name history

We leverage the containers/storage image history tracking feature to
show the previously used image names when running:
`podman images --history`

Signed-off-by: Sascha Grunert <sgrunert@suse.com>
This commit is contained in:
Sascha Grunert
2019-11-26 16:08:04 +01:00
parent aef38585ed
commit 63e46cc85c
24 changed files with 346 additions and 40 deletions

View File

@ -1 +1 @@
1.14.0
1.15.0

View File

@ -16,6 +16,7 @@ import (
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"
@ -97,7 +98,7 @@ func legacyCopy(srcFile io.Reader, dstFile io.Writer) error {
func copyXattr(srcPath, dstPath, attr string) error {
data, err := system.Lgetxattr(srcPath, attr)
if err != nil {
if err != nil && err != unix.EOPNOTSUPP {
return err
}
if data != nil {
@ -271,6 +272,19 @@ func doCopyXattrs(srcPath, dstPath string) error {
return err
}
xattrs, err := system.Llistxattr(srcPath)
if err != nil && err != unix.EOPNOTSUPP {
return err
}
for _, key := range xattrs {
if strings.HasPrefix(key, "user.") {
if err := copyXattr(srcPath, dstPath, key); err != nil {
return err
}
}
}
// We need to copy this attribute if it appears in an overlay upper layer, as
// this function is used to copy those. It is set by overlay if a directory
// is removed and then re-created and should not inherit anything from the

View File

@ -3,5 +3,5 @@ package vfs
import "github.com/containers/storage/drivers/copy"
func dirCopy(srcDir, dstDir string) error {
return copy.DirCopy(srcDir, dstDir, copy.Content, false)
return copy.DirCopy(srcDir, dstDir, copy.Content, true)
}

View File

@ -47,6 +47,11 @@ type Image struct {
// or canonical references.
Names []string `json:"names,omitempty"`
// NamesHistory is an optional set of Names the image had in the past. The
// contained names are free from any duplicates, whereas the newest entry
// is the first one.
NamesHistory []string `json:"names-history,omitempty"`
// TopLayer is the ID of the topmost layer of the image itself, if the
// image contains one or more layers. Multiple images can refer to the
// same top layer.
@ -155,6 +160,7 @@ func copyImage(i *Image) *Image {
Digest: i.Digest,
Digests: copyDigestSlice(i.Digests),
Names: copyStringSlice(i.Names),
NamesHistory: copyStringSlice(i.NamesHistory),
TopLayer: i.TopLayer,
MappedTopLayers: copyStringSlice(i.MappedTopLayers),
Metadata: i.Metadata,
@ -481,6 +487,10 @@ func (r *imageStore) removeName(image *Image, name string) {
image.Names = stringSliceWithoutValue(image.Names, name)
}
func (i *Image) addNameToHistory(name string) {
i.NamesHistory = dedupeNames(append([]string{name}, i.NamesHistory...))
}
func (r *imageStore) SetNames(id string, names []string) error {
if !r.IsReadWrite() {
return errors.Wrapf(ErrStoreIsReadOnly, "not allowed to change image name assignments at %q", r.imagespath())
@ -495,6 +505,7 @@ func (r *imageStore) SetNames(id string, names []string) error {
r.removeName(otherImage, name)
}
r.byname[name] = image
image.addNameToHistory(name)
}
image.Names = names
return r.Save()

View File

@ -59,6 +59,22 @@ func (j *Image) MarshalJSONBuf(buf fflib.EncodingBuffer) error {
}
buf.WriteByte(',')
}
if len(j.NamesHistory) != 0 {
buf.WriteString(`"names-history":`)
if j.NamesHistory != nil {
buf.WriteString(`[`)
for i, v := range j.NamesHistory {
if i != 0 {
buf.WriteString(`,`)
}
fflib.WriteJsonString(buf, string(v))
}
buf.WriteString(`]`)
} else {
buf.WriteString(`null`)
}
buf.WriteByte(',')
}
if len(j.TopLayer) != 0 {
buf.WriteString(`"layer":`)
fflib.WriteJsonString(buf, string(j.TopLayer))
@ -171,6 +187,8 @@ const (
ffjtImageNames
ffjtImageNamesHistory
ffjtImageTopLayer
ffjtImageMappedTopLayers
@ -194,6 +212,8 @@ var ffjKeyImageDigest = []byte("digest")
var ffjKeyImageNames = []byte("names")
var ffjKeyImageNamesHistory = []byte("names-history")
var ffjKeyImageTopLayer = []byte("layer")
var ffjKeyImageMappedTopLayers = []byte("mapped-layers")
@ -348,6 +368,11 @@ mainparse:
currentKey = ffjtImageNames
state = fflib.FFParse_want_colon
goto mainparse
} else if bytes.Equal(ffjKeyImageNamesHistory, kn) {
currentKey = ffjtImageNamesHistory
state = fflib.FFParse_want_colon
goto mainparse
}
}
@ -400,6 +425,12 @@ mainparse:
goto mainparse
}
if fflib.EqualFoldRight(ffjKeyImageNamesHistory, kn) {
currentKey = ffjtImageNamesHistory
state = fflib.FFParse_want_colon
goto mainparse
}
if fflib.EqualFoldRight(ffjKeyImageNames, kn) {
currentKey = ffjtImageNames
state = fflib.FFParse_want_colon
@ -444,6 +475,9 @@ mainparse:
case ffjtImageNames:
goto handle_Names
case ffjtImageNamesHistory:
goto handle_NamesHistory
case ffjtImageTopLayer:
goto handle_TopLayer
@ -608,6 +642,80 @@ handle_Names:
state = fflib.FFParse_after_value
goto mainparse
handle_NamesHistory:
/* handler: j.NamesHistory type=[]string kind=slice quoted=false*/
{
{
if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null {
return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok))
}
}
if tok == fflib.FFTok_null {
j.NamesHistory = nil
} else {
j.NamesHistory = []string{}
wantVal := true
for {
var tmpJNamesHistory string
tok = fs.Scan()
if tok == fflib.FFTok_error {
goto tokerror
}
if tok == fflib.FFTok_right_brace {
break
}
if tok == fflib.FFTok_comma {
if wantVal == true {
// TODO(pquerna): this isn't an ideal error message, this handles
// things like [,,,] as an array value.
return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok))
}
continue
} else {
wantVal = true
}
/* handler: tmpJNamesHistory type=string kind=string quoted=false*/
{
{
if tok != fflib.FFTok_string && tok != fflib.FFTok_null {
return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for string", tok))
}
}
if tok == fflib.FFTok_null {
} else {
outBuf := fs.Output.Bytes()
tmpJNamesHistory = string(string(outBuf))
}
}
j.NamesHistory = append(j.NamesHistory, tmpJNamesHistory)
wantVal = false
}
}
}
state = fflib.FFParse_after_value
goto mainparse
handle_TopLayer:
/* handler: j.TopLayer type=string kind=string quoted=false*/

View File

@ -387,7 +387,10 @@ func fillGo18FileTypeBits(mode int64, fi os.FileInfo) int64 {
// ReadSecurityXattrToTarHeader reads security.capability xattr from filesystem
// to a tar header
func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error {
capability, _ := system.Lgetxattr(path, "security.capability")
capability, err := system.Lgetxattr(path, "security.capability")
if err != nil && err != system.EOPNOTSUPP {
return err
}
if capability != nil {
hdr.Xattrs = make(map[string]string)
hdr.Xattrs["security.capability"] = string(capability)
@ -395,6 +398,27 @@ func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error {
return nil
}
// ReadUserXattrToTarHeader reads user.* xattr from filesystem to a tar header
func ReadUserXattrToTarHeader(path string, hdr *tar.Header) error {
xattrs, err := system.Llistxattr(path)
if err != nil && err != system.EOPNOTSUPP {
return err
}
for _, key := range xattrs {
if strings.HasPrefix(key, "user.") {
value, err := system.Lgetxattr(path, key)
if err != nil {
return err
}
if hdr.Xattrs == nil {
hdr.Xattrs = make(map[string]string)
}
hdr.Xattrs[key] = string(value)
}
}
return nil
}
type tarWhiteoutConverter interface {
ConvertWrite(*tar.Header, string, os.FileInfo) (*tar.Header, error)
ConvertRead(*tar.Header, string) (bool, error)
@ -469,6 +493,9 @@ func (ta *tarAppender) addTarFile(path, name string) error {
if err := ReadSecurityXattrToTarHeader(path, hdr); err != nil {
return err
}
if err := ReadUserXattrToTarHeader(path, hdr); err != nil {
return err
}
if ta.CopyPass {
copyPassHeader(hdr)
}

View File

@ -8,6 +8,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"syscall"
@ -263,6 +264,7 @@ type FileInfo struct {
children map[string]*FileInfo
capability []byte
added bool
xattrs map[string]string
}
// LookUp looks up the file information of a file.
@ -331,7 +333,8 @@ func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) {
// breaks down is if some code intentionally hides a change by setting
// back mtime
if statDifferent(oldStat, oldInfo, newStat, info) ||
!bytes.Equal(oldChild.capability, newChild.capability) {
!bytes.Equal(oldChild.capability, newChild.capability) ||
!reflect.DeepEqual(oldChild.xattrs, newChild.xattrs) {
change := Change{
Path: newChild.path(),
Kind: ChangeModify,

View File

@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"unsafe"
@ -83,7 +84,26 @@ func walkchunk(path string, fi os.FileInfo, dir string, root *FileInfo) error {
return err
}
info.stat = stat
info.capability, _ = system.Lgetxattr(cpath, "security.capability") // lgetxattr(2): fs access
info.capability, err = system.Lgetxattr(cpath, "security.capability") // lgetxattr(2): fs access
if err != nil && err != system.EOPNOTSUPP {
return err
}
xattrs, err := system.Llistxattr(cpath)
if err != nil && err != system.EOPNOTSUPP {
return err
}
for _, key := range xattrs {
if strings.HasPrefix(key, "user.") {
value, err := system.Lgetxattr(cpath, key)
if err != nil {
return err
}
if info.xattrs == nil {
info.xattrs = make(map[string]string)
}
info.xattrs[key] = string(value)
}
}
parent.children[info.name] = info
return nil
}

View File

@ -1,6 +1,16 @@
package system
import "golang.org/x/sys/unix"
import (
"bytes"
"syscall"
"golang.org/x/sys/unix"
)
const (
// Operation not supported
EOPNOTSUPP syscall.Errno = unix.EOPNOTSUPP
)
// Lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
@ -27,3 +37,32 @@ func Lgetxattr(path string, attr string) ([]byte, error) {
func Lsetxattr(path string, attr string, data []byte, flags int) error {
return unix.Lsetxattr(path, attr, data, flags)
}
// Llistxattr lists extended attributes associated with the given path
// in the file system.
func Llistxattr(path string) ([]string, error) {
var dest []byte
for {
sz, err := unix.Llistxattr(path, dest)
if err != nil {
return nil, err
}
if sz > len(dest) {
dest = make([]byte, sz)
} else {
dest = dest[:sz]
break
}
}
var attrs []string
for _, token := range bytes.Split(dest, []byte{0}) {
if len(token) > 0 {
attrs = append(attrs, string(token))
}
}
return attrs, nil
}

View File

@ -2,6 +2,13 @@
package system
import "syscall"
const (
// Operation not supported
EOPNOTSUPP syscall.Errno = syscall.Errno(0)
)
// Lgetxattr is not supported on platforms other than linux.
func Lgetxattr(path string, attr string) ([]byte, error) {
return nil, ErrNotSupportedPlatform
@ -11,3 +18,8 @@ func Lgetxattr(path string, attr string) ([]byte, error) {
func Lsetxattr(path string, attr string, data []byte, flags int) error {
return ErrNotSupportedPlatform
}
// Llistxattr is not supported on platforms other than linux.
func Llistxattr(path string) ([]string, error) {
return nil, ErrNotSupportedPlatform
}

View File

@ -70,6 +70,18 @@ func ParseIDMapping(UIDMapSlice, GIDMapSlice []string, subUIDMap, subGIDMap stri
// GetRootlessRuntimeDir returns the runtime directory when running as non root
func GetRootlessRuntimeDir(rootlessUid int) (string, error) {
path, err := getRootlessRuntimeDir(rootlessUid)
if err != nil {
return "", err
}
path = filepath.Join(path, "containers")
if err := os.MkdirAll(path, 0700); err != nil {
return "", errors.Wrapf(err, "unable to make rootless runtime dir %s", path)
}
return path, nil
}
func getRootlessRuntimeDir(rootlessUid int) (string, error) {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir != "" {