diff --git a/backend/init/init.go b/backend/init/init.go index 1273ff646..1709f93ba 100644 --- a/backend/init/init.go +++ b/backend/init/init.go @@ -10,6 +10,7 @@ import ( backendatlas "github.com/hashicorp/terraform/backend/atlas" backendlocal "github.com/hashicorp/terraform/backend/local" + backendartifactory "github.com/hashicorp/terraform/backend/remote-state/artifactory" backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure" backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul" backendetcdv2 "github.com/hashicorp/terraform/backend/remote-state/etcdv2" @@ -42,19 +43,19 @@ func init() { // Our hardcoded backends. We don't need to acquire a lock here // since init() code is serial and can't spawn goroutines. backends = map[string]func() backend.Backend{ - "atlas": func() backend.Backend { return &backendatlas.Backend{} }, - "http": func() backend.Backend { return backendhttp.New() }, - "local": func() backend.Backend { return &backendlocal.Local{} }, - "consul": func() backend.Backend { return backendconsul.New() }, - "inmem": func() backend.Backend { return backendinmem.New() }, - "swift": func() backend.Backend { return backendSwift.New() }, - "s3": func() backend.Backend { return backendS3.New() }, - "azurerm": func() backend.Backend { return backendAzure.New() }, - "etcd": func() backend.Backend { return backendetcdv2.New() }, - "etcdv3": func() backend.Backend { return backendetcdv3.New() }, - "gcs": func() backend.Backend { return backendGCS.New() }, - "manta": func() backend.Backend { return backendManta.New() }, - } + "artifactory": func() backend.Backend { return backendartifactory.New() }, + "atlas": func() backend.Backend { return &backendatlas.Backend{} }, + "http": func() backend.Backend { return backendhttp.New() }, + "local": func() backend.Backend { return &backendlocal.Local{} }, + "consul": func() backend.Backend { return backendconsul.New() }, + "inmem": func() backend.Backend { return backendinmem.New() }, + "swift": func() backend.Backend { return backendSwift.New() }, + "s3": func() backend.Backend { return backendS3.New() }, + "azurerm": func() backend.Backend { return backendAzure.New() }, + "etcd": func() backend.Backend { return backendetcdv2.New() }, + "etcdv3": func() backend.Backend { return backendetcdv3.New() }, + "gcs": func() backend.Backend { return backendGCS.New() }, + "manta": func() backend.Backend { return backendManta.New() }, "azure": func() backend.Backend { return deprecateBackend( diff --git a/backend/remote-state/artifactory/backend.go b/backend/remote-state/artifactory/backend.go new file mode 100644 index 000000000..cbbf92072 --- /dev/null +++ b/backend/remote-state/artifactory/backend.go @@ -0,0 +1,100 @@ +package artifactory + +import ( + "context" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/state/remote" + artifactory "github.com/lusis/go-artifactory/src/artifactory.v401" +) + +func New() backend.Backend { + s := &schema.Backend{ + Schema: map[string]*schema.Schema{ + "username": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("ARTIFACTORY_USERNAME", nil), + Description: "Username", + }, + "password": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("ARTIFACTORY_PASSWORD", nil), + Description: "Password", + }, + "url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("ARTIFACTORY_URL", nil), + Description: "Artfactory base URL", + }, + "repo": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "The repository name", + }, + "subpath": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Path within the repository", + }, + }, + } + + b := &Backend{Backend: s} + b.Backend.ConfigureFunc = b.configure + return b +} + +type Backend struct { + *schema.Backend + + client *ArtifactoryClient +} + +func (b *Backend) configure(ctx context.Context) error { + data := schema.FromContextBackendConfig(ctx) + + userName := data.Get("username").(string) + password := data.Get("password").(string) + url := data.Get("url").(string) + repo := data.Get("repo").(string) + subpath := data.Get("subpath").(string) + + clientConf := &artifactory.ClientConfig{ + BaseURL: url, + Username: userName, + Password: password, + } + nativeClient := artifactory.NewClient(clientConf) + + b.client = &ArtifactoryClient{ + nativeClient: &nativeClient, + userName: userName, + password: password, + url: url, + repo: repo, + subpath: subpath, + } + return nil +} + +func (b *Backend) States() ([]string, error) { + return nil, backend.ErrNamedStatesNotSupported +} + +func (b *Backend) DeleteState(string) error { + return backend.ErrNamedStatesNotSupported +} + +func (b *Backend) State(name string) (state.State, error) { + if name != backend.DefaultStateName { + return nil, backend.ErrNamedStatesNotSupported + } + return &remote.State{ + Client: b.client, + }, nil +} diff --git a/backend/remote-state/artifactory/client.go b/backend/remote-state/artifactory/client.go new file mode 100644 index 000000000..030c54b78 --- /dev/null +++ b/backend/remote-state/artifactory/client.go @@ -0,0 +1,63 @@ +package artifactory + +import ( + "crypto/md5" + "fmt" + "strings" + + "github.com/hashicorp/terraform/state/remote" + artifactory "github.com/lusis/go-artifactory/src/artifactory.v401" +) + +const ARTIF_TFSTATE_NAME = "terraform.tfstate" + +type ArtifactoryClient struct { + nativeClient *artifactory.ArtifactoryClient + userName string + password string + url string + repo string + subpath string +} + +func (c *ArtifactoryClient) Get() (*remote.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 := &remote.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/backend/remote-state/artifactory/client_test.go similarity index 73% rename from state/remote/artifactory_test.go rename to backend/remote-state/artifactory/client_test.go index 74197fa91..a7f2707d3 100644 --- a/state/remote/artifactory_test.go +++ b/backend/remote-state/artifactory/client_test.go @@ -1,25 +1,21 @@ -package remote +package artifactory import ( "testing" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state/remote" ) func TestArtifactoryClient_impl(t *testing.T) { - var _ Client = new(ArtifactoryClient) + var _ remote.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 := make(map[string]interface{}) config["url"] = "http://artifactory.local:8081/artifactory" config["repo"] = "terraform-repo" config["subpath"] = "myproject" @@ -30,12 +26,14 @@ func TestArtifactoryFactory(t *testing.T) { config["username"] = "test" config["password"] = "testpass" - client, err := artifactoryFactory(config) + b := backend.TestBackendConfig(t, New(), config) + + state, err := b.State(backend.DefaultStateName) if err != nil { - t.Fatalf("Error for valid config") + t.Fatalf("Error for valid config: %s", err) } - artifactoryClient := client.(*ArtifactoryClient) + artifactoryClient := state.(*remote.State).Client.(*ArtifactoryClient) if artifactoryClient.nativeClient.Config.BaseURL != "http://artifactory.local:8081/artifactory" { t.Fatalf("Incorrect url was populated") diff --git a/state/remote/artifactory.go b/state/remote/artifactory.go deleted file mode 100644 index 727e9faf0..000000000 --- a/state/remote/artifactory.go +++ /dev/null @@ -1,117 +0,0 @@ -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/remote.go b/state/remote/remote.go index c38cdfb48..58f2578d2 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -44,6 +44,4 @@ 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{ - "artifactory": artifactoryFactory, -} +var BuiltinClients = map[string]Factory{}