From c3f863f4c5a4cf61eef49dc31db3830116e18694 Mon Sep 17 00:00:00 2001 From: "John E. Vincent" Date: Thu, 29 Oct 2015 09:33:09 -0400 Subject: [PATCH] add artifactory remote state storage --- state/remote/artifactory.go | 117 +++++++++++++++++++++++++++++++ state/remote/artifactory_test.go | 55 +++++++++++++++ state/remote/remote.go | 13 ++-- 3 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 state/remote/artifactory.go create mode 100644 state/remote/artifactory_test.go diff --git a/state/remote/artifactory.go b/state/remote/artifactory.go new file mode 100644 index 000000000..727e9faf0 --- /dev/null +++ b/state/remote/artifactory.go @@ -0,0 +1,117 @@ +package remote + +import ( + "crypto/md5" + "fmt" + "os" + "strings" + + artifactory "github.com/lusis/go-artifactory/src/artifactory.v401" +) + +const ARTIF_TFSTATE_NAME = "terraform.tfstate" + +func artifactoryFactory(conf map[string]string) (Client, error) { + userName, ok := conf["username"] + if !ok { + userName = os.Getenv("ARTIFACTORY_USERNAME") + if userName == "" { + return nil, fmt.Errorf( + "missing 'username' configuration or ARTIFACTORY_USERNAME environment variable") + } + } + password, ok := conf["password"] + if !ok { + password = os.Getenv("ARTIFACTORY_PASSWORD") + if password == "" { + return nil, fmt.Errorf( + "missing 'password' configuration or ARTIFACTORY_PASSWORD environment variable") + } + } + url, ok := conf["url"] + if !ok { + url = os.Getenv("ARTIFACTORY_URL") + if url == "" { + return nil, fmt.Errorf( + "missing 'url' configuration or ARTIFACTORY_URL environment variable") + } + } + repo, ok := conf["repo"] + if !ok { + return nil, fmt.Errorf( + "missing 'repo' configuration") + } + subpath, ok := conf["subpath"] + if !ok { + return nil, fmt.Errorf( + "missing 'subpath' configuration") + } + + clientConf := &artifactory.ClientConfig{ + BaseURL: url, + Username: userName, + Password: password, + } + nativeClient := artifactory.NewClient(clientConf) + + return &ArtifactoryClient{ + nativeClient: &nativeClient, + userName: userName, + password: password, + url: url, + repo: repo, + subpath: subpath, + }, nil + +} + +type ArtifactoryClient struct { + nativeClient *artifactory.ArtifactoryClient + userName string + password string + url string + repo string + subpath string +} + +func (c *ArtifactoryClient) Get() (*Payload, error) { + p := fmt.Sprintf("%s/%s/%s", c.repo, c.subpath, ARTIF_TFSTATE_NAME) + output, err := c.nativeClient.Get(p, make(map[string]string)) + if err != nil { + if strings.Contains(err.Error(), "404") { + return nil, nil + } + return nil, err + } + + // TODO: migrate to using X-Checksum-Md5 header from artifactory + // needs to be exposed by go-artifactory first + + hash := md5.Sum(output) + payload := &Payload{ + Data: output, + MD5: hash[:md5.Size], + } + + // If there was no data, then return nil + if len(payload.Data) == 0 { + return nil, nil + } + + return payload, nil +} + +func (c *ArtifactoryClient) Put(data []byte) error { + p := fmt.Sprintf("%s/%s/%s", c.repo, c.subpath, ARTIF_TFSTATE_NAME) + if _, err := c.nativeClient.Put(p, string(data), make(map[string]string)); err == nil { + return nil + } else { + return fmt.Errorf("Failed to upload state: %v", err) + } +} + +func (c *ArtifactoryClient) Delete() error { + p := fmt.Sprintf("%s/%s/%s", c.repo, c.subpath, ARTIF_TFSTATE_NAME) + err := c.nativeClient.Delete(p) + return err +} diff --git a/state/remote/artifactory_test.go b/state/remote/artifactory_test.go new file mode 100644 index 000000000..74197fa91 --- /dev/null +++ b/state/remote/artifactory_test.go @@ -0,0 +1,55 @@ +package remote + +import ( + "testing" +) + +func TestArtifactoryClient_impl(t *testing.T) { + var _ Client = new(ArtifactoryClient) +} + +func TestArtifactoryFactory(t *testing.T) { + // This test just instantiates the client. Shouldn't make any actual + // requests nor incur any costs. + + config := make(map[string]string) + + // Empty config is an error + _, err := artifactoryFactory(config) + if err == nil { + t.Fatalf("Empty config should be error") + } + + config["url"] = "http://artifactory.local:8081/artifactory" + config["repo"] = "terraform-repo" + config["subpath"] = "myproject" + + // For this test we'll provide the credentials as config. The + // acceptance tests implicitly test passing credentials as + // environment variables. + config["username"] = "test" + config["password"] = "testpass" + + client, err := artifactoryFactory(config) + if err != nil { + t.Fatalf("Error for valid config") + } + + artifactoryClient := client.(*ArtifactoryClient) + + if artifactoryClient.nativeClient.Config.BaseURL != "http://artifactory.local:8081/artifactory" { + t.Fatalf("Incorrect url was populated") + } + if artifactoryClient.nativeClient.Config.Username != "test" { + t.Fatalf("Incorrect username was populated") + } + if artifactoryClient.nativeClient.Config.Password != "testpass" { + t.Fatalf("Incorrect password was populated") + } + if artifactoryClient.repo != "terraform-repo" { + t.Fatalf("Incorrect repo was populated") + } + if artifactoryClient.subpath != "myproject" { + t.Fatalf("Incorrect subpath was populated") + } +} diff --git a/state/remote/remote.go b/state/remote/remote.go index 5337ad7b7..4074c2c64 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -36,12 +36,13 @@ func NewClient(t string, conf map[string]string) (Client, error) { // BuiltinClients is the list of built-in clients that can be used with // NewClient. var BuiltinClients = map[string]Factory{ - "atlas": atlasFactory, - "consul": consulFactory, - "etcd": etcdFactory, - "http": httpFactory, - "s3": s3Factory, - "swift": swiftFactory, + "atlas": atlasFactory, + "consul": consulFactory, + "etcd": etcdFactory, + "http": httpFactory, + "s3": s3Factory, + "swift": swiftFactory, + "artifactory": artifactoryFactory, // This is used for development purposes only. "_local": fileFactory,