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

     1  package builder
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/Cloud-Foundations/Dominator/lib/filesystem/util"
    13  	"github.com/Cloud-Foundations/Dominator/lib/filter"
    14  	"github.com/Cloud-Foundations/Dominator/lib/format"
    15  	"github.com/Cloud-Foundations/Dominator/lib/fsutil"
    16  	"github.com/Cloud-Foundations/Dominator/lib/goroutine"
    17  	"github.com/Cloud-Foundations/Dominator/lib/json"
    18  	"github.com/Cloud-Foundations/Dominator/lib/srpc"
    19  	"github.com/Cloud-Foundations/Dominator/lib/verstr"
    20  )
    21  
    22  func deleteDirectories(directoriesToDelete []string) error {
    23  	for index := len(directoriesToDelete) - 1; index >= 0; index-- {
    24  		if err := os.Remove(directoriesToDelete[index]); err != nil {
    25  			return err
    26  		}
    27  	}
    28  	return nil
    29  }
    30  
    31  func makeDirectory(directory string, directoriesToDelete []string,
    32  	directoriesWhichExist map[string]struct{},
    33  	bindMountDirectories map[string]struct{},
    34  	buildLog io.Writer) ([]string, error) {
    35  	if _, ok := directoriesWhichExist[directory]; ok {
    36  		return directoriesToDelete, nil
    37  	} else if fi, err := os.Stat(directory); err != nil {
    38  		if !os.IsNotExist(err) {
    39  			return directoriesToDelete, err
    40  		}
    41  		var err error
    42  		directoriesToDelete, err = makeDirectory(filepath.Dir(directory),
    43  			directoriesToDelete, directoriesWhichExist, bindMountDirectories,
    44  			buildLog)
    45  		if err != nil {
    46  			return directoriesToDelete, err
    47  		}
    48  		if _, ok := bindMountDirectories[directory]; ok {
    49  			fmt.Fprintf(buildLog, "Making bind mount point: %s\n", directory)
    50  		} else {
    51  			fmt.Fprintf(buildLog,
    52  				"Making intermediate directory for bind mount: %s\n",
    53  				directory)
    54  		}
    55  		if err := os.Mkdir(directory, fsutil.DirPerms); err != nil {
    56  			return nil, err
    57  		}
    58  		directoriesToDelete = append(directoriesToDelete, directory)
    59  		directoriesWhichExist[directory] = struct{}{}
    60  		return directoriesToDelete, nil
    61  	} else if !fi.IsDir() {
    62  		return directoriesToDelete,
    63  			fmt.Errorf("%s is not a directory", directory)
    64  	} else {
    65  		directoriesWhichExist[directory] = struct{}{}
    66  		return directoriesToDelete, nil
    67  	}
    68  }
    69  
    70  func makeMountPoints(rootDir string, bindMounts []string,
    71  	buildLog io.Writer) ([]string, error) {
    72  	var directoriesToDelete []string
    73  	directoriesWhichExist := make(map[string]struct{})
    74  	defer deleteDirectories(directoriesToDelete)
    75  	bindMountDirectories := make(map[string]struct{}, len(bindMounts))
    76  	for _, bindMount := range bindMounts {
    77  		bindMountDirectories[filepath.Join(rootDir, bindMount)] = struct{}{}
    78  	}
    79  	for _, bindMount := range bindMounts {
    80  		directory := filepath.Join(rootDir, bindMount)
    81  		var err error
    82  		directoriesToDelete, err = makeDirectory(directory, directoriesToDelete,
    83  			directoriesWhichExist, bindMountDirectories, buildLog)
    84  		if err != nil {
    85  			return nil, err
    86  		}
    87  	}
    88  	retval := directoriesToDelete
    89  	directoriesToDelete = nil // Do not clean up in the defer.
    90  	return retval, nil
    91  }
    92  
    93  // readManifestFile will read the manifest file in the manifest directory and
    94  // will apply variable expansion to the source image name using envGetter if not
    95  // nil.
    96  func readManifestFile(manifestDir string, envGetter environmentGetter) (
    97  	manifestConfigType, error) {
    98  	manifestFile := filepath.Join(manifestDir, "manifest")
    99  	var manifestConfig manifestConfigType
   100  	if err := json.ReadFromFile(manifestFile, &manifestConfig); err != nil {
   101  		return manifestConfigType{},
   102  			errors.New("error reading manifest file: " + err.Error())
   103  	}
   104  	if envGetter == nil {
   105  		return manifestConfig, nil
   106  	}
   107  	manifestConfig.SourceImage = expandExpression(manifestConfig.SourceImage,
   108  		func(name string) string {
   109  			return envGetter.getenv()[name]
   110  		})
   111  	for key, value := range manifestConfig.SourceImageBuildVariables {
   112  		newValue := expandExpression(value, func(name string) string {
   113  			return envGetter.getenv()[name]
   114  		})
   115  		manifestConfig.SourceImageBuildVariables[key] = newValue
   116  	}
   117  	manifestConfig.SourceImageGitCommitId = expandExpression(
   118  		manifestConfig.SourceImageGitCommitId,
   119  		func(name string) string {
   120  			return envGetter.getenv()[name]
   121  		})
   122  	for _, values := range manifestConfig.SourceImageTagsToMatch {
   123  		for index, value := range values {
   124  			newValue := expandExpression(value, func(name string) string {
   125  				return envGetter.getenv()[name]
   126  			})
   127  			values[index] = newValue
   128  		}
   129  	}
   130  	return manifestConfig, nil
   131  }
   132  
   133  func unpackImageAndProcessManifest(client srpc.ClientI, manifestDir string,
   134  	maxSourceAge time.Duration, rootDir string, bindMounts []string,
   135  	applyFilter bool, envGetter environmentGetter,
   136  	buildLog io.Writer) (manifestType, error) {
   137  	manifestConfig, err := readManifestFile(manifestDir, envGetter)
   138  	if err != nil {
   139  		return manifestType{}, err
   140  	}
   141  	var mtimesCopyAddFilter, mtimesCopyFilter *filter.Filter
   142  	if len(manifestConfig.MtimesCopyAddFilterLines) > 0 {
   143  		mtimesCopyAddFilter, err = filter.New(
   144  			manifestConfig.MtimesCopyAddFilterLines)
   145  		if err != nil {
   146  			return manifestType{}, err
   147  		}
   148  	}
   149  	if len(manifestConfig.MtimesCopyFilterLines) > 0 {
   150  		mtimesCopyFilter, err = filter.New(manifestConfig.MtimesCopyFilterLines)
   151  		if err != nil {
   152  			return manifestType{}, err
   153  		}
   154  	}
   155  	sourceImageInfo, err := unpackImage(client, manifestConfig.SourceImage,
   156  		manifestConfig.SourceImageGitCommitId,
   157  		manifestConfig.SourceImageTagsToMatch,
   158  		maxSourceAge, rootDir, buildLog)
   159  	if err != nil {
   160  		var buildError *buildErrorType
   161  		if errors.As(err, &buildError) {
   162  			buildError.sourceImageBuildVariables =
   163  				manifestConfig.SourceImageBuildVariables
   164  		}
   165  		return manifestType{}, fmt.Errorf("error unpacking image: %w", err)
   166  	}
   167  	startTime := time.Now()
   168  	err = processManifest(manifestDir, rootDir, bindMounts, envGetter, buildLog)
   169  	if err != nil {
   170  		return manifestType{},
   171  			errors.New("error processing manifest: " + err.Error())
   172  	}
   173  	if applyFilter && manifestConfig.Filter != nil {
   174  		err := util.DeletedFilteredFiles(rootDir, manifestConfig.Filter)
   175  		if err != nil {
   176  			return manifestType{}, err
   177  		}
   178  	}
   179  	fmt.Fprintf(buildLog, "Processed manifest in %s\n",
   180  		format.Duration(time.Since(startTime)))
   181  	return manifestType{
   182  		filter:              manifestConfig.Filter,
   183  		mtimesCopyAddFilter: mtimesCopyAddFilter,
   184  		mtimesCopyFilter:    mtimesCopyFilter,
   185  		sourceImageInfo:     sourceImageInfo,
   186  	}, nil
   187  }
   188  
   189  func processManifest(manifestDir, rootDir string, bindMounts []string,
   190  	envGetter environmentGetter, buildLog io.Writer) error {
   191  	// Copy in system /etc/resolv.conf
   192  	file, err := os.Open("/etc/resolv.conf")
   193  	if err != nil {
   194  		return err
   195  	}
   196  	defer file.Close()
   197  	for index, bindMount := range bindMounts {
   198  		bindMounts[index] = filepath.Clean(bindMount)
   199  	}
   200  	directoriesToDelete, err := makeMountPoints(rootDir, bindMounts, buildLog)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	defer func() { // Need to evaluate directoriesToDelete in deferred func.
   205  		deleteDirectories(directoriesToDelete)
   206  	}()
   207  	g, err := newNamespaceTargetWithMounts(rootDir, bindMounts)
   208  	if err != nil {
   209  		return err
   210  	}
   211  	defer g.Quit()
   212  	err = runInTarget(g, file, buildLog, rootDir, envGetter,
   213  		packagerPathname, "copy-in", "/etc/resolv.conf")
   214  	if err != nil {
   215  		return fmt.Errorf("error copying in /etc/resolv.conf: %s", err)
   216  	}
   217  	if err := copyFiles(manifestDir, "files", rootDir, buildLog); err != nil {
   218  		return err
   219  	}
   220  	err = runScripts(g, manifestDir, "pre-install-scripts", rootDir, envGetter,
   221  		buildLog)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	packageList, err := fsutil.LoadLines(filepath.Join(manifestDir,
   226  		"package-list"))
   227  	if err != nil {
   228  		if !os.IsNotExist(err) {
   229  			return err
   230  		}
   231  	}
   232  	if len(packageList) > 0 {
   233  		err := updatePackageDatabase(g, rootDir, envGetter, buildLog)
   234  		if err != nil {
   235  			return err
   236  		}
   237  	}
   238  	err = installPackages(g, packageList, rootDir, envGetter, buildLog)
   239  	if err != nil {
   240  		return errors.New("error installing packages: " + err.Error())
   241  	}
   242  	err = copyFiles(manifestDir, "post-install-files", rootDir, buildLog)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	err = runScripts(g, manifestDir, "scripts", rootDir, envGetter, buildLog)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	if err := cleanPackages(g, rootDir, buildLog); err != nil {
   251  		return err
   252  	}
   253  	if err := clearResolvConf(g, buildLog, rootDir); err != nil {
   254  		return err
   255  	}
   256  	err = copyFiles(manifestDir, "post-scripts-files", rootDir, buildLog)
   257  	if err != nil {
   258  		return err
   259  	}
   260  	if err := deleteDirectories(directoriesToDelete); err != nil {
   261  		return err
   262  	}
   263  	directoriesToDelete = nil
   264  	err = runScripts(nil, manifestDir, "post-cleanup-scripts", rootDir,
   265  		envGetter, buildLog)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	return nil
   270  }
   271  
   272  func copyFiles(manifestDir, dirname, rootDir string, buildLog io.Writer) error {
   273  	startTime := time.Now()
   274  	sourceDir := filepath.Join(manifestDir, dirname)
   275  	cf := func(destFilename, sourceFilename string, mode os.FileMode) error {
   276  		return copyFile(destFilename, sourceFilename, mode, len(manifestDir)+1,
   277  			buildLog)
   278  	}
   279  	if err := fsutil.CopyTreeWithCopyFunc(rootDir, sourceDir, cf); err != nil {
   280  		return fmt.Errorf("error copying %s: %s", dirname, err)
   281  	}
   282  	fmt.Fprintf(buildLog, "\nCopied %s tree in %s\n",
   283  		dirname, format.Duration(time.Since(startTime)))
   284  	return nil
   285  }
   286  
   287  func copyFile(destFilename, sourceFilename string, mode os.FileMode,
   288  	prefixLength int, buildLog io.Writer) error {
   289  	same, err := fsutil.CompareFiles(destFilename, sourceFilename)
   290  	if err != nil && !os.IsNotExist(err) {
   291  		return err
   292  	}
   293  	if same {
   294  		fmt.Fprintf(buildLog, "Same contents for: %s\n",
   295  			sourceFilename[prefixLength:])
   296  		return nil
   297  	}
   298  	return fsutil.CopyFile(destFilename, sourceFilename, mode)
   299  }
   300  
   301  func installPackages(g *goroutine.Goroutine, packageList []string,
   302  	rootDir string, envGetter environmentGetter, buildLog io.Writer) error {
   303  	if len(packageList) < 1 { // Nothing to do.
   304  		fmt.Fprintln(buildLog, "\nNo packages to install")
   305  		return nil
   306  	}
   307  	fmt.Fprintln(buildLog, "\nUpgrading packages:")
   308  	startTime := time.Now()
   309  	err := runInTarget(g, nil, buildLog, rootDir, envGetter,
   310  		packagerPathname, "upgrade")
   311  	if err != nil {
   312  		return errors.New("error upgrading: " + err.Error())
   313  	}
   314  	fmt.Fprintf(buildLog, "Package upgrade took: %s\n",
   315  		format.Duration(time.Since(startTime)))
   316  
   317  	fmt.Fprintln(buildLog, "\nInstalling packages:",
   318  		strings.Join(packageList, " "))
   319  	startTime = time.Now()
   320  	args := []string{"install"}
   321  	args = append(args, packageList...)
   322  	err = runInTarget(g, nil, buildLog, rootDir, envGetter,
   323  		packagerPathname, args...)
   324  	if err != nil {
   325  		return errors.New("error installing: " + err.Error())
   326  	}
   327  	fmt.Fprintf(buildLog, "Package install took: %s\n",
   328  		format.Duration(time.Since(startTime)))
   329  	return nil
   330  }
   331  
   332  func runScripts(g *goroutine.Goroutine, manifestDir, dirname, rootDir string,
   333  	envGetter environmentGetter, buildLog io.Writer) error {
   334  	scriptsDir := filepath.Join(manifestDir, dirname)
   335  	file, err := os.Open(scriptsDir)
   336  	if err != nil {
   337  		if os.IsNotExist(err) {
   338  			fmt.Fprintf(buildLog, "No %s directory\n", dirname)
   339  			return nil
   340  		}
   341  		return err
   342  	}
   343  	names, err := file.Readdirnames(-1)
   344  	file.Close()
   345  	if err != nil {
   346  		return err
   347  	}
   348  	if len(names) < 1 {
   349  		fmt.Fprintln(buildLog, "\nNo scripts to run")
   350  		return nil
   351  	}
   352  	verstr.Sort(names)
   353  	fmt.Fprintf(buildLog, "\nRunning scripts in: %s\n", dirname)
   354  	scriptsStartTime := time.Now()
   355  	tmpDir := filepath.Join(rootDir, ".scripts")
   356  	if err := os.Mkdir(tmpDir, dirPerms); err != nil {
   357  		return err
   358  	}
   359  	defer os.RemoveAll(tmpDir)
   360  	for _, name := range names {
   361  		if len(name) > 0 && name[0] == '.' {
   362  			continue // Skip hidden paths.
   363  		}
   364  		err := fsutil.CopyFile(filepath.Join(tmpDir, name),
   365  			filepath.Join(scriptsDir, name),
   366  			dirPerms)
   367  		if err != nil {
   368  			return err
   369  		}
   370  	}
   371  	if g == nil {
   372  		g, err = newNamespaceTargetWithMounts(rootDir, nil)
   373  		if err != nil {
   374  			return err
   375  		}
   376  		defer g.Quit()
   377  	}
   378  	for _, name := range names {
   379  		fmt.Fprintf(buildLog, "Running script: %s\n", name)
   380  		startTime := time.Now()
   381  		err := runInTarget(g, nil, buildLog, rootDir, envGetter,
   382  			packagerPathname, "run", filepath.Join("/.scripts", name))
   383  		if err != nil {
   384  			return errors.New("error running script: " + name + ": " +
   385  				err.Error())
   386  		}
   387  		timeTaken := time.Since(startTime)
   388  		fmt.Fprintf(buildLog, "Script: %s took %s\n",
   389  			name, format.Duration(timeTaken))
   390  		fmt.Fprintln(buildLog,
   391  			"=================================================================")
   392  	}
   393  	timeTaken := time.Since(scriptsStartTime)
   394  	fmt.Fprintf(buildLog, "Ran scripts in %s\n", format.Duration(timeTaken))
   395  	return nil
   396  }
   397  
   398  func updatePackageDatabase(g *goroutine.Goroutine, rootDir string,
   399  	envGetter environmentGetter, buildLog io.Writer) error {
   400  	fmt.Fprintln(buildLog, "\nUpdating package database:")
   401  	startTime := time.Now()
   402  	err := runInTarget(g, nil, buildLog, rootDir, envGetter,
   403  		packagerPathname, "update")
   404  	if err != nil {
   405  		return errors.New("error updating: " + err.Error())
   406  	}
   407  	fmt.Fprintf(buildLog, "Package databse update took: %s\n",
   408  		format.Duration(time.Since(startTime)))
   409  	return nil
   410  }