From f3c1132f23db40946854f6517dad7dccb87729bb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Dec 2014 15:00:03 -0800 Subject: [PATCH] post-processor/atlas: make it --- plugin/post-processor-atlas/main.go | 15 ++ plugin/post-processor-atlas/main_test.go | 1 + post-processor/atlas/artifact.go | 37 +++ post-processor/atlas/post-processor.go | 258 ++++++++++++++++++++ post-processor/atlas/post-processor_test.go | 136 +++++++++++ post-processor/atlas/util.go | 47 ++++ post-processor/atlas/util_test.go | 36 +++ 7 files changed, 530 insertions(+) create mode 100644 plugin/post-processor-atlas/main.go create mode 100644 plugin/post-processor-atlas/main_test.go create mode 100644 post-processor/atlas/artifact.go create mode 100644 post-processor/atlas/post-processor.go create mode 100644 post-processor/atlas/post-processor_test.go create mode 100644 post-processor/atlas/util.go create mode 100644 post-processor/atlas/util_test.go diff --git a/plugin/post-processor-atlas/main.go b/plugin/post-processor-atlas/main.go new file mode 100644 index 000000000..21bbd13cf --- /dev/null +++ b/plugin/post-processor-atlas/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/post-processor/atlas" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterPostProcessor(new(atlas.PostProcessor)) + server.Serve() +} diff --git a/plugin/post-processor-atlas/main_test.go b/plugin/post-processor-atlas/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/post-processor-atlas/main_test.go @@ -0,0 +1 @@ +package main diff --git a/post-processor/atlas/artifact.go b/post-processor/atlas/artifact.go new file mode 100644 index 000000000..50c503a9b --- /dev/null +++ b/post-processor/atlas/artifact.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" +) + +const BuilderId = "packer.post-processor.atlas" + +type Artifact struct { + Name string + Type string + Version int +} + +func (*Artifact) BuilderId() string { + return BuilderId +} + +func (a *Artifact) Files() []string { + return nil +} + +func (a *Artifact) Id() string { + return fmt.Sprintf("%s/%s/%d", a.Name, a.Type, a.Version) +} + +func (a *Artifact) String() string { + return fmt.Sprintf("%s/%s (v%d)", a.Name, a.Type, a.Version) +} + +func (*Artifact) State(name string) interface{} { + return nil +} + +func (a *Artifact) Destroy() error { + return nil +} diff --git a/post-processor/atlas/post-processor.go b/post-processor/atlas/post-processor.go new file mode 100644 index 000000000..40c2ce397 --- /dev/null +++ b/post-processor/atlas/post-processor.go @@ -0,0 +1,258 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/hashicorp/atlas-go/archive" + "github.com/hashicorp/atlas-go/v1" + "github.com/mitchellh/mapstructure" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" +) + +const BuildEnvKey = "ATLAS_BUILD_ID" + +// Artifacts can return a string for this state key and the post-processor +// will use automatically use this as the type. The user's value overrides +// this if `artifact_type_override` is set to true. +const ArtifactStateType = "atlas.artifact.type" + +// Artifacts can return a map[string]string for this state key and this +// post-processor will automatically merge it into the metadata for any +// uploaded artifact versions. +const ArtifactStateMetadata = "atlas.artifact.metadata" + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + Artifact string + Type string `mapstructure:"artifact_type"` + TypeOverride bool `mapstructure:"artifact_type_override"` + Metadata map[string]string + + ServerAddr string `mapstructure:"server_address"` + Token string + + // This shouldn't ever be set outside of unit tests. + Test bool `mapstructure:"test"` + + tpl *packer.ConfigTemplate + user, name string + buildId int +} + +type PostProcessor struct { + config Config + client *atlas.Client +} + +func (p *PostProcessor) Configure(raws ...interface{}) error { + _, err := common.DecodeConfig(&p.config, raws...) + if err != nil { + return err + } + + p.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + p.config.tpl.UserVars = p.config.PackerUserVars + + templates := map[string]*string{ + "artifact": &p.config.Artifact, + "type": &p.config.Type, + "server_address": &p.config.ServerAddr, + "token": &p.config.Token, + } + + errs := new(packer.MultiError) + for key, ptr := range templates { + *ptr, err = p.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", key, err)) + } + } + + required := map[string]*string{ + "artifact": &p.config.Artifact, + "artifact_type": &p.config.Type, + } + + for key, ptr := range required { + if *ptr == "" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("%s must be set", key)) + } + } + + if len(errs.Errors) > 0 { + return errs + } + + p.config.user, p.config.name, err = atlas.ParseSlug(p.config.Artifact) + if err != nil { + return err + } + + // If we have a build ID, save it + if v := os.Getenv(BuildEnvKey); v != "" { + raw, err := strconv.ParseInt(v, 0, 0) + if err != nil { + return fmt.Errorf( + "Error parsing build ID: %s", err) + } + + p.config.buildId = int(raw) + } + + // Build the client + p.client = atlas.DefaultClient() + if p.config.ServerAddr != "" { + p.client, err = atlas.NewClient(p.config.ServerAddr) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error initializing client: %s", err)) + return errs + } + } + if p.config.Token != "" { + p.client.Token = p.config.Token + } + + if !p.config.Test { + // Verify the client + if err := p.client.Verify(); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error initializing client: %s", err)) + return errs + } + } + + return nil +} + +func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + if _, err := p.client.Artifact(p.config.user, p.config.name); err != nil { + if err != atlas.ErrNotFound { + return nil, false, fmt.Errorf( + "Error finding artifact: %s", err) + } + + // Artifact doesn't exist, create it + ui.Message(fmt.Sprintf("Creating artifact: %s", p.config.Artifact)) + _, err = p.client.CreateArtifact(p.config.user, p.config.name) + if err != nil { + return nil, false, fmt.Errorf( + "Error creating artifact: %s", err) + } + } + + opts := &atlas.UploadArtifactOpts{ + User: p.config.user, + Name: p.config.name, + Type: p.config.Type, + ID: artifact.Id(), + Metadata: p.metadata(artifact), + BuildId: p.config.buildId, + } + + if fs := artifact.Files(); len(fs) > 0 { + var archiveOpts archive.ArchiveOpts + + // We have files. We want to compress/upload them. If we have just + // one file, then we use it as-is. Otherwise, we compress all of + // them into a single file. + var path string + if len(fs) == 1 { + path = fs[0] + } else { + path = longestCommonPrefix(fs) + if path == "" { + return nil, false, fmt.Errorf( + "No common prefix for achiving files: %v", fs) + } + + // Modify the archive options to only include the files + // that are in our file list. + include := make([]string, 0, len(fs)) + for i, f := range fs { + include[i] = strings.Replace(f, path, "", 1) + } + archiveOpts.Include = include + } + + r, err := archive.CreateArchive(path, &archiveOpts) + if err != nil { + return nil, false, fmt.Errorf( + "Error archiving artifact: %s", err) + } + defer r.Close() + + opts.File = r + opts.FileSize = r.Size + } + + ui.Message("Uploading artifact version...") + var av *atlas.ArtifactVersion + doneCh := make(chan struct{}) + errCh := make(chan error, 1) + go func() { + var err error + av, err = p.client.UploadArtifact(opts) + if err != nil { + errCh <- err + return + } + close(doneCh) + }() + + select { + case err := <-errCh: + return nil, false, fmt.Errorf("Error uploading: %s", err) + case <-doneCh: + } + + return &Artifact{ + Name: p.config.Artifact, + Type: p.config.Type, + Version: av.Version, + }, true, nil +} + +func (p *PostProcessor) metadata(artifact packer.Artifact) map[string]string { + var metadata map[string]string + metadataRaw := artifact.State(ArtifactStateMetadata) + if metadataRaw != nil { + if err := mapstructure.Decode(metadataRaw, &metadata); err != nil { + panic(err) + } + } + + if p.config.Metadata != nil { + // If we have no extra metadata, just return as-is + if metadata == nil { + return p.config.Metadata + } + + // Merge the metadata + for k, v := range p.config.Metadata { + metadata[k] = v + } + } + + return metadata +} + +func (p *PostProcessor) artifactType(artifact packer.Artifact) string { + if !p.config.TypeOverride { + if v := artifact.State(ArtifactStateType); v != nil { + return v.(string) + } + } + + return p.config.Type +} diff --git a/post-processor/atlas/post-processor_test.go b/post-processor/atlas/post-processor_test.go new file mode 100644 index 000000000..62fa0e07a --- /dev/null +++ b/post-processor/atlas/post-processor_test.go @@ -0,0 +1,136 @@ +package main + +import ( + "os" + "reflect" + "testing" + + "github.com/mitchellh/packer/packer" +) + +func TestPostProcessorConfigure(t *testing.T) { + var p PostProcessor + if err := p.Configure(validDefaults()); err != nil { + t.Fatalf("err: %s", err) + } + + if p.client == nil { + t.Fatal("should have client") + } + if p.client.Token != "" { + t.Fatal("should not have token") + } +} + +func TestPostProcessorConfigure_buildId(t *testing.T) { + defer os.Setenv(BuildEnvKey, os.Getenv(BuildEnvKey)) + os.Setenv(BuildEnvKey, "5") + + var p PostProcessor + if err := p.Configure(validDefaults()); err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.buildId != 5 { + t.Fatalf("bad: %#v", p.config.buildId) + } +} + +func TestPostProcessorMetadata(t *testing.T) { + var p PostProcessor + if err := p.Configure(validDefaults()); err != nil { + t.Fatalf("err: %s", err) + } + + artifact := new(packer.MockArtifact) + metadata := p.metadata(artifact) + if len(metadata) > 0 { + t.Fatalf("bad: %#v", metadata) + } +} + +func TestPostProcessorMetadata_artifact(t *testing.T) { + config := validDefaults() + config["metadata"] = map[string]string{ + "foo": "bar", + } + + var p PostProcessor + if err := p.Configure(config); err != nil { + t.Fatalf("err: %s", err) + } + + artifact := new(packer.MockArtifact) + artifact.StateValues = map[string]interface{}{ + ArtifactStateMetadata: map[interface{}]interface{}{ + "bar": "baz", + }, + } + + metadata := p.metadata(artifact) + expected := map[string]string{ + "foo": "bar", + "bar": "baz", + } + if !reflect.DeepEqual(metadata, expected) { + t.Fatalf("bad: %#v", metadata) + } +} + +func TestPostProcessorMetadata_config(t *testing.T) { + config := validDefaults() + config["metadata"] = map[string]string{ + "foo": "bar", + } + + var p PostProcessor + if err := p.Configure(config); err != nil { + t.Fatalf("err: %s", err) + } + + artifact := new(packer.MockArtifact) + metadata := p.metadata(artifact) + expected := map[string]string{ + "foo": "bar", + } + if !reflect.DeepEqual(metadata, expected) { + t.Fatalf("bad: %#v", metadata) + } +} + +func TestPostProcessorType(t *testing.T) { + var p PostProcessor + if err := p.Configure(validDefaults()); err != nil { + t.Fatalf("err: %s", err) + } + + artifact := new(packer.MockArtifact) + actual := p.artifactType(artifact) + if actual != "foo" { + t.Fatalf("bad: %#v", actual) + } +} + +func TestPostProcessorType_artifact(t *testing.T) { + var p PostProcessor + if err := p.Configure(validDefaults()); err != nil { + t.Fatalf("err: %s", err) + } + + artifact := new(packer.MockArtifact) + artifact.StateValues = map[string]interface{}{ + ArtifactStateType: "bar", + } + actual := p.artifactType(artifact) + if actual != "bar" { + t.Fatalf("bad: %#v", actual) + } +} + +func validDefaults() map[string]interface{} { + return map[string]interface{}{ + "artifact": "mitchellh/test", + "artifact_type": "foo", + "test": true, + } +} diff --git a/post-processor/atlas/util.go b/post-processor/atlas/util.go new file mode 100644 index 000000000..dba2076c7 --- /dev/null +++ b/post-processor/atlas/util.go @@ -0,0 +1,47 @@ +package main + +import ( + "math" + "strings" +) + +// longestCommonPrefix finds the longest common prefix for all the strings +// given as an argument, or returns the empty string if a prefix can't be +// found. +// +// This function just uses brute force instead of a more optimized algorithm. +func longestCommonPrefix(vs []string) string { + // Find the shortest string + var shortest string + length := math.MaxUint32 + for _, v := range vs { + if len(v) < length { + shortest = v + length = len(v) + } + } + + // Now go through and find a prefix to all the strings using this + // short string, which itself must contain the prefix. + for i := len(shortest); i > 0; i-- { + // We only care about prefixes with path seps + if shortest[i-1] != '/' { + continue + } + + bad := false + prefix := shortest[0 : i] + for _, v := range vs { + if !strings.HasPrefix(v, prefix) { + bad = true + break + } + } + + if !bad { + return prefix + } + } + + return "" +} diff --git a/post-processor/atlas/util_test.go b/post-processor/atlas/util_test.go new file mode 100644 index 000000000..181591e84 --- /dev/null +++ b/post-processor/atlas/util_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "testing" +) + +func TestLongestCommonPrefix(t *testing.T) { + cases := []struct { + Input []string + Output string + }{ + { + []string{"foo", "bar"}, + "", + }, + { + []string{"foo", "foobar"}, + "", + }, + { + []string{"foo/", "foo/bar"}, + "foo/", + }, + { + []string{"/foo/", "/bar"}, + "/", + }, + } + + for _, tc := range cases { + actual := longestCommonPrefix(tc.Input) + if actual != tc.Output { + t.Fatalf("bad: %#v\n\n%#v", actual, tc.Input) + } + } +}