diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go new file mode 100644 index 000000000..d04ccb97f --- /dev/null +++ b/builder/amazon/chroot/builder.go @@ -0,0 +1,111 @@ +// The chroot package is able to create an Amazon AMI without requiring +// the launch of a new instance for every build. It does this by attaching +// and mounting the root volume of another AMI and chrooting into that +// directory. It then creates an AMI from that attached drive. +package chroot + +import ( + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/multistep" + awscommon "github.com/mitchellh/packer/builder/amazon/common" + "github.com/mitchellh/packer/builder/common" + "github.com/mitchellh/packer/packer" + "log" +) + +// The unique ID for this builder +const BuilderId = "mitchellh.amazon.chroot" + +// Config is the configuration that is chained through the steps and +// settable from the template. +type Config struct { + common.PackerConfig `mapstructure:",squash"` + awscommon.AccessConfig `mapstructure:",squash"` +} + +type Builder struct { + config Config + runner multistep.Runner +} + +func (b *Builder) Prepare(raws ...interface{}) error { + md, err := common.DecodeConfig(&b.config, raws...) + if err != nil { + return err + } + + // Defaults + + // Accumulate any errors + errs := common.CheckUnusedConfig(md) + errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...) + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + + log.Printf("Config: %+v", b.config) + return nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + region, err := b.config.Region() + if err != nil { + return nil, err + } + + auth, err := b.config.AccessConfig.Auth() + if err != nil { + return nil, err + } + + ec2conn := ec2.New(auth, region) + + // Setup the state bag and initial state for the steps + state := make(map[string]interface{}) + state["config"] = &b.config + state["ec2"] = ec2conn + state["hook"] = hook + state["ui"] = ui + + // Build the steps + steps := []multistep.Step{} + + // Run! + if b.config.PackerDebug { + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } else { + b.runner = &multistep.BasicRunner{Steps: steps} + } + + b.runner.Run(state) + + // If there was an error, return that + if rawErr, ok := state["error"]; ok { + return nil, rawErr.(error) + } + + // If there are no AMIs, then just return + if _, ok := state["amis"]; !ok { + return nil, nil + } + + // Build the artifact and return it + artifact := &awscommon.Artifact{ + Amis: state["amis"].(map[string]string), + BuilderIdValue: BuilderId, + Conn: ec2conn, + } + + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/amazon/chroot/builder_test.go b/builder/amazon/chroot/builder_test.go new file mode 100644 index 000000000..32a110545 --- /dev/null +++ b/builder/amazon/chroot/builder_test.go @@ -0,0 +1,18 @@ +package chroot + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{} +} + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Fatalf("Builder should be a builder") + } +} diff --git a/builder/amazon/common/access_config.go b/builder/amazon/common/access_config.go index 736d2d190..e78c15fad 100644 --- a/builder/amazon/common/access_config.go +++ b/builder/amazon/common/access_config.go @@ -1,13 +1,17 @@ package common import ( + "fmt" "github.com/mitchellh/goamz/aws" + "strings" + "unicode" ) // AccessConfig is for common configuration related to AWS access type AccessConfig struct { AccessKey string `mapstructure:"access_key"` SecretKey string `mapstructure:"secret_key"` + RawRegion string `mapstructure:"region"` } // Auth returns a valid aws.Auth object for access to AWS services, or @@ -16,6 +20,28 @@ func (c *AccessConfig) Auth() (aws.Auth, error) { return aws.GetAuth(c.AccessKey, c.SecretKey) } +// Region returns the aws.Region object for access to AWS services, requesting +// the region from the instance metadata if possible. +func (c *AccessConfig) Region() (aws.Region, error) { + if c.RawRegion != "" { + return aws.Regions[c.RawRegion], nil + } + + md, err := aws.GetMetaData("placement/availability-zone") + if err != nil { + return aws.Region{}, err + } + + region := strings.TrimRightFunc(string(md), unicode.IsLetter) + return aws.Regions[region], nil +} + func (c *AccessConfig) Prepare() []error { + if c.RawRegion != "" { + if _, ok := aws.Regions[c.RawRegion]; !ok { + return []error{fmt.Errorf("Unknown region: %s", c.RawRegion)} + } + } + return nil } diff --git a/builder/amazon/common/access_config_test.go b/builder/amazon/common/access_config_test.go new file mode 100644 index 000000000..cfb9e07f1 --- /dev/null +++ b/builder/amazon/common/access_config_test.go @@ -0,0 +1,27 @@ +package common + +import ( + "testing" +) + +func testAccessConfig() *AccessConfig { + return &AccessConfig{} +} + +func TestAccessConfigPrepare_Region(t *testing.T) { + c := testAccessConfig() + c.RawRegion = "" + if err := c.Prepare(); err != nil { + t.Fatalf("shouldn't have err: %s", err) + } + + c.RawRegion = "us-east-12" + if err := c.Prepare(); err == nil { + t.Fatal("should have error") + } + + c.RawRegion = "us-east-1" + if err := c.Prepare(); err != nil { + t.Fatalf("shouldn't have err: %s", err) + } +} diff --git a/builder/amazon/common/run_config.go b/builder/amazon/common/run_config.go index bd2186d93..321378cc9 100644 --- a/builder/amazon/common/run_config.go +++ b/builder/amazon/common/run_config.go @@ -3,14 +3,12 @@ package common import ( "errors" "fmt" - "github.com/mitchellh/goamz/aws" "time" ) // RunConfig contains configuration for running an instance from a source // AMI and details on how to access that launched image. type RunConfig struct { - Region string SourceAmi string `mapstructure:"source_ami"` InstanceType string `mapstructure:"instance_type"` RawSSHTimeout string `mapstructure:"ssh_timeout"` @@ -45,12 +43,6 @@ func (c *RunConfig) Prepare() []error { errs = append(errs, errors.New("An instance_type must be specified")) } - if c.Region == "" { - errs = append(errs, errors.New("A region must be specified")) - } else if _, ok := aws.Regions[c.Region]; !ok { - errs = append(errs, fmt.Errorf("Unknown region: %s", c.Region)) - } - if c.SSHUsername == "" { errs = append(errs, errors.New("An ssh_username must be specified")) } diff --git a/builder/amazon/common/run_config_test.go b/builder/amazon/common/run_config_test.go index 0c0d40680..b34b4c614 100644 --- a/builder/amazon/common/run_config_test.go +++ b/builder/amazon/common/run_config_test.go @@ -16,7 +16,6 @@ func init() { func testConfig() *RunConfig { return &RunConfig{ - Region: "us-east-1", SourceAmi: "abcd", InstanceType: "m1.small", SSHUsername: "root", @@ -39,24 +38,6 @@ func TestRunConfigPrepare_InstanceType(t *testing.T) { } } -func TestRunConfigPrepare_Region(t *testing.T) { - c := testConfig() - c.Region = "" - if err := c.Prepare(); len(err) != 1 { - t.Fatalf("err: %s", err) - } - - c.Region = "us-east-12" - if err := c.Prepare(); len(err) != 1 { - t.Fatalf("err: %s", err) - } - - c.Region = "us-east-1" - if err := c.Prepare(); len(err) != 0 { - t.Fatalf("err: %s", err) - } -} - func TestRunConfigPrepare_SourceAmi(t *testing.T) { c := testConfig() c.SourceAmi = "" diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index 46f5231fc..714c96afc 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -8,7 +8,6 @@ package ebs import ( "errors" "fmt" - "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/multistep" awscommon "github.com/mitchellh/packer/builder/amazon/common" @@ -67,9 +66,9 @@ func (b *Builder) Prepare(raws ...interface{}) error { } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { - region, ok := aws.Regions[b.config.Region] - if !ok { - panic("region not found") + region, err := b.config.Region() + if err != nil { + return nil, err } auth, err := b.config.AccessConfig.Auth() diff --git a/builder/amazon/ebs/step_create_ami.go b/builder/amazon/ebs/step_create_ami.go index 0b08bc7c9..5f75fb423 100644 --- a/builder/amazon/ebs/step_create_ami.go +++ b/builder/amazon/ebs/step_create_ami.go @@ -52,7 +52,7 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction { // Set the AMI ID in the state ui.Say(fmt.Sprintf("AMI: %s", createResp.ImageId)) amis := make(map[string]string) - amis[config.Region] = createResp.ImageId + amis[ec2conn.Region.Name] = createResp.ImageId state["amis"] = amis // Wait for the image to become ready diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go index 83e486a49..ffbdfb467 100644 --- a/builder/amazon/instance/builder.go +++ b/builder/amazon/instance/builder.go @@ -5,7 +5,6 @@ package instance import ( "errors" "fmt" - "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/multistep" awscommon "github.com/mitchellh/packer/builder/amazon/common" @@ -134,9 +133,9 @@ func (b *Builder) Prepare(raws ...interface{}) error { } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { - region, ok := aws.Regions[b.config.Region] - if !ok { - panic("region not found") + region, err := b.config.Region() + if err != nil { + return nil, err } auth, err := b.config.AccessConfig.Auth() diff --git a/builder/amazon/instance/step_register_ami.go b/builder/amazon/instance/step_register_ami.go index d95deb888..1accf7e48 100644 --- a/builder/amazon/instance/step_register_ami.go +++ b/builder/amazon/instance/step_register_ami.go @@ -50,7 +50,7 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction // Set the AMI ID in the state ui.Say(fmt.Sprintf("AMI: %s", registerResp.ImageId)) amis := make(map[string]string) - amis[config.Region] = registerResp.ImageId + amis[ec2conn.Region.Name] = registerResp.ImageId state["amis"] = amis // Wait for the image to become ready diff --git a/plugin/builder-amazon-ebs-chroot/main.go b/plugin/builder-amazon-ebs-chroot/main.go new file mode 100644 index 000000000..b7d71df44 --- /dev/null +++ b/plugin/builder-amazon-ebs-chroot/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/builder/amazon/chroot" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + plugin.ServeBuilder(new(chroot.Builder)) +}