github.com/Cloud-Foundations/Dominator@v0.3.4/imagebuilder/logarchiver/impl.go (about)

     1  package logarchiver
     2  
     3  import (
     4  	"container/list"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/Cloud-Foundations/Dominator/lib/format"
    14  	"github.com/Cloud-Foundations/Dominator/lib/fsutil"
    15  	"github.com/Cloud-Foundations/Dominator/lib/json"
    16  	"github.com/Cloud-Foundations/Dominator/lib/wsyscall"
    17  )
    18  
    19  type buildLogArchiver struct {
    20  	options           BuildLogArchiveOptions
    21  	params            BuildLogArchiveParams
    22  	fileSizeIncrement uint64
    23  	mutex             sync.Mutex                  // Lock everything below.
    24  	ageList           list.List                   // Oldest first.
    25  	imageStreams      map[string]*imageStreamType // Key: stream name.
    26  	totalSize         uint64
    27  }
    28  
    29  type imageStreamType struct {
    30  	images map[string]*imageType // Key: image leaf name.
    31  	name   string
    32  }
    33  
    34  type imageType struct {
    35  	ageListElement *list.Element
    36  	buildInfo      BuildInfo
    37  	imageStream    *imageStreamType
    38  	logSize        uint64 // Rounded up.
    39  	modTime        time.Time
    40  	name           string // Leaf name.
    41  }
    42  
    43  func roundUp(value, increment uint64) uint64 {
    44  	numBlocks := value / increment
    45  	if numBlocks*increment == value {
    46  		return value
    47  	}
    48  	return (numBlocks + 1) * increment
    49  }
    50  
    51  func newBuildLogArchive(options BuildLogArchiveOptions,
    52  	params BuildLogArchiveParams) (*buildLogArchiver, error) {
    53  	archive := &buildLogArchiver{
    54  		imageStreams: make(map[string]*imageStreamType),
    55  		options:      options,
    56  		params:       params,
    57  	}
    58  	if err := archive.computeFileSizeIncrement(); err != nil {
    59  		return nil, fmt.Errorf("error computing file size increment: %s", err)
    60  	}
    61  	startTime := time.Now()
    62  	if err := archive.load(""); err != nil {
    63  		return nil, err
    64  	}
    65  	loadedTime := time.Now()
    66  	archive.makeAgeList()
    67  	sortedTime := time.Now()
    68  	archive.params.Logger.Printf(
    69  		"Loaded build log archive %s in %s, sorted in %s\n",
    70  		format.FormatBytes(archive.totalSize),
    71  		format.Duration(loadedTime.Sub(startTime)),
    72  		format.Duration(sortedTime.Sub(loadedTime)))
    73  	return archive, nil
    74  }
    75  
    76  // addEntry adds the image to the image stream and optionally adds it to the
    77  // back of the ageList.
    78  // No lock is taken.
    79  func (a *buildLogArchiver) addEntry(image *imageType, name string,
    80  	addToAgeList bool) {
    81  	streamName := filepath.Dir(name)
    82  	imageStream := a.imageStreams[streamName]
    83  	if imageStream == nil {
    84  		imageStream = &imageStreamType{
    85  			images: make(map[string]*imageType),
    86  			name:   streamName,
    87  		}
    88  		a.imageStreams[streamName] = imageStream
    89  	}
    90  	image.imageStream = imageStream
    91  	imageStream.images[image.name] = image
    92  	a.totalSize += a.imageTotalSize(image)
    93  	if addToAgeList {
    94  		image.ageListElement = a.ageList.PushBack(image)
    95  	}
    96  }
    97  
    98  // addEntryWithCheck checks to see if there is sufficient space (deleting old
    99  // entries if needed) and then adds the image to the image stream and the back
   100  // of the ageList.
   101  func (a *buildLogArchiver) addEntryWithCheck(image *imageType,
   102  	name string) error {
   103  	a.mutex.Lock()
   104  	defer a.mutex.Unlock()
   105  	for a.imageTotalSize(image)+a.totalSize < a.options.Quota {
   106  		a.addEntry(image, name, true)
   107  		return nil
   108  	}
   109  	targetSize := a.options.Quota * 95 / 100
   110  	if a.imageTotalSize(image)+targetSize > a.options.Quota {
   111  		targetSize -= a.imageTotalSize(image)
   112  	}
   113  	var deletedLogs uint
   114  	origTotalSize := a.totalSize
   115  	for a.totalSize > targetSize {
   116  		oldestElement := a.ageList.Front()
   117  		if err := a.deleteEntry(oldestElement); err != nil {
   118  			return err
   119  		}
   120  		a.ageList.Remove(oldestElement)
   121  		deletedLogs++
   122  	}
   123  	a.params.Logger.Printf("Deleted %d archived build logs consuming %s\n",
   124  		deletedLogs, format.FormatBytes(origTotalSize-a.totalSize))
   125  	a.addEntry(image, name, true)
   126  	return nil
   127  }
   128  
   129  func (a *buildLogArchiver) AddBuildLog(imageName string, buildInfo BuildInfo,
   130  	buildLog []byte) error {
   131  	dirname := filepath.Join(a.options.Topdir, imageName)
   132  	if err := os.MkdirAll(filepath.Dir(dirname), fsutil.DirPerms); err != nil {
   133  		return err
   134  	}
   135  	if err := os.Mkdir(dirname, fsutil.DirPerms); err != nil {
   136  		return err
   137  	}
   138  	doDelete := true
   139  	defer func() {
   140  		if doDelete {
   141  			os.RemoveAll(dirname)
   142  		}
   143  	}()
   144  	err := json.WriteToFile(filepath.Join(dirname, "buildInfo"),
   145  		fsutil.PublicFilePerms, "    ", buildInfo)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	logfile := filepath.Join(dirname, "buildLog")
   150  	err = ioutil.WriteFile(logfile, buildLog, fsutil.PublicFilePerms)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	image := a.makeEntry(buildInfo, uint64(len(buildLog)), time.Now(),
   155  		imageName)
   156  	if err := a.addEntryWithCheck(image, imageName); err != nil {
   157  		return err
   158  	}
   159  	doDelete = false
   160  	a.params.Logger.Debugf(0, "Archived build log for: %s, %s (%s total)\n",
   161  		imageName, format.FormatBytes(a.imageTotalSize(image)),
   162  		format.FormatBytes(a.totalSize))
   163  	return nil
   164  }
   165  
   166  func (a *buildLogArchiver) computeFileSizeIncrement() error {
   167  	if err := os.MkdirAll(a.options.Topdir, fsutil.DirPerms); err != nil {
   168  		return err
   169  	}
   170  	file, err := ioutil.TempFile(a.options.Topdir, "******")
   171  	if err != nil {
   172  		return err
   173  	}
   174  	filename := file.Name()
   175  	defer os.Remove(filename)
   176  	if _, err := file.Write([]byte{'\n'}); err != nil {
   177  		file.Close()
   178  		return err
   179  	}
   180  	if err := file.Close(); err != nil {
   181  		return err
   182  	}
   183  	var statbuf wsyscall.Stat_t
   184  	if err := wsyscall.Stat(filename, &statbuf); err != nil {
   185  		return err
   186  	}
   187  	if statbuf.Blocks < 1 {
   188  		statbuf.Blocks = 1
   189  	}
   190  	a.fileSizeIncrement = uint64(statbuf.Blocks) * 512
   191  	return nil
   192  }
   193  
   194  func (a *buildLogArchiver) deleteEntry(element *list.Element) error {
   195  	image := element.Value.(*imageType)
   196  	imageStream := image.imageStream
   197  	dirname := filepath.Join(a.options.Topdir, imageStream.name, image.name)
   198  	if err := os.RemoveAll(dirname); err != nil {
   199  		return err
   200  	}
   201  	delete(imageStream.images, image.name)
   202  	a.totalSize -= a.imageTotalSize(image)
   203  	return nil
   204  }
   205  
   206  func (a *buildLogArchiver) imageTotalSize(image *imageType) uint64 {
   207  	return image.logSize + a.fileSizeIncrement
   208  }
   209  
   210  func (a *buildLogArchiver) load(dirname string) error {
   211  	dirpath := filepath.Join(a.options.Topdir, dirname)
   212  	names, err := fsutil.ReadDirnames(dirpath, false)
   213  	if err != nil {
   214  		return err
   215  	}
   216  	var buildInfoPathname, buildLogPathname string
   217  	for _, name := range names {
   218  		switch name {
   219  		case "buildInfo":
   220  			buildInfoPathname = filepath.Join(dirpath, name)
   221  			continue
   222  		case "buildLog":
   223  			buildLogPathname = filepath.Join(dirpath, name)
   224  			continue
   225  		}
   226  		if err := a.load(filepath.Join(dirname, name)); err != nil {
   227  			return err
   228  		}
   229  	}
   230  	if buildLogPathname == "" {
   231  		return nil
   232  	}
   233  	var buildInfo BuildInfo
   234  	if buildInfoPathname != "" {
   235  		if err := json.ReadFromFile(buildInfoPathname, &buildInfo); err != nil {
   236  			return err
   237  		}
   238  	}
   239  	if fi, err := os.Stat(buildLogPathname); err != nil {
   240  		return err
   241  	} else {
   242  		image := a.makeEntry(buildInfo, uint64(fi.Size()), fi.ModTime(),
   243  			dirname)
   244  		a.addEntry(image, dirname, false)
   245  	}
   246  	return nil
   247  }
   248  
   249  func (a *buildLogArchiver) makeAgeList() {
   250  	var imageList []*imageType
   251  	for _, imageStream := range a.imageStreams {
   252  		for _, image := range imageStream.images {
   253  			imageList = append(imageList, image)
   254  		}
   255  	}
   256  	// Sort so that oldest mtime is the first slice entry.
   257  	sort.Slice(imageList, func(i, j int) bool {
   258  		return imageList[i].modTime.Before(imageList[j].modTime)
   259  	})
   260  	for _, image := range imageList {
   261  		image.ageListElement = a.ageList.PushBack(image)
   262  	}
   263  }
   264  
   265  func (a *buildLogArchiver) makeEntry(buildInfo BuildInfo, logSize uint64,
   266  	modTime time.Time, name string) *imageType {
   267  	image := &imageType{
   268  		buildInfo: buildInfo,
   269  		logSize:   roundUp(logSize, a.fileSizeIncrement),
   270  		modTime:   modTime,
   271  		name:      filepath.Base(name),
   272  	}
   273  	return image
   274  }