maav / guix-mirror (public) (License: GPLv3+) (since 2019-11-02) (hash sha1)
Mirror of GNU Guix (https://git.savannah.gnu.org/git/guix.git) with personal branches integrated into master branch.

/guix/docker.scm (97ac6d982b4f3c06f374d5b512a03046619cbdaf) (11774 bytes) (mode 100644) (type blob)

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017 Ricardo Wurmus <rekado@elephly.net>
;;; Copyright © 2017, 2018, 2019 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2018 Chris Marusich <cmmarusich@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (guix docker)
  #:use-module (gcrypt hash)
  #:use-module (guix base16)
  #:use-module ((guix build utils)
                #:select (mkdir-p
                          delete-file-recursively
                          with-directory-excursion
                          invoke))
  #:use-module (gnu build install)
  #:use-module (json)                             ;guile-json
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-19)
  #:use-module (srfi srfi-26)
  #:use-module ((texinfo string-utils)
                #:select (escape-special-chars))
  #:use-module (rnrs bytevectors)
  #:use-module (ice-9 ftw)
  #:use-module (ice-9 match)
  #:export (build-docker-image))

;; Generate a 256-bit identifier in hexadecimal encoding for the Docker image.
(define docker-id
  (compose bytevector->base16-string sha256 string->utf8))

(define (layer-diff-id layer)
  "Generate a layer DiffID for the given LAYER archive."
  (string-append "sha256:" (bytevector->base16-string (file-sha256 layer))))

;; This is the semantic version of the JSON metadata schema according to
;; https://github.com/docker/docker/blob/master/image/spec/v1.2.md
;; It is NOT the version of the image specification.
(define schema-version "1.0")

(define (image-description id time)
  "Generate a simple image description."
  `((id . ,id)
    (created . ,time)
    (container_config . #nil)))

(define (canonicalize-repository-name name)
  "\"Repository\" names are restricted to roughtl [a-z0-9_.-].
Return a version of TAG that follows these rules."
  (define ascii-letters
    (string->char-set "abcdefghijklmnopqrstuvwxyz"))

  (define separators
    (string->char-set "_-."))

  (define repo-char-set
    (char-set-union char-set:digit ascii-letters separators))

  (string-map (lambda (chr)
                (if (char-set-contains? repo-char-set chr)
                    chr
                    #\.))
              (string-trim (string-downcase name) separators)))

(define* (manifest path id #:optional (tag "guix"))
  "Generate a simple image manifest."
  (let ((tag (canonicalize-repository-name tag)))
    `#(((Config . "config.json")
        (RepoTags . #(,(string-append tag ":latest")))
        (Layers . #(,(string-append id "/layer.tar")))))))

;; According to the specifications this is required for backwards
;; compatibility.  It duplicates information provided by the manifest.
(define* (repositories path id #:optional (tag "guix"))
  "Generate a repositories file referencing PATH and the image ID."
  `((,(canonicalize-repository-name tag) . ((latest . ,id)))))

;; See https://github.com/opencontainers/image-spec/blob/master/config.md
(define* (config layer time arch #:key entry-point (environment '()))
  "Generate a minimal image configuration for the given LAYER file."
  ;; "architecture" must be values matching "platform.arch" in the
  ;; runtime-spec at
  ;; https://github.com/opencontainers/runtime-spec/blob/v1.0.0-rc2/config.md#platform
  `((architecture . ,arch)
    (comment . "Generated by GNU Guix")
    (created . ,time)
    (config . ,`((env . ,(list->vector
                          (map (match-lambda
                                 ((name . value)
                                  (string-append name "=" value)))
                               environment)))
                 ,@(if entry-point
                       `((entrypoint . ,(list->vector entry-point)))
                       '())))
    (container_config . #nil)
    (os . "linux")
    (rootfs . ((type . "layers")
               (diff_ids . #(,(layer-diff-id layer)))))))

(define %tar-determinism-options
  ;; GNU tar options to produce archives deterministically.
  '("--sort=name" "--mtime=@1"
    "--owner=root:0" "--group=root:0"))

(define directive-file
  ;; Return the file or directory created by a 'evaluate-populate-directive'
  ;; directive.
  (match-lambda
    ((source '-> target)
     (string-trim source #\/))
    (('directory name _ ...)
     (string-trim name #\/))))

(define* (build-docker-image image paths prefix
                             #:key
                             (repository "guix")
                             (extra-files '())
                             (transformations '())
                             (system (utsname:machine (uname)))
                             database
                             entry-point
                             (environment '())
                             compressor
                             (creation-time (current-time time-utc)))
  "Write to IMAGE a Docker image archive containing the given PATHS.  PREFIX
must be a store path that is a prefix of any store paths in PATHS.  REPOSITORY
is a descriptive name that will show up in \"REPOSITORY\" column of the output
of \"docker images\".

When DATABASE is true, copy it to /var/guix/db in the image and create
/var/guix/gcroots and friends.

When ENTRY-POINT is true, it must be a list of strings; it is stored as the
entry point in the Docker image JSON structure.

ENVIRONMENT must be a list of name/value pairs.  It specifies the environment
variables that must be defined in the resulting image.

EXTRA-FILES must be a list of directives for 'evaluate-populate-directive'
describing non-store files that must be created in the image.

TRANSFORMATIONS must be a list of (OLD -> NEW) tuples describing how to
transform the PATHS.  Any path in PATHS that begins with OLD will be rewritten
in the Docker image so that it begins with NEW instead.  If a path is a
non-empty directory, then its contents will be recursively added, as well.

SYSTEM is a GNU triplet (or prefix thereof) of the system the binaries in
PATHS are for; it is used to produce metadata in the image.  Use COMPRESSOR, a
command such as '(\"gzip\" \"-9n\"), to compress IMAGE.  Use CREATION-TIME, a
SRFI-19 time-utc object, as the creation time in metadata."
  (define (sanitize path-fragment)
    (escape-special-chars
     ;; GNU tar strips the leading slash off of absolute paths before applying
     ;; the transformations, so we need to do the same, or else our
     ;; replacements won't match any paths.
     (string-trim path-fragment #\/)
     ;; Escape the basic regexp special characters (see: "(sed) BRE syntax").
     ;; We also need to escape "/" because we use it as a delimiter.
     "/*.^$[]\\"
     #\\))
  (define transformation->replacement
    (match-lambda
      ((old '-> new)
       ;; See "(tar) transform" for details on the expression syntax.
       (string-append "s/^" (sanitize old) "/" (sanitize new) "/"))))
  (define (transformations->expression transformations)
    (let ((replacements (map transformation->replacement transformations)))
      (string-append
       ;; Avoid transforming link targets, since that would break some links
       ;; (e.g., symlinks that point to an absolute store path).
       "flags=rSH;"
       (string-join replacements ";")
       ;; Some paths might still have a leading path delimiter even after tar
       ;; transforms them (e.g., "/a/b" might be transformed into "/b"), so
       ;; strip any leading path delimiters that remain.
       ";s,^//*,,")))
  (define transformation-options
    (if (eq? '() transformations)
        '()
        `("--transform" ,(transformations->expression transformations))))
  (let* ((directory "/tmp/docker-image") ;temporary working directory
         (id (docker-id prefix))
         (time (date->string (time-utc->date creation-time) "~4"))
         (arch (let-syntax ((cond* (syntax-rules ()
                                     ((_ (pattern clause) ...)
                                      (cond ((string-prefix? pattern system)
                                             clause)
                                            ...
                                            (else
                                             (error "unsupported system"
                                                    system)))))))
                 (cond* ("x86_64" "amd64")
                        ("i686"   "386")
                        ("arm"    "arm")
                        ("mips64" "mips64le")))))
    ;; Make sure we start with a fresh, empty working directory.
    (mkdir directory)
    (with-directory-excursion directory
      (mkdir id)
      (with-directory-excursion id
        (with-output-to-file "VERSION"
          (lambda () (display schema-version)))
        (with-output-to-file "json"
          (lambda () (scm->json (image-description id time))))

        ;; Create a directory for the non-store files that need to go into the
        ;; archive.
        (mkdir "extra")

        (with-directory-excursion "extra"
          ;; Create non-store files.
          (for-each (cut evaluate-populate-directive <> "./")
                    extra-files)

          (when database
            ;; Initialize /var/guix, assuming PREFIX points to a profile.
            (install-database-and-gc-roots "." database prefix))

          (apply invoke "tar" "-cf" "../layer.tar"
                 `(,@transformation-options
                   ,@%tar-determinism-options
                   ,@paths
                   ,@(scandir "."
                              (lambda (file)
                                (not (member file '("." ".."))))))))

        ;; It is possible for "/" to show up in the archive, especially when
        ;; applying transformations.  For example, the transformation
        ;; "s,^/a,," will (perhaps surprisingly) cause GNU tar to transform
        ;; the path "/a" into "/".  The presence of "/" in the archive is
        ;; probably benign, but it is definitely safe to remove it, so let's
        ;; do that.  This fails when "/" is not in the archive, so use system*
        ;; instead of invoke to avoid an exception in that case, and redirect
        ;; stderr to the bit bucket to avoid "Exiting with failure status"
        ;; error messages.
        (with-error-to-port (%make-void-port "w")
          (lambda ()
            (system* "tar" "--delete" "/" "-f" "layer.tar")))

        (delete-file-recursively "extra"))

      (with-output-to-file "config.json"
        (lambda ()
          (scm->json (config (string-append id "/layer.tar")
                             time arch
                             #:environment environment
                             #:entry-point entry-point))))
      (with-output-to-file "manifest.json"
        (lambda ()
          (scm->json (manifest prefix id repository))))
      (with-output-to-file "repositories"
        (lambda ()
          (scm->json (repositories prefix id repository)))))

    (apply invoke "tar" "-cf" image "-C" directory
           `(,@%tar-determinism-options
             ,@(if compressor
                   (list "-I" (string-join compressor))
                   '())
             "."))
    (delete-file-recursively directory)))


Mode Type Size Ref File
100644 blob 6139 7f310d2612983845551d0a641b730a269a180a3f .dir-locals.el
100644 blob 2533 e2f745b42a5a1591a93a3e9a7ccfc107a6178eff .gitignore
100644 blob 6179 ee164083c8379d90c6e7ab3b38576266f458f40c .guix-authorizations
100644 blob 182 b852180cf2563ec7b74b93c954de38d77237f23f .guix-channel
100644 blob 4499 146e65184a2f987b0307d0ec7300c93e84110625 .mailmap
100644 blob 472 1e30a74a64f51ec735dcc44ff4dfe5fa4fa13c6f AUTHORS
100644 blob 3273 ef90330cdacb9ecf7dbf38a03cdb490db131a4ad CODE-OF-CONDUCT
100644 blob 35147 94a9ed024d3859793618152ea559a168bbcbb5e2 COPYING
100644 blob 163 d6ea6943261fcae51c095ad39fe59140fc62de22 ChangeLog
100644 blob 749 aaa673fc93b0bb74feca4783ae427b9ea1b604ea HACKING
100644 blob 33109 a75d9c1ffc4237c0478b62a235b9b521fd840517 Makefile.am
100644 blob 359348 bb1de1e93802064ff44392db56d05cd0a11fcc15 NEWS
100644 blob 5260 5e9069f80f58d3946cdd588f30919a177eaccb55 README
100644 blob 3237 2475cb637ceb6eb43f54d080c56e5793041b76e5 ROADMAP
100644 blob 2381 af7afd3576f2e6aa5cbafc3c6354bbab1ae00774 THANKS
100644 blob 4360 f854f7fa98e09c7b512f3efb702c290b615186a0 TODO
100755 blob 906 a47269d87f1d6fd27bbaf634ac7439b38b32cca3 bootstrap
040000 tree - b15b9ede344760715e240528bc322c7b0194bbe7 build-aux
100644 blob 4808 50ead355a81edebf5c9419bd76a1dd69e85f5adf config-daemon.ac
100644 blob 8760 6861112eafaed85e107f8976f12e0ddb795571b7 configure.ac
100644 blob 339545 d234c4ec8668cead20279d903589d29c513b4cb6 d3.v3.js
040000 tree - 3429d151d1aa398d5f16df794830da129964d217 doc
040000 tree - e4976b665df8f7ecf62f39110df084f17cfa1f34 etc
100644 blob 5289 f139531ef3ecf56a790ae73934e2d91016c1aba4 gnu.scm
040000 tree - e9f491f3daaa762450fec87d798a464715d480fa gnu
100644 blob 4207 ad8279395d8eb1fe5a836d54ec563a4577f4d135 graph.js
100644 blob 1357 8753c21e423f880e7a6d9f7f6f6ff1139f8b7254 guix.scm
040000 tree - e6c353ec8e6893b71ddd0af75e8d2659b9e030dc guix
040000 tree - 8df9aaabfb400159e2559fd4331fb861cb0a5adc m4
040000 tree - d0ec05821e49fa1536a9c19a33ad13b5ba3ea0c2 nix
040000 tree - 8dac6dd305591d733ef087c35eee3b3acb1daee2 po
040000 tree - 8c4db11917d51c4d71a841813cf8951000b76687 scripts
040000 tree - 80c5d6d29a082dbb4edeaec2539b73c722719686 tests
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/maav/guix-mirror

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/maav/guix-mirror

Clone this repository using git:
git clone git://git.rocketgit.com/user/maav/guix-mirror

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main