github.com/vijayrajah/packer@v1.3.2/post-processor/compress/post-processor.go (about) 1 package compress 2 3 import ( 4 "archive/tar" 5 "archive/zip" 6 "compress/gzip" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 14 "github.com/biogo/hts/bgzf" 15 "github.com/hashicorp/packer/common" 16 "github.com/hashicorp/packer/helper/config" 17 "github.com/hashicorp/packer/packer" 18 "github.com/hashicorp/packer/template/interpolate" 19 "github.com/klauspost/pgzip" 20 "github.com/pierrec/lz4" 21 "github.com/ulikunitz/xz" 22 ) 23 24 var ( 25 // ErrInvalidCompressionLevel is returned when the compression level passed 26 // to gzip is not in the expected range. See compress/flate for details. 27 ErrInvalidCompressionLevel = fmt.Errorf( 28 "Invalid compression level. Expected an integer from -1 to 9.") 29 30 ErrWrongInputCount = fmt.Errorf( 31 "Can only have 1 input file when not using tar/zip") 32 33 filenamePattern = regexp.MustCompile(`(?:\.([a-z0-9]+))`) 34 ) 35 36 type Config struct { 37 common.PackerConfig `mapstructure:",squash"` 38 39 // Fields from config file 40 OutputPath string `mapstructure:"output"` 41 Format string `mapstructure:"format"` 42 CompressionLevel int `mapstructure:"compression_level"` 43 KeepInputArtifact bool `mapstructure:"keep_input_artifact"` 44 45 // Derived fields 46 Archive string 47 Algorithm string 48 49 ctx interpolate.Context 50 } 51 52 type PostProcessor struct { 53 config Config 54 } 55 56 func (p *PostProcessor) Configure(raws ...interface{}) error { 57 err := config.Decode(&p.config, &config.DecodeOpts{ 58 Interpolate: true, 59 InterpolateContext: &p.config.ctx, 60 InterpolateFilter: &interpolate.RenderFilter{ 61 Exclude: []string{"output"}, 62 }, 63 }, raws...) 64 if err != nil { 65 return err 66 } 67 68 errs := new(packer.MultiError) 69 70 // If there is no explicit number of Go threads to use, then set it 71 if os.Getenv("GOMAXPROCS") == "" { 72 runtime.GOMAXPROCS(runtime.NumCPU()) 73 } 74 75 if p.config.OutputPath == "" { 76 p.config.OutputPath = "packer_{{.BuildName}}_{{.BuilderType}}" 77 } 78 79 if p.config.CompressionLevel > pgzip.BestCompression { 80 p.config.CompressionLevel = pgzip.BestCompression 81 } 82 // Technically 0 means "don't compress" but I don't know how to 83 // differentiate between "user entered zero" and "user entered nothing". 84 // Also, why bother creating a compressed file with zero compression? 85 if p.config.CompressionLevel == -1 || p.config.CompressionLevel == 0 { 86 p.config.CompressionLevel = pgzip.DefaultCompression 87 } 88 89 if err = interpolate.Validate(p.config.OutputPath, &p.config.ctx); err != nil { 90 errs = packer.MultiErrorAppend( 91 errs, fmt.Errorf("Error parsing target template: %s", err)) 92 } 93 94 p.config.detectFromFilename() 95 96 if len(errs.Errors) > 0 { 97 return errs 98 } 99 100 return nil 101 } 102 103 func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { 104 105 // These are extra variables that will be made available for interpolation. 106 p.config.ctx.Data = map[string]string{ 107 "BuildName": p.config.PackerBuildName, 108 "BuilderType": p.config.PackerBuilderType, 109 } 110 111 target, err := interpolate.Render(p.config.OutputPath, &p.config.ctx) 112 if err != nil { 113 return nil, false, fmt.Errorf("Error interpolating output value: %s", err) 114 } else { 115 fmt.Println(target) 116 } 117 118 keep := p.config.KeepInputArtifact 119 newArtifact := &Artifact{Path: target} 120 121 if err = os.MkdirAll(filepath.Dir(target), os.FileMode(0755)); err != nil { 122 return nil, false, fmt.Errorf( 123 "Unable to create dir for archive %s: %s", target, err) 124 } 125 outputFile, err := os.Create(target) 126 if err != nil { 127 return nil, false, fmt.Errorf( 128 "Unable to create archive %s: %s", target, err) 129 } 130 defer outputFile.Close() 131 132 // Setup output interface. If we're using compression, output is a 133 // compression writer. Otherwise it's just a file. 134 var output io.WriteCloser 135 switch p.config.Algorithm { 136 case "bgzf": 137 ui.Say(fmt.Sprintf("Using bgzf compression with %d cores for %s", 138 runtime.GOMAXPROCS(-1), target)) 139 output, err = makeBGZFWriter(outputFile, p.config.CompressionLevel) 140 defer output.Close() 141 case "lz4": 142 ui.Say(fmt.Sprintf("Using lz4 compression with %d cores for %s", 143 runtime.GOMAXPROCS(-1), target)) 144 output, err = makeLZ4Writer(outputFile, p.config.CompressionLevel) 145 defer output.Close() 146 case "xz": 147 ui.Say(fmt.Sprintf("Using xz compression with 1 core for %s (library does not support MT)", 148 target)) 149 output, err = makeXZWriter(outputFile) 150 defer output.Close() 151 case "pgzip": 152 ui.Say(fmt.Sprintf("Using pgzip compression with %d cores for %s", 153 runtime.GOMAXPROCS(-1), target)) 154 output, err = makePgzipWriter(outputFile, p.config.CompressionLevel) 155 defer output.Close() 156 default: 157 output = outputFile 158 } 159 160 compression := p.config.Algorithm 161 if compression == "" { 162 compression = "no compression" 163 } 164 165 // Build an archive, if we're supposed to do that. 166 switch p.config.Archive { 167 case "tar": 168 ui.Say(fmt.Sprintf("Tarring %s with %s", target, compression)) 169 err = createTarArchive(artifact.Files(), output) 170 if err != nil { 171 return nil, keep, fmt.Errorf("Error creating tar: %s", err) 172 } 173 case "zip": 174 ui.Say(fmt.Sprintf("Zipping %s", target)) 175 err = createZipArchive(artifact.Files(), output) 176 if err != nil { 177 return nil, keep, fmt.Errorf("Error creating zip: %s", err) 178 } 179 default: 180 // Filename indicates no tarball (just compress) so we'll do an io.Copy 181 // into our compressor. 182 if len(artifact.Files()) != 1 { 183 return nil, keep, fmt.Errorf( 184 "Can only have 1 input file when not using tar/zip. Found %d "+ 185 "files: %v", len(artifact.Files()), artifact.Files()) 186 } 187 archiveFile := artifact.Files()[0] 188 ui.Say(fmt.Sprintf("Archiving %s with %s", archiveFile, compression)) 189 190 source, err := os.Open(archiveFile) 191 if err != nil { 192 return nil, keep, fmt.Errorf( 193 "Failed to open source file %s for reading: %s", 194 archiveFile, err) 195 } 196 defer source.Close() 197 198 if _, err = io.Copy(output, source); err != nil { 199 return nil, keep, fmt.Errorf("Failed to compress %s: %s", 200 archiveFile, err) 201 } 202 } 203 204 ui.Say(fmt.Sprintf("Archive %s completed", target)) 205 206 return newArtifact, keep, nil 207 } 208 209 func (config *Config) detectFromFilename() { 210 var result [][]string 211 212 extensions := map[string]string{ 213 "tar": "tar", 214 "zip": "zip", 215 "gz": "pgzip", 216 "lz4": "lz4", 217 "bgzf": "bgzf", 218 "xz": "xz", 219 } 220 221 if config.Format == "" { 222 result = filenamePattern.FindAllStringSubmatch(config.OutputPath, -1) 223 } else { 224 result = filenamePattern.FindAllStringSubmatch(fmt.Sprintf("%s.%s", config.OutputPath, config.Format), -1) 225 } 226 227 // No dots. Bail out with defaults. 228 if len(result) == 0 { 229 config.Algorithm = "pgzip" 230 config.Archive = "tar" 231 return 232 } 233 234 // Parse the last two .groups, if they're there 235 lastItem := result[len(result)-1][1] 236 var nextToLastItem string 237 if len(result) == 1 { 238 nextToLastItem = "" 239 } else { 240 nextToLastItem = result[len(result)-2][1] 241 } 242 243 // Should we make an archive? E.g. tar or zip? 244 if nextToLastItem == "tar" { 245 config.Archive = "tar" 246 } 247 if lastItem == "zip" || lastItem == "tar" { 248 config.Archive = lastItem 249 // Tar or zip is our final artifact. Bail out. 250 return 251 } 252 253 // Should we compress the artifact? 254 algorithm, ok := extensions[lastItem] 255 if ok { 256 config.Algorithm = algorithm 257 // We found our compression algorithm. Bail out. 258 return 259 } 260 261 // We didn't match a known compression format. Default to tar + pgzip 262 config.Algorithm = "pgzip" 263 config.Archive = "tar" 264 return 265 } 266 267 func makeBGZFWriter(output io.WriteCloser, compressionLevel int) (io.WriteCloser, error) { 268 bgzfWriter, err := bgzf.NewWriterLevel(output, compressionLevel, runtime.GOMAXPROCS(-1)) 269 if err != nil { 270 return nil, ErrInvalidCompressionLevel 271 } 272 return bgzfWriter, nil 273 } 274 275 func makeLZ4Writer(output io.WriteCloser, compressionLevel int) (io.WriteCloser, error) { 276 lzwriter := lz4.NewWriter(output) 277 if compressionLevel > gzip.DefaultCompression { 278 lzwriter.Header.HighCompression = true 279 } 280 return lzwriter, nil 281 } 282 283 func makeXZWriter(output io.WriteCloser) (io.WriteCloser, error) { 284 xzwriter, err := xz.NewWriter(output) 285 if err != nil { 286 return nil, err 287 } 288 return xzwriter, nil 289 } 290 291 func makePgzipWriter(output io.WriteCloser, compressionLevel int) (io.WriteCloser, error) { 292 gzipWriter, err := pgzip.NewWriterLevel(output, compressionLevel) 293 if err != nil { 294 return nil, ErrInvalidCompressionLevel 295 } 296 gzipWriter.SetConcurrency(500000, runtime.GOMAXPROCS(-1)) 297 return gzipWriter, nil 298 } 299 300 func createTarArchive(files []string, output io.WriteCloser) error { 301 archive := tar.NewWriter(output) 302 defer archive.Close() 303 304 for _, path := range files { 305 file, err := os.Open(path) 306 if err != nil { 307 return fmt.Errorf("Unable to read file %s: %s", path, err) 308 } 309 defer file.Close() 310 311 fi, err := file.Stat() 312 if err != nil { 313 return fmt.Errorf("Unable to get fileinfo for %s: %s", path, err) 314 } 315 316 header, err := tar.FileInfoHeader(fi, path) 317 if err != nil { 318 return fmt.Errorf("Failed to create tar header for %s: %s", path, err) 319 } 320 321 // workaround for archive format on go >=1.10 322 setHeaderFormat(header) 323 324 if err := archive.WriteHeader(header); err != nil { 325 return fmt.Errorf("Failed to write tar header for %s: %s", path, err) 326 } 327 328 if _, err := io.Copy(archive, file); err != nil { 329 return fmt.Errorf("Failed to copy %s data to archive: %s", path, err) 330 } 331 } 332 return nil 333 } 334 335 func createZipArchive(files []string, output io.WriteCloser) error { 336 archive := zip.NewWriter(output) 337 defer archive.Close() 338 339 for _, path := range files { 340 path = filepath.ToSlash(path) 341 342 source, err := os.Open(path) 343 if err != nil { 344 return fmt.Errorf("Unable to read file %s: %s", path, err) 345 } 346 defer source.Close() 347 348 target, err := archive.Create(path) 349 if err != nil { 350 return fmt.Errorf("Failed to add zip header for %s: %s", path, err) 351 } 352 353 _, err = io.Copy(target, source) 354 if err != nil { 355 return fmt.Errorf("Failed to copy %s data to archive: %s", path, err) 356 } 357 } 358 return nil 359 }