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