github.com/mgoltzsche/ctnr@v0.7.1-alpha/image/builder/imagebuilder_test.go (about)

     1  package builder
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/containers/image/types"
    14  	bstore "github.com/mgoltzsche/ctnr/bundle/store"
    15  	"github.com/mgoltzsche/ctnr/image"
    16  	"github.com/mgoltzsche/ctnr/image/builder/dockerfile"
    17  	istore "github.com/mgoltzsche/ctnr/image/store"
    18  	extlog "github.com/mgoltzsche/ctnr/pkg/log"
    19  	"github.com/mgoltzsche/ctnr/pkg/log/logrusadapt"
    20  	"github.com/mgoltzsche/ctnr/store"
    21  	"github.com/sirupsen/logrus"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  	"github.com/xeipuuv/gojsonpointer"
    25  )
    26  
    27  // Integration test
    28  func TestImageBuilder(t *testing.T) {
    29  	files, err := filepath.Glob("dockerfile/testfiles/*.test")
    30  	require.NoError(t, err)
    31  	tmpDir, err := ioutil.TempDir("", ".imagebuildertestdata-")
    32  	require.NoError(t, err)
    33  	defer os.RemoveAll(tmpDir)
    34  	srcDir := filepath.Join(tmpDir, "src")
    35  	err = os.Mkdir(srcDir, 0755)
    36  	require.NoError(t, err)
    37  	for _, f := range []string{"entrypoint.sh", "cfg-a.conf", "cfg-b.conf"} {
    38  		err = ioutil.WriteFile(filepath.Join(srcDir, f), []byte("x"), 0740)
    39  		require.NoError(t, err)
    40  	}
    41  	wd, err := os.Getwd()
    42  	require.NoError(t, err)
    43  	err = os.Chdir(tmpDir)
    44  	require.NoError(t, err)
    45  	defer os.Chdir(wd)
    46  	logger := logrus.New()
    47  	logger.Level = logrus.DebugLevel
    48  	logger.Out = os.Stdout
    49  	loggers := extlog.Loggers{
    50  		Error: logrusadapt.NewErrorLogger(logger),
    51  		Warn:  logrusadapt.NewWarnLogger(logger),
    52  		Info:  logrusadapt.NewInfoLogger(logger),
    53  		Debug: logrusadapt.NewDebugLogger(logger),
    54  	}
    55  
    56  	var baseImg *image.Image
    57  
    58  	for _, file := range files {
    59  		if file == "dockerfile/testfiles/10-add.test" {
    60  			continue
    61  		}
    62  		loggers.Info.Println("\n\n  TEST CASE " + file + "\n")
    63  		withNewTestee(t, tmpDir, loggers, func(testee *ImageBuilder) {
    64  			// Read input & assertion from file
    65  			b, err := ioutil.ReadFile(filepath.Join(wd, file))
    66  			require.NoError(t, err, filepath.Base(file))
    67  
    68  			// Run test
    69  			args := map[string]string{
    70  				"argp": "pval",
    71  			}
    72  			testee.SetImageResolver(ResolveDockerImage)
    73  			startTime := time.Now()
    74  			df, err := dockerfile.LoadDockerfile(b, srcDir, args, loggers.Warn)
    75  			require.NoError(t, err, filepath.Base(file))
    76  			err = df.Apply(testee)
    77  			require.NoError(t, err, filepath.Base(file))
    78  			imageId := testee.Image()
    79  			assert.NotNil(t, imageId, "resulting image", filepath.Base(file))
    80  			err = imageId.Validate()
    81  			require.NoError(t, err, "resulting image ID", filepath.Base(file))
    82  			if baseImg == nil {
    83  				img, err := testee.images.ImageByName("docker://alpine:3.7")
    84  				require.NoError(t, err, "get common base image from store after build completed")
    85  				baseImg = &img
    86  			}
    87  			img, err := testee.images.Image(imageId)
    88  			require.NoError(t, err, filepath.Base(file)+" load resulting image")
    89  			elapsedTime1 := time.Now().Sub(startTime)
    90  			cfg := img.Config
    91  
    92  			// Assert
    93  			assertions := []string{}
    94  			for _, line := range strings.Split(string(b), "\n") {
    95  				if strings.HasPrefix(line, "# ASSERT ") {
    96  					assertions = append(assertions, line[9:])
    97  				}
    98  			}
    99  			if len(assertions) == 0 {
   100  				t.Errorf("No assertion found in %s", filepath.Base(file))
   101  				t.FailNow()
   102  			}
   103  
   104  			for _, assertionExpr := range assertions {
   105  				loggers.Info.Println("ASSERTION "+file+":", assertionExpr)
   106  				switch assertionExpr[:3] {
   107  				case "RUN":
   108  					// Assert by running command
   109  					cmd := assertionExpr[4:]
   110  					err = testee.Run([]string{"/bin/sh", "-c", cmd}, nil)
   111  					require.NoError(t, err, filepath.Base(file)+" assertion")
   112  				case "ERR":
   113  					// Assert failing command results in error
   114  					cmd := assertionExpr[4:]
   115  					err = testee.Run([]string{"/bin/sh", "-c", cmd}, nil)
   116  					require.Error(t, err, filepath.Base(file)+" - should fail")
   117  				case "CFG":
   118  					// Assert by JSON query
   119  					query := assertionExpr[4:]
   120  					spacePos := strings.Index(query, "=")
   121  					expected := query[spacePos+1:]
   122  					query = query[:spacePos]
   123  					assertPathEqual(t, &cfg, query, expected, filepath.Base(file)+" assertion query: "+query)
   124  				case "STG":
   125  					startTime = time.Now()
   126  					stage := strings.TrimSpace(assertionExpr[4:])
   127  					df, err := dockerfile.LoadDockerfile(b, srcDir, args, loggers.Warn)
   128  					require.NoError(t, err, filepath.Base(file)+" assertion: stage load")
   129  					err = df.Target(stage)
   130  					require.NoError(t, err, filepath.Base(file)+" assertion: set target")
   131  					err = df.Apply(testee)
   132  					require.NoError(t, err, filepath.Base(file)+" assertion: apply stage")
   133  					// Test if the build was cached since it has been built previously
   134  					elapsedTime2 := time.Now().Sub(startTime)
   135  					if elapsedTime2 > elapsedTime1/2 {
   136  						t.Errorf(filepath.Base(file)+" assertion: stage %q execution took longer than half the full execution previously", stage)
   137  						t.FailNow()
   138  					}
   139  				default:
   140  					t.Errorf("Unsupported assertion in %s: %q", filepath.Base(file), assertionExpr)
   141  					t.FailNow()
   142  				}
   143  			}
   144  
   145  			// Test image size: image is too big it is likely that fsspec integration doesn't work
   146  			if img.Size() >= baseImg.Size()*2 {
   147  				t.Errorf("the whole base image seems to be copied into the next layer because new image size >= base image size * 2")
   148  				t.FailNow()
   149  			}
   150  		})
   151  	}
   152  }
   153  
   154  func assertPathEqual(t *testing.T, o interface{}, query, expected, msg string) {
   155  	jp, err := gojsonpointer.NewJsonPointer(query)
   156  	require.NoError(t, err, msg)
   157  	jsonDoc := map[string]interface{}{}
   158  	b, err := json.Marshal(&o)
   159  	require.NoError(t, err, msg)
   160  	err = json.Unmarshal(b, &jsonDoc)
   161  	require.NoError(t, err, msg)
   162  	valueStr := ""
   163  	match, _, err := jp.Get(jsonDoc)
   164  	if expected != "" {
   165  		require.NoError(t, err, msg)
   166  	}
   167  	if match != nil {
   168  		valueStr = fmt.Sprintf("%s", match)
   169  	}
   170  	if !assert.Equal(t, expected, valueStr, msg) {
   171  		t.FailNow()
   172  	}
   173  }
   174  
   175  func withNewTestee(t *testing.T, tmpDir string, loggers extlog.Loggers, assertions func(*ImageBuilder)) {
   176  	ctx := &types.SystemContext{DockerInsecureSkipTLSVerify: true}
   177  
   178  	// Init image store
   179  	storero, err := store.NewStore(filepath.Join(tmpDir, "image-store"), true, ctx, istore.TrustPolicyInsecure(), loggers)
   180  	require.NoError(t, err)
   181  	lockedStore, err := storero.OpenLockedImageStore()
   182  	require.NoError(t, err)
   183  	defer func() {
   184  		if err := lockedStore.Close(); err != nil {
   185  			t.Error("failed to close locked store: ", err)
   186  		}
   187  	}()
   188  
   189  	// Init bundle store
   190  	bundleDir := filepath.Join(tmpDir, "bundle-store")
   191  	bundleStore := bstore.NewBundleStore(bundleDir, loggers.Info, loggers.Debug)
   192  
   193  	// Init testee
   194  	builderTmpDir := filepath.Join(tmpDir, "tmp")
   195  	testee := NewImageBuilder(ImageBuildConfig{
   196  		Images:                 lockedStore,
   197  		Bundles:                bundleStore,
   198  		Cache:                  NewImageBuildCache(filepath.Join(tmpDir, "cache"), loggers.Warn),
   199  		Tempfs:                 builderTmpDir,
   200  		RunRoot:                filepath.Join(tmpDir, "run"),
   201  		Rootless:               true,
   202  		PRoot:                  "", // TODO: also test using proot
   203  		RemoveSucceededBundles: true,
   204  		RemoveFailedBundle:     true,
   205  		Loggers:                loggers,
   206  	})
   207  	defer func() {
   208  		if err := testee.Close(); err != nil {
   209  			t.Error("failed to close image builder: ", err)
   210  		}
   211  	}()
   212  
   213  	// Do tests
   214  	assertions(testee)
   215  
   216  	// Close builder
   217  	require.NoError(t, testee.Close(), "close image builder")
   218  
   219  	// Assert no temp files left
   220  	if _, err = os.Stat(builderTmpDir); err == nil {
   221  		files, err := ioutil.ReadDir(builderTmpDir)
   222  		require.NoError(t, err)
   223  		if !assert.True(t, len(files) == 0, "builder temp dir should contain no files after closed but found: %v", toFileNames(files)) {
   224  			t.FailNow()
   225  		}
   226  	}
   227  
   228  	// Assert no bundles left
   229  	if _, err = os.Stat(bundleDir); err == nil {
   230  		files, err := ioutil.ReadDir(bundleDir)
   231  		require.NoError(t, err)
   232  		if !assert.True(t, len(files) == 0, "bundle store dir should contain no files after closed but found: %v", toFileNames(files)) {
   233  			t.FailNow()
   234  		}
   235  	}
   236  }
   237  
   238  func toFileNames(files []os.FileInfo) []string {
   239  	s := make([]string, len(files))
   240  	for i, e := range files {
   241  		s[i] = e.Name()
   242  	}
   243  	return s
   244  }