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 }