post-processor/atlas: make it
This commit is contained in:
parent
6f66afcee0
commit
f3c1132f23
|
@ -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()
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package main
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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 ""
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue