diff --git a/command/plugin.go b/command/plugin.go index 61a17de72..95c08dc63 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -33,6 +33,7 @@ import ( amazonimportpostprocessor "github.com/mitchellh/packer/post-processor/amazon-import" artificepostprocessor "github.com/mitchellh/packer/post-processor/artifice" atlaspostprocessor "github.com/mitchellh/packer/post-processor/atlas" + checksumpostprocessor "github.com/mitchellh/packer/post-processor/checksum" compresspostprocessor "github.com/mitchellh/packer/post-processor/compress" dockerimportpostprocessor "github.com/mitchellh/packer/post-processor/docker-import" dockerpushpostprocessor "github.com/mitchellh/packer/post-processor/docker-push" @@ -101,6 +102,7 @@ var PostProcessors = map[string]packer.PostProcessor{ "amazon-import": new(amazonimportpostprocessor.PostProcessor), "artifice": new(artificepostprocessor.PostProcessor), "atlas": new(atlaspostprocessor.PostProcessor), + "checksum": new(checksumpostprocessor.PostProcessor), "compress": new(compresspostprocessor.PostProcessor), "docker-import": new(dockerimportpostprocessor.PostProcessor), "docker-push": new(dockerpushpostprocessor.PostProcessor), diff --git a/post-processor/checksum/LICENSE b/post-processor/checksum/LICENSE new file mode 100644 index 000000000..bd410b049 --- /dev/null +++ b/post-processor/checksum/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Vasiliy Tolstov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/post-processor/checksum/artifact.go b/post-processor/checksum/artifact.go new file mode 100644 index 000000000..78360bb85 --- /dev/null +++ b/post-processor/checksum/artifact.go @@ -0,0 +1,48 @@ +package checksum + +import ( + "fmt" + "os" + "strings" +) + +const BuilderId = "packer.post-processor.checksum" + +type Artifact struct { + files []string +} + +func NewArtifact(files []string) *Artifact { + return &Artifact{files: files} +} + +func (a *Artifact) BuilderId() string { + return BuilderId +} + +func (a *Artifact) Files() []string { + return a.files +} + +func (a *Artifact) Id() string { + return "" +} + +func (a *Artifact) String() string { + files := strings.Join(a.files, ", ") + return fmt.Sprintf("Created artifact from files: %s", files) +} + +func (a *Artifact) State(name string) interface{} { + return nil +} + +func (a *Artifact) Destroy() error { + for _, f := range a.files { + err := os.RemoveAll(f) + if err != nil { + return err + } + } + return nil +} diff --git a/post-processor/checksum/post-processor.go b/post-processor/checksum/post-processor.go new file mode 100644 index 000000000..55ea28ad8 --- /dev/null +++ b/post-processor/checksum/post-processor.go @@ -0,0 +1,129 @@ +package checksum + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "fmt" + "hash" + "io" + "os" + "path/filepath" + + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + Keep bool `mapstructure:"keep_input_artifact"` + ChecksumTypes []string `mapstructure:"checksum_types"` + OutputPath string `mapstructure:"output"` + ctx interpolate.Context +} + +type PostProcessor struct { + config Config +} + +func (p *PostProcessor) Configure(raws ...interface{}) error { + err := config.Decode(&p.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{}, + }, + }, raws...) + if err != nil { + return err + } + + if p.config.ChecksumTypes == nil { + p.config.ChecksumTypes = []string{"md5"} + } + + if p.config.OutputPath == "" { + p.config.OutputPath = "packer_{{.BuildName}}_{{.BuilderType}}" + ".checksum" + } + + errs := new(packer.MultiError) + + if err = interpolate.Validate(p.config.OutputPath, &p.config.ctx); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error parsing target template: %s", err)) + } + + if len(errs.Errors) > 0 { + return errs + } + + return nil +} + +func getHash(t string) hash.Hash { + var h hash.Hash + switch t { + case "md5": + h = md5.New() + case "sha1": + h = sha1.New() + case "sha224": + h = sha256.New224() + case "sha256": + h = sha256.New() + case "sha384": + h = sha512.New384() + case "sha512": + h = sha512.New() + } + return h +} + +func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + files := artifact.Files() + var h hash.Hash + var checksumFile string + + newartifact := NewArtifact(artifact.Files()) + + for _, ct := range p.config.ChecksumTypes { + h = getHash(ct) + + for _, art := range files { + if len(artifact.Files()) > 1 { + checksumFile = filepath.Join(filepath.Dir(art), ct+"sums") + } else if p.config.OutputPath != "" { + checksumFile = p.config.OutputPath + } else { + checksumFile = fmt.Sprintf("%s.%s", art, ct+"sum") + } + if _, err := os.Stat(checksumFile); err != nil { + newartifact.files = append(newartifact.files, checksumFile) + } + + fw, err := os.OpenFile(checksumFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0644)) + if err != nil { + return nil, false, fmt.Errorf("unable to create file %s: %s", checksumFile, err.Error()) + } + fr, err := os.Open(art) + if err != nil { + fw.Close() + return nil, false, fmt.Errorf("unable to open file %s: %s", art, err.Error()) + } + + if _, err = io.Copy(h, fr); err != nil { + fr.Close() + fw.Close() + return nil, false, fmt.Errorf("unable to compute %s hash for %s", ct, art) + } + fr.Close() + fw.WriteString(fmt.Sprintf("%x\t%s\n", h.Sum(nil), filepath.Base(art))) + fw.Close() + } + } + + return newartifact, true, nil +} diff --git a/post-processor/checksum/post-processor_test.go b/post-processor/checksum/post-processor_test.go new file mode 100644 index 000000000..8b60df2b4 --- /dev/null +++ b/post-processor/checksum/post-processor_test.go @@ -0,0 +1,108 @@ +package checksum + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/mitchellh/packer/builder/file" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template" +) + +const expectedFileContents = "Hello world!" + +func TestChecksumSHA1(t *testing.T) { + const config = ` + { + "post-processors": [ + { + "type": "checksum", + "checksum_types": ["sha1"], + "output": "sha1sums" + } + ] + } + ` + artifact := testChecksum(t, config) + defer artifact.Destroy() + + f, err := os.Open("sha1sums") + if err != nil { + t.Errorf("Unable to read checksum file: %s", err) + } + if buf, _ := ioutil.ReadAll(f); !bytes.Equal(buf, []byte("d3486ae9136e7856bc42212385ea797094475802\tpackage.txt\n")) { + t.Errorf("Failed to compate checksum: %s\n%s", buf, "d3486ae9136e7856bc42212385ea797094475802 package.txt") + } + + defer f.Close() +} + +// Test Helpers + +func setup(t *testing.T) (packer.Ui, packer.Artifact, error) { + // Create fake UI and Cache + ui := packer.TestUi(t) + cache := &packer.FileCache{CacheDir: os.TempDir()} + + // Create config for file builder + const fileConfig = `{"builders":[{"type":"file","target":"package.txt","content":"Hello world!"}]}` + tpl, err := template.Parse(strings.NewReader(fileConfig)) + if err != nil { + return nil, nil, fmt.Errorf("Unable to parse setup configuration: %s", err) + } + + // Prepare the file builder + builder := file.Builder{} + warnings, err := builder.Prepare(tpl.Builders["file"].Config) + if len(warnings) > 0 { + for _, warn := range warnings { + return nil, nil, fmt.Errorf("Configuration warning: %s", warn) + } + } + if err != nil { + return nil, nil, fmt.Errorf("Invalid configuration: %s", err) + } + + // Run the file builder + artifact, err := builder.Run(ui, nil, cache) + if err != nil { + return nil, nil, fmt.Errorf("Failed to build artifact: %s", err) + } + + return ui, artifact, err +} + +func testChecksum(t *testing.T, config string) packer.Artifact { + ui, artifact, err := setup(t) + if err != nil { + t.Fatalf("Error bootstrapping test: %s", err) + } + if artifact != nil { + defer artifact.Destroy() + } + + tpl, err := template.Parse(strings.NewReader(config)) + if err != nil { + t.Fatalf("Unable to parse test config: %s", err) + } + + checksum := PostProcessor{} + checksum.Configure(tpl.PostProcessors[0][0].Config) + + // I get the feeling these should be automatically available somewhere, but + // some of the post-processors construct this manually. + checksum.config.ctx.BuildName = "chocolate" + checksum.config.PackerBuildName = "vanilla" + checksum.config.PackerBuilderType = "file" + + artifactOut, _, err := checksum.PostProcess(ui, artifact) + if err != nil { + t.Fatalf("Failed to checksum artifact: %s", err) + } + + return artifactOut +} diff --git a/website/source/docs/post-processors/checksum.html.md b/website/source/docs/post-processors/checksum.html.md new file mode 100644 index 000000000..8a6616c7e --- /dev/null +++ b/website/source/docs/post-processors/checksum.html.md @@ -0,0 +1,46 @@ +--- +description: | + The checksum post-processor computes specified checksum for the artifact list + from an upstream builder or post-processor. All downstream post-processors will + see the new artifacts. The primary use-case is compute checksum for artifacts + allows to verify it later. + So firstly this post-processor get artifact, compute it checksum and pass to + next post-processor original artifacts and checksum files. +layout: docs +page_title: 'Checksum Post-Processor' +... + +# Checksum Post-Processor + +Type: `checksum` + +The checksum post-processor computes specified checksum for the artifact list +from an upstream builder or post-processor. All downstream post-processors will +see the new artifacts. The primary use-case is compute checksum for artifact to +verify it later. + +After computes checksum for artifacts, you can use new artifacts with other +post-processors like +[artifice](https://www.packer.io/docs/post-processors/artifice.html), +[compress](https://www.packer.io/docs/post-processors/compress.html), +[docker-push](https://www.packer.io/docs/post-processors/docker-push.html), +[atlas](https://www.packer.io/docs/post-processors/atlas.html), or a third-party +post-processor. + +## Basic example + +The example below is fully functional. + +``` {.javascript} +{ +"type": "checksum" +} +``` + +## Configuration Reference + +Optional parameters: + +- `checksum_types` (array of strings) - An array of strings of checksum types +to compute. Allowed values are md5, sha1, sha224, sha256, sha384, sha512. +- `output` (string) - Specify filename to store checksums. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 961a7c22a..4a43da5e7 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -75,6 +75,7 @@
  • Artifice
  • Atlas
  • compress
  • +
  • checksum
  • docker-import
  • docker-push
  • docker-save