diff --git a/tfe/provider.go b/tfe/provider.go index c147b8cc8..56aae2795 100644 --- a/tfe/provider.go +++ b/tfe/provider.go @@ -112,6 +112,7 @@ func Provider() *schema.Provider { "tfe_team_member": resourceTFETeamMember(), "tfe_team_members": resourceTFETeamMembers(), "tfe_team_token": resourceTFETeamToken(), + "tfe_terraform_version": resourceTFETerraformVersion(), "tfe_workspace": resourceTFEWorkspace(), "tfe_variable": resourceTFEVariable(), }, diff --git a/tfe/resource_tfe_terraform_version.go b/tfe/resource_tfe_terraform_version.go new file mode 100644 index 000000000..d67b70766 --- /dev/null +++ b/tfe/resource_tfe_terraform_version.go @@ -0,0 +1,136 @@ +package tfe + +import ( + "fmt" + "log" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceTFETerraformVersion() *schema.Resource { + return &schema.Resource{ + Create: resourceTFETerraformVersionCreate, + Read: resourceTFETerraformVersionRead, + Update: resourceTFETerraformVersionUpdate, + Delete: resourceTFETerraformVersionDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "version": { + Type: schema.TypeString, + Required: true, + }, + "url": { + Type: schema.TypeString, + Required: true, + }, + "sha": { + Type: schema.TypeString, + Required: true, + }, + "official": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "beta": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + } +} + +func resourceTFETerraformVersionCreate(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + opts := tfe.AdminTerraformVersionCreateOptions{ + Version: tfe.String(d.Get("version").(string)), + URL: tfe.String(d.Get("url").(string)), + Sha: tfe.String(d.Get("sha").(string)), + Official: tfe.Bool(d.Get("official").(bool)), + Enabled: tfe.Bool(d.Get("enabled").(bool)), + Beta: tfe.Bool(d.Get("beta").(bool)), + } + + log.Printf("[DEBUG] Create new Terraform version: %s", *opts.Version) + v, err := tfeClient.Admin.TerraformVersions.Create(ctx, opts) + if err != nil { + return fmt.Errorf("Error creating the new Terraform version %s: %v", *opts.Version, err) + } + + d.SetId(v.ID) + + return resourceTFETerraformVersionUpdate(d, meta) +} + +func resourceTFETerraformVersionRead(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + log.Printf("[DEBUG] Read configuration of Terraform version: %s", d.Id()) + v, err := tfeClient.Admin.TerraformVersions.Read(ctx, d.Id()) + if err != nil { + if err == tfe.ErrResourceNotFound { + log.Printf("[DEBUG] Terraform version %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + return err + } + + d.Set("version", v.Version) + d.Set("url", v.URL) + d.Set("sha", v.Sha) + d.Set("official", v.Official) + d.Set("enabled", v.Enabled) + d.Set("beta", v.Beta) + + return nil +} + +func resourceTFETerraformVersionUpdate(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + opts := tfe.AdminTerraformVersionUpdateOptions{ + Version: tfe.String(d.Get("version").(string)), + URL: tfe.String(d.Get("url").(string)), + Sha: tfe.String(d.Get("sha").(string)), + Official: tfe.Bool(d.Get("official").(bool)), + Enabled: tfe.Bool(d.Get("enabled").(bool)), + Beta: tfe.Bool(d.Get("beta").(bool)), + } + + log.Printf("[DEBUG] Update configuration of Terraform version: %s", d.Id()) + v, err := tfeClient.Admin.TerraformVersions.Update(ctx, d.Id(), opts) + if err != nil { + return fmt.Errorf("Error updating Terraform version %s: %v", d.Id(), err) + } + + d.SetId(v.ID) + + return resourceTFETerraformVersionRead(d, meta) +} + +func resourceTFETerraformVersionDelete(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + log.Printf("[DEBUG] Delete Terraform version: %s", d.Id()) + err := tfeClient.Admin.TerraformVersions.Delete(ctx, d.Id()) + if err != nil { + if err == tfe.ErrResourceNotFound { + return nil + } + return fmt.Errorf("Error deleting Terraform version %s: %v", d.Id(), err) + } + + return nil +} diff --git a/tfe/resource_tfe_terraform_version_test.go b/tfe/resource_tfe_terraform_version_test.go new file mode 100644 index 000000000..1e78d6206 --- /dev/null +++ b/tfe/resource_tfe_terraform_version_test.go @@ -0,0 +1,215 @@ +package tfe + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "math/rand" + "testing" + "time" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccTFETerraformVersion_basic(t *testing.T) { + skipIfFreeOnly(t) + + tfVersion := &tfe.AdminTerraformVersion{} + sha := genSha(t, "secret", "data") + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + version := genVersion(rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFETerraformVersionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETerraformVersion_basic(version, sha), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETerraformVersionExists("tfe_terraform_version.foobar", tfVersion), + testAccCheckTFETerraformVersionAttributesBasic(tfVersion, version, sha), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "version", version), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "url", "https://www.hashicorp.com"), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "sha", sha), + ), + }, + }, + }) +} + +func TestAccTFETerraformVersion_full(t *testing.T) { + skipIfFreeOnly(t) + + tfVersion := &tfe.AdminTerraformVersion{} + sha := genSha(t, "secret", "data") + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + version := genVersion(rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFETerraformVersionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETerraformVersion_full(version, sha), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETerraformVersionExists("tfe_terraform_version.foobar", tfVersion), + testAccCheckTFETerraformVersionAttributesFull(tfVersion, version, sha), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "version", version), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "url", "https://www.hashicorp.com"), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "sha", sha), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "official", "false"), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "enabled", "true"), + resource.TestCheckResourceAttr( + "tfe_terraform_version.foobar", "beta", "true"), + ), + }, + }, + }) +} + +func testAccCheckTFETerraformVersionDestroy(s *terraform.State) error { + tfeClient := testAccProvider.Meta().(*tfe.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "tfe_terraform_version" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + _, err := tfeClient.Admin.TerraformVersions.Read(ctx, rs.Primary.ID) + if err == nil { + return fmt.Errorf("Terraform version %s still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckTFETerraformVersionExists(n string, tfVersion *tfe.AdminTerraformVersion) resource.TestCheckFunc { + return func(s *terraform.State) error { + tfeClient := testAccProvider.Meta().(*tfe.Client) + + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + v, err := tfeClient.Admin.TerraformVersions.Read(ctx, rs.Primary.ID) + if err != nil { + return err + } + + if v.ID != rs.Primary.ID { + return fmt.Errorf("Terraform version not found") + } + + *tfVersion = *v + + return nil + } +} + +func testAccCheckTFETerraformVersionAttributesBasic(tfVersion *tfe.AdminTerraformVersion, version string, sha string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if tfVersion.URL != "https://www.hashicorp.com" { + return fmt.Errorf("Bad URL: %s", tfVersion.URL) + } + + if tfVersion.Version != version { + return fmt.Errorf("Bad version: %s", tfVersion.Version) + } + + if tfVersion.Sha != sha { + return fmt.Errorf("Bad value for Sha: %v", tfVersion.Sha) + } + + return nil + } +} + +func testAccCheckTFETerraformVersionAttributesFull(tfVersion *tfe.AdminTerraformVersion, version string, sha string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if tfVersion.URL != "https://www.hashicorp.com" { + return fmt.Errorf("Bad URL: %s", tfVersion.URL) + } + + if tfVersion.Version != version { + return fmt.Errorf("Bad version: %s", tfVersion.Version) + } + + if tfVersion.Sha != sha { + return fmt.Errorf("Bad value for Sha: %v", tfVersion.Sha) + } + + if tfVersion.Official != false { + return fmt.Errorf("Bad value for official: %t", tfVersion.Official) + } + + if tfVersion.Enabled != true { + return fmt.Errorf("Bad value for enabled: %t", tfVersion.Enabled) + } + + if tfVersion.Beta != true { + return fmt.Errorf("Bad value for beta: %t", tfVersion.Beta) + } + + return nil + } +} + +func testAccTFETerraformVersion_basic(version string, sha string) string { + return fmt.Sprintf(` +resource "tfe_terraform_version" "foobar" { + version = "%s" + url = "https://www.hashicorp.com" + sha = "%s" +}`, version, sha) +} + +func testAccTFETerraformVersion_full(version string, sha string) string { + return fmt.Sprintf(` +resource "tfe_terraform_version" "foobar" { + version = "%s" + url = "https://www.hashicorp.com" + sha = "%s" + official = false + enabled = true + beta = true +}`, version, sha) +} + +// Helper functions +func genSha(t *testing.T, secret, data string) string { + h := hmac.New(sha256.New, []byte(secret)) + _, err := h.Write([]byte(data)) + if err != nil { + t.Fatalf("error writing hmac: %s", err) + } + + sha := hex.EncodeToString(h.Sum(nil)) + return sha +} + +func genVersion(rInt int) string { + return fmt.Sprintf("%d.%d.%d", rInt, rInt, rInt) +} diff --git a/website/docs/r/terraform_version.html.markdown b/website/docs/r/terraform_version.html.markdown new file mode 100644 index 000000000..cb2341a0f --- /dev/null +++ b/website/docs/r/terraform_version.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_terraform_version" +sidebar_current: "docs-resource-tfe-terraform-version-x" +description: |- + Manages Terraform versions +--- + +# tfe_terraform_version + +Manage Terraform versions available on Terraform Cloud/Enterprise. + +## Example Usage + +Basic Usage: + +```hcl +resource "tfe_terraform_version" "test" { + version = "1.1.2-custom" + url = "https://tfe-host.com/path/to/terraform.zip" + sha = "e75ac73deb69a6b3aa667cb0b8b731aee79e2904" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `version` - (Required) A semantic version string in N.N.N or N.N.N-bundleName format. +* `url` - (Required) The URL where a ZIP-compressed 64-bit Linux binary of this version can be downloaded. +* `sha` - (Required) The SHA-256 checksum of the compressed Terraform binary. +* `official` - (Optional) Whether or not this is an official release of Terraform. Defaults to "false". +* `enabled` - (Optional) Whether or not this version of Terraform is enabled for use in Terraform Cloud/Enterprise. Defaults to "true". +* `beta` - (Optional) Whether or not this version of Terraform is beta pre-release. Defaults to "false". + +## Attributes Reference + +* `id` The ID of the Terraform version + +## Import + +Terraform versions can be imported; use `` as the import ID. For example: + +```shell +terraform import tfe_terraform_version.test tool-L4oe7rNwn7J4E5Yr +``` + +-> **Note:** You can fetch a Terraform version ID from the URL of an exisiting version in the Terraform Cloud UI. The ID is in the format `tool-`