1
0
mirror of https://github.com/projectatomic/atomic.git synced 2026-02-06 12:45:57 +01:00

Refactor images

Covers all but verify and generate.  This is a refactoring of the
images subverbs (i.e. info, version, delete, ...)

Added in a unittest for list and info.

Closes: #771
Approved by: baude
This commit is contained in:
Brent Baude
2016-11-23 15:14:14 -06:00
committed by Atomic Bot
parent a67082f3e4
commit ef984ed066
17 changed files with 494 additions and 361 deletions

View File

@@ -625,7 +625,7 @@ class Atomic(object):
as a JSON string so it can then be parsed appropriately.
"""
try:
return open(os.path.join(self.results, "scan_summary.json"), "r").read()
return json.loads(open(os.path.join(self.results, "scan_summary.json"), "r").read())
except IOError:
return "{}"

View File

@@ -2,7 +2,7 @@ from docker import errors
import Atomic.util as util
from Atomic.backends.backend import Backend
from Atomic.client import get_docker_client
from Atomic.client import AtomicDocker
from Atomic.objects.image import Image
from Atomic.objects.container import Container
from requests import exceptions
@@ -18,7 +18,7 @@ class DockerBackend(Backend):
@property
def d(self):
if not self._d:
self._d = get_docker_client()
self._d = AtomicDocker()
self._ping()
return self._d
return self._d
@@ -82,6 +82,7 @@ class DockerBackend(Backend):
img_obj.repotags = img_struct['RepoTags']
img_obj.created = img_struct['Created']
img_obj.size = img_struct['Size']
img_obj.virtual_size = img_struct['VirtualSize']
if deep:
img_obj.deep = True
@@ -240,11 +241,14 @@ class DockerBackend(Backend):
util.is_insecure_registry(self.d.info()['RegistryConfig'], util.strip_port(registry)))
def prune(self):
for iid in self._get_images(get_all=True, quiet=True, filters={"dangling": True}):
for iid in self.get_dangling_images():
self.delete_image(iid, force=True)
util.write_out("Removed dangling Image {}".format(iid))
return 0
def get_dangling_images(self):
return self._get_images(get_all=True, quiet=True, filters={"dangling": True})
def install(self, image, name):
pass
@@ -257,15 +261,15 @@ class DockerBackend(Backend):
def version(self, image):
return self.get_layers(image)
def _get_layer(self, image):
def get_layer(self, image):
return Layer(self.inspect_image(image))
def get_layers(self, image):
layers = []
layer = self._get_layer(image)
layer = self.get_layer(image)
layers.append(layer)
while layer.parent:
layer = self._get_layer(layer.parent)
layer = self.get_layer(layer.parent)
layers.append(layer)
return layers

View File

@@ -42,29 +42,29 @@ class OSTreeBackend(Backend):
def _make_image(self, image, info):
name = info['Id']
image = Image(image, backend=self, remote=False)
image.name = name
image.config = info
image.backend = self
image.id = name
image.registry = None
image.repo = None
image.image = name
image.tag = name
image.repotags = info['RepoTags']
image.created = info['Created']
image.size = None
image.original_structure = info
image.input_name = info['Id']
image.deep = True
image.labels = info['Labels']
image.version = image.get_label("Version")
image.release = image.get_label("Release")
image.digest = None
image.os = image.get_label("Os")
image.arch = image.get_label("Arch")
image.graph_driver = None
return image
img_obj = Image(image, backend=self, remote=False)
img_obj.input_name = image
img_obj.name = image
img_obj.config = info
img_obj.backend = self
img_obj.id = name
img_obj.registry = None
img_obj.repo = None
img_obj.image = name
img_obj.tag = name
img_obj.repotags = info['RepoTags']
img_obj.created = info['Created']
img_obj.size = None
img_obj.original_structure = info
img_obj.deep = True
img_obj.labels = info['Labels']
img_obj.version = img_obj.get_label("Version")
img_obj.release = img_obj.get_label("Release")
img_obj.digest = None
img_obj.os = img_obj.get_label("Os")
img_obj.arch = img_obj.get_label("Arch")
img_obj.graph_driver = None
return img_obj
def has_image(self, img):
if self.syscontainers.has_image(img):
@@ -137,3 +137,6 @@ class OSTreeBackend(Backend):
layer = self._get_layer(layer.parent)
layers.append(layer)
return layers
def get_dangling_images(self):
return []

View File

@@ -9,7 +9,7 @@ class BackendUtils(object):
BACKENDS = [DockerBackend, OSTreeBackend]
def _get_backend_from_string(self, str_backend):
def get_backend_from_string(self, str_backend):
for _backend in self.BACKENDS:
backend_obj = _backend()
if backend_obj.backend == str_backend:
@@ -27,7 +27,7 @@ class BackendUtils(object):
def backend_has_container(backend, container):
return True if backend.has_container(container) else False
def get_backend_for_image(self, img, str_preferred_backend=None):
def get_backend_and_image(self, img, str_preferred_backend=None):
"""
Given an image name (str) and optionally a str reference to a backend,
this method looks for the image firstly on the preferred backend and
@@ -36,12 +36,13 @@ class BackendUtils(object):
:param str_preferred_backend: i.e. 'docker'
:return: backend object
"""
backends = self.BACKENDS
backends = list(self.BACKENDS)
# Check preferred backend first
if str_preferred_backend:
be = self._get_backend_from_string(str_preferred_backend)
if be.has_image(img):
return be
be = self.get_backend_from_string(str_preferred_backend)
img_obj = be.has_image(img)
if img_obj:
return be, img_obj
# Didnt find in preferred, need to remove it from the list now
del backends[self._get_backend_index_from_string(str_preferred_backend)]
@@ -50,8 +51,9 @@ class BackendUtils(object):
img_in_backends = []
for backend in backends:
be = backend()
if be.has_image(img):
img_in_backends.append(be)
img_obj = be.has_image(img)
if img_obj:
img_in_backends.append((be, img_obj))
if len(img_in_backends) == 1:
return img_in_backends[0]
@@ -69,10 +71,10 @@ class BackendUtils(object):
:param str_preferred_backend: i.e. 'docker'
:return: backend object
"""
backends = self.BACKENDS
backends = list(self.BACKENDS)
# Check preferred backend first
if str_preferred_backend:
be = self._get_backend_from_string(str_preferred_backend)
be = self.get_backend_from_string(str_preferred_backend)
if be.has_container(container):
return be
# Didnt find in preferred, need to remove it from the list now
@@ -90,5 +92,19 @@ class BackendUtils(object):
raise ValueError("Found {} in multiple storage backends: {}".
format(container, ', '.join([x.backend for x in container_in_backends])))
def get_images(self, get_all=False):
backends = self.BACKENDS
img_objs = []
for backend in backends:
be = backend()
img_objs += be.get_images(get_all=get_all)
return img_objs
def get_containers(self):
backends = self.BACKENDS
con_objs = []
for backend in backends:
be = backend()
con_objs += be.get_containers()
return con_objs

View File

@@ -175,7 +175,7 @@ class Containers(Atomic):
def ps(self):
all_containers = []
vuln_ids = self.get_vulnerable_ids()
all_vuln_info = json.loads(self.get_all_vulnerable_info())
all_vuln_info = self.get_all_vulnerable_info()
# Collect the system containers
for i in self.syscontainers.get_containers():

View File

@@ -1,8 +1,7 @@
from . import Atomic
from . import util
from docker.errors import NotFound
from docker.errors import APIError
import sys
from Atomic.backendutils import BackendUtils
class Delete(Atomic):
def __init__(self):
@@ -13,34 +12,62 @@ class Delete(Atomic):
Mark given image(s) for deletion from registry
:return: 0 if all images marked for deletion, otherwise 2 on any failure
"""
if self.args.debug:
util.write_out(str(self.args))
beu = BackendUtils()
# Ensure the input values match up first
delete_objects = []
# We need to decide on new returns for dbus because we now check image
# validity prior to executing the delete. If there is going to be a
# failure, it will be here.
#
# The failure here is basically that it couldnt verify/find the image.
for image in self.args.delete_targets:
be, img_obj = beu.get_backend_and_image(image, str_preferred_backend=self.args.storage)
delete_objects.append((be, img_obj))
if self.args.remote:
return self._delete_remote(self.args.delete_targets)
max_img_name = max([len(x.input_name) for _, x in delete_objects]) + 2
if not self.args.assumeyes:
confirm = util.input("Do you wish to delete {}? (y/N) ".format(self.args.delete_targets))
util.write_out("Do you wish to delete the following images?\n")
two_col = " {0:" + str(max_img_name) + "} {1}"
util.write_out(two_col.format("IMAGE", "STORAGE"))
for del_obj in delete_objects:
be, img_obj = del_obj
util.write_out(two_col.format(img_obj.input_name, be.backend))
confirm = util.input("\nConfirm (y/N) ")
confirm = confirm.strip().lower()
if not confirm in ['y', 'yes']:
util.write_err("User aborted delete operation for {}".format(self.args.delete_targets))
sys.exit(2)
if self.args.remote:
results = self._delete_remote(self.args.delete_targets)
else:
results = self._delete_local(self.args.delete_targets, self.args.force)
return results
# Perform the delete
for del_obj in delete_objects:
be, img_obj = del_obj
be.delete_image(img_obj.input_name, force=self.args.force)
# We need to return something here for dbus
return
def prune_images(self):
"""
Remove dangling images from registry
:return: 0 if all images deleted or no dangling images found
"""
self.syscontainers.prune_ostree_images()
results = self.d.images(filters={"dangling":True}, quiet=True)
if len(results) == 0:
return 0
if self.args.debug:
util.write_out(str(self.args))
for backend in BackendUtils.BACKENDS:
be = backend()
be.prune()
for img in results:
self.d.remove_image(img, force=True)
util.write_out("Removed dangling Image {}".format(img))
return 0
def _delete_remote(self, targets):
@@ -67,44 +94,4 @@ class Delete(Atomic):
results = 2
return results
def _delete_local(self, targets, force=False):
results = 0
for target in targets:
if not self.args.storage:
if self.is_duplicate_image(target):
util.write_err("Failed to delete Image {}: has duplicate naming; please specify "
"--storage to delete from a specific storage.".format(target))
results = 2
else:
if self.syscontainers.has_image(target):
self.syscontainers.delete_image(target)
else:
try:
self.d.remove_image(target, force=force)
except NotFound as e:
util.write_err("Failed to delete Image {}: {}".format(target, e))
results = 2
except APIError as e:
util.write_err("Failed operation for delete Image {}: {}".format(target, e))
results = 2
elif self.args.storage.lower() == "ostree":
if not self.syscontainers.has_image(target):
util.write_err("Failed to delete Image {}: does not exist in ostree.".format(target))
self.syscontainers.delete_image(target)
elif self.args.storage.lower() == "docker":
try:
self.d.remove_image(target, force=force)
except NotFound as e:
util.write_err("Failed to delete Image {}: {}".format(target, e))
results = 2
except APIError as e:
util.write_err("Failed operation for delete Image {}: {}".format(target, e))
results = 2
else:
util.write_err("{} is not a valid storage".format(self.args.storage))
results = 2
return results

View File

@@ -261,7 +261,7 @@ class RegistryInspect():
self.rc = RegistryConnection(debug=self.debug)
self._setup_rc()
try:
util.write_out("Trying {}".format(self.assemble_fqdn(include_tag=True)))
self.assemble_fqdn(include_tag=True)
self.ping()
except RegistryInspectError as e:
util.write_out(str(e))

View File

@@ -6,13 +6,14 @@ from Atomic import help as Help
from Atomic.mount import Mount
from Atomic.delete import Delete
import os
import sys
import json
import math
import shutil
import tempfile
import time
import argparse
from Atomic import backendutils
ATOMIC_CONFIG = util.get_atomic_config()
storage = ATOMIC_CONFIG.get('default_storage', "docker")
def convert_size(size):
if size > 0:
@@ -48,7 +49,7 @@ def cli(subparser):
action="store_true",
help=_("Delete image from remote repository"))
delete_parser.add_argument("--storage", default="", dest="storage",
delete_parser.add_argument("--storage", default=storage, dest="storage",
help=_("Specify the storage from which to delete the image from. "
"If not specified and there are images with the same name in "
"different storages, you will be prompted to specify."))
@@ -109,6 +110,7 @@ def cli(subparser):
class Images(Atomic):
def __init__(self):
super(Images, self).__init__()
self.be_utils = backendutils.BackendUtils()
def display_all_image_info(self):
def get_col_lengths(_images):
@@ -118,106 +120,80 @@ class Images(Atomic):
:return: a set with len of repository and tag
If there are no images, return 1, 1
'''
repo_tags = [[i["repo"], i["tag"]] for i in _images]
repo_tags = [y for x in _images if x.repotags for y in x.split_repotags]
if repo_tags:
return max([len(x[0]) for x in repo_tags]) + 2,\
max([len(x[1]) for x in repo_tags]) + 2
else:
return 1, 1
_images = self.images()
if self.args.debug:
util.write_out(str(self.args))
_images = self._get_images()
if self.args.json:
json.dump(_images, sys.stdout)
util.output_json(self.return_json(_images))
return 0
if len(_images) == 0:
return
if len(_images) >= 0:
_max_repo, _max_tag = get_col_lengths(_images)
if self.args.truncate:
_max_id = 14
_max_repo, _max_tag = get_col_lengths(_images)
if self.args.truncate:
_max_id = 14
else:
_max_id = 65
col_out = "{0:2} {1:" + str(_max_repo) + "} {2:" + str(_max_tag) + \
"} {3:" + str(_max_id) + "} {4:18} {5:14} {6:10}"
if self.args.heading and not self.args.quiet:
util.write_out(col_out.format(" ",
"REPOSITORY",
"TAG",
"IMAGE ID",
"CREATED",
"VIRTUAL SIZE",
"TYPE"))
for image in _images:
if self.args.filter:
if not self._filter_include_image(image):
continue
if self.args.quiet:
util.write_out(image.id)
else:
_max_id = 65
indicator = ""
if image.is_dangling:
indicator += "*"
elif image.used:
indicator += ">"
if image.vulnerable:
space = " " if len(indicator) < 1 else ""
if util.is_python2:
indicator = indicator + self.skull + space
else:
indicator = indicator + str(self.skull, "utf-8") + space
repo, tag = image.split_repotags[0]
_id = image.short_id if self.args.truncate else image.id
util.write_out(col_out.format(indicator, repo or "<none>", tag or "<none>", _id, image.timestamp,
image.virtual_size, image.backend.backend))
util.write_out("")
return
col_out = "{0:2} {1:" + str(_max_repo) + "} {2:" + str(_max_tag) + \
"} {3:" + str(_max_id) + "} {4:18} {5:14} {6:10}"
def _get_images(self):
_images = self.be_utils.get_images(get_all=self.args.all)
if self.args.heading and not self.args.quiet:
util.write_out(col_out.format(" ",
"REPOSITORY",
"TAG",
"IMAGE ID",
"CREATED",
"VIRTUAL SIZE",
"TYPE"))
self._mark_used(_images)
self._mark_vulnerable(_images)
for image in _images:
if self.args.filter:
image_info = {"repo" : image['repo'], "tag" : image['tag'], "id" : image['id'],
"created" : image['created'], "size" : image['virtual_size'], "type" : image['type'],
"dangling": "{}".format(image['is_dangling'])}
if not self._filter_include_image(image_info):
continue
if self.args.quiet:
util.write_out(image['id'])
else:
indicator = ""
if image["is_dangling"]:
indicator += "*"
elif image["used_image"]:
indicator += ">"
if image["vulnerable"]:
space = " " if len(indicator) < 1 else ""
if util.is_python2:
indicator = indicator + self.skull + space
else:
indicator = indicator + str(self.skull, "utf-8") + space
util.write_out(col_out.format(indicator, image['repo'], image['tag'], image['id'], image['created'], image['virtual_size'], image['type']))
util.write_out("")
return
return _images
def images(self):
_images = self.get_images(get_all=self.args.all)
all_image_info = []
if len(_images) >= 0:
vuln_ids = self.get_vulnerable_ids()
all_vuln_info = json.loads(self.get_all_vulnerable_info())
used_image_ids = [x['ImageID'] for x in self.get_containers()]
for image in _images:
image_dict = dict()
if not image["RepoTags"]:
continue
if ':' in image["RepoTags"][0]:
repo, tag = image["RepoTags"][0].rsplit(":", 1)
else:
repo, tag = image["RepoTags"][0], ""
if "Created" in image:
created = time.strftime("%F %H:%M", time.localtime(image["Created"]))
else:
created = ""
if "VirtualSize" in image:
virtual_size = convert_size(image["VirtualSize"])
else:
virtual_size = ""
return self.return_json(self._get_images())
image_dict["is_dangling"] = self.is_dangling(repo)
image_dict["used_image"] = image["Id"] in used_image_ids
image_dict["vulnerable"] = image["Id"] in vuln_ids
image_id = image["Id"][:12] if self.args.truncate else image["Id"]
image_type = image['ImageType']
image_dict["repo"] = repo
image_dict["tag"] = tag
image_dict["id"] = image_id
image_dict["created"] = created
image_dict["virtual_size"] = virtual_size
image_dict["type"] = image_type
image_dict["image_id"] = image["ImageId"]
if image_dict["vulnerable"]:
image_dict["vuln_info"] = all_vuln_info[image["Id"]]
else:
image_dict["vuln_info"] = dict()
all_image_info.append(image_dict)
return all_image_info
def generate_validation_manifest(self):
"""
@@ -254,10 +230,13 @@ class Images(Atomic):
f.write(r.stdout)
shutil.rmtree(tmpdir)
def _filter_include_image(self, image_info):
def _filter_include_image(self, image_obj):
filterables = ["repo", "tag", "id", "created", "size", "type", "dangling"]
for i in self.args.filter:
var, value = str(i).split("=")
try:
var, value = str(i).split("=")
except ValueError:
raise ValueError("The filter {} is not formatted correctly. It should be VAR=VALUE".format(i))
var = var.lower()
if var == "repository":
var = "repo"
@@ -267,8 +246,39 @@ class Images(Atomic):
if var not in filterables: # Default to allowing all images through for non-existing filterable
continue
if value not in image_info[var].lower():
if getattr(image_obj, var, None) != value:
return False
return True
def _mark_used(self, images):
assert isinstance(images, list)
all_containers = [x.id for x in self.be_utils.get_containers()]
for image in images:
if image.id in all_containers:
image.used = True
def _mark_vulnerable(self, images):
assert isinstance(images, list)
vulnerable_uuids = self.get_vulnerable_ids()
for image in images:
if image.id in vulnerable_uuids:
image.vulnerable = True
def return_json(self, images):
all_image_info = []
all_vuln_info = self.get_all_vulnerable_info()
keys = ['is_dangling', 'used', 'vulnerable', 'id', 'type', 'created', 'virtual_size']
for img_obj in images:
if not img_obj.repotags:
continue
img_dict = dict()
img_dict['repo'], img_dict['tag'] = img_obj.split_repotags[0]
for key in keys:
img_dict[key] = getattr(img_obj, key, None)
img_dict['vuln_info'] = \
dict() if not img_obj.vulnerable else all_vuln_info.get(img_obj.id, None) # pylint: disable=no-member
all_image_info.append(img_dict)
return all_image_info

View File

@@ -6,9 +6,18 @@ try:
except ImportError:
from atomic import Atomic # pylint: disable=relative-import
from .atomic import AtomicError
from docker.errors import NotFound
import requests.exceptions
from Atomic.util import get_atomic_config
from Atomic.backendutils import BackendUtils
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from contextlib import closing
from Atomic.discovery import RegistryInspectError
ATOMIC_CONFIG = get_atomic_config()
storage = ATOMIC_CONFIG.get('default_storage', "docker")
def cli(subparser, hidden=False):
# atomic info
@@ -25,7 +34,7 @@ def cli(subparser, hidden=False):
infop.add_argument("--remote", dest="force",
action='store_true', default=False,
help=_('ignore local images and only scan registries'))
infop.add_argument("--storage", default="", dest="storage",
infop.add_argument("--storage", default=storage, dest="storage",
help=_("Specify the storage of the image. "
"If not specified and there are images with the same name in "
"different storages, you will be prompted to specify."))
@@ -44,183 +53,81 @@ def cli_version(subparser, hidden=False):
versionp.add_argument("-r", "--recurse", default=False, dest="recurse",
action="store_true",
help=_("recurse through all layers"))
versionp.add_argument("--storage", default="", dest="storage",
versionp.add_argument("--storage", default=storage, dest="storage",
help=_("Specify the storage of the image. "
"If not specified and there are images with the same name in "
"different storages, you will be prompted to specify."))
versionp.set_defaults(_class=Info, func='print_version')
versionp.set_defaults(_class=Info, func='version')
versionp.add_argument("image", help=_("container image"))
class Info(Atomic):
def __init__(self):
super(Info, self).__init__()
self.beu = BackendUtils()
def version(self):
if not self.args.storage:
if self.is_duplicate_image(self.image):
raise ValueError("Found more than one Image with name {}; "
"please specify with --storage.".format(self.image))
else:
if self.syscontainers.has_image(self.image):
return self.syscontainers.version(self.image)
try:
self.d.inspect_image(self.image)
except (NotFound, requests.exceptions.ConnectionError):
self._no_such_image()
layer_objects = self.get_layer_objects()
max_version_len = max([len(x.long_version) for x in layer_objects])
max_version_len = max_version_len if max_version_len > 9 else 9
max_img_len = len(max([y for x in layer_objects for y in x.repotags], key=len)) + 9
max_img_len = max_img_len if max_img_len > 12 else 12
col_out = "{0:" + str(max_img_len) + "} {1:" + str(max_version_len) + "} {2:10}"
util.write_out(col_out.format("IMAGE NAME", "VERSION", "IMAGE ID"))
for layer in layer_objects:
for int_img_name in range(len(layer.repotags)):
version = layer.long_version if int_img_name < 1 else ""
iid = layer.id[:12] if int_img_name < 1 else ""
space = "" if int_img_name < 1 else " Tag: "
util.write_out(col_out.format(space + layer.repotags[int_img_name], version, iid))
elif self.args.storage.lower() == "ostree":
if self.syscontainers.has_image(self.image):
return self.syscontainers.version(self.image)
self._no_such_image()
def get_layer_objects(self):
_, img_obj = self.beu.get_backend_and_image(self.image, str_preferred_backend=self.args.storage)
return img_obj.layers
elif self.args.storage.lower() == "docker":
try:
self.d.inspect_image(self.image)
except (NotFound, requests.exceptions.ConnectionError):
self._no_such_image()
else:
raise ValueError("{} is not a valid storage".format(self.args.storage))
if self.args.recurse:
return self.get_layers()
else:
return [self._get_layer(self.image)]
def get_version(self):
def dbus_version(self):
layer_objects = self.get_layer_objects()
versions = []
for layer in self.version():
version = "None"
if "Version" in layer and layer["Version"] != '':
version = layer["Version"]
versions.append({"Image": layer['RepoTags'], "Version": version, "iid": layer['Id']})
return versions
for layer in layer_objects:
versions.append({"Image": layer.repotags, "Version": layer.long_version, "iid": layer.id})
def info_tty(self):
if self.args.debug:
util.write_out(str(self.args))
util.write_out(self.info())
def info(self):
"""
Retrieve and print all LABEL information for a given image.
"""
buf = ""
def _no_label():
return ""
if not self.args.storage:
if self.is_duplicate_image(self.image):
raise ValueError("Found more than one Image with name {}; "
"please specify with --storage.".format(self.image))
if self.syscontainers.has_image(self.image):
if not self.args.force:
buf += ("Image Name: {}".format(self.image))
manifest = self.syscontainers.inspect_system_image(self.image)
labels = manifest["Labels"]
for label in labels:
buf += ('\n{0}: {1}'.format(label, labels[label]))
template_variables, template_variables_to_set = self.syscontainers.get_template_variables(self.image)
buf += ("\n\nEnvironment variables with default value, but overridable with --set:")
for variable in template_variables.keys():
buf += ('\n{}: {}'.format(variable, template_variables.get(variable)))
if template_variables_to_set:
buf += ("\n\nEnvironment variables that has no default value, and must be set with --set:")
for variable in template_variables_to_set.keys():
buf += ('\n{}: {}'.format(variable, template_variables_to_set.get(variable)))
return buf
# Check if the input is an image id associated with more than one
# repotag. If so, error out.
else:
try:
iid = self._is_image(self.image)
self.image = self.get_fq_name(self._inspect_image(iid))
except AtomicError:
if self.args.force:
self.image = util.find_remote_image(self.d, self.image)
if self.image is None:
self._no_such_image()
elif self.args.storage.lower() == "ostree":
if self.syscontainers.has_image(self.image):
if not self.args.force:
buf += ("Image Name: {}".format(self.image))
manifest = self.syscontainers.inspect_system_image(self.image)
labels = manifest["Labels"]
for label in labels:
buf += ('\n{0}: {1}'.format(label, labels[label]))
template_variables, template_variables_to_set = self.syscontainers.get_template_variables(self.image)
buf += ("\n\nEnvironment variables with default value, but overridable with --set:")
for variable in template_variables.keys():
buf += ('\n{}: {}'.format(variable, template_variables.get(variable)))
if template_variables_to_set:
buf += ("\n\nEnvironment variables that has no default value, and must be set with --set:")
for variable in template_variables_to_set.keys():
buf += ('\n{}: {}'.format(variable, template_variables_to_set.get(variable)))
return buf
else:
self._no_such_image()
elif self.args.storage.lower() == "docker":
if self.is_iid():
self.get_fq_name(self._inspect_image())
# The input is not an image id
else:
try:
iid = self._is_image(self.image)
self.image = self.get_fq_name(self._inspect_image(iid))
except AtomicError:
if self.args.force:
self.image = util.find_remote_image(self.d, self.image)
if self.image is None:
self._no_such_image()
if self.args.storage == 'ostree' and self.args.force:
# Ostree and remote combos are illegal
raise ValueError("The --remote option cannot be used with the 'ostree' storage option.")
if self.args.force:
# The user wants information on a remote image
be = self.beu.get_backend_from_string(self.args.storage)
img_obj = be.make_remote_image(self.image)
else:
raise ValueError("{} is not a valid storage".format(self.args.storage))
# The image is local
be, img_obj = self.beu.get_backend_and_image(self.image, str_preferred_backend=self.args.storage)
buf += ("Image Name: {}".format(self.image))
inspection = None
if not self.args.force:
inspection = self._inspect_image(self.image)
# No such image locally, but fall back to remote
if inspection is None:
# Shut up pylint in case we're on a machine with upstream
# docker-py, which lacks the remote keyword arg.
# pylint: disable=unexpected-keyword-arg
inspection = util.skopeo_inspect("docker://" + self.image)
# image does not exist on any configured registry
if 'Config' in inspection and 'Labels' in inspection['Config']:
labels = inspection['Config']['Labels']
elif 'Labels' in inspection:
labels = inspection['Labels']
else:
_no_label()
with closing(StringIO()) as buf:
try:
info_name = img_obj.fq_name
except RegistryInspectError:
info_name = img_obj.input_name
buf.write("Image Name: {}\n".format(info_name))
buf.writelines(sorted(["{}: {}\n".format(k, v) for k,v in list(img_obj.labels.items())]))
if img_obj.template_variables_set:
buf.write("\n\nTemplate variables with default value, but overridable with --set:\n")
buf.writelines(["{}: {}\n".format(k, v) for k,v in
list(sorted(img_obj.template_variables_set.items()))])
if img_obj.template_variables_unset:
buf.write("\n\nTemplate variables that has no default value, and must be set with --set:\n")
buf.writelines(["{}: {}\n".format(k, v) for k,v in
list(sorted(img_obj.template_variables_unset.items()))])
return buf.getvalue()
if labels is not None and len(labels) is not 0:
for label in labels:
buf += ('\n{0}: {1}'.format(label, labels[label]))
else:
_no_label()
return buf
def print_version(self):
versions = self.get_version()
max_version_len = len(max([x['Version'] for x in versions], key=len)) + 2
max_version_len = max_version_len if max_version_len > 9 else 9
max_img_len = len(max([y for x in versions for y in x['Image']], key=len)) + 9
max_img_len = max_img_len if max_img_len > 12 else 12
col_out = "{0:" + str(max_img_len) + "} {1:" + str(max_version_len) + "} {2:10}"
util.write_out("")
util.write_out(col_out.format("IMAGE NAME", "VERSION", "IMAGE ID"))
for layer in versions:
for int_img_name in range(len(layer['Image'])):
version = layer['Version'] if int_img_name < 1 else ""
iid = layer['iid'][:12] if int_img_name < 1 else ""
space = "" if int_img_name < 1 else " Tag: "
util.write_out(col_out.format(space + layer['Image'][int_img_name], version, iid))
util.write_out("")

View File

@@ -26,7 +26,7 @@ class Container(object):
def _setup_common(self):
# Items common to backends can go here.
print("")
pass
def dump(self):
# Helper function to dump out known variables in pretty-print style

View File

@@ -1,5 +1,8 @@
from Atomic.util import Decompose, output_json
from Atomic.discovery import RegistryInspect
from Atomic.objects.layer import Layer
import math
import time
class Image(object):
@@ -20,6 +23,7 @@ class Image(object):
self._backend = backend
self.input_name = input_name
self.deep = False
self._virtual_size = None
# Deeper
self.version = None
@@ -32,8 +36,16 @@ class Image(object):
self.graph_driver = None
self.config = {}
self._fq_name = None
self._used = False
self._vulnerable = False
self._template_variables_set = None
self._template_variables_unset = None
self._instantiate()
def __gt__(self, other):
"""
Custom greater than comparison between image objects. This allows you to
@@ -85,6 +97,10 @@ class Image(object):
if self._fq_name:
return self._fq_name
if self.backend.backend == 'ostree':
self._fq_name = self.input_name
return self._fq_name
if self.fully_qualified:
img = self.registry
if self.repo:
@@ -95,7 +111,6 @@ class Image(object):
return img
if not self.registry:
print(self.image)
ri = RegistryInspect(registry=self.registry, repo=self.repo, image=self.image,
tag=self.tag, orig_input=self.input_name)
self._fq_name = ri.find_image_on_registry()
@@ -149,4 +164,108 @@ class Image(object):
_version += "-{}".format(self.version)
if self.release:
_version += "-{}".format(self.release)
return _version
return _version
@property
def is_dangling(self):
if self.id in self.backend.get_dangling_images():
return True
return False
@property
def virtual_size(self):
size = self._virtual_size or self.size
if size:
return convert_size(self._virtual_size)
return ""
@virtual_size.setter
def virtual_size(self, value):
self._virtual_size = value
@property
def split_repotags(self):
_repotags = []
if not self.repotags:
return [('<none>', '<none')]
for _repotag in self.repotags:
if ':' in _repotag:
repo, tag = _repotag.rsplit(':', 1)
else:
repo = tag = ""
_repotags.append((repo, tag))
return _repotags
@property
def used(self):
return self._used
@used.setter
def used(self, value):
assert isinstance(value, bool)
self._used = value
@property
def vulnerable(self):
return self._vulnerable
@vulnerable.setter
def vulnerable(self, value):
assert isinstance(value, bool)
self._vulnerable = value
@property
def short_id(self):
return self.id[:12]
@property
def timestamp(self):
return time.strftime("%F %H:%M", time.localtime(self.created))
@property
def type(self):
return self.backend.backend
def _get_template_info(self):
self._template_variables_set, self._template_variables_unset = self.backend.syscontainers.\
get_template_variables(self.image)
@property
def template_variables_set(self):
if self.backend.backend != 'ostree':
return self._template_variables_set
if not self._template_variables_set and not self.template_variables_unset:
self._get_template_info()
return self._template_variables_set
@property
def template_variables_unset(self):
if self.backend.backend != ' ostree':
return self._template_variables_unset
if not self._template_variables_set and not self.template_variables_unset:
self._get_template_info()
return self._template_variables_unset
@property
def layers(self):
layer_objects = []
# Create the first layer
layer = Layer(self)
layer_objects.append(layer)
while layer.parent:
layer = self.backend.get_layer(layer.parent)
layer_objects.append(layer)
return layer_objects
def convert_size(size):
if size > 0:
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size, 1000)))
p = math.pow(1000, i)
s = round(size/p, 2) # pylint: disable=round-builtin,old-division
if s > 0:
return '%s %s' % (s, size_name[i])
return '0B'

View File

@@ -822,3 +822,4 @@ class Decompose(object):
@property
def all(self):
return self._registry, self._repo, self._image, self._tag, self._digest

View File

@@ -216,7 +216,7 @@ class atomic_dbus(slip.dbus.service.Object):
args = self.Args()
args.all=True
images.set_args(args)
i = images.images()
i = images.display_all_image_info()
return json.dumps(i)
# atomic containers section
@@ -566,7 +566,7 @@ class atomic_dbus(slip.dbus.service.Object):
args.image = image
args.recurse = recurse
info.set_args(args)
return json.dumps(info.get_version())
return json.dumps(info.dbus_version())
if __name__ == "__main__":
mainloop = GObject.MainLoop()

View File

@@ -97,7 +97,7 @@ __atomic_image_repos_and_tags() {
}
__atomic_system_containers_images() {
local images="$(atomic images list --no-trunc -f type=system |
local images="$(atomic images list --no-trunc -f type=ostree|
awk 'NR>1 { if ($1 == ">" || $1 == "*") print $2; else print $1; }')"
COMPREPLY+=( $(compgen -W "$images" -- "$cur") )
}

View File

@@ -12,7 +12,7 @@ setup(
version=_Atomic.__version__,
author=_Atomic.__author__,
author_email=_Atomic.__author_email__,
packages=["Atomic"],
packages=["Atomic", "Atomic/backends", "Atomic/objects"],
data_files=[('/etc/dbus-1/system.d/', ["org.atomic.conf"]),
('/usr/share/dbus-1/system-services', ["org.atomic.service"]),
('/usr/share/polkit-1/actions/', ["org.atomic.policy"]),

View File

@@ -252,7 +252,7 @@ ostree --repo=${ATOMIC_OSTREE_REPO} refs > refs
assert_matches busybox refs
${ATOMIC} --assumeyes images delete -f --storage ostree docker.io/busybox
BUSYBOX_IMAGE_ID=$(${ATOMIC} images list -f type=system | grep busybox | awk '{print $3}')
BUSYBOX_IMAGE_ID=$(${ATOMIC} images list -f type=ostree | grep busybox | awk '{print $3}')
${ATOMIC} --assumeyes images delete -f ${BUSYBOX_IMAGE_ID}
ostree --repo=${ATOMIC_OSTREE_REPO} refs > refs
@@ -270,8 +270,8 @@ image_digest=$(ostree --repo=${ATOMIC_OSTREE_REPO} show --print-metadata-key=doc
${ATOMIC} images list > images.out
grep "busybox.*$image_digest" images.out
${ATOMIC} images list -f type=system > images.out
${ATOMIC} images list -f type=system --all > images.all.out
${ATOMIC} images list -f type=ostree > images.out
${ATOMIC} images list -f type=ostree --all > images.all.out
test $(wc -l < images.out) -lt $(wc -l < images.all.out)
assert_matches '<none>' images.all.out
assert_not_matches '<none>' images.out
@@ -280,19 +280,15 @@ ${ATOMIC} --assumeyes images delete -f --storage ostree busybox
${ATOMIC} images prune
# Test there are still intermediate layers left after prune
${ATOMIC} images list -f type=system --all > images.all.out
${ATOMIC} images list -f type=ostree --all > images.all.out
assert_matches "<none>" images.all.out
# Check to see if deleting a duplicate image will error
OUTPUT=$(! ${ATOMIC} --assumeyes images delete -f atomic-test-system 2>&1)
grep "Failed to delete Image atomic-test-system: has duplicate naming" <<< $OUTPUT
# Now delete from ostree
${ATOMIC} --assumeyes images delete --storage ostree atomic-test-system
${ATOMIC} images prune
# Test there are not intermediate layers left layers now
${ATOMIC} images list -f type=system --all > images.all.out
${ATOMIC} images list -f type=ostree --all > images.all.out
assert_not_matches "<none>" images.all.out
# Verify there are no branches left in the repository as well

90
tests/unit/test_images.py Normal file
View File

@@ -0,0 +1,90 @@
#pylint: skip-file
import unittest
from Atomic.backendutils import BackendUtils
from Atomic.backends._docker import DockerBackend
from Atomic.backends._ostree import OSTreeBackend
from Atomic.info import Info
from Atomic.images import Images
no_mock = True
try:
from unittest.mock import MagicMock, patch
no_mock = False
except ImportError:
try:
from mock import MagicMock, patch
no_mock = False
except ImportError:
# Mock is already set to False
pass
_centos_inspect_image = {u'Comment': u'', u'Container': u'58aeaa4866c2845b48ab998b7cba3856a9fb64a681f92544cb035b85066b5102', u'DockerVersion': u'1.12.1', u'Parent': u'', u'Created': u'2016-11-02T19:52:09.463959047Z', u'Config': {u'Tty': False, u'Cmd': [u'/bin/bash'], u'Volumes': None, u'Domainname': u'', u'WorkingDir': u'', u'Image': u'5a2725191d75eb64e9b7c969cd23d8c67c6e8af9979e521a417bbfa34434fb83', u'Hostname': u'd6dcf178f680', u'StdinOnce': False, u'Labels': {u'build-date': u'20161102', u'vendor': u'CentOS', u'name': u'CentOS Base Image', u'license': u'GPLv2'}, u'AttachStdin': False, u'User': u'', u'Env': [u'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'], u'Entrypoint': None, u'OnBuild': None, u'AttachStderr': False, u'AttachStdout': False, u'OpenStdin': False}, u'Author': u'https://github.com/CentOS/sig-cloud-instance-images', u'GraphDriver': {u'Data': {u'DeviceName': u'docker-253:1-20984667-e3af0c61256f885331fb1a3adc27ea509a10ba9a0ba9175c1a149f81bddcd30d', u'DeviceSize': u'10737418240', u'DeviceId': u'2'}, u'Name': u'devicemapper'}, u'VirtualSize': 196509652, u'Os': u'linux', u'Architecture': u'amd64', u'ContainerConfig': {u'Tty': False, u'Cmd': [u'/bin/sh', u'-c', u'#(nop) ', u'CMD ["/bin/bash"]'], u'Volumes': None, u'Domainname': u'', u'WorkingDir': u'', u'Image': u'5a2725191d75eb64e9b7c969cd23d8c67c6e8af9979e521a417bbfa34434fb83', u'Hostname': u'd6dcf178f680', u'StdinOnce': False, u'Labels': {u'build-date': u'20161102', u'vendor': u'CentOS', u'name': u'CentOS Base Image', u'license': u'GPLv2'}, u'AttachStdin': False, u'User': u'', u'Env': [u'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'], u'Entrypoint': None, u'OnBuild': None, u'AttachStderr': False, u'AttachStdout': False, u'OpenStdin': False}, u'Size': 196509652, u'RepoDigests': [u'docker.io/centos@b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c'], u'Id': u'0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a', u'RepoTags': [u'docker.io/centos:latest']}
_docker_centos_result = 'Image Name: docker.io/library/centos:latest\nbuild-date: 20161102\nlicense: GPLv2\nname: CentOS Base Image\nvendor: CentOS\n'
_ostree_centos_result = 'Image Name: docker.io/library/centos:latest\nbuild-date: 20161102\nlicense: GPLv2\nname: CentOS Base Image\nvendor: CentOS\n\n\nTemplate variables with default value, but overridable with --set:\nRUN_DIRECTORY: {SET_BY_OS}\nSTATE_DIRECTORY: {SET_BY_OS}\n'
_centos_ostree_inspect = {'Version': 'centos', 'Labels': {u'build-date': u'20161102', u'vendor': u'CentOS', u'name': u'CentOS Base Image', u'license': u'GPLv2'}, 'Names': [], 'Created': 1480352808, 'OSTree-rev': 'd2122127d30f94ae12ebe5afa542abdb1870201b0b9750bae3ceb74aa6ed18e6', 'RepoTags': ['centos'], 'Id': u'b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c', 'ImageType': 'system', 'ImageId': u'b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c'}
@unittest.skipIf(no_mock, "Mock not found")
class TestInfo(unittest.TestCase):
class Args():
def __init__(self):
self.storage = None
self.force = False
self.json = False
def test_docker_info(self):
db = DockerBackend()
db._inspect_image = MagicMock(return_value=_centos_inspect_image)
img_obj = db.inspect_image('docker.io/library/centos:latest')
info = Info()
args = self.Args()
args.storage = 'docker'
info.set_args(args)
info.beu.get_backend_and_image = MagicMock(return_value=(db, img_obj))
result = info.info()
self.assertEqual(result, _docker_centos_result)
def test_ostree_info(self):
ob = OSTreeBackend()
ob.syscontainers.inspect_system_image = MagicMock(return_value=_centos_ostree_inspect)
img_obj = ob.inspect_image('docker.io/library/centos:latest')
img_obj._template_variables_set = {'RUN_DIRECTORY': '{SET_BY_OS}', 'STATE_DIRECTORY': '{SET_BY_OS}'}
img_obj._template_variables_unset = {}
info = Info()
args = self.Args()
args.storage = 'ostree'
info.set_args(args)
info.beu.get_backend_and_image = MagicMock(return_value=(ob, img_obj))
result = info.info()
self.assertEqual(result, _ostree_centos_result)
_docker_images = [{'VirtualSize': 196509652, 'Labels': {'vendor': 'CentOS', 'license': 'GPLv2', 'build-date': '20161102', 'name': 'CentOS Base Image'}, 'RepoTags': ['docker.io/centos:latest'], 'ParentId': '', 'Id': '0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a', 'Size': 196509652, 'Created': 1478116329, 'RepoDigests': ['docker.io/centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c']}, {'VirtualSize': 1093484, 'Labels': {}, 'RepoTags': ['docker.io/busybox:latest'], 'ParentId': '', 'Id': 'e02e811dd08fd49e7f6032625495118e63f597eb150403d02e3238af1df240ba', 'Size': 1093484, 'Created': 1475874238, 'RepoDigests': ['docker.io/busybox@sha256:29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912']}]
_system_images = [{'Labels': {'vendor': 'CentOS', 'license': 'GPLv2', 'build-date': '20161102', 'name': 'CentOS Base Image'}, 'ImageId': 'b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c', 'Version': 'centos:latest', 'OSTree-rev': 'd2122127d30f94ae12ebe5afa542abdb1870201b0b9750bae3ceb74aa6ed18e6', 'ImageType': 'system', 'Id': 'b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c', 'Created': 1480352808, 'Names': [], 'RepoTags': ['centos:latest']}, {'Labels': {}, 'ImageId': '29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912', 'Version': 'busybox:latest', 'OSTree-rev': 'f0cbd09116e348782fc353f99db2b111a59fdf929e9a0180f3a8450c145ed8bc', 'ImageType': 'system', 'Id': '29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912', 'Created': 1480348080, 'Names': [], 'RepoTags': ['busybox:latest']}]
@unittest.skipIf(no_mock, "Mock not found")
class TestImages(unittest.TestCase):
class Args():
def __init__(self):
self.storage = None
self.force = False
self.json = False
self.debug = False
self.name = None
self.image = None
self.all = False
def test_images(self):
db = DockerBackend()
db._inspect_image = MagicMock(return_value=_docker_images)
ob = OSTreeBackend()
ob.syscontainers.get_system_images = MagicMock(return_value=_system_images)
images = Images()
args = self.Args()
args.storage = 'docker'
args.json = True
images.set_args(args)
return_value = images.display_all_image_info()
self.assertEqual(return_value, 0)