From 71032c4915b118e781fa0b6925d054b319269afb Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Sun, 17 Mar 2024 08:47:47 -0500 Subject: [PATCH] Feat: Add support for Environment Discovery API (#802) * Feat: Add support for Environment Discovery API * added tests * add create and delete * added optional * added optional * adding tests - WIP * added tests * added tests * fix test * update go version * update go version * added import * Update env0/resource_environment_discovery_configuration.go Co-authored-by: Yaron Yarimi * small rename refactoring * fix race condition * add sleep to avoid race condition --------- Co-authored-by: Yaron Yarimi --- .github/workflows/ci.yml | 9 +- .github/workflows/docs.yml | 2 +- .github/workflows/release.yml | 2 +- README.md | 2 +- client/api_client.go | 3 + client/api_client_mock.go | 44 + client/environment_discovery.go | 68 ++ client/environment_discovery_test.go | 130 +++ env0/data_project.go | 8 +- env0/data_project_policy_test.go | 4 +- env0/provider.go | 1 + env0/resource_cost_credentials_test.go | 10 +- ...rce_environment_discovery_configuration.go | 349 ++++++++ ...nvironment_discovery_configuration_test.go | 795 ++++++++++++++++++ env0/resource_project.go | 6 +- env0/test_helpers.go | 2 +- .../import.sh | 1 + .../resource.tf | 26 + go.mod | 4 +- go.sum | 28 +- tests/integration/020_api_key/main.tf | 11 +- .../conf.tf | 13 + .../expected_outputs.json | 1 + .../main.tf | 30 + 24 files changed, 1518 insertions(+), 31 deletions(-) create mode 100644 client/environment_discovery.go create mode 100644 client/environment_discovery_test.go create mode 100644 env0/resource_environment_discovery_configuration.go create mode 100644 env0/resource_environment_discovery_configuration_test.go create mode 100644 examples/resources/env0_environment_discovery_configuration/import.sh create mode 100644 examples/resources/env0_environment_discovery_configuration/resource.tf create mode 100644 tests/integration/032_environment_discovery_configuration/conf.tf create mode 100644 tests/integration/032_environment_discovery_configuration/expected_outputs.json create mode 100644 tests/integration/032_environment_discovery_configuration/main.tf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd5682b1..36494a16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ env: ENV0_API_ENDPOINT: ${{ secrets.ENV0_API_ENDPOINT }} ENV0_API_KEY: ${{ secrets.TF_PROVIDER_INTEGRATION_TEST_API_KEY }} # API Key for organization 'TF-provider-integration-tests' @ dev ENV0_API_SECRET: ${{ secrets.TF_PROVIDER_INTEGRATION_TEST_API_SECRET }} - GO_VERSION: "1.20" + GO_VERSION: "1.21" TERRAFORM_VERSION: 1.1.7 jobs: @@ -33,11 +33,6 @@ jobs: - name: Go vet run: | ! go vet ./... | read - - name: Go staticcheck - uses: dominikh/staticcheck-action@v1.3.0 - with: - version: "2023.1.3" - install-go: false - name: Go Test run: go test -v ./... @@ -45,7 +40,7 @@ jobs: integration-tests: name: Integration Tests runs-on: ubuntu-20.04 - container: golang:1.20-alpine3.18 + container: golang:1.21-alpine3.18 timeout-minutes: 20 steps: - name: Install Terraform diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bacde3f2..570f7b59 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,7 +6,7 @@ on: - main env: - GO_VERSION: "1.20" + GO_VERSION: "1.21" jobs: generate-docs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cce79df5..b503d93e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ on: - "v*.*.*" env: - GO_VERSION: "1.20" + GO_VERSION: "1.21" jobs: goreleaser: diff --git a/README.md b/README.md index b650e121..50be817a 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ resource "env0_template" "example" { ## Development Setup -> **Supported Go Version: 1.20** +> **Supported Go Version: 1.21** ### Build diff --git a/client/api_client.go b/client/api_client.go index 82a40167..4a95b55a 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -147,6 +147,9 @@ type ApiClientInterface interface { ProjectBudget(projectId string) (*ProjectBudget, error) ProjectBudgetUpdate(projectId string, payload *ProjectBudgetUpdatePayload) (*ProjectBudget, error) ProjectBudgetDelete(projectId string) error + PutEnvironmentDiscovery(projectId string, payload *EnvironmentDiscoveryPutPayload) (*EnvironmentDiscoveryPayload, error) + GetEnvironmentDiscovery(projectId string) (*EnvironmentDiscoveryPayload, error) + DeleteEnvironmentDiscovery(projectId string) error } func NewApiClient(client http.HttpClientInterface, defaultOrganizationId string) ApiClientInterface { diff --git a/client/api_client_mock.go b/client/api_client_mock.go index ad358f45..ac366243 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -570,6 +570,20 @@ func (mr *MockApiClientInterfaceMockRecorder) CustomFlows(arg0 any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomFlows", reflect.TypeOf((*MockApiClientInterface)(nil).CustomFlows), arg0) } +// DeleteEnvironmentDiscovery mocks base method. +func (m *MockApiClientInterface) DeleteEnvironmentDiscovery(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteEnvironmentDiscovery", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteEnvironmentDiscovery indicates an expected call of DeleteEnvironmentDiscovery. +func (mr *MockApiClientInterfaceMockRecorder) DeleteEnvironmentDiscovery(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEnvironmentDiscovery", reflect.TypeOf((*MockApiClientInterface)(nil).DeleteEnvironmentDiscovery), arg0) +} + // Environment mocks base method. func (m *MockApiClientInterface) Environment(arg0 string) (Environment, error) { m.ctrl.T.Helper() @@ -778,6 +792,21 @@ func (mr *MockApiClientInterfaceMockRecorder) Environments() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Environments", reflect.TypeOf((*MockApiClientInterface)(nil).Environments)) } +// GetEnvironmentDiscovery mocks base method. +func (m *MockApiClientInterface) GetEnvironmentDiscovery(arg0 string) (*EnvironmentDiscoveryPayload, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEnvironmentDiscovery", arg0) + ret0, _ := ret[0].(*EnvironmentDiscoveryPayload) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEnvironmentDiscovery indicates an expected call of GetEnvironmentDiscovery. +func (mr *MockApiClientInterfaceMockRecorder) GetEnvironmentDiscovery(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironmentDiscovery", reflect.TypeOf((*MockApiClientInterface)(nil).GetEnvironmentDiscovery), arg0) +} + // GitToken mocks base method. func (m *MockApiClientInterface) GitToken(arg0 string) (*GitToken, error) { m.ctrl.T.Helper() @@ -1399,6 +1428,21 @@ func (mr *MockApiClientInterfaceMockRecorder) Providers() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Providers", reflect.TypeOf((*MockApiClientInterface)(nil).Providers)) } +// PutEnvironmentDiscovery mocks base method. +func (m *MockApiClientInterface) PutEnvironmentDiscovery(arg0 string, arg1 *EnvironmentDiscoveryPutPayload) (*EnvironmentDiscoveryPayload, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutEnvironmentDiscovery", arg0, arg1) + ret0, _ := ret[0].(*EnvironmentDiscoveryPayload) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutEnvironmentDiscovery indicates an expected call of PutEnvironmentDiscovery. +func (mr *MockApiClientInterfaceMockRecorder) PutEnvironmentDiscovery(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutEnvironmentDiscovery", reflect.TypeOf((*MockApiClientInterface)(nil).PutEnvironmentDiscovery), arg0, arg1) +} + // RemoteStateAccessConfiguration mocks base method. func (m *MockApiClientInterface) RemoteStateAccessConfiguration(arg0 string) (*RemoteStateAccessConfiguration, error) { m.ctrl.T.Helper() diff --git a/client/environment_discovery.go b/client/environment_discovery.go new file mode 100644 index 00000000..6f823af6 --- /dev/null +++ b/client/environment_discovery.go @@ -0,0 +1,68 @@ +package client + +type EnvironmentDiscoveryPutPayload struct { + GlobPattern string `json:"globPattern"` + EnvironmentPlacement string `json:"environmentPlacement"` + WorkspaceNaming string `json:"workspaceNaming"` + AutoDeployByCustomGlob string `json:"autoDeployByCustomGlob,omitempty"` + Repository string `json:"repository"` + TerraformVersion string `json:"terraformVersion,omitempty"` + OpentofuVersion string `json:"opentofuVersion,omitempty"` + TerragruntVersion string `json:"terragruntVersion,omitempty"` + TerragruntTfBinary string `json:"terragruntTfBinary,omitempty"` + IsTerragruntRunAll bool `json:"is_terragrunt_run_all"` + Type string `json:"type"` + GitlabProjectId int `json:"gitlabProjectId,omitempty"` + TokenId string `json:"tokenId,omitempty"` + SshKeys []TemplateSshKey `json:"sshKeys,omitempty"` + GithubInstallationId int `json:"githubInstallationId,omitempty"` + BitbucketClientKey string `json:"bitbucketClientKey,omitempty"` + IsAzureDevops bool `json:"isAzureDevOps"` + Retry TemplateRetry `json:"retry"` +} + +type EnvironmentDiscoveryPayload struct { + Id string `json:"id"` + GlobPattern string `json:"globPattern"` + EnvironmentPlacement string `json:"environmentPlacement"` + WorkspaceNaming string `json:"workspaceNaming"` + AutoDeployByCustomGlob string `json:"autoDeployByCustomGlob"` + Repository string `json:"repository"` + TerraformVersion string `json:"terraformVersion"` + OpentofuVersion string `json:"opentofuVersion"` + TerragruntVersion string `json:"terragruntVersion"` + TerragruntTfBinary string `json:"terragruntTfBinary" tfschema:",omitempty"` + IsTerragruntRunAll bool `json:"is_terragrunt_run_all"` + Type string `json:"type"` + GitlabProjectId int `json:"gitlabProjectId"` + TokenId string `json:"tokenId"` + SshKeys []TemplateSshKey `json:"sshKeys" tfschema:"-"` + GithubInstallationId int `json:"githubInstallationId"` + BitbucketClientKey string `json:"bitbucketClientKey"` + IsAzureDevops bool `json:"isAzureDevOps"` + Retry TemplateRetry `json:"retry" tfschema:"-"` +} + +func (client *ApiClient) PutEnvironmentDiscovery(projectId string, payload *EnvironmentDiscoveryPutPayload) (*EnvironmentDiscoveryPayload, error) { + var result EnvironmentDiscoveryPayload + + if err := client.http.Put("/environment-discovery/projects/"+projectId, payload, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func (client *ApiClient) GetEnvironmentDiscovery(projectId string) (*EnvironmentDiscoveryPayload, error) { + var result EnvironmentDiscoveryPayload + + if err := client.http.Get("/environment-discovery/projects/"+projectId, nil, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func (client *ApiClient) DeleteEnvironmentDiscovery(projectId string) error { + return client.http.Delete("/environment-discovery/projects/"+projectId, nil) +} diff --git a/client/environment_discovery_test.go b/client/environment_discovery_test.go new file mode 100644 index 00000000..d1f2f278 --- /dev/null +++ b/client/environment_discovery_test.go @@ -0,0 +1,130 @@ +package client_test + +import ( + "errors" + + . "github.com/env0/terraform-provider-env0/client" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" +) + +var _ = Describe("Environment Discovery", func() { + mockError := errors.New("error") + + projectId := "pid" + + mockEnvironmentDiscovery := EnvironmentDiscoveryPayload{ + Id: "id", + GlobPattern: "**", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + AutoDeployByCustomGlob: "**", + Repository: "https://re.po", + TerraformVersion: "1.5.6", + Type: "terraform", + GithubInstallationId: 1000, + } + + Describe("PUT", func() { + putPayload := EnvironmentDiscoveryPutPayload{ + GlobPattern: mockEnvironmentDiscovery.GlobPattern, + EnvironmentPlacement: mockEnvironmentDiscovery.EnvironmentPlacement, + WorkspaceNaming: mockEnvironmentDiscovery.WorkspaceNaming, + AutoDeployByCustomGlob: mockEnvironmentDiscovery.AutoDeployByCustomGlob, + Repository: mockEnvironmentDiscovery.Repository, + TerraformVersion: mockEnvironmentDiscovery.TerraformVersion, + Type: mockEnvironmentDiscovery.Type, + GithubInstallationId: mockEnvironmentDiscovery.GithubInstallationId, + } + + Describe("success", func() { + var ret *EnvironmentDiscoveryPayload + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Put("/environment-discovery/projects/"+projectId, &putPayload, gomock.Any()). + Do(func(path string, request interface{}, response *EnvironmentDiscoveryPayload) { + *response = mockEnvironmentDiscovery + }).Times(1) + ret, _ = apiClient.PutEnvironmentDiscovery(projectId, &putPayload) + }) + + It("Should return environment discovery", func() { + Expect(*ret).To(Equal(mockEnvironmentDiscovery)) + }) + }) + + Describe("failure", func() { + var err error + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Put("/environment-discovery/projects/"+projectId, &putPayload, gomock.Any()).Return(mockError).Times(1) + _, err = apiClient.PutEnvironmentDiscovery(projectId, &putPayload) + }) + + It("Should return error", func() { + Expect(err).To(Equal(mockError)) + }) + }) + }) + + Describe("GET", func() { + Describe("success", func() { + var ret *EnvironmentDiscoveryPayload + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Get("/environment-discovery/projects/"+projectId, nil, gomock.Any()). + Do(func(path string, request interface{}, response *EnvironmentDiscoveryPayload) { + *response = mockEnvironmentDiscovery + }).Times(1) + ret, _ = apiClient.GetEnvironmentDiscovery(projectId) + }) + + It("Should return environment discovery", func() { + Expect(*ret).To(Equal(mockEnvironmentDiscovery)) + }) + }) + + Describe("failure", func() { + var err error + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Get("/environment-discovery/projects/"+projectId, nil, gomock.Any()).Return(mockError).Times(1) + _, err = apiClient.GetEnvironmentDiscovery(projectId) + }) + + It("Should return error", func() { + Expect(err).To(Equal(mockError)) + }) + }) + }) + + Describe("DELETE", func() { + Describe("success", func() { + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT().Delete("/environment-discovery/projects/"+projectId, nil).Times(1) + apiClient.DeleteEnvironmentDiscovery(projectId) + }) + + It("Should send DELETE request", func() {}) + }) + + Describe("failure", func() { + var err error + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Delete("/environment-discovery/projects/"+projectId, nil).Return(mockError).Times(1) + err = apiClient.DeleteEnvironmentDiscovery(projectId) + }) + + It("Should return error", func() { + Expect(err).To(Equal(mockError)) + }) + }) + }) +}) diff --git a/env0/data_project.go b/env0/data_project.go index 6e2534d3..fee6b7f2 100644 --- a/env0/data_project.go +++ b/env0/data_project.go @@ -92,7 +92,7 @@ func dataProjectRead(ctx context.Context, d *schema.ResourceData, meta interface return nil } -func filterByParentProjectId(name string, parentId string, projects []client.Project) ([]client.Project, error) { +func filterByParentProjectId(parentId string, projects []client.Project) ([]client.Project, error) { filteredProjects := make([]client.Project, 0) for _, project := range projects { if len(project.ParentProjectId) == 0 { @@ -107,7 +107,7 @@ func filterByParentProjectId(name string, parentId string, projects []client.Pro return filteredProjects, nil } -func filterByParentProjectName(name string, parentName string, projects []client.Project, meta interface{}) ([]client.Project, error) { +func filterByParentProjectName(parentName string, projects []client.Project, meta interface{}) ([]client.Project, error) { filteredProjects := make([]client.Project, 0) for _, project := range projects { if len(project.ParentProjectId) == 0 { @@ -142,13 +142,13 @@ func getProjectByName(name string, parentId string, parentName string, meta inte } if len(parentId) > 0 { // Use parentId filter to reduce the results. - projectsByName, err = filterByParentProjectId(name, parentId, projectsByName) + projectsByName, err = filterByParentProjectId(parentId, projectsByName) if err != nil { return client.Project{}, err } } else if len(parentName) > 0 { // Use parentName filter to reduce the results. - projectsByName, err = filterByParentProjectName(name, parentName, projectsByName, meta) + projectsByName, err = filterByParentProjectName(parentName, projectsByName, meta) if err != nil { return client.Project{}, err } diff --git a/env0/data_project_policy_test.go b/env0/data_project_policy_test.go index 4a5514ae..cd33d1d1 100644 --- a/env0/data_project_policy_test.go +++ b/env0/data_project_policy_test.go @@ -29,7 +29,7 @@ func TestPolicyDataSource(t *testing.T) { resourceName := "test_policy" accessor := dataSourceAccessor(resourceType, resourceName) - getValidTestCase := func(input map[string]interface{}) resource.TestCase { + getValidTestCase := func() resource.TestCase { return resource.TestCase{ Steps: []resource.TestStep{ { @@ -57,7 +57,7 @@ func TestPolicyDataSource(t *testing.T) { } t.Run("valid", func(t *testing.T) { - runUnitTest(t, getValidTestCase(map[string]interface{}{}), func(mock *client.MockApiClientInterface) { + runUnitTest(t, getValidTestCase(), func(mock *client.MockApiClientInterface) { mock.EXPECT().Policy(policy.ProjectId).AnyTimes().Return(policy, nil) }) }) diff --git a/env0/provider.go b/env0/provider.go index c192b067..9777b31c 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -146,6 +146,7 @@ func Provider(version string) plugin.ProviderFunc { "env0_approval_policy": resourceApprovalPolicy(), "env0_approval_policy_assignment": resourceApprovalPolicyAssignment(), "env0_project_budget": resourceProjectBudget(), + "env0_environment_discovery_configuration": resourceEnvironmentDiscoveryConfiguration(), }, } diff --git a/env0/resource_cost_credentials_test.go b/env0/resource_cost_credentials_test.go index fee66e01..3f7036e1 100644 --- a/env0/resource_cost_credentials_test.go +++ b/env0/resource_cost_credentials_test.go @@ -281,11 +281,11 @@ func TestUnitAzureCostCredentialsResource(t *testing.T) { t.Run("validate missing arguments", func(t *testing.T) { missingArgumentsTestCases := []resource.TestCase{ - missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}, "client_id"), - missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}, "client_secret"), - missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}, "subscription_id"), - missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}, "tenant_id"), - missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}, "name"), + missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}), + missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}), + missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}), + missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}), + missingArgumentTestCaseForCostCred(resourceType, resourceName, map[string]interface{}{}), } for _, testCase := range missingArgumentsTestCases { tc := testCase diff --git a/env0/resource_environment_discovery_configuration.go b/env0/resource_environment_discovery_configuration.go new file mode 100644 index 00000000..150368b6 --- /dev/null +++ b/env0/resource_environment_discovery_configuration.go @@ -0,0 +1,349 @@ +package env0 + +import ( + "context" + "errors" + "fmt" + "strings" + + "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 resourceEnvironmentDiscoveryConfiguration() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceEnvironmentDiscoveryConfigurationPut, + ReadContext: resourceEnvironmentDiscoveryConfigurationGet, + UpdateContext: resourceEnvironmentDiscoveryConfigurationPut, + DeleteContext: resourceEnvironmentDiscoveryConfigurationDelete, + + Importer: &schema.ResourceImporter{StateContext: resourceEnvironmentDiscoveryConfigurationImport}, + + Description: "See https://docs.env0.com/docs/environment-discovery for additional details", + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeString, + Description: "the project id", + Required: true, + ForceNew: true, + }, + "glob_pattern": { + Type: schema.TypeString, + Description: "the environments glob pattern. Any match to this pattern will result in an Environment creation and plan", + Required: true, + }, + "repository": { + Type: schema.TypeString, + Description: "the repository to run discovery on", + Required: true, + }, + "type": { + Type: schema.TypeString, + Description: "the infrastructure type use. Valid values: 'opentofu', 'terraform', 'terragrunt', 'workflow' (default: 'opentofu')", + Default: "opentofu", + ValidateDiagFunc: NewStringInValidator([]string{"opentofu", "terraform", "terragrunt", "workflow"}), + Optional: true, + }, + "environment_placement": { + Type: schema.TypeString, + Description: "the environment placement strategy with the project (default: 'topProject')", + Default: "topProject", + ValidateDiagFunc: NewStringInValidator([]string{"existingSubProject", "topProject"}), + Optional: true, + }, + "workspace_naming": { + Type: schema.TypeString, + Description: "the Workspace namimg strategy (default: 'default')", + Default: "default", + ValidateDiagFunc: NewStringInValidator([]string{"default", "environmentName"}), + Optional: true, + }, + "auto_deploy_by_custom_glob": { + Type: schema.TypeString, + Description: "If specified, deploy/plan on changes matching the given pattern (glob). Otherwise, deploy on template folder changes only", + Optional: true, + }, + "terraform_version": { + Type: schema.TypeString, + Description: "the Terraform version to use (example: 1.7.4). Setting to `RESOLVE_FROM_TERRAFORM_CODE` defaults to the version of `terraform.required_version` during run-time (resolve from terraform code). Setting to `latest`, the version used will be the most recent one available for Terraform.", + Optional: true, + ValidateDiagFunc: NewRegexValidator(`^(?:[0-9]\.[0-9]{1,2}\.[0-9]{1,2})|RESOLVE_FROM_TERRAFORM_CODE|latest$`), + }, + "opentofu_version": { + Type: schema.TypeString, + Description: "the Opentofu version to use (example: 1.6.1). Setting to `latest`, the version used will be the most recent one available for OpenTofu.", + Optional: true, + ValidateDiagFunc: NewRegexValidator(`^(?:[0-9]\.[0-9]{1,2}\.[0-9]{1,2})|latest$`), + }, + "terragrunt_version": { + Type: schema.TypeString, + Description: "the Terragrunt version to use (example: 0.52.0)", + ValidateDiagFunc: NewRegexValidator(`^[0-9]\.[0-9]{1,2}\.[0-9]{1,2}$`), + Optional: true, + }, + "terragrunt_tf_binary": { + Type: schema.TypeString, + Optional: true, + Description: "The binary to use with Terragrunt. Valid values: 'opentofu' and 'terraform' (default: 'opentofu')", + ValidateDiagFunc: NewStringInValidator([]string{"opentofu", "terraform"}), + Default: "opentofu", + }, + "is_terragrunt_run_all": { + Type: schema.TypeBool, + Optional: true, + Description: "If set to 'true', execute terragrunt commands with 'run all'", + Default: false, + }, + "ssh_key_id": { + Type: schema.TypeString, + Description: "The ssh key id that will be available during deployment", + Optional: true, + RequiredWith: []string{"ssh_key_name"}, + }, + "ssh_key_name": { + Type: schema.TypeString, + Description: "The ssh key name that will be available during deployment", + Optional: true, + RequiredWith: []string{"ssh_key_id"}, + }, + "retries_on_deploy": { + Type: schema.TypeInt, + Description: "number of times to retry when deploy fails (between 1 and 3)", + Optional: true, + ValidateDiagFunc: ValidateRetries, + }, + "retry_on_deploy_only_when_matches_regex": { + Type: schema.TypeString, + Description: "retry (on deploy) if error matches the specified regex", + Optional: true, + RequiredWith: []string{"retries_on_deploy"}, + }, + "retries_on_destroy": { + Type: schema.TypeInt, + Description: "number of times to retry when destroy fails (between 1 and 3)", + Optional: true, + ValidateDiagFunc: ValidateRetries, + }, + "retry_on_destroy_only_when_matches_regex": { + Type: schema.TypeString, + Description: "retry (on destroy) if error matches the specified regex", + Optional: true, + RequiredWith: []string{"retries_on_destroy"}, + }, + "github_installation_id": { + Type: schema.TypeInt, + Description: "github repository id", + Optional: true, + }, + "bitbucket_client_key": { + Type: schema.TypeString, + Description: "bitbucket client", + Optional: true, + }, + "gitlab_project_id": { + Type: schema.TypeInt, + Description: "gitlab project id", + Optional: true, + RequiredWith: []string{"token_id"}, + }, + "is_azure_devops": { + Type: schema.TypeBool, + Optional: true, + Description: "set to true if azure devops is used", + Default: false, + RequiredWith: []string{"token_id"}, + }, + "token_id": { + Type: schema.TypeString, + Description: "a token id to be used with 'gitlab' or 'azure_devops'", + Optional: true, + }, + }, + } +} + +func discoveryReadSshKeyHelper(putPayload *client.EnvironmentDiscoveryPutPayload, d *schema.ResourceData) { + sshKeyId := d.Get("ssh_key_id").(string) + if sshKeyId != "" { + sshKeyName := d.Get("ssh_key_name").(string) + putPayload.SshKeys = append(putPayload.SshKeys, client.TemplateSshKey{ + Id: sshKeyId, + Name: sshKeyName, + }) + } +} + +func discoveryWriteSshKeyHelper(getPayload *client.EnvironmentDiscoveryPayload, d *schema.ResourceData) { + var sshKey client.TemplateSshKey + + if len(getPayload.SshKeys) > 0 { + sshKey = getPayload.SshKeys[0] + } + + d.Set("ssh_key_id", sshKey.Id) + d.Set("ssh_key_name", sshKey.Name) +} + +func discoveryValidatePutPayload(putPayload *client.EnvironmentDiscoveryPutPayload) error { + opentofuVersionSet := putPayload.OpentofuVersion != "" + terraformVersionSet := putPayload.TerraformVersion != "" + terragruntVersionSet := putPayload.TerragruntVersion != "" + + switch putPayload.Type { + case "opentofu": + if !opentofuVersionSet { + return errors.New("'opentofu_version' not set") + } + case "terraform": + if !terraformVersionSet { + return errors.New("'terraform_version' not set") + } + case "terragrunt": + if !terragruntVersionSet { + return errors.New("'terragrunt_version' not set") + } + + if putPayload.TerragruntTfBinary == "opentofu" && !opentofuVersionSet { + return errors.New("'terragrunt_tf_binary' is set to 'opentofu', but 'opentofu_version' not set") + } + + if putPayload.TerragruntTfBinary == "terraform" && !terraformVersionSet { + return errors.New("'terragrunt_tf_binary' is set to 'terraform', but 'terraform_version' not set") + } + case "workflow": + default: + return fmt.Errorf("unhandled type %s", putPayload.Type) + } + + vcsCounter := 0 + vcsEnabledAttributes := []string{} + + if putPayload.GithubInstallationId != 0 { + vcsCounter++ + vcsEnabledAttributes = append(vcsEnabledAttributes, "github_installation_id") + } + + if putPayload.BitbucketClientKey != "" { + vcsCounter++ + vcsEnabledAttributes = append(vcsEnabledAttributes, "bitbucket_client_key") + } + + if putPayload.GitlabProjectId != 0 { + vcsCounter++ + vcsEnabledAttributes = append(vcsEnabledAttributes, "gitlab_project_id") + } + + if putPayload.IsAzureDevops { + vcsCounter++ + vcsEnabledAttributes = append(vcsEnabledAttributes, "is_azure_devops") + } + + if vcsCounter == 0 { + return errors.New("must set exactly one vcs, none were configured: github_installation_id, bitbucket_client_key, gitlab_project_id, or is_azure_devops") + } + + if vcsCounter > 1 { + return fmt.Errorf("must set exactly one vcs, but more were configured: %s", strings.Join(vcsEnabledAttributes, ", ")) + } + + return nil +} + +func resourceEnvironmentDiscoveryConfigurationPut(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + var putPayload client.EnvironmentDiscoveryPutPayload + if err := readResourceData(&putPayload, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + discoveryReadSshKeyHelper(&putPayload, d) + + templateCreatePayloadRetryOnHelper("", d, "deploy", &putPayload.Retry.OnDeploy) + templateCreatePayloadRetryOnHelper("", d, "destroy", &putPayload.Retry.OnDestroy) + + if err := discoveryValidatePutPayload(&putPayload); err != nil { + return diag.Errorf("validation error: %s", err.Error()) + } + + if putPayload.Type != "terragrunt" { + // Remove the default terragrunt_tf_binary if terragrunt isn't used. + putPayload.TerragruntTfBinary = "" + } + + res, err := apiClient.PutEnvironmentDiscovery(d.Get("project_id").(string), &putPayload) + if err != nil { + return diag.Errorf("enable/update environment discovery configuration request failed: %s", err.Error()) + } + + d.SetId(res.Id) + + return nil +} + +func resourceEnvironmentDiscoveryConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + projectId := d.Get("project_id").(string) + + if err := apiClient.DeleteEnvironmentDiscovery(projectId); err != nil { + return diag.Errorf("delete environment discovery configuration request failed: %s", err.Error()) + } + + return nil +} + +func setResourceEnvironmentDiscoveryConfiguration(d *schema.ResourceData, getPayload *client.EnvironmentDiscoveryPayload) error { + if err := writeResourceData(getPayload, d); err != nil { + return fmt.Errorf("schema resource data serialization failed: %v", err) + } + + discoveryWriteSshKeyHelper(getPayload, d) + + templateReadRetryOnHelper("", d, "deploy", getPayload.Retry.OnDeploy) + templateReadRetryOnHelper("", d, "destroy", getPayload.Retry.OnDestroy) + + return nil +} + +func resourceEnvironmentDiscoveryConfigurationGet(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + projectId := d.Get("project_id").(string) + + getPayload, err := apiClient.GetEnvironmentDiscovery(projectId) + if err != nil { + return ResourceGetFailure(ctx, "environment_discovery_configuration", d, err) + } + + if err := setResourceEnvironmentDiscoveryConfiguration(d, getPayload); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceEnvironmentDiscoveryConfigurationImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + apiClient := meta.(client.ApiClientInterface) + + projectId := d.Id() + + getPayload, err := apiClient.GetEnvironmentDiscovery(projectId) + if err != nil { + return nil, err + } + + if err := setResourceEnvironmentDiscoveryConfiguration(d, getPayload); err != nil { + return nil, err + } + + d.Set("project_id", projectId) + + if _, ok := d.GetOk("terragrunt_tf_binary"); !ok { + d.Set("terragrunt_tf_binary", "opentofu") + } + + return []*schema.ResourceData{d}, nil +} diff --git a/env0/resource_environment_discovery_configuration_test.go b/env0/resource_environment_discovery_configuration_test.go new file mode 100644 index 00000000..f805dfd1 --- /dev/null +++ b/env0/resource_environment_discovery_configuration_test.go @@ -0,0 +1,795 @@ +package env0 + +import ( + "regexp" + "strconv" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "go.uber.org/mock/gomock" +) + +func TestUnitEnvironmentDiscoveryConfigurationResource(t *testing.T) { + resourceType := "env0_environment_discovery_configuration" + resourceName := "test" + resourceNameImport := resourceType + "." + resourceName + + accessor := resourceAccessor(resourceType, resourceName) + + projectId := "pid" + id := "id" + + t.Run("default (opentofu and terragrunt)", func(t *testing.T) { + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "opentofu", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + OpentofuVersion: "1.6.2", + GithubInstallationId: 12345, + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + OpentofuVersion: putPayload.OpentofuVersion, + GithubInstallationId: putPayload.GithubInstallationId, + } + + updatePutPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**/**", + Repository: "https://re.po", + Type: "terragrunt", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + OpentofuVersion: "1.6.3", + TerragruntVersion: "0.63.0", + GithubInstallationId: 3213, + TerragruntTfBinary: "opentofu", + } + + updateGetPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: updatePutPayload.GlobPattern, + Repository: updatePutPayload.Repository, + Type: updatePutPayload.Type, + EnvironmentPlacement: updatePutPayload.EnvironmentPlacement, + WorkspaceNaming: updatePutPayload.WorkspaceNaming, + OpentofuVersion: updatePutPayload.OpentofuVersion, + TerragruntVersion: updatePutPayload.TerragruntVersion, + GithubInstallationId: updatePutPayload.GithubInstallationId, + TerragruntTfBinary: "opentofu", + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "opentofu_version": putPayload.OpentofuVersion, + "github_installation_id": putPayload.GithubInstallationId, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", putPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", putPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", putPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", putPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", putPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "opentofu_version", putPayload.OpentofuVersion), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(putPayload.GithubInstallationId)), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": updatePutPayload.GlobPattern, + "repository": updatePutPayload.Repository, + "opentofu_version": updatePutPayload.OpentofuVersion, + "github_installation_id": updatePutPayload.GithubInstallationId, + "type": updatePutPayload.Type, + "terragrunt_version": updatePutPayload.TerragruntVersion, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", updatePutPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", updatePutPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", updatePutPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", updatePutPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", updatePutPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "opentofu_version", updatePutPayload.OpentofuVersion), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(updatePutPayload.GithubInstallationId)), + resource.TestCheckResourceAttr(accessor, "terragrunt_version", updatePutPayload.TerragruntVersion), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(2).Return(&getPayload, nil), + mock.EXPECT().PutEnvironmentDiscovery(projectId, &updatePutPayload).Times(1).Return(&updateGetPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(1).Return(&updateGetPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) + + t.Run("terraform", func(t *testing.T) { + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "terraform", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + TerraformVersion: "1.6.2", + GithubInstallationId: 12345, + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + TerraformVersion: putPayload.TerraformVersion, + GithubInstallationId: putPayload.GithubInstallationId, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "type": putPayload.Type, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "terraform_version": putPayload.TerraformVersion, + "github_installation_id": putPayload.GithubInstallationId, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", putPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", putPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", putPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", putPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", putPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "terraform_version", putPayload.TerraformVersion), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(putPayload.GithubInstallationId)), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(1).Return(&getPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) + + t.Run("workflow", func(t *testing.T) { + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "workflow", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + GithubInstallationId: 12345, + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + GithubInstallationId: putPayload.GithubInstallationId, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "type": putPayload.Type, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "github_installation_id": putPayload.GithubInstallationId, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", putPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", putPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", putPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", putPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", putPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(putPayload.GithubInstallationId)), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(1).Return(&getPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) + + t.Run("bitbucket & terragrunt & terraform", func(t *testing.T) { + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "terragrunt", + TerragruntTfBinary: "terraform", + TerragruntVersion: "0.65.0", + TerraformVersion: "1.5.0", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + BitbucketClientKey: "key", + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + TerragruntTfBinary: putPayload.TerragruntTfBinary, + TerragruntVersion: putPayload.TerragruntVersion, + TerraformVersion: putPayload.TerraformVersion, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + BitbucketClientKey: putPayload.BitbucketClientKey, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "type": putPayload.Type, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "terragrunt_tf_binary": putPayload.TerragruntTfBinary, + "terragrunt_version": putPayload.TerragruntVersion, + "terraform_version": putPayload.TerraformVersion, + "bitbucket_client_key": putPayload.BitbucketClientKey, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", putPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", putPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", putPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", putPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", putPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "bitbucket_client_key", putPayload.BitbucketClientKey), + resource.TestCheckResourceAttr(accessor, "terragrunt_tf_binary", putPayload.TerragruntTfBinary), + resource.TestCheckResourceAttr(accessor, "terragrunt_version", putPayload.TerragruntVersion), + resource.TestCheckResourceAttr(accessor, "terraform_version", putPayload.TerraformVersion), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(1).Return(&getPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) + + t.Run("terraform + gitlab", func(t *testing.T) { + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "terraform", + TerraformVersion: "1.7.8", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + GitlabProjectId: 12345, + TokenId: "abcdefg", + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + TerraformVersion: putPayload.TerraformVersion, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + GitlabProjectId: putPayload.GitlabProjectId, + TokenId: putPayload.TokenId, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "type": putPayload.Type, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "gitlab_project_id": putPayload.GitlabProjectId, + "token_id": putPayload.TokenId, + "terraform_version": putPayload.TerraformVersion, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", putPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", putPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", putPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", putPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", putPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "gitlab_project_id", strconv.Itoa(putPayload.GitlabProjectId)), + resource.TestCheckResourceAttr(accessor, "token_id", putPayload.TokenId), + resource.TestCheckResourceAttr(accessor, "terraform_version", putPayload.TerraformVersion), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(1).Return(&getPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) + + t.Run("azure devops", func(t *testing.T) { + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "workflow", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + TokenId: "12345", + IsAzureDevops: true, + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + TokenId: putPayload.TokenId, + IsAzureDevops: putPayload.IsAzureDevops, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "type": putPayload.Type, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "token_id": putPayload.TokenId, + "is_azure_devops": putPayload.IsAzureDevops, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", putPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", putPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", putPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", putPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", putPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "token_id", putPayload.TokenId), + resource.TestCheckResourceAttr(accessor, "is_azure_devops", "true"), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(1).Return(&getPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) + + t.Run("sshkey", func(t *testing.T) { + sshKeyId := "sshi" + sshKeyName := "sshn" + sshKeys := []client.TemplateSshKey{ + { + Id: sshKeyId, + Name: sshKeyName, + }, + } + + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "terraform", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + TerraformVersion: "1.6.2", + GithubInstallationId: 12345, + SshKeys: sshKeys, + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + TerraformVersion: putPayload.TerraformVersion, + GithubInstallationId: putPayload.GithubInstallationId, + SshKeys: putPayload.SshKeys, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "type": putPayload.Type, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "terraform_version": putPayload.TerraformVersion, + "github_installation_id": putPayload.GithubInstallationId, + "ssh_key_name": sshKeyName, + "ssh_key_id": sshKeyId, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", putPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", putPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", putPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", putPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", putPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "terraform_version", putPayload.TerraformVersion), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(putPayload.GithubInstallationId)), + resource.TestCheckResourceAttr(accessor, "ssh_key_name", sshKeyName), + resource.TestCheckResourceAttr(accessor, "ssh_key_id", sshKeyId), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(1).Return(&getPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) + + t.Run("retry", func(t *testing.T) { + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "terraform", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + TerraformVersion: "1.6.2", + GithubInstallationId: 12345, + Retry: client.TemplateRetry{ + OnDeploy: &client.TemplateRetryOn{ + Times: 3, + ErrorRegex: "abc", + }, + OnDestroy: &client.TemplateRetryOn{ + Times: 1, + }, + }, + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + TerraformVersion: putPayload.TerraformVersion, + GithubInstallationId: putPayload.GithubInstallationId, + Retry: putPayload.Retry, + } + + updatePutPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**/**", + Repository: "https://re.po", + Type: "terraform", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + TerraformVersion: "1.6.2", + GithubInstallationId: 12345, + Retry: client.TemplateRetry{ + OnDestroy: &client.TemplateRetryOn{ + Times: 1, + }, + }, + } + + updateGetPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: updatePutPayload.GlobPattern, + Repository: updatePutPayload.Repository, + Type: updatePutPayload.Type, + EnvironmentPlacement: updatePutPayload.EnvironmentPlacement, + WorkspaceNaming: updatePutPayload.WorkspaceNaming, + TerraformVersion: updatePutPayload.TerraformVersion, + GithubInstallationId: updatePutPayload.GithubInstallationId, + Retry: updatePutPayload.Retry, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "type": putPayload.Type, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "terraform_version": putPayload.TerraformVersion, + "github_installation_id": putPayload.GithubInstallationId, + "retries_on_deploy": 3, + "retry_on_deploy_only_when_matches_regex": "abc", + "retries_on_destroy": 1, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", putPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", putPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", putPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", putPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", putPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "terraform_version", putPayload.TerraformVersion), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(putPayload.GithubInstallationId)), + resource.TestCheckResourceAttr(accessor, "retries_on_deploy", "3"), + resource.TestCheckResourceAttr(accessor, "retry_on_deploy_only_when_matches_regex", "abc"), + resource.TestCheckResourceAttr(accessor, "retries_on_destroy", "1"), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "type": updatePutPayload.Type, + "glob_pattern": updatePutPayload.GlobPattern, + "repository": updatePutPayload.Repository, + "terraform_version": updatePutPayload.TerraformVersion, + "github_installation_id": updatePutPayload.GithubInstallationId, + "retries_on_destroy": 1, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "project_id", projectId), + resource.TestCheckResourceAttr(accessor, "glob_pattern", updatePutPayload.GlobPattern), + resource.TestCheckResourceAttr(accessor, "repository", updatePutPayload.Repository), + resource.TestCheckResourceAttr(accessor, "type", updatePutPayload.Type), + resource.TestCheckResourceAttr(accessor, "environment_placement", updatePutPayload.EnvironmentPlacement), + resource.TestCheckResourceAttr(accessor, "workspace_naming", updatePutPayload.WorkspaceNaming), + resource.TestCheckResourceAttr(accessor, "terraform_version", updatePutPayload.TerraformVersion), + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(updatePutPayload.GithubInstallationId)), + resource.TestCheckResourceAttr(accessor, "retries_on_destroy", "1"), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(2).Return(&getPayload, nil), + mock.EXPECT().PutEnvironmentDiscovery(projectId, &updatePutPayload).Times(1).Return(&updateGetPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(1).Return(&updateGetPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) + + t.Run("error: default (opentofu) with no version", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": "**", + "repository": "https://re.po", + "github_installation_id": 1234, + }), + ExpectError: regexp.MustCompile("'opentofu_version' not set"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + + t.Run("error: terraform with no version", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": "**", + "repository": "https://re.po", + "github_installation_id": 1234, + "type": "terraform", + }), + ExpectError: regexp.MustCompile("'terraform_version' not set"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + + t.Run("error: terragrunt with no version", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": "**", + "repository": "https://re.po", + "github_installation_id": 1234, + "type": "terragrunt", + }), + ExpectError: regexp.MustCompile("'terragrunt_version' not set"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + + t.Run("error: opentofu (with terragrunt) version not set", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": "**", + "repository": "https://re.po", + "github_installation_id": 1234, + "type": "terragrunt", + "terragrunt_version": "0.65.1", + }), + ExpectError: regexp.MustCompile("'terragrunt_tf_binary' is set to 'opentofu', but 'opentofu_version' not set"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + + t.Run("error: opentofu (with terragrunt) version not set", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": "**", + "repository": "https://re.po", + "github_installation_id": 1234, + "type": "terragrunt", + "terragrunt_version": "0.65.1", + "terragrunt_tf_binary": "terraform", + }), + ExpectError: regexp.MustCompile("'terragrunt_tf_binary' is set to 'terraform', but 'terraform_version' not set"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + + t.Run("error: no vcs set", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": "**", + "repository": "https://re.po", + "type": "workflow", + }), + ExpectError: regexp.MustCompile("must set exactly one vcs, none were configured"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + + t.Run("error: more than one vcs set", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": "**", + "repository": "https://re.po", + "type": "workflow", + "github_installation_id": 1234, + "gitlab_project_id": 5678, + "token_id": "1345", + }), + ExpectError: regexp.MustCompile("must set exactly one vcs, but more were configured: github_installation_id, gitlab_project_id"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + + t.Run("import", func(t *testing.T) { + putPayload := client.EnvironmentDiscoveryPutPayload{ + GlobPattern: "**", + Repository: "https://re.po", + Type: "opentofu", + EnvironmentPlacement: "topProject", + WorkspaceNaming: "default", + OpentofuVersion: "1.6.2", + GithubInstallationId: 12345, + } + + getPayload := client.EnvironmentDiscoveryPayload{ + Id: id, + GlobPattern: putPayload.GlobPattern, + Repository: putPayload.Repository, + Type: putPayload.Type, + EnvironmentPlacement: putPayload.EnvironmentPlacement, + WorkspaceNaming: putPayload.WorkspaceNaming, + OpentofuVersion: putPayload.OpentofuVersion, + GithubInstallationId: putPayload.GithubInstallationId, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "project_id": projectId, + "glob_pattern": putPayload.GlobPattern, + "repository": putPayload.Repository, + "opentofu_version": putPayload.OpentofuVersion, + "github_installation_id": putPayload.GithubInstallationId, + }), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: projectId, + ImportStateVerify: true, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().PutEnvironmentDiscovery(projectId, &putPayload).Times(1).Return(&getPayload, nil), + mock.EXPECT().GetEnvironmentDiscovery(projectId).Times(3).Return(&getPayload, nil), + mock.EXPECT().DeleteEnvironmentDiscovery(projectId).Times(1).Return(nil), + ) + }) + }) +} diff --git a/env0/resource_project.go b/env0/resource_project.go index 34c0d366..e648997a 100644 --- a/env0/resource_project.go +++ b/env0/resource_project.go @@ -130,7 +130,7 @@ func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, meta int return nil } -func resourceProjectAssertCanDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) error { +func resourceProjectAssertCanDelete(d *schema.ResourceData, meta interface{}) error { forceDestroy := d.Get("force_destroy").(bool) if forceDestroy { return nil @@ -179,7 +179,7 @@ func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, meta int done <- true return case <-ticker.C: - err := resourceProjectAssertCanDelete(ctx, d, meta) + err := resourceProjectAssertCanDelete(d, meta) if err != nil { if aeerr, ok := err.(*ActiveEnvironmentError); ok { @@ -198,7 +198,7 @@ func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, meta int <-done } - if err := resourceProjectAssertCanDelete(ctx, d, meta); err != nil { + if err := resourceProjectAssertCanDelete(d, meta); err != nil { return diag.Errorf("could not delete project: %v", err) } diff --git a/env0/test_helpers.go b/env0/test_helpers.go index b09a393d..d887afa8 100644 --- a/env0/test_helpers.go +++ b/env0/test_helpers.go @@ -88,7 +88,7 @@ func missingArgumentTestCase(resourceType string, resourceName string, errorReso return testCaseFormMissingValidInputError } -func missingArgumentTestCaseForCostCred(resourceType string, resourceName string, errorResource map[string]interface{}, missingArgumentKey string) resource.TestCase { +func missingArgumentTestCaseForCostCred(resourceType string, resourceName string, errorResource map[string]interface{}) resource.TestCase { testCaseFormMissingValidInputError := resource.TestCase{ Steps: []resource.TestStep{ { diff --git a/examples/resources/env0_environment_discovery_configuration/import.sh b/examples/resources/env0_environment_discovery_configuration/import.sh new file mode 100644 index 00000000..818d9833 --- /dev/null +++ b/examples/resources/env0_environment_discovery_configuration/import.sh @@ -0,0 +1 @@ +terraform import env0_environment_discovery_configuration.by_project_id 29b8037a-f877-48f5-a60b-3152ae1a1405 \ No newline at end of file diff --git a/examples/resources/env0_environment_discovery_configuration/resource.tf b/examples/resources/env0_environment_discovery_configuration/resource.tf new file mode 100644 index 00000000..015c1de8 --- /dev/null +++ b/examples/resources/env0_environment_discovery_configuration/resource.tf @@ -0,0 +1,26 @@ +data "env0_project" "project" { + name = "existing-project" +} + +resource "env0_project" "new_project" { + name = "new-project" +} + +resource "env0_environment_discovery_configuration" "example" { + project_id = data.env0_project.project.id + glob_pattern = "**" + repository = "https://github.com/env0/templates" + opentofu_version = "1.6.7" + github_installation_id = 12345 +} + +resource "env0_environment_discovery_configuration" "terragrunt_example" { + project_id = env0_project.new_project.id + glob_pattern = "**" + repository = "https://github.com/env0/blueprints" + type = "terragrunt" + terraform_version = "1.7.1" + terragrunt_version = "0.67.4" + terragrunt_tf_binary = "terraform" + github_installation_id = 12345 +} diff --git a/go.mod b/go.mod index beb85f5b..37279670 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/env0/terraform-provider-env0 -go 1.20 +go 1.21 require ( github.com/Masterminds/semver/v3 v3.2.1 @@ -80,7 +80,7 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect google.golang.org/grpc v1.60.1 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index dac706fa..9692f2a9 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -9,6 +10,7 @@ github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFP github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/adhocore/gronx v1.6.6 h1:Gk1OAP4CCSs2/i3f7HHwB2tX/EtYP3TzzWSHvesTR4k= @@ -24,31 +26,40 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git/v5 v5.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk= +github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg= github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -129,16 +140,22 @@ github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k= github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w= github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -178,19 +195,23 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.12.0 h1:p4oGGk2M2UJc0wWN4lHFvIB71lxsh0T/UiKCCgFADY8= github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= @@ -209,6 +230,7 @@ github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= @@ -310,6 +332,7 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -329,8 +352,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -338,6 +361,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/tests/integration/020_api_key/main.tf b/tests/integration/020_api_key/main.tf index 0c49a9be..ba55ef0e 100644 --- a/tests/integration/020_api_key/main.tf +++ b/tests/integration/020_api_key/main.tf @@ -41,13 +41,20 @@ resource "env0_user_project_assignment" "api_key_project_assignment" { role = var.second_run ? "Viewer" : "Planner" } +resource "time_sleep" "wait_15_seconds" { + depends_on = [env0_api_key.test_api_key] + + create_duration = "15s" +} + data "env0_api_key" "test_api_key1" { name = env0_api_key.test_api_key.name - depends_on = [env0_api_key.test_api_key] + depends_on = [time_sleep.wait_15_seconds] } data "env0_api_key" "test_api_key2" { - id = env0_api_key.test_api_key.id + id = env0_api_key.test_api_key.id + depends_on = [time_sleep.wait_15_seconds] } resource "env0_api_key" "test_api_key_omitted" { diff --git a/tests/integration/032_environment_discovery_configuration/conf.tf b/tests/integration/032_environment_discovery_configuration/conf.tf new file mode 100644 index 00000000..58dc45e8 --- /dev/null +++ b/tests/integration/032_environment_discovery_configuration/conf.tf @@ -0,0 +1,13 @@ +terraform { + backend "local" { + } + required_providers { + env0 = { + source = "terraform-registry.env0.com/env0/env0" + } + } +} + +provider "env0" {} + +variable "second_run" {} diff --git a/tests/integration/032_environment_discovery_configuration/expected_outputs.json b/tests/integration/032_environment_discovery_configuration/expected_outputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/integration/032_environment_discovery_configuration/expected_outputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/integration/032_environment_discovery_configuration/main.tf b/tests/integration/032_environment_discovery_configuration/main.tf new file mode 100644 index 00000000..90631b50 --- /dev/null +++ b/tests/integration/032_environment_discovery_configuration/main.tf @@ -0,0 +1,30 @@ +provider "random" {} + +resource "random_string" "random" { + length = 8 + special = false + min_lower = 8 +} + +resource "env0_project" "project" { + name = "project-${random_string.random.result}" +} + +data "env0_template" "github_template" { + name = "Github Integrated Template" +} + +resource "env0_template_project_assignment" "assignment" { + template_id = data.env0_template.github_template.id + project_id = env0_project.project.id +} + +resource "env0_environment_discovery_configuration" "example" { + project_id = env0_project.project.id + glob_pattern = var.second_run ? "**" : "**/**" + opentofu_version = "1.6.2" + repository = data.env0_template.github_template.repository + github_installation_id = data.env0_template.github_template.github_installation_id + + depends_on = [env0_template_project_assignment.assignment] +}