diff --git a/builtin/providers/atlas/provider.go b/builtin/providers/atlas/provider.go new file mode 100644 index 000000000..ad17cc063 --- /dev/null +++ b/builtin/providers/atlas/provider.go @@ -0,0 +1,78 @@ +package atlas + +import ( + "os" + + "github.com/hashicorp/atlas-go/v1" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +const ( + // defaultAtlasServer is the default endpoint for Atlas if + // none is specified. + defaultAtlasServer = "https://atlas.hashicorp.com" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFunc("ATLAS_ADDRESS", defaultAtlasServer), + Description: descriptions["address"], + }, + + "token": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFunc("ATLAS_TOKEN", ""), + Description: descriptions["token"], + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "atlas_artifact": resourceArtifact(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + var err error + client := atlas.DefaultClient() + if v := d.Get("address").(string); v != "" { + client, err = atlas.NewClient(v) + if err != nil { + return nil, err + } + } + client.Token = d.Get("token").(string) + + return client, nil +} + +func envDefaultFunc(k, alt string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + if v := os.Getenv(k); v != "" { + return v, nil + } + + return alt, nil + } +} + +var descriptions map[string]string + +func init() { + descriptions = map[string]string{ + "address": "The address of the Atlas server. If blank, the public\n" + + "server at atlas.hashicorp.com will be used.", + + "token": "The access token for reading artifacts. This is required\n" + + "if reading private artifacts.", + } +} diff --git a/builtin/providers/atlas/provider_test.go b/builtin/providers/atlas/provider_test.go new file mode 100644 index 000000000..cd03dc3a5 --- /dev/null +++ b/builtin/providers/atlas/provider_test.go @@ -0,0 +1,35 @@ +package atlas + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "atlas": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("ATLAS_TOKEN"); v == "" { + t.Fatal("ATLAS_TOKEN must be set for acceptance tests") + } +} diff --git a/builtin/providers/atlas/resource_artifact.go b/builtin/providers/atlas/resource_artifact.go new file mode 100644 index 000000000..7353543d2 --- /dev/null +++ b/builtin/providers/atlas/resource_artifact.go @@ -0,0 +1,170 @@ +package atlas + +import ( + "fmt" + "regexp" + + "github.com/hashicorp/atlas-go/v1" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +var ( + // saneMetaKey is used to sanitize the metadata keys so that + // they can be accessed as a variable interpolation from TF + saneMetaKey = regexp.MustCompile("[^a-zA-Z0-9-_]") +) + +func resourceArtifact() *schema.Resource { + return &schema.Resource{ + Create: resourceArtifactRead, + Read: resourceArtifactRead, + Update: resourceArtifactRead, + Delete: resourceArtifactDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "build": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "metadata_keys": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + + "metadata": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + }, + + "file_url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "metadata_full": &schema.Schema{ + Type: schema.TypeMap, + Computed: true, + }, + + "slug": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "version_real": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceArtifactRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*atlas.Client) + + // Parse the slug from the name given of the artifact since the API + // expects these to be split. + user, name, err := atlas.ParseSlug(d.Get("name").(string)) + if err != nil { + return err + } + + // Filter by version or build if given + var build, version string + if v, ok := d.GetOk("version"); ok { + version = v.(string) + } else if b, ok := d.GetOk("build"); ok { + build = b.(string) + } + + // If we have neither, default to latest version + if build == "" && version == "" { + version = "latest" + } + + // Compile the metadata search params + md := make(map[string]string) + for _, v := range d.Get("metadata_keys").(*schema.Set).List() { + md[v.(string)] = atlas.MetadataAnyValue + } + for k, v := range d.Get("metadata").(map[string]interface{}) { + md[k] = v.(string) + } + + // Do the search! + vs, err := client.ArtifactSearch(&atlas.ArtifactSearchOpts{ + User: user, + Name: name, + Type: d.Get("type").(string), + Build: build, + Version: version, + Metadata: md, + }) + if err != nil { + return fmt.Errorf( + "Error searching for artifact: %s", err) + } + + if len(vs) == 0 { + return fmt.Errorf("No matching artifact") + } else if len(vs) > 1 { + return fmt.Errorf("Got %d results, only one is allowed", len(vs)) + } + v := vs[0] + + d.SetId(v.ID) + if v.ID == "" { + d.SetId(fmt.Sprintf("%s %d", v.Tag, v.Version)) + } + d.Set("version_real", v.Version) + d.Set("metadata_full", cleanMetadata(v.Metadata)) + d.Set("slug", v.Slug) + + d.Set("file_url", "") + if u, err := client.ArtifactFileURL(v); err != nil { + return fmt.Errorf( + "Error reading file URL: %s", err) + } else if u != nil { + d.Set("file_url", u.String()) + } + + return nil +} + +func resourceArtifactDelete(d *schema.ResourceData, meta interface{}) error { + // This just always succeeds since this is a readonly element. + d.SetId("") + return nil +} + +// cleanMetadata is used to ensure the metadata is accessible as +// a variable by doing a simple re-write. +func cleanMetadata(in map[string]string) map[string]string { + out := make(map[string]string, len(in)) + for k, v := range in { + sane := saneMetaKey.ReplaceAllString(k, "-") + out[sane] = v + } + return out +} diff --git a/builtin/providers/atlas/resource_artifact_test.go b/builtin/providers/atlas/resource_artifact_test.go new file mode 100644 index 000000000..19f6680d8 --- /dev/null +++ b/builtin/providers/atlas/resource_artifact_test.go @@ -0,0 +1,166 @@ +package atlas + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccArtifact_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccArtifact_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckArtifactState("name", "hashicorp/tf-provider-test"), + ), + }, + }, + }) +} + +func TestAccArtifact_metadata(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccArtifact_metadata, + Check: resource.ComposeTestCheckFunc( + testAccCheckArtifactState("name", "hashicorp/tf-provider-test"), + testAccCheckArtifactState("id", "x86"), + testAccCheckArtifactState("metadata_full.arch", "x86"), + ), + }, + }, + }) +} + +func TestAccArtifact_metadataSet(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccArtifact_metadataSet, + Check: resource.ComposeTestCheckFunc( + testAccCheckArtifactState("name", "hashicorp/tf-provider-test"), + testAccCheckArtifactState("id", "x64"), + testAccCheckArtifactState("metadata_full.arch", "x64"), + ), + }, + }, + }) +} + +func TestAccArtifact_buildLatest(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccArtifact_buildLatest, + Check: resource.ComposeTestCheckFunc( + testAccCheckArtifactState("name", "hashicorp/tf-provider-test"), + ), + }, + }, + }) +} + +func TestAccArtifact_versionAny(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccArtifact_versionAny, + Check: resource.ComposeTestCheckFunc( + testAccCheckArtifactState("name", "hashicorp/tf-provider-test"), + ), + }, + }, + }) +} + +func testAccCheckArtifactState(key, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources["atlas_artifact.foobar"] + if !ok { + return fmt.Errorf("Not found: %s", "atlas_artifact.foobar") + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + p := rs.Primary + if p.Attributes[key] != value { + return fmt.Errorf( + "%s != %s (actual: %s)", key, value, p.Attributes[key]) + } + + return nil + } +} + +func TestCleanMetadata(t *testing.T) { + in := map[string]string{ + "region.us-east-1": "in", + "what is this?": "out", + } + exp := map[string]string{ + "region-us-east-1": "in", + "what-is-this-": "out", + } + out := cleanMetadata(in) + if !reflect.DeepEqual(out, exp) { + t.Fatalf("bad: %#v", out) + } +} + +const testAccArtifact_basic = ` +resource "atlas_artifact" "foobar" { + name = "hashicorp/tf-provider-test" + type = "foo" +}` + +const testAccArtifact_metadata = ` +resource "atlas_artifact" "foobar" { + name = "hashicorp/tf-provider-test" + type = "foo" + metadata { + arch = "x86" + } + version = "any" +}` + +const testAccArtifact_metadataSet = ` +resource "atlas_artifact" "foobar" { + name = "hashicorp/tf-provider-test" + type = "foo" + metadata_keys = ["arch"] + version = "any" +}` + +const testAccArtifact_buildLatest = ` +resource "atlas_artifact" "foobar" { + name = "hashicorp/tf-provider-test" + type = "foo" + build = "latest" + metadata { + arch = "x86" + } +}` + +const testAccArtifact_versionAny = ` +resource "atlas_artifact" "foobar" { + name = "hashicorp/tf-provider-test" + type = "foo" + version = "any" +}`