github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/imagebuilder/builder/addImage.go (about)

     1  package builder
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"sort"
    12  	"time"
    13  
    14  	imageclient "github.com/Cloud-Foundations/Dominator/imageserver/client"
    15  	"github.com/Cloud-Foundations/Dominator/lib/filesystem"
    16  	"github.com/Cloud-Foundations/Dominator/lib/filesystem/scanner"
    17  	"github.com/Cloud-Foundations/Dominator/lib/filesystem/util"
    18  	"github.com/Cloud-Foundations/Dominator/lib/filter"
    19  	"github.com/Cloud-Foundations/Dominator/lib/format"
    20  	"github.com/Cloud-Foundations/Dominator/lib/hash"
    21  	"github.com/Cloud-Foundations/Dominator/lib/image"
    22  	objectclient "github.com/Cloud-Foundations/Dominator/lib/objectserver/client"
    23  	"github.com/Cloud-Foundations/Dominator/lib/srpc"
    24  	"github.com/Cloud-Foundations/Dominator/lib/triggers"
    25  	proto "github.com/Cloud-Foundations/Dominator/proto/imaginator"
    26  )
    27  
    28  const timeFormat = "2006-01-02:15:04:05"
    29  
    30  var errorTestTimedOut = errors.New("test timed out")
    31  
    32  type hasher struct {
    33  	objQ *objectclient.ObjectAdderQueue
    34  }
    35  
    36  type testResultType struct {
    37  	buffer   chan byte
    38  	duration time.Duration
    39  	err      error
    40  	prog     string
    41  }
    42  
    43  func (h *hasher) Hash(reader io.Reader, length uint64) (
    44  	hash.Hash, error) {
    45  	hash, err := h.objQ.Add(reader, length)
    46  	if err != nil {
    47  		return hash, errors.New("error sending image data: " + err.Error())
    48  	}
    49  	return hash, nil
    50  }
    51  
    52  func addImage(client *srpc.Client, request proto.BuildImageRequest,
    53  	img *image.Image) (string, error) {
    54  	if request.ExpiresIn > 0 {
    55  		img.ExpiresAt = time.Now().Add(request.ExpiresIn)
    56  	}
    57  	name := path.Join(request.StreamName, time.Now().Format(timeFormat))
    58  	if err := imageclient.AddImage(client, name, img); err != nil {
    59  		return "", errors.New("remote error: " + err.Error())
    60  	}
    61  	return name, nil
    62  }
    63  
    64  func buildFileSystem(client *srpc.Client, dirname string,
    65  	scanFilter *filter.Filter) (
    66  	*filesystem.FileSystem, error) {
    67  	var h hasher
    68  	var err error
    69  	h.objQ, err = objectclient.NewObjectAdderQueue(client)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	fs, err := buildFileSystemWithHasher(dirname, &h, scanFilter)
    74  	if err != nil {
    75  		h.objQ.Close()
    76  		return nil, err
    77  	}
    78  	err = h.objQ.Close()
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	return fs, nil
    83  }
    84  
    85  func buildFileSystemWithHasher(dirname string, h *hasher,
    86  	scanFilter *filter.Filter) (
    87  	*filesystem.FileSystem, error) {
    88  	fs, err := scanner.ScanFileSystem(dirname, nil, scanFilter, nil, h, nil)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	return &fs.FileSystem, nil
    93  }
    94  
    95  func listPackages(rootDir string) ([]image.Package, error) {
    96  	output := new(bytes.Buffer)
    97  	err := runInTarget(nil, output, rootDir, nil, packagerPathname,
    98  		"show-size-multiplier")
    99  	if err != nil {
   100  		return nil, fmt.Errorf("error getting size multiplier: %s", err)
   101  	}
   102  	sizeMultiplier := uint64(1)
   103  	nScanned, err := fmt.Fscanf(output, "%d", &sizeMultiplier)
   104  	if err != nil {
   105  		if err != io.EOF {
   106  			return nil, fmt.Errorf(
   107  				"error decoding size multiplier: %s", err)
   108  		}
   109  	} else if nScanned != 1 {
   110  		return nil, errors.New("malformed size multiplier")
   111  	}
   112  	output.Reset()
   113  	err = runInTarget(nil, output, rootDir, nil, packagerPathname, "list")
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	packageMap := make(map[string]image.Package)
   118  	for {
   119  		var name, version string
   120  		var size uint64
   121  		nScanned, err := fmt.Fscanf(output, "%s %s %d\n",
   122  			&name, &version, &size)
   123  		if err != nil {
   124  			if err == io.EOF {
   125  				break
   126  			}
   127  			return nil, err
   128  		}
   129  		if nScanned != 3 {
   130  			return nil, errors.New("malformed line")
   131  		}
   132  		packageMap[name] = image.Package{
   133  			Name:    name,
   134  			Size:    size * sizeMultiplier,
   135  			Version: version,
   136  		}
   137  	}
   138  	packageNames := make([]string, 0, len(packageMap))
   139  	for name := range packageMap {
   140  		packageNames = append(packageNames, name)
   141  	}
   142  	sort.Strings(packageNames)
   143  	var packages []image.Package
   144  	for _, name := range packageNames {
   145  		packages = append(packages, packageMap[name])
   146  	}
   147  	return packages, nil
   148  }
   149  
   150  func packImage(client *srpc.Client, request proto.BuildImageRequest,
   151  	dirname string, scanFilter *filter.Filter,
   152  	computedFilesList []util.ComputedFile, imageFilter *filter.Filter,
   153  	trig *triggers.Triggers, buildLog buildLogger) (*image.Image, error) {
   154  	packages, err := listPackages(dirname)
   155  	if err != nil {
   156  		return nil, fmt.Errorf("error listing packages: %s", err)
   157  	}
   158  	buildStartTime := time.Now()
   159  	fs, err := buildFileSystem(client, dirname, scanFilter)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("error building file-system: %s", err)
   162  	}
   163  	if err := util.SpliceComputedFiles(fs, computedFilesList); err != nil {
   164  		return nil, fmt.Errorf("error splicing computed files: %s", err)
   165  	}
   166  	fs.ComputeTotalDataBytes()
   167  	duration := time.Since(buildStartTime)
   168  	speed := uint64(float64(fs.TotalDataBytes) / duration.Seconds())
   169  	fmt.Fprintf(buildLog,
   170  		"Scanned file-system and uploaded %d objects (%s) in %s (%s/s)\n",
   171  		len(fs.InodeTable), format.FormatBytes(fs.TotalDataBytes),
   172  		format.Duration(duration), format.FormatBytes(speed))
   173  	_, oldImage, err := getLatestImage(client, request.StreamName, buildLog)
   174  	if err != nil {
   175  		return nil, fmt.Errorf("error getting latest image: %s", err)
   176  	} else if oldImage != nil {
   177  		patchStartTime := time.Now()
   178  		util.CopyMtimes(oldImage.FileSystem, fs)
   179  		fmt.Fprintf(buildLog, "Copied mtimes in %s\n",
   180  			format.Duration(time.Since(patchStartTime)))
   181  	}
   182  	if err := runTests(dirname, buildLog); err != nil {
   183  		return nil, err
   184  	}
   185  	objClient := objectclient.AttachObjectClient(client)
   186  	// Make a copy of the build log because AddObject() drains the buffer.
   187  	logReader := bytes.NewBuffer(buildLog.Bytes())
   188  	hashVal, _, err := objClient.AddObject(logReader, uint64(logReader.Len()),
   189  		nil)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	if err := objClient.Close(); err != nil {
   194  		return nil, err
   195  	}
   196  	img := &image.Image{
   197  		BuildLog:   &image.Annotation{Object: &hashVal},
   198  		FileSystem: fs,
   199  		Filter:     imageFilter,
   200  		Triggers:   trig,
   201  		Packages:   packages,
   202  	}
   203  	if err := img.Verify(); err != nil {
   204  		return nil, err
   205  	}
   206  	return img, nil
   207  }
   208  
   209  func runTests(rootDir string, buildLog buildLogger) error {
   210  	var testProgrammes []string
   211  	err := filepath.Walk(filepath.Join(rootDir, "tests"),
   212  		func(path string, fi os.FileInfo, err error) error {
   213  			if fi == nil || !fi.Mode().IsRegular() || fi.Mode()&0100 == 0 {
   214  				return nil
   215  			}
   216  			testProgrammes = append(testProgrammes, path[len(rootDir):])
   217  			return nil
   218  		})
   219  	if err != nil {
   220  		return err
   221  	}
   222  	if len(testProgrammes) < 1 {
   223  		return nil
   224  	}
   225  	fmt.Fprintf(buildLog, "Running %d tests\n", len(testProgrammes))
   226  	results := make(chan testResultType, 1)
   227  	for _, prog := range testProgrammes {
   228  		go func(prog string) {
   229  			results <- runTest(rootDir, prog)
   230  		}(prog)
   231  	}
   232  	numFailures := 0
   233  	for range testProgrammes {
   234  		result := <-results
   235  		io.Copy(buildLog, &result)
   236  		if result.err != nil {
   237  			fmt.Fprintf(buildLog, "error running: %s: %s\n",
   238  				result.prog, result.err)
   239  			numFailures++
   240  		} else {
   241  			fmt.Fprintf(buildLog, "%s passed in %s\n",
   242  				result.prog, format.Duration(result.duration))
   243  		}
   244  		fmt.Fprintln(buildLog)
   245  	}
   246  	if numFailures > 0 {
   247  		return fmt.Errorf("%d tests failed", numFailures)
   248  	}
   249  	return nil
   250  }
   251  
   252  func runTest(rootDir, prog string) testResultType {
   253  	startTime := time.Now()
   254  	result := testResultType{
   255  		buffer: make(chan byte, 4096),
   256  		prog:   prog,
   257  	}
   258  	errChannel := make(chan error, 1)
   259  	timer := time.NewTimer(time.Second * 10)
   260  	go func() {
   261  		errChannel <- runInTarget(nil, &result, rootDir, nil, packagerPathname,
   262  			"run", prog)
   263  	}()
   264  	select {
   265  	case result.err = <-errChannel:
   266  		result.duration = time.Since(startTime)
   267  	case <-timer.C:
   268  		result.err = errorTestTimedOut
   269  	}
   270  	return result
   271  }
   272  
   273  func (w *testResultType) Read(p []byte) (int, error) {
   274  	for count := 0; count < len(p); count++ {
   275  		select {
   276  		case p[count] = <-w.buffer:
   277  		default:
   278  			return count, io.EOF
   279  		}
   280  	}
   281  	return len(p), nil
   282  }
   283  
   284  func (w *testResultType) Write(p []byte) (int, error) {
   285  	for index, ch := range p {
   286  		select {
   287  		case w.buffer <- ch:
   288  		default:
   289  			return index, io.ErrShortWrite
   290  		}
   291  	}
   292  	return len(p), nil
   293  }