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 }