1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2017 Ricardo Wurmus <rekado@elephly.net>
3 ;;; Copyright © 2017, 2018 Ludovic Courtès <ludo@gnu.org>
4 ;;; Copyright © 2018 Chris Marusich <cmmarusich@gmail.com>
6 ;;; This file is part of GNU Guix.
8 ;;; GNU Guix is free software; you can redistribute it and/or modify it
9 ;;; under the terms of the GNU General Public License as published by
10 ;;; the Free Software Foundation; either version 3 of the License, or (at
11 ;;; your option) any later version.
13 ;;; GNU Guix is distributed in the hope that it will be useful, but
14 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;;; GNU General Public License for more details.
18 ;;; You should have received a copy of the GNU General Public License
19 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
21 (define-module (guix docker)
22 #:use-module (gcrypt hash)
23 #:use-module (guix base16)
24 #:use-module ((guix build utils)
26 delete-file-recursively
27 with-directory-excursion
29 #:use-module (json) ;guile-json
30 #:use-module (srfi srfi-19)
31 #:use-module (srfi srfi-26)
32 #:use-module ((texinfo string-utils)
33 #:select (escape-special-chars))
34 #:use-module (rnrs bytevectors)
35 #:use-module (ice-9 match)
36 #:export (build-docker-image))
38 ;; Generate a 256-bit identifier in hexadecimal encoding for the Docker image.
40 (compose bytevector->base16-string sha256 string->utf8))
42 (define (layer-diff-id layer)
43 "Generate a layer DiffID for the given LAYER archive."
44 (string-append "sha256:" (bytevector->base16-string (file-sha256 layer))))
46 ;; This is the semantic version of the JSON metadata schema according to
47 ;; https://github.com/docker/docker/blob/master/image/spec/v1.2.md
48 ;; It is NOT the version of the image specification.
49 (define schema-version "1.0")
51 (define (image-description id time)
52 "Generate a simple image description."
55 (container_config . #nil)))
57 (define (generate-tag path)
58 "Generate an image tag for the given PATH."
59 (match (string-split (basename path) #\-)
60 ((hash name . rest) (string-append name ":" hash))))
62 (define (manifest path id)
63 "Generate a simple image manifest."
64 `(((Config . "config.json")
65 (RepoTags . (,(generate-tag path)))
66 (Layers . (,(string-append id "/layer.tar"))))))
68 ;; According to the specifications this is required for backwards
69 ;; compatibility. It duplicates information provided by the manifest.
70 (define (repositories path id)
71 "Generate a repositories file referencing PATH and the image ID."
72 `((,(generate-tag path) . ((latest . ,id)))))
74 ;; See https://github.com/opencontainers/image-spec/blob/master/config.md
75 (define (config layer time arch)
76 "Generate a minimal image configuration for the given LAYER file."
77 ;; "architecture" must be values matching "platform.arch" in the
79 ;; https://github.com/opencontainers/runtime-spec/blob/v1.0.0-rc2/config.md#platform
80 `((architecture . ,arch)
81 (comment . "Generated by GNU Guix")
84 (container_config . #nil)
86 (rootfs . ((type . "layers")
87 (diff_ids . (,(layer-diff-id layer)))))))
89 (define %tar-determinism-options
90 ;; GNU tar options to produce archives deterministically.
91 '("--sort=name" "--mtime=@1"
92 "--owner=root:0" "--group=root:0"))
94 (define symlink-source
97 (string-trim source #\/))))
99 (define (topmost-component file)
100 "Return the topmost component of FILE. For instance, if FILE is \"/a/b/c\",
102 (match (string-tokenize file (char-set-complement (char-set #\/)))
106 (define* (build-docker-image image paths prefix
109 (transformations '())
110 (system (utsname:machine (uname)))
112 (creation-time (current-time time-utc)))
113 "Write to IMAGE a Docker image archive containing the given PATHS. PREFIX
114 must be a store path that is a prefix of any store paths in PATHS.
116 SYMLINKS must be a list of (SOURCE -> TARGET) tuples describing symlinks to be
117 created in the image, where each TARGET is relative to PREFIX.
118 TRANSFORMATIONS must be a list of (OLD -> NEW) tuples describing how to
119 transform the PATHS. Any path in PATHS that begins with OLD will be rewritten
120 in the Docker image so that it begins with NEW instead. If a path is a
121 non-empty directory, then its contents will be recursively added, as well.
123 SYSTEM is a GNU triplet (or prefix thereof) of the system the binaries in
124 PATHS are for; it is used to produce metadata in the image. Use COMPRESSOR, a
125 command such as '(\"gzip\" \"-9n\"), to compress IMAGE. Use CREATION-TIME, a
126 SRFI-19 time-utc object, as the creation time in metadata."
127 (define (sanitize path-fragment)
128 (escape-special-chars
129 ;; GNU tar strips the leading slash off of absolute paths before applying
130 ;; the transformations, so we need to do the same, or else our
131 ;; replacements won't match any paths.
132 (string-trim path-fragment #\/)
133 ;; Escape the basic regexp special characters (see: "(sed) BRE syntax").
134 ;; We also need to escape "/" because we use it as a delimiter.
137 (define transformation->replacement
140 ;; See "(tar) transform" for details on the expression syntax.
141 (string-append "s/^" (sanitize old) "/" (sanitize new) "/"))))
142 (define (transformations->expression transformations)
143 (let ((replacements (map transformation->replacement transformations)))
145 ;; Avoid transforming link targets, since that would break some links
146 ;; (e.g., symlinks that point to an absolute store path).
148 (string-join replacements ";")
149 ;; Some paths might still have a leading path delimiter even after tar
150 ;; transforms them (e.g., "/a/b" might be transformed into "/b"), so
151 ;; strip any leading path delimiters that remain.
153 (define transformation-options
154 (if (eq? '() transformations)
156 `("--transform" ,(transformations->expression transformations))))
157 (let* ((directory "/tmp/docker-image") ;temporary working directory
158 (id (docker-id prefix))
159 (time (date->string (time-utc->date creation-time) "~4"))
160 (arch (let-syntax ((cond* (syntax-rules ()
161 ((_ (pattern clause) ...)
162 (cond ((string-prefix? pattern system)
166 (error "unsupported system"
168 (cond* ("x86_64" "amd64")
171 ("mips64" "mips64le")))))
172 ;; Make sure we start with a fresh, empty working directory.
174 (with-directory-excursion directory
176 (with-directory-excursion id
177 (with-output-to-file "VERSION"
178 (lambda () (display schema-version)))
179 (with-output-to-file "json"
180 (lambda () (scm->json (image-description id time))))
183 (for-each (match-lambda
185 (let ((source (string-trim source #\/)))
186 (mkdir-p (dirname source))
187 (symlink (string-append prefix "/" target)
191 (apply invoke "tar" "-cf" "layer.tar"
192 `(,@transformation-options
193 ,@%tar-determinism-options
195 ,@(map symlink-source symlinks)))
196 ;; It is possible for "/" to show up in the archive, especially when
197 ;; applying transformations. For example, the transformation
198 ;; "s,^/a,," will (perhaps surprisingly) cause GNU tar to transform
199 ;; the path "/a" into "/". The presence of "/" in the archive is
200 ;; probably benign, but it is definitely safe to remove it, so let's
201 ;; do that. This fails when "/" is not in the archive, so use system*
202 ;; instead of invoke to avoid an exception in that case.
203 (system* "tar" "--delete" "/" "-f" "layer.tar")
204 (for-each delete-file-recursively
205 (map (compose topmost-component symlink-source)
208 (with-output-to-file "config.json"
210 (scm->json (config (string-append id "/layer.tar")
212 (with-output-to-file "manifest.json"
214 (scm->json (manifest prefix id))))
215 (with-output-to-file "repositories"
217 (scm->json (repositories prefix id)))))
219 (apply invoke "tar" "-cf" image "-C" directory
220 `(,@%tar-determinism-options
222 (list "-I" (string-join compressor))
225 (delete-file-recursively directory)))