diff --git a/client/api_client.go b/client/api_client.go index 930d545e..5cb43f96 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -136,6 +136,10 @@ type ApiClientInterface interface { AssignTeamRoleToEnvironment(payload *AssignTeamRoleToEnvironmentPayload) (*TeamRoleEnvironmentAssignment, error) RemoveTeamRoleFromEnvironment(environmentId string, teamId string) error TeamRoleEnvironmentAssignments(environmentId string) ([]TeamRoleEnvironmentAssignment, error) + ApprovalPolicies(name string) ([]ApprovalPolicy, error) + ApprovalPolicyAssign(assignment *ApprovalPolicyAssignment) (*ApprovalPolicyAssignment, error) + ApprovalPolicyUnassign(scope string, scopeId string) error + ApprovalPolicyByScope(scope string, scopeId string) ([]ApprovalPolicyByScope, error) } func NewApiClient(client http.HttpClientInterface, defaultOrganizationId string) ApiClientInterface { diff --git a/client/api_client_mock.go b/client/api_client_mock.go index d8d98cae..3ea41129 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -107,6 +107,65 @@ func (mr *MockApiClientInterfaceMockRecorder) ApiKeys() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApiKeys", reflect.TypeOf((*MockApiClientInterface)(nil).ApiKeys)) } +// ApprovalPolicies mocks base method. +func (m *MockApiClientInterface) ApprovalPolicies(arg0 string) ([]ApprovalPolicy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApprovalPolicies", arg0) + ret0, _ := ret[0].([]ApprovalPolicy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ApprovalPolicies indicates an expected call of ApprovalPolicies. +func (mr *MockApiClientInterfaceMockRecorder) ApprovalPolicies(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApprovalPolicies", reflect.TypeOf((*MockApiClientInterface)(nil).ApprovalPolicies), arg0) +} + +// ApprovalPolicyAssign mocks base method. +func (m *MockApiClientInterface) ApprovalPolicyAssign(arg0 *ApprovalPolicyAssignment) (*ApprovalPolicyAssignment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApprovalPolicyAssign", arg0) + ret0, _ := ret[0].(*ApprovalPolicyAssignment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ApprovalPolicyAssign indicates an expected call of ApprovalPolicyAssign. +func (mr *MockApiClientInterfaceMockRecorder) ApprovalPolicyAssign(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApprovalPolicyAssign", reflect.TypeOf((*MockApiClientInterface)(nil).ApprovalPolicyAssign), arg0) +} + +// ApprovalPolicyByScope mocks base method. +func (m *MockApiClientInterface) ApprovalPolicyByScope(arg0, arg1 string) ([]ApprovalPolicyByScope, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApprovalPolicyByScope", arg0, arg1) + ret0, _ := ret[0].([]ApprovalPolicyByScope) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ApprovalPolicyByScope indicates an expected call of ApprovalPolicyByScope. +func (mr *MockApiClientInterfaceMockRecorder) ApprovalPolicyByScope(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApprovalPolicyByScope", reflect.TypeOf((*MockApiClientInterface)(nil).ApprovalPolicyByScope), arg0, arg1) +} + +// ApprovalPolicyUnassign mocks base method. +func (m *MockApiClientInterface) ApprovalPolicyUnassign(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApprovalPolicyUnassign", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ApprovalPolicyUnassign indicates an expected call of ApprovalPolicyUnassign. +func (mr *MockApiClientInterfaceMockRecorder) ApprovalPolicyUnassign(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApprovalPolicyUnassign", reflect.TypeOf((*MockApiClientInterface)(nil).ApprovalPolicyUnassign), arg0, arg1) +} + // AssignAgentsToProjects mocks base method. func (m *MockApiClientInterface) AssignAgentsToProjects(arg0 AssignProjectsAgentsAssignmentsPayload) (*ProjectsAgentsAssignments, error) { m.ctrl.T.Helper() diff --git a/client/approval_policy.go b/client/approval_policy.go new file mode 100644 index 00000000..2c42c5d9 --- /dev/null +++ b/client/approval_policy.go @@ -0,0 +1,77 @@ +package client + +import "fmt" + +type ApprovalPolicy struct { + Id string `json:"id"` + Name string `json:"name"` + Repository string `json:"repository"` + Path string `json:"path" tfschema:",omitempty"` + Revision string `json:"revision" tfschema:",omitempty"` + TokenId string `json:"tokenId" tfschema:",omitempty"` + SshKeys []TemplateSshKey `json:"sshKeys"` + GithubInstallationId int `json:"githubInstallationId" tfschema:",omitempty"` + BitbucketClientKey string `json:"bitbucketClientKey" tfschema:",omitempty"` + IsBitbucketServer bool `json:"isBitbucketServer"` + IsGitlabEnterprise bool `json:"isGitLabEnterprise"` + IsGithubEnterprise bool `json:"isGitHubEnterprise"` + IsGitLab bool `json:"isGitLab" tfschema:"is_gitlab"` + IsAzureDevOps bool `json:"isAzureDevOps" tfschema:"is_azure_devops"` + IsTerragruntRunAll bool `json:"isTerragruntRunAll"` +} + +type ApprovalPolicyByScope struct { + Scope string `json:"scope"` + ScopeId string `json:"scopeId"` + ApprovalPolicy *ApprovalPolicy `json:"blueprint"` +} + +type ApprovalPolicyAssignmentScope string + +const ( + ApprovalPolicyProjectScope ApprovalPolicyAssignmentScope = "PROJECT" +) + +type ApprovalPolicyAssignment struct { + Scope ApprovalPolicyAssignmentScope `json:"scope"` + ScopeId string `json:"scopeId"` + BlueprintId string `json:"blueprintId"` +} + +func (client *ApiClient) ApprovalPolicies(name string) ([]ApprovalPolicy, error) { + organizationId, err := client.OrganizationId() + if err != nil { + return nil, err + } + + var result []ApprovalPolicy + if err := client.http.Get("/approval-policy", map[string]string{"organizationId": organizationId, "name": name}, &result); err != nil { + return nil, err + } + + return result, err +} + +func (client *ApiClient) ApprovalPolicyAssign(assignment *ApprovalPolicyAssignment) (*ApprovalPolicyAssignment, error) { + var result ApprovalPolicyAssignment + + if err := client.http.Post("/approval-policy/assignment", assignment, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func (client *ApiClient) ApprovalPolicyUnassign(scope string, scopeId string) error { + return client.http.Delete(fmt.Sprintf("/approval-policy/assignment/%s/%s", scope, scopeId), nil) +} + +func (client *ApiClient) ApprovalPolicyByScope(scope string, scopeId string) ([]ApprovalPolicyByScope, error) { + var result []ApprovalPolicyByScope + + if err := client.http.Get(fmt.Sprintf("/approval-policy/%s/%s", scope, scopeId), nil, &result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/client/approval_policy_test.go b/client/approval_policy_test.go new file mode 100644 index 00000000..c4fdeac4 --- /dev/null +++ b/client/approval_policy_test.go @@ -0,0 +1,120 @@ +package client_test + +import ( + "fmt" + + . "github.com/env0/terraform-provider-env0/client" + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Approval Policy Client", func() { + mockApprovalPolicy := ApprovalPolicy{ + Id: "id", + Name: "name", + Repository: "repository", + Path: "path", + Revision: "revision", + TokenId: "tokenId", + SshKeys: []TemplateSshKey{ + {Id: "id", Name: "name"}, + }, + GithubInstallationId: 1, + BitbucketClientKey: "bitbucket-key", + IsBitbucketServer: true, + IsGitlabEnterprise: false, + IsGithubEnterprise: true, + IsGitLab: false, + IsAzureDevOps: true, + IsTerragruntRunAll: false, + } + + Describe("Get Custom Flows By Name", func() { + var returnedApprovalPolicies []ApprovalPolicy + mockApprovalPolicies := []ApprovalPolicy{mockApprovalPolicy} + + BeforeEach(func() { + mockOrganizationIdCall(organizationId) + httpCall = mockHttpClient.EXPECT(). + Get("/approval-policy", map[string]string{"organizationId": organizationId, "name": mockApprovalPolicy.Name}, gomock.Any()). + Do(func(path string, request interface{}, response *[]ApprovalPolicy) { + *response = mockApprovalPolicies + }) + organizationIdCall.Times(1) + httpCall.Times(1) + returnedApprovalPolicies, _ = apiClient.ApprovalPolicies(mockApprovalPolicy.Name) + }) + + It("Should return approval policies", func() { + Expect(returnedApprovalPolicies).To(Equal(mockApprovalPolicies)) + }) + }) + + mockAssignment := ApprovalPolicyAssignment{ + Scope: ApprovalPolicyProjectScope, + ScopeId: "scope_id", + BlueprintId: "blueprint_id", + } + + Describe("Assign Approval Policy", func() { + var returnedApprovalPolicyAssignment *ApprovalPolicyAssignment + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Post("/approval-policy/assignment", &mockAssignment, gomock.Any()). + Do(func(path string, request interface{}, response *ApprovalPolicyAssignment) { + *response = mockAssignment + }) + httpCall.Times(1) + returnedApprovalPolicyAssignment, _ = apiClient.ApprovalPolicyAssign(&mockAssignment) + }) + + It("Should return approval policy assignment", func() { + Expect(*returnedApprovalPolicyAssignment).To(Equal(mockAssignment)) + }) + }) + + Describe("Unassign Custom Flow", func() { + var err error + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT().Delete(fmt.Sprintf("/approval-policy/assignment/%s/%s", ApprovalPolicyProjectScope, "scope_id"), nil) + httpCall.Times(1) + err = apiClient.ApprovalPolicyUnassign(string(ApprovalPolicyProjectScope), "scope_id") + }) + + It("Should not return an error", func() { + Expect(err).To(BeNil()) + }) + }) + + Describe("Get Approval Policy By Scope", func() { + var ret []ApprovalPolicyByScope + + scope := string(mockAssignment.Scope) + scopeId := mockAssignment.ScopeId + + mockApprovalPolicyByScope := ApprovalPolicyByScope{ + Scope: scope, + ScopeId: scopeId, + ApprovalPolicy: &mockApprovalPolicy, + } + + mockApprovalPolicyByScopeArr := []ApprovalPolicyByScope{mockApprovalPolicyByScope} + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Get(fmt.Sprintf("/approval-policy/%s/%s", scope, scopeId), nil, gomock.Any()). + Do(func(path string, request interface{}, response *[]ApprovalPolicyByScope) { + *response = mockApprovalPolicyByScopeArr + }) + httpCall.Times(1) + ret, _ = apiClient.ApprovalPolicyByScope(scope, scopeId) + }) + + It("Should return approval policy assignment", func() { + Expect(ret).To(Equal(mockApprovalPolicyByScopeArr)) + }) + }) +}) diff --git a/client/template.go b/client/template.go index 64914e46..9ca50b9d 100644 --- a/client/template.go +++ b/client/template.go @@ -60,6 +60,7 @@ type Template struct { IsAzureDevOps bool `json:"isAzureDevOps" tfschema:"is_azure_devops"` IsHelmRepository bool `json:"isHelmRepository"` HelmChartName string `json:"helmChartName,omitempty" tfschema:",omitempty"` + IsGitLab bool `json:"isGitLab" tfschema:"is_gitlab"` } type TemplateCreatePayload struct { diff --git a/env0/configuration_template.go b/env0/configuration_template.go new file mode 100644 index 00000000..e17b1b0d --- /dev/null +++ b/env0/configuration_template.go @@ -0,0 +1,115 @@ +package env0 + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type TemplateType string + +const ( + CustomFlow TemplateType = "custom-flow" + ApprovalPolicy TemplateType = "approval-policy" +) + +func getConfigurationTemplateSchema(templateType TemplateType) map[string]*schema.Schema { + var text string + + switch templateType { + case CustomFlow: + text = "custom flow" + case ApprovalPolicy: + text = "approval policy" + } + + s := map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: fmt.Sprintf("id of the %s", text), + Computed: true, + }, + "repository": { + Type: schema.TypeString, + Description: fmt.Sprintf("repository url for the %s source code", text), + Required: true, + }, + "path": { + Type: schema.TypeString, + Description: "terraform / terragrunt file folder inside source code. Should be the full path including the .yaml/.yml file", + Optional: true, + }, + "revision": { + Type: schema.TypeString, + Description: "source code revision (branch / tag) to use", + Optional: true, + }, + "token_id": { + Type: schema.TypeString, + Description: "the git token id to be used", + Optional: true, + }, + "ssh_keys": { + Type: schema.TypeList, + Description: "an array of references to 'data_ssh_key' to use when accessing git over ssh", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeMap, + Description: "a map of env0_ssh_key.id and env0_ssh_key.name for each project", + }, + }, + "gitlab_project_id": { + Type: schema.TypeInt, + Description: "the project id of the relevant repository", + Optional: true, + RequiredWith: []string{"token_id"}, + }, + "github_installation_id": { + Type: schema.TypeInt, + Description: "the env0 application installation id on the relevant github repository", + Optional: true, + }, + "bitbucket_client_key": { + Type: schema.TypeString, + Description: "the bitbucket client key used for integration", + Optional: true, + }, + "is_bitbucket_server": { + Type: schema.TypeBool, + Description: fmt.Sprintf("true if this %s uses bitbucket server repository", text), + Optional: true, + Default: false, + }, + "is_gitlab_enterprise": { + Type: schema.TypeBool, + Description: fmt.Sprintf("true if this %s uses gitlab enterprise repository", text), + Optional: true, + Default: false, + }, + "is_github_enterprise": { + Type: schema.TypeBool, + Description: fmt.Sprintf("true if this %s uses github enterprise repository", text), + Optional: true, + Default: false, + }, + "is_gitlab": { + Type: schema.TypeBool, + Optional: true, + Description: fmt.Sprintf("true if this %s integrates with gitlab repository", text), + Default: false, + }, + "is_azure_devops": { + Type: schema.TypeBool, + Optional: true, + Description: fmt.Sprintf("true if this %s integrates with azure dev ops repository", text), + Default: false, + }, + "name": { + Type: schema.TypeString, + Description: fmt.Sprintf("name for the %s", text), + Required: true, + }, + } + + return s +} diff --git a/env0/provider.go b/env0/provider.go index 786c67f3..2afd8c0e 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -132,10 +132,13 @@ func Provider(version string) plugin.ProviderFunc { "env0_provider": resourceProvider(), "env0_user_environment_assignment": resourceUserEnvironmentAssignment(), "env0_team_environment_assignment": resourceTeamEnvironmentAssignment(), + "env0_approval_policy": resourceApprovalPolicy(), + "env0_approval_policy_assignment": resourceApprovalPolicyAssignment(), }, } provider.ConfigureContextFunc = configureProvider(version, provider) + return provider } } diff --git a/env0/resource_approval_policy.go b/env0/resource_approval_policy.go new file mode 100644 index 00000000..28d1a6eb --- /dev/null +++ b/env0/resource_approval_policy.go @@ -0,0 +1,147 @@ +package env0 + +import ( + "context" + "fmt" + "log" + + "github.com/env0/terraform-provider-env0/client" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceApprovalPolicy() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceApprovalPolicyCreate, + ReadContext: resourceApprovalPolicyRead, + UpdateContext: resourceApprovalPolicyUpdate, + DeleteContext: resourceApprovalPolicyDelete, + + Importer: &schema.ResourceImporter{StateContext: resourceApprovalPolicyImport}, + + Schema: getConfigurationTemplateSchema(ApprovalPolicy), + } +} + +func resourceApprovalPolicyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + var payload client.TemplateCreatePayload + if err := readResourceData(&payload, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + payload.Type = string(ApprovalPolicy) + + template, err := apiClient.TemplateCreate(payload) + if err != nil { + return diag.Errorf("could not create approval policy template: %v", err) + } + + d.SetId(template.Id) + + return nil +} + +func resourceApprovalPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + approvalPolicy, err := apiClient.Template(d.Id()) + if err != nil { + return ResourceGetFailure("approval policy", d, err) + } + + if approvalPolicy.IsDeleted && !d.IsNewResource() { + log.Printf("[WARN] Drift Detected: Terraform will remove %s from state", d.Id()) + d.SetId("") + return nil + } + + if err := writeResourceData(&approvalPolicy, d); err != nil { + return diag.Errorf("schema resource data serialization failed: %v", err) + } + + return nil +} + +func resourceApprovalPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + request, problem := templateCreatePayloadFromParameters("", d) + if problem != nil { + return problem + } + + request.Type = string(ApprovalPolicy) + + _, err := apiClient.TemplateUpdate(d.Id(), request) + if err != nil { + return diag.Errorf("could not update approval policy template: %v", err) + } + + return nil +} + +func resourceApprovalPolicyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + if err := apiClient.TemplateDelete(d.Id()); err != nil { + return diag.Errorf("could not delete approval policy template: %v", err) + } + + return nil +} + +func getApprovalPolicyByName(name string, meta interface{}) (*client.ApprovalPolicy, error) { + apiClient := meta.(client.ApiClientInterface) + + approvalPolicies, err := apiClient.ApprovalPolicies(name) + if err != nil { + return nil, err + } + + if len(approvalPolicies) == 0 { + return nil, fmt.Errorf("approval policy with name %v not found", name) + } + + if len(approvalPolicies) > 1 { + return nil, fmt.Errorf("found multiple approval policies with name: %s. Use id instead or make sure approval policy names are unique %v", name, approvalPolicies) + } + + return &approvalPolicies[0], nil +} + +func getApprovalPolicy(id string, meta interface{}) (interface{}, error) { + if _, err := uuid.Parse(id); err == nil { + log.Println("[INFO] Resolving approval policy by id: ", id) + + template, err := meta.(client.ApiClientInterface).Template(id) + if err != nil { + return nil, err + } + + if template.Type != string(ApprovalPolicy) { + return nil, fmt.Errorf("template type requires type %s but received type %s", ApprovalPolicy, template.Type) + } + + return &template, nil + } else { + log.Println("[INFO] Resolving approval policy by name: ", id) + + return getApprovalPolicyByName(id, meta) + } +} + +func resourceApprovalPolicyImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + approvalPolicy, err := getApprovalPolicy(d.Id(), meta) + if err != nil { + return nil, err + } + + if err := writeResourceData(approvalPolicy, d); err != nil { + return nil, fmt.Errorf("schema resource data serialization failed: %v", err) + } + + return []*schema.ResourceData{d}, nil +} diff --git a/env0/resource_approval_policy_assignment.go b/env0/resource_approval_policy_assignment.go new file mode 100644 index 00000000..50479215 --- /dev/null +++ b/env0/resource_approval_policy_assignment.go @@ -0,0 +1,126 @@ +package env0 + +import ( + "context" + "fmt" + "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 setApprovalPolicyAssignmentId(d *schema.ResourceData, assignment *client.ApprovalPolicyAssignment) { + d.SetId(fmt.Sprintf("%s|%s|%s", assignment.BlueprintId, assignment.Scope, assignment.ScopeId)) +} + +func resourceApprovalPolicyAssignment() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceApprovalPolicyAssignmentCreate, + ReadContext: resourceApprovalPolicyAssignmentRead, + DeleteContext: resourceApprovalPolicyAssignmentDelete, + + Schema: map[string]*schema.Schema{ + "scope": { + Type: schema.TypeString, + Description: "the type of the scope. Valid values: PROJECT. Default value: PROJECT", + Optional: true, + Default: client.ApprovalPolicyProjectScope, + ForceNew: true, + }, + "scope_id": { + Type: schema.TypeString, + Description: "the id of the scope (E.g. project id)", + Required: true, + ForceNew: true, + }, + "blueprint_id": { + Type: schema.TypeString, + Description: "the id of the approval policy", + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceApprovalPolicyAssignmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + scope := d.Get("scope").(string) + scopeId := d.Get("scope_id").(string) + blueprintId := d.Get("blueprint_id").(string) + + template, err := apiClient.Template(blueprintId) + if err != nil { + return diag.Errorf("unable to get template with id %s: %v", blueprintId, err) + } + + if template.Type != string(ApprovalPolicy) { + return diag.Errorf("template with id %s is of type %s, but %s type is required", blueprintId, template.Type, ApprovalPolicy) + } + + assignment := client.ApprovalPolicyAssignment{ + Scope: client.ApprovalPolicyAssignmentScope(scope), + ScopeId: scopeId, + BlueprintId: blueprintId, + } + + if _, err := apiClient.ApprovalPolicyAssign(&assignment); err != nil { + return diag.Errorf("could not assign approval policy %s to scope %s %s: %v", blueprintId, scope, scopeId, err) + } + + setApprovalPolicyAssignmentId(d, &assignment) + + return nil +} + +func resourceApprovalPolicyAssignmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + scope := d.Get("scope").(string) + scopeId := d.Get("scope_id").(string) + blueprintId := d.Get("blueprint_id").(string) + + approvalPolicyByScopeArr, err := apiClient.ApprovalPolicyByScope(scope, scopeId) + if err != nil { + return ResourceGetFailure("approval policy assignment", d, err) + } + + found := false + for _, approvalPolicyByScope := range approvalPolicyByScopeArr { + if approvalPolicyByScope.ApprovalPolicy.Id == blueprintId { + found = true + break + } + } + + if !found { + log.Printf("[WARN] Drift Detected: Terraform will remove %s from state", d.Id()) + d.SetId("") + return nil + } + + assignment := client.ApprovalPolicyAssignment{ + Scope: client.ApprovalPolicyAssignmentScope(scope), + ScopeId: scopeId, + BlueprintId: blueprintId, + } + + setApprovalPolicyAssignmentId(d, &assignment) + + return nil +} + +func resourceApprovalPolicyAssignmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + scope := d.Get("scope").(string) + scopeId := d.Get("scope_id").(string) + + if err := apiClient.ApprovalPolicyUnassign(scope, scopeId); err != nil { + return diag.Errorf("failed to unassign approval policy from scope %s %s: %v", scope, scopeId, err) + } + + return nil +} diff --git a/env0/resource_approval_policy_assignment_test.go b/env0/resource_approval_policy_assignment_test.go new file mode 100644 index 00000000..24f635ef --- /dev/null +++ b/env0/resource_approval_policy_assignment_test.go @@ -0,0 +1,185 @@ +package env0 + +import ( + "errors" + "fmt" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/golang/mock/gomock" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestUnitResourceApprovalPolicyAssignmentResource(t *testing.T) { + resourceType := "env0_approval_policy_assignment" + resourceName := "test" + accessor := resourceAccessor(resourceType, resourceName) + + assignment := client.ApprovalPolicyAssignment{ + Scope: "PROJECT", + ScopeId: "scope_id", + BlueprintId: "blueprint_id", + } + + stepConfig := resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "scope_id": assignment.ScopeId, + "blueprint_id": assignment.BlueprintId, + }) + + validTemplate := client.Template{ + Id: assignment.BlueprintId, + Type: "approval-policy", + Name: "approval-policy-" + string(assignment.Scope) + "-" + assignment.ScopeId, + } + + approvalPolicyByScope := client.ApprovalPolicyByScope{ + Scope: string(assignment.Scope), + ScopeId: assignment.ScopeId, + ApprovalPolicy: &client.ApprovalPolicy{ + Id: validTemplate.Id, + Name: validTemplate.Name, + }, + } + + t.Run("Create assignment", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", fmt.Sprintf("%s|%s|%s", assignment.BlueprintId, assignment.Scope, assignment.ScopeId)), + resource.TestCheckResourceAttr(accessor, "scope_id", assignment.ScopeId), + resource.TestCheckResourceAttr(accessor, "scope", "PROJECT"), + resource.TestCheckResourceAttr(accessor, "blueprint_id", assignment.BlueprintId), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(assignment.BlueprintId).Times(1).Return(validTemplate, nil), + mock.EXPECT().ApprovalPolicyAssign(&assignment).Times(1).Return(&assignment, nil), + mock.EXPECT().ApprovalPolicyByScope(string(assignment.Scope), assignment.ScopeId).Times(1).Return([]client.ApprovalPolicyByScope{approvalPolicyByScope}, nil), + mock.EXPECT().ApprovalPolicyUnassign(string(assignment.Scope), assignment.ScopeId).Times(1).Return(nil), + ) + }) + }) + + t.Run("Create assignment - type mismatch", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + ExpectError: regexp.MustCompile("template with id"), + }, + }, + } + + invalidTemplate := validTemplate + invalidTemplate.Type = "terraform" + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(assignment.BlueprintId).Times(1).Return(invalidTemplate, nil), + ) + }) + }) + + t.Run("Create assignment - error when assigning", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + ExpectError: regexp.MustCompile("could not assign approval policy"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(assignment.BlueprintId).Times(1).Return(validTemplate, nil), + mock.EXPECT().ApprovalPolicyAssign(&assignment).Times(1).Return(nil, errors.New("error")), + ) + }) + }) + + t.Run("Create assignment - error when requesting template", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + ExpectError: regexp.MustCompile("unable to get template with"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(assignment.BlueprintId).Times(1).Return(client.Template{}, errors.New("error")), + ) + }) + }) + + t.Run("Detect drift - not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", fmt.Sprintf("%s|%s|%s", assignment.BlueprintId, assignment.Scope, assignment.ScopeId)), + resource.TestCheckResourceAttr(accessor, "scope_id", assignment.ScopeId), + resource.TestCheckResourceAttr(accessor, "scope", "PROJECT"), + resource.TestCheckResourceAttr(accessor, "blueprint_id", assignment.BlueprintId), + ), + ExpectNonEmptyPlan: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(assignment.BlueprintId).Times(1).Return(validTemplate, nil), + mock.EXPECT().ApprovalPolicyAssign(&assignment).Times(1).Return(&assignment, nil), + mock.EXPECT().ApprovalPolicyByScope(string(assignment.Scope), assignment.ScopeId).Times(1).Return(nil, &client.NotFoundError{}), + mock.EXPECT().ApprovalPolicyUnassign(string(assignment.Scope), assignment.ScopeId).Times(1).Return(nil), + ) + }) + }) + + t.Run("Detect drift - mismatch", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", fmt.Sprintf("%s|%s|%s", assignment.BlueprintId, assignment.Scope, assignment.ScopeId)), + resource.TestCheckResourceAttr(accessor, "scope_id", assignment.ScopeId), + resource.TestCheckResourceAttr(accessor, "scope", "PROJECT"), + resource.TestCheckResourceAttr(accessor, "blueprint_id", assignment.BlueprintId), + ), + ExpectNonEmptyPlan: true, + }, + }, + } + + approvalPolicyByScopeMismatch := client.ApprovalPolicyByScope{ + Scope: string(assignment.Scope), + ScopeId: assignment.ScopeId, + ApprovalPolicy: &client.ApprovalPolicy{ + Id: "other_id", + Name: validTemplate.Name, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(assignment.BlueprintId).Times(1).Return(validTemplate, nil), + mock.EXPECT().ApprovalPolicyAssign(&assignment).Times(1).Return(&assignment, nil), + mock.EXPECT().ApprovalPolicyByScope(string(assignment.Scope), assignment.ScopeId).Times(1).Return([]client.ApprovalPolicyByScope{approvalPolicyByScopeMismatch}, nil), + mock.EXPECT().ApprovalPolicyUnassign(string(assignment.Scope), assignment.ScopeId).Times(1).Return(nil), + ) + }) + }) +} diff --git a/env0/resource_approval_policy_test.go b/env0/resource_approval_policy_test.go new file mode 100644 index 00000000..6bd7d599 --- /dev/null +++ b/env0/resource_approval_policy_test.go @@ -0,0 +1,485 @@ +package env0 + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/env0/terraform-provider-env0/client/http" + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/jinzhu/copier" +) + +func TestUnitApprovalPolicyResource(t *testing.T) { + resourceType := "env0_approval_policy" + resourceName := "test" + resourceNameImport := resourceType + "." + resourceName + accessor := resourceAccessor(resourceType, resourceName) + + approvalPolicy := client.ApprovalPolicy{ + Id: uuid.NewString(), + Name: "name", + Repository: "repository", + Path: "path", + Revision: "revision", + TokenId: "token_id", + GithubInstallationId: 1, + IsGithubEnterprise: true, + } + + var template client.Template + copier.Copy(&template, &approvalPolicy) + template.Type = string(ApprovalPolicy) + + deletedTemplate := template + deletedTemplate.IsDeleted = true + + notApprovalPolicyTemplate := template + notApprovalPolicyTemplate.Type = "terraform" + + updatedApprovalPolicy := client.ApprovalPolicy{ + Id: approvalPolicy.Id, + Name: "name", + Repository: "repository2", + Path: "path2", + Revision: "revision2", + TokenId: "token_id2", + IsAzureDevOps: true, + } + + var updatedTemplate client.Template + copier.Copy(&updatedTemplate, &updatedApprovalPolicy) + template.Type = string(ApprovalPolicy) + + createPayload := client.TemplateCreatePayload{ + Name: approvalPolicy.Name, + Repository: approvalPolicy.Repository, + Path: approvalPolicy.Path, + Revision: approvalPolicy.Revision, + TokenId: approvalPolicy.TokenId, + GithubInstallationId: approvalPolicy.GithubInstallationId, + IsGithubEnterprise: approvalPolicy.IsGithubEnterprise, + Type: string(ApprovalPolicy), + } + + updatePayload := client.TemplateCreatePayload{ + Name: updatedApprovalPolicy.Name, + Repository: updatedApprovalPolicy.Repository, + Path: updatedApprovalPolicy.Path, + Revision: updatedApprovalPolicy.Revision, + TokenId: updatedApprovalPolicy.TokenId, + IsAzureDevOps: updatedApprovalPolicy.IsAzureDevOps, + Type: string(ApprovalPolicy), + } + + t.Run("Success", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", approvalPolicy.Id), + resource.TestCheckResourceAttr(accessor, "name", approvalPolicy.Name), + resource.TestCheckResourceAttr(accessor, "repository", approvalPolicy.Repository), + resource.TestCheckResourceAttr(accessor, "path", approvalPolicy.Path), + resource.TestCheckResourceAttr(accessor, "revision", approvalPolicy.Revision), + resource.TestCheckResourceAttr(accessor, "token_id", approvalPolicy.TokenId), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(approvalPolicy.GithubInstallationId)), + resource.TestCheckResourceAttr(accessor, "is_github_enterprise", strconv.FormatBool(approvalPolicy.IsGithubEnterprise)), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": updatedApprovalPolicy.Name, + "repository": updatedApprovalPolicy.Repository, + "path": updatedApprovalPolicy.Path, + "revision": updatedApprovalPolicy.Revision, + "token_id": updatedApprovalPolicy.TokenId, + "is_azure_devops": "true", + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", updatedApprovalPolicy.Id), + resource.TestCheckResourceAttr(accessor, "name", updatedApprovalPolicy.Name), + resource.TestCheckResourceAttr(accessor, "repository", updatedApprovalPolicy.Repository), + resource.TestCheckResourceAttr(accessor, "path", updatedApprovalPolicy.Path), + resource.TestCheckResourceAttr(accessor, "revision", updatedApprovalPolicy.Revision), + resource.TestCheckResourceAttr(accessor, "token_id", updatedApprovalPolicy.TokenId), + resource.TestCheckResourceAttr(accessor, "github_installation_id", "0"), + resource.TestCheckResourceAttr(accessor, "is_github_enterprise", "false"), + resource.TestCheckResourceAttr(accessor, "is_azure_devops", strconv.FormatBool(updatedApprovalPolicy.IsAzureDevOps)), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(2).Return(template, nil), + mock.EXPECT().TemplateUpdate(approvalPolicy.Id, updatePayload).Times(1).Return(updatedTemplate, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(1).Return(updatedTemplate, nil), + mock.EXPECT().TemplateDelete(approvalPolicy.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("Drift detected - deleted", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", approvalPolicy.Id), + resource.TestCheckResourceAttr(accessor, "name", approvalPolicy.Name), + resource.TestCheckResourceAttr(accessor, "repository", approvalPolicy.Repository), + resource.TestCheckResourceAttr(accessor, "path", approvalPolicy.Path), + resource.TestCheckResourceAttr(accessor, "revision", approvalPolicy.Revision), + resource.TestCheckResourceAttr(accessor, "token_id", approvalPolicy.TokenId), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(approvalPolicy.GithubInstallationId)), + resource.TestCheckResourceAttr(accessor, "is_github_enterprise", strconv.FormatBool(approvalPolicy.IsGithubEnterprise)), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": updatedApprovalPolicy.Name, + "repository": updatedApprovalPolicy.Repository, + "path": updatedApprovalPolicy.Path, + "revision": updatedApprovalPolicy.Revision, + "token_id": updatedApprovalPolicy.TokenId, + "is_azure_devops": "true", + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", updatedApprovalPolicy.Id), + resource.TestCheckResourceAttr(accessor, "name", updatedApprovalPolicy.Name), + resource.TestCheckResourceAttr(accessor, "repository", updatedApprovalPolicy.Repository), + resource.TestCheckResourceAttr(accessor, "path", updatedApprovalPolicy.Path), + resource.TestCheckResourceAttr(accessor, "revision", updatedApprovalPolicy.Revision), + resource.TestCheckResourceAttr(accessor, "token_id", updatedApprovalPolicy.TokenId), + resource.TestCheckResourceAttr(accessor, "github_installation_id", "0"), + resource.TestCheckResourceAttr(accessor, "is_github_enterprise", "false"), + resource.TestCheckResourceAttr(accessor, "is_azure_devops", strconv.FormatBool(updatedApprovalPolicy.IsAzureDevOps)), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(template.Id).Times(2).Return(deletedTemplate, nil), + ) + }) + }) + + t.Run("Drift detected - not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", approvalPolicy.Id), + resource.TestCheckResourceAttr(accessor, "name", approvalPolicy.Name), + resource.TestCheckResourceAttr(accessor, "repository", approvalPolicy.Repository), + resource.TestCheckResourceAttr(accessor, "path", approvalPolicy.Path), + resource.TestCheckResourceAttr(accessor, "revision", approvalPolicy.Revision), + resource.TestCheckResourceAttr(accessor, "token_id", approvalPolicy.TokenId), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(approvalPolicy.GithubInstallationId)), + resource.TestCheckResourceAttr(accessor, "is_github_enterprise", strconv.FormatBool(approvalPolicy.IsGithubEnterprise)), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": updatedApprovalPolicy.Name, + "repository": updatedApprovalPolicy.Repository, + "path": updatedApprovalPolicy.Path, + "revision": updatedApprovalPolicy.Revision, + "token_id": updatedApprovalPolicy.TokenId, + "is_azure_devops": "true", + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", updatedApprovalPolicy.Id), + resource.TestCheckResourceAttr(accessor, "name", updatedApprovalPolicy.Name), + resource.TestCheckResourceAttr(accessor, "repository", updatedApprovalPolicy.Repository), + resource.TestCheckResourceAttr(accessor, "path", updatedApprovalPolicy.Path), + resource.TestCheckResourceAttr(accessor, "revision", updatedApprovalPolicy.Revision), + resource.TestCheckResourceAttr(accessor, "token_id", updatedApprovalPolicy.TokenId), + resource.TestCheckResourceAttr(accessor, "github_installation_id", "0"), + resource.TestCheckResourceAttr(accessor, "is_github_enterprise", "false"), + resource.TestCheckResourceAttr(accessor, "is_azure_devops", strconv.FormatBool(updatedApprovalPolicy.IsAzureDevOps)), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(template.Id).Times(2).Return(client.Template{}, http.NewMockFailedResponseError(404)), + ) + }) + }) + + t.Run("Create Failure", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + ExpectError: regexp.MustCompile("could not create approval policy template: error"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(client.Template{}, errors.New("error")) + }) + }) + + t.Run("Update Failure", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": updatedApprovalPolicy.Name, + "repository": updatedApprovalPolicy.Repository, + "path": updatedApprovalPolicy.Path, + "revision": updatedApprovalPolicy.Revision, + "token_id": updatedApprovalPolicy.TokenId, + "is_azure_devops": "true", + }), + ExpectError: regexp.MustCompile("could not update approval policy template: error"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(2).Return(template, nil), + mock.EXPECT().TemplateUpdate(approvalPolicy.Id, updatePayload).Times(1).Return(client.Template{}, errors.New("error")), + mock.EXPECT().TemplateDelete(approvalPolicy.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("Import By Id", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: approvalPolicy.Id, + ImportStateVerify: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(3).Return(template, nil), + mock.EXPECT().TemplateDelete(approvalPolicy.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("Import By Id - not approval policy type", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: approvalPolicy.Id, + ImportStateVerify: true, + ExpectError: regexp.MustCompile("template type requires type approval-policy but received type terraform"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(1).Return(template, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(1).Return(notApprovalPolicyTemplate, nil), + mock.EXPECT().TemplateDelete(approvalPolicy.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("Import By Name", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: approvalPolicy.Name, + ImportStateVerify: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(1).Return(template, nil), + mock.EXPECT().ApprovalPolicies(approvalPolicy.Name).Times(1).Return([]client.ApprovalPolicy{approvalPolicy}, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(1).Return(template, nil), + mock.EXPECT().TemplateDelete(approvalPolicy.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("Import By Name - not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: approvalPolicy.Name, + ImportStateVerify: true, + ExpectError: regexp.MustCompile(fmt.Sprintf("approval policy with name %v not found", approvalPolicy.Name)), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(1).Return(template, nil), + mock.EXPECT().ApprovalPolicies(approvalPolicy.Name).Times(1).Return([]client.ApprovalPolicy{}, nil), + mock.EXPECT().TemplateDelete(approvalPolicy.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("Import By Name - too many results", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": approvalPolicy.Name, + "repository": approvalPolicy.Repository, + "path": approvalPolicy.Path, + "revision": approvalPolicy.Revision, + "token_id": approvalPolicy.TokenId, + "github_installation_id": approvalPolicy.GithubInstallationId, + "is_github_enterprise": "true", + }), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: approvalPolicy.Name, + ImportStateVerify: true, + ExpectError: regexp.MustCompile("found multiple approval policies with"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().TemplateCreate(createPayload).Times(1).Return(template, nil), + mock.EXPECT().Template(approvalPolicy.Id).Times(1).Return(template, nil), + mock.EXPECT().ApprovalPolicies(approvalPolicy.Name).Times(1).Return([]client.ApprovalPolicy{approvalPolicy, approvalPolicy}, nil), + mock.EXPECT().TemplateDelete(approvalPolicy.Id).Times(1).Return(nil), + ) + }) + }) +} diff --git a/env0/resource_custom_flow.go b/env0/resource_custom_flow.go index 10fb9e05..1c638d53 100644 --- a/env0/resource_custom_flow.go +++ b/env0/resource_custom_flow.go @@ -20,93 +20,7 @@ func resourceCustomFlow() *schema.Resource { Importer: &schema.ResourceImporter{StateContext: resourceCustomFlowImport}, - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Description: "id of the custom flow", - Computed: true, - }, - "name": { - Type: schema.TypeString, - Description: "name for the custom flow. note: for the UI to render the custom-flow please use `project-`", - Required: true, - }, - "repository": { - Type: schema.TypeString, - Description: "repository url for the custom flow source code", - Required: true, - }, - "path": { - Type: schema.TypeString, - Description: "terraform / terragrunt file folder inside source code. Should be the full path including the .yaml/.yml file", - Optional: true, - }, - "revision": { - Type: schema.TypeString, - Description: "source code revision (branch / tag) to use", - Optional: true, - }, - "token_id": { - Type: schema.TypeString, - Description: "the git token id to be used", - Optional: true, - }, - "ssh_keys": { - Type: schema.TypeList, - Description: "an array of references to 'data_ssh_key' to use when accessing git over ssh", - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeMap, - Description: "a map of env0_ssh_key.id and env0_ssh_key.name for each project", - }, - }, - "gitlab_project_id": { - Type: schema.TypeInt, - Description: "the project id of the relevant repository", - Optional: true, - RequiredWith: []string{"token_id"}, - }, - "github_installation_id": { - Type: schema.TypeInt, - Description: "the env0 application installation id on the relevant github repository", - Optional: true, - }, - "bitbucket_client_key": { - Type: schema.TypeString, - Description: "the bitbucket client key used for integration", - Optional: true, - }, - "is_bitbucket_server": { - Type: schema.TypeBool, - Description: "true if this custom flow uses bitbucket server repository", - Optional: true, - Default: false, - }, - "is_gitlab_enterprise": { - Type: schema.TypeBool, - Description: "true if this custom flow uses gitlab enterprise repository", - Optional: true, - Default: false, - }, - "is_github_enterprise": { - Type: schema.TypeBool, - Description: "true if this custom flow uses github enterprise repository", - Optional: true, - Default: false, - }, - "is_gitlab": { - Type: schema.TypeBool, - Optional: true, - Description: "true if this custom flow integrates with gitlab repository", - Default: false, - }, - "is_azure_devops": { - Type: schema.TypeBool, - Optional: true, - Description: "true if this custom flow integrates with azure dev ops repository", - Default: false, - }, - }, + Schema: getConfigurationTemplateSchema(CustomFlow), } } diff --git a/examples/resources/env0_approval_policy/import.sh b/examples/resources/env0_approval_policy/import.sh new file mode 100644 index 00000000..0452f750 --- /dev/null +++ b/examples/resources/env0_approval_policy/import.sh @@ -0,0 +1,2 @@ +terraform import env0_approval_policy.by_id ddda7b30-6789-4d24-937c-22322754934e +terraform import env0_approval_policy.by_name "approval-policy-PROJECT-aaaa7b30-5784-4d24-937c-223227541111" diff --git a/examples/resources/env0_approval_policy/resource.tf b/examples/resources/env0_approval_policy/resource.tf new file mode 100644 index 00000000..85fcd574 --- /dev/null +++ b/examples/resources/env0_approval_policy/resource.tf @@ -0,0 +1,16 @@ +resource "env0_project" "project" { + name = "project-name" + description = "project-description" +} + +resource "env0_approval_policy" "approval-policy" { + name = "approval-policy-PROJECT-${env0_project.project.id}" + repository = "repo" + github_installation_id = 1234 +} + +resource "env0_approval_policy_assignment" "assignment" { + scope_id = env0_project.project.id + scope = "PROJECT" + blueprint_id = env0_approval_policy.approval-policy.id +} diff --git a/tests/integration/030_approval_policy/conf.tf b/tests/integration/030_approval_policy/conf.tf new file mode 100644 index 00000000..8d6d2954 --- /dev/null +++ b/tests/integration/030_approval_policy/conf.tf @@ -0,0 +1,15 @@ +terraform { + backend "local" { + } + required_providers { + env0 = { + source = "terraform-registry.env0.com/env0/env0" + } + } +} + +provider "env0" {} + +variable "second_run" { + default = false +} diff --git a/tests/integration/030_approval_policy/expected_outputs.json b/tests/integration/030_approval_policy/expected_outputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/integration/030_approval_policy/expected_outputs.json @@ -0,0 +1 @@ +{} diff --git a/tests/integration/030_approval_policy/main.tf b/tests/integration/030_approval_policy/main.tf new file mode 100644 index 00000000..845f8232 --- /dev/null +++ b/tests/integration/030_approval_policy/main.tf @@ -0,0 +1,29 @@ +provider "random" {} + +resource "random_string" "random" { + length = 8 + special = false + min_lower = 8 +} + +resource "env0_project" "project" { + name = "Test-Project-Custom-Flow-${random_string.random.result}" + description = "Test Description" +} + +data "env0_template" "github_template" { + name = "Github Integrated Template" +} + +resource "env0_approval_policy" "test" { + name = "approval-policy-${random_string.random.result}" + repository = data.env0_template.github_template.repository + github_installation_id = data.env0_template.github_template.github_installation_id + path = var.second_run ? "second" : "misc/null-resource" + +} + +resource "env0_approval_policy_assignment" "assignment" { + scope_id = env0_project.project.id + blueprint_id = env0_approval_policy.test.id +}