Skip to content

Commit

Permalink
libimage: follow-up changes
Browse files Browse the repository at this point in the history
The following changes were not split into smaller commits since the
entire package is still work in progress and I want to keep moving:

 * Various small fixes.

 * The internal image cache has been removed as it's a recipe for
   inconsistencies for longer running processes.  This should make
   libimage easier to use for CRI-O and a Podman service.

 * LookupImage now returns storage.ErrUnknownImage rather than nil.
   This simplifies the callers and makes sure we have a consistent
   error.

 * LookupImage is now able to handle manifests lists.  Unless the
   platform is explicitly ignored via the options, the matching
   image within the manifest list is now returned.  This greatly
   simplifies the spec generation in Podman; no callers should have
   to worry about this kind of detail.

 * LookupImage has been refactored into smaller-sized and easier to
   read functions.

 * RemoveImages has been changed to assemble the data of removed or
   untagged images.  This comes in handy for pruning images.  I am
   heavily against having a dedicated API for pruning since the it's
   really just a combination of filtering and removing images which
   RemoveImages already supports.  Hence these changes to satisfy
   the needs of `podman image prune`.
   Furthermore, it now returns an []error slice rather than a single
   error.  Again to make Podman happy which needs to inspect *all*
   errors for setting the appropriate exit code.

 * A rather large refactoring of the removal code along with very
   verbose comments.  Those were largely absent in the Podman code base
   but there many rules and contracts embedded that I partially could
   only reconstruct by manually tests and comparing to Docker.

 * Add a new `containers={true,false}` filter which allows filtering
   images whether they are used by containers (=true) or if no container
   is using them (=false).  This filter is required for pruning images
   in Podman.

 * `libimage/types` has been merged into `libimage`.  Podman has to do
   _a lot of_ massaging for the remote client already and the types
   are pretty much nailed down for the remote API.  Hence, I prefer to
   do some translation between `libimage` types and what Podman needs
   rather than splitting `libimage` in half without an obvious reason.
   This way the package is self-contained allowing for an easier
   navigation and maintenance.

 * `libimage.PullPolicy` has been merged into `pkg/config.PullPolicy`
   to have _one_ central place to deal with pull policies.  The type
   system in `pkg/config` sets "always" as the default unfortunately
   but I think consistency is more important at that point.

 * Added `CopyOptions.DirForceCompress` to enforce layer compression
   when copying to a `dir` destination.

 * We now use `github.com/disiqueira/gotree` for pretty printing image
   trees.  That greatly simplifies the code and we don't have to worry
   about the logic of printing a tree.  Note that trees are now always
   printed top down!

 * Added a new `libimage.ManifestList` type along with an API for local
   lookups and performing certain operations on it to wrap around
   `libimage/manifests` as previously done in `libpod/image` and other
   places in Podman.

 * Correct caching of `(*Image).Inspect`.

 * In addition to username, password and credentials, allow for
   speciying an identity token for copying images.  That's needed for
   Podman's remote API.

 * Make image removal more tolerant toward corrupted images.

 * A new "until=timestamp" filter that can be used by all APIs
   supporting filtering.

 * An empty string now resolves to PullPolicyMissing.

 * `(*Runtime) systemContextCopy()` returns a deep copy of the runtime's
   system context.  Golang's shallow copies are very dangerous for long
   running processes such as Podman's system service.  Hence, we need to
   make sure that base data is not altered over time.  That adds another
   external dependency but I do not see a way around that.  Long term,
   I desire a `(*containers/image/types.SystemContext).Copy()` function.

Signed-off-by: Valentin Rothberg <[email protected]>
  • Loading branch information
vrothberg committed May 3, 2021
1 parent e46355c commit c53283f
Show file tree
Hide file tree
Showing 38 changed files with 2,484 additions and 536 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ require (
github.com/containers/image/v5 v5.11.1
github.com/containers/ocicrypt v1.1.1
github.com/containers/storage v1.30.1
github.com/disiqueira/gotree/v3 v3.0.2
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.3-0.20210216175712-646072ed6524+incompatible
github.com/docker/go-units v0.4.0
github.com/ghodss/yaml v1.0.0
github.com/google/go-cmp v0.5.5 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/jinzhu/copier v0.3.0
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/onsi/ginkgo v1.16.1
github.com/onsi/gomega v1.11.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWhkNRq8=
github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
Expand Down Expand Up @@ -405,6 +407,8 @@ github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
github.com/jinzhu/copier v0.3.0 h1:P5zN9OYSxmtzZmwgcVmt5Iu8egfP53BGMPAFgEksKPI=
github.com/jinzhu/copier v0.3.0/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
Expand Down
50 changes: 33 additions & 17 deletions libimage/copier.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type CopyOptions struct {
BlobInfoCacheDirPath string
// Path to the certificates directory.
CertDirPath string
// Force layer compression when copying to a `dir` transport destination.
DirForceCompress bool
// Allow contacting registries over HTTP, or HTTPS with failed TLS
// verification. Note that this does not affect other TLS connections.
InsecureSkipTLSVerify types.OptionalBool
Expand Down Expand Up @@ -115,6 +117,9 @@ type CopyOptions struct {
// "username[:password]". Cannot be used in combination with
// Username/Password.
Credentials string
// IdentityToken is used to authenticate the user and get
// an access token for the registry.
IdentityToken string `json:"identitytoken,omitempty"`

// ----- internal -----------------------------------------------------

Expand Down Expand Up @@ -146,30 +151,33 @@ var (
// getDockerAuthConfig extracts a docker auth config from the CopyOptions. Returns
// nil if no credentials are set.
func (options *CopyOptions) getDockerAuthConfig() (*types.DockerAuthConfig, error) {
authConf := &types.DockerAuthConfig{IdentityToken: options.IdentityToken}

if options.Username != "" {
if options.Credentials != "" {
return nil, errors.New("username/password cannot be used with credentials")
}
return &types.DockerAuthConfig{
Username: options.Username,
Password: options.Password,
}, nil
authConf.Username = options.Username
authConf.Password = options.Password
return authConf, nil
}

if options.Credentials != "" {
var username, password string
split := strings.SplitN(options.Credentials, ":", 2)
switch len(split) {
case 1:
username = split[0]
authConf.Username = split[0]
default:
username = split[0]
password = split[1]
authConf.Username = split[0]
authConf.Password = split[1]
}
return &types.DockerAuthConfig{
Username: username,
Password: password,
}, nil
return authConf, nil
}

// We should return nil unless a token was set. That's especially
// useful for Podman's remote API.
if options.IdentityToken != "" {
return authConf, nil
}

return nil, nil
Expand All @@ -178,8 +186,9 @@ func (options *CopyOptions) getDockerAuthConfig() (*types.DockerAuthConfig, erro
// newCopier creates a copier. Note that fields in options *may* overwrite the
// counterparts of the specified system context. Please make sure to call
// `(*copier).close()`.
func newCopier(sys *types.SystemContext, options *CopyOptions) (*copier, error) {
func (r *Runtime) newCopier(options *CopyOptions) (*copier, error) {
c := copier{}
c.systemContext = r.systemContextCopy()

if options.SourceLookupReferenceFunc != nil {
c.sourceLookup = options.SourceLookupReferenceFunc
Expand All @@ -189,11 +198,14 @@ func newCopier(sys *types.SystemContext, options *CopyOptions) (*copier, error)
c.destinationLookup = options.DestinationLookupReferenceFunc
}

c.systemContext = sys
if c.systemContext == nil {
c.systemContext = &types.SystemContext{}
if options.InsecureSkipTLSVerify != types.OptionalBoolUndefined {
c.systemContext.DockerInsecureSkipTLSVerify = options.InsecureSkipTLSVerify
c.systemContext.OCIInsecureSkipTLSVerify = options.InsecureSkipTLSVerify == types.OptionalBoolTrue
c.systemContext.DockerDaemonInsecureSkipTLSVerify = options.InsecureSkipTLSVerify == types.OptionalBoolTrue
}

c.systemContext.DirForceCompress = c.systemContext.DirForceCompress || options.DirForceCompress

if options.AuthFilePath != "" {
c.systemContext.AuthFilePath = options.AuthFilePath
}
Expand Down Expand Up @@ -226,7 +238,11 @@ func newCopier(sys *types.SystemContext, options *CopyOptions) (*copier, error)
c.systemContext.BlobInfoCacheDir = options.BlobInfoCacheDirPath
}

policy, err := signature.DefaultPolicy(sys)
if options.CertDirPath != "" {
c.systemContext.DockerCertPath = options.CertDirPath
}

policy, err := signature.DefaultPolicy(c.systemContext)
if err != nil {
return nil, err
}
Expand Down
126 changes: 126 additions & 0 deletions libimage/disk_usage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package libimage

import (
"context"
"time"
)

// ImageDiskUsage reports the total size of an image. That is the size
type ImageDiskUsage struct {
// Number of containers using the image.
Containers int
// ID of the image.
ID string
// Repository of the image.
Repository string
// Tag of the image.
Tag string
// Created time stamp.
Created time.Time
// The amount of space that an image shares with another one (i.e. their common data).
SharedSize int64
// The the amount of space that is only used by a given image.
UniqueSize int64
// Sum of shared an unique size.
Size int64
}

// DiskUsage calculates the disk usage for each image in the local containers
// storage. Note that a single image may yield multiple usage reports, one for
// each repository tag.
func (r *Runtime) DiskUsage(ctx context.Context) ([]ImageDiskUsage, error) {
layerTree, err := r.layerTree()
if err != nil {
return nil, err
}

images, err := r.ListImages(ctx, nil, nil)
if err != nil {
return nil, err
}

var allUsages []ImageDiskUsage
for _, image := range images {
usages, err := diskUsageForImage(ctx, image, layerTree)
if err != nil {
return nil, err
}
allUsages = append(allUsages, usages...)
}
return allUsages, err
}

// diskUsageForImage returns the disk-usage baseistics for the specified image.
func diskUsageForImage(ctx context.Context, image *Image, tree *layerTree) ([]ImageDiskUsage, error) {
base := ImageDiskUsage{
ID: image.ID(),
Created: image.Created(),
Repository: "<none>",
Tag: "<none>",
}

// Shared, unique and total size.
parent, err := tree.parent(ctx, image)
if err != nil {
return nil, err
}
childIDs, err := tree.children(ctx, image, false)
if err != nil {
return nil, err
}

// Optimistically set unique size to the full size of the image.
size, err := image.Size()
if err != nil {
return nil, err
}
base.UniqueSize = size

if len(childIDs) > 0 {
// If we have children, we share everything.
base.SharedSize = base.UniqueSize
base.UniqueSize = 0
} else if parent != nil {
// If we have no children but a parent, remove the parent
// (shared) size from the unique one.
size, err := parent.Size()
if err != nil {
return nil, err
}
base.UniqueSize -= size
base.SharedSize = size
}

base.Size = base.SharedSize + base.UniqueSize

// Number of containers using the image.
containers, err := image.Containers()
if err != nil {
return nil, err
}
base.Containers = len(containers)

repoTags, err := image.NamedRepoTags()
if err != nil {
return nil, err
}

if len(repoTags) == 0 {
return []ImageDiskUsage{base}, nil
}

pairs, err := ToNameTagPairs(repoTags)
if err != nil {
return nil, err
}

results := make([]ImageDiskUsage, len(pairs))
for i, pair := range pairs {
res := base
res.Repository = pair.Name
res.Tag = pair.Tag
results[i] = res
}

return results, nil
}
43 changes: 32 additions & 11 deletions libimage/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

filtersPkg "github.com/containers/common/pkg/filters"
"github.com/containers/common/pkg/timetype"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -45,15 +46,12 @@ func filterImages(images []*Image, filters []filterFunc) ([]*Image, error) {

// compileImageFilters creates `filterFunc`s for the specified filters. The
// required format is `key=value` with the following supported keys:
// after, since, before, dangling, id, label, readonly, reference, intermediate
// after, since, before, containers, dangling, id, label, readonly, reference, intermediate
func (r *Runtime) compileImageFilters(ctx context.Context, filters []string) ([]filterFunc, error) {
logrus.Tracef("Parsing image filters %s", filters)

filterFuncs := []filterFunc{}
visitedKeys := make(map[string]bool)

for _, filter := range filters {
// First, parse the filter.
var key, value string
split := strings.SplitN(filter, "=", 2)
if len(split) != 2 {
Expand All @@ -62,13 +60,6 @@ func (r *Runtime) compileImageFilters(ctx context.Context, filters []string) ([]

key = split[0]
value = split[1]

if _, exists := visitedKeys[key]; exists {
return nil, errors.Errorf("image filter %q specified multiple times", key)
}
visitedKeys[key] = true

// Second, dispatch the filters.
switch key {

case "after", "since":
Expand All @@ -85,6 +76,13 @@ func (r *Runtime) compileImageFilters(ctx context.Context, filters []string) ([]
}
filterFuncs = append(filterFuncs, filterBefore(img.Created()))

case "containers":
containers, err := strconv.ParseBool(value)
if err != nil {
return nil, errors.Wrapf(err, "non-boolean value %q for dangling filter", value)
}
filterFuncs = append(filterFuncs, filterContainers(containers))

case "dangling":
dangling, err := strconv.ParseBool(value)
if err != nil {
Expand Down Expand Up @@ -115,6 +113,18 @@ func (r *Runtime) compileImageFilters(ctx context.Context, filters []string) ([]
case "reference":
filterFuncs = append(filterFuncs, filterReference(value))

case "until":
ts, err := timetype.GetTimestamp(value, time.Now())
if err != nil {
return nil, err
}
seconds, nanoseconds, err := timetype.ParseTimestamps(ts, 0)
if err != nil {
return nil, err
}
until := time.Unix(seconds, nanoseconds)
filterFuncs = append(filterFuncs, filterBefore(until))

default:
return nil, errors.Errorf("unsupported image filter %q", key)
}
Expand Down Expand Up @@ -179,6 +189,17 @@ func filterReadOnly(value bool) filterFunc {
}
}

// filterContainers creates a container filter for matching the specified value.
func filterContainers(value bool) filterFunc {
return func(img *Image) (bool, error) {
ctrs, err := img.Containers()
if err != nil {
return false, err
}
return (len(ctrs) > 0) == value, nil
}
}

// filterDangling creates a dangling filter for matching the specified value.
func filterDangling(value bool) filterFunc {
return func(img *Image) (bool, error) {
Expand Down
Loading

0 comments on commit c53283f

Please sign in to comment.