github.com/leowmjw/otto@v0.2.1-0.20160126165905-6400716cf085/helper/packer/build.go (about)

     1  package packer
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/atlas-go/archive"
    11  	"github.com/hashicorp/otto/app"
    12  	"github.com/hashicorp/otto/directory"
    13  	"github.com/hashicorp/otto/foundation"
    14  )
    15  
    16  type BuildOptions struct {
    17  	// Dir is the directory where Packer will be executed from.
    18  	// If this isn't set, it'll default to "#{ctx.Dir}/build"
    19  	Dir string
    20  
    21  	// The path to the template to execute. If this isn't set, it'll
    22  	// default to "#{Dir}/template.json"
    23  	TemplatePath string
    24  
    25  	// InfraOutputMap is a map to change the key of an infra output
    26  	// to a different key for a Packer variable. The key of this map
    27  	// is the infra output key, and teh value is the Packer variable name.
    28  	InfraOutputMap map[string]string
    29  }
    30  
    31  // Build can be used to build an artifact with Packer and parse the
    32  // artifact out into a Build properly.
    33  //
    34  // This function automatically knows how to parse various built-in
    35  // artifacts of Packer. For the exact functionality of the parse
    36  // functions, see the documentation of the various parse functions.
    37  //
    38  // This function implements the app.App.Build function.
    39  // TODO: Test
    40  func Build(ctx *app.Context, opts *BuildOptions) error {
    41  	project := Project(&ctx.Shared)
    42  	if err := project.InstallIfNeeded(); err != nil {
    43  		return err
    44  	}
    45  
    46  	ctx.Ui.Header("Querying infrastructure data for build...")
    47  
    48  	// Get the infrastructure, since it needs to be ready for building
    49  	// to occur. We'll copy the outputs and the credentials as variables
    50  	// to Packer.
    51  	infra, err := ctx.Directory.GetInfra(&directory.Infra{
    52  		Lookup: directory.Lookup{
    53  			Infra: ctx.Appfile.ActiveInfrastructure().Name}})
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	// If the infra isn't ready then we can't build
    59  	if infra == nil || infra.State != directory.InfraStateReady {
    60  		return fmt.Errorf(
    61  			"Infrastructure for this application hasn't been built yet.\n" +
    62  				"The build step requires this because the target infrastructure\n" +
    63  				"as well as its final properties can affect the build process.\n" +
    64  				"Please run `otto infra` to build the underlying infrastructure,\n" +
    65  				"then run `otto build` again.")
    66  	}
    67  
    68  	// Construct the variables for Packer from the infra. We copy them as-is.
    69  	vars := make(map[string]string)
    70  	for k, v := range infra.Outputs {
    71  		if opts.InfraOutputMap != nil {
    72  			if nk, ok := opts.InfraOutputMap[k]; ok {
    73  				k = nk
    74  			}
    75  		}
    76  
    77  		vars[k] = v
    78  	}
    79  	for k, v := range ctx.InfraCreds {
    80  		vars[k] = v
    81  	}
    82  
    83  	// Setup the vars
    84  	if err := foundation.WriteVars(&ctx.Shared); err != nil {
    85  		return fmt.Errorf("Error preparing build: %s", err)
    86  	}
    87  
    88  	ctx.Ui.Header("Building deployment archive...")
    89  	slugPath, err := createAppSlug(filepath.Dir(ctx.Appfile.Path))
    90  	if err != nil {
    91  		return err
    92  	}
    93  	vars["slug_path"] = slugPath
    94  
    95  	// Start building the resulting build
    96  	build := &directory.Build{
    97  		Lookup: directory.Lookup{
    98  			AppID:       ctx.Appfile.ID,
    99  			Infra:       ctx.Tuple.Infra,
   100  			InfraFlavor: ctx.Tuple.InfraFlavor,
   101  		},
   102  
   103  		Artifact: make(map[string]string),
   104  	}
   105  
   106  	// Get the paths for Packer execution
   107  	packerDir := opts.Dir
   108  	templatePath := opts.TemplatePath
   109  	if opts.Dir == "" {
   110  		packerDir = filepath.Join(ctx.Dir, "build")
   111  	}
   112  	if opts.TemplatePath == "" {
   113  		templatePath = filepath.Join(packerDir, "template.json")
   114  	}
   115  
   116  	ctx.Ui.Header("Building deployment artifact with Packer...")
   117  	ctx.Ui.Message(
   118  		"Raw Packer output will begin streaming in below. Otto\n" +
   119  			"does not create this output. It is mirrored directly from\n" +
   120  			"Packer while the build is being run.\n\n")
   121  
   122  	// Build and execute Packer
   123  	p := &Packer{
   124  		Path:      project.Path(),
   125  		Dir:       packerDir,
   126  		Ui:        ctx.Ui,
   127  		Variables: vars,
   128  		Callbacks: map[string]OutputCallback{
   129  			"artifact": ParseArtifactAmazon(build.Artifact),
   130  		},
   131  	}
   132  	if err := p.Execute("build", templatePath); err != nil {
   133  		return err
   134  	}
   135  
   136  	// Store the build!
   137  	ctx.Ui.Header("Storing build data in directory...")
   138  	if err := ctx.Directory.PutBuild(build); err != nil {
   139  		return fmt.Errorf(
   140  			"Error storing the build in the directory service: %s\n\n"+
   141  				"Despite the build itself completing successfully, Otto must\n"+
   142  				"also successfully store the results in the directory service\n"+
   143  				"to be able to deploy this build. Please fix the above error and\n"+
   144  				"rebuild.",
   145  			err)
   146  	}
   147  
   148  	ctx.Ui.Header("[green]Build success!")
   149  	ctx.Ui.Message(
   150  		"[green]The build was completed successfully and stored within\n" +
   151  			"the directory service, meaning other members of your team\n" +
   152  			"don't need to rebuild this same version and can deploy it\n" +
   153  			"immediately.")
   154  
   155  	return nil
   156  }
   157  
   158  // ParseArtifactAmazon parses AMIs out of the output.
   159  //
   160  // The map will be populated where the key is the region and the value is
   161  // the AMI ID.
   162  func ParseArtifactAmazon(m map[string]string) OutputCallback {
   163  	return func(o *Output) {
   164  		// We're looking for ID events.
   165  		//
   166  		// Example: 1440649959,amazon-ebs,artifact,0,id,us-east-1:ami-9d66def6
   167  		if len(o.Data) < 3 || o.Data[1] != "id" {
   168  			return
   169  		}
   170  
   171  		// TODO: multiple AMIs
   172  		parts := strings.Split(o.Data[2], ":")
   173  		m[parts[0]] = parts[1]
   174  	}
   175  }
   176  
   177  // createAppSlug makes an archive of the app with (otto-specific exclusions)
   178  // and yields a path to a tempfile containing that archive
   179  //
   180  // TODO: allow customization of the Exclude patterns
   181  func createAppSlug(path string) (string, error) {
   182  	archive, err := archive.CreateArchive(path, &archive.ArchiveOpts{
   183  		Exclude: []string{".otto", ".vagrant"},
   184  		VCS:     true,
   185  	})
   186  	if err != nil {
   187  		return "", err
   188  	}
   189  	defer archive.Close()
   190  
   191  	// Archive is just a reader, and we need it in a file. The below seems
   192  	// fiddly, could there be a better way?
   193  	slug, err := ioutil.TempFile("", "otto-slug-")
   194  	if err != nil {
   195  		return "", err
   196  	}
   197  
   198  	_, err = io.Copy(slug, archive)
   199  	cerr := slug.Close()
   200  	if err != nil {
   201  		return "", err
   202  	}
   203  	if cerr != nil {
   204  		return "", err
   205  	}
   206  
   207  	return slug.Name(), nil
   208  }