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 }