Skip to content

Commit e16a468

Browse files
authored
Use Skopeo to pull container images
This commit enables the use of Skopeo to pull container images. Skopeo uses the OCI schema version 2 to fetch container images. It lays out the images on disk differently than Docker after a pull. In order to enable analysis of containers pulled in this way, this PR introduces the OCIImage class which reflects the expected layout. In order to deal with the different expected directory structures, we move a commonly used function in rootfs.py, get_untar_dir(), to the ImageLayer class, and creates a new property called 'image_layout'. In this way, container image layouts on disk can be dealt with based on derived Image classes. We also introduce the OCIImage class and changes to the DockerImage class which makes use of the new ImageLayer property and method. We replace all instances of get_untar_dir() with the ImageLayer instance's get_untar_dir() method. We connect all the pieces from the command line option to the image extraction method. Finally, we add Skopeo to the list of requirements for Tern in the documentation, Dockerfiles and the development environments. Lastly, we deal with the different image dictionary layouts based on the image layout in the html report specifically. Fixes #948 Signed-off-by: Nisha K <[email protected]>
2 parents fad5fb1 + 3bdbd08 commit e16a468

27 files changed

+452
-112
lines changed

README.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ Tern gives you a deeper understanding of your container's bill of materials so y
5656

5757
![Tern quick demo](/docs/img/tern_demo_fast.gif)
5858

59-
6059
# Getting Started<a name="getting-started"/>
6160

6261
## GitHub Action<a name="github-action"/>
@@ -70,13 +69,16 @@ If you have a Linux OS you will need a distro with a kernel version >= 4.0 (Ubun
7069
- Python 3.6 or newer (sudo apt-get install python3.6(3.7) or sudo dnf install python36(37))
7170
- Pip (sudo apt-get install python3-pip).
7271
- jq (sudo apt-get install jq or sudo dnf install jq)
72+
- skopeo (See [here](https://github.com/containers/skopeo/blob/main/install.md) for installation instructions or building from source)
7373

74-
Some distro versions have all of these except `attr` and/or `jq` preinstalled but both are common utilities and are available via the package manager.
74+
Some distro versions have all of these except `attr`, `jq`, and/or `skopeo` preinstalled. `attr` and `jq` are common utilities and are available via the package manager. `skopeo` has only recently been packaged for common Linux distros. If you don't see your distro in the list, your best bet is building from source, which is reasonably straightforward if you have Go installed.
7575

76-
For Docker containers
76+
For analyzing Dockerfiles and to use the "lock" function
7777
- Docker CE (Installation instructions can be found here: https://docs.docker.com/engine/installation/#server)
7878

79-
Make sure the docker daemon is running.
79+
*NOTE:* We do not provide advice on the usage of [Docker Desktop](https://www.docker.com/blog/updating-product-subscriptions/)
80+
81+
Once installed, make sure the docker daemon is running.
8082

8183
Create a python3 virtual environment:
8284
```
@@ -103,7 +105,7 @@ $ tern report -o output.txt -i debian:buster
103105
```
104106

105107
## Getting Started with Docker<a name="getting-started-with-docker">
106-
Docker is the most widely used tool to build and run containers. If you already have Docker installed, you can run Tern by building a container with the Dockerfile provided and the `docker_run.sh` script:
108+
Docker is the most widely used tool to build and run containers. If you already have Docker installed, you can run Tern by building a container with the Dockerfile provided.
107109

108110
Clone this repository:
109111
```
@@ -132,7 +134,13 @@ $ docker build -f ci/Dockerfile -t ternd .
132134
+ENTRYPOINT ["tern", "-q"]
133135
```
134136

135-
Run the script `docker_run.sh`. You may need to use sudo. In the below command `debian` is the docker hub container image name and `buster` is the tag that identifies the version we are interested in analyzing.
137+
Run the ternd container image
138+
139+
```
140+
$ docker run --rm ternd report -i debian:buster
141+
```
142+
143+
If you are using this container to analyze Dockerfiles and to use the "lock" feature, then you must volume mount the docker socket. We have a convenience script which will do that for you.
136144

137145
```
138146
$ ./docker_run.sh ternd "report -i debian:buster" > output.txt
@@ -143,15 +151,16 @@ To produce a json report run
143151
$ ./docker_run.sh ternd "report -f json -i debian:buster"
144152
```
145153

146-
What the `docker_run.sh` script does is run the built container.
147-
148154
Tern is not distributed as Docker images yet. This is coming soon. Watch the [Project Status](#project-status) for updates.
149155

150156
**WARNING**: If using the `--driver fuse` or `--driver overlay2` storage driver options, then the docker image needs to run as privileged.
157+
151158
```
152-
docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock ternd "--driver fuse report -i debian:buster"
159+
docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock ternd --driver fuse report -i debian:buster
153160
```
154161

162+
You can make this change to the `docker_run.sh` script to make it easier.
163+
155164
## Getting Started with Vagrant<a name="getting-started-with-vagrant">
156165
Vagrant is a tool to setup an isolated virtual software development environment. If you are using Windows or Mac OSes and want to run Tern from the command line (not in a Docker container) this is the best way to get started as Tern does not run natively in a Mac OS or Windows environment at this time.
157166

ci/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ RUN echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.lis
2424
fuse3/bullseye \
2525
git \
2626
jq \
27+
skopeo \
2728
&& rm -rf /var/lib/apt/lists/*
2829

2930
COPY --from=builder /install /usr/local

ci/test_files_touched.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright (c) 2019-2020 VMware, Inc. All Rights Reserved.
3+
# Copyright (c) 2019-2021 VMware, Inc. All Rights Reserved.
44
# SPDX-License-Identifier: BSD-2-Clause
55

66
from git import Repo
@@ -48,8 +48,12 @@
4848
# tern/classes
4949
re.compile('tern/classes/command.py'):
5050
['python tests/test_class_command.py'],
51+
re.compile('tern/classes/oci_image.py'):
52+
['tern report -i photon:3.0',
53+
'python tests/test_class_oci_image.py'],
5154
re.compile('tern/classes/docker_image.py'):
52-
['tern report -i photon:3.0'],
55+
['tern report -d samples/alpine_python/Dockerfile',
56+
'python tests/test_class_docker_image.py'],
5357
re.compile('tern/classes/file_data.py'):
5458
['python tests/test_class_file_data.py'],
5559
re.compile('tern/classes/image.py'):
@@ -121,6 +125,8 @@
121125
['python tests/test_analyze_default_dockerfile_parse.py'],
122126
re.compile('tests/test_class_command.py'):
123127
['python tests/test_class_command.py'],
128+
re.compile('tests/test_class_oci_image.py'):
129+
['python tests/test_class_oci_image.py'],
124130
re.compile('tests/test_class_docker_image.py'):
125131
['python tests/test_class_docker_image.py',
126132
'tern report -w photon.tar'],

docker/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ RUN echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.lis
2323
fuse3/bullseye \
2424
git \
2525
jq \
26+
skopeo \
2627
&& rm -rf /var/lib/apt/lists/*
2728

2829
COPY --from=builder /install /usr/local

tern/__main__.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ def check_image_input(options):
7070
logger.critical(errors.incorrect_raw_option)
7171
sys.exit(1)
7272
# Check if the image string has the right format
73-
if options.docker_image:
74-
if not check_image_string(options.docker_image):
73+
if options.image:
74+
if not check_image_string(options.image):
7575
logger.critical(errors.incorrect_image_string_format)
7676
sys.exit(1)
7777

@@ -108,9 +108,9 @@ def do_main(args):
108108
drun.execute_dockerfile(args)
109109
else:
110110
logger.critical("Currently --layer/-y can only be used with"
111-
" --docker-image/-i")
111+
" --image/-i")
112112
sys.exit(1)
113-
elif args.docker_image or args.raw_image:
113+
elif args.image or args.raw_image:
114114
check_image_input(args)
115115
# If the checks are OK, execute for docker image
116116
crun.execute_image(args)
@@ -167,15 +167,13 @@ def main():
167167
parser_report.add_argument('-d', '--dockerfile', type=check_file_existence,
168168
help="Dockerfile used to build the Docker"
169169
" image")
170-
parser_report.add_argument('-i', '--docker-image',
171-
help="Docker image that exists locally -"
172-
" image:tag"
173-
" The option can be used to pull docker"
174-
" images by digest as well -"
175-
" <repo>@<digest-type>:<digest>")
170+
parser_report.add_argument('-i', '--image',
171+
help="A container image referred either by "
172+
" repo:tag or repo@digest-type:digest")
176173
parser_report.add_argument('-w', '--raw-image', metavar='FILE',
177174
help="Raw container image that exists locally "
178-
"in the form of a tar archive.")
175+
"in the form of a tar archive. Only the output"
176+
"of 'docker save' is supported")
179177
parser_report.add_argument('-y', '--layer', metavar='LAYER_NUMBER',
180178
const=1, action='store',
181179
dest='load_until_layer',

tern/analyze/common.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
#
3-
# Copyright (c) 2017-2020 VMware, Inc. All Rights Reserved.
3+
# Copyright (c) 2017-2021 VMware, Inc. All Rights Reserved.
44
# SPDX-License-Identifier: BSD-2-Clause
55

66
'''
@@ -18,7 +18,6 @@
1818
from tern.utils import cache
1919
from tern.utils import constants
2020
from tern.utils import general
21-
from tern.utils import rootfs
2221
from debian_inspector import debcon
2322
from debian_inspector import copyright as debut_copyright
2423

@@ -152,7 +151,7 @@ def save_to_cache(image):
152151

153152
def is_empty_layer(layer):
154153
'''Return True if the given image layer is empty'''
155-
cwd = rootfs.get_untar_dir(layer.tar_file)
154+
cwd = layer.get_untar_dir()
156155
if len(os.listdir(cwd)) == 0:
157156
return True
158157
return False

tern/analyze/default/container/image.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from tern.classes.notice import Notice
1515
from tern.classes.docker_image import DockerImage
16+
from tern.classes.oci_image import OCIImage
1617
from tern.utils import constants
1718
from tern.analyze import passthrough
1819
from tern.analyze.default.container import single_layer
@@ -23,24 +24,29 @@
2324
logger = logging.getLogger(constants.logger_name)
2425

2526

26-
def load_full_image(image_tag_string, load_until_layer=0):
27-
'''Create image object from image name and tag and return the object.
28-
Loads only as many layers as needed.'''
29-
test_image = DockerImage(image_tag_string)
27+
def load_full_image(image_tag_string, image_type='oci', load_until_layer=0):
28+
"""Create image object from image name and tag and return the object.
29+
The kind of image object is created based on the image_type.
30+
image_type = oci OR docker
31+
Loads only as many layers as needed."""
32+
if image_type == 'oci':
33+
image = OCIImage(image_tag_string)
34+
elif image_type == 'docker':
35+
image = DockerImage(image_tag_string)
3036
failure_origin = formats.image_load_failure.format(
31-
testimage=test_image.repotag)
37+
testimage=image.repotag)
3238
try:
33-
test_image.load_image(load_until_layer)
39+
image.load_image(load_until_layer)
3440
except (NameError,
3541
subprocess.CalledProcessError,
3642
IOError,
3743
docker.errors.APIError,
3844
ValueError,
3945
EOFError) as error:
4046
logger.warning('Error in loading image: %s', str(error))
41-
test_image.origins.add_notice_to_origins(
47+
image.origins.add_notice_to_origins(
4248
failure_origin, Notice(str(error), 'error'))
43-
return test_image
49+
return image
4450

4551

4652
def default_analyze(image_obj, options):

tern/analyze/default/container/multi_layer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@
3030
def mount_overlay_fs(image_obj, top_layer, driver):
3131
'''Given the image object and the top most layer, mount all the layers
3232
until the top layer using overlayfs'''
33-
tar_layers = []
33+
layer_paths = []
3434
for index in range(0, top_layer + 1):
35-
tar_layers.append(image_obj.layers[index].tar_file)
36-
target = rootfs.mount_diff_layers(tar_layers, driver)
35+
layer_paths.append(image_obj.layers[index].get_untar_dir())
36+
target = rootfs.mount_diff_layers(layer_paths, driver)
3737
return target
3838

3939

4040
def apply_layers(image_obj, top_layer):
4141
"""Apply image diff layers without using a kernel snapshot driver"""
4242
# All merging happens in the merge directory
4343
target = os.path.join(rootfs.get_working_dir(), constants.mergedir)
44-
layer_dir = rootfs.get_untar_dir(image_obj.layers[top_layer].tar_file)
44+
layer_dir = image_obj.layers[top_layer].get_untar_dir()
4545
layer_contents = layer_dir + '/*'
4646
# Account for whiteout files
4747
for fd in image_obj.layers[top_layer].files:

tern/analyze/default/container/run.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from tern.report import report
1515
from tern.report import formats
1616
from tern import prep
17-
from tern.load import docker_api
17+
from tern.load import skopeo
1818
from tern.analyze import common
1919
from tern.analyze.default.container import image as cimage
2020

@@ -27,28 +27,20 @@ def extract_image(args):
2727
"""The image can either be downloaded from a container registry or provided
2828
as an image tarball. Extract the image into a working directory accordingly
2929
Return an image name and tag and an image digest if it exists"""
30-
if args.docker_image:
31-
# extract the docker image
32-
image_attrs = docker_api.dump_docker_image(args.docker_image)
33-
if image_attrs:
34-
# repo name and digest is preferred, but if that doesn't exist
35-
# the repo name and tag will do. If neither exist use repo Id.
36-
if image_attrs['Id']:
37-
image_string = image_attrs['Id']
38-
if image_attrs['RepoTags']:
39-
image_string = image_attrs['RepoTags'][0]
40-
if image_attrs['RepoDigests']:
41-
image_string = image_attrs['RepoDigests'][0]
42-
return image_string
43-
logger.critical("Cannot extract Docker image")
30+
if args.image:
31+
# download the image
32+
result = skopeo.pull_image(args.image)
33+
if result:
34+
return 'oci', args.image
35+
logger.critical("Cannot download Container image: \"%s\"", args.image)
4436
if args.raw_image:
4537
# for now we assume that the raw image tarball is always
4638
# the product of "docker save", hence it will be in
4739
# the docker style layout
4840
if rootfs.extract_tarfile(args.raw_image, rootfs.get_working_dir()):
49-
return args.raw_image
50-
logger.critical("Cannot extract raw image")
51-
return None
41+
return 'docker', args.raw_image
42+
logger.critical("Cannot extract raw Docker image")
43+
return None, None
5244

5345

5446
def setup(image_obj):
@@ -72,11 +64,11 @@ def teardown(image_obj):
7264
def execute_image(args):
7365
"""Execution path for container images"""
7466
logger.debug('Starting analysis...')
75-
image_string = extract_image(args)
67+
image_type, image_string = extract_image(args)
7668
# If the image has been extracted, load the metadata
77-
if image_string:
69+
if image_type and image_string:
7870
full_image = cimage.load_full_image(
79-
image_string, args.load_until_layer)
71+
image_string, image_type, args.load_until_layer)
8072
# check if the image was loaded successfully
8173
if full_image.origins.is_empty():
8274
# Add an image origin here

tern/analyze/default/container/single_layer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def find_os_release(host_path):
6262
def get_os_release(base_layer):
6363
"""Assuming that the layer tarball is untarred are ready to be inspected,
6464
get the OS information from the os-release file"""
65-
return find_os_release(rootfs.get_untar_dir(base_layer.tar_file))
65+
return find_os_release(base_layer.get_untar_dir())
6666

6767

6868
def get_os_style(image_layer, binary):

0 commit comments

Comments
 (0)