diff --git a/env0/provider.go b/env0/provider.go index aa46c2c7..96ffaf89 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -107,6 +107,7 @@ func Provider(version string) plugin.ProviderFunc { "env0_team": resourceTeam(), "env0_environment": resourceEnvironment(), "env0_workflow_triggers": resourceWorkflowTriggers(), + "env0_workflow_trigger": resourceWorkflowTrigger(), "env0_environment_scheduling": resourceEnvironmentScheduling(), "env0_environment_drift_detection": resourceDriftDetection(), "env0_notification": resourceNotification(), diff --git a/env0/resource_workflow_trigger.go b/env0/resource_workflow_trigger.go new file mode 100644 index 00000000..25d43f71 --- /dev/null +++ b/env0/resource_workflow_trigger.go @@ -0,0 +1,90 @@ +package env0 + +import ( + "context" + "log" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceWorkflowTrigger() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceWorkflowTriggerCreate, + ReadContext: resourceWorkflowTriggerRead, + DeleteContext: resourceWorkflowTriggerDelete, + Description: "cannot be used with env0_workflow_triggers", + + Schema: map[string]*schema.Schema{ + "environment_id": { + Type: schema.TypeString, + Description: "id of the source environment", + Required: true, + ForceNew: true, + }, + "downstream_environment_id": { + Type: schema.TypeString, + Description: "environment to trigger", + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceWorkflowTriggerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + environmentId := d.Get("environment_id").(string) + downstreamEnvironmentId := d.Get("downstream_environment_id").(string) + + triggers, err := apiClient.WorkflowTrigger(environmentId) + + if err != nil { + return diag.Errorf("could not get workflow triggers: %v", err) + } + + for _, trigger := range triggers { + if trigger.Id == downstreamEnvironmentId { + return nil + } + } + + log.Printf("[WARN] Drift Detected: Terraform will remove %s from state", d.Id()) + d.SetId("") + + return nil +} + +func resourceWorkflowTriggerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + environmentId := d.Get("environment_id").(string) + downstreamEnvironmentId := d.Get("downstream_environment_id").(string) + + payload := client.WorkflowTriggerEnvironments{DownstreamEnvironmentIds: []string{downstreamEnvironmentId}} + + if err := apiClient.SubscribeWorkflowTrigger(environmentId, payload); err != nil { + return diag.Errorf("failed to subscribe a workflow trigger: %v", err) + } + + d.SetId(environmentId + "_" + downstreamEnvironmentId) + + return nil +} + +func resourceWorkflowTriggerDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + environmentId := d.Get("environment_id").(string) + downstreamEnvironmentId := d.Get("downstream_environment_id").(string) + + payload := client.WorkflowTriggerEnvironments{DownstreamEnvironmentIds: []string{downstreamEnvironmentId}} + + if err := apiClient.UnsubscribeWorkflowTrigger(environmentId, payload); err != nil { + return diag.Errorf("failed to unsubscribe a workflow trigger: %v", err) + } + + return nil +} diff --git a/env0/resource_workflow_trigger_test.go b/env0/resource_workflow_trigger_test.go new file mode 100644 index 00000000..bd7f13ad --- /dev/null +++ b/env0/resource_workflow_trigger_test.go @@ -0,0 +1,197 @@ +package env0 + +import ( + "errors" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/golang/mock/gomock" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestUnitWorkflowTriggerResource(t *testing.T) { + resourceType := "env0_workflow_trigger" + resourceName := "test" + accessor := resourceAccessor(resourceType, resourceName) + environmentId := "environment_id" + triggerId := "trigger_environment_id" + otherTriggerId := "other_trigger_environment_id" + + t.Run("Success", func(t *testing.T) { + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "environment_id": environmentId, + "downstream_environment_id": triggerId, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", environmentId+"_"+triggerId), + resource.TestCheckResourceAttr(accessor, "environment_id", environmentId), + resource.TestCheckResourceAttr(accessor, "downstream_environment_id", triggerId), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "environment_id": environmentId, + "downstream_environment_id": otherTriggerId, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", environmentId+"_"+otherTriggerId), + resource.TestCheckResourceAttr(accessor, "environment_id", environmentId), + resource.TestCheckResourceAttr(accessor, "downstream_environment_id", otherTriggerId), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().SubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(nil), + mock.EXPECT().WorkflowTrigger(environmentId).Times(2).Return([]client.WorkflowTrigger{ + { + Id: triggerId, + }, + }, nil), + mock.EXPECT().UnsubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(nil), + mock.EXPECT().SubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{otherTriggerId}, + }).Times(1).Return(nil), + mock.EXPECT().WorkflowTrigger(environmentId).Times(1).Return([]client.WorkflowTrigger{ + { + Id: otherTriggerId, + }, + }, nil), + mock.EXPECT().UnsubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{otherTriggerId}, + }).Times(1).Return(nil), + ) + }) + + }) + + t.Run("Failure in Get Triggers", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "environment_id": environmentId, + "downstream_environment_id": triggerId, + }), + ExpectError: regexp.MustCompile("could not get workflow triggers: error"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().SubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(nil), + mock.EXPECT().WorkflowTrigger(environmentId).Times(1).Return(nil, errors.New("error")), + mock.EXPECT().UnsubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(nil), + ) + }) + }) + + t.Run("Failure in Unsubscribe", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "environment_id": environmentId, + "downstream_environment_id": triggerId, + }), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "environment_id": environmentId, + "downstream_environment_id": otherTriggerId, + }), + ExpectError: regexp.MustCompile("failed to unsubscribe a workflow trigger: error"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().SubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(nil), + mock.EXPECT().WorkflowTrigger(environmentId).Times(2).Return([]client.WorkflowTrigger{ + { + Id: triggerId, + }, + }, nil), + mock.EXPECT().UnsubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(errors.New("error")), + mock.EXPECT().UnsubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(nil), + ) + }) + }) + + t.Run("Failure in Subscribe", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "environment_id": environmentId, + "downstream_environment_id": triggerId, + }), + ExpectError: regexp.MustCompile("failed to subscribe a workflow trigger: error"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().SubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(errors.New("error")), + ) + }) + }) + + t.Run("Drift", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "environment_id": environmentId, + "downstream_environment_id": triggerId, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", environmentId+"_"+triggerId), + resource.TestCheckResourceAttr(accessor, "environment_id", environmentId), + resource.TestCheckResourceAttr(accessor, "downstream_environment_id", triggerId), + ), + ExpectNonEmptyPlan: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().SubscribeWorkflowTrigger(environmentId, client.WorkflowTriggerEnvironments{ + DownstreamEnvironmentIds: []string{triggerId}, + }).Times(1).Return(nil), + mock.EXPECT().WorkflowTrigger(environmentId).Times(2).Return([]client.WorkflowTrigger{ + { + Id: otherTriggerId, + }, + }, nil), + ) + }) + }) +} diff --git a/env0/resource_workflow_triggers.go b/env0/resource_workflow_triggers.go index e81534b4..4773d727 100644 --- a/env0/resource_workflow_triggers.go +++ b/env0/resource_workflow_triggers.go @@ -14,6 +14,7 @@ func resourceWorkflowTriggers() *schema.Resource { ReadContext: resourceWorkflowTriggersRead, UpdateContext: resourceWorkflowTriggersCreateOrUpdate, DeleteContext: resourceWorkflowTriggersDelete, + Description: "cannot be used with env0_workflow_trigger", Schema: map[string]*schema.Schema{ "environment_id": { diff --git a/env0/resource_workflow_triggers_test.go b/env0/resource_workflow_triggers_test.go index fd043634..ab8e9c6c 100644 --- a/env0/resource_workflow_triggers_test.go +++ b/env0/resource_workflow_triggers_test.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestUnitWorkflowTriggerResource(t *testing.T) { +func TestUnitWorkflowTriggersResource(t *testing.T) { resourceType := "env0_workflow_triggers" resourceName := "test" accessor := resourceAccessor(resourceType, resourceName) diff --git a/examples/resources/env0_workflow_trigger/resource.tf b/examples/resources/env0_workflow_trigger/resource.tf new file mode 100644 index 00000000..5c5c1ce5 --- /dev/null +++ b/examples/resources/env0_workflow_trigger/resource.tf @@ -0,0 +1,32 @@ +data "env0_project" "default" { + name = "Default Organization Project" +} + +resource "env0_template" "template" { + name = "Template for environment resource" + type = "terraform" + repository = "https://github.com/env0/templates" + path = "misc/null-resource" + terraform_version = "0.15.1" +} + +resource "env0_environment" "the_trigger" { + force_destroy = true + name = "the_trigger" + project_id = data.env0_project.default.id + template_id = env0_template.template.id + approve_plan_automatically = true +} + +resource "env0_environment" "downstream_environment" { + force_destroy = true + name = "downstream_environment" + project_id = data.env0_project.default.id + template_id = env0_template.template.id + approve_plan_automatically = true +} + +resource "env0_workflow_trigger" "trigger_link" { + environment_id = env0_environment.the_trigger.id + downstream_environment_id = env0_environment.downstream_environment.id +} diff --git a/tests/integration/013_downstream_environments/main.tf b/tests/integration/013_downstream_environments/main.tf index a57cf63e..125e8f68 100644 --- a/tests/integration/013_downstream_environments/main.tf +++ b/tests/integration/013_downstream_environments/main.tf @@ -47,3 +47,26 @@ resource "env0_workflow_triggers" "trigger_link" { env0_environment.downstream_environment.id ] } + +resource "env0_environment" "the_trigger_2" { + depends_on = [env0_template_project_assignment.assignment] + force_destroy = true + name = "the_trigger-${random_string.random.result}-2" + project_id = env0_project.test_project.id + template_id = env0_template.template.id + approve_plan_automatically = true +} + +resource "env0_environment" "downstream_environment_2" { + depends_on = [env0_template_project_assignment.assignment] + force_destroy = true + name = "downstream_environment-${random_string.random.result}-2" + project_id = env0_project.test_project.id + template_id = env0_template.template.id + approve_plan_automatically = true +} + +resource "env0_workflow_trigger" "trigger_link_2" { + environment_id = env0_environment.the_trigger_2.id + downstream_environment_id = env0_environment.downstream_environment_2.id +}