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