Skip to content

Commit

Permalink
Add verifyTrace buildImage option
Browse files Browse the repository at this point in the history
This option can to be set to debug digest mismatch issue. It generates
a trace a build time and a trace at runtime. These trace contains all
file attribute written to the tar stream and the chechsum of all file
writter to the tar stream.

This could allow to identify differences because of the sandbox used
at build time while the Nix build sandbox is not used to run time,
when the image is pushed to a destination.
  • Loading branch information
nlewo committed May 8, 2024
1 parent 0b4d54f commit 0de8ec5
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 61 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,22 @@ in [this branch](https://github.com/nlewo/image/tree/nix).

For more information, refer to [the Go
documentation](https://pkg.go.dev/github.com/nlewo/nix2container).

## How to debug the "Digest did not match" issue

nix2container generates the digest of layers at build time, in the Nix
sandbox. At runtime, this digest is announced to the destination and
when it doesn't exist on this destination, the missing layer is
created by reading all required store paths (outside of the Nix sandbox).

Theorically, we should not observe any differences when reading store
paths at build time or at runtime. But, in practice, bugs exist and it
can be really hard to identify where the differences are.

The `bulidImage.verifyTrace` option allows you to easily identify
these differences. A trace is generated at build time and compared to
a trace generated at runtime. These traces contains the attributes and
checksum of all files written to the tar stream.

Note you also need to ensure `buildLayer.trace` is set to `true` to
all your layers.
28 changes: 26 additions & 2 deletions cmd/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"runtime"

Expand All @@ -14,13 +15,15 @@ import (
)

var fromImageFilename string
var traces []string
var traceOutput string

var imageCmd = &cobra.Command{
Use: "image OUTPUT-FILENAME CONFIG.JSON LAYERS-1.JSON LAYERS-2.JSON ...",
Short: "Generate an image.json file from a image configuration and layers",
Args: cobra.MinimumNArgs(3),
Run: func(cmd *cobra.Command, args []string) {
err := image(args[0], args[1], fromImageFilename, args[2:])
err := image(args[0], args[1], fromImageFilename, args[2:], traces, traceOutput)
if err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(1)
Expand Down Expand Up @@ -88,7 +91,7 @@ func imageFromManifest(outputFilename, manifestFilename string, blobsFilename st
return nil
}

func image(outputFilename, imageConfigPath string, fromImageFilename string, layerPaths []string) error {
func image(outputFilename, imageConfigPath string, fromImageFilename string, layerPaths []string, tracePaths []string, traceOutput string) error {
var imageConfig v1.ImageConfig
var image types.Image

Expand Down Expand Up @@ -139,12 +142,33 @@ func image(outputFilename, imageConfigPath string, fromImageFilename string, lay
return err
}
logrus.Infof("Image has been written to %s", outputFilename)

if len(tracePaths) > 0 {
destination, err := os.Create(traceOutput)
if err != nil {
return err
}
for _, path := range tracePaths {
source, err := os.Open(path)
if err != nil {
return err
}
defer source.Close()
_, err = io.Copy(destination, source)
if err != nil {
return err
}
}
logrus.Infof("Image trace has been written to %s", traceOutput)
}
return nil
}

func init() {
rootCmd.AddCommand(imageCmd)
imageCmd.Flags().StringVarP(&fromImageFilename, "from-image", "", "", "A JSON file describing the base image")
imageCmd.Flags().StringSliceVar(&traces, "traces", traces, "The list of trace files")
imageCmd.Flags().StringVar(&traceOutput, "trace-output", "trace", "The path of the trace output")
rootCmd.AddCommand(imageFromDirCmd)
rootCmd.AddCommand(imageFromManifestCmd)
}
10 changes: 9 additions & 1 deletion cmd/layers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ var tarDirectory string
var permsFilepath string
var rewritesFilepath string
var maxLayers int
var traceFilename string

// layerCmd represents the layer command
var layersReproducibleCmd = &cobra.Command{
Use: "layers-from-reproducible-storepaths OUTPUT-FILENAME.JSON CLOSURE-GRAPH.JSON LAYER-1.JSON LAYER-2.json ...",
Short: "Generate a layers.json file from a list of reproducible paths",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
var err error
var layers []types.Layer
closureGraph, err := closure.ReadClosureGraphFile(args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
Expand Down Expand Up @@ -62,7 +65,11 @@ var layersReproducibleCmd = &cobra.Command{
os.Exit(1)
}
}
layers, err := nix.NewLayers(storepaths, maxLayers, parents, rewrites, ignore, perms)
if traceFilename == "" {
layers, err = nix.NewLayers(storepaths, maxLayers, parents, rewrites, ignore, perms)
} else {
layers, err = nix.NewLayersWithTrace(storepaths, maxLayers, parents, rewrites, ignore, perms, traceFilename)
}
if err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(1)
Expand Down Expand Up @@ -164,5 +171,6 @@ func init() {
layersReproducibleCmd.Flags().StringVarP(&rewritesFilepath, "rewrites", "", "", "A JSON file containing path rewrites")
layersReproducibleCmd.Flags().StringVarP(&permsFilepath, "perms", "", "", "A JSON file containing file permissions")
layersReproducibleCmd.Flags().IntVarP(&maxLayers, "max-layers", "", 1, "The maximum number of layers")
layersReproducibleCmd.Flags().StringVarP(&traceFilename, "trace-filename", "", "", "When set, generates a trace (be careful, it slows down the process)")

}
30 changes: 30 additions & 0 deletions cmd/trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cmd

import (
"fmt"
"os"

"github.com/nlewo/nix2container/nix"
"github.com/spf13/cobra"
)

var traceCmd = &cobra.Command{
Use: "trace IMAGE.JSON",
Short: "Generate a trace based on the image.json",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
image, err := nix.NewImageFromFile(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(1)
}

for _, l := range image.Layers {
nix.TarPathsTrace(l.Paths, os.Stdout)

Check failure on line 23 in cmd/trace.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `nix.TarPathsTrace` is not checked (errcheck)
}
},
}

func init() {
rootCmd.AddCommand(traceCmd)
}
30 changes: 27 additions & 3 deletions default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ let
maxLayers ? 1,
# Deprecated: will be removed on v1
contents ? null,
# Whether to generate a trace. Be careful, this slows down the
# process. (This only works when reproducible is true.)
trace ? false,
}: let
subcommand = if reproducible
then "layers-from-reproducible-storepaths"
Expand All @@ -271,6 +274,7 @@ let
permsFlag = l.optionalString (perms != []) "--perms ${permsFile}";
allDeps = deps ++ copyToRootList;
tarDirectory = l.optionalString (! reproducible) "--tar-directory $out";
traceFilename = l.optionalString trace "--trace-filename $out/trace";
layersJSON = pkgs.runCommand "layers.json" {} ''
mkdir $out
${nix2container-bin}/bin/nix2container ${subcommand} \
Expand All @@ -280,6 +284,7 @@ let
${rewritesFlag} \
${permsFlag} \
${tarDirectory} \
${traceFilename} \
${l.concatMapStringsSep " " (l: l + "/layers.json") layers} \
'';
in checked { inherit copyToRoot contents; } layersJSON;
Expand Down Expand Up @@ -394,6 +399,11 @@ let
# Deprecated: will be removed
contents ? null,
meta ? {},
# Whether to verify the buildtime trace and runtime trace are
# identical. This is only a debugging option which slows down the
# image construction process.
# This is especially useful to debug the famous "digest mismatch" issue.
verifyTrace ? false
}:
let
configFile = pkgs.writeText "config.json" (l.toJSON config);
Expand Down Expand Up @@ -438,9 +448,21 @@ let
deps = [configFile];
ignore = configFile;
layers = layers;
trace = verifyTrace;
};
fromImageFlag = l.optionalString (fromImage != "") "--from-image ${fromImage}";
layerPaths = l.concatMapStringsSep " " (l: l + "/layers.json") (layers ++ [customizationLayer]);
tracePaths = l.optionalString verifyTrace (l.concatMapStringsSep " " (l: "--traces " + l + "/trace") (layers ++ [customizationLayer]));
traceOutput = l.optionalString verifyTrace "--trace-output $out/trace";
traceCheck = l.optionalString verifyTrace ''
${nix2container-bin}/bin/nix2container trace $out/image.json > trace
if cmp -s trace $out/trace; then
printf 'The build time trace "%s" and run time trace "%s" are identical' "$out/image" "${nix2container-bin}/bin/nix2container trace $out/image.json"
else
printf 'The buildtime trace "%s" and runtime trace "%s" are different' "$out/image" "${nix2container-bin}/bin/nix2container trace $out/image.json"
printf 'Hints: you need to manually compare these files to know identify different store paths'
fi
'';
image = let
imageName = l.toLower name;
imageTag =
Expand All @@ -463,14 +485,16 @@ let
copyTo = copyTo image;
};
}
''
(''
mkdir $out
${nix2container-bin}/bin/nix2container image \
$out/image.json \
${fromImageFlag} \
${configFile} \
${layerPaths}
'';
${layerPaths} \
${tracePaths} \
${traceOutput}
'' + traceCheck);
in checked { inherit copyToRoot contents; } image;

checked = { copyToRoot, contents }:
Expand Down
20 changes: 17 additions & 3 deletions nix/layers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package nix
import (
_ "crypto/sha256"
_ "crypto/sha512"
"os"
"reflect"

"github.com/nlewo/nix2container/types"
Expand Down Expand Up @@ -64,7 +65,7 @@ func getPaths(storePaths []string, parents []types.Layer, rewrites []types.Rewri
// If tarDirectory is not an empty string, the tar layer is written to
// the disk. This is useful for layer containing non reproducible
// store paths.
func newLayers(paths types.Paths, tarDirectory string, maxLayers int) (layers []types.Layer, err error) {
func newLayers(paths types.Paths, tarDirectory string, traceFilename string, maxLayers int) (layers []types.Layer, err error) {
offset := 0
for offset < len(paths) {
max := offset + 1
Expand All @@ -80,6 +81,14 @@ func newLayers(paths types.Paths, tarDirectory string, maxLayers int) (layers []
} else {
layerPath, digest, size, err = TarPathsWrite(paths, tarDirectory)
}
if traceFilename != "" {
file, err := os.Create(traceFilename)
if err != nil {
return layers, err
}
defer file.Close()
TarPathsTrace(layerPaths, file)

Check failure on line 90 in nix/layers.go

View workflow job for this annotation

GitHub Actions / lint

Error return value is not checked (errcheck)
}
if err != nil {
return layers, err
}
Expand All @@ -104,14 +113,19 @@ func newLayers(paths types.Paths, tarDirectory string, maxLayers int) (layers []
return layers, nil
}

func NewLayersWithTrace(storePaths []string, maxLayers int, parents []types.Layer, rewrites []types.RewritePath, exclude string, perms []types.PermPath, traceFilename string) ([]types.Layer, error) {
paths := getPaths(storePaths, parents, rewrites, exclude, perms)
return newLayers(paths, "", traceFilename, maxLayers)
}

func NewLayers(storePaths []string, maxLayers int, parents []types.Layer, rewrites []types.RewritePath, exclude string, perms []types.PermPath) ([]types.Layer, error) {
paths := getPaths(storePaths, parents, rewrites, exclude, perms)
return newLayers(paths, "", maxLayers)
return newLayers(paths, "", "", maxLayers)
}

func NewLayersNonReproducible(storePaths []string, maxLayers int, tarDirectory string, parents []types.Layer, rewrites []types.RewritePath, exclude string, perms []types.PermPath) (layers []types.Layer, err error) {
paths := getPaths(storePaths, parents, rewrites, exclude, perms)
return newLayers(paths, tarDirectory, maxLayers)
return newLayers(paths, tarDirectory, "", maxLayers)
}

func isPathInLayers(layers []types.Layer, path types.Path) bool {
Expand Down
Loading

0 comments on commit 0de8ec5

Please sign in to comment.