From dd48b5b7e74a4837465ffbe4ecd52f0ad1c9e8d4 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Fri, 28 Apr 2017 15:12:54 -0400 Subject: [PATCH] provider/heroku: heroku_pipeline_coupling Also adds validators for UUID and pipeline stage names --- builtin/providers/heroku/provider.go | 17 +-- .../resource_heroku_pipeline_coupling.go | 88 +++++++++++++ .../resource_heroku_pipeline_coupling_test.go | 124 ++++++++++++++++++ builtin/providers/heroku/validators.go | 38 ++++++ builtin/providers/heroku/validators_test.go | 53 ++++++++ 5 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 builtin/providers/heroku/resource_heroku_pipeline_coupling.go create mode 100644 builtin/providers/heroku/resource_heroku_pipeline_coupling_test.go create mode 100644 builtin/providers/heroku/validators.go create mode 100644 builtin/providers/heroku/validators_test.go diff --git a/builtin/providers/heroku/provider.go b/builtin/providers/heroku/provider.go index 8afbc80eb..fec57ca59 100644 --- a/builtin/providers/heroku/provider.go +++ b/builtin/providers/heroku/provider.go @@ -27,14 +27,15 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "heroku_addon": resourceHerokuAddon(), - "heroku_app": resourceHerokuApp(), - "heroku_app_feature": resourceHerokuAppFeature(), - "heroku_cert": resourceHerokuCert(), - "heroku_domain": resourceHerokuDomain(), - "heroku_drain": resourceHerokuDrain(), - "heroku_pipeline": resourceHerokuPipeline(), - "heroku_space": resourceHerokuSpace(), + "heroku_addon": resourceHerokuAddon(), + "heroku_app": resourceHerokuApp(), + "heroku_app_feature": resourceHerokuAppFeature(), + "heroku_cert": resourceHerokuCert(), + "heroku_domain": resourceHerokuDomain(), + "heroku_drain": resourceHerokuDrain(), + "heroku_pipeline": resourceHerokuPipeline(), + "heroku_pipeline_coupling": resourceHerokuPipelineCoupling(), + "heroku_space": resourceHerokuSpace(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/heroku/resource_heroku_pipeline_coupling.go b/builtin/providers/heroku/resource_heroku_pipeline_coupling.go new file mode 100644 index 000000000..7fb4548a4 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_pipeline_coupling.go @@ -0,0 +1,88 @@ +package heroku + +import ( + "context" + "fmt" + "log" + + "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceHerokuPipelineCoupling() *schema.Resource { + return &schema.Resource{ + Create: resourceHerokuPipelineCouplingCreate, + Read: resourceHerokuPipelineCouplingRead, + Delete: resourceHerokuPipelineCouplingDelete, + + Schema: map[string]*schema.Schema{ + "app": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "pipeline": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateUUID, + }, + "stage": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validatePipelineStageName, + }, + }, + } +} + +func resourceHerokuPipelineCouplingCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + opts := heroku.PipelineCouplingCreateOpts{} + opts.App = d.Get("app").(string) + opts.Pipeline = d.Get("pipeline").(string) + opts.Stage = d.Get("stage").(string) + + log.Printf("[DEBUG] PipelineCoupling create configuration: %#v", opts) + + p, err := client.PipelineCouplingCreate(context.TODO(), opts) + if err != nil { + return fmt.Errorf("Error creating pipeline: %s", err) + } + + d.SetId(p.ID) + + log.Printf("[INFO] PipelineCoupling ID: %s", d.Id()) + + return resourceHerokuPipelineCouplingRead(d, meta) +} + +func resourceHerokuPipelineCouplingDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + log.Printf("[INFO] Deleting pipeline: %s", d.Id()) + + _, err := client.PipelineCouplingDelete(context.TODO(), d.Id()) + if err != nil { + return fmt.Errorf("Error deleting pipeline: %s", err) + } + + return nil +} + +func resourceHerokuPipelineCouplingRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + p, err := client.PipelineCouplingInfo(context.TODO(), d.Id()) + if err != nil { + return fmt.Errorf("Error retrieving pipeline: %s", err) + } + + d.Set("app", p.App) + d.Set("pipeline", p.Pipeline) + d.Set("stage", p.Stage) + + return nil +} diff --git a/builtin/providers/heroku/resource_heroku_pipeline_coupling_test.go b/builtin/providers/heroku/resource_heroku_pipeline_coupling_test.go new file mode 100644 index 000000000..9e600d1e3 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_pipeline_coupling_test.go @@ -0,0 +1,124 @@ +package heroku + +import ( + "context" + "fmt" + "testing" + + heroku "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuPipelineCoupling_Basic(t *testing.T) { + var coupling heroku.PipelineCouplingInfoResult + + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + pipelineName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + stageName := "development" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuPipelineCouplingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuPipelineCouplingConfig_basic(appName, pipelineName, stageName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuPipelineCouplingExists("heroku_pipeline_coupling.default", &coupling), + testAccCheckHerokuPipelineCouplingAttributes( + &coupling, + "heroku_app.default", + "heroku_pipeline.default", + stageName, + ), + ), + }, + }, + }) +} + +func testAccCheckHerokuPipelineCouplingConfig_basic(appName, pipelineName, stageName string) string { + return fmt.Sprintf(` +resource "heroku_app" "default" { + name = "%s" + region = "us" +} + +resource "heroku_pipeline" "default" { + name = "%s" +} + +resource "heroku_pipeline_coupling" "default" { + app = "${heroku_app.default.id}" + pipeline = "${heroku_pipeline.default.id}" + stage = "%s" +} +`, appName, pipelineName, stageName) +} + +func testAccCheckHerokuPipelineCouplingExists(n string, pipeline *heroku.PipelineCouplingInfoResult) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No coupling ID set") + } + + client := testAccProvider.Meta().(*heroku.Service) + + foundPipelineCoupling, err := client.PipelineCouplingInfo(context.TODO(), rs.Primary.ID) + if err != nil { + return err + } + + if foundPipelineCoupling.ID != rs.Primary.ID { + return fmt.Errorf("PipelineCoupling not found") + } + + *pipeline = *foundPipelineCoupling + + return nil + } +} + +func testAccCheckHerokuPipelineCouplingAttributes(coupling *heroku.PipelineCouplingInfoResult, _, pipelineResource, stageName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + pipeline, ok := s.RootModule().Resources[pipelineResource] + if !ok { + return fmt.Errorf("Pipeline not found: %s", pipelineResource) + } + + if coupling.Pipeline.ID != pipeline.Primary.ID { + return fmt.Errorf("Bad pipeline ID: %v != %v", coupling.Pipeline.ID, pipeline.Primary.ID) + } + if coupling.Stage != stageName { + return fmt.Errorf("Bad stage: %s", coupling.Stage) + } + + return nil + } +} + +func testAccCheckHerokuPipelineCouplingDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "heroku_pipeline_coupling" { + continue + } + + _, err := client.PipelineCouplingInfo(context.TODO(), rs.Primary.ID) + + if err == nil { + return fmt.Errorf("PipelineCoupling still exists") + } + } + + return nil +} diff --git a/builtin/providers/heroku/validators.go b/builtin/providers/heroku/validators.go new file mode 100644 index 000000000..35ac26477 --- /dev/null +++ b/builtin/providers/heroku/validators.go @@ -0,0 +1,38 @@ +package heroku + +import ( + "fmt" + "strings" + + "github.com/satori/uuid" +) + +var validPipelineStageNames = []string{ + "review", + "development", + "staging", + "production", +} + +func validatePipelineStageName(v interface{}, k string) (ws []string, errors []error) { + for _, s := range validPipelineStageNames { + if v == s { + return + } + } + + err := fmt.Errorf( + "%s is an invalid pipeline stage, must be one of [%s]", + v, + strings.Join(validPipelineStageNames, ", "), + ) + errors = append(errors, err) + return +} + +func validateUUID(v interface{}, k string) (ws []string, errors []error) { + if _, err := uuid.FromString(v.(string)); err != nil { + errors = append(errors, fmt.Errorf("%q is an invalid UUID: %s", k, err)) + } + return +} diff --git a/builtin/providers/heroku/validators_test.go b/builtin/providers/heroku/validators_test.go new file mode 100644 index 000000000..6131be8bc --- /dev/null +++ b/builtin/providers/heroku/validators_test.go @@ -0,0 +1,53 @@ +package heroku + +import "testing" + +func TestPipelineStage(t *testing.T) { + valid := []string{ + "review", + "development", + "staging", + "production", + } + for _, v := range valid { + _, errors := validatePipelineStageName(v, "stage") + if len(errors) != 0 { + t.Fatalf("%q should be a valid stage: %q", v, errors) + } + } + + invalid := []string{ + "foobarbaz", + "another-stage", + "", + } + for _, v := range invalid { + _, errors := validatePipelineStageName(v, "stage") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid stage", v) + } + } +} + +func TestValidateUUID(t *testing.T) { + valid := []string{ + "4812ccbc-2a2e-4c6c-bae4-a3d04ed51c0e", + } + for _, v := range valid { + _, errors := validateUUID(v, "id") + if len(errors) != 0 { + t.Fatalf("%q should be a valid UUID: %q", v, errors) + } + } + + invalid := []string{ + "foobarbaz", + "my-app-name", + } + for _, v := range invalid { + _, errors := validateUUID(v, "id") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid UUID", v) + } + } +}