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

     1  package builder
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	stdlog "log"
     9  	"net/url"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/Cloud-Foundations/Dominator/lib/filesystem"
    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/fsutil"
    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  type gitInfoType struct {
    29  	branch   string
    30  	commitId string
    31  }
    32  
    33  func (stream *imageStreamType) build(b *Builder, client *srpc.Client,
    34  	request proto.BuildImageRequest, buildLog buildLogger) (
    35  	*image.Image, error) {
    36  	manifestDirectory, gitInfo, err := stream.getManifest(b, request.StreamName,
    37  		request.GitBranch, request.Variables, buildLog)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	defer os.RemoveAll(manifestDirectory)
    42  	img, err := buildImageFromManifest(client, manifestDirectory, request,
    43  		b.bindMounts, stream, gitInfo, buildLog)
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	return img, nil
    48  }
    49  
    50  func (stream *imageStreamType) getenv() map[string]string {
    51  	envTable := make(map[string]string, 1)
    52  	envTable["IMAGE_STREAM"] = stream.name
    53  	envTable["IMAGE_STREAM_DIRECTORY_NAME"] = filepath.Dir(stream.name)
    54  	envTable["IMAGE_STREAM_LEAF_NAME"] = filepath.Base(stream.name)
    55  	return envTable
    56  }
    57  
    58  func (stream *imageStreamType) getManifest(b *Builder, streamName string,
    59  	gitBranch string, variables map[string]string,
    60  	buildLog io.Writer) (string, *gitInfoType, error) {
    61  	if gitBranch == "" {
    62  		gitBranch = "master"
    63  	}
    64  	variableFunc := b.getVariableFunc(stream.getenv(), variables)
    65  	manifestRoot, err := makeTempDirectory("",
    66  		strings.Replace(streamName, "/", "_", -1)+".manifest")
    67  	if err != nil {
    68  		return "", nil, err
    69  	}
    70  	doCleanup := true
    71  	defer func() {
    72  		if doCleanup {
    73  			os.RemoveAll(manifestRoot)
    74  		}
    75  	}()
    76  	manifestDirectory := os.Expand(stream.ManifestDirectory, variableFunc)
    77  	manifestUrl := os.Expand(stream.ManifestUrl, variableFunc)
    78  	if parsedUrl, err := url.Parse(manifestUrl); err == nil {
    79  		if parsedUrl.Scheme == "dir" {
    80  			if parsedUrl.Path[0] != '/' {
    81  				return "", nil, fmt.Errorf("missing leading slash: %s",
    82  					parsedUrl.Path)
    83  			}
    84  			if gitBranch != "master" {
    85  				return "", nil,
    86  					fmt.Errorf("branch: %s is not master", gitBranch)
    87  			}
    88  			sourceTree := filepath.Join(parsedUrl.Path, manifestDirectory)
    89  			fmt.Fprintf(buildLog, "Copying manifest tree: %s\n", sourceTree)
    90  			if err := fsutil.CopyTree(manifestRoot, sourceTree); err != nil {
    91  				return "", nil, fmt.Errorf("error copying manifest: %s", err)
    92  			}
    93  			doCleanup = false
    94  			return manifestRoot, nil, nil
    95  		}
    96  	}
    97  	fmt.Fprintf(buildLog, "Cloning repository: %s branch: %s\n",
    98  		stream.ManifestUrl, gitBranch)
    99  	err = runCommand(buildLog, "", "git", "init", manifestRoot)
   100  	if err != nil {
   101  		return "", nil, err
   102  	}
   103  	err = runCommand(buildLog, manifestRoot, "git", "remote", "add", "origin",
   104  		manifestUrl)
   105  	if err != nil {
   106  		return "", nil, err
   107  	}
   108  	err = runCommand(buildLog, manifestRoot, "git", "config",
   109  		"core.sparsecheckout", "true")
   110  	if err != nil {
   111  		return "", nil, err
   112  	}
   113  	directorySelector := "*\n"
   114  	if manifestDirectory != "" {
   115  		directorySelector = manifestDirectory + "/*\n"
   116  	}
   117  	err = ioutil.WriteFile(
   118  		filepath.Join(manifestRoot, ".git", "info", "sparse-checkout"),
   119  		[]byte(directorySelector), 0644)
   120  	if err != nil {
   121  		return "", nil, err
   122  	}
   123  	startTime := time.Now()
   124  	err = runCommand(buildLog, manifestRoot, "git", "pull", "--depth=1",
   125  		"origin", gitBranch)
   126  	if err != nil {
   127  		return "", nil, err
   128  	}
   129  	if gitBranch != "master" {
   130  		err = runCommand(buildLog, manifestRoot, "git", "checkout", gitBranch)
   131  		if err != nil {
   132  			return "", nil, err
   133  		}
   134  	}
   135  	loadTime := time.Since(startTime)
   136  	repoSize, err := getTreeSize(manifestRoot)
   137  	if err != nil {
   138  		return "", nil, err
   139  	}
   140  	speed := float64(repoSize) / loadTime.Seconds()
   141  	fmt.Fprintf(buildLog,
   142  		"Downloaded partial repository in %s, size: %s (%s/s)\n",
   143  		format.Duration(loadTime), format.FormatBytes(repoSize),
   144  		format.FormatBytes(uint64(speed)))
   145  	gitDirectory := filepath.Join(manifestRoot, ".git")
   146  	var gitInfo *gitInfoType
   147  	filename := filepath.Join(gitDirectory, "refs", "heads", gitBranch)
   148  	if lines, err := fsutil.LoadLines(filename); err != nil {
   149  		return "", nil, err
   150  	} else if len(lines) != 1 {
   151  		return "", nil, fmt.Errorf("%s does not have only one line", filename)
   152  	} else {
   153  		gitInfo = &gitInfoType{
   154  			branch:   gitBranch,
   155  			commitId: strings.TrimSpace(lines[0]),
   156  		}
   157  	}
   158  	if err := os.RemoveAll(gitDirectory); err != nil {
   159  		return "", nil, err
   160  	}
   161  	if manifestDirectory != "" {
   162  		// Move manifestDirectory into manifestRoot, remove anything else.
   163  		err := os.Rename(filepath.Join(manifestRoot, manifestDirectory),
   164  			gitDirectory)
   165  		if err != nil {
   166  			return "", nil, err
   167  		}
   168  		filenames, err := listDirectory(manifestRoot)
   169  		if err != nil {
   170  			return "", nil, err
   171  		}
   172  		for _, filename := range filenames {
   173  			if filename == ".git" {
   174  				continue
   175  			}
   176  			err := os.RemoveAll(filepath.Join(manifestRoot, filename))
   177  			if err != nil {
   178  				return "", nil, err
   179  			}
   180  		}
   181  		filenames, err = listDirectory(gitDirectory)
   182  		if err != nil {
   183  			return "", nil, err
   184  		}
   185  		for _, filename := range filenames {
   186  			err := os.Rename(filepath.Join(gitDirectory, filename),
   187  				filepath.Join(manifestRoot, filename))
   188  			if err != nil {
   189  				return "", nil, err
   190  			}
   191  		}
   192  		if err := os.Remove(gitDirectory); err != nil {
   193  			return "", nil, err
   194  		}
   195  	}
   196  	doCleanup = false
   197  	return manifestRoot, gitInfo, nil
   198  }
   199  
   200  func getTreeSize(dirname string) (uint64, error) {
   201  	var size uint64
   202  	err := filepath.Walk(dirname,
   203  		func(path string, info os.FileInfo, err error) error {
   204  			if err != nil {
   205  				return err
   206  			}
   207  			size += uint64(info.Size())
   208  			return nil
   209  		})
   210  	if err != nil {
   211  		return 0, err
   212  	}
   213  	return size, nil
   214  }
   215  
   216  func listDirectory(directoryName string) ([]string, error) {
   217  	directory, err := os.Open(directoryName)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	defer directory.Close()
   222  	filenames, err := directory.Readdirnames(-1)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return filenames, nil
   227  }
   228  
   229  func runCommand(buildLog io.Writer, cwd string, args ...string) error {
   230  	cmd := exec.Command(args[0], args[1:]...)
   231  	cmd.Dir = cwd
   232  	cmd.Stdout = buildLog
   233  	cmd.Stderr = buildLog
   234  	return cmd.Run()
   235  }
   236  
   237  func buildImageFromManifest(client *srpc.Client, manifestDir string,
   238  	request proto.BuildImageRequest, bindMounts []string,
   239  	envGetter environmentGetter, gitInfo *gitInfoType,
   240  	buildLog buildLogger) (*image.Image, error) {
   241  	// First load all the various manifest files (fail early on error).
   242  	computedFilesList, addComputedFiles, err := loadComputedFiles(manifestDir)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	imageFilter, addFilter, err := loadFilter(manifestDir)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  	imageTriggers, addTriggers, err := loadTriggers(manifestDir)
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  	rootDir, err := makeTempDirectory("",
   255  		strings.Replace(request.StreamName, "/", "_", -1)+".root")
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	defer os.RemoveAll(rootDir)
   260  	fmt.Fprintf(buildLog, "Created image working directory: %s\n", rootDir)
   261  	manifest, err := unpackImageAndProcessManifest(client, manifestDir,
   262  		rootDir, bindMounts, false, envGetter, buildLog)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	if fi, err := os.Lstat(filepath.Join(manifestDir, "tests")); err == nil {
   267  		if fi.IsDir() {
   268  			testsDir := filepath.Join(rootDir, "tests", request.StreamName)
   269  			if err := os.MkdirAll(testsDir, fsutil.DirPerms); err != nil {
   270  				return nil, err
   271  			}
   272  			err := copyFiles(manifestDir, "tests", testsDir, buildLog)
   273  			if err != nil {
   274  				return nil, err
   275  			}
   276  		}
   277  	}
   278  	if addComputedFiles {
   279  		computedFilesList = util.MergeComputedFiles(
   280  			manifest.sourceImageInfo.computedFiles, computedFilesList)
   281  	}
   282  	if addFilter {
   283  		mergeableFilter := &filter.MergeableFilter{}
   284  		mergeableFilter.Merge(manifest.sourceImageInfo.filter)
   285  		mergeableFilter.Merge(imageFilter)
   286  		imageFilter = mergeableFilter.ExportFilter()
   287  	}
   288  	if addTriggers {
   289  		mergeableTriggers := &triggers.MergeableTriggers{}
   290  		mergeableTriggers.Merge(manifest.sourceImageInfo.triggers)
   291  		mergeableTriggers.Merge(imageTriggers)
   292  		imageTriggers = mergeableTriggers.ExportTriggers()
   293  	}
   294  	img, err := packImage(client, request, rootDir, manifest.filter,
   295  		computedFilesList, imageFilter, imageTriggers, buildLog)
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	if gitInfo != nil {
   300  		img.BuildBranch = gitInfo.branch
   301  		img.BuildCommitId = gitInfo.commitId
   302  	}
   303  	return img, nil
   304  }
   305  
   306  func buildImageFromManifestAndUpload(client *srpc.Client, manifestDir string,
   307  	request proto.BuildImageRequest, bindMounts []string,
   308  	envGetter environmentGetter,
   309  	buildLog buildLogger) (*image.Image, string, error) {
   310  	img, err := buildImageFromManifest(client, manifestDir, request, bindMounts,
   311  		envGetter, nil, buildLog)
   312  	if err != nil {
   313  		return nil, "", err
   314  	}
   315  	name, err := addImage(client, request, img)
   316  	if err != nil {
   317  		return nil, "", err
   318  	}
   319  	return img, name, nil
   320  }
   321  
   322  func buildTreeFromManifest(client *srpc.Client, manifestDir string,
   323  	bindMounts []string, envGetter environmentGetter,
   324  	buildLog io.Writer) (string, error) {
   325  	rootDir, err := makeTempDirectory("", "tree")
   326  	if err != nil {
   327  		return "", err
   328  	}
   329  	_, err = unpackImageAndProcessManifest(client, manifestDir, rootDir,
   330  		bindMounts, true, envGetter, buildLog)
   331  	if err != nil {
   332  		os.RemoveAll(rootDir)
   333  		return "", err
   334  	}
   335  	return rootDir, nil
   336  }
   337  
   338  func listComputedFiles(fs *filesystem.FileSystem) []util.ComputedFile {
   339  	var computedFiles []util.ComputedFile
   340  	fs.ForEachFile(
   341  		func(path string, _ uint64, inode filesystem.GenericInode) error {
   342  			if inode, ok := inode.(*filesystem.ComputedRegularInode); ok {
   343  				computedFiles = append(computedFiles, util.ComputedFile{
   344  					Filename: path,
   345  					Source:   inode.Source,
   346  				})
   347  			}
   348  			return nil
   349  		})
   350  	return computedFiles
   351  }
   352  
   353  func loadComputedFiles(manifestDir string) ([]util.ComputedFile, bool, error) {
   354  	computedFiles, err := util.LoadComputedFiles(filepath.Join(manifestDir,
   355  		"computed-files.json"))
   356  	if os.IsNotExist(err) {
   357  		computedFiles, err = util.LoadComputedFiles(
   358  			filepath.Join(manifestDir, "computed-files"))
   359  	}
   360  	if err != nil && !os.IsNotExist(err) {
   361  		return nil, false, err
   362  	}
   363  	haveComputedFiles := err == nil
   364  	addComputedFiles, err := util.LoadComputedFiles(
   365  		filepath.Join(manifestDir, "computed-files.add.json"))
   366  	if os.IsNotExist(err) {
   367  		addComputedFiles, err = util.LoadComputedFiles(
   368  			filepath.Join(manifestDir, "computed-files.add"))
   369  	}
   370  	if err != nil && !os.IsNotExist(err) {
   371  		return nil, false, err
   372  	}
   373  	haveAddComputedFiles := err == nil
   374  	if !haveComputedFiles && !haveAddComputedFiles {
   375  		return nil, false, nil
   376  	} else if haveComputedFiles && haveAddComputedFiles {
   377  		return nil, false, errors.New(
   378  			"computed-files and computed-files.add files both present")
   379  	} else if haveComputedFiles {
   380  		return computedFiles, false, nil
   381  	} else {
   382  		return addComputedFiles, true, nil
   383  	}
   384  }
   385  
   386  func loadFilter(manifestDir string) (*filter.Filter, bool, error) {
   387  	imageFilter, err := filter.Load(filepath.Join(manifestDir, "filter"))
   388  	if err != nil && !os.IsNotExist(err) {
   389  		return nil, false, err
   390  	}
   391  	addFilter, err := filter.Load(filepath.Join(manifestDir, "filter.add"))
   392  	if err != nil && !os.IsNotExist(err) {
   393  		return nil, false, err
   394  	}
   395  	if imageFilter == nil && addFilter == nil {
   396  		return nil, false, nil
   397  	} else if imageFilter != nil && addFilter != nil {
   398  		return nil, false, errors.New(
   399  			"filter and filter.add files both present")
   400  	} else if imageFilter != nil {
   401  		return imageFilter, false, nil
   402  	} else {
   403  		return addFilter, true, nil
   404  	}
   405  }
   406  
   407  func loadTriggers(manifestDir string) (*triggers.Triggers, bool, error) {
   408  	imageTriggers, err := triggers.Load(filepath.Join(manifestDir, "triggers"))
   409  	if err != nil && !os.IsNotExist(err) {
   410  		return nil, false, err
   411  	}
   412  	addTriggers, err := triggers.Load(filepath.Join(manifestDir,
   413  		"triggers.add"))
   414  	if err != nil && !os.IsNotExist(err) {
   415  		return nil, false, err
   416  	}
   417  	if imageTriggers == nil && addTriggers == nil {
   418  		return nil, false, nil
   419  	} else if imageTriggers != nil && addTriggers != nil {
   420  		return nil, false, errors.New(
   421  			"triggers and triggers.add files both present")
   422  	} else if imageTriggers != nil {
   423  		return imageTriggers, false, nil
   424  	} else {
   425  		return addTriggers, true, nil
   426  	}
   427  }
   428  
   429  func unpackImage(client *srpc.Client, streamName string,
   430  	maxSourceAge, expiresIn time.Duration, rootDir string,
   431  	buildLog io.Writer) (*sourceImageInfoType, error) {
   432  	imageName, sourceImage, err := getLatestImage(client, streamName, buildLog)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	if sourceImage == nil {
   437  		return nil, errors.New(errNoSourceImage + streamName)
   438  	}
   439  	if maxSourceAge > 0 && time.Since(sourceImage.CreatedOn) > maxSourceAge {
   440  		return nil, errors.New(errNoSourceImage + streamName)
   441  	}
   442  	objClient := objectclient.AttachObjectClient(client)
   443  	defer objClient.Close()
   444  	err = util.Unpack(sourceImage.FileSystem, objClient, rootDir,
   445  		stdlog.New(buildLog, "", 0))
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  	fmt.Fprintf(buildLog, "Source image: %s\n", imageName)
   450  	return &sourceImageInfoType{
   451  		listComputedFiles(sourceImage.FileSystem),
   452  		sourceImage.Filter,
   453  		sourceImage.Triggers,
   454  	}, nil
   455  }