github.com/rahart/packer@v0.12.2-0.20161229105310-282bb6ad370f/builder/amazon/chroot/builder.go (about)

     1  // The chroot package is able to create an Amazon AMI without requiring
     2  // the launch of a new instance for every build. It does this by attaching
     3  // and mounting the root volume of another AMI and chrooting into that
     4  // directory. It then creates an AMI from that attached drive.
     5  package chroot
     6  
     7  import (
     8  	"errors"
     9  	"log"
    10  	"runtime"
    11  
    12  	"github.com/aws/aws-sdk-go/aws/session"
    13  	"github.com/aws/aws-sdk-go/service/ec2"
    14  	"github.com/mitchellh/multistep"
    15  	awscommon "github.com/mitchellh/packer/builder/amazon/common"
    16  	"github.com/mitchellh/packer/common"
    17  	"github.com/mitchellh/packer/helper/config"
    18  	"github.com/mitchellh/packer/packer"
    19  	"github.com/mitchellh/packer/template/interpolate"
    20  )
    21  
    22  // The unique ID for this builder
    23  const BuilderId = "mitchellh.amazon.chroot"
    24  
    25  // Config is the configuration that is chained through the steps and
    26  // settable from the template.
    27  type Config struct {
    28  	common.PackerConfig       `mapstructure:",squash"`
    29  	awscommon.AMIBlockDevices `mapstructure:",squash"`
    30  	awscommon.AMIConfig       `mapstructure:",squash"`
    31  	awscommon.AccessConfig    `mapstructure:",squash"`
    32  
    33  	ChrootMounts      [][]string                 `mapstructure:"chroot_mounts"`
    34  	CommandWrapper    string                     `mapstructure:"command_wrapper"`
    35  	CopyFiles         []string                   `mapstructure:"copy_files"`
    36  	DevicePath        string                     `mapstructure:"device_path"`
    37  	FromScratch       bool                       `mapstructure:"from_scratch"`
    38  	MountOptions      []string                   `mapstructure:"mount_options"`
    39  	MountPartition    int                        `mapstructure:"mount_partition"`
    40  	MountPath         string                     `mapstructure:"mount_path"`
    41  	PostMountCommands []string                   `mapstructure:"post_mount_commands"`
    42  	PreMountCommands  []string                   `mapstructure:"pre_mount_commands"`
    43  	RootDeviceName    string                     `mapstructure:"root_device_name"`
    44  	RootVolumeSize    int64                      `mapstructure:"root_volume_size"`
    45  	SourceAmi         string                     `mapstructure:"source_ami"`
    46  	SourceAmiFilter   awscommon.AmiFilterOptions `mapstructure:"source_ami_filter"`
    47  
    48  	ctx interpolate.Context
    49  }
    50  
    51  type wrappedCommandTemplate struct {
    52  	Command string
    53  }
    54  
    55  type Builder struct {
    56  	config Config
    57  	runner multistep.Runner
    58  }
    59  
    60  func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
    61  	b.config.ctx.Funcs = awscommon.TemplateFuncs
    62  	err := config.Decode(&b.config, &config.DecodeOpts{
    63  		Interpolate:        true,
    64  		InterpolateContext: &b.config.ctx,
    65  		InterpolateFilter: &interpolate.RenderFilter{
    66  			Exclude: []string{
    67  				"command_wrapper",
    68  				"post_mount_commands",
    69  				"pre_mount_commands",
    70  				"mount_path",
    71  			},
    72  		},
    73  	}, raws...)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	// Defaults
    79  	if b.config.ChrootMounts == nil {
    80  		b.config.ChrootMounts = make([][]string, 0)
    81  	}
    82  
    83  	if b.config.CopyFiles == nil {
    84  		b.config.CopyFiles = make([]string, 0)
    85  	}
    86  
    87  	if len(b.config.ChrootMounts) == 0 {
    88  		b.config.ChrootMounts = [][]string{
    89  			{"proc", "proc", "/proc"},
    90  			{"sysfs", "sysfs", "/sys"},
    91  			{"bind", "/dev", "/dev"},
    92  			{"devpts", "devpts", "/dev/pts"},
    93  			{"binfmt_misc", "binfmt_misc", "/proc/sys/fs/binfmt_misc"},
    94  		}
    95  	}
    96  
    97  	if len(b.config.CopyFiles) == 0 && !b.config.FromScratch {
    98  		b.config.CopyFiles = []string{"/etc/resolv.conf"}
    99  	}
   100  
   101  	if b.config.CommandWrapper == "" {
   102  		b.config.CommandWrapper = "{{.Command}}"
   103  	}
   104  
   105  	if b.config.MountPath == "" {
   106  		b.config.MountPath = "/mnt/packer-amazon-chroot-volumes/{{.Device}}"
   107  	}
   108  
   109  	if b.config.MountPartition == 0 {
   110  		b.config.MountPartition = 1
   111  	}
   112  
   113  	// Accumulate any errors or warnings
   114  	var errs *packer.MultiError
   115  	var warns []string
   116  
   117  	errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...)
   118  	errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(&b.config.ctx)...)
   119  
   120  	for _, mounts := range b.config.ChrootMounts {
   121  		if len(mounts) != 3 {
   122  			errs = packer.MultiErrorAppend(
   123  				errs, errors.New("Each chroot_mounts entry should be three elements."))
   124  			break
   125  		}
   126  	}
   127  
   128  	if b.config.FromScratch {
   129  		if b.config.SourceAmi != "" || !b.config.SourceAmiFilter.Empty() {
   130  			warns = append(warns, "source_ami and source_ami_filter are unused when from_scratch is true")
   131  		}
   132  		if b.config.RootVolumeSize == 0 {
   133  			errs = packer.MultiErrorAppend(
   134  				errs, errors.New("root_volume_size is required with from_scratch."))
   135  		}
   136  		if len(b.config.PreMountCommands) == 0 {
   137  			errs = packer.MultiErrorAppend(
   138  				errs, errors.New("pre_mount_commands is required with from_scratch."))
   139  		}
   140  		if b.config.AMIVirtType == "" {
   141  			errs = packer.MultiErrorAppend(
   142  				errs, errors.New("ami_virtualization_type is required with from_scratch."))
   143  		}
   144  		if b.config.RootDeviceName == "" {
   145  			errs = packer.MultiErrorAppend(
   146  				errs, errors.New("root_device_name is required with from_scratch."))
   147  		}
   148  		if len(b.config.AMIMappings) == 0 {
   149  			errs = packer.MultiErrorAppend(
   150  				errs, errors.New("ami_block_device_mappings is required with from_scratch."))
   151  		}
   152  	} else {
   153  		if b.config.SourceAmi == "" && b.config.SourceAmiFilter.Empty() {
   154  			errs = packer.MultiErrorAppend(
   155  				errs, errors.New("source_ami or source_ami_filter is required."))
   156  		}
   157  		if len(b.config.AMIMappings) != 0 {
   158  			warns = append(warns, "ami_block_device_mappings are unused when from_scratch is false")
   159  		}
   160  		if b.config.RootDeviceName != "" {
   161  			warns = append(warns, "root_device_name is unused when from_scratch is false")
   162  		}
   163  	}
   164  
   165  	if errs != nil && len(errs.Errors) > 0 {
   166  		return warns, errs
   167  	}
   168  
   169  	log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
   170  	return warns, nil
   171  }
   172  
   173  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   174  	if runtime.GOOS != "linux" {
   175  		return nil, errors.New("The amazon-chroot builder only works on Linux environments.")
   176  	}
   177  
   178  	config, err := b.config.Config()
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	session, err := session.NewSession(config)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	ec2conn := ec2.New(session)
   188  
   189  	wrappedCommand := func(command string) (string, error) {
   190  		ctx := b.config.ctx
   191  		ctx.Data = &wrappedCommandTemplate{Command: command}
   192  		return interpolate.Render(b.config.CommandWrapper, &ctx)
   193  	}
   194  
   195  	// Setup the state bag and initial state for the steps
   196  	state := new(multistep.BasicStateBag)
   197  	state.Put("config", &b.config)
   198  	state.Put("ec2", ec2conn)
   199  	state.Put("hook", hook)
   200  	state.Put("ui", ui)
   201  	state.Put("wrappedCommand", CommandWrapper(wrappedCommand))
   202  
   203  	// Build the steps
   204  	steps := []multistep.Step{
   205  		&awscommon.StepPreValidate{
   206  			DestAmiName:     b.config.AMIName,
   207  			ForceDeregister: b.config.AMIForceDeregister,
   208  		},
   209  		&StepInstanceInfo{},
   210  	}
   211  
   212  	if !b.config.FromScratch {
   213  		steps = append(steps,
   214  			&awscommon.StepSourceAMIInfo{
   215  				SourceAmi:          b.config.SourceAmi,
   216  				EnhancedNetworking: b.config.AMIEnhancedNetworking,
   217  				AmiFilters:         b.config.SourceAmiFilter,
   218  			},
   219  			&StepCheckRootDevice{},
   220  		)
   221  	}
   222  
   223  	steps = append(steps,
   224  		&StepFlock{},
   225  		&StepPrepareDevice{},
   226  		&StepCreateVolume{
   227  			RootVolumeSize: b.config.RootVolumeSize,
   228  		},
   229  		&StepAttachVolume{},
   230  		&StepEarlyUnflock{},
   231  		&StepPreMountCommands{
   232  			Commands: b.config.PreMountCommands,
   233  		},
   234  		&StepMountDevice{
   235  			MountOptions:   b.config.MountOptions,
   236  			MountPartition: b.config.MountPartition,
   237  		},
   238  		&StepPostMountCommands{
   239  			Commands: b.config.PostMountCommands,
   240  		},
   241  		&StepMountExtra{},
   242  		&StepCopyFiles{},
   243  		&StepChrootProvision{},
   244  		&StepEarlyCleanup{},
   245  		&StepSnapshot{},
   246  		&awscommon.StepDeregisterAMI{
   247  			ForceDeregister:     b.config.AMIForceDeregister,
   248  			ForceDeleteSnapshot: b.config.AMIForceDeleteSnapshot,
   249  			AMIName:             b.config.AMIName,
   250  		},
   251  		&StepRegisterAMI{
   252  			RootVolumeSize: b.config.RootVolumeSize,
   253  		},
   254  		&awscommon.StepAMIRegionCopy{
   255  			AccessConfig: &b.config.AccessConfig,
   256  			Regions:      b.config.AMIRegions,
   257  			Name:         b.config.AMIName,
   258  		},
   259  		&awscommon.StepModifyAMIAttributes{
   260  			Description:    b.config.AMIDescription,
   261  			Users:          b.config.AMIUsers,
   262  			Groups:         b.config.AMIGroups,
   263  			ProductCodes:   b.config.AMIProductCodes,
   264  			SnapshotUsers:  b.config.SnapshotUsers,
   265  			SnapshotGroups: b.config.SnapshotGroups,
   266  		},
   267  		&awscommon.StepCreateTags{
   268  			Tags:         b.config.AMITags,
   269  			SnapshotTags: b.config.SnapshotTags,
   270  		},
   271  	)
   272  
   273  	// Run!
   274  	b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
   275  	b.runner.Run(state)
   276  
   277  	// If there was an error, return that
   278  	if rawErr, ok := state.GetOk("error"); ok {
   279  		return nil, rawErr.(error)
   280  	}
   281  
   282  	// If there are no AMIs, then just return
   283  	if _, ok := state.GetOk("amis"); !ok {
   284  		return nil, nil
   285  	}
   286  
   287  	// Build the artifact and return it
   288  	artifact := &awscommon.Artifact{
   289  		Amis:           state.Get("amis").(map[string]string),
   290  		BuilderIdValue: BuilderId,
   291  		Conn:           ec2conn,
   292  	}
   293  
   294  	return artifact, nil
   295  }
   296  
   297  func (b *Builder) Cancel() {
   298  	if b.runner != nil {
   299  		log.Println("Cancelling the step runner...")
   300  		b.runner.Cancel()
   301  	}
   302  }