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

     1  // +build go1.10
     2  
     3  package builder
     4  
     5  import (
     6  	"bufio"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"sort"
    15  	"strings"
    16  	"syscall"
    17  	"time"
    18  
    19  	"github.com/Cloud-Foundations/Dominator/lib/format"
    20  	"github.com/Cloud-Foundations/Dominator/lib/image"
    21  	"github.com/Cloud-Foundations/Dominator/lib/srpc"
    22  	"github.com/Cloud-Foundations/Dominator/lib/wsyscall"
    23  	proto "github.com/Cloud-Foundations/Dominator/proto/imaginator"
    24  )
    25  
    26  const (
    27  	cmdPerms = syscall.S_IRWXU | syscall.S_IRGRP | syscall.S_IXGRP |
    28  		syscall.S_IROTH | syscall.S_IXOTH
    29  	dirPerms = syscall.S_IRWXU | syscall.S_IRGRP | syscall.S_IXGRP |
    30  		syscall.S_IROTH | syscall.S_IXOTH
    31  	packagerPathname = "/bin/generic-packager"
    32  )
    33  
    34  var environmentToCopy = map[string]struct{}{
    35  	"PATH":  {},
    36  	"TZ":    {},
    37  	"SHELL": {},
    38  }
    39  
    40  var environmentToSet = map[string]string{
    41  	"HOME":    "/",
    42  	"LOGNAME": "root",
    43  	"USER":    "root",
    44  }
    45  
    46  func cleanPackages(rootDir string, buildLog io.Writer) error {
    47  	fmt.Fprintln(buildLog, "\nCleaning packages:")
    48  	startTime := time.Now()
    49  	err := runInTarget(nil, buildLog, rootDir, nil, packagerPathname, "clean")
    50  	if err != nil {
    51  		return errors.New("error cleaning: " + err.Error())
    52  	}
    53  	fmt.Fprintf(buildLog, "Package clean took: %s\n",
    54  		format.Duration(time.Since(startTime)))
    55  	return nil
    56  }
    57  
    58  func makeTempDirectory(dir, prefix string) (string, error) {
    59  	tmpDir, err := ioutil.TempDir(dir, prefix)
    60  	if err != nil {
    61  		return "", err
    62  	}
    63  	if err := os.Chmod(tmpDir, dirPerms); err != nil {
    64  		os.RemoveAll(tmpDir)
    65  		return "", err
    66  	}
    67  	return tmpDir, nil
    68  }
    69  
    70  func (stream *bootstrapStream) build(b *Builder, client *srpc.Client,
    71  	request proto.BuildImageRequest,
    72  	buildLog buildLogger) (*image.Image, error) {
    73  	startTime := time.Now()
    74  	args := make([]string, 0, len(stream.BootstrapCommand))
    75  	rootDir, err := makeTempDirectory("",
    76  		strings.Replace(request.StreamName, "/", "_", -1))
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	defer os.RemoveAll(rootDir)
    81  	fmt.Fprintf(buildLog, "Created image working directory: %s\n", rootDir)
    82  	for _, arg := range stream.BootstrapCommand {
    83  		if arg == "$dir" {
    84  			arg = rootDir
    85  		}
    86  		args = append(args, arg)
    87  	}
    88  	cmd := exec.Command(args[0], args[1:]...)
    89  	cmd.Stdout = buildLog
    90  	cmd.Stderr = buildLog
    91  	if err := cmd.Run(); err != nil {
    92  		return nil, err
    93  	} else {
    94  		packager := b.packagerTypes[stream.PackagerType]
    95  		if err := packager.writePackageInstaller(rootDir); err != nil {
    96  			return nil, err
    97  		}
    98  		if err := clearResolvConf(buildLog, rootDir); err != nil {
    99  			return nil, err
   100  		}
   101  		buildDuration := time.Since(startTime)
   102  		fmt.Fprintf(buildLog, "\nBuild time: %s\n",
   103  			format.Duration(buildDuration))
   104  		if err := cleanPackages(rootDir, buildLog); err != nil {
   105  			return nil, err
   106  		}
   107  		return packImage(client, request, rootDir,
   108  			stream.Filter, nil, stream.imageFilter, stream.imageTriggers,
   109  			buildLog)
   110  	}
   111  }
   112  
   113  func (packager *packagerType) writePackageInstaller(rootDir string) error {
   114  	filename := filepath.Join(rootDir, packagerPathname)
   115  	file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, cmdPerms)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	defer file.Close()
   120  	writer := bufio.NewWriter(file)
   121  	defer writer.Flush()
   122  	packager.writePackageInstallerContents(writer)
   123  	return writer.Flush()
   124  }
   125  
   126  func (packager *packagerType) writePackageInstallerContents(writer io.Writer) {
   127  	fmt.Fprintln(writer, "#! /bin/sh")
   128  	fmt.Fprintln(writer, "# Created by imaginator.")
   129  	fmt.Fprintln(writer, "mount -n none -t proc /proc")
   130  	fmt.Fprintln(writer, "mount -n none -t sysfs /sys")
   131  	for _, line := range packager.Verbatim {
   132  		fmt.Fprintln(writer, line)
   133  	}
   134  	fmt.Fprintln(writer, "cmd=\"$1\"; shift")
   135  	writePackagerCommand(writer, "clean", packager.CleanCommand)
   136  	fmt.Fprintln(writer, `[ "$cmd" = "copy-in" ] && exec cat > "$1"`)
   137  	writePackagerCommand(writer, "install", packager.InstallCommand)
   138  	writePackagerCommand(writer, "list", packager.ListCommand.ArgList)
   139  	writePackagerCommand(writer, "remove", packager.RemoveCommand)
   140  	fmt.Fprintln(writer, `[ "$cmd" = "run" ] && exec "$@"`)
   141  	multiplier := packager.ListCommand.SizeMultiplier
   142  	if multiplier < 1 {
   143  		multiplier = 1
   144  	}
   145  	fmt.Fprintf(writer,
   146  		"[ \"$cmd\" = \"show-size-multiplier\" ] && exec echo %d\n", multiplier)
   147  	writePackagerCommand(writer, "update", packager.UpdateCommand)
   148  	writePackagerCommand(writer, "upgrade", packager.UpgradeCommand)
   149  	fmt.Fprintln(writer, "echo \"Invalid command: $cmd\"")
   150  	fmt.Fprintln(writer, "exit 2")
   151  }
   152  
   153  func writePackagerCommand(writer io.Writer, cmd string, command []string) {
   154  	if len(command) < 1 {
   155  		fmt.Fprintf(writer, "[ \"$cmd\" = \"%s\" ] && exit 0\n", cmd)
   156  	} else {
   157  		fmt.Fprintf(writer, "[ \"$cmd\" = \"%s\" ] && exec", cmd)
   158  		for _, arg := range command {
   159  			writeArgument(writer, arg)
   160  		}
   161  		fmt.Fprintf(writer, " \"$@\"\n")
   162  	}
   163  }
   164  
   165  func writeArgument(writer io.Writer, arg string) {
   166  	if len(strings.Fields(arg)) < 2 {
   167  		fmt.Fprintf(writer, " %s", arg)
   168  	} else {
   169  		lenArg := len(arg)
   170  		if lenArg > 0 && arg[lenArg-1] == '\n' {
   171  			arg = arg[:lenArg-1] + `\n`
   172  		}
   173  		fmt.Fprintf(writer, " '%s'", arg)
   174  	}
   175  }
   176  
   177  func clearResolvConf(writer io.Writer, rootDir string) error {
   178  	return runInTarget(nil, writer, rootDir, nil,
   179  		"cp", "/dev/null", "/etc/resolv.conf")
   180  }
   181  
   182  func runInTarget(input io.Reader, output io.Writer, rootDir string,
   183  	envGetter environmentGetter, prog string, args ...string) error {
   184  	var environmentToInject map[string]string
   185  	if envGetter != nil {
   186  		environmentToInject = envGetter.getenv()
   187  	}
   188  	cmd := exec.Command(prog, args...)
   189  	cmd.Env = stripVariables(os.Environ(), environmentToCopy, environmentToSet,
   190  		environmentToInject)
   191  	cmd.Dir = "/"
   192  	cmd.Stdin = input
   193  	cmd.Stdout = output
   194  	cmd.Stderr = output
   195  	cmd.SysProcAttr = &syscall.SysProcAttr{
   196  		Chroot:     rootDir,
   197  		Setsid:     true,
   198  		Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWPID,
   199  	}
   200  	return cmd.Run()
   201  }
   202  
   203  func runInTargetWithBindMounts(input io.Reader, output io.Writer,
   204  	rootDir string, bindMounts []string, envGetter environmentGetter,
   205  	prog string, args ...string) error {
   206  	if len(bindMounts) < 1 {
   207  		return runInTarget(input, output, rootDir, envGetter, prog, args...)
   208  	}
   209  	errChannel := make(chan error)
   210  	go func() {
   211  		err := func() error {
   212  			if err := wsyscall.UnshareMountNamespace(); err != nil {
   213  				return err
   214  			}
   215  			for _, bindMount := range bindMounts {
   216  				err := wsyscall.Mount(bindMount,
   217  					filepath.Join(rootDir, bindMount), "",
   218  					wsyscall.MS_BIND|wsyscall.MS_RDONLY, "")
   219  				if err != nil {
   220  					return fmt.Errorf("error bind mounting: %s: %s",
   221  						bindMount, err)
   222  				}
   223  			}
   224  			return runInTarget(input, output, rootDir, envGetter, prog, args...)
   225  		}()
   226  		errChannel <- err
   227  	}()
   228  	return <-errChannel
   229  }
   230  
   231  func stripVariables(input []string, varsToCopy map[string]struct{},
   232  	varsToSet ...map[string]string) []string {
   233  	output := make([]string, 0)
   234  	for _, nameValue := range os.Environ() {
   235  		split := strings.SplitN(nameValue, "=", 2)
   236  		if len(split) == 2 {
   237  			if _, ok := varsToCopy[split[0]]; ok {
   238  				output = append(output, nameValue)
   239  			}
   240  		}
   241  	}
   242  	for _, varTable := range varsToSet {
   243  		for name, value := range varTable {
   244  			output = append(output, name+"="+value)
   245  		}
   246  	}
   247  	sort.Strings(output)
   248  	return output
   249  }