mirror of
https://github.com/awakesecurity/hocker.git
synced 2024-11-22 02:12:35 +03:00
Initial public release of hocker
This commit is contained in:
commit
9b39e78744
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
dist
|
||||
dist-*
|
||||
cabal-dev
|
||||
*.o
|
||||
*.hi
|
||||
*.chi
|
||||
*.chs.h
|
||||
*.dyn_o
|
||||
*.dyn_hi
|
||||
.hpc
|
||||
.hsenv
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
*.prof
|
||||
*.aux
|
||||
*.hp
|
||||
*.eventlog
|
||||
.stack-work/
|
||||
cabal.project.local
|
||||
throwaway/*
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2016 Awake Networks
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
96
README.md
Normal file
96
README.md
Normal file
@ -0,0 +1,96 @@
|
||||
# Welcome!
|
||||
The `hocker` package provides a small set of utilities to fetch docker image
|
||||
artifacts from docker registries and produce Nix derivations marrying docker and
|
||||
Nix elegantly:
|
||||
|
||||
- [`hocker-image`](./hocker-image/README.md) for fetching a docker image
|
||||
- [`hocker-layer`](./hocker-layer/README.md) for fetching a docker image's layers
|
||||
- [`hocker-config`](./hocker-config/README.md) for fetching a docker image's configuration JSON
|
||||
- [`hocker-manifest`](./hocker-manifest/README.md) for fetching docker registry image manifest
|
||||
- [`docker2nix`](./docker2nix/README.md) for generating Nix expressions calling the `fetchdocker`
|
||||
derivations, given a docker registry image manifest
|
||||
|
||||
These tools _only_ work with version 2 of the **docker registry** and **docker
|
||||
(>=) v1.10**.
|
||||
|
||||
The motivation for this tool came from a need to fetch docker image artifacts
|
||||
from a docker registry without the stock docker tooling designed to only work
|
||||
with the docker daemon.
|
||||
|
||||
Our use-case (and the reason why this package exposes a `docker2nix` tool) was
|
||||
the need to pull our docker images into a [NixOS system's store](https://nixos.org/nix/manual/#ch-about-nix) and load
|
||||
those images from the store into the docker daemon running on that same system.
|
||||
|
||||
We desired this for two critical reasons:
|
||||
1. The docker daemon no longer required an internet connection in order to pull
|
||||
the docker images it needed
|
||||
2. By virtue of fetching the docker images at build-time as opposed to run-time,
|
||||
failures resulting in non-existent images or image tags we caught earlier
|
||||
|
||||
We strived to make this tool useful outside of the context of Nix and NixOS,
|
||||
therefore all of these tools are usable without Nix in the workflow.
|
||||
|
||||
For high-level documentation of each utility, please refer to the README's in
|
||||
each project's respective directory (links are in the above list).
|
||||
|
||||
## Quickstart
|
||||
Let's first retrieve a docker registry image manifest for the `debian:jessie`
|
||||
docker image (note that we need the `library/` repository prefix because we are
|
||||
pulling from the official debian repository!):
|
||||
|
||||
```shell
|
||||
$ hocker-manifest library/debian jessie
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1528,
|
||||
"digest": "sha256:054abe38b1e6f863befa4258cbfaf127b1cc9440d2e2e349b15d22e676b591e7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 52550276,
|
||||
"digest": "sha256:cd0a524342efac6edff500c17e625735bbe479c926439b263bbe3c8518a0849c"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Private Registries
|
||||
We developed these tools with private registries in-mind and they currently
|
||||
support three modes of authentication:
|
||||
|
||||
1. Nothing at all (simply do not supply `--token` or `--username` and
|
||||
`--password`)
|
||||
2. Bearer token-based authentication, you should retrieve a token and then give
|
||||
it via the `--token` flag
|
||||
3. Basic authentication with `--username` and `--password` (most common with
|
||||
nginx proxied registries providing basic auth protection; you should be
|
||||
careful to ensure you're only sending requests to registries exposed via TLS
|
||||
or SSL!)
|
||||
|
||||
A caveat to #1 if you do not supply any authentication credential flags and you
|
||||
also do not supply a `--registry` flag then the tools assume you wish to make a
|
||||
request to the public docker hub registry, in which case they ask for a
|
||||
short-lived authentication token from the registry auth server and then make the
|
||||
request to the public docker hub registry.
|
||||
|
||||
# TODO
|
||||
- [X] ~Get a nix-build workflow working for hocker~
|
||||
- [ ] Work on a nix-shell based dev workflow
|
||||
- [ ] Document types in `Exceptions`, `ErrorHandling`, etc.
|
||||
- [x] ~Rename the `Types/Extra.hs` module, that's poorly named~ (I got rid of it)
|
||||
- [x] ~Write an updated and accurate README introduction~
|
||||
- [X] Rename `ContainerName` and `ContainerTag` to `ImageName` and `ImageTag` to
|
||||
be more consistent with the correct docker terminology
|
||||
- [x] ~Remove the run prefix from most of the `V1_2.hs` module functions~ (replaced with a `do` prefix)
|
||||
- [X] ~Use HockerException in docker2nix's lib functions~
|
||||
- [x] ~Better document the types and function signatures in `Nix/FetchDocker.hs`~
|
||||
- [X] L258 fix docker-layer to hocker-layer
|
||||
- [ ] Proofread comments
|
||||
- [ ] `Data/Docker/Image/Types.hs` can probably move to a more general location
|
||||
I think
|
||||
- [ ] Use friendly module prefixing more consistently and cleanup usage
|
||||
- [ ] Strip out the unused docker image V1 code
|
46
config.nix
Normal file
46
config.nix
Normal file
@ -0,0 +1,46 @@
|
||||
{ allowUnfree = true;
|
||||
packageOverrides = pkgs: {
|
||||
haskellPackages = pkgs.haskellPackages.override {
|
||||
overrides = haskellPackagesNew: haskellPackagesOld: {
|
||||
optparse-applicative =
|
||||
pkgs.haskell.lib.dontCheck
|
||||
(haskellPackagesNew.callPackage ./nix/optparse-applicative.nix { });
|
||||
|
||||
optparse-generic =
|
||||
haskellPackagesNew.callPackage ./nix/optparse-generic.nix { };
|
||||
|
||||
turtle =
|
||||
haskellPackagesNew.callPackage ./nix/turtle.nix { };
|
||||
|
||||
wreq =
|
||||
haskellPackagesNew.callPackage ./nix/wreq.nix { };
|
||||
|
||||
http-client =
|
||||
haskellPackagesNew.callPackage ./nix/http-client.nix { };
|
||||
|
||||
http-client-tls =
|
||||
haskellPackagesNew.callPackage ./nix/http-client-tls.nix { };
|
||||
|
||||
hocker =
|
||||
pkgs.haskell.lib.overrideCabal
|
||||
( haskellPackagesNew.callPackage ./default.nix { } )
|
||||
( oldDerivation: {
|
||||
testToolDepends =
|
||||
(oldDerivation.testToolDepends or []) ++[ pkgs.nix ];
|
||||
buildDepends =
|
||||
(oldDerivation.buildDepends or []) ++ [ pkgs.makeWrapper ];
|
||||
|
||||
postInstall =
|
||||
(oldDerivation.postInstall or "") + ''
|
||||
wrapProgram $out/bin/hocker-* \
|
||||
--suffix PATH : ${pkgs.nix}/bin
|
||||
|
||||
wrapProgram $out/bin/docker2nix \
|
||||
--suffix PATH : ${pkgs.nix}/bin
|
||||
'';
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
39
default.nix
Normal file
39
default.nix
Normal file
@ -0,0 +1,39 @@
|
||||
{ mkDerivation, aeson, aeson-pretty, ansi-wl-pprint, async, base
|
||||
, bytestring, concurrentoutput, containers, cryptonite, data-fix
|
||||
, deepseq, directory, exceptions, filepath, foldl, hnix
|
||||
, http-client, http-types, lens, lens-aeson, lifted-base, memory
|
||||
, mtl, neat-interpolation, network, network-uri, optional-args
|
||||
, optparse-applicative, optparse-generic, pooled-io, pureMD5
|
||||
, scientific, stdenv, tar, tasty, tasty-golden, tasty-hunit
|
||||
, tasty-quickcheck, tasty-smallcheck, temporary, text, time
|
||||
, transformers, turtle, unordered-containers, uri-bytestring
|
||||
, vector, wreq, zlib
|
||||
}:
|
||||
mkDerivation {
|
||||
pname = "hocker";
|
||||
version = "0.1.0.0";
|
||||
src = ./.;
|
||||
isLibrary = true;
|
||||
isExecutable = true;
|
||||
libraryHaskellDepends = [
|
||||
aeson aeson-pretty ansi-wl-pprint async base bytestring
|
||||
concurrentoutput containers cryptonite data-fix deepseq directory
|
||||
exceptions filepath foldl hnix http-client http-types lens
|
||||
lens-aeson lifted-base memory mtl neat-interpolation network
|
||||
network-uri optional-args optparse-applicative optparse-generic
|
||||
pooled-io pureMD5 scientific tar temporary text time transformers
|
||||
turtle unordered-containers uri-bytestring vector wreq zlib
|
||||
];
|
||||
executableHaskellDepends = [
|
||||
base bytestring cryptonite data-fix filepath hnix lens mtl network
|
||||
optional-args optparse-applicative optparse-generic temporary text
|
||||
];
|
||||
testHaskellDepends = [
|
||||
aeson ansi-wl-pprint base bytestring containers cryptonite mtl
|
||||
network network-uri tasty tasty-golden tasty-hunit tasty-quickcheck
|
||||
tasty-smallcheck text unordered-containers
|
||||
];
|
||||
homepage = "https://github.com/awakenetworks/hocker#readme";
|
||||
description = "CLI tools and library to interact with a V2 Docker Registry";
|
||||
license = stdenv.lib.licenses.asl20;
|
||||
}
|
77
docker2nix/Main.hs
Normal file
77
docker2nix/Main.hs
Normal file
@ -0,0 +1,77 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# OPTIONS -fno-warn-orphans #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : docker2nix/Main
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Main where
|
||||
|
||||
import Data.ByteString.Lazy.Char8 as C8L
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text as T
|
||||
import Options.Generic
|
||||
import System.IO (hWaitForInput, stdin)
|
||||
|
||||
import Data.Docker.Image.Types
|
||||
import Data.Docker.Nix.FetchDocker as Nix.FetchDocker
|
||||
import Lib
|
||||
import Network.Wreq.Docker.Registry.V2 as Docker.Registry
|
||||
import Types
|
||||
import Types.ImageName
|
||||
import Types.ImageTag
|
||||
|
||||
-- | Top-level optparse-generic CLI args data type and specification.
|
||||
data ProgArgs w = ProgArgs
|
||||
{ -- | URI for the registry, optional
|
||||
registry :: w ::: Maybe RegistryURI
|
||||
<?> "URI of registry, defaults to the Docker Hub registry"
|
||||
-- | Filepath to a file containing the manifest JSON
|
||||
, manifest :: w ::: Maybe FilePath
|
||||
<?> "Fetch image manifest from a path on the filesystem"
|
||||
-- | Alternative docker image name made available in the Nix
|
||||
-- expression fetchdocker derivation
|
||||
, altImageName :: w ::: Maybe T.Text
|
||||
<?> "Alternate image name provided in the `fetcdocker` derivation"
|
||||
-- | Docker image name (includes the reponame, e.g: library/debian)
|
||||
, name :: ImageName
|
||||
-- | Docker image tag
|
||||
, imageTag :: ImageTag
|
||||
} deriving (Generic)
|
||||
|
||||
|
||||
instance ParseRecord (ProgArgs Wrapped)
|
||||
deriving instance Show (ProgArgs Unwrapped)
|
||||
|
||||
progSummary :: T.Text
|
||||
progSummary = "Produce a Nix expression given a manifest for a docker image via stdin or via a filepath"
|
||||
|
||||
main :: IO ()
|
||||
main = unwrapRecord progSummary >>= \ProgArgs{..} -> do
|
||||
let (imageRepo, imageName) = Lib.splitImgName name
|
||||
dockerRegistry = fromMaybe defaultRegistry registry
|
||||
|
||||
manifestJSON <-
|
||||
case manifest of
|
||||
Just f -> C8L.readFile f
|
||||
Nothing -> do
|
||||
let h = stdin
|
||||
hWaitForInput h (-1)
|
||||
C8L.hGetContents h
|
||||
|
||||
exprs <- Nix.FetchDocker.generate HockerImageMeta{..}
|
||||
either (Lib.exitProgFail . show) Lib.pprintNixExpr exprs
|
62
docker2nix/README.md
Normal file
62
docker2nix/README.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Generate nix expression to fetch a docker image
|
||||
This tool takes a docker registry V2 image manifest JSON on stdin or as a file
|
||||
to read from and generates a Nix expression that uses the fetchdocker machinery
|
||||
to pull all individual layers and generate an image compositor that can stream
|
||||
to `docker load`.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```shell
|
||||
$ docker2nix --help
|
||||
Produce a Nix expression given a manifest for a docker image via stdin or via a
|
||||
filepath
|
||||
|
||||
Usage: docker2nix [--registry URI] [--manifest STRING] [--altName TEXT]
|
||||
IMAGE-NAME IMAGE-TAG
|
||||
|
||||
Available options:
|
||||
-h,--help Show this help text
|
||||
--registry URI URI of registry, defaults to the Docker Hub registry
|
||||
--manifest STRING Fetch image manifest from a path on the filesystem
|
||||
--altName TEXT Alternate image name provided in the `fetcdocker`
|
||||
derivation
|
||||
IMAGE-NAME Docker image name, e.g: 'debian' in debian:jessie
|
||||
IMAGE-TAG Docker image tag identifier, e.g: 'jessie' in
|
||||
debian:jessie
|
||||
```
|
||||
|
||||
Generating a fetchdocker Nix expression from a docker registry V2 image manifest
|
||||
JSON retrieved by `hocker-manifest`:
|
||||
|
||||
```shell
|
||||
$ hocker-manifest library/debian jessie | docker2nix library/debian jessie
|
||||
{
|
||||
config.docker.images.debian = pkgs.fetchdocker {
|
||||
name = "debian";
|
||||
registry = "https://registry-1.docker.io/v2/";
|
||||
repository = "library";
|
||||
imageName = "debian";
|
||||
tag = "jessie";
|
||||
imageConfig = pkgs.fetchDockerConfig {
|
||||
inherit registry repository imageName tag;
|
||||
sha256 = "1rwinmvfc8jxn54y7qnj82acrc97y7xcnn22zaz67y76n4wbwjh5";
|
||||
};
|
||||
imageLayers = let
|
||||
layer0 = pkgs.fetchDockerLayer {
|
||||
inherit registry repository imageName tag;
|
||||
layerDigest = "cd0a524342efac6edff500c17e625735bbe479c926439b263bbe3c8518a0849c";
|
||||
sha256 = "1744l0c8ag5y7ck9nhr6r5wy9frmaxi7xh80ypgnxb7g891m42nd";
|
||||
};
|
||||
in [ layer0 ];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
And to load a fetched docker image into a running docker daemon on a NixOS
|
||||
system (NB the preferred method to do the below might be in a systemd unit with
|
||||
the `config.docker.images.debian` attribute parametrizing the path to
|
||||
`compositeImage.sh`):
|
||||
|
||||
```shell
|
||||
$ /nix/store/6qn5i7p6x3c3qylvzqf76fqgd0gl47cv-debian/compositeImage.sh | docker load
|
||||
```
|
41
hocker-config/Main.hs
Normal file
41
hocker-config/Main.hs
Normal file
@ -0,0 +1,41 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : hocker-config/Main
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Main where
|
||||
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text
|
||||
import Options.Generic
|
||||
|
||||
import Lib
|
||||
import Network.Wreq.Docker.Image.V1_2 as Docker.Image
|
||||
import Network.Wreq.Docker.Registry.V2
|
||||
import Types
|
||||
|
||||
progSummary :: Data.Text.Text
|
||||
progSummary = "Fetch a docker image config JSON from the registry"
|
||||
|
||||
main :: IO ()
|
||||
main = unwrapRecord progSummary >>= \OptArgs{..} -> do
|
||||
let dockerRegistry = fromMaybe defaultRegistry registry
|
||||
|
||||
auth <- mkAuth dockerRegistry imageName credentials
|
||||
config <- Docker.Image.fetchConfig $
|
||||
HockerMeta
|
||||
{ outDir = Nothing
|
||||
, imageLayer = Nothing
|
||||
, ..
|
||||
}
|
||||
either (Lib.exitProgFail . show) (Lib.writeOrPrint out) config
|
107
hocker-config/README.md
Normal file
107
hocker-config/README.md
Normal file
@ -0,0 +1,107 @@
|
||||
# Retrieve a docker image configuration JSON
|
||||
This tool fetches the specified docker image's configuration JSON from the
|
||||
docker registry.
|
||||
|
||||
## Quickstart
|
||||
```shell
|
||||
Fetch a docker image config JSON from the registry
|
||||
|
||||
Usage: hocker-config [--registry URI] ([-u|--username BASIC USERNAME]
|
||||
[-p|--password BASIC PASSWORD] | [-t|--token BEARER TOKEN])
|
||||
[--out STRING] IMAGE-NAME IMAGE-TAG
|
||||
|
||||
Available options:
|
||||
-h,--help Show this help text
|
||||
--registry URI URI of registry, defaults to the Docker Hub registry
|
||||
-u,--username BASIC USERNAME
|
||||
Username part of a basic auth credential
|
||||
-p,--password BASIC PASSWORD
|
||||
Password part of a basic auth credential
|
||||
-t,--token BEARER TOKEN Bearer token retrieved from a call to `docker login`
|
||||
(mutually exclusive to --username and --password)
|
||||
--out STRING Write content to location
|
||||
IMAGE-NAME Docker image name, e.g: 'debian' in debian:jessie
|
||||
IMAGE-TAG Docker image tag identifier, e.g: 'jessie' in
|
||||
debian:jessie
|
||||
```
|
||||
|
||||
```shell
|
||||
$ hocker-config library/debian jessie | jq
|
||||
{
|
||||
"architecture": "amd64",
|
||||
"config": {
|
||||
"Hostname": "200591939db7",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/bash"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:9e77974e778cc6730c21889f33f2dcb141f9f632745ba2c3914dd62250ea93c9",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"container": "9a3fb25551fee47cea1203cbc2a6022dc3ffea8bc2010733e1286c4702cdf778",
|
||||
"container_config": {
|
||||
"Hostname": "200591939db7",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) ",
|
||||
"CMD [\"/bin/bash\"]"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:9e77974e778cc6730c21889f33f2dcb141f9f632745ba2c3914dd62250ea93c9",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"created": "2017-05-08T23:28:15.327579341Z",
|
||||
"docker_version": "17.04.0-ce",
|
||||
"history": [
|
||||
{
|
||||
"created": "2017-05-08T23:28:14.437236885Z",
|
||||
"created_by": "/bin/sh -c #(nop) ADD file:f4e6551ac34ab446a297849489a5693d67a7e76c9cb9ed9346d82392c9d9a5fe in / "
|
||||
},
|
||||
{
|
||||
"created": "2017-05-08T23:28:15.327579341Z",
|
||||
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]",
|
||||
"empty_layer": true
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": [
|
||||
"sha256:8d4d1ab5ff74fc361fb74212fff3b6dc1e6c16d1e1f0e8b44f9a9112b00b564f"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
44
hocker-image/Main.hs
Normal file
44
hocker-image/Main.hs
Normal file
@ -0,0 +1,44 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : hocker-fetch/Main
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Main where
|
||||
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text
|
||||
import Data.Text.IO as TIO
|
||||
import Options.Generic
|
||||
import System.IO.Temp as Tmp
|
||||
|
||||
import Lib
|
||||
import Network.Wreq.Docker.Image.V1_2 as Docker.Image
|
||||
import Network.Wreq.Docker.Registry.V2
|
||||
import Types
|
||||
|
||||
progSummary :: Data.Text.Text
|
||||
progSummary = "Fetch a docker image from a docker registry without using docker"
|
||||
|
||||
main :: IO ()
|
||||
main = unwrapRecord progSummary >>= \OptArgs{..} -> do
|
||||
let dockerRegistry = fromMaybe defaultRegistry registry
|
||||
|
||||
auth <- mkAuth dockerRegistry imageName credentials
|
||||
img <- withSystemTempDirectory "hocker-image-XXXXXX" $ \d ->
|
||||
Docker.Image.fetchAndAssemble $
|
||||
HockerMeta
|
||||
{ outDir = Just d
|
||||
, imageLayer = Nothing
|
||||
, ..
|
||||
}
|
||||
either (Lib.exitProgFail . show) TIO.putStrLn img
|
31
hocker-image/README.md
Normal file
31
hocker-image/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Fetch a docker image
|
||||
|
||||
## Quickstart
|
||||
|
||||
```shell
|
||||
Fetch a docker image from a docker registry without using docker
|
||||
|
||||
Usage: hocker-image [--registry URI] ([-u|--username BASIC USERNAME]
|
||||
[-p|--password BASIC PASSWORD] | [-t|--token BEARER TOKEN])
|
||||
[--out STRING] IMAGE-NAME IMAGE-TAG
|
||||
|
||||
Available options:
|
||||
-h,--help Show this help text
|
||||
--registry URI URI of registry, defaults to the Docker Hub registry
|
||||
-u,--username BASIC USERNAME
|
||||
Username part of a basic auth credential
|
||||
-p,--password BASIC PASSWORD
|
||||
Password part of a basic auth credential
|
||||
-t,--token BEARER TOKEN Bearer token retrieved from a call to `docker login`
|
||||
(mutually exclusive to --username and --password)
|
||||
--out STRING Write content to location
|
||||
IMAGE-NAME Docker image name, e.g: 'debian' in debian:jessie
|
||||
IMAGE-TAG Docker image tag identifier, e.g: 'jessie' in
|
||||
debian:jessie
|
||||
```
|
||||
|
||||
```shell
|
||||
$ hocker-image --out=./debian-latest.tar.gz library/debian latest
|
||||
Downloading layer: 22def84 => decompressed => wrote 159fbd8
|
||||
./debian-latest.tar.gz
|
||||
```
|
72
hocker-layer/Main.hs
Normal file
72
hocker-layer/Main.hs
Normal file
@ -0,0 +1,72 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : hocker-layer/Main
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Main where
|
||||
|
||||
import qualified Crypto.Hash as Hash
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text
|
||||
import Options.Generic
|
||||
|
||||
import Lib
|
||||
import Network.Wreq.Docker.Image.V1_2 as Docker.Image
|
||||
import Network.Wreq.Docker.Registry.V2
|
||||
import Types
|
||||
import Types.Hash ()
|
||||
import Types.ImageName
|
||||
import Types.ImageTag
|
||||
import Types.URI ()
|
||||
|
||||
data ProgArgs w = ProgArgs
|
||||
{ -- | URI for the registry, optional
|
||||
registry :: w ::: Maybe RegistryURI
|
||||
<?> "URI of registry, defaults to the Docker Hub registry"
|
||||
, credentials :: Maybe Credentials
|
||||
-- | Filesystem path to write output to
|
||||
, out :: w ::: Maybe FilePath
|
||||
<?> "Write content to location"
|
||||
-- | Layer sha256 hash digest to fetch from registry
|
||||
, imageLayer :: w ::: Hash.Digest Hash.SHA256
|
||||
<?> "Layer to fetch, by hash digest (unprefixed by the hash algorithm identifier)"
|
||||
-- | Docker image name (includes the repository, e.g: library/debian)
|
||||
, imageName :: ImageName
|
||||
-- | Docker image tag
|
||||
, imageTag :: ImageTag
|
||||
} deriving (Generic)
|
||||
|
||||
instance ParseRecord (ProgArgs Wrapped)
|
||||
deriving instance Show (ProgArgs Unwrapped)
|
||||
|
||||
progSummary :: Data.Text.Text
|
||||
progSummary = "Fetch a docker image layer from a docker registry without using docker"
|
||||
|
||||
main :: IO ()
|
||||
main = unwrapRecord progSummary >>= \ProgArgs{..} -> do
|
||||
let dockerRegistry = fromMaybe defaultRegistry registry
|
||||
|
||||
auth <- mkAuth dockerRegistry imageName credentials
|
||||
layerPath <- Docker.Image.fetchLayer $
|
||||
HockerMeta
|
||||
{ outDir = Nothing
|
||||
, imageLayer = Just imageLayer
|
||||
, ..
|
||||
}
|
||||
either (Lib.exitProgFail . show) Prelude.putStrLn layerPath
|
27
hocker-layer/README.md
Normal file
27
hocker-layer/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Retrieve an individual docker image layer
|
||||
|
||||
## Quickstart
|
||||
|
||||
```shell
|
||||
Fetch a docker image layer from a docker registry without using docker
|
||||
|
||||
Usage: hocker-layer [--registry URI] ([-u|--username BASIC USERNAME]
|
||||
[-p|--password BASIC PASSWORD] | [-t|--token BEARER TOKEN])
|
||||
[--out STRING] (-l|--layer SHA256) IMAGE-NAME IMAGE-TAG
|
||||
|
||||
Available options:
|
||||
-h,--help Show this help text
|
||||
--registry URI URI of registry, defaults to the Docker Hub registry
|
||||
-u,--username BASIC USERNAME
|
||||
Username part of a basic auth credential
|
||||
-p,--password BASIC PASSWORD
|
||||
Password part of a basic auth credential
|
||||
-t,--token BEARER TOKEN Bearer token retrieved from a call to `docker login`
|
||||
(mutually exclusive to --username and --password)
|
||||
--out STRING Write content to location
|
||||
-l,--layer SHA256 Layer to fetch, by hash digest (unprefixed by the
|
||||
hash algorithm identifier)
|
||||
IMAGE-NAME Docker image name, e.g: 'debian' in debian:jessie
|
||||
IMAGE-TAG Docker image tag identifier, e.g: 'jessie' in
|
||||
debian:jessie
|
||||
```
|
41
hocker-manifest/Main.hs
Normal file
41
hocker-manifest/Main.hs
Normal file
@ -0,0 +1,41 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : hocker-manifest/Main
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Main where
|
||||
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text
|
||||
import Options.Generic
|
||||
|
||||
import Lib
|
||||
import Network.Wreq.Docker.Image.V1_2 as Docker.Image
|
||||
import Network.Wreq.Docker.Registry.V2
|
||||
import Types
|
||||
|
||||
progSummary :: Data.Text.Text
|
||||
progSummary = "Pull a docker image manifest from the registry"
|
||||
|
||||
main :: IO ()
|
||||
main = unwrapRecord progSummary >>= \OptArgs{..} -> do
|
||||
let dockerRegistry = fromMaybe defaultRegistry registry
|
||||
|
||||
auth <- mkAuth dockerRegistry imageName credentials
|
||||
manifest <- Docker.Image.fetchImageManifest $
|
||||
HockerMeta
|
||||
{ outDir = Nothing
|
||||
, imageLayer = Nothing
|
||||
, ..
|
||||
}
|
||||
either (Lib.exitProgFail . show) (Lib.writeOrPrint out) manifest
|
52
hocker-manifest/README.md
Normal file
52
hocker-manifest/README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Retrieve a docker registry V2 image manifest
|
||||
This utility retrieves a V2 docker image manifest from the docker registry.
|
||||
|
||||
NB: the V2 docker image manifest retrieved from the docker registry is a
|
||||
manifest of the configuration JSON and layer blobs stored by the registry, this
|
||||
is _not_ the same manifest JSON file of the docker image V1.2 _image_
|
||||
specification.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```shell
|
||||
Pull a docker image manifest from the registry
|
||||
|
||||
Usage: hocker-manifest [--registry URI] ([-u|--username BASIC USERNAME]
|
||||
[-p|--password BASIC PASSWORD] |
|
||||
[-t|--token BEARER TOKEN]) [--out STRING] IMAGE-NAME
|
||||
IMAGE-TAG
|
||||
|
||||
Available options:
|
||||
-h,--help Show this help text
|
||||
--registry URI URI of registry, defaults to the Docker Hub registry
|
||||
-u,--username BASIC USERNAME
|
||||
Username part of a basic auth credential
|
||||
-p,--password BASIC PASSWORD
|
||||
Password part of a basic auth credential
|
||||
-t,--token BEARER TOKEN Bearer token retrieved from a call to `docker login`
|
||||
(mutually exclusive to --username and --password)
|
||||
--out STRING Write content to location
|
||||
IMAGE-NAME Docker image name, e.g: 'debian' in debian:jessie
|
||||
IMAGE-TAG Docker image tag identifier, e.g: 'jessie' in
|
||||
debian:jessie
|
||||
```
|
||||
|
||||
```shell
|
||||
hocker-manifest library/debian jessie
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1528,
|
||||
"digest": "sha256:3e83c23dba6a16cd936a3dc044df71b26706c5a4c28181bc3ca4a4af9f5f38ee"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 52584016,
|
||||
"digest": "sha256:10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
220
hocker.cabal
Normal file
220
hocker.cabal
Normal file
@ -0,0 +1,220 @@
|
||||
name: hocker
|
||||
version: 0.1.0.0
|
||||
synopsis: CLI tools and library to interact with a V2 Docker Registry
|
||||
description: Please see README.md
|
||||
homepage: https://github.com/awakenetworks/hocker#readme
|
||||
license: Apache-2.0
|
||||
license-file: LICENSE
|
||||
author: Awake networks
|
||||
maintainer: opensource@awakenetworks.com
|
||||
copyright: 2016 Awake Networks
|
||||
category: Web
|
||||
build-type: Simple
|
||||
extra-source-files: LICENSE
|
||||
cabal-version: >=1.10
|
||||
|
||||
library
|
||||
ghc-options: -Wall
|
||||
hs-source-dirs: src
|
||||
exposed-modules:
|
||||
Lib,
|
||||
Types,
|
||||
Types.URI,
|
||||
Types.Hash,
|
||||
Types.ImageName,
|
||||
Types.ImageTag,
|
||||
Types.Exceptions,
|
||||
Data.Docker.Nix,
|
||||
Data.Docker.Nix.Lib,
|
||||
Data.Docker.Nix.FetchDocker,
|
||||
Data.Docker.Image.Types,
|
||||
Data.Docker.Image.AesonHelpers,
|
||||
Data.Docker.Image.V1.Layer,
|
||||
Data.Docker.Image.V1.Types,
|
||||
Data.Docker.Image.V1_2.Types,
|
||||
Network.Wreq.ErrorHandling,
|
||||
Network.Wreq.Docker.Registry.V2,
|
||||
Network.Wreq.Docker.Image.V1_2,
|
||||
Network.Wreq.Docker.Image.Lib
|
||||
build-depends:
|
||||
base >= 4.9 && < 5,
|
||||
aeson >= 0.9.0.1,
|
||||
ansi-wl-pprint >= 0.6.7.3,
|
||||
lens-aeson >= 1.0,
|
||||
async >= 2.0.0.0 && < 2.2,
|
||||
exceptions >= 0.8,
|
||||
text >= 1.2,
|
||||
directory >= 1.2.2.0,
|
||||
temporary >= 1.2,
|
||||
pureMD5 >= 2.1,
|
||||
vector >= 0.11,
|
||||
optparse-generic >= 1.1.5,
|
||||
optparse-applicative >= 0.13,
|
||||
aeson-pretty >= 0.8,
|
||||
filepath >= 1.4,
|
||||
deepseq >= 1.4,
|
||||
lens >= 4.0,
|
||||
mtl >= 2.2,
|
||||
transformers >= 0.4,
|
||||
lifted-base >= 0.2.3.8,
|
||||
zlib >= 0.6,
|
||||
http-types >= 0.9.1,
|
||||
http-client >= 0.4,
|
||||
tar >= 0.5,
|
||||
network >= 2.6,
|
||||
scientific >= 0.3,
|
||||
pooled-io >= 0.0.2,
|
||||
concurrentoutput >= 0.2,
|
||||
bytestring >= 0.10,
|
||||
uri-bytestring >= 0.2,
|
||||
unordered-containers >= 0.2,
|
||||
containers >= 0.5,
|
||||
memory >= 0.11,
|
||||
turtle >= 1.3,
|
||||
cryptonite >= 0.13,
|
||||
foldl >= 1.0,
|
||||
time >= 1.4,
|
||||
network-uri >= 2.6,
|
||||
wreq >= 0.4,
|
||||
data-fix >= 0.0.3,
|
||||
hnix >= 0.3.4,
|
||||
neat-interpolation >= 0.3.2,
|
||||
optional-args
|
||||
|
||||
default-language: Haskell2010
|
||||
|
||||
executable hocker-image
|
||||
hs-source-dirs: hocker-image
|
||||
main-is: Main.hs
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
|
||||
build-depends:
|
||||
base >= 4.9 && < 5,
|
||||
text >= 1.2,
|
||||
lens >= 4.0,
|
||||
optparse-generic >= 1.1.5,
|
||||
temporary >= 1.2,
|
||||
optparse-applicative >= 0.13,
|
||||
filepath >= 1.4,
|
||||
mtl >= 2.2,
|
||||
network >= 2.6,
|
||||
bytestring >= 0.10,
|
||||
optional-args,
|
||||
hocker
|
||||
|
||||
default-language: Haskell2010
|
||||
|
||||
executable hocker-layer
|
||||
hs-source-dirs: hocker-layer
|
||||
main-is: Main.hs
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
|
||||
build-depends:
|
||||
base >= 4.9 && < 5,
|
||||
text >= 1.2,
|
||||
lens >= 4.0,
|
||||
optparse-generic >= 1.1.5,
|
||||
temporary >= 1.2,
|
||||
optparse-applicative >= 0.13,
|
||||
filepath >= 1.4,
|
||||
mtl >= 2.2,
|
||||
network >= 2.6,
|
||||
bytestring >= 0.10,
|
||||
cryptonite >= 0.13,
|
||||
optional-args,
|
||||
hocker
|
||||
|
||||
default-language: Haskell2010
|
||||
|
||||
executable hocker-config
|
||||
hs-source-dirs: hocker-config
|
||||
main-is: Main.hs
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
|
||||
build-depends:
|
||||
base >= 4.9 && < 5,
|
||||
text >= 1.2,
|
||||
lens >= 4.0,
|
||||
optparse-generic >= 1.1.5,
|
||||
temporary >= 1.2,
|
||||
optparse-applicative >= 0.13,
|
||||
filepath >= 1.4,
|
||||
mtl >= 2.2,
|
||||
network >= 2.6,
|
||||
bytestring >= 0.10,
|
||||
optional-args,
|
||||
hocker
|
||||
|
||||
default-language: Haskell2010
|
||||
|
||||
executable hocker-manifest
|
||||
hs-source-dirs: hocker-manifest
|
||||
main-is: Main.hs
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
|
||||
build-depends:
|
||||
base >= 4.9 && < 5,
|
||||
text >= 1.2,
|
||||
lens >= 4.0,
|
||||
optparse-generic >= 1.1.5,
|
||||
temporary >= 1.2,
|
||||
optparse-applicative >= 0.13,
|
||||
filepath >= 1.4,
|
||||
mtl >= 2.2,
|
||||
network >= 2.6,
|
||||
bytestring >= 0.10,
|
||||
optional-args,
|
||||
hocker
|
||||
|
||||
default-language: Haskell2010
|
||||
|
||||
executable docker2nix
|
||||
hs-source-dirs: docker2nix
|
||||
main-is: Main.hs
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
|
||||
build-depends:
|
||||
base >= 4.9 && < 5,
|
||||
text >= 1.2,
|
||||
lens >= 4.0,
|
||||
optparse-generic >= 1.1.5,
|
||||
temporary >= 1.2,
|
||||
optparse-applicative >= 0.13,
|
||||
filepath >= 1.4,
|
||||
mtl >= 2.2,
|
||||
network >= 2.6,
|
||||
bytestring >= 0.10,
|
||||
hnix >= 0.3.4,
|
||||
data-fix >= 0.0.3,
|
||||
optional-args,
|
||||
hocker
|
||||
|
||||
test-suite hocker-tests
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Tests.Data.Docker.Image.V1,
|
||||
Tests.Data.Docker.Image.V1_2,
|
||||
Tests.Data.Docker.Nix.FetchDocker
|
||||
build-depends:
|
||||
base >= 4.9 && < 5,
|
||||
aeson >= 0.9.0.1,
|
||||
tasty >= 0.11,
|
||||
tasty-hunit >= 0.9,
|
||||
text >= 1.2,
|
||||
network >= 2.6,
|
||||
network-uri >= 2.6,
|
||||
ansi-wl-pprint >= 0.6.7.3,
|
||||
unordered-containers >= 0.2,
|
||||
tasty-quickcheck >= 0.8,
|
||||
tasty-smallcheck >= 0.8,
|
||||
tasty-golden >= 2.3,
|
||||
mtl >= 2.2,
|
||||
bytestring >= 0.10,
|
||||
cryptonite >= 0.13,
|
||||
containers >= 0.5,
|
||||
hocker
|
||||
|
||||
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
default-language: Haskell2010
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/awakenetworks/hocker
|
19
nix/http-client-tls.nix
Normal file
19
nix/http-client-tls.nix
Normal file
@ -0,0 +1,19 @@
|
||||
{ mkDerivation, base, bytestring, case-insensitive, connection
|
||||
, cryptonite, data-default-class, exceptions, hspec, http-client
|
||||
, http-types, memory, network, stdenv, tls, transformers
|
||||
}:
|
||||
mkDerivation {
|
||||
pname = "http-client-tls";
|
||||
version = "0.3.3";
|
||||
sha256 = "0r50h7lhrwmxcmiq5nw1rxnpda3k6mhz4jsd86m56ymai5lnf77c";
|
||||
libraryHaskellDepends = [
|
||||
base bytestring case-insensitive connection cryptonite
|
||||
data-default-class exceptions http-client http-types memory network
|
||||
tls transformers
|
||||
];
|
||||
testHaskellDepends = [ base hspec http-client http-types ];
|
||||
doCheck = false;
|
||||
homepage = "https://github.com/snoyberg/http-client";
|
||||
description = "http-client backend using the connection package and tls library";
|
||||
license = stdenv.lib.licenses.mit;
|
||||
}
|
27
nix/http-client.nix
Normal file
27
nix/http-client.nix
Normal file
@ -0,0 +1,27 @@
|
||||
{ mkDerivation, array, async, base, base64-bytestring
|
||||
, blaze-builder, bytestring, case-insensitive, containers, cookie
|
||||
, deepseq, directory, exceptions, filepath, ghc-prim, hspec
|
||||
, http-types, mime-types, monad-control, network, network-uri
|
||||
, random, stdenv, streaming-commons, text, time, transformers, zlib
|
||||
}:
|
||||
mkDerivation {
|
||||
pname = "http-client";
|
||||
version = "0.5.6.1";
|
||||
sha256 = "1v9bdb8dkhb5g6jl9azk86ig7ia8xh9arr64n7s8r94fp0vl6c1c";
|
||||
libraryHaskellDepends = [
|
||||
array base base64-bytestring blaze-builder bytestring
|
||||
case-insensitive containers cookie deepseq exceptions filepath
|
||||
ghc-prim http-types mime-types network network-uri random
|
||||
streaming-commons text time transformers
|
||||
];
|
||||
testHaskellDepends = [
|
||||
async base base64-bytestring blaze-builder bytestring
|
||||
case-insensitive containers deepseq directory hspec http-types
|
||||
monad-control network network-uri streaming-commons text time
|
||||
transformers zlib
|
||||
];
|
||||
doCheck = false;
|
||||
homepage = "https://github.com/snoyberg/http-client";
|
||||
description = "An HTTP client engine";
|
||||
license = stdenv.lib.licenses.mit;
|
||||
}
|
15
nix/optparse-applicative.nix
Normal file
15
nix/optparse-applicative.nix
Normal file
@ -0,0 +1,15 @@
|
||||
{ mkDerivation, ansi-wl-pprint, base, process, QuickCheck, stdenv
|
||||
, transformers, transformers-compat
|
||||
}:
|
||||
mkDerivation {
|
||||
pname = "optparse-applicative";
|
||||
version = "0.13.0.0";
|
||||
sha256 = "1b0c5fdq8bd070g24vrjrwlq979r8dk8mys6aji9hy1l9pcv3inf";
|
||||
libraryHaskellDepends = [
|
||||
ansi-wl-pprint base process transformers transformers-compat
|
||||
];
|
||||
testHaskellDepends = [ base QuickCheck ];
|
||||
homepage = "https://github.com/pcapriotti/optparse-applicative";
|
||||
description = "Utilities and combinators for parsing command line options";
|
||||
license = stdenv.lib.licenses.bsd3;
|
||||
}
|
14
nix/optparse-generic.nix
Normal file
14
nix/optparse-generic.nix
Normal file
@ -0,0 +1,14 @@
|
||||
{ mkDerivation, base, bytestring, optparse-applicative, semigroups
|
||||
, stdenv, system-filepath, text, time, transformers, void
|
||||
}:
|
||||
mkDerivation {
|
||||
pname = "optparse-generic";
|
||||
version = "1.1.5";
|
||||
sha256 = "1xg6c7h6h8q64gwskh7l4h7qn7w4y0ixf88grgk23xdficgmsyms";
|
||||
libraryHaskellDepends = [
|
||||
base bytestring optparse-applicative semigroups system-filepath
|
||||
text time transformers void
|
||||
];
|
||||
description = "Auto-generate a command-line parser for your datatype";
|
||||
license = stdenv.lib.licenses.bsd3;
|
||||
}
|
11
nix/shell.nix
Normal file
11
nix/shell.nix
Normal file
@ -0,0 +1,11 @@
|
||||
{ ghc }:
|
||||
let
|
||||
config = import ../config.nix;
|
||||
pkgs = import <nixpkgs> { inherit config; };
|
||||
in with pkgs; pkgs.haskell.lib.buildStackProject {
|
||||
inherit ghc;
|
||||
name = "hocker-stack-shell";
|
||||
buildInputs = [
|
||||
zlib cabal-install
|
||||
];
|
||||
}
|
20
nix/turtle.nix
Normal file
20
nix/turtle.nix
Normal file
@ -0,0 +1,20 @@
|
||||
{ mkDerivation, ansi-wl-pprint, async, base, bytestring, clock
|
||||
, directory, doctest, foldl, hostname, managed, optional-args
|
||||
, optparse-applicative, process, stdenv, stm, system-fileio
|
||||
, system-filepath, temporary, text, time, transformers, unix
|
||||
, unix-compat
|
||||
}:
|
||||
mkDerivation {
|
||||
pname = "turtle";
|
||||
version = "1.3.1";
|
||||
sha256 = "0pnxislwq6vzllrlva9la9wisvz54gb74n3nprw145rxszw0ag93";
|
||||
libraryHaskellDepends = [
|
||||
ansi-wl-pprint async base bytestring clock directory foldl hostname
|
||||
managed optional-args optparse-applicative process stm
|
||||
system-fileio system-filepath temporary text time transformers unix
|
||||
unix-compat
|
||||
];
|
||||
testHaskellDepends = [ base doctest ];
|
||||
description = "Shell programming, Haskell-style";
|
||||
license = stdenv.lib.licenses.bsd3;
|
||||
}
|
34
nix/wreq.nix
Normal file
34
nix/wreq.nix
Normal file
@ -0,0 +1,34 @@
|
||||
{ mkDerivation, aeson, aeson-pretty, attoparsec, authenticate-oauth
|
||||
, base, base16-bytestring, base64-bytestring, byteable, bytestring
|
||||
, case-insensitive, containers, cryptohash, directory, doctest
|
||||
, exceptions, filepath, ghc-prim, hashable, http-client
|
||||
, http-client-tls, http-types, HUnit, lens, lens-aeson, mime-types
|
||||
, network-info, psqueues, QuickCheck, snap-core, snap-server
|
||||
, stdenv, template-haskell, temporary, test-framework
|
||||
, test-framework-hunit, test-framework-quickcheck2, text, time
|
||||
, time-locale-compat, transformers, unix-compat
|
||||
, unordered-containers, uuid, vector
|
||||
}:
|
||||
mkDerivation {
|
||||
pname = "wreq";
|
||||
version = "0.5.0.1";
|
||||
sha256 = "138n138rczs5xb7pr25b5a2ajhhxph7vfrh02x71w2alh2xr4akc";
|
||||
libraryHaskellDepends = [
|
||||
aeson attoparsec authenticate-oauth base base16-bytestring byteable
|
||||
bytestring case-insensitive containers cryptohash exceptions
|
||||
ghc-prim hashable http-client http-client-tls http-types lens
|
||||
lens-aeson mime-types psqueues template-haskell text time
|
||||
time-locale-compat unordered-containers
|
||||
];
|
||||
testHaskellDepends = [
|
||||
aeson aeson-pretty base base64-bytestring bytestring
|
||||
case-insensitive containers directory doctest filepath hashable
|
||||
http-client http-types HUnit lens lens-aeson network-info
|
||||
QuickCheck snap-core snap-server temporary test-framework
|
||||
test-framework-hunit test-framework-quickcheck2 text time
|
||||
transformers unix-compat unordered-containers uuid vector
|
||||
];
|
||||
homepage = "http://www.serpentine.com/wreq";
|
||||
description = "An easy-to-use HTTP client library";
|
||||
license = stdenv.lib.licenses.bsd3;
|
||||
}
|
13
release.nix
Normal file
13
release.nix
Normal file
@ -0,0 +1,13 @@
|
||||
let config = import ./config.nix;
|
||||
in
|
||||
{ pkgs ? import <nixpkgs> { inherit config; } }:
|
||||
let
|
||||
darwinPkgs = import <nixpkgs> { inherit config; system = "x86_64-darwin"; };
|
||||
linuxPkgs = import <nixpkgs> { inherit config; system = "x86_64-linux" ; };
|
||||
pkgs = import <nixpkgs> { inherit config; };
|
||||
|
||||
in
|
||||
{ hocker-linux = linuxPkgs.haskellPackages.hocker;
|
||||
hocker-darwin = darwinPkgs.haskellPackages.hocker;
|
||||
hocker = pkgs.haskellPackages.hocker;
|
||||
}
|
18
src/Data/Docker/Image/AesonHelpers.hs
Normal file
18
src/Data/Docker/Image/AesonHelpers.hs
Normal file
@ -0,0 +1,18 @@
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Image.AesonHelpers
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Data.Docker.Image.AesonHelpers where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson.TH
|
||||
|
||||
-- | Produce a default option record with @omitNothingFields@ set to
|
||||
-- True by default.
|
||||
stdOpts :: Options
|
||||
stdOpts = defaultOptions{ omitNothingFields = True }
|
52
src/Data/Docker/Image/Types.hs
Normal file
52
src/Data/Docker/Image/Types.hs
Normal file
@ -0,0 +1,52 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Image.Types
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Data.Docker.Image.Types where
|
||||
|
||||
import Data.ByteString.Lazy.Char8 as C8L
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Types
|
||||
import Types.ImageTag
|
||||
|
||||
-- | Record of all the metadata we need for a docker image; this
|
||||
-- includes the basics like registry location, image repository name,
|
||||
-- image name, image tag, a possible alternative image name, and
|
||||
-- finally the full manifest JSON for the docker image from which a
|
||||
-- complete image can be constructed (supplying the config JSON and
|
||||
-- references to all of the layers).
|
||||
data HockerImageMeta = HockerImageMeta
|
||||
{ -- | Docker image repo, the first part of a repository+name
|
||||
-- separated by a "/"; e.g: library/debian.
|
||||
imageRepo :: RepoNamePart
|
||||
-- | Docker image name, the second part of a repository+name
|
||||
-- separated by a "/"; e.g: library/debian.
|
||||
, imageName :: ImageNamePart
|
||||
-- | Docker image tag
|
||||
, imageTag :: ImageTag
|
||||
|
||||
-- | A docker image manifest JSON blob as usually fetched from a
|
||||
-- docker registry.
|
||||
--
|
||||
-- TODO: switch this to the JSON AST type?
|
||||
, manifestJSON :: C8L.ByteString
|
||||
-- | The URI (even if the default public registry) of the docker
|
||||
-- registry.
|
||||
, dockerRegistry :: RegistryURI
|
||||
-- | An alternative name for the docker image provided in the
|
||||
-- output Nix `fetchdocker` derivation expressions. Not replacing
|
||||
-- @imageName@ but providing a method for declaring up-front a
|
||||
-- possibly cleaner or more intuitive name for use within Nix.
|
||||
, altImageName :: Maybe T.Text
|
||||
} deriving (Show)
|
249
src/Data/Docker/Image/V1/Layer.hs
Normal file
249
src/Data/Docker/Image/V1/Layer.hs
Normal file
@ -0,0 +1,249 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Image.V1.Layer
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
--
|
||||
-- Many of these functions are named after their equivalent functions
|
||||
-- in the docker Golang source code.
|
||||
--
|
||||
-- <https://github.com/docker/docker/blob/6e8a2cd29113896acfc3f97a43dd27f751f2f519/layer/layer.go layer.go>
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Data.Docker.Image.V1.Layer where
|
||||
|
||||
import Control.Monad
|
||||
import Data.Aeson
|
||||
import qualified Data.ByteString.Lazy.Char8 as CL8
|
||||
import Data.Coerce
|
||||
import Data.Docker.Image.V1.Types
|
||||
import Data.Foldable
|
||||
import qualified Data.HashMap.Strict as H
|
||||
import Data.Monoid
|
||||
import Data.Sequence as Seq
|
||||
import Data.Sequence.Lens
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Lib
|
||||
|
||||
type Parent = ChainID
|
||||
type TopLayerJSON = Data.Aeson.Object
|
||||
|
||||
-- | Produce a @ChainID@ using a sequence of layer @DiffIDs@.
|
||||
--
|
||||
-- <https://github.com/docker/docker/blob/6e8a2cd29113896acfc3f97a43dd27f751f2f519/layer/layer.go#L239 layer.CreateChainID>
|
||||
createChainID :: Seq DiffID -- ^ A sequence of layer @DiffID@s, (usually) fetched from the image's config JSON.
|
||||
-> Maybe ChainID
|
||||
createChainID = createChainIDFromParent Nothing
|
||||
|
||||
-- | Produce a @ChainID@ given the @ChainID@ of a parent layer and a
|
||||
-- sequence of layer @DiffIDs@.
|
||||
--
|
||||
-- <https://github.com/docker/docker/blob/6e8a2cd29113896acfc3f97a43dd27f751f2f519/layer/layer.go#L243 layer.createChainIDFromParent>
|
||||
createChainIDFromParent :: Maybe Parent -- ^ Previous (parent) @ChainID@ in the sequence used to produce the next @ChainID@.
|
||||
-> Seq DiffID -- ^ A sequence of layer @DiffID@s, (usually) fetched from the image's config JSON.
|
||||
-> Maybe ChainID
|
||||
createChainIDFromParent parent (Seq.viewl -> EmptyL) = parent
|
||||
createChainIDFromParent parent (Seq.viewl -> h :< rest) =
|
||||
createChainIDFromParent (maybe root layer parent) rest
|
||||
where
|
||||
root = Just $ coerce h
|
||||
layer = Just . flip chainDigest h
|
||||
|
||||
createChainIDFromParent parent _ = parent
|
||||
|
||||
-- | Produce a @ChainID@ given a parent @ChainID@ and a layer
|
||||
-- @DiffID@.
|
||||
chainDigest :: Parent -- ^ Parent @ChainID@ used to produce a child @ChainID@.
|
||||
-> DiffID -- ^ Layer @DiffID@.
|
||||
-> ChainID
|
||||
chainDigest (show -> c) (show -> d) = ChainID .
|
||||
Lib.sha256 . CL8.pack $ concat [c, " ", d]
|
||||
|
||||
-- | Produce a sequence of @ChainID@s from a sequence of layer
|
||||
-- @DiffID@s.
|
||||
--
|
||||
-- <https://github.com/docker/docker/blob/b826bebda0cff2cc2d3083b954c810d2889eefe5/image/tarexport/save.go#L242 save.saveImage>
|
||||
chainIDSequence :: Seq DiffID
|
||||
-> Seq (Maybe ChainID)
|
||||
chainIDSequence diffIDSeq = mapWithIndex chainIDSlice diffIDSeq
|
||||
where
|
||||
chainIDSlice (succ -> i) _ =
|
||||
createChainID $ seqOf (slicedTo i) diffIDSeq
|
||||
|
||||
-- | Produce a sequence of unwrapped Just's from a sequence of
|
||||
-- Maybe's.
|
||||
squishMaybe :: MonadPlus m => m (Maybe a) -> m a
|
||||
squishMaybe = join . fmap adapt
|
||||
where
|
||||
adapt Nothing = mzero
|
||||
adapt (Just x) = return x
|
||||
|
||||
-- | Produce layer content ID hashes given an empty JSON config with
|
||||
-- the layer's @ChainID@ injected as the value of the `layer_id` key
|
||||
-- and, if not the base layer, the previous @ContentID@ injected as
|
||||
-- the value of the `parent` key.
|
||||
--
|
||||
-- The JSON that is encoded *must* be in the canonical format
|
||||
-- specified by Docker, please see @Lib.encodeCanonical@ for a
|
||||
-- convenience function to encode an @Aeson.Value@ satisfying those
|
||||
-- rules.
|
||||
contentIDSequence :: Seq ChainID -- ^ A sequence of @ChainID@s, please see @chainIDSequence@.
|
||||
-> TopLayerJSON -- ^ Config JSON paired with the top-most layer of the image.
|
||||
-> Seq ContentID
|
||||
contentIDSequence cids fj = foldl' (contentIDFold fj $ Seq.length cids) Seq.empty cids
|
||||
|
||||
-- | A folding function given to @foldl'@. This function computes the
|
||||
-- @ContentID@'s for each layer using the last computed @ContentID@ as
|
||||
-- the parent @ContentID@ for each iteration.
|
||||
--
|
||||
-- The first two arguments are closed over before being fed to
|
||||
-- @foldl'@ producing a partial function that satisfies @foldl'@'s
|
||||
-- first argument type signature.
|
||||
contentIDFold :: TopLayerJSON -- ^ Config JSON to be hashed with the top-most layer of the image.
|
||||
-> Int -- ^ Length of the @ChainID@ sequence being folded over.
|
||||
-> Seq ContentID -- ^ The sequence of @ContentID@s accumulated.
|
||||
-> ChainID -- ^ The @ChainID@ for producing a @ContentID@.
|
||||
-> Seq ContentID
|
||||
contentIDFold _ _ acc@(Seq.viewr -> EmptyR) chainid =
|
||||
acc |> hashContent Nothing chainid emptyLayerJSON
|
||||
contentIDFold topLayerJSON ln acc@(Seq.viewr -> _ :> parent) chainid =
|
||||
acc |> hashedContentID
|
||||
where
|
||||
-- Check to see if we're at the end of the sequence we're folding
|
||||
-- over, if so then hash the content using the top-layer config
|
||||
-- JSON instead of the empty JSON
|
||||
hashedContentID =
|
||||
if ln == (succ $ Seq.length acc)
|
||||
then hashContent (Just parent) chainid topLayerJSON
|
||||
else hashContent (Just parent) chainid emptyLayerJSON
|
||||
|
||||
contentIDFold _ _ acc chainid =
|
||||
acc |> hashContent Nothing chainid emptyLayerJSON
|
||||
|
||||
-- | Produce a @ContentID@, given a parent and a @ChainID@, builds the
|
||||
-- empty JSON object with those two values and encodes it following
|
||||
-- the canonical JSON rules.
|
||||
hashContent :: Maybe ContentID -- ^ Parent @ContentID@ for injection into the hashing JSON.
|
||||
-> ChainID -- ^ @ChainID@ to be hashed with the hashing JSON.
|
||||
-> Data.Aeson.Object -- ^ Aeson AST to be canonically encoded; this can be either the ephemeral JSON or the config JSON.
|
||||
-> ContentID
|
||||
hashContent p c jsn = mk $ ephemeralHashableLayerJSON p c jsn
|
||||
where
|
||||
mk = ContentID . Lib.sha256 . Lib.encodeCanonical
|
||||
|
||||
-- | @emptyLayerJSON@ produces "empty" JSON for use in layer content
|
||||
-- hashing.
|
||||
--
|
||||
-- The Aeson instances for @ContentID@, @DiffID@, and @ChainID@ will
|
||||
-- correctly output a hex serialization of the SHA256 digest and
|
||||
-- prefix it with "sha256:", which is necessary to correctly hash the
|
||||
-- layer config in the same way that Docker's Golang code does it.
|
||||
--
|
||||
-- NB: I've manually assembled this in the "canonical order" it needs
|
||||
-- to be in, in order to correctly hash the JSON string. There is also
|
||||
-- a custom Aeson pretty printing function that serializes ADTs into
|
||||
-- the canonical form and should make this function moot once an
|
||||
-- appropriate ADT is in place.
|
||||
--
|
||||
-- TODO: codify this as an ADT to get rid of this manual construction
|
||||
-- and make things clearer. For now, the manually constructed one is
|
||||
-- fine (to get things working).
|
||||
emptyLayerJSON :: Data.Aeson.Object
|
||||
emptyLayerJSON = H.fromList
|
||||
[ "container_config" .= object
|
||||
[ "Hostname" .= ("" :: String)
|
||||
, "Domainname" .= ("" :: String) -- NB: this one isn't cased like the others :(
|
||||
, "User" .= ("" :: String)
|
||||
, "AttachStdin" .= False
|
||||
, "AttachStdout" .= False
|
||||
, "AttachStderr" .= False
|
||||
, "Tty" .= False
|
||||
, "OpenStdin" .= False
|
||||
, "StdinOnce" .= False
|
||||
, "Env" .= (Nothing :: Maybe String)
|
||||
, "Cmd" .= (Nothing :: Maybe String)
|
||||
, "Image" .= ("" :: String)
|
||||
|
||||
-- This is a object with significant keys and empty values
|
||||
-- (don't ask me why)
|
||||
, "Volumes" .= (Nothing :: Maybe Data.Aeson.Value)
|
||||
, "WorkingDir" .= ("" :: String)
|
||||
, "Entrypoint" .= (Nothing :: Maybe String)
|
||||
, "OnBuild" .= (Nothing :: Maybe String)
|
||||
, "Labels" .= (Nothing :: Maybe [String])
|
||||
]
|
||||
|
||||
-- This is the "canonical" empty timestamp
|
||||
, "created" .= emptyTimeStamp
|
||||
]
|
||||
|
||||
-- | Produce an "empty" JSON object given a parent and a
|
||||
-- @ChainID@. This is used internally to produce the @ContentID@ hash
|
||||
-- for a given layer.
|
||||
ephemeralHashableLayerJSON :: Maybe ContentID -- ^ Parent @ContentID@, if Nothing, will not be included in the Aeson AST.
|
||||
-> ChainID -- ^ @ChainID@ of the layer we're producing the @ContentID@ for.
|
||||
-> Data.Aeson.Object -- ^ Aeson AST we want to inject the parent @ContentID@ and layer @ChainID@ into.
|
||||
-> Data.Aeson.Value
|
||||
ephemeralHashableLayerJSON parent layerid layerJSON =
|
||||
Object $ layerJSON `H.union` H.fromList
|
||||
([ "layer_id" .= layerid ] <> (maybeSingletonParent parent))
|
||||
|
||||
-- | Produce a layer JSON object given a parent, a @ContentID@, and an
|
||||
-- Aeson Value Object. This function is different from
|
||||
-- @ephemeralHashableLayerJSON@ in that its output is (later on)
|
||||
-- written to the filesystem alongside the `layer.tar` file within the
|
||||
-- directory named after the @ContentID@ hash.
|
||||
permanentLayerJSON :: Maybe ContentID
|
||||
-> ContentID
|
||||
-> Data.Aeson.Object
|
||||
-> Data.Aeson.Value
|
||||
permanentLayerJSON parent layerContentId layerJSON =
|
||||
Object $ layerJSON `H.union` H.fromList
|
||||
([ "id" .= (mkPermHash layerContentId) ] <> maybeSingletonParent (mkPermHash <$> parent))
|
||||
where
|
||||
mkPermHash = Lib.stripHashId . T.pack . show
|
||||
|
||||
-- TODO: this should be parsed into an ADT, transformed algebraically
|
||||
-- into what it should be, then re-encoded; instead of performing
|
||||
-- Map-based operations on the AST. This was the quicker option though
|
||||
-- for now; need to get something working first.
|
||||
imageConfig2LayerConfig :: Data.Aeson.Object
|
||||
-> Data.Aeson.Object
|
||||
imageConfig2LayerConfig = H.filterWithKey keyWhitelist
|
||||
where
|
||||
keyWhitelist k _ = k `elem`
|
||||
[ "container"
|
||||
, "container_config"
|
||||
, "docker_version"
|
||||
, "config"
|
||||
, "architecture"
|
||||
, "os"
|
||||
]
|
||||
|
||||
-- | Produce mempty if the parent is Nothing; if the parent is @Just
|
||||
-- ContentID@ then it returns a singleton list with the expected
|
||||
-- @Data.Aeson.Pair@ construction for the empty layer JSON.
|
||||
--
|
||||
-- The input argument is parameterized because the permanent JSON
|
||||
-- config objects store hashes with the "sha256:" prefix stripped, but
|
||||
-- the ephemeral JSON objects used to produce the Content ID hashes
|
||||
-- want the "sha256:" prefix to be present!
|
||||
maybeSingletonParent :: ToJSON a
|
||||
=> Maybe a
|
||||
-> [(T.Text, Data.Aeson.Value)]
|
||||
maybeSingletonParent = maybe mempty (singletonList . ("parent" .=))
|
||||
where
|
||||
-- Alternatively - singleton v = [v]
|
||||
singletonList = (: [])
|
||||
|
||||
-- | Produce the string "0001-01-01T00:00:00Z".
|
||||
emptyTimeStamp :: String
|
||||
emptyTimeStamp = "0001-01-01T00:00:00Z"
|
108
src/Data/Docker/Image/V1/Types.hs
Normal file
108
src/Data/Docker/Image/V1/Types.hs
Normal file
@ -0,0 +1,108 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Image.V1.Types
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Data.Docker.Image.V1.Types where
|
||||
|
||||
import qualified Crypto.Hash as Hash
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Types
|
||||
import qualified Data.ByteArray as BA
|
||||
import qualified Data.ByteArray.Encoding as BA
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
|
||||
-- | Attempt to parse a @C8.ByteString@ into a @Hash.Digest
|
||||
-- Hash.SHA256@.
|
||||
--
|
||||
-- A @Digest@ in Docker Golang-code parlance is the string hexadecimal
|
||||
-- representation of a hashing function's digest with the hashing
|
||||
-- function identifier prefixed onto the string. Right now they only
|
||||
-- use SHA256 everywhere and also don't really do anything to
|
||||
-- parameterize it.
|
||||
--
|
||||
-- There is a custom Show instance for this newtype to output a string
|
||||
-- representation of the digest prefixed by its hashing function
|
||||
-- identifier.
|
||||
toDigest :: C8.ByteString -> Maybe (Hash.Digest Hash.SHA256)
|
||||
toDigest = from . C8.break (== ':')
|
||||
where
|
||||
from ("sha256", r) = either (const Nothing) Hash.digestFromByteString . toBytes $ C8.tail r
|
||||
from (_, _) = Nothing
|
||||
|
||||
toBytes :: C8.ByteString -> Either String BA.Bytes
|
||||
toBytes = BA.convertFromBase BA.Base16
|
||||
|
||||
-- | A special kind of SHA256 hash digest identifying a layer by its
|
||||
-- *content*. This value is a hash of an empty, canonicalized JSON
|
||||
-- string with a "layer_id" (which is actually the layer's @ChainID@)
|
||||
-- and possibly a parent ID (which is the previous-layer-in-sequence
|
||||
-- @ContentID@).
|
||||
newtype ContentID = ContentID (Hash.Digest Hash.SHA256)
|
||||
deriving (Eq)
|
||||
|
||||
-- | A special kind of SHA256 digest identifying a specific sequence
|
||||
-- of layers.
|
||||
--
|
||||
-- <https://github.com/docker/docker/blob/6e8a2cd29113896acfc3f97a43dd27f751f2f519/layer/layer.go#L60 layer.ChainID>
|
||||
newtype ChainID = ChainID (Hash.Digest Hash.SHA256)
|
||||
deriving (Eq)
|
||||
|
||||
-- | A special kind of a SHA256 digest identifying a layer by the
|
||||
-- sha256 sum of the uncompressed layer tarball. "Diff" in this
|
||||
-- context refers to the root filesystem contents of the tarball
|
||||
-- identified by @DiffID@ representing the difference from the
|
||||
-- previous layer.
|
||||
--
|
||||
-- <https://github.com/docker/docker/blob/6e8a2cd29113896acfc3f97a43dd27f751f2f519/layer/layer.go#L68 layer.DiffID>
|
||||
newtype DiffID = DiffID (Hash.Digest Hash.SHA256)
|
||||
deriving (Eq)
|
||||
|
||||
-- | Show a hexadecimal encoded SHA256 hash digest and prefix
|
||||
-- "sha256:" to it.
|
||||
showSHA :: Hash.Digest Hash.SHA256 -> String
|
||||
showSHA = ("sha256:" ++) . show
|
||||
|
||||
instance Show ContentID where
|
||||
show (ContentID d) = showSHA d
|
||||
instance Show ChainID where
|
||||
show (ChainID d) = showSHA d
|
||||
instance Show DiffID where
|
||||
show (DiffID d) = showSHA d
|
||||
|
||||
instance ToJSON ContentID where
|
||||
toJSON v = String . T.pack $ show v
|
||||
instance ToJSON ChainID where
|
||||
toJSON v = String . T.pack $ show v
|
||||
instance ToJSON DiffID where
|
||||
toJSON v = String . T.pack $ show v
|
||||
|
||||
instance FromJSON ContentID where
|
||||
parseJSON o@(String v) =
|
||||
case toDigest $ encodeUtf8 v of
|
||||
Just v' -> return $ ContentID v'
|
||||
Nothing -> typeMismatch "SHA256 Digest" o
|
||||
parseJSON inv = typeMismatch "SHA256 Digest" inv
|
||||
instance FromJSON ChainID where
|
||||
parseJSON o@(String v) =
|
||||
case toDigest $ encodeUtf8 v of
|
||||
Just v' -> return $ ChainID v'
|
||||
Nothing -> typeMismatch "SHA256 Digest" o
|
||||
parseJSON inv = typeMismatch "SHA256 Digest" inv
|
||||
instance FromJSON DiffID where
|
||||
parseJSON o@(String v) =
|
||||
case toDigest $ encodeUtf8 v of
|
||||
Just v' -> return $ DiffID v'
|
||||
Nothing -> typeMismatch "SHA256 Digest" o
|
||||
parseJSON inv = typeMismatch "SHA256 Digest" inv
|
108
src/Data/Docker/Image/V1_2/Types.hs
Normal file
108
src/Data/Docker/Image/V1_2/Types.hs
Normal file
@ -0,0 +1,108 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PartialTypeSignatures #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Image.V1_2.Types
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
--
|
||||
-- The types in this module are used to describe two specific pieces
|
||||
-- of JSON within the v1.2 Docker Image spec: @manifest.json@ and
|
||||
-- @repositories@.
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Data.Docker.Image.V1_2.Types where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson.TH
|
||||
import Data.Aeson.Types
|
||||
import Data.HashMap.Strict as H
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Data.Docker.Image.AesonHelpers
|
||||
import Lib
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
--
|
||||
|
||||
-- Pretty-printed example of the `manifest.json` file.
|
||||
{-
|
||||
[
|
||||
{
|
||||
"Config": "3e83c23dba6a16cd936a3dc044df71b26706c5a4c28181bc3ca4a4af9f5f38ee.json",
|
||||
"Layers": [
|
||||
"10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9.tar"
|
||||
],
|
||||
"RepoTags": [
|
||||
"library/debian:jessie"
|
||||
]
|
||||
}
|
||||
]
|
||||
-}
|
||||
|
||||
-- Pretty-printed example of the `repositories` json file.
|
||||
{-
|
||||
{
|
||||
"library/debian": {
|
||||
"jessie": "10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9"
|
||||
}
|
||||
}
|
||||
-}
|
||||
|
||||
-- | A 'Text' representing a layer hash digest sourced from a docker
|
||||
-- image's config JSON (different from the image's manifest JSON).
|
||||
type RefLayer = T.Text
|
||||
|
||||
-- | A 'String' representing the full repository tag, e.g: @library/debian@.
|
||||
type RepoTag = String
|
||||
|
||||
-- | Represents a v1.2 Docker Image manifest.
|
||||
data ImageManifest = ImageManifest
|
||||
{ -- | 'FilePath' within the image archive of the image's config
|
||||
-- JSON
|
||||
config :: FilePath
|
||||
-- | List of image repository tags
|
||||
, repoTags :: [T.Text]
|
||||
-- | List of layers within the image archive named by their hash
|
||||
-- digest and with the tar extension appended
|
||||
, layers :: [FilePath]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
-- | Represents an object of 'ImageRepo's. The repository names are the
|
||||
-- top-level keys and their value is an object who's keys are the tags
|
||||
-- of the repository with the hash-value of the layer that tag
|
||||
-- references.
|
||||
data ImageRepositories = ImageRepositories [ImageRepo]
|
||||
deriving (Show, Eq)
|
||||
|
||||
data ImageRepo = ImageRepo
|
||||
{ -- | Repository tag
|
||||
repo :: T.Text
|
||||
-- | 'HashMap' of tags to the top-most layer associated with that tag
|
||||
, tags :: H.HashMap T.Text T.Text
|
||||
} deriving (Show, Eq)
|
||||
|
||||
$(deriveJSON stdOpts{ fieldLabelModifier = upperFirst } ''ImageManifest)
|
||||
|
||||
instance ToJSON ImageRepositories where
|
||||
toJSON (ImageRepositories r) =
|
||||
Object . H.unions $ [i | o@(Object i) <- (fmap toJSON r), isObject o]
|
||||
where
|
||||
isObject (Object _) = True
|
||||
isObject _ = False
|
||||
|
||||
instance ToJSON ImageRepo where
|
||||
toJSON (ImageRepo r t) = object [ r .= toJSON t ]
|
||||
|
||||
instance FromJSON ImageRepositories where
|
||||
parseJSON (Object v) = ImageRepositories <$> (mapM buildRepo $ H.toList v)
|
||||
where
|
||||
buildRepo (k,v') = ImageRepo k <$> parseJSON v'
|
||||
parseJSON v = typeMismatch "ImageRepositories" v
|
22
src/Data/Docker/Nix.hs
Normal file
22
src/Data/Docker/Nix.hs
Normal file
@ -0,0 +1,22 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Nix
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
--
|
||||
-- This module only re-exports Nix modules providing Docker-specific
|
||||
-- functionality as it pertains to Nix.
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Data.Docker.Nix
|
||||
( -- * Generating `fetchdocker` Nix Derivation Expressions
|
||||
module Data.Docker.Nix.FetchDocker
|
||||
) where
|
||||
|
||||
import Data.Docker.Nix.FetchDocker
|
224
src/Data/Docker/Nix/FetchDocker.hs
Normal file
224
src/Data/Docker/Nix/FetchDocker.hs
Normal file
@ -0,0 +1,224 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Nix.FetchDocker
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Data.Docker.Nix.FetchDocker where
|
||||
|
||||
import Control.Lens
|
||||
import Control.Monad
|
||||
import Control.Monad.Except as Except
|
||||
import Data.Aeson.Lens
|
||||
import qualified Data.Bifunctor as Bifunctor
|
||||
import Data.Coerce
|
||||
import Data.Fix
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeUtf8')
|
||||
import Data.Text.Encoding.Error
|
||||
import Nix.Expr
|
||||
import URI.ByteString
|
||||
|
||||
import Data.Docker.Image.Types
|
||||
import Data.Docker.Nix.Lib as Nix.Lib
|
||||
import Lib
|
||||
import Network.Wreq.Docker.Registry.V2 (pluckLayersFrom)
|
||||
import Types
|
||||
import Types.Exceptions
|
||||
import Types.ImageTag
|
||||
|
||||
{- Example output of the pretty-printed, generated Nix expression AST.
|
||||
{
|
||||
config.docker.images.debian = pkgs.fetchdocker {
|
||||
name = "debian";
|
||||
registry = "https://registry-1.docker.io/v2/";
|
||||
repository = "library";
|
||||
imageName = "debian";
|
||||
tag = "latest";
|
||||
imageConfig = pkgs.fetchDockerConfig {
|
||||
inherit registry repository imageName tag;
|
||||
sha256 = "1viqbygsz9547jy830f2lk2hcrxjf7gl9h1xda9ws5kap8yw50ry";
|
||||
};
|
||||
imageLayers = let
|
||||
layer0 = pkgs.fetchDockerLayer {
|
||||
inherit registry repository imageName tag;
|
||||
layerDigest = "10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9";
|
||||
sha256 = "1fcmx3aklbr24qsjhm6cvmhqhmrxr6xlpq75mzrk0dj2gz36g8hh";
|
||||
};
|
||||
in [ layer0 ];
|
||||
};
|
||||
}
|
||||
-}
|
||||
|
||||
-- | @fetchdocker@ derivation name.
|
||||
constFetchdocker :: T.Text
|
||||
constFetchdocker = "fetchdocker"
|
||||
|
||||
-- | @fetchDockerConfig@ derivation name.
|
||||
constFetchDockerConfig :: T.Text
|
||||
constFetchDockerConfig = "fetchDockerConfig"
|
||||
|
||||
-- | @fetchDockerLayer@ derivation name.
|
||||
constFetchDockerLayer :: T.Text
|
||||
constFetchDockerLayer = "fetchDockerLayer"
|
||||
|
||||
-- | Generate a Nix expression AST from a @HockerImageMeta@
|
||||
-- record. This function crucially checks that the supplied manifest
|
||||
-- JSON contains a key in the top-level object describing what version
|
||||
-- of the manifest we have.
|
||||
generate :: HockerImageMeta -> IO (Either HockerException NExpr)
|
||||
generate dim@HockerImageMeta{..} = runExceptT $
|
||||
case (manifestJSON ^? key "schemaVersion" . _Integer) of
|
||||
Just 2 -> do
|
||||
nixhash <- Lib.findExec "nix-hash"
|
||||
configDigest <- Nix.Lib.toBase32Nix nixhash . Base16Digest $ pluckedConfigDigest
|
||||
layerDigests <- forM pluckedLayerDigests $ \d16 ->
|
||||
(Base16Digest d16,) <$> (Nix.Lib.toBase32Nix nixhash $ Base16Digest d16)
|
||||
|
||||
ExceptT (pure $ generateFetchDockerExpr dim configDigest layerDigests)
|
||||
Just v ->
|
||||
throwError $ HockerException ("Expected: 2 but got: " <> (show v)) Nothing Nothing
|
||||
Nothing ->
|
||||
throwError $ HockerException "No key 'schemaVersion' in JSON object" Nothing Nothing
|
||||
|
||||
where
|
||||
-- 'stripHashId' is necessary because digests in the manifest are
|
||||
-- prefixed by the hash algorithm used to generate them
|
||||
pluckedConfigDigest = Lib.stripHashId $ manifestJSON ^. key "config" . key "digest" . _String
|
||||
pluckedLayerDigests = Lib.stripHashId <$> pluckLayersFrom manifestJSON
|
||||
|
||||
|
||||
|
||||
{-| Generate a top-level Nix Expression AST from a 'HockerImageMeta'
|
||||
record, a config digest, and a list of layer digests.
|
||||
|
||||
The generated AST, pretty-printed, may look similar to the following:
|
||||
@
|
||||
{
|
||||
config.docker.images.debian = pkgs.fetchdocker {
|
||||
name = "debian";
|
||||
registry = "https://registry-1.docker.io/v2/";
|
||||
repository = "library";
|
||||
imageName = "debian";
|
||||
tag = "latest";
|
||||
imageConfig = pkgs.fetchDockerConfig {
|
||||
inherit registry repository imageName tag;
|
||||
sha256 = "1viqbygsz9547jy830f2lk2hcrxjf7gl9h1xda9ws5kap8yw50ry";
|
||||
};
|
||||
imageLayers = let
|
||||
layer0 = pkgs.fetchDockerLayer {
|
||||
inherit registry repository imageName tag;
|
||||
layerDigest = "10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9";
|
||||
sha256 = "1fcmx3aklbr24qsjhm6cvmhqhmrxr6xlpq75mzrk0dj2gz36g8hh";
|
||||
};
|
||||
in [ layer0 ];
|
||||
};
|
||||
}
|
||||
@
|
||||
-}
|
||||
generateFetchDockerExpr :: HockerImageMeta -> ConfigDigest -> [(Base16Digest, Base32Digest)] -> Either HockerException NExpr
|
||||
generateFetchDockerExpr dim@HockerImageMeta{..} configDigest layerDigests = do
|
||||
let fetchconfig = mkFetchDockerConfig commonInherits configDigest
|
||||
fetchlayers =
|
||||
mkLets
|
||||
(mkFetchDockerLayers commonInherits layerDigests)
|
||||
(mkList $ fmap genLayerId [0..(Prelude.length layerDigests)-1])
|
||||
|
||||
fetchDockerExpr <- mkFetchDocker dim fetchconfig fetchlayers
|
||||
|
||||
pure (Fix $ NSet [ dockerImgExpr fetchDockerExpr ])
|
||||
|
||||
where
|
||||
dockerImgExpr fDockerExpr = NamedVar imgSelector fDockerExpr
|
||||
genLayerId i = mkSym . T.pack $ "layer" <> show i
|
||||
imgSelector =
|
||||
[ StaticKey "config"
|
||||
, StaticKey "docker"
|
||||
, StaticKey "images"
|
||||
, StaticKey imageName
|
||||
]
|
||||
commonInherits = inherit
|
||||
[ StaticKey "registry"
|
||||
, StaticKey "repository"
|
||||
, StaticKey "imageName"
|
||||
, StaticKey "tag"
|
||||
]
|
||||
|
||||
-- | Generate a @pkgs.fetchdocker { ... }@ function call and argument
|
||||
-- attribute set. Please see 'generateNixExprs' documentation for an
|
||||
-- example of full output.
|
||||
mkFetchDocker :: HockerImageMeta -> NExpr -> NExpr -> Either HockerException NExpr
|
||||
mkFetchDocker HockerImageMeta{..} fetchconfig fetchlayers = do
|
||||
registry <- Bifunctor.first mkHockerException serializedRegistry
|
||||
pure
|
||||
(mkApp (mkPkgsAttrSelector constFetchdocker)
|
||||
(attrsE
|
||||
[ ("name", mkStr $ fromMaybe imageName altImageName)
|
||||
, ("registry", mkStr registry)
|
||||
, ("repository", mkStr imageRepo)
|
||||
, ("imageName", mkStr imageName)
|
||||
, ("tag", mkStr (T.pack $ coerce imageTag))
|
||||
, ("imageConfig", fetchconfig)
|
||||
, ("imageLayers", fetchlayers)
|
||||
]))
|
||||
where
|
||||
serializedRegistry = decodeUtf8' (serializeURIRef' dockerRegistry)
|
||||
mkHockerException (DecodeError err char) =
|
||||
HockerException (err <> " " <> (show char)) Nothing Nothing
|
||||
mkHockerException err =
|
||||
HockerException (show err) Nothing Nothing
|
||||
|
||||
|
||||
-- | Generate a @pkgs.fetchDockerConfig { ... }@ function call and
|
||||
-- argument attrset. This function takes an argument for a list of
|
||||
-- static keys to inherit from the parent attribute set; it helps
|
||||
-- reduce the noise in the output expression.
|
||||
mkFetchDockerConfig :: Binding NExpr -> Base32Digest -> NExpr
|
||||
mkFetchDockerConfig inherits (Base32Digest digest) =
|
||||
mkApp (mkPkgsAttrSelector constFetchDockerConfig)
|
||||
(Fix $ NSet [ inherits, "sha256" $= (mkStr digest) ])
|
||||
|
||||
-- | Generate a list of Nix expression ASTs representing
|
||||
-- @pkgs.fetchDockerLayer { ... }@ function calls. This function takes
|
||||
-- an argument for a list of static keys to inherit from the parent
|
||||
-- attribute set; it helps reduce the noise in the output expression.
|
||||
--
|
||||
-- NB: the hash digest tuple in the second argument is the base16
|
||||
-- encoded hash digest plucked from the image's manifest JSON and a
|
||||
-- @nix-hash@ base32 encoded copy.
|
||||
--
|
||||
-- This is necessary because fixed output derivations require a
|
||||
-- pre-computed hash (which we have, thanks to the manifest) and the
|
||||
-- hash must be base32 encoded using @nix-hash@'s own base32
|
||||
-- encoding. The base16 encoded hash digest is needed intact in order
|
||||
-- for the @pkgs.fetchDockerLayer@ builder script (which calls the
|
||||
-- @hocker-layer@ utility) to download the layer from a docker
|
||||
-- registry.
|
||||
mkFetchDockerLayers :: Binding NExpr -> [(Base16Digest, Base32Digest)] -> [Binding NExpr]
|
||||
mkFetchDockerLayers inherits layerDigests =
|
||||
fmap mkFetchLayer $ Prelude.zip [0..(Prelude.length layerDigests)] layerDigests
|
||||
where
|
||||
mkLayerId i = T.pack $ "layer" <> show i
|
||||
mkFetchLayer (i, ((Base16Digest d16), (Base32Digest d32))) =
|
||||
(mkLayerId i) $= mkApp (mkPkgsAttrSelector constFetchDockerLayer)
|
||||
(Fix $ NSet
|
||||
[ inherits
|
||||
, "layerDigest" $= (mkStr d16) -- Required in order to perform a registry request
|
||||
, "sha256" $= (mkStr d32) -- Required by Nix for fixed output derivations
|
||||
])
|
||||
|
||||
-- | Generate a selector for an attribute within the @pkgs@ set; i.e
|
||||
-- @pkgs.fetchDockerLayer@.
|
||||
mkPkgsAttrSelector :: T.Text -> NExpr
|
||||
mkPkgsAttrSelector k = Fix $ NSelect (mkSym "pkgs") [StaticKey k] Nothing
|
57
src/Data/Docker/Nix/Lib.hs
Normal file
57
src/Data/Docker/Nix/Lib.hs
Normal file
@ -0,0 +1,57 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Nix.Lib
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Data.Docker.Nix.Lib where
|
||||
|
||||
import Control.Foldl as Foldl
|
||||
import Turtle
|
||||
import Control.Monad.Except as Except
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Types
|
||||
import Types.Exceptions
|
||||
|
||||
-- | Convert a @Base16Digest@ to a @Base32Digest@ using the supplied
|
||||
-- `nix-hash` utility.
|
||||
--
|
||||
-- NB: Nix implements its own custom base32 encoding function for
|
||||
-- hashes that is not compatible with other more standard and native
|
||||
-- implementations in Haskell. I opted to call out to `nix-hash`
|
||||
-- instead of re-implementing their algorithm here in Haskell because
|
||||
-- it's non-standard and may change, creating a maintenance headache
|
||||
-- and "surprise" behavior for users.
|
||||
toBase32Nix :: (MonadIO m, Except.MonadError HockerException m)
|
||||
=> Prelude.FilePath -- ^ Path to the `nix-hash` executable, see @Lib.findExec@.
|
||||
-> Base16Digest -- ^ @Base16@ hash digest to @Base32@ encode.
|
||||
-> m Base32Digest
|
||||
toBase32Nix nixhash (Base16Digest d16) =
|
||||
Turtle.fold convertDigest Foldl.head >>= \case
|
||||
Nothing -> throwError $ HockerException "nothing was returned by `nix-hash', not even an error" Nothing Nothing
|
||||
Just result ->
|
||||
either
|
||||
(throwError . hockerExc . T.unpack . lineToText)
|
||||
(return . Base32Digest . lineToText)
|
||||
result
|
||||
where
|
||||
hockerExc m = HockerException m Nothing Nothing
|
||||
convertDigest =
|
||||
inprocWithErr
|
||||
(T.pack nixhash)
|
||||
[ "--type"
|
||||
, "sha256"
|
||||
, "--to-base32"
|
||||
, d16
|
||||
]
|
||||
Turtle.empty
|
190
src/Lib.hs
Normal file
190
src/Lib.hs
Normal file
@ -0,0 +1,190 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Lib
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Lib where
|
||||
|
||||
import Control.Exception (throwIO)
|
||||
import Control.Lens
|
||||
import qualified Control.Monad.Except as Except
|
||||
import Control.Monad.IO.Class (MonadIO (..))
|
||||
import qualified Crypto.Hash as Hash
|
||||
import qualified Data.Aeson
|
||||
import qualified Data.Aeson.Encode.Pretty as AP
|
||||
import Data.Aeson.Lens
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import Data.ByteString.Lazy.Char8 as C8L
|
||||
import Data.Char
|
||||
import Data.Coerce
|
||||
import Data.Monoid
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Network.Wreq
|
||||
import Nix.Expr (NExpr)
|
||||
import Nix.Pretty
|
||||
import System.Directory (findExecutable)
|
||||
import System.Environment (getProgName)
|
||||
import System.Exit as Exit
|
||||
import System.FilePath.Posix as File
|
||||
import System.IO (stdout)
|
||||
import Text.PrettyPrint.ANSI.Leijen as Text.PrettyPrint (SimpleDoc,
|
||||
displayIO,
|
||||
renderPretty)
|
||||
import URI.ByteString
|
||||
|
||||
import Data.Docker.Image.V1.Types
|
||||
|
||||
import Types
|
||||
import Types.Exceptions
|
||||
import Types.ImageName
|
||||
import Types.ImageTag
|
||||
|
||||
-- | Throw a @userError@, exiting the program with the supplied
|
||||
-- message.
|
||||
die :: MonadIO io => T.Text -> io a
|
||||
die = liftIO . throwIO . userError . T.unpack
|
||||
|
||||
-- | Print an error message to stderr and return a non-zero exit code,
|
||||
-- the message is prefixed with the name of the program.
|
||||
exitProgFail :: String -> IO a
|
||||
exitProgFail msg = do
|
||||
name <- getProgName
|
||||
Exit.die $ name ++ ": " ++ msg
|
||||
|
||||
-- | Writes a bytestring to the provided filesystem path if it
|
||||
-- @isJust@ and prints the path it wrote to the screen, otherwise
|
||||
-- print the entire contents to the screen.
|
||||
writeOrPrint :: Maybe FilePath -> C8L.ByteString -> IO ()
|
||||
writeOrPrint o r = case o of
|
||||
Just p' -> C8L.writeFile p' r >> Prelude.putStrLn p'
|
||||
Nothing -> C8L.putStrLn r
|
||||
|
||||
-- | Make a path given a base path and a docker container name.
|
||||
mkOutImage :: ImageName -> FilePath -> FilePath
|
||||
mkOutImage n o = o </> (takeBaseName $ coerce n)
|
||||
|
||||
-- | Make a path given a base path, a docker container name, and a
|
||||
-- docker container tag appending "-config.json" to the basename.
|
||||
mkOutConfig :: ImageName -> ImageTag -> FilePath -> FilePath
|
||||
mkOutConfig n t o = o </> Prelude.concat
|
||||
[ (takeBaseName $ coerce n)
|
||||
, "_", coerce t
|
||||
, "-config.json"
|
||||
]
|
||||
|
||||
-- | Make a path given a base path, a docker container name, and a
|
||||
-- docker container tag appending "-manifest.json" to the basename.
|
||||
mkOutManifest :: ImageName -> ImageTag -> FilePath -> FilePath
|
||||
mkOutManifest n t o = o </> Prelude.concat
|
||||
[ (takeBaseName $ coerce n)
|
||||
, "_", coerce t
|
||||
, "-manifest.json"
|
||||
]
|
||||
|
||||
-- | Safely join a list of strings and a Network.URI record together
|
||||
-- using @joinPath@.
|
||||
joinURIPath :: [String] -> RegistryURI -> RegistryURI
|
||||
joinURIPath pts uri@URI{..} = uri { uriPath = joinedParts }
|
||||
where
|
||||
joinedParts = C8.pack $ File.joinPath ("/":"v2":(C8.unpack uriPath):pts)
|
||||
|
||||
-- | Produce an @Options@ using @Network.Wreq.defaults@ and an @Auth@.
|
||||
opts :: Maybe Auth -> Options
|
||||
opts bAuth = Network.Wreq.defaults & Network.Wreq.auth .~ bAuth
|
||||
|
||||
-- | Hash a @Data.ByteString.Lazy.Char8@ using the SHA256 algorithm.
|
||||
sha256 :: C8L.ByteString -> Hash.Digest Hash.SHA256
|
||||
sha256 = Hash.hashlazy
|
||||
|
||||
-- | Strip the hash algorithm identifier prefix from the beginning of
|
||||
-- a hash digest string; e.g: "sha256:<digest>" becomes "<digest>".
|
||||
stripHashId :: T.Text -> T.Text
|
||||
stripHashId = snd . T.breakOnEnd ":"
|
||||
|
||||
-- | Encode, following Docker's canonical JSON rules, any @ToJSON@
|
||||
-- data type.
|
||||
--
|
||||
-- The canonicalization rules enable consistent hashing of encoded
|
||||
-- JSON, a process relied upon heavily by docker for content
|
||||
-- addressability and unique identification of resources within a
|
||||
-- docker registry. Notably, an image's config JSON file and layers.
|
||||
--
|
||||
-- NB: <http://54.71.194.30:4016/registry/spec/json Docker's canonical JSON spec>
|
||||
-- intentionally *does not* follow the <http://wiki.laptop.org/go/Canonical_JSON OLPC>'s
|
||||
-- Canonical JSON format even though it was inspired by it.
|
||||
encodeCanonical :: Data.Aeson.ToJSON a => a -> C8L.ByteString
|
||||
encodeCanonical = AP.encodePretty' conf
|
||||
where
|
||||
-- NB: the spec requires keys to be in lexically sorted order and
|
||||
-- it appears that the Ord instance of @Text@ behaves the same way
|
||||
-- the Ord instance for @String@ does: it sorts lexically.
|
||||
conf = AP.defConfig { AP.confIndent = AP.Spaces 0, AP.confCompare = compare }
|
||||
|
||||
-- | Throw an error if `Maybe FilePath` is `Nothing`, otherwise return
|
||||
-- the @FilePath@ unwrapped.
|
||||
requireOutPath :: (Except.MonadError HockerException m)
|
||||
=> Maybe FilePath
|
||||
-> m (FilePath)
|
||||
requireOutPath = maybe outPathError return
|
||||
where
|
||||
outPathError = Except.throwError $
|
||||
hockerException "To fetch and assemble a docker image, `--out=<path>` must be supplied"
|
||||
|
||||
-- | Pluck the digest value for the config JSON given a docker
|
||||
-- registry image manifest. Attempting to parse and return the digest
|
||||
-- value as a `Digest SHA256`, otherwise throwing an error.
|
||||
getConfigDigest :: (Except.MonadError HockerException m)
|
||||
=> C8L.ByteString
|
||||
-> m (Hash.Digest Hash.SHA256)
|
||||
getConfigDigest (view (key "config" . key "digest" . _String) -> digest) =
|
||||
maybe badDigest return parsedDigest
|
||||
where
|
||||
parsedDigest = toDigest $ encodeUtf8 digest
|
||||
badDigest = Except.throwError $ hockerException "Failed parsing the config hash digest"
|
||||
|
||||
-- | @upperFirst@ uppercases the first letter of the string.
|
||||
upperFirst :: String -> String
|
||||
upperFirst [] = []
|
||||
upperFirst (h:t) = toUpper h : t
|
||||
|
||||
-- | Split a docker image's name on the forward slash separator so we
|
||||
-- get the distinct repo name and image name.
|
||||
splitImgName :: ImageName -> (RepoNamePart, ImageNamePart)
|
||||
splitImgName (ImageName (T.pack -> n)) = over _2 T.tail $ T.break (=='/') n
|
||||
|
||||
-- | Pretty print a Nix expression and return a
|
||||
-- @Text.PrettyPrint.SimpleDoc@, this can in turn be displayed to the
|
||||
-- screen using @Text.PrettyPrint.displayIO@ or transformed into a
|
||||
-- string using @Text.PrettyPrint.displayS@.
|
||||
renderNixExpr :: NExpr -> Text.PrettyPrint.SimpleDoc
|
||||
renderNixExpr = renderPretty 0.4 120 . prettyNix
|
||||
|
||||
-- | Pretty print a Nix expression AST and print to stdout.
|
||||
pprintNixExpr :: NExpr -> IO ()
|
||||
pprintNixExpr = displayIO stdout . renderNixExpr
|
||||
|
||||
-- | Given an executable's name, try to find it in the current
|
||||
-- process's PATH context.
|
||||
findExec :: (MonadIO m, Except.MonadError HockerException m)
|
||||
=> String
|
||||
-> m Prelude.FilePath
|
||||
findExec execname = (liftIO $ findExecutable execname) >>= \case
|
||||
Just v -> return v
|
||||
Nothing -> Except.throwError $
|
||||
HockerException
|
||||
("cannot find executable `" <> execname <> "'")
|
||||
Nothing
|
||||
Nothing
|
150
src/Network/Wreq/Docker/Image/Lib.hs
Normal file
150
src/Network/Wreq/Docker/Image/Lib.hs
Normal file
@ -0,0 +1,150 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Network.Wreq.Docker.Image.Lib
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Network.Wreq.Docker.Image.Lib where
|
||||
|
||||
import qualified Codec.Archive.Tar as Tar
|
||||
import qualified Codec.Compression.GZip as GZip
|
||||
import qualified Control.Concurrent.PooledIO.Final as Pool
|
||||
import Control.Lens
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import qualified Data.ByteString.Lazy.Char8 as C8L
|
||||
import Data.Coerce
|
||||
import qualified Data.HashMap.Strict as HashMap
|
||||
import Data.Monoid
|
||||
import qualified Data.Text as Text
|
||||
import qualified Network.Wreq as Wreq
|
||||
import qualified System.Directory as Directory
|
||||
import System.FilePath.Posix as File
|
||||
import System.Terminal.Concurrent
|
||||
|
||||
import Data.Docker.Image.V1_2.Types
|
||||
import Lib
|
||||
import Network.Wreq.Docker.Registry.V2 as Docker.Registry
|
||||
import Types
|
||||
import Types.Exceptions
|
||||
import Types.ImageTag
|
||||
|
||||
-- | Like @mapM@ but concurrently applies a function to the elements
|
||||
-- of the @Traversable@, limiting the maximum number of worker threads
|
||||
-- by *n*.
|
||||
mapPool :: Traversable t
|
||||
=> Int -- ^ Number of pooled worker threads
|
||||
-> ((String -> IO ()) -> a -> Hocker FilePath) -- ^ Processing function
|
||||
-> t a -- ^ A Traversable container
|
||||
-> Hocker (t (Either HockerException FilePath))
|
||||
mapPool n f l = do
|
||||
env <- ask
|
||||
writeC <- liftIO getConcurrentOutputter
|
||||
let f' v = (runHocker (f writeC v) env)
|
||||
|
||||
-- TODO: because I'm re-wrapping the function traversing the
|
||||
-- traversable, I need to extract the Left's from the result and
|
||||
-- propagate an error up with @throwError@ from this function.
|
||||
--
|
||||
-- TODO: refactor this such that the previous TODO is unnecessary.
|
||||
liftIO . Pool.runLimited n $ traverse (Pool.fork . f') l
|
||||
|
||||
-- | Like @mapPool@ but with the arguments flipped.
|
||||
forPool :: Traversable t
|
||||
=> Int -- ^ Number of pooled worker threads
|
||||
-> t a -- ^ A Traversable container
|
||||
-> ((String -> IO ()) -> a -> Hocker FilePath) -- ^ Processing function
|
||||
-> Hocker (t (Either HockerException FilePath))
|
||||
forPool n = flip $ mapPool n
|
||||
|
||||
-- | Download, verify, decompress, and write a docker container image
|
||||
-- layer to the filesystem.
|
||||
fetchLayer :: (String -> IO ()) -- ^ Concurrent terminal output function
|
||||
-> (RefLayer, Layer) -- ^ A tuple of the reference layer hash digest from the image's config JSON and the hash digest from the image's manifest JSON
|
||||
-> Hocker FilePath
|
||||
fetchLayer writeC layer@(refl, (stripHashId -> layer')) = ask >>= \HockerMeta{..} -> do
|
||||
liftIO . writeC . Text.unpack $ "Downloading layer: " <> (Text.take 7 layer')
|
||||
|
||||
fetchedImageLayer <- checkResponseIntegrity' =<< (Docker.Registry.fetchLayer $ snd layer)
|
||||
|
||||
let decompressed = fetchedImageLayer & Wreq.responseBody %~ GZip.decompress
|
||||
shortRef = Text.take 7 refl
|
||||
|
||||
imageOutDir <- Lib.requireOutPath outDir
|
||||
|
||||
liftIO $ writeC " => decompressed "
|
||||
|
||||
let layerOutPath = File.joinPath [imageOutDir, Text.unpack refl] `addExtension` "tar"
|
||||
layerPath <- writeRespBody layerOutPath refl decompressed
|
||||
|
||||
liftIO . writeC $ Text.unpack ("=> wrote " <> shortRef)
|
||||
|
||||
return layerPath
|
||||
|
||||
-- | Generate a @manifest.json@ file.
|
||||
createImageManifest :: RepoTag -- ^ e.g: registry.mydomain.net:5001/reponame/imagename
|
||||
-> FilePath -- ^ Path of image config file for manifest
|
||||
-> [RefLayer] -- ^ Layer hash digests sourced from the image's config JSON
|
||||
-> Hocker ()
|
||||
createImageManifest repoTag imageConfigFile refls = ask >>= \HockerMeta{..} -> do
|
||||
let imageManifest = [
|
||||
ImageManifest
|
||||
(takeBaseName imageConfigFile `addExtension` "json")
|
||||
[Text.pack (repoTag ++ ":" ++ coerce imageTag)]
|
||||
(fmap ((`addExtension` "tar") . Text.unpack) refls) ]
|
||||
imageOutDir <- Lib.requireOutPath outDir
|
||||
liftIO $ C8L.writeFile
|
||||
(imageOutDir </> "manifest" `addExtension` "json")
|
||||
(Lib.encodeCanonical imageManifest)
|
||||
|
||||
-- | Generate a @repositories@ json file.
|
||||
--
|
||||
-- NB: it is JSON but Docker doesn't want it with a JSON extension
|
||||
-- unlike its sibling the @manifest.json@ file.
|
||||
createImageRepository :: RepoTag -- ^ e.g: registry.mydomain.net:5001/reponame/imagename
|
||||
-> [RefLayer] -- ^ Layer hash digests sourced from the image's configuration JSON
|
||||
-> Hocker ()
|
||||
createImageRepository repoTag refls = ask >>= \HockerMeta{..} -> do
|
||||
let repositories =
|
||||
ImageRepo
|
||||
(Text.pack repoTag)
|
||||
|
||||
-- Create a singleton map from a tag and the "latest" layer;
|
||||
-- Aeson will correctly encode this as an object with a key
|
||||
-- (the tag) and value (the layer within the archive named
|
||||
-- by its hash digest)
|
||||
(HashMap.singleton
|
||||
(Text.pack $ coerce imageTag)
|
||||
((Prelude.last refls) <> ".tar"))
|
||||
imageOutDir <- Lib.requireOutPath outDir
|
||||
liftIO $ C8L.writeFile
|
||||
(imageOutDir </> "repositories")
|
||||
(Lib.encodeCanonical repositories)
|
||||
|
||||
-- | Tar and gzip the output dir into the final docker image archive
|
||||
-- and remove the output dir.
|
||||
createImageTar :: Hocker FilePath
|
||||
createImageTar = ask >>= \HockerMeta{..} -> do
|
||||
imageOutDir <- Lib.requireOutPath outDir
|
||||
archivePath <- Lib.requireOutPath out
|
||||
|
||||
entries <- liftIO $ Directory.getDirectoryContents imageOutDir
|
||||
|
||||
-- TODO: remove once we have a newer `directory`
|
||||
let entriesToPack = [e | e <- entries, e /= ".", e /= ".."]
|
||||
|
||||
liftIO $ Tar.create archivePath imageOutDir entriesToPack
|
||||
|
||||
-- Cleanup after ourselves
|
||||
liftIO $ Directory.removeDirectoryRecursive imageOutDir
|
||||
|
||||
return $ archivePath
|
158
src/Network/Wreq/Docker/Image/V1_2.hs
Normal file
158
src/Network/Wreq/Docker/Image/V1_2.hs
Normal file
@ -0,0 +1,158 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Network.Wreq.Docker.Image.V1_2
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Network.Wreq.Docker.Image.V1_2 where
|
||||
|
||||
|
||||
import Control.Lens
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Data.ByteString.Lazy.Char8 as C8L
|
||||
import Data.Coerce
|
||||
import Data.Either
|
||||
import Data.HashSet as Set
|
||||
import Data.Monoid
|
||||
import qualified Data.Text as T
|
||||
import NeatInterpolation
|
||||
import qualified Network.Wreq as Wreq
|
||||
import System.FilePath.Posix as File
|
||||
import System.Terminal.Concurrent
|
||||
|
||||
import Data.Docker.Image.V1.Types
|
||||
import Lib
|
||||
|
||||
import Network.Wreq.Docker.Image.Lib as Docker.Image
|
||||
import Network.Wreq.Docker.Registry.V2 as Docker.Registry
|
||||
import Types
|
||||
import Types.Exceptions
|
||||
import Types.ImageName
|
||||
|
||||
-- | Fetches an image from the specified (or default) V2 Docker
|
||||
-- Registery and assembles the artifacts into a compatible Docker V1.2
|
||||
-- Image.
|
||||
fetchAndAssemble :: HockerMeta -> IO (Either HockerException T.Text)
|
||||
fetchAndAssemble = runHocker doFetchImage
|
||||
|
||||
-- | Fetches a layer by its digest key from the specified (or default)
|
||||
-- V2 Docker Registery.
|
||||
fetchLayer :: HockerMeta -> IO (Either HockerException FilePath)
|
||||
fetchLayer = runHocker doFetchLayer
|
||||
|
||||
-- | Fetches the config file of the specified image from the specified
|
||||
-- (or default) V2 Docker Registry and returns it.
|
||||
fetchConfig :: HockerMeta -> IO (Either HockerException C8L.ByteString)
|
||||
fetchConfig = runHocker doFetchConfig
|
||||
|
||||
-- | Fetches the manifest file of the specified image from the
|
||||
-- specified (or default) V2 Docker Registry and returns it.
|
||||
fetchImageManifest :: HockerMeta -> IO (Either HockerException C8L.ByteString)
|
||||
fetchImageManifest = runHocker doFetch
|
||||
where
|
||||
doFetch = fetchManifest >>= return . view Wreq.responseBody
|
||||
|
||||
-- | Executes the monadic logic for fetching the docker image config
|
||||
-- JSON within the ReaderT monad.
|
||||
doFetchConfig :: Hocker C8L.ByteString
|
||||
doFetchConfig = ask >>= \HockerMeta{..} -> do
|
||||
configDigest <-
|
||||
fetchManifest
|
||||
>>= checkResponseIntegrity'
|
||||
>>= getConfigDigest . view Wreq.responseBody
|
||||
|
||||
fetchImageConfig configDigest
|
||||
>>= return . view Wreq.responseBody
|
||||
|
||||
-- | Executes the monadic logic for fetching and saving a layer tar
|
||||
-- archive.
|
||||
doFetchLayer :: Hocker FilePath
|
||||
doFetchLayer = ask >>= \HockerMeta{..} -> do
|
||||
layerOut <- Lib.requireOutPath out
|
||||
|
||||
layerDigest <- T.pack . show <$> maybe
|
||||
(throwError $ hockerException
|
||||
"a layer digest is expected!")
|
||||
return
|
||||
imageLayer
|
||||
|
||||
let shortRef = T.take 7 layerDigest
|
||||
|
||||
writeC <- liftIO $ getConcurrentOutputter
|
||||
liftIO . writeC . T.unpack $ "Downloading layer: " <> shortRef
|
||||
|
||||
fetchedImageLayer <- checkResponseIntegrity' =<< Docker.Registry.fetchLayer ("sha256:" <> layerDigest)
|
||||
layerPath <- writeRespBody layerOut layerDigest fetchedImageLayer
|
||||
|
||||
liftIO . writeC $ T.unpack ("=> wrote " <> shortRef)
|
||||
|
||||
return layerPath
|
||||
|
||||
-- | Executes the monadic logic for fetching, transforming, and
|
||||
-- assembling a docker container image.
|
||||
doFetchImage :: Hocker T.Text
|
||||
doFetchImage = ask >>= \HockerMeta{..} -> do
|
||||
imageOutDir <- Lib.requireOutPath outDir
|
||||
|
||||
manifest <- fetchManifest >>= checkResponseIntegrity'
|
||||
configDigest <- getConfigDigest $ manifest ^. Wreq.responseBody
|
||||
|
||||
-- TODO: ALL of the below steps that handle saving things to the
|
||||
-- disk should probably be wrapped in a bracket function responsible
|
||||
-- for cleaning up any partially written data if there's a
|
||||
-- failure... Or perhaps instad of bracketing in here, we bracket
|
||||
-- around the @runExceptT@?
|
||||
|
||||
-- Fetch and write the configuration json file for the image
|
||||
let configFileHash = Lib.stripHashId . T.pack $ showSHA configDigest
|
||||
imageConfig <- fetchImageConfig configDigest
|
||||
imageConfigFile <- writeRespBody
|
||||
(File.joinPath [imageOutDir, T.unpack configFileHash] `addExtension` "json")
|
||||
configFileHash
|
||||
imageConfig
|
||||
|
||||
let refLayers = pluckRefLayersFrom $ imageConfig ^. Wreq.responseBody
|
||||
refLayers' = fmap Lib.stripHashId refLayers
|
||||
refLayerSet = Set.fromList refLayers'
|
||||
manifestLayers = pluckLayersFrom $ manifest ^. Wreq.responseBody
|
||||
(_, strippedReg) = T.breakOnEnd "//" . T.pack . show $ dockerRegistry
|
||||
repoTags = (T.unpack strippedReg) </> (coerce imageName)
|
||||
|
||||
-- Concurrently fetch layers and write to disk with a limit of three
|
||||
-- threads
|
||||
layers <- mapPool 3 Docker.Image.fetchLayer $ Prelude.zip refLayers' manifestLayers
|
||||
|
||||
let writtenLayerSet = Set.fromList . fmap (T.pack . takeBaseName) $ rights layers
|
||||
refLayerSetTxt = T.pack (show refLayerSet)
|
||||
wrtLayerSetTxt = T.pack (show writtenLayerSet)
|
||||
dffLayerSetTxt = T.pack (show $ Set.difference refLayerSet writtenLayerSet)
|
||||
|
||||
when (writtenLayerSet /= refLayerSet) $
|
||||
throwError . hockerException $ T.unpack
|
||||
([text|
|
||||
Written layers do not match the reference layers!
|
||||
|
||||
Reference layers: ${refLayerSetTxt}
|
||||
Written layers: ${wrtLayerSetTxt}
|
||||
|
||||
Difference: ${dffLayerSetTxt}
|
||||
|])
|
||||
|
||||
createImageRepository repoTags refLayers'
|
||||
createImageManifest repoTags imageConfigFile refLayers'
|
||||
|
||||
archivePath <- createImageTar
|
||||
|
||||
return $ T.pack archivePath
|
231
src/Network/Wreq/Docker/Registry/V2.hs
Normal file
231
src/Network/Wreq/Docker/Registry/V2.hs
Normal file
@ -0,0 +1,231 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Network.Wreq.Docker.Registry.V2
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
--
|
||||
-- Convenience functions for interacting with an instance of Docker
|
||||
-- Distribution (Docker Registry V2). I've kept the module naming
|
||||
-- consistent with the docker registry terms since that appears to be
|
||||
-- what everyone uses colloquially even though the formal name for the
|
||||
-- software is "docker distribution".
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Network.Wreq.Docker.Registry.V2 where
|
||||
|
||||
import Control.Lens
|
||||
import qualified Control.Monad.Except as Except
|
||||
import Control.Monad.Reader
|
||||
import Data.Monoid
|
||||
import qualified Crypto.Hash as Hash
|
||||
import Data.Aeson.Lens
|
||||
import Data.ByteString.Lazy.Char8 as C8L
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
||||
import URI.ByteString
|
||||
import NeatInterpolation
|
||||
import qualified Data.Text as Text
|
||||
import qualified Network.Wreq as Wreq
|
||||
import System.Directory
|
||||
|
||||
import Data.Docker.Image.V1.Types
|
||||
import Lib
|
||||
import Types
|
||||
import Types.Exceptions
|
||||
import Types.ImageName
|
||||
import Types.ImageTag
|
||||
|
||||
-- | Default docker hub registry.
|
||||
defaultRegistry :: URIRef Absolute
|
||||
defaultRegistry = URI
|
||||
{ uriScheme = Scheme "https"
|
||||
, uriAuthority = Just $ Authority
|
||||
{ authorityUserInfo = Nothing
|
||||
, authorityHost = Host "registry-1.docker.io"
|
||||
, authorityPort = Nothing
|
||||
}
|
||||
, uriPath = "/v2/"
|
||||
, uriQuery = Query []
|
||||
, uriFragment = Nothing
|
||||
}
|
||||
|
||||
mkAuth :: RegistryURI
|
||||
-> ImageName
|
||||
-> Maybe Credentials
|
||||
-> IO (Maybe Wreq.Auth)
|
||||
mkAuth reg (ImageName img) credentials =
|
||||
case credentials of
|
||||
Just (BearerToken token)
|
||||
-> pure (Just $ Wreq.oauth2Bearer (encodeUtf8 token))
|
||||
Just (Basic username password)
|
||||
-> pure (Just $ Wreq.basicAuth (encodeUtf8 username) (encodeUtf8 password))
|
||||
Nothing | reg /= defaultRegistry
|
||||
-> pure Nothing
|
||||
| otherwise
|
||||
-> getHubToken >>= pure . mkHubBearer
|
||||
where
|
||||
getHubToken = Wreq.get ("https://auth.docker.io/token?service=registry.docker.io&scope=repository:"<>img<>":pull")
|
||||
mkHubBearer rsp = (Wreq.oauth2Bearer . encodeUtf8) <$> (rsp ^? Wreq.responseBody . key "token" . _String)
|
||||
|
||||
-- | Retrieve a list of layer hash digests from an image's manifest
|
||||
-- JSON.
|
||||
--
|
||||
-- TODO: pluck out the layer's size and digest into a tuple.
|
||||
pluckLayersFrom :: Manifest -> [Layer]
|
||||
pluckLayersFrom = toListOf (key "layers" . values . key "digest" . _String)
|
||||
|
||||
-- | Retrieve a list of layer hash digests from an image's config
|
||||
-- JSON.
|
||||
--
|
||||
-- This is subtly different from @pluckLayersFrom@ because both list
|
||||
-- hash digests for the image's layers but the manifest's layer hash
|
||||
-- digests are keys into the registry's blob storage referencing the
|
||||
-- *compressed* layer archive. The config JSON's layer hash digests
|
||||
-- reference the uncompressed layer tar archives within the image.
|
||||
pluckRefLayersFrom :: ImageConfigJSON -> [Layer]
|
||||
pluckRefLayersFrom = toListOf (key "rootfs" . key "diff_ids" . values . _String)
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Top-level docker-registry V2 REST interface functions
|
||||
|
||||
-- | Request a V2 registry manifest for the specified docker image.
|
||||
fetchManifest :: Hocker RspBS
|
||||
fetchManifest = ask >>= \HockerMeta{..} ->
|
||||
liftIO $ Wreq.getWith (opts auth & accept) (mkURL imageName imageTag dockerRegistry)
|
||||
where
|
||||
mkURL (ImageName n) (ImageTag t) r = C8.unpack (serializeURIRef' $ Lib.joinURIPath [n, "manifests", t] r)
|
||||
accept = Wreq.header "Accept" .~
|
||||
[ "application/vnd.docker.distribution.manifest.v2+json"
|
||||
, "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
]
|
||||
|
||||
-- | Retrieve the config json of an image by its hash digest (found in
|
||||
-- the V2 manifest for an image given by a name and tag).
|
||||
fetchImageConfig :: (Hash.Digest Hash.SHA256) -> Hocker RspBS
|
||||
fetchImageConfig (showSHA -> digest) = ask >>= \HockerMeta{..} ->
|
||||
liftIO $ Wreq.getWith (opts auth) (mkURL imageName dockerRegistry)
|
||||
where
|
||||
mkURL (ImageName n) r = C8.unpack (serializeURIRef' $ Lib.joinURIPath [n, "blobs", digest] r)
|
||||
|
||||
-- | Retrieve a compressed layer blob by its hash digest.
|
||||
--
|
||||
-- TODO: take advantage of registry's support for the Range header so
|
||||
-- we can stream downloads.
|
||||
fetchLayer :: Layer -> Hocker RspBS
|
||||
fetchLayer layer = ask >>= \HockerMeta{..} ->
|
||||
liftIO $ Wreq.getWith (opts auth) (mkURL layer imageName dockerRegistry)
|
||||
where
|
||||
mkURL
|
||||
(Text.unpack -> digest)
|
||||
(ImageName name)
|
||||
registry
|
||||
= C8.unpack (serializeURIRef' $ joinURIPath [name, "blobs", digest] registry)
|
||||
|
||||
-- | Write a @Wreq@ response body to the specified @FilePath@,
|
||||
-- checking the integrity of the file with its sha256 hash digest.
|
||||
--
|
||||
-- The second argument, the @StrippedDigest@, must be a hash digest
|
||||
-- stripped of the "sha256:" hash algorithm identifier prefix.
|
||||
writeRespBody :: FilePath -- ^ Filesystem path to write the content to
|
||||
-> StrippedDigest -- ^ Hash digest, stripped of its hash algorithm identifier prefix
|
||||
-> RspBS -- ^ Wreq lazy bytestring response object
|
||||
-> Hocker FilePath
|
||||
writeRespBody out digest resp = do
|
||||
liftIO . C8L.writeFile out $ resp ^. Wreq.responseBody
|
||||
|
||||
-- Now, verify the file; we assume the sha256 function since that is
|
||||
-- used everywhere
|
||||
verified <- liftIO $ checkFileIntegrity out digest
|
||||
either (Except.throwError . hockerException) return verified
|
||||
|
||||
-- | Write a response to the filesystem without a request hash
|
||||
-- digest. Attempt to fetch the value of the `ETag` header to verify
|
||||
-- the integrity of the content received.
|
||||
--
|
||||
-- The Docker docs do *not* recommended this method for verification
|
||||
-- because the ETag and Docker-Content-Digest headers may change
|
||||
-- between the time you issue a request with a digest and when you
|
||||
-- receive a response back!
|
||||
writeRespBody' :: FilePath -- ^ Filesystem path to write the content to
|
||||
-> RspBS -- ^ Wreq lazy bytestring response object
|
||||
-> Hocker FilePath
|
||||
writeRespBody' out r = writeRespBody out etagHash r
|
||||
where
|
||||
etagHash = decodeUtf8 $ r ^. Wreq.responseHeader "ETag"
|
||||
|
||||
-- | Compute a sha256 hash digest of the response body and compare it
|
||||
-- against the supplied hash digest.
|
||||
checkResponseIntegrity :: (Except.MonadError HockerException m)
|
||||
=> RspBS -- ^ Wreq lazy bytestring response object
|
||||
-> StrippedDigest -- ^ Hash digest, stripped of its hash algorithm identifier prefix
|
||||
-> m RspBS
|
||||
checkResponseIntegrity r d = do
|
||||
let contentHash = show . Lib.sha256 $ r ^. Wreq.responseBody
|
||||
digestHash = Text.unpack d
|
||||
if | contentHash == digestHash -> pure r
|
||||
| otherwise ->
|
||||
let chTxt = Text.pack contentHash
|
||||
dgTxt = Text.pack digestHash
|
||||
in Except.throwError
|
||||
(hockerException
|
||||
(Text.unpack [text|
|
||||
Response content hash is $chTxt
|
||||
and it does not match the addressable content hash
|
||||
$dgTxt
|
||||
|]))
|
||||
|
||||
-- | Compute a sha256 hash digest of the response body and compare it
|
||||
-- against the @Docker-Content-Digest@ header from the response.
|
||||
--
|
||||
-- The Docker docs do *not* recommended this method for verification
|
||||
-- because the Docker-Content-Digest header may change between the
|
||||
-- time you issue a request with a digest and when you receive a
|
||||
-- response back!
|
||||
--
|
||||
-- NB: some registries do not send a @Docker-Content-Digest@ header,
|
||||
-- I'm not sure yet what the cause for this is but this function's
|
||||
-- behavior lacking that information is to ignore the hash check.
|
||||
checkResponseIntegrity' :: (Except.MonadError HockerException m)
|
||||
=> RspBS -- ^ Wreq lazy bytestring response object
|
||||
-> m RspBS
|
||||
checkResponseIntegrity' rsp =
|
||||
case decodeUtf8 (rsp ^. Wreq.responseHeader "Docker-Content-Digest") of
|
||||
-- Since some registries may send back no Docker-Content-Digest
|
||||
-- header, or an empty one, if it is empty then ignore it
|
||||
"" -> pure rsp
|
||||
digest -> checkResponseIntegrity rsp (Lib.stripHashId digest)
|
||||
|
||||
-- | Compute a sha256 hash digest for a file and compare that hash to
|
||||
-- the supplied hash digest.
|
||||
checkFileIntegrity :: FilePath -- ^ Filesystem path of file to verify
|
||||
-> StrippedDigest -- ^ Hash digest, stripped of its hash algorithm identifier prefix
|
||||
-> IO (Either String FilePath)
|
||||
checkFileIntegrity fp digest =
|
||||
Except.runExceptT $ do
|
||||
exists <- liftIO (doesFileExist fp)
|
||||
when (not exists) $
|
||||
fail (fp <> " does not exist")
|
||||
|
||||
fileHash <- liftIO (return . show . Lib.sha256 =<< C8L.readFile fp)
|
||||
|
||||
when (Text.unpack digest /= fileHash) $
|
||||
let fhTxt = Text.pack fileHash
|
||||
fpTxt = Text.pack fp
|
||||
in fail $ Text.unpack
|
||||
([text|
|
||||
The sha256 hash for $fpTxt: $fhTxt
|
||||
Does not match the expected digest: $digest
|
||||
|])
|
||||
|
||||
return fp
|
60
src/Network/Wreq/ErrorHandling.hs
Normal file
60
src/Network/Wreq/ErrorHandling.hs
Normal file
@ -0,0 +1,60 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Network.Wreq.ErrorHandling
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Network.Wreq.ErrorHandling where
|
||||
|
||||
import Control.Exception.Lifted as Lifted
|
||||
import Control.Lens
|
||||
import Control.Monad.Except
|
||||
import Data.ByteString.Char8 as C8
|
||||
import Data.Monoid
|
||||
import Network.HTTP.Client
|
||||
import Network.HTTP.Types.Status
|
||||
|
||||
#if !MIN_VERSION_http_client(0,5,0)
|
||||
import Data.HashMap.Lazy as H
|
||||
#endif
|
||||
|
||||
import Types.Exceptions
|
||||
|
||||
interceptHttpExc :: ExceptT HockerException IO a
|
||||
-> ExceptT HockerException IO a
|
||||
interceptHttpExc a = Lifted.try a >>= except . over _Left prettify
|
||||
where
|
||||
except (Left e) = throwError e
|
||||
except (Right v) = return v
|
||||
|
||||
prettify :: HttpException -> HockerException
|
||||
#if MIN_VERSION_http_client(0,5,0)
|
||||
prettify
|
||||
(HttpExceptionRequest _
|
||||
(StatusCodeException
|
||||
(responseStatus -> (Status code msg)) body))
|
||||
= HockerException
|
||||
(show code <> " " <> C8.unpack msg)
|
||||
(Just $ C8.unpack body)
|
||||
Nothing
|
||||
#else
|
||||
prettify
|
||||
(StatusCodeException (Status code msg) (H.fromList -> e) _)
|
||||
= HockerException
|
||||
((show code) <> " " <> C8.unpack msg)
|
||||
(C8.unpack <$> H.lookup "X-Response-Body-Start" e)
|
||||
Nothing
|
||||
#endif
|
||||
|
||||
prettify e = HockerException (show e) Nothing Nothing
|
189
src/Types.hs
Normal file
189
src/Types.hs
Normal file
@ -0,0 +1,189 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PartialTypeSignatures #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
{-# OPTIONS -fno-warn-orphans #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Types
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Types where
|
||||
|
||||
import Control.Applicative
|
||||
import Control.Monad.Error.Class
|
||||
import qualified Control.Monad.Except as Except
|
||||
import Control.Monad.IO.Class
|
||||
import qualified Control.Monad.Reader as Reader
|
||||
import Control.Monad.Reader.Class
|
||||
import qualified Crypto.Hash as Hash
|
||||
import qualified Data.ByteString.Lazy
|
||||
import Data.Monoid
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as Text
|
||||
import Network.Wreq
|
||||
import qualified Network.Wreq as Wreq
|
||||
import Network.Wreq.ErrorHandling
|
||||
import qualified Options.Applicative as Options
|
||||
import Options.Generic
|
||||
import URI.ByteString
|
||||
|
||||
import Types.Exceptions
|
||||
import Types.Hash ()
|
||||
import Types.ImageName
|
||||
import Types.ImageTag
|
||||
import Types.URI ()
|
||||
|
||||
-- | Docker registry URI.
|
||||
type RegistryURI = (URIRef Absolute)
|
||||
|
||||
-- | Docker registry username.
|
||||
type Username = Text
|
||||
|
||||
-- | Docker registry user password.
|
||||
type Password = Text
|
||||
|
||||
-- | Docker image layer sha256 hash digest.
|
||||
type Layer = Text
|
||||
|
||||
-- | SHA256 hash digest with the hash algorithm identifier prefix,
|
||||
-- stripped
|
||||
type StrippedDigest = Text
|
||||
|
||||
-- | Docker image manifest JSON.
|
||||
type Manifest = Data.ByteString.Lazy.ByteString
|
||||
|
||||
-- | Docker image config JSON.
|
||||
type ImageConfigJSON = Data.ByteString.Lazy.ByteString
|
||||
|
||||
-- | Wreq response type parameterized by the lazy bytestring type.
|
||||
type RspBS = Network.Wreq.Response Data.ByteString.Lazy.ByteString
|
||||
|
||||
-- | A file extension.
|
||||
type Extension = String
|
||||
|
||||
-- | RepoName is the part before the forward slash in a docker image
|
||||
-- name, e.g: @library@ in @library/debian@
|
||||
type RepoNamePart = Text
|
||||
|
||||
-- | ImageName is the part after the forward slash in a docker image
|
||||
-- name, e.g: @library@ in @library/debian@
|
||||
type ImageNamePart = Text
|
||||
|
||||
-- | Docker image config JSON file's sha256 hash digest in Nix's
|
||||
-- base32 encoding.
|
||||
--
|
||||
-- NB: it's very important to realize there's a significant difference
|
||||
-- between Nix's base32 encoding and the standard base32 encoding!
|
||||
-- (i.e, they're not compatible).
|
||||
type ConfigDigest = Base32Digest
|
||||
|
||||
-- | Generic top-level optparse-generic CLI args data type and
|
||||
-- specification.
|
||||
--
|
||||
-- NOTE: `hocker-layer` does not use this data type because it
|
||||
-- requires an additional layer sha256 hash digest argument.
|
||||
data OptArgs w = OptArgs
|
||||
{ -- | URI for the registry, optional
|
||||
registry :: w ::: Maybe RegistryURI
|
||||
<?> "URI of registry, defaults to the Docker Hub registry"
|
||||
, credentials :: Maybe Credentials
|
||||
-- | Filesystem path to write output to
|
||||
, out :: w ::: Maybe FilePath
|
||||
<?> "Write content to location"
|
||||
-- | Docker image name (includes the reponame, e.g: library/debian)
|
||||
, imageName :: ImageName
|
||||
-- | Docker image tag
|
||||
, imageTag :: ImageTag
|
||||
} deriving (Generic)
|
||||
|
||||
instance ParseRecord (OptArgs Wrapped)
|
||||
deriving instance Show (OptArgs Unwrapped)
|
||||
|
||||
-- | Hocker 'ExceptT' and 'ReaderT' transformer stack threading a
|
||||
-- 'HockerMeta' data type.
|
||||
newtype Hocker a = Hocker { unHocker :: Reader.ReaderT HockerMeta (Except.ExceptT HockerException IO) a }
|
||||
deriving
|
||||
( Functor
|
||||
, Applicative
|
||||
, Monad
|
||||
, MonadIO
|
||||
, MonadReader HockerMeta
|
||||
, MonadError HockerException
|
||||
)
|
||||
|
||||
runHocker :: Hocker a -> HockerMeta -> IO (Either HockerException a)
|
||||
runHocker (unHocker -> d) = Except.runExceptT . interceptHttpExc . Reader.runReaderT d
|
||||
|
||||
-- | Red wagon record carrying around the environment as we fetch,
|
||||
-- transform, and assemble docker image artifacts.
|
||||
data HockerMeta = HockerMeta
|
||||
{ dockerRegistry :: RegistryURI
|
||||
, auth :: Maybe Wreq.Auth
|
||||
, imageName :: ImageName
|
||||
, imageTag :: ImageTag
|
||||
, out :: Maybe FilePath
|
||||
, outDir :: Maybe FilePath
|
||||
, imageLayer :: Maybe (Hash.Digest Hash.SHA256)
|
||||
} deriving (Show)
|
||||
|
||||
-- | Newtype base32 encoding of a hash digest.
|
||||
--
|
||||
-- Please note, this base32 encoding is unique to Nix and not
|
||||
-- compatible with other base32 encodings.
|
||||
newtype Base32Digest = Base32Digest Text
|
||||
deriving (Show, Read, Eq)
|
||||
|
||||
-- | Newtype base16 encoding of a hash digest.
|
||||
--
|
||||
-- This encoding has no known idiosyncracies specific to Nix, it
|
||||
-- should be compatible with other tools and library's expectations.
|
||||
newtype Base16Digest = Base16Digest Text
|
||||
deriving (Show, Read, Eq)
|
||||
|
||||
data Credentials = Basic Username Password | BearerToken Text
|
||||
deriving (Show)
|
||||
|
||||
instance ParseField Credentials where
|
||||
parseField _ _ = (Basic <$> parseUsername <*> parsePassword) <|> (BearerToken <$> parseToken)
|
||||
where
|
||||
parseUsername = Text.pack <$>
|
||||
(Options.option Options.str $
|
||||
( Options.metavar "BASIC USERNAME"
|
||||
<> Options.long "username"
|
||||
<> Options.short 'u'
|
||||
<> Options.help "Username part of a basic auth credential"
|
||||
)
|
||||
)
|
||||
parsePassword = Text.pack <$>
|
||||
(Options.option Options.str $
|
||||
( Options.metavar "BASIC PASSWORD"
|
||||
<> Options.long "password"
|
||||
<> Options.short 'p'
|
||||
<> Options.help "Password part of a basic auth credential"
|
||||
)
|
||||
)
|
||||
parseToken = Text.pack <$>
|
||||
(Options.option Options.str $
|
||||
( Options.metavar "BEARER TOKEN"
|
||||
<> Options.long "token"
|
||||
<> Options.short 't'
|
||||
<> Options.help "Bearer token retrieved from a call to `docker login` (mutually exclusive to --username and --password)"
|
||||
)
|
||||
)
|
||||
|
||||
instance ParseFields Credentials
|
||||
instance ParseRecord Credentials where
|
||||
parseRecord = fmap Options.Generic.getOnly parseRecord
|
39
src/Types/Exceptions.hs
Normal file
39
src/Types/Exceptions.hs
Normal file
@ -0,0 +1,39 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Types.Exceptions
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Types.Exceptions where
|
||||
|
||||
import Control.DeepSeq
|
||||
import Control.Exception
|
||||
import Data.Monoid
|
||||
import GHC.Generics
|
||||
|
||||
data HockerException = HockerException
|
||||
{ baseMsg :: String
|
||||
, expected :: Maybe String
|
||||
, received :: Maybe String
|
||||
} deriving (Read, Generic, NFData)
|
||||
|
||||
instance Exception HockerException
|
||||
instance Show HockerException where
|
||||
show (HockerException m e r) = m <> (ext $ e <> r)
|
||||
where
|
||||
ext (Just v) = "; " <> v
|
||||
ext Nothing = mempty
|
||||
|
||||
hockerException :: String -> HockerException
|
||||
hockerException m = HockerException m Nothing Nothing
|
43
src/Types/Hash.hs
Normal file
43
src/Types/Hash.hs
Normal file
@ -0,0 +1,43 @@
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# OPTIONS -fno-warn-orphans #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Types.Hash
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Types.Hash where
|
||||
|
||||
import qualified Crypto.Hash as Hash
|
||||
import qualified Data.ByteArray as BA
|
||||
import qualified Data.ByteArray.Encoding as BA
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import Data.Monoid
|
||||
import qualified Data.Text
|
||||
import qualified Options.Applicative as Options
|
||||
import Options.Generic
|
||||
|
||||
toBytes :: C8.ByteString -> Either String BA.Bytes
|
||||
toBytes = BA.convertFromBase BA.Base16
|
||||
|
||||
readSHA256 :: C8.ByteString -> Maybe (Hash.Digest Hash.SHA256)
|
||||
readSHA256 = either (const Nothing) Hash.digestFromByteString . toBytes
|
||||
|
||||
instance ParseField (Hash.Digest Hash.SHA256) where
|
||||
parseField h _ =
|
||||
(Options.option (Options.maybeReader (readSHA256 . C8.pack)) $
|
||||
( Options.metavar "SHA256"
|
||||
<> Options.short 'l'
|
||||
<> Options.long "layer"
|
||||
<> maybe mempty (Options.help . Data.Text.unpack) h
|
||||
)
|
||||
)
|
||||
|
||||
instance ParseFields (Hash.Digest Hash.SHA256) where
|
||||
instance ParseRecord (Hash.Digest Hash.SHA256) where
|
||||
parseRecord = fmap getOnly parseRecord
|
38
src/Types/ImageName.hs
Normal file
38
src/Types/ImageName.hs
Normal file
@ -0,0 +1,38 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# OPTIONS -fno-warn-orphans #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Types.ImageName
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Types.ImageName where
|
||||
|
||||
import Control.DeepSeq
|
||||
import Data.Monoid
|
||||
import qualified Options.Applicative as Options
|
||||
import Options.Generic
|
||||
|
||||
newtype ImageName = ImageName { unImageName :: String }
|
||||
deriving (Generic, Show)
|
||||
|
||||
instance ParseField ImageName where
|
||||
parseField _ _ =
|
||||
ImageName <$>
|
||||
(Options.argument Options.str $
|
||||
( Options.metavar "IMAGE-NAME"
|
||||
<> Options.help "Docker image name, e.g: 'debian' in debian:jessie"
|
||||
)
|
||||
)
|
||||
|
||||
instance ParseFields ImageName where
|
||||
instance ParseRecord ImageName where
|
||||
parseRecord = fmap getOnly parseRecord
|
||||
|
||||
instance NFData ImageName
|
38
src/Types/ImageTag.hs
Normal file
38
src/Types/ImageTag.hs
Normal file
@ -0,0 +1,38 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# OPTIONS -fno-warn-orphans #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Types.ImageTag
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Types.ImageTag where
|
||||
|
||||
import Control.DeepSeq
|
||||
import Data.Monoid
|
||||
import qualified Options.Applicative as Options
|
||||
import Options.Generic
|
||||
|
||||
newtype ImageTag = ImageTag { unImageTag :: String }
|
||||
deriving (Generic, Show)
|
||||
|
||||
instance ParseField ImageTag where
|
||||
parseField _ _ =
|
||||
ImageTag <$>
|
||||
(Options.argument Options.str $
|
||||
( Options.metavar "IMAGE-TAG"
|
||||
<> Options.help "Docker image tag identifier, e.g: 'jessie' in debian:jessie"
|
||||
)
|
||||
)
|
||||
|
||||
instance ParseFields ImageTag where
|
||||
instance ParseRecord ImageTag where
|
||||
parseRecord = fmap getOnly parseRecord
|
||||
|
||||
instance NFData ImageTag
|
44
src/Types/URI.hs
Normal file
44
src/Types/URI.hs
Normal file
@ -0,0 +1,44 @@
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
{-# OPTIONS -fno-warn-orphans #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Types.URI
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Types.URI where
|
||||
|
||||
import Control.Lens
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import Data.Monoid
|
||||
import qualified Data.Text as Text
|
||||
import qualified Options.Applicative as Options
|
||||
import Options.Applicative.Builder
|
||||
import Options.Generic
|
||||
import URI.ByteString
|
||||
|
||||
-- | Parse a URI value.
|
||||
uriReader :: ReadM (URIRef Absolute)
|
||||
uriReader = Options.eitherReader parseURIArg
|
||||
where
|
||||
parseURIArg (parseURI strictURIParserOptions . C8.pack -> parsedURI) =
|
||||
over _Left show parsedURI
|
||||
|
||||
instance ParseField (URIRef Absolute) where
|
||||
parseField h n =
|
||||
(Options.option uriReader $
|
||||
( Options.metavar "URI"
|
||||
<> foldMap (Options.long . Text.unpack) n
|
||||
<> foldMap (Options.help . Text.unpack) h
|
||||
)
|
||||
)
|
||||
|
||||
instance ParseFields (URIRef Absolute) where
|
||||
instance ParseRecord (URIRef Absolute) where
|
||||
parseRecord = fmap getOnly parseRecord
|
10
stack.yaml
Normal file
10
stack.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
flags: {}
|
||||
nix:
|
||||
shell-file: nix/shell.nix
|
||||
extra-package-dbs: []
|
||||
packages:
|
||||
- '.'
|
||||
extra-deps:
|
||||
- concurrentoutput-0.2.0.2
|
||||
- hnix-0.3.4
|
||||
resolver: lts-8.12
|
19
test/Main.hs
Normal file
19
test/Main.hs
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
module Main where
|
||||
|
||||
import Test.Tasty
|
||||
import Test.Tasty.HUnit
|
||||
|
||||
import qualified Tests.Data.Docker.Image.V1 as ImageV1Tests
|
||||
import qualified Tests.Data.Docker.Image.V1_2 as ImageV1_2Tests
|
||||
import qualified Tests.Data.Docker.Nix.FetchDocker as FetchDockerTests
|
||||
|
||||
main :: IO ()
|
||||
main = defaultMain tests
|
||||
|
||||
tests :: TestTree
|
||||
tests = testGroup "Tests"
|
||||
[ ImageV1Tests.unitTests
|
||||
, ImageV1_2Tests.unitTests
|
||||
, FetchDockerTests.tests
|
||||
]
|
86
test/Tests/Data/Docker/Image/V1.hs
Normal file
86
test/Tests/Data/Docker/Image/V1.hs
Normal file
@ -0,0 +1,86 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Image.V1
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Tests.Data.Docker.Image.V1 where
|
||||
|
||||
import qualified Crypto.Hash as Hash
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import Data.Docker.Image.V1.Layer
|
||||
import Data.Docker.Image.V1.Types
|
||||
import Data.Maybe
|
||||
import Data.Sequence as Seq
|
||||
|
||||
import Test.Tasty
|
||||
import Test.Tasty.HUnit
|
||||
|
||||
import Lib
|
||||
|
||||
unitTests = testGroup "V1 Image Tests"
|
||||
[ testCase "Digest (De)Serialization" testDigest
|
||||
, testCase "Handle bad digest" testBadDigest1
|
||||
, testCase "Handle bad digest" testBadDigest2
|
||||
, testCase "Digest == ChainID" testChainID
|
||||
, testCase "Digest == DiffID" testDiffID
|
||||
, testCase "ChainID sequence generation" testChainIDGeneration
|
||||
]
|
||||
|
||||
mkHash = Lib.sha256 "somestring"
|
||||
|
||||
-- DiffID sequence from a real Docker Image.
|
||||
diffIds :: Seq DiffID
|
||||
diffIds = fromList $ fmap (DiffID . fromJust . toDigest)
|
||||
[ "sha256:f96222d75c5563900bc4dd852179b720a0885de8f7a0619ba0ac76e92542bbc8"
|
||||
, "sha256:149636c850120e59e6bb79f2fc23ed423030afc73841c221906a147d61da11a9"
|
||||
, "sha256:33c3a104206aed2ae947e03c48cc011af0a3e5b87e7ba8e7cbc298273a638921"
|
||||
, "sha256:2681a05b8f8288a384dbddf0b899ec9d2bea3ee265f1678230d0bdac6dc13da1"
|
||||
, "sha256:dcfda398b984bb5a55e1932079b6cc4823e433bd6c962f9667eaf28b0f1fe7e0"
|
||||
, "sha256:2a182bf72d68b9c7cb76be0f9dcadd047088ae6f8cb85e7ac9661f68537acccd"
|
||||
, "sha256:647af69f55fd5fdc27db7b6aa51945aec53b0b03d17095e79b4c69c6432195c7"
|
||||
, "sha256:c7ef4827bb9592e9788c1cc49e3db4e265c12f49c9b1f6c9bb99551eb5189020"
|
||||
, "sha256:f9361c1f9b1eb2d93709546fe6ad48786cea55c03c4e52d3f1cdb341e0d398da"
|
||||
]
|
||||
|
||||
-- Pre-computed golden result produced by a valid Python
|
||||
-- implementation of the ChainID sequence generation logic.
|
||||
preComputedChainIds :: Seq ChainID
|
||||
preComputedChainIds = fromList $ fmap (ChainID . fromJust . toDigest)
|
||||
[ "sha256:f96222d75c5563900bc4dd852179b720a0885de8f7a0619ba0ac76e92542bbc8"
|
||||
, "sha256:5e6f832cd2df18460af48ed117c5b63bc2189971c9346e6d952376b5a8ba74ff"
|
||||
, "sha256:19947c09eddb9dab0d1b938660cd72ea4bb8f0f24c604cf9e1d9b14772d7bd6d"
|
||||
, "sha256:b0fbea1a99ec834d59e524733f1be81f1dce325dbe9df58bba5dec7014b386c8"
|
||||
, "sha256:262faf2cc4db81d3bcb526099b7dc33069b24f4028a9a23d46edca2493077ce0"
|
||||
, "sha256:ac07dba5e07787c2a10edc3f8d8052f38cb5bec6767520bbab4289cb55b3a3f4"
|
||||
, "sha256:c781557b490e1e8ff2132af386abe2a9c2d3cb66df06ee2cbd489d869432328a"
|
||||
, "sha256:ff275e52e374819094e8035459820bf8e5fc42f287f603b445a8aee7aba2b689"
|
||||
, "sha256:ffd859ffb35598eeec1283f3ccb3633f2798c042343425f635d616633cf63c2b"
|
||||
]
|
||||
|
||||
testDigest =
|
||||
let digest = mkHash
|
||||
digestStr = showSHA digest
|
||||
in toDigest (C8.pack digestStr) @?= (Just digest)
|
||||
|
||||
testBadDigest1 = toDigest "ffd859ffb35598eeec1283f3ccb3633f2798c042343425f635d616633cf63c2b" @?= Nothing
|
||||
testBadDigest2 = toDigest "ffd859ffb35598eeec1283f3corrupt?" @?= Nothing
|
||||
|
||||
testChainID =
|
||||
let digest = mkHash
|
||||
in (show $ ChainID digest) @?= showSHA digest
|
||||
|
||||
testDiffID =
|
||||
let digest = mkHash
|
||||
in (show $ DiffID digest) @?= showSHA digest
|
||||
|
||||
testChainIDGeneration =
|
||||
let chainIDs = squishMaybe $ chainIDSequence diffIds
|
||||
in chainIDs @?= preComputedChainIds
|
74
test/Tests/Data/Docker/Image/V1_2.hs
Normal file
74
test/Tests/Data/Docker/Image/V1_2.hs
Normal file
@ -0,0 +1,74 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Image.V1_2
|
||||
-- Copyright : (C) 2016 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Tests.Data.Docker.Image.V1_2 where
|
||||
|
||||
import Data.Aeson
|
||||
import qualified Data.ByteString.Lazy.Char8 as C8L
|
||||
import Data.Docker.Image.V1_2.Types
|
||||
import Data.HashMap.Strict as H
|
||||
import Test.Tasty
|
||||
import Test.Tasty.HUnit
|
||||
|
||||
import Lib
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
--
|
||||
unitTests = testGroup "V1.2 Image Tests"
|
||||
[ testCase "ImageManifest golden encoding" testImageManifestGoldenEncoding
|
||||
, testCase "ImageManifest two-way encoding" testImageManifestTwoWayEncoding
|
||||
, testCase "ImageRepositories golden encoding" testImageRepositoriesGoldenEncoding
|
||||
, testCase "ImageRepositories two-way encoding" testImageRepositoriesTwoWayEncoding
|
||||
]
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- TESTS
|
||||
|
||||
testImageManifestGoldenEncoding =
|
||||
let goldenStr = "[{\"Config\":\"3e83c23dba6a16cd936a3dc044df71b26706c5a4c28181bc3ca4a4af9f5f38ee.json\",\"Layers\":[\"10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9/layer.tar\"],\"RepoTags\":[\"library/debian:jessie\"]}]"
|
||||
imgManifest = [ImageManifest
|
||||
"3e83c23dba6a16cd936a3dc044df71b26706c5a4c28181bc3ca4a4af9f5f38ee.json"
|
||||
[ "library/debian:jessie" ]
|
||||
[ "10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9/layer.tar" ]
|
||||
]
|
||||
in (Lib.encodeCanonical imgManifest) @?= (C8L.pack goldenStr)
|
||||
|
||||
testImageManifestTwoWayEncoding =
|
||||
let imgManifest = [ImageManifest
|
||||
"3e83c23dba6a16cd936a3dc044df71b26706c5a4c28181bc3ca4a4af9f5f38ee.json"
|
||||
[ "library/debian:jessie" ]
|
||||
[ "10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9/layer.tar" ]
|
||||
]
|
||||
encoded = Lib.encodeCanonical imgManifest
|
||||
in decode encoded @?= (Just imgManifest)
|
||||
|
||||
testImageRepositoriesGoldenEncoding =
|
||||
let goldenStr = "{\"library/debian\":{\"jessie\":\"10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9\"}}"
|
||||
imgRepos = ImageRepositories
|
||||
[ImageRepo
|
||||
"library/debian"
|
||||
(H.singleton
|
||||
"jessie"
|
||||
"10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9")]
|
||||
|
||||
in (Lib.encodeCanonical imgRepos) @?= (C8L.pack goldenStr)
|
||||
|
||||
testImageRepositoriesTwoWayEncoding =
|
||||
let imgRepos = ImageRepositories
|
||||
[ImageRepo
|
||||
"library/debian"
|
||||
(H.singleton
|
||||
"jessie"
|
||||
"10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9")]
|
||||
encoded = Lib.encodeCanonical imgRepos
|
||||
in decode encoded @?= (Just imgRepos)
|
73
test/Tests/Data/Docker/Nix/FetchDocker.hs
Normal file
73
test/Tests/Data/Docker/Nix/FetchDocker.hs
Normal file
@ -0,0 +1,73 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Data.Docker.Nix.FetchDocker
|
||||
-- Copyright : (C) 2017 Awake Networks
|
||||
-- License : AllRightsReserved
|
||||
-- Maintainer : Awake Networks <opensource@awakenetworks.com>
|
||||
-- Stability : stable
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
module Tests.Data.Docker.Nix.FetchDocker where
|
||||
|
||||
import Control.Exception as CE
|
||||
import Control.Monad.Except as Except
|
||||
import Data.ByteString.Lazy.Char8 as C8L
|
||||
import Data.Either (either)
|
||||
import qualified Data.Text as T
|
||||
import Network.URI
|
||||
import Test.Tasty
|
||||
import Test.Tasty.Golden
|
||||
import Test.Tasty.HUnit
|
||||
import Text.PrettyPrint.ANSI.Leijen as Text.PrettyPrint (displayS)
|
||||
|
||||
import Data.Docker.Image.Types
|
||||
import Data.Docker.Nix.FetchDocker as Nix.FetchDocker
|
||||
import Data.Docker.Nix.Lib as Nix.Lib
|
||||
import Lib
|
||||
import Network.Wreq.Docker.Registry.V2 as Docker.Registry
|
||||
import Types
|
||||
import Types.ImageTag
|
||||
|
||||
tests = testGroup "FetchDocker Nix Generation Tests"
|
||||
[ goldenVsString
|
||||
"Golden vs. Generated `fetchDocker' Nix Expression"
|
||||
"test/data/golden-debian:jessie.nix"
|
||||
generateFetchDockerNix
|
||||
, testCase "Base16 Digest to Base32 Digest" testBase16toBase32
|
||||
]
|
||||
|
||||
testBase16toBase32 :: Assertion
|
||||
testBase16toBase32 = do
|
||||
let b16 = Base16Digest "5c90d4a2d1a8dfffd05ff2dd659923f0ca2d843b5e45d030e17abbcd06a11b5b"
|
||||
b32 = Base32Digest "0nqvl43cvfvsw4qd0iay7f22vjph4fcnbpgjbz8gzpx8s6id942w"
|
||||
|
||||
res <- Except.runExceptT $ do
|
||||
nixhash <- Lib.findExec "nix-hash"
|
||||
Nix.Lib.toBase32Nix nixhash b16
|
||||
|
||||
either
|
||||
(assertFailure . show)
|
||||
(assertEqual "" b32)
|
||||
res
|
||||
|
||||
generateFetchDockerNix :: IO C8L.ByteString
|
||||
generateFetchDockerNix = do
|
||||
manifest <- C8L.readFile "test/data/manifest-debian:jessie.json"
|
||||
nixExpression <- Nix.FetchDocker.generate
|
||||
HockerImageMeta
|
||||
{ imageRepo = "library"
|
||||
, imageName = "debian"
|
||||
, imageTag = ImageTag "jessie"
|
||||
, manifestJSON = manifest
|
||||
, dockerRegistry = defaultRegistry
|
||||
, altImageName = Nothing
|
||||
}
|
||||
|
||||
either
|
||||
(Lib.die . T.pack . show)
|
||||
(return . C8L.pack . (flip displayS "") . Lib.renderNixExpr)
|
||||
nixExpression
|
20
test/data/golden-debian:jessie.nix
Normal file
20
test/data/golden-debian:jessie.nix
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
config.docker.images.debian = pkgs.fetchdocker {
|
||||
name = "debian";
|
||||
registry = "https://registry-1.docker.io/v2/";
|
||||
repository = "library";
|
||||
imageName = "debian";
|
||||
tag = "jessie";
|
||||
imageConfig = pkgs.fetchDockerConfig {
|
||||
inherit registry repository imageName tag;
|
||||
sha256 = "1rwinmvfc8jxn54y7qnj82acrc97y7xcnn22zaz67y76n4wbwjh5";
|
||||
};
|
||||
imageLayers = let
|
||||
layer0 = pkgs.fetchDockerLayer {
|
||||
inherit registry repository imageName tag;
|
||||
layerDigest = "cd0a524342efac6edff500c17e625735bbe479c926439b263bbe3c8518a0849c";
|
||||
sha256 = "1744l0c8ag5y7ck9nhr6r5wy9frmaxi7xh80ypgnxb7g891m42nd";
|
||||
};
|
||||
in [ layer0 ];
|
||||
};
|
||||
}
|
16
test/data/manifest-debian:jessie.json
Normal file
16
test/data/manifest-debian:jessie.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1528,
|
||||
"digest": "sha256:054abe38b1e6f863befa4258cbfaf127b1cc9440d2e2e349b15d22e676b591e7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 52550276,
|
||||
"digest": "sha256:cd0a524342efac6edff500c17e625735bbe479c926439b263bbe3c8518a0849c"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user