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  }